diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py index 4ca0ec807e491178b16384e4a671c64563753acf..0f2774cbca1d7d3291750a63841b9642b1a1549d 100644 --- a/aleksis/apps/chronos/managers.py +++ b/aleksis/apps/chronos/managers.py @@ -114,10 +114,8 @@ class LessonSubstitutionManager(CurrentSiteManager): class WeekQuerySetMixin: - def annotate_week(self, week: Union[CalendarWeek, int]): + def annotate_week(self, week: Union[CalendarWeek]): """Annotate all lessons in the QuerySet with the number of the provided calendar week.""" - if isinstance(week, int): - week = CalendarWeek(week=week) return self.annotate( _week=models.Value(week.week, models.IntegerField()), @@ -226,6 +224,7 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin): **{ self._subst_path + "teachers": teacher, self._subst_path + "week": F("_week"), + self._subst_path + "year": F("_year"), } ) @@ -235,7 +234,11 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin): """Filter for all lessons taking part in a certain room.""" qs1 = self.filter(**{self._period_path + "room": room}) qs2 = self.filter( - **{self._subst_path + "room": room, self._subst_path + "week": F("_week"),} + **{ + self._subst_path + "room": room, + self._subst_path + "week": F("_week"), + self._subst_path + "year": F("_year"), + } ) return qs1.union(qs2) @@ -303,6 +306,8 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin): else: week = reference._week + week = CalendarWeek(week=week, year=reference.lesson.get_year(week)) + return self.annotate_week(week).all()[next_index] @@ -409,7 +414,7 @@ class SupervisionQuerySet(ValidityRangeRelatedQuerySet, WeekQuerySetMixin): """Filter for all supervisions given by a certain teacher.""" if self.count() > 0: if hasattr(self[0], "_week"): - week = CalendarWeek(week=self[0]._week) + week = CalendarWeek(week=self[0]._week, year=self[0]._year) else: week = CalendarWeek.current_week() @@ -517,6 +522,8 @@ class ExtraLessonQuerySet( return self.filter( week__gte=week_start.week, week__lte=week_end.week, + year__gte=week_start.year, + year__lte=week_end.year, period__weekday__gte=start.weekday(), period__weekday__lte=end.weekday(), ) diff --git a/aleksis/apps/chronos/migrations/0004_substitution_extra_lesson_year.py b/aleksis/apps/chronos/migrations/0004_substitution_extra_lesson_year.py new file mode 100644 index 0000000000000000000000000000000000000000..cb7383592d167f7d3aa4bf203aaa8cee503d7551 --- /dev/null +++ b/aleksis/apps/chronos/migrations/0004_substitution_extra_lesson_year.py @@ -0,0 +1,52 @@ +# Generated by Django 3.0.9 on 2020-08-13 14:06 + +from django.db import migrations, models + +import aleksis.apps.chronos.util.date + + +def migrate_data(apps, schema_editor): + LessonSubstitution = apps.get_model("chronos", "LessonSubstitution") + ExtraLesson = apps.get_model("chronos", "ExtraLesson") + + db_alias = schema_editor.connection.alias + + for sub in LessonSubstitution.objects.using(db_alias).all(): + year = sub.lesson_period.lesson.validity.date_start.year + if sub.week < int(sub.lesson_period.lesson.validity.date_start.strftime("%V")): + year += 1 + sub.year = year + sub.save() + + for extra_lesson in ExtraLesson.objects.using(db_alias).all(): + year = aleksis.apps.chronos.util.date.get_current_year() + extra_lesson.year = year + extra_lesson.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0003_drop_image_cropping"), + ("chronos", "0003_school_term_validity_fixes"), + ] + + operations = [ + migrations.AddField( + model_name="extralesson", + name="year", + field=models.IntegerField( + default=aleksis.apps.chronos.util.date.get_current_year, + verbose_name="Year", + ), + ), + migrations.AddField( + model_name="lessonsubstitution", + name="year", + field=models.IntegerField( + default=aleksis.apps.chronos.util.date.get_current_year, + verbose_name="Year", + ), + ), + migrations.RunPython(migrate_data), + ] diff --git a/aleksis/apps/chronos/mixins.py b/aleksis/apps/chronos/mixins.py index dabaabf6550210088f2d4481b62612f6130ef814..83fc1e289bf29ceedb2bcde045b3e8ca824d3a4e 100644 --- a/aleksis/apps/chronos/mixins.py +++ b/aleksis/apps/chronos/mixins.py @@ -1,6 +1,12 @@ +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_period_to_date from aleksis.core.managers import CurrentSiteManagerWithoutMigrations from aleksis.core.mixins import ExtensibleModel @@ -25,3 +31,26 @@ class ValidityRangeRelatedExtensibleModel(ExtensibleModel): class Meta: abstract = True + + +class WeekRelatedMixin: + @property + def date(self) -> date: + return week_period_to_date(self.calendar_week, self.lesson_period) + + @property + def calendar_week(self) -> CalendarWeek: + return CalendarWeek(week=self.week, year=self.year) + + +class WeekAnnotationMixin: + @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 diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index 336b184b1f2ae4ba6c0a265627c1b82bca16b3c2..31a7fed9592d78e6d88ca66eddc2b3d1233d91c1 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -35,7 +35,12 @@ from aleksis.apps.chronos.managers import ( TeacherPropertiesMixin, ValidityRangeQuerySet, ) -from aleksis.apps.chronos.mixins import ValidityRangeRelatedExtensibleModel +from aleksis.apps.chronos.mixins import ( + ValidityRangeRelatedExtensibleModel, + WeekAnnotationMixin, + WeekRelatedMixin, +) +from aleksis.apps.chronos.util.date import get_current_year from aleksis.apps.chronos.util.format import format_m2m from aleksis.core.managers import CurrentSiteManagerWithoutMigrations from aleksis.core.mixins import ExtensibleModel, SchoolTermRelatedExtensibleModel @@ -135,15 +140,12 @@ class TimePeriod(ValidityRangeRelatedExtensibleModel): return periods - def get_date(self, week: Optional[Union[CalendarWeek, int]] = None) -> date: + def get_date(self, week: Optional[CalendarWeek] = None) -> date: if isinstance(week, CalendarWeek): wanted_week = week else: - year = date.today().year - week_number = week or getattr(self, "_week", None) or CalendarWeek().week - - if week_number < SchoolTerm.current.date_start.isocalendar()[1]: - year += 1 + year = getattr(self, "_year", None) or date.today().year + week_number = getattr(self, "_week", None) or CalendarWeek().week wanted_week = CalendarWeek(year=year, week=week_number) @@ -335,12 +337,13 @@ class Lesson( verbose_name_plural = _("Lessons") -class LessonSubstitution(ExtensibleModel): +class LessonSubstitution(ExtensibleModel, WeekRelatedMixin): objects = LessonSubstitutionManager.from_queryset(LessonSubstitutionQuerySet)() week = models.IntegerField( verbose_name=_("Week"), default=CalendarWeek.current_week ) + year = models.IntegerField(verbose_name=_("Year"), default=get_current_year) lesson_period = models.ForeignKey( "LessonPeriod", models.CASCADE, "substitutions", verbose_name=_("Lesson period") @@ -379,7 +382,7 @@ class LessonSubstitution(ExtensibleModel): @property def date(self): - week = CalendarWeek(week=self.week, year=self.lesson_period.lesson.get_year(self.week)) + week = CalendarWeek(week=self.week, year=self.year) return week[self.lesson_period.period.weekday] def __str__(self): @@ -388,7 +391,7 @@ class LessonSubstitution(ExtensibleModel): class Meta: unique_together = [["lesson_period", "week"]] ordering = [ - "lesson_period__lesson__validity__date_start", + "year", "week", "lesson_period__period__weekday", "lesson_period__period__period", @@ -403,7 +406,7 @@ class LessonSubstitution(ExtensibleModel): verbose_name_plural = _("Lesson substitutions") -class LessonPeriod(ExtensibleModel): +class LessonPeriod(ExtensibleModel, WeekAnnotationMixin): label_ = "lesson_period" objects = LessonPeriodManager.from_queryset(LessonPeriodQuerySet)() @@ -429,14 +432,19 @@ class LessonPeriod(ExtensibleModel): verbose_name=_("Room"), ) - def get_substitution(self, week: Optional[int] = None) -> LessonSubstitution: - wanted_week = week or getattr(self, "_week", None) or CalendarWeek().week + def get_substitution( + self, week: Optional[CalendarWeek] = None + ) -> LessonSubstitution: + wanted_week = week or self.week or CalendarWeek() # We iterate over all substitutions because this can make use of # prefetching when this model is loaded from outside, in contrast # to .filter() for substitution in self.substitutions.all(): - if substitution.week == wanted_week: + if ( + substitution.week == wanted_week.week + and substitution.year == wanted_week.year + ): return substitution return None @@ -485,17 +493,6 @@ class LessonPeriod(ExtensibleModel): """ return LessonPeriod.objects.filter(lesson=self.lesson).next_lesson(self, -1) - @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 - class Meta: ordering = [ "lesson__validity__date_start", @@ -797,7 +794,7 @@ class Break(ValidityRangeRelatedExtensibleModel): verbose_name_plural = _("Breaks") -class Supervision(ValidityRangeRelatedExtensibleModel): +class Supervision(ValidityRangeRelatedExtensibleModel, WeekAnnotationMixin): objects = CurrentSiteManager.from_queryset(SupervisionQuerySet)() area = models.ForeignKey( @@ -816,11 +813,16 @@ class Supervision(ValidityRangeRelatedExtensibleModel): verbose_name=_("Teacher"), ) + def get_year(self, week: int) -> int: + year = self.validity.date_start.year + if week < int(self.validity.date_start.strftime("%V")): + year += 1 + return year + def get_substitution( - self, week: Optional[int] = None + self, week: Optional[CalendarWeek] = None ) -> Optional[SupervisionSubstitution]: - wanted_week = week or getattr(self, "_week", None) or CalendarWeek().week - wanted_week = CalendarWeek(week=wanted_week) + wanted_week = week or self.week or CalendarWeek() # We iterate over all substitutions because this can make use of # prefetching when this model is loaded from outside, in contrast # to .filter() @@ -939,7 +941,9 @@ class Event( verbose_name_plural = _("Events") -class ExtraLesson(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin): +class ExtraLesson( + SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, WeekRelatedMixin +): label_ = "extra_lesson" objects = CurrentSiteManager.from_queryset(ExtraLessonQuerySet)() @@ -947,6 +951,7 @@ class ExtraLesson(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin): week = models.IntegerField( verbose_name=_("Week"), default=CalendarWeek.current_week ) + year = models.IntegerField(verbose_name=_("Year"), default=get_current_year) period = models.ForeignKey( "TimePeriod", models.CASCADE, diff --git a/aleksis/apps/chronos/tables.py b/aleksis/apps/chronos/tables.py index 92a966fe11a3d866bdd9d160ebba712ab4e3066d..b24ac8728c07d490e9a23df6dd424b096c08f999 100644 --- a/aleksis/apps/chronos/tables.py +++ b/aleksis/apps/chronos/tables.py @@ -14,8 +14,8 @@ def _css_class_from_lesson_state( record: Optional[LessonPeriod] = None, table: Optional[LessonsTable] = None ) -> str: """Return CSS class depending on lesson state.""" - if record.get_substitution(record._week): - if record.get_substitution(record._week).cancelled: + if record.get_substitution(): + if record.get_substitution().cancelled: return "success" else: return "warning" diff --git a/aleksis/apps/chronos/templatetags/week_helpers.py b/aleksis/apps/chronos/templatetags/week_helpers.py index c97703175ec5c4f39cf1757b5ee426bddb4e0dbb..2f66d51e449c2a0d46f53700b32b79e4b53fc3fa 100644 --- a/aleksis/apps/chronos/templatetags/week_helpers.py +++ b/aleksis/apps/chronos/templatetags/week_helpers.py @@ -22,7 +22,7 @@ def week_end(week: CalendarWeek) -> date: @register.filter def only_week(qs: QuerySet, week: Optional[CalendarWeek]) -> QuerySet: wanted_week = week or CalendarWeek() - return qs.filter(week=wanted_week.week) + return qs.filter(week=wanted_week.week, year=wanted_week.year) @register.simple_tag @@ -31,12 +31,12 @@ def weekday_to_date(week: CalendarWeek, weekday: int) -> date: @register.simple_tag -def period_to_date(week: Union[CalendarWeek, int], period) -> date: +def period_to_date(week: CalendarWeek, period) -> date: return week_period_to_date(week, period) @register.simple_tag -def period_to_time_start(week: Union[CalendarWeek, int], period) -> date: +def period_to_time_start(week: CalendarWeek, period) -> date: return period.get_datetime_start(week) diff --git a/aleksis/apps/chronos/util/build.py b/aleksis/apps/chronos/util/build.py index 48fc7cddcf4d5e909f49c45525ffc61606e7edde..07b6d68fa06784858680b385e8a10bc8fe97b762 100644 --- a/aleksis/apps/chronos/util/build.py +++ b/aleksis/apps/chronos/util/build.py @@ -61,9 +61,9 @@ def build_timetable( if is_person: extra_lessons = ExtraLesson.objects.on_day(date_ref).filter_from_person(obj) else: - extra_lessons = ExtraLesson.objects.filter(week=date_ref.week).filter_from_type( - type_, obj - ) + extra_lessons = ExtraLesson.objects.filter( + week=date_ref.week, year=date_ref.year + ).filter_from_type(type_, obj) # Sort lesson periods in a dict extra_lessons_per_period = extra_lessons.group_by_periods(is_person=is_person) diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py index 64349e54ee52ba36643d82a973e8baac5124bef2..ae3cc9c934dd4b4e0121f5bac683e850d8ab7a9c 100644 --- a/aleksis/apps/chronos/util/chronos_helpers.py +++ b/aleksis/apps/chronos/util/chronos_helpers.py @@ -32,5 +32,5 @@ def get_substitution_by_id(request: HttpRequest, id_: int, week: int): wanted_week = lesson_period.lesson.get_calendar_week(week) return LessonSubstitution.objects.filter( - week=wanted_week.week, lesson_period=lesson_period + week=wanted_week.week, year=wanted_week.year, lesson_period=lesson_period ).first() diff --git a/aleksis/apps/chronos/util/date.py b/aleksis/apps/chronos/util/date.py index e31775ef74860c2276bfaf39d758c283691e487a..159f8e4cbfa08df8d5bf286b141e482591bfc658 100644 --- a/aleksis/apps/chronos/util/date.py +++ b/aleksis/apps/chronos/util/date.py @@ -1,6 +1,8 @@ from datetime import date from typing import List, Tuple, Union +from django.utils import timezone + from calendarweek import CalendarWeek @@ -14,7 +16,7 @@ def week_weekday_to_date(week: CalendarWeek, weekday: int) -> date: return week[weekday] -def week_period_to_date(week: Union[CalendarWeek, int], period) -> date: +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) @@ -31,3 +33,9 @@ def get_weeks_for_year(year: int) -> List[CalendarWeek]: current_week += 1 return weeks + + +def get_current_year() -> int: + """Get current year.""" + + return timezone.now().year