diff --git a/.dev-js/package.json b/.dev-js/package.json new file mode 100644 index 0000000000000000000000000000000000000000..f1a3b8b143115ffb8f0fee2d452ec2830cd888a3 --- /dev/null +++ b/.dev-js/package.json @@ -0,0 +1,14 @@ +{ + "name": "aleksis-builddeps", + "version": "1.0.0", + "dependencies": { + "@intlify/eslint-plugin-vue-i18n": "^2.0.0", + "eslint": "^8.26.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-vue": "^9.7.0", + "prettier": "^3.0.0", + "stylelint": "^15.0.0", + "stylelint-config-prettier": "^9.0.3", + "stylelint-config-standard": "^34.0.0" + } +} diff --git a/.gitignore b/.gitignore index 031b076060cbc08961e8996eaeb61c394a46efaf..79b5b76de6f6445254cbf64f7e4fb228ffdf0ab8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,29 @@ # Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] *$py.class +*.py[cod] +__pycache__/ # Distribution / packaging +*.egg +*.egg-info/ .Python +.eggs/ +.installed.cfg build/ develop-eggs/ dist/ downloads/ eggs/ -.eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ -*.egg-info/ -.installed.cfg -*.egg # Installer logs -pip-log.txt pip-delete-this-directory.txt +pip-log.txt # Translations *.mo @@ -39,22 +39,56 @@ local_settings.py # Environments .env .venv +ENV/ env/ venv/ -ENV/ # Editors *~ DEADJOE \#*# +# IntelliJ +.idea +.idea/ + +# VSCode +.vscode/ +.history/ +*.code-workspace + # Database db.sqlite3 # Sphinx docs/_build/ -# Test -.tox/ +# TeX +*.aux + +# Generated files +/cache +/node_modules +.dev-js/node_modules +/static/ +/whoosh_index/ +.vite +.dev-js/.yarn +.dev-js/.pnp.cjs +.dev-js/.pnp.loader.mjs +# Lock files poetry.lock +package-lock.json +yarn.lock +.dev-js/yarn.lock + +# Tests +.coverage +.mypy_cache/ +.tox/ +htmlcov/ + +# Data +maintenance_mode_state.txt +media/ 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/CHANGELOG.rst b/CHANGELOG.rst index c4910d94b5b1e282a9bb6b1ede5c930df33d3c6c..643ff17db9651a866c10dedddd59f1feb9d6f84f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,9 @@ and this project adheres to `Semantic Versioning`_. Unreleased ---------- +`3.0.2`_ - 2023-09-10 +--------------------- + Fixed ~~~~~ @@ -401,4 +404,4 @@ Fixed .. _3.0b0: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/3.0b0 .. _3.0: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/3.0 .. _3.0.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/3.0.1 - +.. _3.0.2: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/3.0.2 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/form_extensions.py b/aleksis/apps/chronos/form_extensions.py deleted file mode 100644 index 9cb3685319279e96401f373f97fa9e322fd0885c..0000000000000000000000000000000000000000 --- a/aleksis/apps/chronos/form_extensions.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.utils.translation import gettext as _ - -from material import Fieldset - -from aleksis.core.forms import AnnouncementForm, EditGroupForm - -AnnouncementForm.add_node_to_layout(Fieldset(_("Options for timetables"), "show_in_timetables")) -EditGroupForm.add_node_to_layout(Fieldset(_("Optional data for timetables"), "subject_id")) diff --git a/aleksis/apps/chronos/frontend/components/SelectTimetable.vue b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue index fb98d1f56c51d4261502df5d8ec2889e39e6445c..4f880bdc2bf7952017f7c92cd149e1cd5ad23c27 100644 --- a/aleksis/apps/chronos/frontend/components/SelectTimetable.vue +++ b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue @@ -68,7 +68,7 @@ export default { class="flex-grow-1" :value="type.id" > - {{ type.name }} + {{ $t(type.name) }} </v-btn> </v-btn-toggle> </v-card-text> diff --git a/aleksis/apps/chronos/frontend/components/Timetable.vue b/aleksis/apps/chronos/frontend/components/Timetable.vue index e4fca5dfb63f26597cd534315fed8f32b9b8a2de..83b13708fb6a2ed38c9cd3ee7a8c34f129e1ffee 100644 --- a/aleksis/apps/chronos/frontend/components/Timetable.vue +++ b/aleksis/apps/chronos/frontend/components/Timetable.vue @@ -20,8 +20,8 @@ export default { this.availableTimetables.find( (t) => t.objId === this.$route.params.id && - t.type.toLowerCase() === this.$route.params.type - ) + t.type.toLowerCase() === this.$route.params.type, + ), ); } }, @@ -59,7 +59,7 @@ export default { methods: { findNextTimetable(offset = 1) { const currentIndex = this.availableTimetablesIds.indexOf( - this.selected.id + this.selected.id, ); const newIndex = currentIndex + offset; if (newIndex < 0 || newIndex >= this.availableTimetablesIds.length) { diff --git a/aleksis/apps/chronos/frontend/components/timetableTypes.js b/aleksis/apps/chronos/frontend/components/timetableTypes.js index 692854f18411cc2f3ed1abf35069d28c9b18e51c..2aa2ae17decbed87abea83e54627a9067db1d712 100644 --- a/aleksis/apps/chronos/frontend/components/timetableTypes.js +++ b/aleksis/apps/chronos/frontend/components/timetableTypes.js @@ -1,13 +1,13 @@ export default { GROUP: { - name: "Groups", + name: "chronos.timetable.types.groups", id: "GROUP", icon: "mdi-account-group-outline", }, TEACHER: { - name: "Teachers", + name: "chronos.timetable.types.teachers", id: "TEACHER", icon: "mdi-account-outline", }, - ROOM: { name: "Rooms", id: "ROOM", icon: "mdi-door" }, + ROOM: { name: "chronos.timetable.types.rooms", id: "ROOM", icon: "mdi-door" }, }; diff --git a/aleksis/apps/chronos/frontend/index.js b/aleksis/apps/chronos/frontend/index.js index 30451f93c2f0dfcb769a4ab92385b50fe1bfe61c..a6a93183bca2cd7e07a5bc5a86a16a9c12e80938 100644 --- a/aleksis/apps/chronos/frontend/index.js +++ b/aleksis/apps/chronos/frontend/index.js @@ -6,6 +6,7 @@ export default { inMenu: true, titleKey: "chronos.menu_title", icon: "mdi-school-outline", + iconActive: "mdi-school", validators: [hasPersonValidator], }, props: { diff --git a/aleksis/apps/chronos/frontend/messages/de.json b/aleksis/apps/chronos/frontend/messages/de.json index 512498ccfae594033f7634e09bfba992233612fe..86148a7bac93fe2dfbd36568994a2c1af35e8dbe 100644 --- a/aleksis/apps/chronos/frontend/messages/de.json +++ b/aleksis/apps/chronos/frontend/messages/de.json @@ -12,7 +12,12 @@ "search": "Stundenpläne suchen", "prev": "Vorheriger Stundenplan", "next": "Nächster Stundenplan", - "select": "Stundenplan auswählen" + "select": "Stundenplan auswählen", + "types": { + "groups": "Gruppen", + "teachers": "Lehrkräfte", + "rooms": "Räume" + } }, "lessons": { "menu_title_daily": "Tagesstunden" diff --git a/aleksis/apps/chronos/frontend/messages/en.json b/aleksis/apps/chronos/frontend/messages/en.json index b995b749a1677cdfbdbfef9888f4bd427062717c..ee23c28f02b5b872222461653ea1e624eff54c5b 100644 --- a/aleksis/apps/chronos/frontend/messages/en.json +++ b/aleksis/apps/chronos/frontend/messages/en.json @@ -12,7 +12,12 @@ "search": "Search Timetables", "prev": "Previous Timetable", "next": "Next Timetable", - "select": "Select Timetable" + "select": "Select Timetable", + "types": { + "groups": "Groups", + "teachers": "Teachers", + "rooms": "Rooms" + } }, "lessons": { "menu_title_daily": "Daily lessons" diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py index 79bd043f4b28300ca5d6df961d43e820ae92a40b..fae25d1817462aa3f0fd0babf619bf480cb65320 100644 --- a/aleksis/apps/chronos/managers.py +++ b/aleksis/apps/chronos/managers.py @@ -1,8 +1,8 @@ +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 from django.db.models import ExpressionWrapper, F, Func, Q, QuerySet, Value from django.db.models.fields import DateField @@ -12,10 +12,18 @@ from calendarweek import CalendarWeek from polymorphic.managers import PolymorphicQuerySet from aleksis.apps.chronos.util.date import week_weekday_from_date, week_weekday_to_date -from aleksis.core.managers import DateRangeQuerySetMixin, SchoolTermRelatedQuerySet +from aleksis.apps.cursus.models import Course +from aleksis.core.managers import ( + AlekSISBaseManagerWithoutMigrations, + DateRangeQuerySetMixin, + SchoolTermRelatedQuerySet, +) from aleksis.core.models import Group, Person, Room from aleksis.core.util.core_helpers import get_site_preferences +if TYPE_CHECKING: + from .models import Holiday, LessonPeriod, ValidityRange + class ValidityRangeQuerySet(QuerySet, DateRangeQuerySetMixin): """Custom query set for validity ranges.""" @@ -66,10 +74,6 @@ class ValidityRangeRelatedQuerySet(QuerySet): return None -class CurrentSiteManager(_CurrentSiteManager): - use_in_migrations = False - - class TimetableType(Enum): """Enum for different types of timetables.""" @@ -82,7 +86,7 @@ class TimetableType(Enum): return cls.__members__.get(s.upper()) -class LessonPeriodManager(CurrentSiteManager): +class LessonPeriodManager(AlekSISBaseManagerWithoutMigrations): """Manager adding specific methods to lesson periods.""" def get_queryset(self): @@ -107,7 +111,7 @@ class LessonPeriodManager(CurrentSiteManager): ) -class LessonSubstitutionManager(CurrentSiteManager): +class LessonSubstitutionManager(AlekSISBaseManagerWithoutMigrations): """Manager adding specific methods to lesson substitutions.""" def get_queryset(self): @@ -133,7 +137,7 @@ class LessonSubstitutionManager(CurrentSiteManager): ) -class SupervisionManager(CurrentSiteManager): +class SupervisionManager(AlekSISBaseManagerWithoutMigrations): """Manager adding specific methods to supervisions.""" def get_queryset(self): @@ -151,7 +155,7 @@ class SupervisionManager(CurrentSiteManager): ) -class SupervisionSubstitutionManager(CurrentSiteManager): +class SupervisionSubstitutionManager(AlekSISBaseManagerWithoutMigrations): """Manager adding specific methods to supervision substitutions.""" def get_queryset(self): @@ -171,7 +175,7 @@ class SupervisionSubstitutionManager(CurrentSiteManager): ) -class EventManager(CurrentSiteManager): +class EventManager(AlekSISBaseManagerWithoutMigrations): """Manager adding specific methods to events.""" def get_queryset(self): @@ -190,7 +194,7 @@ class EventManager(CurrentSiteManager): ) -class ExtraLessonManager(CurrentSiteManager): +class ExtraLessonManager(AlekSISBaseManagerWithoutMigrations): """Manager adding specific methods to extra lessons.""" def get_queryset(self): @@ -208,7 +212,7 @@ class ExtraLessonManager(CurrentSiteManager): ) -class BreakManager(CurrentSiteManager): +class BreakManager(AlekSISBaseManagerWithoutMigrations): """Manager adding specific methods to breaks.""" def get_queryset(self): @@ -403,7 +407,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: @@ -636,7 +640,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: @@ -885,6 +889,12 @@ class LessonEventQuerySet(PolymorphicQuerySet): ) return self.filter(Q(rooms=room) | Q(pk__in=amended)).distinct() + def for_course(self, course: Union[int, Course]): + amended = self.filter(Q(amended_by__isnull=False) & (Q(course=course))).values_list( + "amended_by__pk", flat=True + ) + return self.filter(Q(course=course) | Q(pk__in=amended)).distinct() + def for_person(self, person: Union[int, Person]): amended = self.filter( Q(amended_by__isnull=False) & (Q(teachers=person) | Q(groups__members=person)) @@ -892,3 +902,18 @@ class LessonEventQuerySet(PolymorphicQuerySet): return self.filter( Q(teachers=person) | Q(groups__members=person) | Q(pk__in=amended) ).distinct() + + def related_to_person(self, person: Union[int, Person]): + amended = self.filter( + Q(amended_by__isnull=False) + & (Q(teachers=person) | Q(groups__members=person) | Q(groups__owners=person)) + ).values_list("amended_by__pk", flat=True) + return self.filter( + Q(teachers=person) | Q(groups__members=person) | Q(groups__owners=person) + ).distinct() + + def not_amended(self): + return self.filter(amended_by__isnull=True) + + def not_amending(self): + return self.filter(amends__isnull=True) diff --git a/aleksis/apps/chronos/migrations/0001_initial.py b/aleksis/apps/chronos/migrations/0001_initial.py index 023ea06eb613a00e8b24b0f90046d92ae906a126..758f099cade786ea7cbab3cb6a4616847fa2c2d6 100644 --- a/aleksis/apps/chronos/migrations/0001_initial.py +++ b/aleksis/apps/chronos/migrations/0001_initial.py @@ -18,7 +18,6 @@ class Migration(migrations.Migration): dependencies = [ ("core", "0001_initial"), - ("sites", "0002_alter_domain_unique"), ] operations = [ @@ -237,15 +236,6 @@ class Migration(migrations.Migration): ), ("time_start", models.TimeField(verbose_name="Start time")), ("time_end", models.TimeField(verbose_name="End time")), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ], options={ "verbose_name": "Time period", @@ -273,15 +263,6 @@ class Migration(migrations.Migration): ), ), ("date", models.DateField(verbose_name="Date")), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ( "supervision", models.ForeignKey( @@ -339,15 +320,6 @@ class Migration(migrations.Migration): "colour_bg", colorfield.fields.ColorField(default="#FFFFFF", max_length=18), ), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ], options={ "verbose_name": "Supervision area", @@ -376,16 +348,6 @@ class Migration(migrations.Migration): verbose_name="Break", ), ), - migrations.AddField( - model_name="supervision", - name="site", - field=models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), migrations.AddField( model_name="supervision", name="teacher", @@ -444,15 +406,6 @@ class Migration(migrations.Migration): verbose_name="Background colour", ), ), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ], options={ "verbose_name": "Subject", @@ -486,15 +439,6 @@ class Migration(migrations.Migration): ), ), ("name", models.CharField(max_length=255, verbose_name="Long name")), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ], options={ "verbose_name": "Room", @@ -561,15 +505,6 @@ class Migration(migrations.Migration): verbose_name="Room", ), ), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ( "subject", models.ForeignKey( @@ -623,16 +558,6 @@ class Migration(migrations.Migration): verbose_name="Room", ), ), - migrations.AddField( - model_name="lessonperiod", - name="site", - field=models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), migrations.AddField( model_name="lesson", name="periods", @@ -643,16 +568,6 @@ class Migration(migrations.Migration): verbose_name="Periods", ), ), - migrations.AddField( - model_name="lesson", - name="site", - field=models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), migrations.AddField( model_name="lesson", name="subject", @@ -697,15 +612,6 @@ class Migration(migrations.Migration): "comments", models.TextField(blank=True, null=True, verbose_name="Comments"), ), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ], options={ "verbose_name": "Holiday", @@ -771,15 +677,6 @@ class Migration(migrations.Migration): verbose_name="Room", ), ), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ( "subject", models.ForeignKey( @@ -857,15 +754,6 @@ class Migration(migrations.Migration): verbose_name="End period", ), ), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ], options={ "verbose_name": "Exam", @@ -930,15 +818,6 @@ class Migration(migrations.Migration): related_name="events", to="chronos.Room", verbose_name="Rooms" ), ), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ( "teachers", models.ManyToManyField( @@ -981,16 +860,6 @@ class Migration(migrations.Migration): verbose_name="Time period before break ends", ), ), - migrations.AddField( - model_name="break", - name="site", - field=models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), migrations.CreateModel( name="AbsenceReason", fields=[ @@ -1019,15 +888,6 @@ class Migration(migrations.Migration): blank=True, max_length=255, null=True, verbose_name="Name" ), ), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ], options={ "verbose_name": "Absence reason", @@ -1112,15 +972,6 @@ class Migration(migrations.Migration): verbose_name="Room", ), ), - ( - "site", - models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), ( "teacher", models.ForeignKey( diff --git a/aleksis/apps/chronos/migrations/0002_school_term_validity.py b/aleksis/apps/chronos/migrations/0002_school_term_validity.py index 947ccad0a618cc00f2697d17c9d4f8e4d60b0d65..9bab775a78f46c6cdfc62138d8bcc4f9d37ce405 100644 --- a/aleksis/apps/chronos/migrations/0002_school_term_validity.py +++ b/aleksis/apps/chronos/migrations/0002_school_term_validity.py @@ -43,7 +43,6 @@ def migrate_lesson(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ ("core", "0002_school_term"), - ("sites", "0002_alter_domain_unique"), ("chronos", "0001_initial"), ] @@ -186,16 +185,6 @@ class Migration(migrations.Migration): verbose_name="School term", ), ), - migrations.AddField( - model_name="validityrange", - name="site", - field=models.ForeignKey( - default=1, - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="sites.Site", - ), - ), migrations.AddField( model_name="lesson", name="validity", diff --git a/aleksis/apps/chronos/migrations/0008_unique_constraints.py b/aleksis/apps/chronos/migrations/0008_unique_constraints.py index df88345e151019bdc5830323aefb13c2e8cf2716..a009d458ab31a172ec3a24eb0e52f6d1f25f57fb 100644 --- a/aleksis/apps/chronos/migrations/0008_unique_constraints.py +++ b/aleksis/apps/chronos/migrations/0008_unique_constraints.py @@ -48,7 +48,7 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='absencereason', - constraint=models.UniqueConstraint(fields=('site_id', 'short_name'), name='unique_short_name_per_site_absence_reason'), + constraint=models.UniqueConstraint(fields=('short_name',), name='unique_short_name_per_site_absence_reason'), ), migrations.AddConstraint( model_name='break', @@ -60,19 +60,19 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='room', - constraint=models.UniqueConstraint(fields=('site_id', 'short_name'), name='unique_short_name_per_site_room'), + constraint=models.UniqueConstraint(fields=('short_name',), name='unique_short_name_per_site_room'), ), migrations.AddConstraint( model_name='subject', - constraint=models.UniqueConstraint(fields=('site_id', 'short_name'), name='unique_short_name_per_site_subject'), + constraint=models.UniqueConstraint(fields=('short_name',), name='unique_short_name_per_site_subject'), ), migrations.AddConstraint( model_name='subject', - constraint=models.UniqueConstraint(fields=('site_id', 'name'), name='unique_name_per_site'), + constraint=models.UniqueConstraint(fields=('name',), name='unique_name_per_site'), ), migrations.AddConstraint( model_name='supervisionarea', - constraint=models.UniqueConstraint(fields=('site_id', 'short_name'), name='unique_short_name_per_site_supervision_area'), + constraint=models.UniqueConstraint(fields=('short_name',), name='unique_short_name_per_site_supervision_area'), ), migrations.AddConstraint( model_name='timeperiod', diff --git a/aleksis/apps/chronos/migrations/0009_automaticplan.py b/aleksis/apps/chronos/migrations/0009_automaticplan.py index 1bcc9f3c28c5d832914c7b5c15993a1bbee85208..f35a6789cf5dc0db69cdb2e902b81c3e5b6a6538 100644 --- a/aleksis/apps/chronos/migrations/0009_automaticplan.py +++ b/aleksis/apps/chronos/migrations/0009_automaticplan.py @@ -29,7 +29,6 @@ class Migration(migrations.Migration): }, bases=('resint.livedocument',), managers=[ - ('objects', aleksis.core.managers.PolymorphicCurrentSiteManager()), ], ), ] diff --git a/aleksis/apps/chronos/migrations/0015_add_managed_by_app_label.py b/aleksis/apps/chronos/migrations/0014_add_managed_by_app_label.py similarity index 98% rename from aleksis/apps/chronos/migrations/0015_add_managed_by_app_label.py rename to aleksis/apps/chronos/migrations/0014_add_managed_by_app_label.py index d4906fcd7eefb3fffd36136c75fa8d4feee76cc8..6b38675ef2d294bf221b7e0f9a5d46d37c03a278 100644 --- a/aleksis/apps/chronos/migrations/0015_add_managed_by_app_label.py +++ b/aleksis/apps/chronos/migrations/0014_add_managed_by_app_label.py @@ -8,8 +8,7 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('sites', '0002_alter_domain_unique'), - ('chronos', '0014_lessonevent'), + ('chronos', '0013_move_room_to_core'), ] operations = [ diff --git a/aleksis/apps/chronos/migrations/0015_drop_site.py b/aleksis/apps/chronos/migrations/0015_drop_site.py new file mode 100644 index 0000000000000000000000000000000000000000..708dee3b879fe81afb3afe2be3fc0bfd69bec478 --- /dev/null +++ b/aleksis/apps/chronos/migrations/0015_drop_site.py @@ -0,0 +1,149 @@ +# Generated by Django 4.2.9 on 2024-01-09 15:08 + +import colorfield.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chronos', '0014_add_managed_by_app_label'), + ] + + operations = [ + migrations.AlterModelManagers( + name='absencereason', + managers=[ + ], + ), + migrations.AlterModelManagers( + name='subject', + managers=[ + ], + ), + migrations.AlterModelManagers( + name='supervisionarea', + managers=[ + ], + ), + migrations.RemoveConstraint( + model_name='absencereason', + name='unique_short_name_per_site_absence_reason', + ), + migrations.RemoveConstraint( + model_name='break', + name='unique_short_name_per_site_break', + ), + migrations.RemoveConstraint( + model_name='subject', + name='unique_short_name_per_site_subject', + ), + migrations.RemoveConstraint( + model_name='supervisionarea', + name='unique_short_name_per_site_supervision_area', + ), + ] + [ + migrations.RunSQL( + f"ALTER TABLE chronos_{model_name} drop column if exists site_id;" + ) for model_name in + [ + "absence", + "absencereason", + "automaticplan", + "break", + "event", + "exam", + "extralesson", + "holiday", + "lesson", + "lessonperiod", + "lessonsubstitution", + "subject", + "supervision", + "supervisionarea", + "supervisionsubstitution", + "timeperiod", + "validityrange" + ] + ] + [ + migrations.AlterField( + model_name='absence', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + preserve_default=False, + ), + migrations.AlterField( + model_name='absencereason', + name='name', + field=models.CharField(blank=True, default='', max_length=255, verbose_name='Name'), + preserve_default=False, + ), + migrations.AlterField( + model_name='absencereason', + name='short_name', + field=models.CharField(max_length=255, unique=True, verbose_name='Short name'), + ), + migrations.AlterField( + model_name='event', + name='title', + field=models.CharField(blank=True, default='', max_length=255, verbose_name='Title'), + preserve_default=False, + ), + migrations.AlterField( + model_name='extralesson', + name='comment', + field=models.CharField(blank=True, default='', max_length=255, verbose_name='Comment'), + preserve_default=False, + ), + migrations.AlterField( + model_name='holiday', + name='comments', + field=models.TextField(blank=True, default='', verbose_name='Comments'), + preserve_default=False, + ), + migrations.AlterField( + model_name='lessonsubstitution', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + preserve_default=False, + ), + migrations.AlterField( + model_name='subject', + name='colour_bg', + field=colorfield.fields.ColorField(blank=True, default='', image_field=None, max_length=25, samples=None, verbose_name='Background colour'), + ), + migrations.AlterField( + model_name='subject', + name='colour_fg', + field=colorfield.fields.ColorField(blank=True, default='', image_field=None, max_length=25, samples=None, verbose_name='Foreground colour'), + ), + migrations.AlterField( + model_name='subject', + name='short_name', + field=models.CharField(max_length=255, unique=True, verbose_name='Short name'), + ), + migrations.AlterField( + model_name='supervisionarea', + name='colour_bg', + field=colorfield.fields.ColorField(default='#FFFFFF', image_field=None, max_length=25, samples=None), + ), + migrations.AlterField( + model_name='supervisionarea', + name='colour_fg', + field=colorfield.fields.ColorField(default='#000000', image_field=None, max_length=25, samples=None), + ), + migrations.AlterField( + model_name='supervisionarea', + name='short_name', + field=models.CharField(max_length=255, unique=True, verbose_name='Short name'), + ), + migrations.AlterField( + model_name='timeperiod', + name='weekday', + field=models.PositiveSmallIntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')], verbose_name='Week day'), + ), + migrations.AddConstraint( + model_name='break', + constraint=models.UniqueConstraint(fields=('validity', 'short_name'), name='unique_short_name_per_validity_break'), + ), + ] diff --git a/aleksis/apps/chronos/migrations/0014_lessonevent.py b/aleksis/apps/chronos/migrations/0016_lessonevent.py similarity index 97% rename from aleksis/apps/chronos/migrations/0014_lessonevent.py rename to aleksis/apps/chronos/migrations/0016_lessonevent.py index 7b0e0cbac8478b850d847d067591b6885d606d2a..a89abc8906ff6cec9672d0e2a9b46aac39ed80aa 100644 --- a/aleksis/apps/chronos/migrations/0014_lessonevent.py +++ b/aleksis/apps/chronos/migrations/0016_lessonevent.py @@ -6,10 +6,9 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ("sites", "0002_alter_domain_unique"), ("core", "0051_calendarevent_and_holiday"), ("cursus", "0001_initial"), - ("chronos", "0013_move_room_to_core"), + ("chronos", "0015_drop_site"), ] operations = [ diff --git a/aleksis/apps/chronos/mixins.py b/aleksis/apps/chronos/mixins.py index 5a784579b4ca7a1adbbc32ec2c54f44cc28f18b0..8cb4dab29467236f950b5b9ff56f5fa7ab7da435 100644 --- a/aleksis/apps/chronos/mixins.py +++ b/aleksis/apps/chronos/mixins.py @@ -7,7 +7,7 @@ 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 CurrentSiteManagerWithoutMigrations +from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations from aleksis.core.mixins import ExtensibleModel from .managers import ValidityRangeRelatedQuerySet @@ -16,7 +16,7 @@ from .managers import ValidityRangeRelatedQuerySet class ValidityRangeRelatedExtensibleModel(ExtensibleModel): """Add relation to validity range.""" - objects = CurrentSiteManagerWithoutMigrations.from_queryset(ValidityRangeRelatedQuerySet)() + objects = AlekSISBaseManagerWithoutMigrations.from_queryset(ValidityRangeRelatedQuerySet)() validity = models.ForeignKey( "chronos.ValidityRange", diff --git a/aleksis/apps/chronos/model_extensions.py b/aleksis/apps/chronos/model_extensions.py index 1cbe2eca074d4e42c5ac9e558999191c652787f8..8f43350ee5a5ab019bad41f9674737e414c9d2f3 100644 --- a/aleksis/apps/chronos/model_extensions.py +++ b/aleksis/apps/chronos/model_extensions.py @@ -4,14 +4,13 @@ from typing import Optional, Union from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ -from jsonstore import BooleanField 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, Subject +from .models import Lesson, LessonPeriod from .util.change_tracker import timetable_data_changed from .util.notifications import send_notifications_for_object @@ -134,15 +133,10 @@ def previous_lesson(self, lesson_period: "LessonPeriod", day: date) -> Union["Le def for_timetables(cls): """Return all announcements that should be shown in timetable views.""" - return cls.objects.filter(show_in_timetables=True) + return cls.objects.all() Announcement.class_method(for_timetables) -Announcement.field( - show_in_timetables=BooleanField(verbose_name=_("Show announcement in timetable views?")) -) - -Group.foreign_key("subject", Subject, related_name="groups") # Dynamically add extra permissions to Group and Person models in core # Note: requires migrate afterwards diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index 11ba2702e648d0eff7e6eb5c29a3748a8f8329c5..230520c67dba948326b3bc6a2ee50c6ef0db5276 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -2,15 +2,13 @@ from __future__ import annotations import itertools -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.exceptions import PermissionDenied, ValidationError from django.core.validators import MinValueValidator from django.db import models from django.db.models import Max, Min, Q @@ -26,8 +24,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 @@ -35,7 +31,6 @@ from reversion.models import Revision, Version from aleksis.apps.chronos.managers import ( AbsenceQuerySet, BreakManager, - CurrentSiteManager, EventManager, EventQuerySet, ExtraLessonManager, @@ -64,7 +59,7 @@ from aleksis.apps.chronos.util.format import format_m2m from aleksis.apps.cursus import models as cursus_models from aleksis.apps.cursus.models import Course from aleksis.apps.resint.models import LiveDocument -from aleksis.core.managers import CurrentSiteManagerWithoutMigrations, PolymorphicCurrentSiteManager +from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations, PolymorphicBaseManager from aleksis.core.mixins import ( ExtensibleModel, GlobalPermissionModel, @@ -72,7 +67,6 @@ from aleksis.core.mixins import ( ) from aleksis.core.models import CalendarEvent, DashboardWidget, Group, Person, Room, SchoolTerm from aleksis.core.util.core_helpers import has_person -from aleksis.core.util.pdf import generate_pdf_from_template class ValidityRange(ExtensibleModel): @@ -81,7 +75,7 @@ class ValidityRange(ExtensibleModel): This is used to link data to a validity range. """ - objects = CurrentSiteManagerWithoutMigrations.from_queryset(ValidityRangeQuerySet)() + objects = AlekSISBaseManagerWithoutMigrations.from_queryset(ValidityRangeQuerySet)() school_term = models.ForeignKey( SchoolTerm, @@ -96,7 +90,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: @@ -113,12 +107,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: @@ -135,7 +128,6 @@ class ValidityRange(ExtensibleModel): verbose_name = _("Validity range") verbose_name_plural = _("Validity ranges") constraints = [ - # Heads up: Uniqueness per term implies uniqueness per site models.UniqueConstraint( fields=["school_term", "date_start", "date_end"], name="unique_dates_per_term" ), @@ -161,14 +153,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: @@ -179,37 +171,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) @@ -229,7 +210,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() @@ -239,15 +220,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)) @@ -258,7 +239,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. @@ -285,12 +266,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 @@ -313,7 +294,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) @@ -330,7 +311,6 @@ class TimePeriod(ValidityRangeRelatedExtensibleModel): class Meta: constraints = [ - # Heads up: Uniqueness per validity range implies validity per site models.UniqueConstraint( fields=["weekday", "period", "validity"], name="unique_period_per_range" ), @@ -342,7 +322,7 @@ class TimePeriod(ValidityRangeRelatedExtensibleModel): class Subject(ExtensibleModel): - short_name = models.CharField(verbose_name=_("Short name"), max_length=255) + short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True) name = models.CharField(verbose_name=_("Long name"), max_length=255) colour_fg = ColorField(verbose_name=_("Foreground colour"), blank=True) @@ -355,11 +335,6 @@ class Subject(ExtensibleModel): ordering = ["name", "short_name"] verbose_name = _("Subject") verbose_name_plural = _("Subjects") - constraints = [ - models.UniqueConstraint( - fields=["site_id", "short_name"], name="unique_short_name_per_site_subject" - ), - ] class Lesson(ValidityRangeRelatedExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin): @@ -411,7 +386,6 @@ class Lesson(ValidityRangeRelatedExtensibleModel, GroupPropertiesMixin, TeacherP return f"{format_m2m(self.groups)}, {self.subject.short_name}, {format_m2m(self.teachers)}" class Meta: - # Heads up: Link to periods implies uniqueness per site ordering = ["validity__date_start", "subject"] verbose_name = _("Lesson") verbose_name_plural = _("Lessons") @@ -452,7 +426,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: @@ -488,7 +462,6 @@ class LessonSubstitution(ExtensibleModel, TeacherPropertiesMixin, WeekRelatedMix check=~Q(cancelled=True, subject__isnull=False), name="either_substituted_or_cancelled", ), - # Heads up: Link to period implies uniqueness per site models.UniqueConstraint( fields=["lesson_period", "week", "year"], name="unique_period_per_week" ), @@ -527,7 +500,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 @@ -538,7 +511,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 @@ -552,7 +525,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: @@ -581,7 +554,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:: @@ -590,7 +563,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:: @@ -599,7 +572,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])) @@ -630,7 +603,6 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) return True class Meta: - # Heads up: Link to period implies uniqueness per site ordering = [ "lesson__validity__date_start", "period__weekday", @@ -685,8 +657,8 @@ 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) + short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True) + name = models.CharField(verbose_name=_("Name"), blank=True, max_length=255) def __str__(self): if self.name: @@ -697,15 +669,10 @@ class AbsenceReason(ExtensibleModel): class Meta: verbose_name = _("Absence reason") verbose_name_plural = _("Absence reasons") - constraints = [ - models.UniqueConstraint( - fields=["site_id", "short_name"], name="unique_short_name_per_site_absence_reason" - ), - ] class Absence(SchoolTermRelatedExtensibleModel): - objects = CurrentSiteManager.from_queryset(AbsenceQuerySet)() + objects = AlekSISBaseManagerWithoutMigrations.from_queryset(AbsenceQuerySet)() reason = models.ForeignKey( "AbsenceReason", @@ -757,7 +724,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: @@ -770,7 +737,6 @@ class Absence(SchoolTermRelatedExtensibleModel): return _("Unknown absence") class Meta: - # Heads up: Link to period implies uniqueness per site ordering = ["date_start"] indexes = [models.Index(fields=["date_start", "date_end"])] verbose_name = _("Absence") @@ -803,7 +769,6 @@ class Exam(SchoolTermRelatedExtensibleModel): comment = models.TextField(verbose_name=_("Comment"), blank=True) class Meta: - # Heads up: Link to period implies uniqueness per site ordering = ["date"] indexes = [models.Index(fields=["date"])] verbose_name = _("Exam") @@ -811,12 +776,12 @@ class Exam(SchoolTermRelatedExtensibleModel): class Holiday(ExtensibleModel): - objects = CurrentSiteManager.from_queryset(HolidayQuerySet)() + objects = AlekSISBaseManagerWithoutMigrations.from_queryset(HolidayQuerySet)() 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 @@ -824,7 +789,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] @@ -832,7 +797,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) @@ -860,7 +825,7 @@ class Holiday(ExtensibleModel): class SupervisionArea(ExtensibleModel): - short_name = models.CharField(verbose_name=_("Short name"), max_length=255) + short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True) name = models.CharField(verbose_name=_("Long name"), max_length=255) colour_fg = ColorField(default="#000000") colour_bg = ColorField() @@ -872,11 +837,6 @@ class SupervisionArea(ExtensibleModel): ordering = ["name"] verbose_name = _("Supervision area") verbose_name_plural = _("Supervision areas") - constraints = [ - models.UniqueConstraint( - fields=["site_id", "short_name"], name="unique_short_name_per_site_supervision_area" - ), - ] class Break(ValidityRangeRelatedExtensibleModel): @@ -923,7 +883,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_ @@ -940,7 +900,7 @@ class Break(ValidityRangeRelatedExtensibleModel): verbose_name_plural = _("Breaks") constraints = [ models.UniqueConstraint( - fields=["validity", "short_name"], name="unique_short_name_per_site_break" + fields=["validity", "short_name"], name="unique_short_name_per_validity_break" ), ] @@ -975,9 +935,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 @@ -1051,7 +1009,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) @@ -1169,7 +1127,6 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope ), timezone.datetime.combine(self.date_end, self.period_to.time_end) class Meta: - # Heads up: Link to period implies uniqueness per site ordering = ["date_start"] indexes = [ models.Index( @@ -1222,7 +1179,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", @@ -1256,7 +1213,6 @@ class ExtraLesson( ), timezone.datetime.combine(self.date, self.period.time_end) class Meta: - # Heads up: Link to period implies uniqueness per site verbose_name = _("Extra lesson") verbose_name_plural = _("Extra lessons") indexes = [models.Index(fields=["week", "year"], name="extra_lesson_week_year")] @@ -1296,7 +1252,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 @@ -1371,7 +1327,7 @@ class LessonEvent(CalendarEvent): name = "lesson" verbose_name = _("Lessons") - objects = PolymorphicCurrentSiteManager.from_queryset(LessonEventQuerySet)() + objects = PolymorphicBaseManager.from_queryset(LessonEventQuerySet)() title = models.CharField(verbose_name=_("Name"), max_length=255, blank=True) @@ -1418,34 +1374,34 @@ class LessonEvent(CalendarEvent): ) @property - def actual_groups(self: "LessonEvent"): + def actual_groups(self: LessonEvent): return self.groups.all() if self.amends else self.real_amends.groups.all() @property - def all_members(self: "LessonEvent") -> list[Person]: + def all_members(self: LessonEvent) -> list[Person]: return list(itertools.chain(*[list(g.members.all()) for g in self.actual_groups])) @property - def all_teachers(self: "LessonEvent") -> list[Person]: + def all_teachers(self: LessonEvent) -> list[Person]: all_teachers = list(self.teachers.all()) if self.amends: all_teachers += list(self.real_amends.teachers.all()) return all_teachers @property - def group_names(self: "LessonEvent") -> str: + def group_names(self: LessonEvent) -> str: return ", ".join([g.name for g in self.actual_groups]) @property - def teacher_names(self: "LessonEvent") -> str: + def teacher_names(self: LessonEvent) -> str: return ", ".join([t.full_name for t in self.teachers.all()]) @property - def room_names(self: "LessonEvent") -> str: + def room_names(self: LessonEvent) -> str: return ", ".join([r.name for r in self.rooms.all()]) @property - def room_names_with_amends(self: "LessonEvent") -> str: + def room_names_with_amends(self: LessonEvent) -> str: my_room_names = self.room_names amended_room_names = self.real_amends.room_names if self.amends else "" @@ -1456,7 +1412,7 @@ class LessonEvent(CalendarEvent): return my_room_names @property - def teacher_names_with_amends(self: "LessonEvent") -> str: + def teacher_names_with_amends(self: LessonEvent) -> str: my_teacher_names = self.teacher_names amended_teacher_names = self.real_amends.teacher_names if self.amends else "" @@ -1467,8 +1423,8 @@ class LessonEvent(CalendarEvent): return my_teacher_names @property - def subject_name_with_amends(self: "LessonEvent") -> str: - my_subject = self.subject.name if self.subject else "" + def subject_name_with_amends(self: LessonEvent) -> str: + my_subject = self.subject.name amended_subject = self.real_amends.subject.name if self.amends else "" if my_subject and amended_subject: @@ -1480,14 +1436,14 @@ class LessonEvent(CalendarEvent): return _("Lesson") @property - def real_amends(self: "LessonEvent") -> "LessonEvent": + def real_amends(self: LessonEvent) -> LessonEvent: # FIXME THIS IS AWFUL SLOW if self.amends: return LessonEvent.objects.get(pk=self.amends.pk) return self @classmethod - def value_title(cls, reference_object: "LessonEvent", request) -> str: + def value_title(cls, reference_object: LessonEvent, request) -> str: """Get the title of the event.""" if reference_object.title: return reference_object.title @@ -1506,11 +1462,11 @@ class LessonEvent(CalendarEvent): return _("Lesson") @classmethod - def value_description(cls, reference_object: "LessonEvent", request) -> str: + def value_description(cls, reference_object: LessonEvent, request) -> str: return render_to_string("chronos/lesson_event_description.txt", {"event": reference_object}) @classmethod - def value_color(cls, reference_object: "LessonEvent", request) -> str: + def value_color(cls, reference_object: LessonEvent, request) -> str: """Get the color of the event.""" if reference_object.cancelled: return "#eeeeee" @@ -1521,25 +1477,25 @@ class LessonEvent(CalendarEvent): return super().value_color(reference_object, request) @classmethod - def value_attendee(cls, reference_object: "LessonEvent", request) -> str: + def value_attendee(cls, reference_object: LessonEvent, request) -> str: """Get the attendees of the event.""" attendees = [t.get_vcal_address(role="CHAIR") for t in reference_object.teachers.all()] return [a for a in attendees if a] @classmethod - def value_location(cls, reference_object: "LessonEvent", request) -> str: + def value_location(cls, reference_object: LessonEvent, request) -> str: """Get the location of the event.""" return ", ".join([r.name for r in reference_object.rooms.all()]) @classmethod - def value_status(cls, reference_object: "LessonEvent", request) -> str: + def value_status(cls, reference_object: LessonEvent, request) -> str: """Get the status of the event.""" if reference_object.cancelled: return "CANCELLED" return "CONFIRMED" @classmethod - def value_meta(cls, reference_object: "LessonEvent", request) -> str: + def value_meta(cls, reference_object: LessonEvent, request) -> str: """Get the meta of the event.""" real_amends = reference_object.real_amends @@ -1584,17 +1540,40 @@ class LessonEvent(CalendarEvent): def get_objects(cls, request, params=None) -> Iterable: """Return all objects that should be included in the calendar.""" objs = super().get_objects(request, params).not_instance_of(SupervisionEvent) + + if not has_person(request.user): + raise PermissionDenied() + if params: obj_id = int(params.get("id", 0)) - type = params.get("type", None) + type_ = params.get("type", None) + not_amended = params.get("not_amended", False) + not_amending = params.get("not_amending", False) + own = params.get("own", False) + + if not_amended: + objs = objs.not_amended() + + if not_amending: + objs = objs.not_amending() + + if "own" in params: + if own: + objs = objs.for_person(request.user.person) + else: + objs = objs.related_to_person(request.user.person) - if type and obj_id: - if type == "TEACHER": + if type_ and obj_id: + if type_ == "TEACHER": return objs.for_teacher(obj_id) - elif type == "GROUP": + elif type_ == "GROUP": return objs.for_group(obj_id) - elif type == "ROOM": + elif type_ == "ROOM": return objs.for_room(obj_id) + elif type_ == "COURSE": + return objs.for_course(obj_id) + elif "own" in params: + return objs return objs.for_person(request.user.person) class Meta: @@ -1606,16 +1585,16 @@ class SupervisionEvent(LessonEvent): name = "supervision" verbose_name = _("Supervisions") - objects = PolymorphicCurrentSiteManager.from_queryset(LessonEventQuerySet)() + objects = PolymorphicBaseManager.from_queryset(LessonEventQuerySet)() @classmethod - def value_title(cls, reference_object: "LessonEvent", request) -> str: + def value_title(cls, reference_object: LessonEvent, request) -> str: """Get the title of the event.""" return _("Supervision: {}").format(reference_object.room_names) @classmethod - def value_description(cls, reference_object: "LessonEvent", request) -> str: + def value_description(cls, reference_object: LessonEvent, request) -> str: return render_to_string( "chronos/supervision_event_description.txt", {"event": reference_object} ) @@ -1626,13 +1605,13 @@ class SupervisionEvent(LessonEvent): objs = cls.objects.instance_of(cls) if params: obj_id = int(params.get("id", 0)) - type = params.get("type", None) + type_ = params.get("type", None) - if type and obj_id: - if type == "TEACHER": + if type_ and obj_id: + if type_ == "TEACHER": return objs.for_teacher(obj_id) - elif type == "GROUP": + elif type_ == "GROUP": return objs.for_group(obj_id) - elif type == "ROOM": + elif type_ == "ROOM": return objs.for_room(obj_id) return objs.for_person(request.user.person) diff --git a/aleksis/apps/chronos/schema/__init__.py b/aleksis/apps/chronos/schema/__init__.py index f7cc25ff46c7fe7018876cc782fb6ad4e92e4eca..0bdbe2dbb8988d04fb351fb8758e709cbdea1aeb 100644 --- a/aleksis/apps/chronos/schema/__init__.py +++ b/aleksis/apps/chronos/schema/__init__.py @@ -139,6 +139,15 @@ class TimetableObjectType(graphene.ObjectType): return f"{root.type.value}-{root.id}" +class LessonEventType(DjangoObjectType): + class Meta: + model = LessonEvent + fields = ("id",) + filter_fields = { + "id": ["exact", "lte", "gte"], + } + + class Query(graphene.ObjectType): timetable_teachers = graphene.List(TimetablePersonType) timetable_groups = graphene.List(TimetableGroupType) 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/templates/chronos/lesson_event_description.txt b/aleksis/apps/chronos/templates/chronos/lesson_event_description.txt index 573c92354535f0f4a5ed029e9efab63bc71c3639..0b9a2edc8f0e32ccdb6bc9c4181bfbbff35a26eb 100644 --- a/aleksis/apps/chronos/templates/chronos/lesson_event_description.txt +++ b/aleksis/apps/chronos/templates/chronos/lesson_event_description.txt @@ -3,4 +3,4 @@ {% trans "Teachers" %}: {{ event.teacher_names_with_amends|default:"–" }} {% trans "Rooms" %}: {{ event.room_names_with_amends|default:"–" }}{% if event.comment %} -{{ event.comment }}{% endif %} \ No newline at end of file +{{ event.comment }}{% endif %} diff --git a/aleksis/apps/chronos/templates/chronos/supervision_event_description.txt b/aleksis/apps/chronos/templates/chronos/supervision_event_description.txt index d2f311f240e5cf527033bc08de15869085dc0ee5..dee2e549c16c765884c47a28d3aa5db311be6dee 100644 --- a/aleksis/apps/chronos/templates/chronos/supervision_event_description.txt +++ b/aleksis/apps/chronos/templates/chronos/supervision_event_description.txt @@ -1,2 +1,2 @@ {% load i18n %}{% trans "Teachers" %}: {{ event.teacher_names }} -{% trans "Areas" %}: {{ event.room_names }} \ No newline at end of file +{% trans "Areas" %}: {{ event.room_names }} diff --git a/aleksis/apps/chronos/tests/test_notifications.py b/aleksis/apps/chronos/tests/test_notifications.py index 9180bc22f997093bd24d594083efc2b0c10969af..2e2393cb2b3fa962613615115ce57966b538af7a 100644 --- a/aleksis/apps/chronos/tests/test_notifications.py +++ b/aleksis/apps/chronos/tests/test_notifications.py @@ -32,26 +32,26 @@ class NotificationTests(TransactionTestCase): ) self.teacher_a = Person.objects.create( - first_name="Teacher", last_name="A", short_name="A", email="test@example.org" + first_name="Teacher", last_name="A", short_name="A", email="test1@example.org" ) self.teacher_b = Person.objects.create( - first_name="Teacher", last_name="B", short_name="B", email="test@example.org" + first_name="Teacher", last_name="B", short_name="B", email="test2@example.org" ) self.student_a = Person.objects.create( - first_name="Student", last_name="A", email="test@example.org" + first_name="Student", last_name="A", email="test3@example.org" ) self.student_b = Person.objects.create( - first_name="Student", last_name="B", email="test@example.org" + first_name="Student", last_name="B", email="test4@example.org" ) self.student_c = Person.objects.create( - first_name="Student", last_name="C", email="test@example.org" + first_name="Student", last_name="C", email="test5@example.org" ) self.student_d = Person.objects.create( - first_name="Student", last_name="D", email="test@example.org" + first_name="Student", last_name="D", email="test6@example.org" ) self.student_e = Person.objects.create( - first_name="Student", last_name="E", email="test@example.org" + first_name="Student", last_name="E", email="test7@example.org" ) self.group_a = Group.objects.create( 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 5664c0b97e13e68ab18120a2cadfd39396eaa8a4..1f299424047084ceeec346a22b9e9c1aa00127ed 100644 --- a/aleksis/apps/chronos/util/chronos_helpers.py +++ b/aleksis/apps/chronos/util/chronos_helpers.py @@ -106,10 +106,7 @@ def get_classes(user: "User"): lessons_count=Count("lesson_events"), child_lessons_count=Count("child_groups__lesson_events"), ) - .filter( - Q(lessons_count__gt=0, parent_groups=None) - | Q(child_lessons_count__gt=0, parent_groups=None) - ) + .filter(Q(lessons_count__gt=0) | Q(child_lessons_count__gt=0)) .order_by("short_name", "name") ) @@ -189,7 +186,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: @@ -199,9 +196,7 @@ def get_substitutions_context_data( subs = build_substitutions_list(day) day_contexts[day]["substitutions"] = subs - day_contexts[day]["announcements"] = ( - Announcement.for_timetables().on_date(day).filter(show_in_timetables=True) - ) + day_contexts[day]["announcements"] = Announcement.for_timetables().on_date(day) if show_header_box: subs = LessonSubstitution.objects.on_day(day).order_by( 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/docs/conf.py b/docs/conf.py index 743bdf5de706d498d24775f49dd925ab819d70dd..019586d7d6674aac00e67f8fd079925ec2e29aa1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ author = "The AlekSIS Team" # The short X.Y version version = "3.0" # The full version, including alpha/beta/rc tags -release = "3.0.2.dev0" +release = "3.0.3.dev0" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 32832395ff78f72b83016625a3bfd35612da15bf..b399b79ca2354aa14bbdbd02b25003d5f05852b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "AlekSIS-App-Chronos" -version = "4.0.dev1" +version = "4.0.0.dev1" packages = [ { include = "aleksis" } ] @@ -47,35 +47,24 @@ priority = "primary" name = "gitlab" url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple" priority = "supplemental" + [tool.poetry.dependencies] -python = "^3.9" +python = "^3.10" calendarweek = "^0.5.0" -aleksis-core = "^4.0.0.dev0" -aleksis-app-resint = "^4.0.dev0" +aleksis-core = "^4.0.0.dev2" +aleksis-app-resint = "^4.0.0.dev1" aleksis-app-cursus = "^0.1.dev0" + [tool.poetry.plugins."aleksis.app"] chronos = "aleksis.apps.chronos.apps:ChronosConfig" [tool.poetry.group.dev.dependencies] django-stubs = "^4.2" - safety = "^2.3.5" -flake8 = "^6.0.0" -flake8-django = "~1.2" -flake8-fixme = "^1.1.1" -flake8-bandit = "^4.1.1" -flake8-builtins = "^2.0.0" -flake8-docstrings = "^1.5.0" -flake8-rst-docstrings = "^0.3.0" - -black = ">=21.0" -flake8-black = "^0.3.0" - -isort = "^5.0.0" -flake8-isort = "^6.0.0" +ruff = "^0.1.5" curlylint = "^0.13.0" @@ -98,10 +87,20 @@ sphinxcontrib-svg2pdfconverter = "^1.1.1" sphinx-autodoc-typehints = "^1.7" sphinx_material = "^0.0.35" -[tool.black] +[tool.ruff] +exclude = ["migrations", "tests"] line-length = 100 -exclude = "/migrations/" +[tool.ruff.lint] +select = ["E", "F", "UP", "B", "SIM", "I", "DJ", "A", "S"] +ignore = ["UP034", "UP015", "B028"] + +[tool.ruff.isort] +known-first-party = ["aleksis"] +section-order = ["future", "standard-library", "django", "third-party", "first-party", "local-folder"] + +[tool.ruff.isort.sections] +django = ["django"] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" 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