diff --git a/aleksis/apps/alsijil/migrations/0019_add_documentation_and_participation.py b/aleksis/apps/alsijil/migrations/0019_add_documentation_and_participation.py new file mode 100644 index 0000000000000000000000000000000000000000..b85055df033eb9df52daaab1ecc0b50b27dfb328 --- /dev/null +++ b/aleksis/apps/alsijil/migrations/0019_add_documentation_and_participation.py @@ -0,0 +1,159 @@ +# Generated by Django 4.2.4 on 2023-08-13 14:53 + +import aleksis.apps.alsijil.models +import aleksis.core.managers +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("sites", "0002_alter_domain_unique"), + ("chronos", "0015_add_managed_by_app_label"), + ("core", "0052_site_related_name"), + ("cursus", "0001_initial"), + ("alsijil", "0018_add_managed_by_app_label"), + ] + + operations = [ + migrations.CreateModel( + name="Documentation", + fields=[ + ( + "calendarevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="core.calendarevent", + ), + ), + ( + "topic", + models.CharField(blank=True, max_length=255, verbose_name="Lesson topic"), + ), + ("homework", models.CharField(blank=True, max_length=255, verbose_name="Homework")), + ( + "group_note", + models.CharField(blank=True, max_length=255, verbose_name="Group note"), + ), + ( + "course", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="documentations", + to="cursus.course", + ), + ), + ( + "lesson_event", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="documentation", + to="chronos.lessonevent", + ), + ), + ( + "subject", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="cursus.subject", + ), + ), + ], + options={ + "verbose_name": "Teaching documentation", + "verbose_name_plural": "Teaching documentations", + }, + bases=("core.calendarevent",), + ), + migrations.CreateModel( + name="Participation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "managed_by_app_label", + models.CharField( + blank=True, + editable=False, + max_length=255, + verbose_name="App label of app responsible for managing this instance", + ), + ), + ("extended_data", models.JSONField(default=dict, editable=False)), + ("remarks", models.CharField(blank=True, max_length=255)), + ( + "documentation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="participations", + to="alsijil.documentation", + ), + ), + ( + "extra_marks", + models.ManyToManyField( + blank=True, to="alsijil.extramark", verbose_name="Extra marks" + ), + ), + ("groups_of_person", models.ManyToManyField(related_name="+", to="core.group")), + ( + "person", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="participations", + to="core.person", + ), + ), + ( + "site", + models.ForeignKey( + default=1, + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="sites.site", + ), + ), + ], + options={ + "verbose_name": "Participation note", + "verbose_name_plural": "Participation notes", + "ordering": ["documentation", "person__last_name", "person__first_name"], + }, + bases=(aleksis.apps.alsijil.models.RegisterObjectRelatedMixin, models.Model), + managers=[ + ("objects", aleksis.core.managers.AlekSISBaseManager()), + ], + ), + migrations.AddConstraint( + model_name="participation", + constraint=models.UniqueConstraint( + fields=("documentation", "person"), name="unique_participation_per_documentation" + ), + ), + migrations.AddConstraint( + model_name="documentation", + constraint=models.CheckConstraint( + check=models.Q( + ("course__isnull", True), ("lesson_event__isnull", True), _negated=True + ), + name="either_course_or_lesson_event", + ), + ), + ] diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index 17a2552567df74f80b4c7aecfcf5465f7dd0bc76..4f2b215c2f7930610bab29623908f92fde6a2532 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -3,6 +3,7 @@ from typing import Optional, Union from urllib.parse import urlparse from django.db import models +from django.db.models import QuerySet from django.db.models.constraints import CheckConstraint from django.db.models.query_utils import Q from django.urls import reverse @@ -31,10 +32,12 @@ from aleksis.apps.alsijil.managers import ( ) from aleksis.apps.chronos.managers import GroupPropertiesMixin from aleksis.apps.chronos.mixins import WeekRelatedMixin -from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod, TimePeriod +from aleksis.apps.chronos.models import Event, ExtraLesson, LessonEvent, LessonPeriod, TimePeriod +from aleksis.apps.chronos.util.format import format_m2m +from aleksis.apps.cursus.models import Course, Subject from aleksis.core.data_checks import field_validation_data_check_factory from aleksis.core.mixins import ExtensibleModel, GlobalPermissionModel -from aleksis.core.models import SchoolTerm +from aleksis.core.models import CalendarEvent, Group, SchoolTerm from aleksis.core.util.core_helpers import get_site_preferences from aleksis.core.util.model_helpers import ICONS @@ -439,6 +442,102 @@ class ExtraMark(ExtensibleModel): verbose_name_plural = _("Extra marks") +class Documentation(CalendarEvent): + """A documentation on teaching content in a freely choosable time frame. + + Non-personal, includes the topic and homework of the lesson. + """ + + # FIXME: DataCheck + + course = models.ForeignKey( + Course, models.CASCADE, related_name="documentations", blank=True, null=True + ) + + lesson_event = models.ForeignKey( + LessonEvent, models.CASCADE, related_name="documentation", blank=True, null=True + ) + + subject = models.ForeignKey( + Subject, models.CASCADE, related_name="+", blank=True, null=True + ) + + topic = models.CharField(verbose_name=_("Lesson topic"), max_length=255, blank=True) + homework = models.CharField(verbose_name=_("Homework"), max_length=255, blank=True) + group_note = models.CharField(verbose_name=_("Group note"), max_length=255, blank=True) + + def get_subject(self) -> str: + if self.subject: + return self.subject + if self.lesson_event: + if self.lesson_event.subject: + return self.lesson_event.subject + if self.lesson_event.course: + return self.lesson_event.course.subject + if self.course: + return self.course.subject + + def get_groups(self) -> QuerySet[Group]: + if self.lesson_event: + return self.lesson_event.actual_groups + if self.course: + return self.course.groups.all() + + def __str__(self) -> str: + start_datetime = CalendarEvent.value_start_datetime(self, None) + end_datetime = CalendarEvent.value_end_datetime(self, None) + return f"{format_m2m(self.get_groups())} {self.get_subject()} {start_datetime} - {end_datetime}" + + class Meta: + verbose_name = _("Teaching documentation") + verbose_name_plural = _("Teaching documentations") + constraints = [ + models.CheckConstraint( + check=~Q(course__isnull=True, lesson_event__isnull=True), + name="either_course_or_lesson_event", + ), + ] + + +class Participation(RegisterObjectRelatedMixin, ExtensibleModel): + """A personal note about a single person. + + Used in the class register to note participation and remarks about a student + in a documented unit (e.g. a single lesson event or a custom time frame; see Documentation). + """ + + # FIXME: DataChecks + + person = models.ForeignKey("core.Person", models.CASCADE, related_name="participations") + groups_of_person = models.ManyToManyField("core.Group", related_name="+") + + documentation = models.ForeignKey( + Documentation, models.CASCADE, related_name="participations" + ) + + remarks = models.CharField(max_length=255, blank=True) + + extra_marks = models.ManyToManyField("ExtraMark", blank=True, verbose_name=_("Extra marks")) + + def __str__(self) -> str: + return f"{self.documentation}, {self.person}" + + class Meta: + verbose_name = _("Participation note") + verbose_name_plural = _("Participation notes") + ordering = [ + "documentation", + "person__last_name", + "person__first_name", + ] + constraints = [ + models.UniqueConstraint( + fields=("documentation", "person"), + name="unique_participation_per_documentation", + ), + ] + + class GroupRole(ExtensibleModel): data_checks = [field_validation_data_check_factory("alsijil", "GroupRole", "icon")]