Skip to content
Snippets Groups Projects
Verified Commit f8556c99 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Drop old models and files

parent 5080ab98
No related branches found
No related tags found
1 merge request!362Migration path
Showing
with 16 additions and 3467 deletions
# noqa
from django.contrib import admin
from django.utils.html import format_html
from guardian.admin import GuardedModelAdmin
from aleksis.core.models import Room
from .models import (
Absence,
AbsenceReason,
AutomaticPlan,
Break,
Event,
ExtraLesson,
Holiday,
Lesson,
LessonPeriod,
LessonSubstitution,
Subject,
Supervision,
SupervisionArea,
SupervisionSubstitution,
TimePeriod,
ValidityRange,
)
from .util.format import format_date_period, format_m2m
def colour_badge(fg: str, bg: str, val: str):
html = """
<div style="
color: {};
background-color: {};
padding-top: 3px;
padding-bottom: 4px;
text-align: center;
border-radius: 3px;
">{}</span>
"""
return format_html(html, fg, bg, val)
class AbsenceReasonAdmin(admin.ModelAdmin):
list_display = ("short_name", "name")
list_display_links = ("short_name", "name")
admin.site.register(AbsenceReason, AbsenceReasonAdmin)
class AbsenceAdmin(admin.ModelAdmin):
def start(self, obj):
return format_date_period(obj.date_start, obj.period_from)
def end(self, obj):
return format_date_period(obj.date_end, obj.period_to)
list_display = ("__str__", "reason", "start", "end")
admin.site.register(Absence, AbsenceAdmin)
class SupervisionInline(admin.TabularInline):
model = Supervision
class BreakAdmin(admin.ModelAdmin):
list_display = ("__str__", "after_period", "before_period")
inlines = [SupervisionInline]
admin.site.register(Break, BreakAdmin)
class EventAdmin(admin.ModelAdmin):
def start(self, obj):
return format_date_period(obj.date_start, obj.period_from)
def end(self, obj):
return format_date_period(obj.date_end, obj.period_to)
def _groups(self, obj):
return format_m2m(obj.groups)
def _teachers(self, obj):
return format_m2m(obj.teachers)
def _rooms(self, obj):
return format_m2m(obj.rooms)
filter_horizontal = ("groups", "teachers", "rooms")
list_display = ("__str__", "_groups", "_teachers", "_rooms", "start", "end")
admin.site.register(Event, EventAdmin)
class ExtraLessonAdmin(admin.ModelAdmin):
def _groups(self, obj):
return format_m2m(obj.groups)
def _teachers(self, obj):
return format_m2m(obj.teachers)
list_display = ("week", "period", "subject", "_groups", "_teachers", "room")
admin.site.register(ExtraLesson, ExtraLessonAdmin)
class HolidayAdmin(admin.ModelAdmin):
list_display = ("title", "date_start", "date_end")
admin.site.register(Holiday, HolidayAdmin)
class LessonPeriodInline(admin.TabularInline):
model = LessonPeriod
class LessonSubstitutionAdmin(admin.ModelAdmin):
list_display = ("lesson_period", "week", "date")
list_display_links = ("lesson_period", "week", "date")
filter_horizontal = ("teachers",)
admin.site.register(LessonSubstitution, LessonSubstitutionAdmin)
class LessonAdmin(admin.ModelAdmin):
def _groups(self, obj):
return format_m2m(obj.groups)
def _teachers(self, obj):
return format_m2m(obj.teachers)
filter_horizontal = ["teachers", "groups"]
inlines = [LessonPeriodInline]
list_filter = ("subject", "groups", "groups__parent_groups", "teachers")
list_display = ("_groups", "subject", "_teachers")
admin.site.register(Lesson, LessonAdmin)
class RoomAdmin(GuardedModelAdmin):
list_display = ("short_name", "name")
list_display_links = ("short_name", "name")
admin.site.register(Room, RoomAdmin)
class SubjectAdmin(admin.ModelAdmin):
def _colour(self, obj):
return colour_badge(
obj.colour_fg,
obj.colour_bg,
obj.short_name,
)
list_display = ("short_name", "name", "_colour")
list_display_links = ("short_name", "name")
admin.site.register(Subject, SubjectAdmin)
class SupervisionAreaAdmin(admin.ModelAdmin):
def _colour(self, obj):
return colour_badge(
obj.colour_fg,
obj.colour_bg,
obj.short_name,
)
list_display = ("short_name", "name", "_colour")
list_display_links = ("short_name", "name")
inlines = [SupervisionInline]
admin.site.register(SupervisionArea, SupervisionAreaAdmin)
class SupervisionSubstitutionAdmin(admin.ModelAdmin):
list_display = ("supervision", "date")
admin.site.register(SupervisionSubstitution, SupervisionSubstitutionAdmin)
class SupervisionAdmin(admin.ModelAdmin):
list_display = ("break_item", "area", "teacher")
admin.site.register(Supervision, SupervisionAdmin)
class TimePeriodAdmin(admin.ModelAdmin):
list_display = ("weekday", "period", "time_start", "time_end")
list_display_links = ("weekday", "period")
admin.site.register(TimePeriod, TimePeriodAdmin)
class ValidityRangeAdmin(admin.ModelAdmin):
list_display = ("__str__", "date_start", "date_end")
list_display_links = ("__str__", "date_start", "date_end")
admin.site.register(ValidityRange, ValidityRangeAdmin)
admin.site.register(AutomaticPlan)
from typing import Any, Optional
import django.apps
from django.db import transaction
from reversion.signals import post_revision_commit
......@@ -37,75 +34,3 @@ class ChronosConfig(AppConfig):
transaction.on_commit(lambda: handle_new_revision.delay(revision.pk))
post_revision_commit.connect(_handle_post_revision_commit, weak=False)
def _ensure_notification_task(self):
"""Update or create the task for sending notifications."""
from django.conf import settings # noqa
from celery import schedules
from django_celery_beat.models import CrontabSchedule, PeriodicTask
from aleksis.core.util.core_helpers import get_site_preferences
time_for_sending = get_site_preferences()["chronos__time_for_sending_notifications"]
active = get_site_preferences()["chronos__send_notifications_site"]
if active:
schedule = schedules.crontab(
minute=str(time_for_sending.minute), hour=str(time_for_sending.hour)
)
schedule = CrontabSchedule.from_schedule(schedule)
schedule.timezone = settings.TIME_ZONE
schedule.save()
possible_periodic_tasks = PeriodicTask.objects.filter(
task="chronos_send_notifications_for_next_day"
)
if not active:
possible_periodic_tasks.delete()
elif possible_periodic_tasks.exists():
task = possible_periodic_tasks[0]
for d_task in possible_periodic_tasks:
if d_task != task:
d_task.delete()
if task.crontab != schedule:
task.interval, task.solar, task.clocked = None, None, None
task.crontab = schedule
task.save()
elif active:
PeriodicTask.objects.get_or_create(
task="chronos_send_notifications_for_next_day",
crontab=schedule,
defaults=dict(name="Send notifications for next day (automatic schedule)"),
)
def preference_updated(
self,
sender: Any,
section: Optional[str] = None,
name: Optional[str] = None,
old_value: Optional[Any] = None,
new_value: Optional[Any] = None,
**kwargs,
) -> None:
if section == "chronos" and name in (
"send_notifications_site",
"time_for_sending_notifications",
):
self._ensure_notification_task()
def post_migrate(
self,
app_config: django.apps.AppConfig,
verbosity: int,
interactive: bool,
using: str,
**kwargs,
) -> None:
super().post_migrate(app_config, verbosity, interactive, using, **kwargs)
# Ensure that the notification task is created after setting up AlekSIS
self._ensure_notification_task()
This diff is collapsed.
from datetime import date
from typing import Union
from django.db import models
from django.utils.translation import gettext as _
from calendarweek import CalendarWeek
from aleksis.apps.chronos.util.date import week_weekday_to_date
from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations
from aleksis.core.mixins import ExtensibleModel
from .managers import ValidityRangeRelatedQuerySet
class ValidityRangeRelatedExtensibleModel(ExtensibleModel):
"""Add relation to validity range."""
objects = AlekSISBaseManagerWithoutMigrations.from_queryset(ValidityRangeRelatedQuerySet)()
validity = models.ForeignKey(
"chronos.ValidityRange",
on_delete=models.CASCADE,
related_name="+",
verbose_name=_("Linked validity range"),
null=True,
blank=True,
)
class Meta:
abstract = True
class WeekRelatedMixin:
@property
def date(self) -> date:
period = self.lesson_period.period if hasattr(self, "lesson_period") else self.period
return week_weekday_to_date(self.calendar_week, period.weekday)
@property
def calendar_week(self) -> CalendarWeek:
return CalendarWeek(week=self.week, year=self.year)
class WeekAnnotationMixin:
def annotate_week(self, week: CalendarWeek):
"""Annotate this lesson with the number of the provided calendar week."""
self._week = week.week
self._year = week.year
@property
def week(self) -> Union[CalendarWeek, None]:
"""Get annotated week as `CalendarWeek`.
Defaults to `None` if no week is annotated.
"""
if hasattr(self, "_week"):
return CalendarWeek(week=self._week, year=self._year)
else:
return None
from datetime import date
from typing import Optional, Union
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from reversion.models import Revision
from aleksis.core.models import Announcement, Group, Person
from aleksis.core.util.core_helpers import get_site_preferences
from .managers import TimetableType
from .models import Lesson, LessonPeriod
from .util.change_tracker import timetable_data_changed
from .util.notifications import send_notifications_for_object
@Person.property_
def is_teacher(self):
"""Check if the user has lessons as a teacher."""
return self.lesson_periods_as_teacher.exists()
@Person.property_
def timetable_type(self) -> Optional[TimetableType]:
"""Return which type of timetable this user has."""
if self.is_teacher:
return TimetableType.TEACHER
elif self.primary_group:
return TimetableType.GROUP
else:
return None
@Person.property_
def timetable_object(self) -> Optional[Union[Group, Person]]:
"""Return the object which has the user's timetable."""
type_ = self.timetable_type
if type_ == TimetableType.TEACHER:
return self
elif type_ == TimetableType.GROUP:
return self.primary_group
else:
return None
@Person.property_
def lessons_as_participant(self):
"""Return a `QuerySet` containing all `Lesson`s this person participates in (as student).
.. note:: Only available when AlekSIS-App-Chronos is installed.
:Date: 2019-11-07
:Authors:
- Dominik George <dominik.george@teckids.org>
"""
return Lesson.objects.filter(groups__members=self)
@Person.property_
def lesson_periods_as_participant(self):
"""Return a `QuerySet` containing all `LessonPeriod`s this person participates in (as student).
.. note:: Only available when AlekSIS-App-Chronos is installed.
:Date: 2019-11-07
:Authors:
- Dominik George <dominik.george@teckids.org>
"""
return LessonPeriod.objects.filter(lesson__groups__members=self)
@Person.property_
def lesson_periods_as_teacher(self):
"""Return a `QuerySet` containing all `Lesson`s this person gives (as teacher).
.. note:: Only available when AlekSIS-App-Chronos is installed.
:Date: 2019-11-07
:Authors:
- Dominik George <dominik.george@teckids.org>
"""
return LessonPeriod.objects.filter(lesson__teachers=self)
@Person.method
def lessons_on_day(self, day: date):
"""Get all lessons of this person (either as participant or teacher) on the given day."""
qs = LessonPeriod.objects.on_day(day).filter_from_person(self)
if qs:
# This is a union queryset, so order by must be after the union.
return qs.order_by("period__period")
return None
@Person.method
def _adjacent_lesson(
self, lesson_period: "LessonPeriod", day: date, offset: int = 1
) -> Union["LessonPeriod", None]:
"""Get next/previous lesson of the person (either as participant or teacher) on the same day."""
daily_lessons = self.lessons_on_day(day)
if not daily_lessons:
return None
ids = list(daily_lessons.values_list("id", flat=True))
# Check if the lesson period is one of the person's lesson periods on this day
# and return None if it's not so
if lesson_period.pk not in ids:
return None
index = ids.index(lesson_period.pk)
if (offset > 0 and index + offset < len(ids)) or (offset < 0 and index >= -offset):
return daily_lessons[index + offset]
else:
return None
@Person.method
def next_lesson(self, lesson_period: "LessonPeriod", day: date) -> Union["LessonPeriod", None]:
"""Get next lesson of the person (either as participant or teacher) on the same day."""
return self._adjacent_lesson(lesson_period, day)
@Person.method
def previous_lesson(self, lesson_period: "LessonPeriod", day: date) -> Union["LessonPeriod", None]:
"""Get previous lesson of the person (either as participant or teacher) on the same day."""
return self._adjacent_lesson(lesson_period, day, offset=-1)
def for_timetables(cls):
"""Return all announcements that should be shown in timetable views."""
return cls.objects.all()
Announcement.class_method(for_timetables)
from aleksis.core.models import Group, Person
# Dynamically add extra permissions to Group and Person models in core
# Note: requires migrate afterwards
......@@ -148,16 +12,3 @@ Person.add_permission(
"view_person_timetable",
_("Can view person timetable"),
)
@receiver(timetable_data_changed)
def send_notifications(sender: Revision, **kwargs):
"""Send notifications to users about the changes."""
if not get_site_preferences()["chronos__send_notifications_site"]:
return
for change in sender.changes.values():
if change.deleted:
continue
send_notifications_for_object(change.instance)
This diff is collapsed.
......@@ -16,7 +16,7 @@ from dynamic_preferences.types import (
)
from aleksis.core.models import GroupType
from aleksis.core.registries import person_preferences_registry, site_preferences_registry
from aleksis.core.registries import site_preferences_registry
chronos = Section("chronos", verbose_name=_("Timetables"))
......@@ -34,27 +34,6 @@ class UseParentGroups(BooleanPreference):
)
@person_preferences_registry.register
class ShortenGroups(BooleanPreference):
section = chronos
name = "shorten_groups"
default = True
verbose_name = _("Shorten groups in timetable views")
help_text = _("If there are more groups than the set limit, they will be collapsed.")
@site_preferences_registry.register
class ShortenGroupsLimit(IntegerPreference):
section = chronos
name = "shorten_groups_limit"
default = 4
verbose_name = _("Limit of groups for shortening of groups")
help_text = _(
"If a user activates shortening of groups,"
"they will be collapsed if there are more groups than this limit."
)
@site_preferences_registry.register
class SubstitutionsRelevantDays(MultipleChoicePreference):
"""Relevant days which have substitution plans."""
......@@ -110,44 +89,6 @@ class AffectedGroupsUseParentGroups(BooleanPreference):
)
@site_preferences_registry.register
class DaysInAdvanceNotifications(IntegerPreference):
section = chronos
name = "days_in_advance_notifications"
default = 1
verbose_name = _("How many days in advance users should be notified about timetable changes?")
@site_preferences_registry.register
class TimeForSendingNotifications(TimePreference):
section = chronos
name = "time_for_sending_notifications"
default = time(17, 00)
verbose_name = _("Time for sending notifications about timetable changes")
required = True
help_text = _(
"This is only used for scheduling notifications "
"which doesn't affect the time period configured above. "
"All other notifications affecting the next days are sent immediately."
)
@site_preferences_registry.register
class SendNotifications(BooleanPreference):
section = chronos
name = "send_notifications_site"
default = True
verbose_name = _("Send notifications for current timetable changes")
@person_preferences_registry.register
class SendNotificationsPerson(BooleanPreference):
section = chronos
name = "send_notifications"
default = True
verbose_name = _("Send notifications for current timetable changes")
@site_preferences_registry.register
class GroupTypesTimetables(ModelMultipleChoicePreference):
section = chronos
......
......@@ -6,7 +6,7 @@ from aleksis.core.util.predicates import (
has_person,
)
from .util.predicates import has_any_timetable_object, has_room_timetable_perm, has_timetable_perm
from .util.predicates import has_any_timetable_object, has_timetable_perm
# View timetable overview
view_timetable_overview_predicate = has_person & (
......@@ -14,55 +14,27 @@ view_timetable_overview_predicate = has_person & (
)
add_perm("chronos.view_timetable_overview_rule", view_timetable_overview_predicate)
# View my timetable
add_perm("chronos.view_my_timetable_rule", has_person)
# View timetable
view_timetable_predicate = has_person & has_timetable_perm
add_perm("chronos.view_timetable_rule", view_timetable_predicate)
# View all lessons per day
view_lessons_day_predicate = has_person & has_global_perm("chronos.view_lessons_day")
add_perm("chronos.view_lessons_day_rule", view_lessons_day_predicate)
# Edit substition
edit_substitution_predicate = has_person & (
has_global_perm("chronos.change_lessonsubstitution")
| has_object_perm("chronos.change_lessonsubstitution")
has_global_perm("chronos.change_lessonevent") | has_object_perm("chronos.change_lessonevent")
)
add_perm("chronos.edit_substitution_rule", edit_substitution_predicate)
# Delete substitution
delete_substitution_predicate = has_person & (
has_global_perm("chronos.delete_lessonsubstitution")
| has_object_perm("chronos.delete_lessonsubstitution")
has_global_perm("chronos.delete_lessonevent") | has_object_perm("chronos.delete_lessonevent")
)
add_perm("chronos.delete_substitution_rule", delete_substitution_predicate)
# View substitutions
view_substitutions_predicate = has_person & (has_global_perm("chronos.view_lessonsubstitution"))
view_substitutions_predicate = has_person & (has_global_perm("chronos.view_substitutions"))
add_perm("chronos.view_substitutions_rule", view_substitutions_predicate)
# View all supervisions per day
view_supervisions_day_predicate = has_person & has_global_perm("chronos.view_supervisions_day")
add_perm("chronos.view_supervisions_day_rule", view_supervisions_day_predicate)
# Edit supervision substitution
edit_supervision_substitution_predicate = has_person & (
has_global_perm("chronos.change_supervisionsubstitution")
)
add_perm("chronos.edit_supervision_substitution_rule", edit_supervision_substitution_predicate)
# Delete supervision substitution
delete_supervision_substitution_predicate = has_person & (
has_global_perm("chronos.delete_supervisionsubstitution")
)
add_perm("chronos.delete_supervision_substitution_rule", delete_supervision_substitution_predicate)
# View room (timetable)
view_room_predicate = has_person & has_room_timetable_perm
add_perm("chronos.view_room_rule", view_room_predicate)
# View parent menu entry
view_menu_predicate = has_person & (view_timetable_overview_predicate)
add_perm("chronos.view_menu_rule", view_menu_predicate)
.timetable-plan .row,
.timetable-plan .col {
display: flex;
padding: 0rem;
}
.timetable-plan .row {
margin-bottom: 0rem;
}
.lesson-card,
.timetable-title-card {
margin: 0;
display: flex;
flex-grow: 1;
min-height: 40px;
box-shadow: none;
border: 1px solid black;
margin-right: -1px;
margin-top: -1px;
border-radius: 0px;
font-size: 11px;
}
.lesson-card .card-content > div {
padding: 1px;
}
.card .card-title {
font-size: 18px;
}
.timetable-title-card .card-content {
padding: 7px;
}
var data = getJSONScript("datepicker_data");
var activeDate = new Date(data.date);
function updateDatepicker() {
$("#date").val(formatDate(activeDate));
}
function loadNew() {
window.location.href = data.dest + formatDateForDjango(activeDate);
}
function onDateChanged() {
activeDate = M.Datepicker.getInstance($("#date")).date;
loadNew();
}
$(document).ready(function () {
$("#date").change(onDateChanged);
updateDatepicker();
});
var data = getJSONScript("week_select");
function goToCalendarWeek(cw, year) {
window.location.href = data.dest.replace("year", year).replace("cw", cw);
}
function onCalendarWeekChanged(where) {
goToCalendarWeek($(where).val(), data.year);
}
$(document).ready(function () {
$("#calendar-week-1").change(function () {
onCalendarWeekChanged("#calendar-week-1");
});
$("#calendar-week-2").change(function () {
onCalendarWeekChanged("#calendar-week-2");
});
$("#calendar-week-3").change(function () {
onCalendarWeekChanged("#calendar-week-3");
});
});
from django import template
register = template.Library()
class SetVarNode(template.Node):
def __init__(self, var_name, var_value):
self.var_name = var_name
self.var_value = var_value
def render(self, context):
try:
value = template.Variable(self.var_value).resolve(context)
except template.VariableDoesNotExist:
value = ""
context[self.var_name] = value
return ""
@register.tag(name="set")
def set_var(parser, token):
"""Set var.
{% set some_var = '123' %}
"""
parts = token.split_contents()
if len(parts) < 4:
raise template.TemplateSyntaxError(
"'set' tag must be of the form: {% set <var_name> = <var_value> %}"
)
return SetVarNode(parts[1], parts[3])
from datetime import date, datetime
from typing import Optional, Union
from django import template
from django.db.models.query import QuerySet
from aleksis.apps.chronos.util.date import CalendarWeek, week_period_to_date, week_weekday_to_date
register = template.Library()
@register.filter
def week_start(week: CalendarWeek) -> date:
return week[0]
@register.filter
def week_end(week: CalendarWeek) -> date:
return week[-1]
@register.filter
def only_week(qs: QuerySet, week: Optional[CalendarWeek]) -> QuerySet:
wanted_week = week or CalendarWeek()
return qs.filter(week=wanted_week.week, year=wanted_week.year)
@register.simple_tag
def weekday_to_date(week: CalendarWeek, weekday: int) -> date:
return week_weekday_to_date(week, weekday)
@register.simple_tag
def period_to_date(week: CalendarWeek, period) -> date:
return week_period_to_date(week, period)
@register.simple_tag
def period_to_time_start(date_ref: Union[CalendarWeek, int, date], period) -> date:
return period.get_datetime_start(date_ref)
@register.simple_tag
def period_to_time_end(date_ref: Union[CalendarWeek, int, date], period) -> date:
return period.get_datetime_end(date_ref)
@register.simple_tag
def today() -> date:
return date.today()
@register.simple_tag
def now_datetime() -> datetime:
return datetime.now()
from collections import OrderedDict
from datetime import date, datetime, time
from typing import Union
from django.apps import apps
from calendarweek import CalendarWeek
from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import SupervisionEvent
from aleksis.core.models import Group, Person, Room
LessonPeriod = apps.get_model("chronos", "LessonPeriod")
LessonEvent = apps.get_model("chronos", "LessonEvent")
TimePeriod = apps.get_model("chronos", "TimePeriod")
Break = apps.get_model("chronos", "Break")
Supervision = apps.get_model("chronos", "Supervision")
LessonSubstitution = apps.get_model("chronos", "LessonSubstitution")
SupervisionSubstitution = apps.get_model("chronos", "SupervisionSubstitution")
Event = apps.get_model("chronos", "Event")
Holiday = apps.get_model("chronos", "Holiday")
ExtraLesson = apps.get_model("chronos", "ExtraLesson")
def build_timetable(
type_: Union[TimetableType, str],
obj: Union[Group, Room, Person],
date_ref: Union[CalendarWeek, date],
with_holidays: bool = True,
):
needed_breaks = []
is_person = False
if type_ == "person":
is_person = True
type_ = obj.timetable_type
is_week = False
if isinstance(date_ref, CalendarWeek):
is_week = True
if type_ is None:
return None
# Get matching holidays
if is_week:
holidays_per_weekday = Holiday.in_week(date_ref) if with_holidays else {}
else:
holiday = Holiday.on_day(date_ref) if with_holidays else None
# Get matching lesson periods
lesson_periods = LessonPeriod.objects
lesson_periods = (
lesson_periods.select_related(None)
.select_related("lesson", "lesson__subject", "period", "room")
.only(
"lesson",
"period",
"room",
"lesson__subject",
"period__weekday",
"period__period",
"lesson__subject__short_name",
"lesson__subject__name",
"lesson__subject__colour_fg",
"lesson__subject__colour_bg",
"room__short_name",
"room__name",
)
)
if is_week:
lesson_periods = lesson_periods.in_week(date_ref)
else:
lesson_periods = lesson_periods.on_day(date_ref)
if is_person:
lesson_periods = lesson_periods.filter_from_person(obj)
else:
lesson_periods = lesson_periods.filter_from_type(type_, obj, is_smart=with_holidays)
# Sort lesson periods in a dict
lesson_periods_per_period = lesson_periods.group_by_periods(is_week=is_week)
# Get events
extra_lessons = ExtraLesson.objects
if is_week:
extra_lessons = extra_lessons.filter(week=date_ref.week, year=date_ref.year)
else:
extra_lessons = extra_lessons.on_day(date_ref)
if is_person:
extra_lessons = extra_lessons.filter_from_person(obj)
else:
extra_lessons = extra_lessons.filter_from_type(type_, obj)
extra_lessons = extra_lessons.only(
"week",
"year",
"period",
"subject",
"room",
"comment",
"period__weekday",
"period__period",
"subject__short_name",
"subject__name",
"subject__colour_fg",
"subject__colour_bg",
"room__short_name",
"room__name",
)
# Sort lesson periods in a dict
extra_lessons_per_period = extra_lessons.group_by_periods(is_week=is_week)
# Get events
events = Event.objects
events = events.in_week(date_ref) if is_week else events.on_day(date_ref)
events = events.only(
"id",
"title",
"date_start",
"date_end",
"period_from",
"period_to",
"period_from__weekday",
"period_from__period",
"period_to__weekday",
"period_to__period",
)
if is_person:
events_to_display = events.filter_from_person(obj)
else:
events_to_display = events.filter_from_type(type_, obj)
# Sort events in a dict
events_per_period = {}
events_for_replacement_per_period = {}
for event in events:
if is_week and event.date_start < date_ref[TimePeriod.weekday_min]:
# If start date not in current week, set weekday and period to min
weekday_from = TimePeriod.weekday_min
period_from_first_weekday = TimePeriod.period_min
else:
weekday_from = event.date_start.weekday()
period_from_first_weekday = event.period_from.period
if is_week and event.date_end > date_ref[TimePeriod.weekday_max]:
# If end date not in current week, set weekday and period to max
weekday_to = TimePeriod.weekday_max
period_to_last_weekday = TimePeriod.period_max
else:
weekday_to = event.date_end.weekday()
period_to_last_weekday = event.period_to.period
for weekday in range(weekday_from, weekday_to + 1):
if not is_week and weekday != date_ref.weekday():
# If daily timetable for person, skip other weekdays
continue
# If start day, use start period else use min period
period_from = (
period_from_first_weekday if weekday == weekday_from else TimePeriod.period_min
)
# If end day, use end period else use max period
period_to = period_to_last_weekday if weekday == weekday_to else TimePeriod.periox_max
for period in range(period_from, period_to + 1):
# The following events are possibly replacing some lesson periods
if period not in events_for_replacement_per_period:
events_for_replacement_per_period[period] = {} if is_week else []
if is_week and weekday not in events_for_replacement_per_period[period]:
events_for_replacement_per_period[period][weekday] = []
if not is_week:
events_for_replacement_per_period[period].append(event)
else:
events_for_replacement_per_period[period][weekday].append(event)
# and the following will be displayed in the timetable
if event in events_to_display:
if period not in events_per_period:
events_per_period[period] = {} if is_week else []
if is_week and weekday not in events_per_period[period]:
events_per_period[period][weekday] = []
if not is_week:
events_per_period[period].append(event)
else:
events_per_period[period][weekday].append(event)
if type_ == TimetableType.TEACHER:
# Get matching supervisions
week = CalendarWeek.from_date(date_ref) if not is_week else date_ref
supervisions = (
Supervision.objects.in_week(week)
.all()
.annotate_week(week)
.filter_by_teacher(obj)
.only(
"area",
"break_item",
"teacher",
"area",
"area__short_name",
"area__name",
"area__colour_fg",
"area__colour_bg",
"break_item__short_name",
"break_item__name",
"break_item__after_period__period",
"break_item__after_period__weekday",
"break_item__before_period__period",
"break_item__before_period__weekday",
"teacher__short_name",
"teacher__first_name",
"teacher__last_name",
)
)
if not is_week:
supervisions = supervisions.filter_by_weekday(date_ref.weekday())
supervisions_per_period_after = {}
for supervision in supervisions:
weekday = supervision.break_item.weekday
period_after_break = supervision.break_item.before_period_number
if period_after_break not in needed_breaks:
needed_breaks.append(period_after_break)
if is_week and period_after_break not in supervisions_per_period_after:
supervisions_per_period_after[period_after_break] = {}
if not is_week:
supervisions_per_period_after[period_after_break] = supervision
else:
supervisions_per_period_after[period_after_break][weekday] = supervision
# Get ordered breaks
breaks = OrderedDict(sorted(Break.get_breaks_dict().items()))
rows = []
for period, break_ in breaks.items(): # period is period after break
# Break
if type_ == TimetableType.TEACHER and period in needed_breaks:
row = {
"type": "break",
"after_period": break_.after_period_number,
"before_period": break_.before_period_number,
"time_start": break_.time_start,
"time_end": break_.time_end,
}
if is_week:
cols = []
for weekday in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
col = None
if (
period in supervisions_per_period_after
and weekday not in holidays_per_weekday
) and weekday in supervisions_per_period_after[period]:
col = supervisions_per_period_after[period][weekday]
cols.append(col)
row["cols"] = cols
else:
col = None
if period in supervisions_per_period_after and not holiday:
col = supervisions_per_period_after[period]
row["col"] = col
rows.append(row)
# Period
if period <= TimePeriod.period_max:
row = {
"type": "period",
"period": period,
"time_start": break_.before_period.time_start,
"time_end": break_.before_period.time_end,
}
if is_week:
cols = []
for weekday in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
# Skip this period if there are holidays
if weekday in holidays_per_weekday:
cols.append([])
continue
col = []
events_for_this_period = (
events_per_period[period].get(weekday, [])
if period in events_per_period
else []
)
events_for_replacement_for_this_period = (
events_for_replacement_per_period[period].get(weekday, [])
if period in events_for_replacement_per_period
else []
)
lesson_periods_for_this_period = (
lesson_periods_per_period[period].get(weekday, [])
if period in lesson_periods_per_period
else []
)
# Add lesson periods
if lesson_periods_for_this_period:
if events_for_replacement_for_this_period:
# If there is a event in this period,
# we have to check whether the actual lesson is taking place.
for lesson_period in lesson_periods_for_this_period:
replaced_by_event = lesson_period.is_replaced_by_event(
events_for_replacement_for_this_period,
[obj] if type_ == TimetableType.GROUP else None,
)
lesson_period.replaced_by_event = replaced_by_event
if not replaced_by_event or (
replaced_by_event and type_ != TimetableType.GROUP
):
col.append(lesson_period)
else:
col += lesson_periods_for_this_period
# Add extra lessons
if period in extra_lessons_per_period:
col += extra_lessons_per_period[period].get(weekday, [])
# Add events
col += events_for_this_period
cols.append(col)
row["cols"] = cols
else:
col = []
# Skip this period if there are holidays
if holiday:
continue
events_for_this_period = events_per_period.get(period, [])
events_for_replacement_for_this_period = events_for_replacement_per_period.get(
period, []
)
lesson_periods_for_this_period = lesson_periods_per_period.get(period, [])
# Add lesson periods
if lesson_periods_for_this_period:
if events_for_replacement_for_this_period:
# If there is a event in this period,
# we have to check whether the actual lesson is taking place.
lesson_periods_to_keep = []
for lesson_period in lesson_periods_for_this_period:
if not lesson_period.is_replaced_by_event(
events_for_replacement_for_this_period
):
lesson_periods_to_keep.append(lesson_period)
col += lesson_periods_to_keep
else:
col += lesson_periods_for_this_period
# Add events and extra lessons
col += extra_lessons_per_period.get(period, [])
col += events_for_this_period
row["col"] = col
rows.append(row)
return rows
from aleksis.apps.chronos.models import LessonEvent, SupervisionEvent
from aleksis.core.models import Group, Person
def build_substitutions_list(wanted_day: date) -> tuple[list[dict], set[Person], set[Group]]:
......@@ -433,23 +55,3 @@ def build_substitutions_list(wanted_day: date) -> tuple[list[dict], set[Person],
rows.sort(key=lambda row: row["sort_a"] + row["sort_b"])
return rows, affected_teachers, affected_groups
def build_weekdays(
base: list[tuple[int, str]], wanted_week: CalendarWeek, with_holidays: bool = True
) -> list[dict]:
if with_holidays:
holidays_per_weekday = Holiday.in_week(wanted_week)
weekdays = []
for key, name in base[TimePeriod.weekday_min : TimePeriod.weekday_max + 1]:
weekday = {
"key": key,
"name": name,
"date": wanted_week[key],
}
if with_holidays:
weekday["holiday"] = holidays_per_weekday[key] if key in holidays_per_weekday else None
weekdays.append(weekday)
return weekdays
......@@ -2,8 +2,6 @@ from datetime import date, datetime, timedelta
from typing import TYPE_CHECKING, Optional
from django.db.models import Count, Q
from django.http import HttpRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404
from guardian.core import ObjectPermissionChecker
......@@ -11,13 +9,6 @@ from aleksis.core.models import Announcement, Group, Person, Room
from aleksis.core.util.core_helpers import get_site_preferences
from aleksis.core.util.predicates import check_global_permission
from ..managers import TimetableType
from ..models import (
LessonPeriod,
LessonSubstitution,
Supervision,
SupervisionSubstitution,
)
from .build import build_substitutions_list
if TYPE_CHECKING:
......@@ -26,42 +17,6 @@ if TYPE_CHECKING:
User = get_user_model() # noqa
def get_el_by_pk(
request: HttpRequest,
type_: str,
pk: int,
prefetch: bool = False,
*args,
**kwargs,
):
if type_ == TimetableType.GROUP.value:
return get_object_or_404(
Group.objects.prefetch_related("owners", "parent_groups") if prefetch else Group,
pk=pk,
)
elif type_ == TimetableType.TEACHER.value:
return get_object_or_404(Person, pk=pk)
elif type_ == TimetableType.ROOM.value:
return get_object_or_404(Room, pk=pk)
else:
return HttpResponseNotFound()
def get_substitution_by_id(request: HttpRequest, id_: int, week: int):
lesson_period = get_object_or_404(LessonPeriod, pk=id_)
wanted_week = lesson_period.lesson.get_calendar_week(week)
return LessonSubstitution.objects.filter(
week=wanted_week.week, year=wanted_week.year, lesson_period=lesson_period
).first()
def get_supervision_substitution_by_id(request: HttpRequest, id_: int, date: datetime.date):
supervision = get_object_or_404(Supervision, pk=id_)
return SupervisionSubstitution.objects.filter(date=date, supervision=supervision).first()
def get_teachers(user: "User"):
"""Get the teachers whose timetables are allowed to be seen by current user."""
checker = ObjectPermissionChecker(user)
......
from datetime import date
from django.utils import timezone
from calendarweek import CalendarWeek
def week_weekday_from_date(when: date) -> tuple[CalendarWeek, int]:
"""Return a tuple of week and weekday from a given date."""
return (CalendarWeek.from_date(when), when.weekday())
def week_weekday_to_date(week: CalendarWeek, weekday: int) -> date:
"""Return a date object for one day in a calendar week."""
return week[weekday]
def week_period_to_date(week: CalendarWeek, period) -> date:
"""Return the date of a lesson period in a given week."""
return period.get_date(week)
def get_weeks_for_year(year: int) -> list[CalendarWeek]:
"""Generate all weeks for one year."""
weeks = []
# Go for all weeks in year and create week list
current_week = CalendarWeek(year=year, week=1)
while current_week.year == year:
weeks.append(current_week)
current_week += 1
return weeks
def get_current_year() -> int:
"""Get current year."""
return timezone.now().year
from datetime import date
from typing import TYPE_CHECKING
from django.utils.formats import date_format
if TYPE_CHECKING:
from ..models import TimePeriod
def format_m2m(f, attr: str = "short_name") -> str:
"""Join a attribute of all elements of a ManyToManyField."""
return ", ".join([getattr(x, attr) for x in f.all()])
def format_date_period(day: date, period: "TimePeriod") -> str:
"""Format date and time period."""
return f"{date_format(day)}, {period.period}."
from datetime import date, datetime, time
def date_unix(value: date) -> int:
"""Convert a date object to an UNIX timestamp."""
value = datetime.combine(value, time(hour=0, minute=0))
return int(value.timestamp()) * 1000
from datetime import datetime, timedelta
from typing import Union
from urllib.parse import urljoin
from django.conf import settings
from django.urls import reverse
from django.utils import timezone
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
import zoneinfo
from aleksis.core.models import Notification, Person
from aleksis.core.util.core_helpers import get_site_preferences
from ..models import Event, ExtraLesson, LessonSubstitution, SupervisionSubstitution
def send_notifications_for_object(
instance: Union[ExtraLesson, LessonSubstitution, Event, SupervisionSubstitution],
):
"""Send notifications for a change object."""
recipients = []
if isinstance(instance, LessonSubstitution):
recipients += instance.lesson_period.lesson.teachers.all()
recipients += instance.teachers.all()
recipients += Person.objects.filter(
member_of__in=instance.lesson_period.lesson.groups.all()
)
elif isinstance(instance, (Event, ExtraLesson)):
recipients += instance.teachers.all()
recipients += Person.objects.filter(member_of__in=instance.groups.all())
elif isinstance(instance, SupervisionSubstitution):
recipients.append(instance.teacher)
recipients.append(instance.supervision.teacher)
description = ""
if isinstance(instance, LessonSubstitution):
# Date, lesson, subject
subject = instance.lesson_period.lesson.subject
day = instance.date
period = instance.lesson_period.period
if instance.cancelled:
description += (
_(
"The {subject} lesson in the {period}. period on {day} has been cancelled."
).format(subject=subject.name, period=period.period, day=date_format(day))
+ " "
)
else:
description += (
_(
"The {subject} lesson in the {period}. period "
"on {day} has some current changes."
).format(subject=subject.name, period=period.period, day=date_format(day))
+ " "
)
if instance.teachers.all():
description += (
ngettext(
"The teacher {old} is substituted by {new}.",
"The teachers {old} are substituted by {new}.",
instance.teachers.count(),
).format(
old=instance.lesson_period.lesson.teacher_names,
new=instance.teacher_names,
)
+ " "
)
if instance.subject:
description += (
_("The subject is changed to {subject}.").format(subject=instance.subject.name)
+ " "
)
if instance.room:
description += (
_("The lesson is moved from {old} to {new}.").format(
old=instance.lesson_period.room.name,
new=instance.room.name,
)
+ " "
)
if instance.comment:
description += (
_("There is an additional comment: {comment}.").format(comment=instance.comment)
+ " "
)
elif isinstance(instance, Event):
if instance.date_start != instance.date_end:
description += (
_(
"There is an event that starts on {date_start}, {period_from}. period "
"and ends on {date_end}, {period_to}. period:"
).format(
date_start=date_format(instance.date_start),
date_end=date_format(instance.date_end),
period_from=instance.period_from.period,
period_to=instance.period_to.period,
)
+ "\n"
)
else:
description += (
_(
"There is an event on {date} from the "
"{period_from}. period to the {period_to}. period:"
).format(
date=date_format(instance.date_start),
period_from=instance.period_from.period,
period_to=instance.period_to.period,
)
+ "\n"
)
if instance.groups.all():
description += _("Groups: {groups}").format(groups=instance.group_names) + "\n"
if instance.teachers.all():
description += _("Teachers: {teachers}").format(teachers=instance.teacher_names) + "\n"
if instance.rooms.all():
description += (
_("Rooms: {rooms}").format(
rooms=", ".join([room.name for room in instance.rooms.all()])
)
+ "\n"
)
elif isinstance(instance, ExtraLesson):
description += (
_("There is an extra lesson on {date} in the {period}. period:").format(
date=date_format(instance.date),
period=instance.period.period,
)
+ "\n"
)
if instance.groups.all():
description += _("Groups: {groups}").format(groups=instance.group_names) + "\n"
if instance.room:
description += _("Subject: {subject}").format(subject=instance.subject.name) + "\n"
if instance.teachers.all():
description += _("Teachers: {teachers}").format(teachers=instance.teacher_names) + "\n"
if instance.room:
description += _("Room: {room}").format(room=instance.room.name) + "\n"
if instance.comment:
description += _("Comment: {comment}.").format(comment=instance.comment) + "\n"
elif isinstance(instance, SupervisionSubstitution):
description += _(
"The supervision of {old} on {date} between the {period_from}. period "
"and the {period_to}. period in the area {area} is substituted by {new}."
).format(
old=instance.supervision.teacher.full_name,
date=date_format(instance.date),
period_from=instance.supervision.break_item.after_period_number,
period_to=instance.supervision.break_item.before_period_number,
area=instance.supervision.area.name,
new=instance.teacher.full_name,
)
day = instance.date if hasattr(instance, "date") else instance.date_start
url = urljoin(
settings.BASE_URL,
reverse(
"my_timetable_by_date",
args=[day.year, day.month, day.day],
),
)
dt_start, dt_end = instance.time_range
dt_start = dt_start.replace(tzinfo=zoneinfo.ZoneInfo(settings.TIME_ZONE))
dt_end = dt_end.replace(tzinfo=zoneinfo.ZoneInfo(settings.TIME_ZONE))
send_time = get_site_preferences()["chronos__time_for_sending_notifications"]
number_of_days = get_site_preferences()["chronos__days_in_advance_notifications"]
start_range = timezone.now().replace(hour=send_time.hour, minute=send_time.minute)
if timezone.now().time() > send_time:
start_range = start_range - timedelta(days=1)
end_range = start_range + timedelta(days=number_of_days)
if dt_start < start_range and dt_end < end_range:
# Skip this, because the change is in the past
return
if dt_start <= end_range and dt_end >= start_range:
# Send immediately
send_at = timezone.now()
else:
# Schedule for later
send_at = datetime.combine(
dt_start.date() - timedelta(days=number_of_days), send_time
).replace(tzinfo=zoneinfo.ZoneInfo(settings.TIME_ZONE))
for recipient in recipients:
if recipient.preferences["chronos__send_notifications"]:
n = Notification(
recipient=recipient,
sender=_("Timetable"),
title=_("There are current changes to your timetable."),
description=description,
link=url,
send_at=send_at,
)
n.save()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment