diff --git a/.gitignore b/.gitignore index c7a8aefaf61fbf7d80eeba081b6192eca60acb32..79b5b76de6f6445254cbf64f7e4fb228ffdf0ab8 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,9 @@ docs/_build/ /static/ /whoosh_index/ .vite +.dev-js/.yarn +.dev-js/.pnp.cjs +.dev-js/.pnp.loader.mjs # Lock files poetry.lock diff --git a/.prettierignore b/.prettierignore index 38d141b743fd55678f50077c0617924475817095..de783fc60a1ca5f6e25204bb7a51eab815bc77ce 100644 --- a/.prettierignore +++ b/.prettierignore @@ -89,3 +89,6 @@ yarn.lock # Do not check/reformat generated files aleksis/core/util/licenses.json .vite/ + +.pnp.cjs +.pnp.loader.mjs diff --git a/aleksis/apps/chronos/filters.py b/aleksis/apps/chronos/filters.py index d868fdf59cdd0850f4f623db13da424e79819be4..5e231ef98ed73c3102288a09d8acabb7a39d0972 100644 --- a/aleksis/apps/chronos/filters.py +++ b/aleksis/apps/chronos/filters.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence from django.db.models import Count, Q from django.forms import RadioSelect diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py index 486409f95052b51431fcaf74121690dafcb31d7e..73476003eab7821a9d73b326a686d079d85d2417 100644 --- a/aleksis/apps/chronos/managers.py +++ b/aleksis/apps/chronos/managers.py @@ -1,6 +1,7 @@ +from collections.abc import Iterable from datetime import date, datetime, timedelta from enum import Enum -from typing import Dict, Iterable, List, Optional, Union +from typing import TYPE_CHECKING, Optional, Union from django.contrib.sites.managers import CurrentSiteManager as _CurrentSiteManager from django.db import models @@ -15,6 +16,9 @@ from aleksis.core.managers import DateRangeQuerySetMixin, SchoolTermRelatedQuery from aleksis.core.models import Group, Person from aleksis.core.util.core_helpers import get_site_preferences +if TYPE_CHECKING: + from .models import Holiday, LessonPeriod, Room, ValidityRange + class ValidityRangeQuerySet(QuerySet, DateRangeQuerySetMixin): """Custom query set for validity ranges.""" @@ -402,7 +406,7 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin): return lesson_periods - def group_by_validity(self) -> Dict["ValidityRange", List["LessonPeriod"]]: + def group_by_validity(self) -> dict["ValidityRange", list["LessonPeriod"]]: """Group lesson periods by validity range as dictionary.""" lesson_periods_by_validity = {} for lesson_period in self: @@ -635,7 +639,7 @@ class AbsenceQuerySet(DateRangeQuerySetMixin, SchoolTermRelatedQuerySet): class HolidayQuerySet(QuerySet, DateRangeQuerySetMixin): """QuerySet with custom query methods for holidays.""" - def get_all_days(self) -> List[date]: + def get_all_days(self) -> list[date]: """Get all days included in the selected holidays.""" holiday_days = [] for holiday in self: diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index ed6905004d257c6e89bcf22ef0b87cd1a1a248e5..a4bf3831063fd76bf20bd9da681b00973de9000f 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -2,22 +2,19 @@ from __future__ import annotations -import os +from collections.abc import Iterable, Iterator from datetime import date, datetime, time, timedelta from itertools import chain -from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union +from typing import Any from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError -from django.core.files.base import ContentFile, File -from django.core.files.storage import default_storage from django.core.validators import MinValueValidator from django.db import models from django.db.models import Max, Min, Q from django.db.models.functions import Coalesce from django.dispatch import receiver from django.forms import Media -from django.template.loader import render_to_string from django.urls import reverse from django.utils import timezone from django.utils.formats import date_format @@ -26,8 +23,6 @@ from django.utils.translation import gettext_lazy as _ from cache_memoize import cache_memoize from calendarweek.django import CalendarWeek, i18n_day_abbr_choices_lazy, i18n_day_name_choices_lazy -from celery.result import allow_join_result -from celery.states import SUCCESS from colorfield.fields import ColorField from model_utils import FieldTracker from reversion.models import Revision, Version @@ -69,7 +64,6 @@ from aleksis.core.mixins import ( ) from aleksis.core.models import DashboardWidget, Group, Room, SchoolTerm from aleksis.core.util.core_helpers import has_person -from aleksis.core.util.pdf import generate_pdf_from_template class ValidityRange(ExtensibleModel): @@ -93,7 +87,7 @@ class ValidityRange(ExtensibleModel): @classmethod @cache_memoize(3600) - def get_current(cls, day: Optional[date] = None): + def get_current(cls, day: date | None = None): if not day: day = timezone.now().date() try: @@ -110,12 +104,11 @@ class ValidityRange(ExtensibleModel): if self.date_end < self.date_start: raise ValidationError(_("The start date must be earlier than the end date.")) - if self.school_term: - if ( - self.date_end > self.school_term.date_end - or self.date_start < self.school_term.date_start - ): - raise ValidationError(_("The validity range must be within the school term.")) + if self.school_term and ( + self.date_end > self.school_term.date_end + or self.date_start < self.school_term.date_start + ): + raise ValidationError(_("The validity range must be within the school term.")) qs = ValidityRange.objects.within_dates(self.date_start, self.date_end) if self.pk: @@ -158,14 +151,14 @@ class TimePeriod(ValidityRangeRelatedExtensibleModel): return f"{self.get_weekday_display()}, {self.period}." @classmethod - def get_times_dict(cls) -> Dict[int, Tuple[datetime, datetime]]: + def get_times_dict(cls) -> dict[int, tuple[datetime, datetime]]: periods = {} for period in cls.objects.for_current_or_all().all(): periods[period.period] = (period.time_start, period.time_end) return periods - def get_date(self, week: Optional[CalendarWeek] = None) -> date: + def get_date(self, week: CalendarWeek | None = None) -> date: if isinstance(week, CalendarWeek): wanted_week = week else: @@ -176,37 +169,26 @@ class TimePeriod(ValidityRangeRelatedExtensibleModel): return wanted_week[self.weekday] - def get_datetime_start( - self, date_ref: Optional[Union[CalendarWeek, int, date]] = None - ) -> datetime: + def get_datetime_start(self, date_ref: CalendarWeek | int | date | None = None) -> datetime: """Get datetime of lesson start in a specific week.""" - if isinstance(date_ref, date): - day = date_ref - else: - day = self.get_date(date_ref) + day = date_ref if isinstance(date_ref, date) else self.get_date(date_ref) return datetime.combine(day, self.time_start) - def get_datetime_end( - self, date_ref: Optional[Union[CalendarWeek, int, date]] = None - ) -> datetime: + def get_datetime_end(self, date_ref: CalendarWeek | int | date | None = None) -> datetime: """Get datetime of lesson end in a specific week.""" - if isinstance(date_ref, date): - day = date_ref - else: - day = self.get_date(date_ref) + day = date_ref if isinstance(date_ref, date) else self.get_date(date_ref) return datetime.combine(day, self.time_end) @classmethod def get_next_relevant_day( - cls, day: Optional[date] = None, time: Optional[time] = None, prev: bool = False + cls, day: date | None = None, time: time | None = None, prev: bool = False ) -> date: """Return next (previous) day with lessons depending on date and time.""" if day is None: day = timezone.now().date() - if time is not None and cls.time_max and not prev: - if time > cls.time_max: - day += timedelta(days=1) + if time is not None and cls.time_max and not prev and time > cls.time_max: + day += timedelta(days=1) cw = CalendarWeek.from_date(day) @@ -226,7 +208,7 @@ class TimePeriod(ValidityRangeRelatedExtensibleModel): return day @classmethod - def get_relevant_week_from_datetime(cls, when: Optional[datetime] = None) -> CalendarWeek: + def get_relevant_week_from_datetime(cls, when: datetime | None = None) -> CalendarWeek: """Return currently relevant week depending on current date and time.""" if not when: when = timezone.now() @@ -236,15 +218,15 @@ class TimePeriod(ValidityRangeRelatedExtensibleModel): week = CalendarWeek.from_date(day) - if cls.weekday_max and day.weekday() > cls.weekday_max: - week += 1 - elif cls.time_max and time > cls.time_max and day.weekday() == cls.weekday_max: + if (cls.weekday_max and day.weekday() > cls.weekday_max) or ( + cls.time_max and time > cls.time_max and day.weekday() == cls.weekday_max + ): week += 1 return week @classmethod - def get_prev_next_by_day(cls, day: date, url: str) -> Tuple[str, str]: + def get_prev_next_by_day(cls, day: date, url: str) -> tuple[str, str]: """Build URLs for previous/next day.""" day_prev = cls.get_next_relevant_day(day - timedelta(days=1), prev=True) day_next = cls.get_next_relevant_day(day + timedelta(days=1)) @@ -255,7 +237,7 @@ class TimePeriod(ValidityRangeRelatedExtensibleModel): return url_prev, url_next @classmethod - def from_period(cls, period: int, day: date) -> "TimePeriod": + def from_period(cls, period: int, day: date) -> TimePeriod: """Get `TimePeriod` object for a period on a specific date. This will respect the relation to validity ranges. @@ -282,12 +264,12 @@ class TimePeriod(ValidityRangeRelatedExtensibleModel): @classproperty @cache_memoize(3600) - def time_min(cls) -> Optional[time]: + def time_min(cls) -> time | None: return cls.objects.for_current_or_all().aggregate(Min("time_start")).get("time_start__min") @classproperty @cache_memoize(3600) - def time_max(cls) -> Optional[time]: + def time_max(cls) -> time | None: return cls.objects.for_current_or_all().aggregate(Max("time_end")).get("time_end__max") @classproperty @@ -310,7 +292,7 @@ class TimePeriod(ValidityRangeRelatedExtensibleModel): @classproperty @cache_memoize(3600) - def period_choices(cls) -> List[Tuple[Union[str, int], str]]: + def period_choices(cls) -> list[tuple[str | int, str]]: """Build choice list of periods for usage within Django.""" time_periods = ( cls.objects.filter(weekday=cls.weekday_min) @@ -449,7 +431,7 @@ class LessonSubstitution(ExtensibleModel, TeacherPropertiesMixin, WeekRelatedMix default=False, verbose_name=_("Cancelled for teachers?") ) - comment = models.TextField(verbose_name=_("Comment"), blank=True, null=True) + comment = models.TextField(verbose_name=_("Comment"), blank=True) def clean(self) -> None: if self.subject and self.cancelled: @@ -524,7 +506,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) verbose_name=_("Room"), ) - def get_substitution(self, week: Optional[CalendarWeek] = None) -> LessonSubstitution: + def get_substitution(self, week: CalendarWeek | None = None) -> LessonSubstitution: wanted_week = week or self.week or CalendarWeek() # We iterate over all substitutions because this can make use of @@ -535,7 +517,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) return substitution return None - def get_subject(self) -> Optional[Subject]: + def get_subject(self) -> Subject | None: sub = self.get_substitution() if sub and sub.subject: return sub.subject @@ -549,7 +531,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) else: return self.lesson.teachers - def get_room(self) -> Optional[Room]: + def get_room(self) -> Room | None: if self.get_substitution() and self.get_substitution().room: return self.get_substitution().room else: @@ -578,7 +560,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) return LessonPeriod.objects.filter(lesson__in=self.lesson._equal_lessons) @property - def next(self) -> "LessonPeriod": + def next(self) -> LessonPeriod: # noqa """Get next lesson period of this lesson. .. warning:: @@ -587,7 +569,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) return self._equal_lesson_periods.next_lesson(self) @property - def prev(self) -> "LessonPeriod": + def prev(self) -> LessonPeriod: """Get previous lesson period of this lesson. .. warning:: @@ -596,7 +578,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) return self._equal_lesson_periods.next_lesson(self, -1) def is_replaced_by_event( - self, events: Iterable[Event], groups: Optional[Iterable[Group]] = None + self, events: Iterable[Event], groups: Iterable[Group] | None = None ) -> bool: """Check if this lesson period is replaced by an event.""" groups_of_event = set(chain(*[event.groups.all() for event in events])) @@ -683,7 +665,7 @@ class TimetableWidget(DashboardWidget): class AbsenceReason(ExtensibleModel): short_name = models.CharField(verbose_name=_("Short name"), max_length=255) - name = models.CharField(verbose_name=_("Name"), blank=True, null=True, max_length=255) + name = models.CharField(verbose_name=_("Name"), blank=True, max_length=255) def __str__(self): if self.name: @@ -754,7 +736,7 @@ class Absence(SchoolTermRelatedExtensibleModel): null=True, related_name="+", ) - comment = models.TextField(verbose_name=_("Comment"), blank=True, null=True) + comment = models.TextField(verbose_name=_("Comment"), blank=True) def __str__(self): if self.teacher: @@ -813,7 +795,7 @@ class Holiday(ExtensibleModel): title = models.CharField(verbose_name=_("Title"), max_length=255) date_start = models.DateField(verbose_name=_("Start date"), null=True) date_end = models.DateField(verbose_name=_("End date"), null=True) - comments = models.TextField(verbose_name=_("Comments"), blank=True, null=True) + comments = models.TextField(verbose_name=_("Comments"), blank=True) def get_days(self) -> Iterator[date]: delta = self.date_end - self.date_start @@ -821,7 +803,7 @@ class Holiday(ExtensibleModel): yield self.date_start + timedelta(days=i) @classmethod - def on_day(cls, day: date) -> Optional["Holiday"]: + def on_day(cls, day: date) -> Holiday | None: holidays = cls.objects.on_day(day) if holidays.exists(): return holidays[0] @@ -829,7 +811,7 @@ class Holiday(ExtensibleModel): return None @classmethod - def in_week(cls, week: CalendarWeek) -> Dict[int, Optional["Holiday"]]: + def in_week(cls, week: CalendarWeek) -> dict[int, Holiday | None]: per_weekday = {} holidays = Holiday.objects.in_week(week) @@ -920,7 +902,7 @@ class Break(ValidityRangeRelatedExtensibleModel): return self.before_period.time_start if self.before_period else None @classmethod - def get_breaks_dict(cls) -> Dict[int, Tuple[datetime, datetime]]: + def get_breaks_dict(cls) -> dict[int, tuple[datetime, datetime]]: breaks = {} for break_ in cls.objects.all(): breaks[break_.before_period_number] = break_ @@ -972,9 +954,7 @@ class Supervision(ValidityRangeRelatedExtensibleModel, WeekAnnotationMixin): return CalendarWeek(year=year, week=week) - def get_substitution( - self, week: Optional[CalendarWeek] = None - ) -> Optional[SupervisionSubstitution]: + def get_substitution(self, week: CalendarWeek | None = None) -> SupervisionSubstitution | None: 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 @@ -1048,7 +1028,7 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope objects = EventManager.from_queryset(EventQuerySet)() - title = models.CharField(verbose_name=_("Title"), max_length=255, blank=True, null=True) + title = models.CharField(verbose_name=_("Title"), max_length=255, blank=True) date_start = models.DateField(verbose_name=_("Start date"), null=True) date_end = models.DateField(verbose_name=_("End date"), null=True) @@ -1219,7 +1199,7 @@ class ExtraLesson( verbose_name=_("Room"), ) - comment = models.CharField(verbose_name=_("Comment"), blank=True, null=True, max_length=255) + comment = models.CharField(verbose_name=_("Comment"), blank=True, max_length=255) exam = models.ForeignKey( "Exam", @@ -1293,7 +1273,7 @@ class AutomaticPlan(LiveDocument): """Get last day which should be shown in the PDF.""" return self.current_start_day + timedelta(days=self.number_of_days - 1) - def get_context_data(self) -> Dict[str, Any]: + def get_context_data(self) -> dict[str, Any]: """Get context data for generating the substitutions PDF.""" from aleksis.apps.chronos.util.chronos_helpers import get_substitutions_context_data # noqa diff --git a/aleksis/apps/chronos/tables.py b/aleksis/apps/chronos/tables.py index 07c72aee28de6738206f5506f8cef629cace56ec..7a60b1f6254cfef5bfaf2a415c5bfcff58f6156c 100644 --- a/aleksis/apps/chronos/tables.py +++ b/aleksis/apps/chronos/tables.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional, Union - from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ @@ -12,8 +10,8 @@ from .models import LessonPeriod, Supervision def _title_attr_from_lesson_or_supervision_state( - record: Optional[Union[LessonPeriod, Supervision]] = None, - table: Optional[Union[LessonsTable, SupervisionsTable]] = None, + record: LessonPeriod | Supervision | None = None, + table: LessonsTable | SupervisionsTable | None = None, ) -> str: """Return HTML title depending on lesson or supervision state.""" if record.get_substitution(): @@ -26,7 +24,7 @@ def _title_attr_from_lesson_or_supervision_state( class SubstitutionColumn(tables.Column): - def render(self, value, record: Optional[Union[LessonPeriod, Supervision]] = None): + def render(self, value, record: LessonPeriod | Supervision | None = None): if record.get_substitution(): return ( format_html( @@ -48,7 +46,7 @@ class SubstitutionColumn(tables.Column): class LessonStatusColumn(tables.Column): - def render(self, record: Optional[Union[LessonPeriod, Supervision]] = None): + def render(self, record: LessonPeriod | Supervision | None = None): if record.get_substitution(): return ( format_html( diff --git a/aleksis/apps/chronos/util/build.py b/aleksis/apps/chronos/util/build.py index f79ecf6484dbaa18f22447ad9a0a0c57160add96..625998db7a20e293f0f8edc3e4f04f4109c3c9db 100644 --- a/aleksis/apps/chronos/util/build.py +++ b/aleksis/apps/chronos/util/build.py @@ -1,6 +1,6 @@ from collections import OrderedDict from datetime import date -from typing import List, Tuple, Union +from typing import Union from django.apps import apps @@ -113,10 +113,7 @@ def build_timetable( # Get events events = Event.objects - if is_week: - events = events.in_week(date_ref) - else: - events = events.on_day(date_ref) + events = events.in_week(date_ref) if is_week else events.on_day(date_ref) events = events.only( "id", @@ -161,19 +158,13 @@ def build_timetable( # If daily timetable for person, skip other weekdays continue - if weekday == weekday_from: - # If start day, use start period - period_from = period_from_first_weekday - else: - # If not start day, use min period - period_from = TimePeriod.period_min + # 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 weekday == weekday_to: - # If end day, use end period - period_to = period_to_last_weekday - else: - # If not end day, use max period - period_to = TimePeriod.period_max + # 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 @@ -203,10 +194,7 @@ def build_timetable( if type_ == TimetableType.TEACHER: # Get matching supervisions - if not is_week: - week = CalendarWeek.from_date(date_ref) - else: - week = date_ref + week = CalendarWeek.from_date(date_ref) if not is_week else date_ref supervisions = ( Supervision.objects.in_week(week) .all() @@ -275,9 +263,8 @@ def build_timetable( if ( period in supervisions_per_period_after and weekday not in holidays_per_weekday - ): - if weekday in supervisions_per_period_after[period]: - col = supervisions_per_period_after[period][weekday] + ) and weekday in supervisions_per_period_after[period]: + col = supervisions_per_period_after[period][weekday] cols.append(col) row["cols"] = cols @@ -336,7 +323,7 @@ def build_timetable( ) lesson_period.replaced_by_event = replaced_by_event if not replaced_by_event or ( - replaced_by_event and not type_ == TimetableType.GROUP + replaced_by_event and type_ != TimetableType.GROUP ): col.append(lesson_period) @@ -393,7 +380,7 @@ def build_timetable( return rows -def build_substitutions_list(wanted_day: date) -> List[dict]: +def build_substitutions_list(wanted_day: date) -> list[dict]: rows = [] subs = LessonSubstitution.objects.on_day(wanted_day).order_by( @@ -466,10 +453,7 @@ def build_substitutions_list(wanted_day: date) -> List[dict]: events = Event.objects.on_day(wanted_day).annotate_day(wanted_day) for event in events: - if event.groups.all(): - sort_a = event.group_names - else: - sort_a = f"Z.{event.teacher_names}" + sort_a = event.group_names if event.groups.all() else f"Z.{event.teacher_names}" row = { "type": "event", @@ -489,8 +473,8 @@ def build_substitutions_list(wanted_day: date) -> List[dict]: def build_weekdays( - base: List[Tuple[int, str]], wanted_week: CalendarWeek, with_holidays: bool = True -) -> List[dict]: + 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) diff --git a/aleksis/apps/chronos/util/change_tracker.py b/aleksis/apps/chronos/util/change_tracker.py index 348899dd30cb3328831b6cd859a1aaf68bebcfed..c9b8afb1b94c9bd5630a50b36d36e08ba58d31fd 100644 --- a/aleksis/apps/chronos/util/change_tracker.py +++ b/aleksis/apps/chronos/util/change_tracker.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Type +from typing import Any, Optional from django.contrib.contenttypes.models import ContentType from django.db import transaction @@ -41,7 +41,7 @@ class TimetableDataChangeTracker: """Helper class for tracking changes in timetable models by using signals.""" @classmethod - def get_models(cls) -> list[Type[Model]]: + def get_models(cls) -> list[type[Model]]: """Return all models that should be tracked.""" from aleksis.apps.chronos.models import ( Event, @@ -70,7 +70,7 @@ class TimetableDataChangeTracker: if f.many_to_many } self.m2m_fields.update(m2m_fields) - for through_model, field in m2m_fields.items(): + for through_model, _field in m2m_fields.items(): m2m_changed.connect(self._handle_m2m_changed, sender=through_model, weak=False) transaction.on_commit(self.close) @@ -87,24 +87,24 @@ class TimetableDataChangeTracker: else: self.changes[key].changed_fields.update(change.changed_fields) - def _handle_save(self, sender: Type[Model], instance: Model, created: bool, **kwargs): + def _handle_save(self, sender: type[Model], instance: Model, created: bool, **kwargs): """Handle the save signal.""" change = TimetableChange(instance, created=created) if not created: change.changed_fields = instance.tracker.changed() self._add_change(change) - def _handle_delete(self, sender: Type[Model], instance: Model, **kwargs): + def _handle_delete(self, sender: type[Model], instance: Model, **kwargs): """Handle the delete signal.""" change = TimetableChange(instance, deleted=True) self._add_change(change) def _handle_m2m_changed( self, - sender: Type[Model], + sender: type[Model], instance: Model, action: str, - model: Type[Model], + model: type[Model], pk_set: set, **kwargs, ): @@ -127,7 +127,7 @@ class TimetableDataChangeTracker: post_save.disconnect(self._handle_save, sender=model) pre_delete.disconnect(self._handle_delete, sender=model) - for through_model, field in self.m2m_fields.items(): + for through_model, _field in self.m2m_fields.items(): m2m_changed.disconnect(self._handle_m2m_changed, sender=through_model) timetable_data_changed.send(sender=self, changes=self.changes) diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py index 17d98c54a88e28c9be5ca81d8561924d4155c83a..26b2f13052f8823e9c3152980d59c40ff8879d20 100644 --- a/aleksis/apps/chronos/util/chronos_helpers.py +++ b/aleksis/apps/chronos/util/chronos_helpers.py @@ -190,7 +190,7 @@ def get_substitutions_context_data( if is_print: next_day = wanted_day - for i in range(day_number): + for _i in range(day_number): day_contexts[next_day] = {"day": next_day} next_day = TimePeriod.get_next_relevant_day(next_day + timedelta(days=1)) else: diff --git a/aleksis/apps/chronos/util/date.py b/aleksis/apps/chronos/util/date.py index d25375c7916abdc7664dad085704076eab5d1d5b..9a2eb10592509b12f4e915a5e115b96d95ce939a 100644 --- a/aleksis/apps/chronos/util/date.py +++ b/aleksis/apps/chronos/util/date.py @@ -1,12 +1,11 @@ from datetime import date -from typing import List, Tuple from django.utils import timezone from calendarweek import CalendarWeek -def week_weekday_from_date(when: date) -> Tuple[CalendarWeek, int]: +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()) @@ -21,7 +20,7 @@ def week_period_to_date(week: CalendarWeek, period) -> date: return period.get_date(week) -def get_weeks_for_year(year: int) -> List[CalendarWeek]: +def get_weeks_for_year(year: int) -> list[CalendarWeek]: """Generate all weeks for one year.""" weeks = [] diff --git a/aleksis/apps/chronos/util/format.py b/aleksis/apps/chronos/util/format.py index 6818947d0d5350754e5bc9a7804b46b4cf56fb60..0dd640f3c6e892741eb81d8de61421681160c9c9 100644 --- a/aleksis/apps/chronos/util/format.py +++ b/aleksis/apps/chronos/util/format.py @@ -1,7 +1,11 @@ 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.""" diff --git a/aleksis/apps/chronos/util/notifications.py b/aleksis/apps/chronos/util/notifications.py index 2c7d7dbce281c41d03d124ecbde96bf6c63eeb01..847dc599ab47699ec4867d22822f81c8f1ec00ff 100644 --- a/aleksis/apps/chronos/util/notifications.py +++ b/aleksis/apps/chronos/util/notifications.py @@ -1,4 +1,3 @@ -import zoneinfo from datetime import datetime, timedelta from typing import Union from urllib.parse import urljoin @@ -10,6 +9,8 @@ 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 @@ -17,7 +18,7 @@ from ..models import Event, ExtraLesson, LessonSubstitution, SupervisionSubstitu def send_notifications_for_object( - instance: Union[ExtraLesson, LessonSubstitution, Event, SupervisionSubstitution] + instance: Union[ExtraLesson, LessonSubstitution, Event, SupervisionSubstitution], ): """Send notifications for a change object.""" recipients = [] @@ -27,10 +28,7 @@ def send_notifications_for_object( recipients += Person.objects.filter( member_of__in=instance.lesson_period.lesson.groups.all() ) - elif isinstance(instance, Event): - recipients += instance.teachers.all() - recipients += Person.objects.filter(member_of__in=instance.groups.all()) - elif isinstance(instance, ExtraLesson): + elif isinstance(instance, (Event, ExtraLesson)): recipients += instance.teachers.all() recipients += Person.objects.filter(member_of__in=instance.groups.all()) elif isinstance(instance, SupervisionSubstitution): diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py index a7ae9b77b322c00f41860c39a3f6cc7ae6376687..8bd3307977dbe5a625df0223edf7a85044ca5f78 100644 --- a/aleksis/apps/chronos/views.py +++ b/aleksis/apps/chronos/views.py @@ -291,22 +291,21 @@ def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse context["substitution"] = lesson_substitution - if request.method == "POST": - if edit_substitution_form.is_valid(): - with reversion.create_revision(atomic=True): - tracker = TimetableDataChangeTracker() + if request.method == "POST" and edit_substitution_form.is_valid(): + with reversion.create_revision(atomic=True): + TimetableDataChangeTracker() - lesson_substitution = edit_substitution_form.save(commit=False) - if not lesson_substitution.pk: - lesson_substitution.lesson_period = lesson_period - lesson_substitution.week = wanted_week.week - lesson_substitution.year = wanted_week.year - lesson_substitution.save() - edit_substitution_form.save_m2m() + lesson_substitution = edit_substitution_form.save(commit=False) + if not lesson_substitution.pk: + lesson_substitution.lesson_period = lesson_period + lesson_substitution.week = wanted_week.week + lesson_substitution.year = wanted_week.year + lesson_substitution.save() + edit_substitution_form.save_m2m() - messages.success(request, _("The substitution has been saved.")) + messages.success(request, _("The substitution has been saved.")) - return redirect("lessons_day_by_date", year=day.year, month=day.month, day=day.day) + return redirect("lessons_day_by_date", year=day.year, month=day.month, day=day.day) context["edit_substitution_form"] = edit_substitution_form @@ -432,23 +431,20 @@ def edit_supervision_substitution(request: HttpRequest, id_: int, week: int) -> context["substitution"] = supervision_substitution - if request.method == "POST": - if edit_supervision_substitution_form.is_valid(): - with reversion.create_revision(atomic=True): - tracker = TimetableDataChangeTracker() + if request.method == "POST" and edit_supervision_substitution_form.is_valid(): + with reversion.create_revision(atomic=True): + TimetableDataChangeTracker() - supervision_substitution = edit_supervision_substitution_form.save(commit=False) - if not supervision_substitution.pk: - supervision_substitution.supervision = supervision - supervision_substitution.date = date - supervision_substitution.save() - edit_supervision_substitution_form.save_m2m() + supervision_substitution = edit_supervision_substitution_form.save(commit=False) + if not supervision_substitution.pk: + supervision_substitution.supervision = supervision + supervision_substitution.date = date + supervision_substitution.save() + edit_supervision_substitution_form.save_m2m() - messages.success(request, _("The substitution has been saved.")) + messages.success(request, _("The substitution has been saved.")) - return redirect( - "supervisions_day_by_date", year=date.year, month=date.month, day=date.day - ) + return redirect("supervisions_day_by_date", year=date.year, month=date.month, day=date.day) context["edit_supervision_substitution_form"] = edit_supervision_substitution_form diff --git a/package.json b/package.json deleted file mode 100644 index 55795327ff2174e7bfa6778a26173717367abd5a..0000000000000000000000000000000000000000 --- a/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "aleksis-builddeps", - "version": "1.0.0", - "dependencies": { - "@intlify/eslint-plugin-vue-i18n": "^2.0.0", - "eslint": "^8.26.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-vue": "^9.7.0", - "prettier": "^2.8.1", - "stylelint": "^14.14.0", - "stylelint-config-prettier": "^9.0.3", - "stylelint-config-standard": "^29.0.0" - } -} diff --git a/tox.ini b/tox.ini index 85c2494a5a2f5480bb05d48781edfaa74803eeab..294e65bc96d4e06262b67e3e6b8a987307b226e4 100644 --- a/tox.ini +++ b/tox.ini @@ -4,11 +4,12 @@ skip_missing_interpreters = true envlist = py39,py310,py311 [testenv] -allowlist_externals = poetry +allowlist_externals = + poetry + yarnpkg skip_install = true -envdir = {toxworkdir}/globalenv commands_pre = - poetry install + poetry install --all-extras poetry run aleksis-admin vite build poetry run aleksis-admin collectstatic --no-input commands = @@ -22,14 +23,17 @@ setenv = TEST_HOST = {env:TEST_HOST:172.17.0.1} [testenv:lint] +commands_pre = + poetry install --only=dev + yarnpkg --cwd=.dev-js commands = - poetry run black --check --diff aleksis/ - poetry run isort -c --diff --stdout aleksis/ - poetry run flake8 {posargs} aleksis/ - poetry run sh -c "aleksis-admin yarn run prettier --check --ignore-path={toxinidir}/.prettierignore {toxinidir}" - poetry run sh -c "aleksis-admin yarn run eslint {toxinidir}/aleksis/**/*/frontend/**/*.{js,vue} --config={toxinidir}/.eslintrc.js --resolve-plugins-relative-to=." + poetry run ruff check {posargs} aleksis/ + yarnpkg --cwd=.dev-js run prettier --ignore-path={toxinidir}/.prettierignore {posargs} --check .. + yarnpkg --cwd=.dev-js run eslint ../aleksis/**/*/frontend/**/*.{js,vue} --config={toxinidir}/.eslintrc.js --resolve-plugins-relative-to=. [testenv:security] +commands_pre = + poetry install --all-extras commands = poetry show --no-dev poetry run safety check --full-report @@ -41,33 +45,25 @@ commands_pre = commands = poetry build [testenv:docs] +commands_pre = + poetry install --with docs commands = poetry run make -C docs/ html {posargs} [testenv:reformat] +commands_pre = + poetry install --only=dev + yarnpkg --cwd=.dev-js commands = - poetry run isort aleksis/ - poetry run black aleksis/ - poetry run sh -c "aleksis-admin yarn run prettier --write --ignore-path={toxinidir}/.prettierignore {toxinidir}" + poetry run ruff format aleksis/ + yarnpkg --cwd=.dev-js run prettier --ignore-path={toxinidir}/.prettierignore --write .. [testenv:makemessages] +commands_pre = + poetry install commands = poetry run aleksis-admin makemessages --no-wrap -e html,txt,py,email -i static -l ar -l de_DE -l fr -l nb_NO -l tr_TR -l la -l uk -l ru poetry run aleksis-admin makemessages --no-wrap -d djangojs -i **/node_modules -l ar -l de_DE -l fr -l nb_NO -l tr_TR -l la -l uk -l ru -[flake8] -max_line_length = 100 -exclude = migrations,tests -ignore = BLK100,E203,E231,W503,D100,D101,D102,D103,D104,D105,D106,D107,RST215,RST214,F821,F841,S106,T100,T101,DJ05 - -[isort] -profile = black -line_length = 100 -default_section = THIRDPARTY -known_first_party = aleksis -known_django = django -skip = migrations -sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER - [pytest] DJANGO_SETTINGS_MODULE = aleksis.core.settings junit_family = legacy