From 31630982cbfedfb7c24cb0b3c981a3981f06d6ca Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Mon, 1 May 2023 15:21:03 +0200
Subject: [PATCH] Fix several things in models

---
 aleksis/apps/lesrooster/admin.py              |   2 +-
 aleksis/apps/lesrooster/managers.py           |   7 +
 .../lesrooster/migrations/0001_initial.py     | 358 ++++++++++++++++++
 ...lidityrange_managers_slot_name_and_more.py |  42 ++
 aleksis/apps/lesrooster/model_extensions.py   |   9 +
 aleksis/apps/lesrooster/models.py             |  43 ++-
 6 files changed, 442 insertions(+), 19 deletions(-)
 create mode 100644 aleksis/apps/lesrooster/managers.py
 create mode 100644 aleksis/apps/lesrooster/migrations/0001_initial.py
 create mode 100644 aleksis/apps/lesrooster/migrations/0002_alter_validityrange_managers_slot_name_and_more.py
 create mode 100644 aleksis/apps/lesrooster/model_extensions.py

diff --git a/aleksis/apps/lesrooster/admin.py b/aleksis/apps/lesrooster/admin.py
index 72775563..7d87fd0a 100644
--- a/aleksis/apps/lesrooster/admin.py
+++ b/aleksis/apps/lesrooster/admin.py
@@ -17,7 +17,7 @@ admin.site.register(ValidityRange, ValidityRangeAdmin)
 
 
 class SlotAdmin(admin.ModelAdmin):
-    list_display = ("school_term", "weekday", "period", "time_start", "time_end")
+    list_display = ("validity_range", "name", "weekday", "period", "time_start", "time_end")
     list_display_links = ("weekday", "period")
 
 
diff --git a/aleksis/apps/lesrooster/managers.py b/aleksis/apps/lesrooster/managers.py
new file mode 100644
index 00000000..c8207723
--- /dev/null
+++ b/aleksis/apps/lesrooster/managers.py
@@ -0,0 +1,7 @@
+from django.db.models import QuerySet
+
+from aleksis.core.managers import DateRangeQuerySetMixin
+
+
+class ValidityRangeQuerySet(QuerySet, DateRangeQuerySetMixin):
+    """Custom query set for validity ranges."""
diff --git a/aleksis/apps/lesrooster/migrations/0001_initial.py b/aleksis/apps/lesrooster/migrations/0001_initial.py
new file mode 100644
index 00000000..2088ef43
--- /dev/null
+++ b/aleksis/apps/lesrooster/migrations/0001_initial.py
@@ -0,0 +1,358 @@
+# Generated by Django 4.1.6 on 2023-02-10 19:54
+
+import aleksis.core.managers
+import django.contrib.sites.managers
+from django.db import migrations, models
+import django.db.models.deletion
+import recurrence.fields
+
+
+class Migration(migrations.Migration):
+    initial = True
+
+    dependencies = [
+        ("contenttypes", "0002_remove_content_type_name"),
+        ("cursus", "0001_initial"),
+        ("sites", "0002_alter_domain_unique"),
+        ("core", "0048_delete_personalicalurl"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="Slot",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("extended_data", models.JSONField(default=dict, editable=False)),
+                (
+                    "weekday",
+                    models.PositiveSmallIntegerField(
+                        choices=[
+                            (0, "Monday"),
+                            (1, "Tuesday"),
+                            (2, "Wednesday"),
+                            (3, "Thursday"),
+                            (4, "Friday"),
+                            (5, "Saturday"),
+                            (6, "Sunday"),
+                        ],
+                        verbose_name="Week day",
+                    ),
+                ),
+                (
+                    "period",
+                    models.PositiveSmallIntegerField(
+                        blank=True, null=True, verbose_name="Number of period"
+                    ),
+                ),
+                ("time_start", models.TimeField(verbose_name="Start time")),
+                ("time_end", models.TimeField(verbose_name="End time")),
+                (
+                    "polymorphic_ctype",
+                    models.ForeignKey(
+                        editable=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="polymorphic_%(app_label)s.%(class)s_set+",
+                        to="contenttypes.contenttype",
+                    ),
+                ),
+                (
+                    "site",
+                    models.ForeignKey(
+                        default=1,
+                        editable=False,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="+",
+                        to="sites.site",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Slot",
+                "verbose_name_plural": "Slots",
+                "ordering": ["weekday", "period"],
+            },
+            managers=[
+                ("objects", aleksis.core.managers.PolymorphicCurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name="Break",
+            fields=[
+                (
+                    "slot_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="lesrooster.slot",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Break",
+                "verbose_name_plural": "Breaks",
+            },
+            bases=("lesrooster.slot",),
+            managers=[
+                ("objects", aleksis.core.managers.PolymorphicCurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name="ValidityRange",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("extended_data", models.JSONField(default=dict, editable=False)),
+                (
+                    "name",
+                    models.CharField(blank=True, max_length=255, verbose_name="Name"),
+                ),
+                ("date_start", models.DateField(verbose_name="Start date")),
+                ("date_end", models.DateField(verbose_name="End date")),
+                (
+                    "school_term",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="lr_validity_ranges",
+                        to="core.schoolterm",
+                        verbose_name="School term",
+                    ),
+                ),
+                (
+                    "site",
+                    models.ForeignKey(
+                        default=1,
+                        editable=False,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="+",
+                        to="sites.site",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Validity range",
+                "verbose_name_plural": "Validity ranges",
+            },
+            managers=[
+                ("objects", django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name="Supervision",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("extended_data", models.JSONField(default=dict, editable=False)),
+                (
+                    "rooms",
+                    models.ManyToManyField(
+                        related_name="lr_supervisions",
+                        to="core.room",
+                        verbose_name="Rooms",
+                    ),
+                ),
+                (
+                    "site",
+                    models.ForeignKey(
+                        default=1,
+                        editable=False,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="+",
+                        to="sites.site",
+                    ),
+                ),
+                (
+                    "teachers",
+                    models.ManyToManyField(
+                        related_name="lr_supervisions",
+                        to="core.person",
+                        verbose_name="Teachers",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Supervision",
+                "verbose_name_plural": "Supervisions",
+            },
+            managers=[
+                ("objects", django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.AddField(
+            model_name="slot",
+            name="validity_range",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="lr_slots",
+                to="lesrooster.validityrange",
+                verbose_name="Linked validity range",
+            ),
+        ),
+        migrations.CreateModel(
+            name="Lesson",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("extended_data", models.JSONField(default=dict, editable=False)),
+                (
+                    "recurrence",
+                    recurrence.fields.RecurrenceField(
+                        blank=True,
+                        help_text="Leave empty for a single lesson.",
+                        null=True,
+                        verbose_name="Recurrence",
+                    ),
+                ),
+                (
+                    "course",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="cursus.course",
+                        verbose_name="Course",
+                    ),
+                ),
+                (
+                    "rooms",
+                    models.ManyToManyField(
+                        blank=True,
+                        null=True,
+                        related_name="lr_lessons",
+                        to="core.room",
+                        verbose_name="Rooms",
+                    ),
+                ),
+                (
+                    "site",
+                    models.ForeignKey(
+                        default=1,
+                        editable=False,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="+",
+                        to="sites.site",
+                    ),
+                ),
+                (
+                    "slot_end",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="+",
+                        to="lesrooster.slot",
+                        verbose_name="End slot",
+                    ),
+                ),
+                (
+                    "slot_start",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="+",
+                        to="lesrooster.slot",
+                        verbose_name="Start slot",
+                    ),
+                ),
+                (
+                    "subject",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="lr_lessons",
+                        to="cursus.subject",
+                        verbose_name="Subject",
+                    ),
+                ),
+                (
+                    "teachers",
+                    models.ManyToManyField(
+                        blank=True,
+                        null=True,
+                        related_name="lr_lessons_as_teacher",
+                        to="core.person",
+                        verbose_name="Teachers",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Lesson",
+                "verbose_name_plural": "Lessons",
+                "ordering": [
+                    "slot_start__validity_range__date_start",
+                    "slot_start__weekday",
+                    "slot_start__time_start",
+                    "subject",
+                ],
+            },
+            managers=[
+                ("objects", django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.AddIndex(
+            model_name="validityrange",
+            index=models.Index(
+                fields=["date_start", "date_end"], name="lr_validity_date_start_end"
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name="validityrange",
+            constraint=models.UniqueConstraint(
+                fields=("school_term", "date_start", "date_end"),
+                name="lr_unique_dates_per_term",
+            ),
+        ),
+        migrations.AddField(
+            model_name="supervision",
+            name="slot",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="lr_supervisions",
+                to="lesrooster.break",
+                verbose_name="Slot",
+            ),
+        ),
+        migrations.AddIndex(
+            model_name="slot",
+            index=models.Index(
+                fields=["time_start", "time_end"], name="lesrooster__time_st_8ec433_idx"
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name="slot",
+            constraint=models.UniqueConstraint(
+                fields=("weekday", "period", "validity_range"),
+                name="lr_unique_period_per_range",
+            ),
+        ),
+    ]
diff --git a/aleksis/apps/lesrooster/migrations/0002_alter_validityrange_managers_slot_name_and_more.py b/aleksis/apps/lesrooster/migrations/0002_alter_validityrange_managers_slot_name_and_more.py
new file mode 100644
index 00000000..f97132f1
--- /dev/null
+++ b/aleksis/apps/lesrooster/migrations/0002_alter_validityrange_managers_slot_name_and_more.py
@@ -0,0 +1,42 @@
+# Generated by Django 4.1.6 on 2023-02-12 18:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("core", "0050_alter_custommenuitem_icon_alter_notification_icon"),
+        ("lesrooster", "0001_initial"),
+    ]
+
+    operations = [
+        migrations.AlterModelManagers(
+            name="validityrange",
+            managers=[],
+        ),
+        migrations.AddField(
+            model_name="slot",
+            name="name",
+            field=models.CharField(blank=True, max_length=255, verbose_name="Name"),
+        ),
+        migrations.AlterField(
+            model_name="lesson",
+            name="rooms",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="lr_lessons",
+                to="core.room",
+                verbose_name="Rooms",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="lesson",
+            name="teachers",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="lr_lessons_as_teacher",
+                to="core.person",
+                verbose_name="Teachers",
+            ),
+        ),
+    ]
diff --git a/aleksis/apps/lesrooster/model_extensions.py b/aleksis/apps/lesrooster/model_extensions.py
new file mode 100644
index 00000000..7401fac7
--- /dev/null
+++ b/aleksis/apps/lesrooster/model_extensions.py
@@ -0,0 +1,9 @@
+from django.utils.translation import gettext as _
+
+from jsonstore import BooleanField
+
+from aleksis.core.models import Room
+
+Room.field(
+    is_supervision_area=BooleanField(verbose_name=_("Is supervision area"), null=True, blank=True)
+)
diff --git a/aleksis/apps/lesrooster/models.py b/aleksis/apps/lesrooster/models.py
index 2fcfc4e4..b1130e25 100644
--- a/aleksis/apps/lesrooster/models.py
+++ b/aleksis/apps/lesrooster/models.py
@@ -13,18 +13,23 @@ from calendarweek.django import i18n_day_abbr_choices_lazy, i18n_day_name_choice
 from recurrence.fields import RecurrenceField
 
 from aleksis.apps.cursus.models import Course, Subject
+from aleksis.core.managers import CurrentSiteManagerWithoutMigrations
 from aleksis.core.mixins import ExtensibleModel, ExtensiblePolymorphicModel
 from aleksis.core.models import Person, Room, SchoolTerm
 
+from .managers import ValidityRangeQuerySet
+
 
 class ValidityRange(ExtensibleModel):
     """A validity range is a date range in which certain data are valid."""
 
+    objects = CurrentSiteManagerWithoutMigrations.from_queryset(ValidityRangeQuerySet)()
+
     school_term = models.ForeignKey(
         SchoolTerm,
         on_delete=models.CASCADE,
         verbose_name=_("School term"),
-        related_name="validity_ranges",
+        related_name="lr_validity_ranges",
     )
     name = models.CharField(verbose_name=_("Name"), max_length=255, blank=True)
 
@@ -75,11 +80,11 @@ class ValidityRange(ExtensibleModel):
         constraints = [
             # Heads up: Uniqueness per term implies uniqueness per site
             models.UniqueConstraint(
-                fields=["school_term", "date_start", "date_end"], name="unique_dates_per_term"
+                fields=["school_term", "date_start", "date_end"], name="lr_unique_dates_per_term"
             ),
         ]
         indexes = [
-            models.Index(fields=["date_start", "date_end"], name="validity_date_start_date_end")
+            models.Index(fields=["date_start", "date_end"], name="lr_validity_date_start_end")
         ]
 
 
@@ -90,12 +95,14 @@ class Slot(ExtensiblePolymorphicModel):
     WEEKDAY_CHOICES_SHORT = i18n_day_abbr_choices_lazy()
 
     validity_range = models.ForeignKey(
-        "chronos.ValidityRange",
+        ValidityRange,
         on_delete=models.CASCADE,
-        related_name="slots",
+        related_name="lr_slots",
         verbose_name=_("Linked validity range"),
     )
 
+    name = models.CharField(verbose_name=_("Name"), max_length=255, blank=True)
+
     weekday = models.PositiveSmallIntegerField(
         verbose_name=_("Week day"), choices=i18n_day_name_choices_lazy()
     )
@@ -107,7 +114,9 @@ class Slot(ExtensiblePolymorphicModel):
     time_end = models.TimeField(verbose_name=_("End time"))
 
     def __str__(self) -> str:
-        if self.period:
+        if self.name:
+            suffix = self.name
+        elif self.period:
             suffix = f"{self.period}."
         else:
             suffix = f"{time_format(self.time_start)}–{time_format(self.time_end)}"
@@ -137,7 +146,7 @@ class Slot(ExtensiblePolymorphicModel):
         constraints = [
             # Heads up: Uniqueness per validity range implies validity per site
             models.UniqueConstraint(
-                fields=["weekday", "period", "validity"], name="unique_period_per_range"
+                fields=["weekday", "period", "validity_range"], name="lr_unique_period_per_range"
             ),
         ]
         ordering = ["weekday", "period"]
@@ -161,13 +170,13 @@ class Lesson(ExtensibleModel):
         Slot,
         on_delete=models.CASCADE,
         verbose_name=_("Start slot"),
-        related_name="lessons",
+        related_name="+",
     )
     slot_end = models.ForeignKey(
         Slot,
         on_delete=models.CASCADE,
         verbose_name=_("End slot"),
-        related_name="lessons",
+        related_name="+",
     )
 
     # Recurrence rules allow to define a series of lessons
@@ -184,22 +193,20 @@ class Lesson(ExtensibleModel):
     rooms = models.ManyToManyField(
         Room,
         verbose_name=_("Rooms"),
-        related_name="lessons",
+        related_name="lr_lessons",
         blank=True,
-        null=True,
     )
     teachers = models.ManyToManyField(
         Person,
         verbose_name=_("Teachers"),
-        related_name="lessons",
+        related_name="lr_lessons_as_teacher",
         blank=True,
-        null=True,
     )
     subject = models.ForeignKey(
         Subject,
         on_delete=models.CASCADE,
         verbose_name=_("Subject"),
-        related_name="lessons",
+        related_name="lr_lessons",
         blank=True,
         null=True,
     )
@@ -212,7 +219,7 @@ class Lesson(ExtensibleModel):
     class Meta:
         # Heads up: Link to slot implies uniqueness per site
         ordering = [
-            "slot_start__validity__date_start",
+            "slot_start__validity_range__date_start",
             "slot_start__weekday",
             "slot_start__time_start",
             "subject",
@@ -233,18 +240,18 @@ class Supervision(ExtensibleModel):
     rooms = models.ManyToManyField(
         Room,
         verbose_name=_("Rooms"),
-        related_name="supervisions",
+        related_name="lr_supervisions",
     )
     teachers = models.ManyToManyField(
         Person,
         verbose_name=_("Teachers"),
-        related_name="supervisions",
+        related_name="lr_supervisions",
     )
     slot = models.ForeignKey(
         Break,
         on_delete=models.CASCADE,
         verbose_name=_("Slot"),
-        related_name="supervisions",
+        related_name="lr_supervisions",
     )
 
     class Meta:
-- 
GitLab