diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index 6d85f5de4f6c23bd43956b2475f754c0578d4346..b17aefc91f99e61f970225347442c094e947153d 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -35,6 +35,8 @@ from aleksis.apps.chronos.mixins import WeekRelatedMixin 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.apps.kolego.models import Absence as KolegoAbsence +from aleksis.apps.kolego.models import AbsenceReason from aleksis.core.data_checks import field_validation_data_check_factory from aleksis.core.mixins import ExtensibleModel, GlobalPermissionModel from aleksis.core.models import CalendarEvent, Group, SchoolTerm @@ -451,11 +453,21 @@ class Documentation(CalendarEvent): # FIXME: DataCheck course = models.ForeignKey( - Course, models.CASCADE, related_name="documentations", blank=True, null=True, verbose_name=_("Course") + Course, + models.CASCADE, + related_name="documentations", + blank=True, + null=True, + verbose_name=_("Course"), ) lesson_event = models.ForeignKey( - LessonEvent, models.CASCADE, related_name="documentation", blank=True, null=True, verbose_name=_("Lesson Event") + LessonEvent, + models.CASCADE, + related_name="documentation", + blank=True, + null=True, + verbose_name=_("Lesson Event"), ) subject = models.ForeignKey( @@ -499,26 +511,29 @@ class Documentation(CalendarEvent): ] -class Participation(RegisterObjectRelatedMixin, ExtensibleModel): - """A personal note about a single person. +class Participation(ExtensibleModel): + """A participation record about a single person. - Used in the class register to note participation and remarks about a student + Used in the class register to note participation of 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", verbose_name=_("Person")) - groups_of_person = models.ManyToManyField("core.Group", related_name="+", verbose_name=_("Groups of Person")) + person = models.ForeignKey( + "core.Person", models.CASCADE, related_name="participations", verbose_name=_("Person") + ) + groups_of_person = models.ManyToManyField( + "core.Group", related_name="+", verbose_name=_("Groups of Person") + ) documentation = models.ForeignKey( - Documentation, models.CASCADE, related_name="participations", verbose_name=_("Documentation") + Documentation, + models.CASCADE, + related_name="participations", + verbose_name=_("Documentation"), ) - remarks = models.CharField(max_length=255, blank=True, verbose_name=_("Remarks")) - - extra_marks = models.ManyToManyField("ExtraMark", blank=True, verbose_name=_("Extra Marks")) - def __str__(self) -> str: return f"{self.documentation}, {self.person}" @@ -538,6 +553,88 @@ class Participation(RegisterObjectRelatedMixin, ExtensibleModel): ] +class Absence(ExtensibleModel): + """An absence record about a single person. + + Used in the class register to note absence of a student + in a documented unit (e.g. a single lesson event or a custom time frame; see Documentation). + """ + + # FIXME: DataChecks + + reason = models.ForeignKey( + AbsenceReason, verbose_name=_("Absence Reason"), on_delete=models.CASCADE + ) + + person = models.ForeignKey( + "core.Person", models.CASCADE, related_name="lesson_absences", verbose_name=_("Person") + ) + groups_of_person = models.ManyToManyField( + "core.Group", related_name="+", verbose_name=_("Groups of Person") + ) + + documentation = models.ForeignKey( + Documentation, models.CASCADE, related_name="absences", verbose_name=_("Documentation") + ) + + excused = models.BooleanField(default=False, verbose_name=_("Excused")) + + base_absence = models.ForeignKey( + KolegoAbsence, models.SET_NULL, related_name="absences", verbose_name=_("Base Absence") + ) + + def __str__(self) -> str: + return f"{self.documentation}, {self.person}" + + class Meta: + verbose_name = _("Absence note") + verbose_name_plural = _("Absence notes") + ordering = [ + "documentation", + "person__last_name", + "person__first_name", + ] + constraints = [ + models.UniqueConstraint( + fields=("documentation", "person"), + name="unique_absence_per_documentation", + ), + ] + + +class NewPersonalNote(ExtensibleModel): + person = models.ForeignKey( + "core.Person", models.CASCADE, related_name="new_personal_notes", verbose_name=_("Person") + ) + + documentation = models.ForeignKey( + Documentation, + models.CASCADE, + related_name="personal_notes", + verbose_name=_("Documentation"), + blank=True, + null=True, + ) + + note = models.TextField(blank=True, verbose_name=_("Note")) + extra_mark = models.ForeignKey( + ExtraMark, on_delete=models.CASCADE, blank=True, null=True, verbose_name=_("Extra Mark") + ) + + def __str__(self) -> str: + return f"{self.person}, {self.note}, {self.extra_mark}" + + class Meta: + verbose_name = _("Personal Note") + verbose_name_plural = _("Personal Notes") + constraints = [ + models.CheckConstraint( + check=~Q(note="") | Q(extra_mark__isnull=False), + name="unique_absence_per_documentation", + ), + ] + + class GroupRole(ExtensibleModel): data_checks = [field_validation_data_check_factory("alsijil", "GroupRole", "icon")] diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py index 94fd3aebc1e627fd8654c4bd7f46e7c1ce901ea8..fc4cdfe6e791482e22026f843192d0a267976a3f 100644 --- a/aleksis/apps/alsijil/rules.py +++ b/aleksis/apps/alsijil/rules.py @@ -356,14 +356,12 @@ view_register_objects_list_predicate = has_person & ( add_perm("alsijil.view_register_objects_list_rule", view_register_objects_list_predicate) view_documentation_predicate = has_person & ( - has_global_perm("alsijil.view_documentation") - | can_view_documentation + has_global_perm("alsijil.view_documentation") | can_view_documentation ) add_perm("alsijil.view_documentation_rule", view_documentation_predicate) edit_documentation_predicate = has_person & ( - has_global_perm("alsijil.change_documentation") - | can_edit_documentation + has_global_perm("alsijil.change_documentation") | can_edit_documentation ) add_perm("alsijil.edit_documentation_rule", edit_documentation_predicate) add_perm("alsijil.delete_documentation_rule", edit_documentation_predicate) diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py index 26cd82507b81a0fff831d23c210cc5f71852be97..b442c7b6e980587a67bd07e3171d5315b58a0c26 100644 --- a/aleksis/apps/alsijil/schema/__init__.py +++ b/aleksis/apps/alsijil/schema/__init__.py @@ -1,26 +1,30 @@ +from django.db.models.query_utils import Q + import graphene from graphene_django import DjangoObjectType -from django.db.models.query_utils import Q - from aleksis.core.schema.base import DjangoFilterMixin, FilterOrderList from ..models import Documentation, Participation from .documentation import ( - DocumentationType, - DocumentationCreateMutation, DocumentationBatchCreateMutation, + DocumentationBatchPatchMutation, + DocumentationCreateMutation, DocumentationDeleteMutation, - DocumentationBatchPatchMutation + DocumentationType, ) class Query(graphene.ObjectType): documentations = FilterOrderList(DocumentationType) - documentations_by_course_id = FilterOrderList(DocumentationType, course_id=graphene.ID(required=True)) + documentations_by_course_id = FilterOrderList( + DocumentationType, course_id=graphene.ID(required=True) + ) def resolve_documentations_by_course_id(root, info, course_id, **kwargs): - documentations = Documentation.objects.filter(Q(course__pk=course_id) | Q(lesson_event__course__pk=course_id)) + documentations = Documentation.objects.filter( + Q(course__pk=course_id) | Q(lesson_event__course__pk=course_id) + ) return documentations diff --git a/aleksis/apps/alsijil/schema/documentation.py b/aleksis/apps/alsijil/schema/documentation.py index d8d5911a24bed3a32ce88a48df9276630e4dadf5..06ef6b26e94e007363061f5dbb1eb46d5a55b107 100644 --- a/aleksis/apps/alsijil/schema/documentation.py +++ b/aleksis/apps/alsijil/schema/documentation.py @@ -24,7 +24,19 @@ from ..models import Documentation class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType): class Meta: model = Documentation - fields = ("id", "course", "lesson_event", "subject", "topic", "homework", "group_note", "datetime_start", "datetime_end", "date_start", "date_end") + fields = ( + "id", + "course", + "lesson_event", + "subject", + "topic", + "homework", + "group_note", + "datetime_start", + "datetime_end", + "date_start", + "date_end", + ) filter_fields = { "id": ["exact", "lte", "gte"], "course__name": ["exact"], @@ -38,15 +50,49 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp class DocumentationCreateMutation(DjangoCreateMutation): class Meta: model = Documentation - fields = ("course", "lesson_event", "subject", "topic", "homework", "group_note", "datetime_start", "datetime_end", "date_start", "date_end") - optional_fields = ("course", "lesson_event", "subject", "topic", "homework", "group_note", "datetime_start", "datetime_end", "date_start", "date_end") + fields = ( + "course", + "lesson_event", + "subject", + "topic", + "homework", + "group_note", + "datetime_start", + "datetime_end", + "date_start", + "date_end", + ) + optional_fields = ( + "course", + "lesson_event", + "subject", + "topic", + "homework", + "group_note", + "datetime_start", + "datetime_end", + "date_start", + "date_end", + ) permissions = ("alsijil.add_documentation",) # FIXME class DocumentationBatchCreateMutation(DjangoBatchCreateMutation): class Meta: model = Documentation - fields = ("id", "course", "lesson_event", "subject", "topic", "homework", "group_note", "datetime_start", "datetime_end", "date_start", "date_end") + fields = ( + "id", + "course", + "lesson_event", + "subject", + "topic", + "homework", + "group_note", + "datetime_start", + "datetime_end", + "date_start", + "date_end", + ) permissions = ("alsijil.add_documentation",) # FIXME @@ -58,5 +104,17 @@ class DocumentationDeleteMutation(DeleteMutation): class DocumentationBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation): class Meta: model = Documentation - fields = ("id", "course", "lesson_event", "subject", "topic", "homework", "group_note", "datetime_start", "datetime_end", "date_start", "date_end") + fields = ( + "id", + "course", + "lesson_event", + "subject", + "topic", + "homework", + "group_note", + "datetime_start", + "datetime_end", + "date_start", + "date_end", + ) permissions = ("alsijil.edit_documentation_rule",) # FIXME diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py index b261173ab2e799a91306fd94e1a420c0c49d56f9..0459a96b8c820c6c439b59349cc065745b84d4f9 100644 --- a/aleksis/apps/alsijil/util/predicates.py +++ b/aleksis/apps/alsijil/util/predicates.py @@ -312,10 +312,7 @@ def is_lesson_event_teacher(user: User, obj: LessonEvent): or a teacher of the course, if the lesson event has one. """ if obj: - return ( - obj.course and is_course_teacher(user, obj) - or user.person in obj.all_teachers() - ) + return obj.course and is_course_teacher(user, obj) or user.person in obj.all_teachers() return False @@ -355,7 +352,9 @@ def can_view_documentation(user: User, obj: Documentation): if obj.course: return is_course_teacher(user, obj.course) | is_course_member(user, obj.course) if obj.lesson_event: - return is_lesson_event_teacher(user, obj.course) | is_lesson_event_member(user, obj.course) + return is_lesson_event_teacher(user, obj.course) | is_lesson_event_member( + user, obj.course + ) return False