Newer
Older
from datetime import date
from typing import Optional, Union
from urllib.parse import urlparse

Jonathan Weth
committed
from django.db.models.constraints import CheckConstraint
from django.db.models.query_utils import Q
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _

Jonathan Weth
committed
from calendarweek import CalendarWeek
from colorfield.fields import ColorField

Jonathan Weth
committed
from aleksis.apps.alsijil.data_checks import (
ExcusesWithoutAbsences,
LessonDocumentationOnHolidaysDataCheck,
NoGroupsOfPersonsSetInPersonalNotesDataCheck,
NoPersonalNotesInCancelledLessonsDataCheck,
PersonalNoteOnHolidaysDataCheck,
)

Jonathan Weth
committed
from aleksis.apps.alsijil.managers import (
GroupRoleAssignmentManager,
GroupRoleAssignmentQuerySet,
GroupRoleManager,
GroupRoleQuerySet,

Jonathan Weth
committed
LessonDocumentationManager,
LessonDocumentationQuerySet,
PersonalNoteManager,
PersonalNoteQuerySet,
)
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.core.mixins import ExtensibleModel, GlobalPermissionModel
from aleksis.core.models import SchoolTerm

Jonathan Weth
committed
from aleksis.core.util.core_helpers import get_site_preferences
from aleksis.core.util.model_helpers import ICONS
def isidentifier(value: str) -> bool:
return value.isidentifier()
class ExcuseType(ExtensibleModel):
"""An type of excuse.
Can be used to count different types of absences separately.
"""
short_name = models.CharField(max_length=255, unique=True, verbose_name=_("Short name"))
name = models.CharField(max_length=255, unique=True, verbose_name=_("Name"))
def __str__(self):
return f"{self.name} ({self.short_name})"
@property
def count_label(self):
return f"{self.short_name}_count"
class Meta:
ordering = ["name"]
verbose_name = _("Excuse type")
verbose_name_plural = _("Excuse types")
constraints = [
models.UniqueConstraint(
fields=("site_id", "short_name"), name="unique_excuse_short_name"
),
models.UniqueConstraint(fields=("site_id", "name"), name="unique_excuse_name"),
]

Jonathan Weth
committed
lesson_related_constraint_q = (
Q(
lesson_period__isnull=False,
event__isnull=True,
extra_lesson__isnull=True,
week__isnull=False,
year__isnull=False,
)
| Q(
lesson_period__isnull=True,
event__isnull=False,
extra_lesson__isnull=True,
week__isnull=True,
year__isnull=True,
)
| Q(
lesson_period__isnull=True,
event__isnull=True,
extra_lesson__isnull=False,
week__isnull=True,
year__isnull=True,
)
)
class RegisterObjectRelatedMixin(WeekRelatedMixin):
"""Mixin with common API for lesson documentations and personal notes."""
def register_object(
self: Union["LessonDocumentation", "PersonalNote"]
) -> Union[LessonPeriod, Event, ExtraLesson]:
"""Get the object related to this lesson documentation or personal note."""
if self.lesson_period:
return self.lesson_period
elif self.event:
return self.event
else:
return self.extra_lesson
@property
def calendar_week(self: Union["LessonDocumentation", "PersonalNote"]) -> CalendarWeek:
"""Get the calendar week of this lesson documentation or personal note.
.. note::
As events can be longer than one week,
this will return the week of the start date for events.
"""
if self.lesson_period:
return CalendarWeek(week=self.week, year=self.year)
elif self.extra_lesson:
return self.extra_lesson.calendar_week
else:
return CalendarWeek.from_date(self.register_object.date_start)
def school_term(self: Union["LessonDocumentation", "PersonalNote"]) -> SchoolTerm:
"""Get the school term of the related register object."""
if self.lesson_period:
return self.lesson_period.lesson.validity.school_term
else:
return self.register_object.school_term
def date(self: Union["LessonDocumentation", "PersonalNote"]) -> Optional[date]:
"""Get the date of this lesson documentation or personal note.
:: warning::
As events can be longer than one day,
this will return `None` for events.
"""
if self.lesson_period:
return super().date
elif self.extra_lesson:
return self.extra_lesson.date
return None
@property
def date_formatted(self: Union["LessonDocumentation", "PersonalNote"]) -> str:
"""Get a formatted version of the date of this object.
Lesson periods, extra lessons: formatted date
Events: formatted date range
"""
return (
date_format(self.date)
if self.date
else f"{date_format(self.event.date_start)}–{date_format(self.event.date_end)}"
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
@property
def period(self: Union["LessonDocumentation", "PersonalNote"]) -> TimePeriod:
"""Get the date of this lesson documentation or personal note.
:: warning::
As events can be longer than one day,
this will return `None` for events.
"""
if self.event:
return self.event.period_from
else:
return self.register_object.period
@property
def period_formatted(self: Union["LessonDocumentation", "PersonalNote"]) -> str:
"""Get a formatted version of the period of this object.
Lesson periods, extra lessons: formatted period
Events: formatted period range
"""
return (
f"{self.period.period}."
if not self.event
else f"{self.event.period_from.period}.–{self.event.period_to.period}."
)
def get_absolute_url(self: Union["LessonDocumentation", "PersonalNote"]) -> str:
"""Get the absolute url of the detail view for the related register object."""
return self.register_object.get_alsijil_url(self.calendar_week)
class PersonalNote(RegisterObjectRelatedMixin, ExtensibleModel):
"""A personal note about a single person.
Used in the class register to note absences, excuses
and remarks about a student in a single lesson period.
data_checks = [
NoPersonalNotesInCancelledLessonsDataCheck,
NoGroupsOfPersonsSetInPersonalNotesDataCheck,
PersonalNoteOnHolidaysDataCheck,
ExcusesWithoutAbsences,
]

Jonathan Weth
committed
objects = PersonalNoteManager.from_queryset(PersonalNoteQuerySet)()
person = models.ForeignKey("core.Person", models.CASCADE, related_name="personal_notes")
groups_of_person = models.ManyToManyField("core.Group", related_name="+")

Jonathan Weth
committed
week = models.IntegerField(blank=True, null=True)
year = models.IntegerField(verbose_name=_("Year"), blank=True, null=True)

Jonathan Weth
committed
"chronos.LessonPeriod", models.CASCADE, related_name="personal_notes", blank=True, null=True
)
event = models.ForeignKey(
"chronos.Event", models.CASCADE, related_name="personal_notes", blank=True, null=True
)
extra_lesson = models.ForeignKey(
"chronos.ExtraLesson", models.CASCADE, related_name="personal_notes", blank=True, null=True
absent = models.BooleanField(default=False)
late = models.PositiveSmallIntegerField(default=0)
excused = models.BooleanField(default=False)
ExcuseType, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Excuse type"),
remarks = models.CharField(max_length=200, blank=True)
extra_marks = models.ManyToManyField("ExtraMark", blank=True, verbose_name=_("Extra marks"))
def save(self, *args, **kwargs):
if self.excuse_type:
self.excused = True
if not self.absent:
self.excused = False
self.excuse_type = None
def reset_values(self):
"""Reset all saved data to default values.
.. warning ::
This won't save the data, please execute ``save`` extra.
defaults = PersonalNote()
self.absent = defaults.absent
self.late = defaults.late
self.excused = defaults.excused
self.excuse_type = defaults.excuse_type
self.remarks = defaults.remarks
self.extra_marks.clear()
def __str__(self) -> str:
return f"{self.date_formatted}, {self.lesson_period}, {self.person}"
def get_absolute_url(self) -> str:
"""Get the absolute url of the detail view for the related register object."""
return urlparse(super().get_absolute_url())._replace(fragment="personal-notes").geturl()
verbose_name = _("Personal note")
verbose_name_plural = _("Personal notes")
"week",
"lesson_period__period__weekday",
"lesson_period__period__period",
"person__last_name",
"person__first_name",
]

Jonathan Weth
committed
constraints = [
CheckConstraint(
check=lesson_related_constraint_q, name="one_relation_only_personal_note"
),
models.UniqueConstraint(
fields=("week", "year", "lesson_period", "person"), name="unique_note_per_lp",
),
models.UniqueConstraint(
fields=("week", "year", "event", "person"), name="unique_note_per_ev",
),
models.UniqueConstraint(
fields=("week", "year", "extra_lesson", "person"), name="unique_note_per_el",

Jonathan Weth
committed
]
class LessonDocumentation(RegisterObjectRelatedMixin, ExtensibleModel):
"""A documentation on a single lesson period.
Non-personal, includes the topic and homework of the lesson.

Jonathan Weth
committed
objects = LessonDocumentationManager.from_queryset(LessonDocumentationQuerySet)()
data_checks = [LessonDocumentationOnHolidaysDataCheck]

Jonathan Weth
committed
week = models.IntegerField(blank=True, null=True)
year = models.IntegerField(verbose_name=_("Year"), blank=True, null=True)

Jonathan Weth
committed
"chronos.LessonPeriod", models.CASCADE, related_name="documentations", blank=True, null=True
)
event = models.ForeignKey(
"chronos.Event", models.CASCADE, related_name="documentations", blank=True, null=True
)
extra_lesson = models.ForeignKey(
"chronos.ExtraLesson", models.CASCADE, related_name="documentations", blank=True, null=True
topic = models.CharField(verbose_name=_("Lesson topic"), max_length=200, blank=True)
homework = models.CharField(verbose_name=_("Homework"), max_length=200, blank=True)
group_note = models.CharField(verbose_name=_("Group note"), max_length=200, blank=True)
"""Carry over data to directly adjacent periods in this lesson if data is not already set.

Jonathan Weth
committed
Can be deactivated using site preference ``alsijil__carry_over``.

Jonathan Weth
committed
"""
all_periods_of_lesson = LessonPeriod.objects.filter(
lesson=self.lesson_period.lesson, period__weekday=self.lesson_period.period.weekday,
for period in all_periods_of_lesson:
lesson_documentation = period.get_or_create_lesson_documentation(
CalendarWeek(week=self.week, year=self.year)

Jonathan Weth
committed

Jonathan Weth
committed
if not lesson_documentation.topic:
lesson_documentation.topic = self.topic
changed = True

Jonathan Weth
committed
if not lesson_documentation.homework:
lesson_documentation.homework = self.homework
changed = True

Jonathan Weth
committed
if not lesson_documentation.group_note:
lesson_documentation.group_note = self.group_note
changed = True

Jonathan Weth
committed
lesson_documentation.save(carry_over=False)

Jonathan Weth
committed
return f"{self.lesson_period}, {self.date_formatted}"
def save(self, carry_over=True, *args, **kwargs):
if (
get_site_preferences()["alsijil__carry_over"]
and (self.topic or self.homework or self.group_note)
and self.lesson_period
):
self._carry_over_data()

Jonathan Weth
committed
super().save(*args, **kwargs)
verbose_name = _("Lesson documentation")
verbose_name_plural = _("Lesson documentations")
"week",
"lesson_period__period__weekday",
"lesson_period__period__period",
]

Jonathan Weth
committed
constraints = [
CheckConstraint(
check=lesson_related_constraint_q, name="one_relation_only_lesson_documentation",
),
models.UniqueConstraint(
fields=("week", "year", "lesson_period"), name="unique_documentation_per_lp",
),
models.UniqueConstraint(
fields=("week", "year", "event"), name="unique_documentation_per_ev",
),
models.UniqueConstraint(
fields=("week", "year", "extra_lesson"), name="unique_documentation_per_el",

Jonathan Weth
committed
]
class ExtraMark(ExtensibleModel):
"""A model for extra marks.
Can be used for lesson-based counting of things (like forgotten homework).
"""
short_name = models.CharField(max_length=255, unique=True, verbose_name=_("Short name"))
name = models.CharField(max_length=255, unique=True, verbose_name=_("Name"))
def __str__(self):
return f"{self.name}"
@property
def count_label(self):
return f"{self.short_name}_count"
class Meta:
ordering = ["short_name"]
verbose_name = _("Extra mark")
verbose_name_plural = _("Extra marks")
constraints = [
models.UniqueConstraint(
fields=("site_id", "short_name"), name="unique_mark_short_name"
),
models.UniqueConstraint(fields=("site_id", "name"), name="unique_mark_name"),
]
objects = GroupRoleManager.from_queryset(GroupRoleQuerySet)()
name = models.CharField(max_length=255, verbose_name=_("Name"))
icon = models.CharField(max_length=50, blank=True, choices=ICONS, verbose_name=_("Icon"))
colour = ColorField(blank=True, verbose_name=_("Colour"))
def __str__(self):
return self.name
class Meta:
verbose_name = _("Group role")
verbose_name_plural = _("Group roles")
constraints = [
models.UniqueConstraint(fields=("site_id", "name"), name="unique_role_per_site"),
]
class GroupRoleAssignment(GroupPropertiesMixin, ExtensibleModel):
objects = GroupRoleAssignmentManager.from_queryset(GroupRoleAssignmentQuerySet)()
on_delete=models.CASCADE,
related_name="assignments",
)
person = models.ForeignKey(
"core.Person",
on_delete=models.CASCADE,
"core.Group", related_name="group_roles", verbose_name=_("Groups"),
)
date_start = models.DateField(verbose_name=_("Start date"))
date_end = models.DateField(
blank=True,
null=True,
verbose_name=_("End date"),
help_text=_("Can be left empty if end date is not clear yet"),
)
def __str__(self):
date_end = date_format(self.date_end) if self.date_end else "?"
return f"{self.role}: {self.person}, {date_format(self.date_start)}–{date_end}"
@property
def date_range(self) -> str:
if not self.date_end:
return f"{date_format(self.date_start)}–?"
else:
return f"{date_format(self.date_start)}–{date_format(self.date_end)}"
verbose_name = _("Group role assignment")
verbose_name_plural = _("Group role assignments")
class AlsijilGlobalPermissions(GlobalPermissionModel):
class Meta:
managed = False
permissions = (
("view_week", _("Can view week overview")),
("register_absence", _("Can register absence")),
("list_personal_note_filters", _("Can list all personal note filters")),
)