Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • AlekSIS/official/AlekSIS-App-Alsijil
  • sunweaver/AlekSIS-App-Alsijil
  • 8tincsoVluke/AlekSIS-App-Alsijil
  • perfreicpo/AlekSIS-App-Alsijil
  • noifobarep/AlekSIS-App-Alsijil
  • 7ingannisdo/AlekSIS-App-Alsijil
  • unmruntartpa/AlekSIS-App-Alsijil
  • balrorebta/AlekSIS-App-Alsijil
  • comliFdifwa/AlekSIS-App-Alsijil
  • 3ranaadza/AlekSIS-App-Alsijil
10 results
Show changes
Commits on Source (117)
Showing
with 605 additions and 525 deletions
......@@ -6,7 +6,17 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog`_,
and this project adheres to `Semantic Versioning`_.
`4.0.0.dev2`_ - 2024-07-10
Unreleased
----------
Upgrade notice
~~~~~~~~~~~~~~
If you're upgrading from 3.x, there is now a migration path to use.
Therefore, please install ``AlekSIS-App-Lesrooster`` which now
includes parts of the legacy Chronos and the migration path.
`4.0.0.dev3`_ - 2024-07-10
--------------------------
Added
......@@ -15,6 +25,11 @@ Added
* Support for entering personal notes for students in the new coursebook interface.
* Support for entering tardiness for students in the new coursebook interface.
`4.0.0.dev2`_ - 2024-07-13
--------------------------
Fixed version of 4.0.0.dev1
`4.0.0.dev1`_ - 2024-06-13
--------------------------
......@@ -33,8 +48,7 @@ Starting from the class register core functionality, Alsijil is getting a entire
of both its frontend and backend. The models formerly used for lesson documentation, notably
`LessonDocumentation` and `PersonalNote` are replaced by new ones based on the calendar framework
provided by `AlekSIS-Core` and the absense framework provided by `AlekSIS-App-Kolego`. The legacy
views providing management functionality for those legacy models are not available anymore. Currently,
there exists no migration path away from legacy data.
views providing management functionality for those legacy models are not available anymore.
Changed
~~~~~~~
......@@ -149,7 +163,7 @@ Changed
~~~~~~~
* Use start date of current SchoolTerm as default value for PersonalNote filter in overview.
Julia ist eine höhere Programmiersprache, die vor allem für numerisches und wissenschaftliches Rechnen entwickelt wurde und auch als Allzweck-Programmiersprache verwendet werden kann, bei gleichzeitiger Wahrung einer hohen Ausführungsgeschwindigkeit. Wikipedia
Fixed
~~~~~
......@@ -372,3 +386,4 @@ Fixed
.. _4.0.0.dev0: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev0
.. _4.0.0.dev1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev1
.. _4.0.0.dev2: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev2
.. _4.0.0.dev3: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev3
from typing import Callable, Sequence
from django.contrib import messages
from django.contrib.humanize.templatetags.humanize import apnumber
from django.http import HttpRequest
from django.template.loader import get_template
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from aleksis.apps.alsijil.models import PersonalNote
from aleksis.core.models import Notification
def mark_as_excused(modeladmin, request, queryset):
queryset.filter(absent=True).update(excused=True, excuse_type=None)
mark_as_excused.short_description = _("Mark as excused")
def mark_as_unexcused(modeladmin, request, queryset):
queryset.filter(absent=True).update(excused=False, excuse_type=None)
mark_as_unexcused.short_description = _("Mark as unexcused")
def mark_as_excuse_type_generator(excuse_type) -> Callable:
def mark_as_excuse_type(modeladmin, request, queryset):
queryset.filter(absent=True).update(excused=True, excuse_type=excuse_type)
mark_as_excuse_type.short_description = _(f"Mark as {excuse_type.name}")
mark_as_excuse_type.__name__ = f"mark_as_excuse_type_{excuse_type.short_name}"
return mark_as_excuse_type
def delete_personal_note(modeladmin, request, queryset):
notes = []
for personal_note in queryset:
personal_note.reset_values()
notes.append(personal_note)
PersonalNote.objects.bulk_update(
notes, fields=["absent", "excused", "tardiness", "excuse_type", "remarks"]
)
delete_personal_note.short_description = _("Delete")
def send_request_to_check_entry(modeladmin, request: HttpRequest, selected_items: Sequence[dict]):
"""Send notifications to the teachers of the selected register objects.
Action for use with ``RegisterObjectTable`` and ``RegisterObjectActionForm``.
"""
# Group class register entries by teachers so each teacher gets just one notification
grouped_by_teachers = {}
for entry in selected_items:
teachers = entry["register_object"].get_teachers().all()
for teacher in teachers:
grouped_by_teachers.setdefault(teacher, [])
grouped_by_teachers[teacher].append(entry)
template = get_template("alsijil/notifications/check.html")
for teacher, items in grouped_by_teachers.items():
msg = template.render({"items": items})
title = _("{} asks you to check some class register entries.").format(
request.user.person.addressing_name
)
n = Notification(
title=title,
description=msg,
sender=request.user.person.addressing_name,
recipient=teacher,
link=request.build_absolute_uri(reverse("overview_me")),
)
n.save()
count_teachers = len(grouped_by_teachers.keys())
count_items = len(selected_items)
messages.success(
request,
_(
"We have successfully sent notifications to "
"{count_teachers} persons for {count_items} lessons."
).format(count_teachers=apnumber(count_teachers), count_items=apnumber(count_items)),
)
send_request_to_check_entry.short_description = _("Ask teacher to check data")
import logging
from datetime import datetime, time
from typing import TYPE_CHECKING
from django.db.models import F
from django.db.models.query_utils import Q
from django.utils.translation import gettext as _
from aleksis.apps.chronos.models import LessonEvent
from aleksis.core.data_checks import DataCheck, IgnoreSolveOption, SolveOption
if TYPE_CHECKING:
......@@ -32,22 +33,12 @@ class SetGroupsWithCurrentGroupsSolveOption(SolveOption):
check_result.delete()
class ResetPersonalNoteSolveOption(SolveOption):
name = "reset_personal_note"
verbose_name = _("Reset personal note to defaults")
@classmethod
def solve(cls, check_result: "DataCheckResult"):
note = check_result.related_object
note.reset_values()
note.save()
check_result.delete()
class NoPersonalNotesInCancelledLessonsDataCheck(DataCheck):
name = "no_personal_notes_in_cancelled_lessons"
verbose_name = _("Ensure that there are no personal notes in cancelled lessons")
problem_name = _("The personal note is related to a cancelled lesson.")
class NoParticipationStatusesPersonalNotesInCancelledLessonsDataCheck(DataCheck):
name = "no_personal_notes_participation_statuses_in_cancelled_lessons"
verbose_name = _(
"Ensure that there are no participation statuses and personal notes in cancelled lessons"
)
problem_name = _("The participation status or personal note is related to a cancelled lesson.")
solve_options = {
DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption,
IgnoreSolveOption.name: IgnoreSolveOption,
......@@ -55,27 +46,28 @@ class NoPersonalNotesInCancelledLessonsDataCheck(DataCheck):
@classmethod
def check_data(cls):
from .models import PersonalNote
personal_notes = (
PersonalNote.objects.not_empty()
.filter(
lesson_period__substitutions__cancelled=True,
lesson_period__substitutions__week=F("week"),
lesson_period__substitutions__year=F("year"),
)
.prefetch_related("lesson_period", "lesson_period__substitutions")
from .models import NewPersonalNote, ParticipationStatus
participation_statuses = ParticipationStatus.objects.filter(
related_documentation__amends__in=LessonEvent.objects.filter(cancelled=True)
)
personal_notes = NewPersonalNote.objects.filter(
documentation__amends__in=LessonEvent.objects.filter(cancelled=True)
)
for status in participation_statuses:
logging.info(f"Check participation status {status}")
cls.register_result(status)
for note in personal_notes:
logging.info(f"Check personal note {note}")
cls.register_result(note)
class NoGroupsOfPersonsSetInPersonalNotesDataCheck(DataCheck):
name = "no_groups_of_persons_set_in_personal_notes"
verbose_name = _("Ensure that 'groups_of_person' is set for every personal note")
problem_name = _("The personal note has no group in 'groups_of_person'.")
class NoGroupsOfPersonsSetInParticipationStatusesDataCheck(DataCheck):
name = "no_groups_of_persons_set_in_participation_statuses"
verbose_name = _("Ensure that 'groups_of_person' is set for every participation status")
problem_name = _("The participation status has no group in 'groups_of_person'.")
solve_options = {
SetGroupsWithCurrentGroupsSolveOption.name: SetGroupsWithCurrentGroupsSolveOption,
DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption,
......@@ -84,24 +76,21 @@ class NoGroupsOfPersonsSetInPersonalNotesDataCheck(DataCheck):
@classmethod
def check_data(cls):
from .models import PersonalNote
personal_notes = PersonalNote.objects.filter(groups_of_person__isnull=True)
from .models import ParticipationStatus
for note in personal_notes:
logging.info(f"Check personal note {note}")
cls.register_result(note)
participation_statuses = ParticipationStatus.objects.filter(groups_of_person__isnull=True)
for status in participation_statuses:
logging.info(f"Check participation status {status}")
cls.register_result(status)
class LessonDocumentationOnHolidaysDataCheck(DataCheck):
"""Checks for lesson documentation objects on holidays.
This ignores empty lesson documentation as they are created by default.
"""
class DocumentationOnHolidaysDataCheck(DataCheck):
"""Checks for documentation objects on holidays."""
name = "lesson_documentation_on_holidays"
verbose_name = _("Ensure that there are no filled out lesson documentations on holidays")
problem_name = _("The lesson documentation is on holidays.")
name = "documentation_on_holidays"
verbose_name = _("Ensure that there are no documentations on holidays")
problem_name = _("The documentation is on holidays.")
solve_options = {
DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption,
IgnoreSolveOption.name: IgnoreSolveOption,
......@@ -109,33 +98,33 @@ class LessonDocumentationOnHolidaysDataCheck(DataCheck):
@classmethod
def check_data(cls):
from aleksis.apps.chronos.models import Holiday
from aleksis.core.models import Holiday
from .models import LessonDocumentation
from .models import Documentation
holidays = Holiday.objects.all()
documentations = LessonDocumentation.objects.not_empty().annotate_date_range()
q = Q(pk__in=[])
for holiday in holidays:
q = q | Q(day_end__gte=holiday.date_start, day_start__lte=holiday.date_end)
documentations = documentations.filter(q)
q = q | Q(
datetime_start__gte=datetime.combine(holiday.date_start, time.min),
datetime_end__lte=datetime.combine(holiday.date_end, time.max),
)
documentations = Documentation.objects.filter(q)
for doc in documentations:
logging.info(f"Lesson documentation {doc} is on holidays")
logging.info(f"Documentation {doc} is on holidays")
cls.register_result(doc)
class PersonalNoteOnHolidaysDataCheck(DataCheck):
"""Checks for personal note objects on holidays.
This ignores empty personal notes as they are created by default.
"""
class ParticipationStatusPersonalNoteOnHolidaysDataCheck(DataCheck):
"""Checks for participation status and personal note objects on holidays."""
name = "personal_note_on_holidays"
verbose_name = _("Ensure that there are no filled out personal notes on holidays")
problem_name = _("The personal note is on holidays.")
name = "participation_status_personal_note_on_holidays"
verbose_name = _(
"Ensure that there are no participation statuses or personal notes on holidays"
)
problem_name = _("The participation status or personal note is on holidays.")
solve_options = {
DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption,
IgnoreSolveOption.name: IgnoreSolveOption,
......@@ -143,39 +132,21 @@ class PersonalNoteOnHolidaysDataCheck(DataCheck):
@classmethod
def check_data(cls):
from aleksis.apps.chronos.models import Holiday
from aleksis.core.models import Holiday
from .models import PersonalNote
from .models import ParticipationStatus
holidays = Holiday.objects.all()
personal_notes = PersonalNote.objects.not_empty().annotate_date_range()
q = Q(pk__in=[])
for holiday in holidays:
q = q | Q(day_end__gte=holiday.date_start, day_start__lte=holiday.date_end)
personal_notes = personal_notes.filter(q)
for note in personal_notes:
logging.info(f"Personal note {note} is on holidays")
cls.register_result(note)
class ExcusesWithoutAbsences(DataCheck):
name = "excuses_without_absences"
verbose_name = _("Ensure that there are no excused personal notes without an absence")
problem_name = _("The personal note is marked as excused, but not as absent.")
solve_options = {
ResetPersonalNoteSolveOption.name: ResetPersonalNoteSolveOption,
IgnoreSolveOption.name: IgnoreSolveOption,
}
@classmethod
def check_data(cls):
from .models import PersonalNote
q = q | Q(
datetime_start__gte=datetime.combine(holiday.date_start, time.min),
datetime_end__lte=datetime.combine(holiday.date_end, time.max),
)
personal_notes = PersonalNote.objects.filter(excused=True, absent=False)
participation_statuses = ParticipationStatus.objects.filter(q)
for note in personal_notes:
logging.info(f"Check personal note {note}")
cls.register_result(note)
for status in participation_statuses:
logging.info(f"Participation status {status} is on holidays")
cls.register_result(status)
from django.utils.translation import gettext as _
from django_filters import CharFilter, DateFilter, FilterSet
from material import Layout, Row
from aleksis.core.models import SchoolTerm
from .models import PersonalNote
class PersonalNoteFilter(FilterSet):
day_start = DateFilter(lookup_expr="gte", label=_("After"))
day_end = DateFilter(lookup_expr="lte", label=_("Before"))
subject = CharFilter(lookup_expr="icontains", label=_("Subject"))
def __init__(self, data=None, *args, **kwargs):
if data is not None:
data = data.copy()
current_school_term = SchoolTerm.current
if not data.get("day_start") and current_school_term:
data["day_start"] = current_school_term.date_start
for name, f in self.base_filters.items():
initial = f.extra.get("initial")
if not data.get(name) and initial:
data[name] = initial
super().__init__(data, *args, **kwargs)
self.form.fields["tardiness__lt"].label = _("Tardiness is lower than")
self.form.fields["tardiness__gt"].label = _("Tardiness is bigger than")
self.form.layout = Layout(
Row("subject"),
Row("day_start", "day_end"),
Row("absent", "excused", "excuse_type"),
Row("tardiness__gt", "tardiness__lt", "extra_marks"),
)
class Meta:
model = PersonalNote
fields = {
"excused": ["exact"],
"tardiness": ["lt", "gt"],
"absent": ["exact"],
"excuse_type": ["exact"],
"extra_marks": ["exact"],
}
from datetime import datetime, timedelta
from typing import Optional, Sequence
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count, Q
from django.http import HttpRequest
from django.utils import timezone
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
from guardian.shortcuts import get_objects_for_user
from material import Fieldset, Layout, Row
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget
from material import Layout, Row
from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import LessonPeriod, Subject, TimePeriod
from aleksis.core.forms import ActionForm, ListActionForm
from aleksis.core.models import Group, Person, SchoolTerm
from aleksis.core.models import Group, Person
from aleksis.core.util.core_helpers import get_site_preferences
from aleksis.core.util.predicates import check_global_permission
from .actions import (
delete_personal_note,
mark_as_excuse_type_generator,
mark_as_excused,
mark_as_unexcused,
send_request_to_check_entry,
)
from .models import (
ExcuseType,
ExtraMark,
GroupRole,
GroupRoleAssignment,
LessonDocumentation,
PersonalNote,
)
class LessonDocumentationForm(forms.ModelForm):
class Meta:
model = LessonDocumentation
fields = ["topic", "homework", "group_note"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["homework"].label = _("Homework for the next lesson")
if (
self.instance.lesson_period
and get_site_preferences()["alsijil__allow_carry_over_same_week"]
):
self.fields["carry_over_week"] = forms.BooleanField(
label=_("Carry over data to all other lessons with the same subject in this week"),
initial=True,
required=False,
)
def save(self, **kwargs):
lesson_documentation = super().save(commit=True)
if (
get_site_preferences()["alsijil__allow_carry_over_same_week"]
and self.cleaned_data["carry_over_week"]
and (
lesson_documentation.topic
or lesson_documentation.homework
or lesson_documentation.group_note
)
and lesson_documentation.lesson_period
):
lesson_documentation.carry_over_data(
LessonPeriod.objects.filter(lesson=lesson_documentation.lesson_period.lesson)
)
class PersonalNoteForm(forms.ModelForm):
class Meta:
model = PersonalNote
fields = ["absent", "tardiness", "excused", "excuse_type", "extra_marks", "remarks"]
person_name = forms.CharField(disabled=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["person_name"].widget.attrs.update(
{"class": "alsijil-lesson-personal-note-name"}
)
self.fields["person_name"].widget = forms.HiddenInput()
if self.instance and getattr(self.instance, "person", None):
self.fields["person_name"].initial = str(self.instance.person)
class SelectForm(forms.Form):
layout = Layout(Row("group", "teacher"))
group = forms.ModelChoiceField(
queryset=None,
label=_("Group"),
required=False,
widget=Select2Widget,
)
teacher = forms.ModelChoiceField(
queryset=None,
label=_("Teacher"),
required=False,
widget=Select2Widget,
)
def clean(self) -> dict:
data = super().clean()
if data.get("group") and not data.get("teacher"):
type_ = TimetableType.GROUP
instance = data["group"]
elif data.get("teacher") and not data.get("group"):
type_ = TimetableType.TEACHER
instance = data["teacher"]
elif not data.get("teacher") and not data.get("group"):
return data
else:
raise ValidationError(_("You can't select a group and a teacher both."))
data["type_"] = type_
data["instance"] = instance
return data
def __init__(self, request, *args, **kwargs):
self.request = request
super().__init__(*args, **kwargs)
person = self.request.user.person
group_qs = Group.get_groups_with_lessons()
# Filter selectable groups by permissions
if not check_global_permission(self.request.user, "alsijil.view_week"):
# 1) All groups the user is allowed to see the week view by object permissions
# 2) All groups the user is a member of an owner of
# 3) If the corresponding preference is turned on:
# All groups that have a parent group the user is an owner of
group_qs = (
group_qs.filter(
pk__in=get_objects_for_user(
self.request.user, "core.view_week_class_register_group", Group
).values_list("pk", flat=True)
)
).union(
group_qs.filter(
Q(members=person) | Q(owners=person) | Q(parent_groups__owners=person)
if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]
else Q(members=person) | Q(owners=person)
)
)
# Flatten query by filtering groups by pk
self.fields["group"].queryset = Group.objects.filter(
pk__in=list(group_qs.values_list("pk", flat=True))
)
teacher_qs = Person.objects.annotate(lessons_count=Count("lessons_as_teacher")).filter(
lessons_count__gt=0
)
# Filter selectable teachers by permissions
if not check_global_permission(self.request.user, "alsijil.view_week"):
# If the user hasn't got the global permission and the inherit privileges preference is
# turned off, the user is only allowed to see their own person. Otherwise, the user
# is allowed to see all persons that teach lessons that the given groups attend.
if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]:
teacher_pks = []
for group in group_qs:
for lesson in group.lessons.all():
for teacher in lesson.teachers.all():
teacher_pks.append(teacher.pk)
teacher_qs = teacher_qs.filter(pk__in=teacher_pks)
else:
teacher_qs = teacher_qs.filter(pk=person.pk)
self.fields["teacher"].queryset = teacher_qs
PersonalNoteFormSet = forms.modelformset_factory(
PersonalNote, form=PersonalNoteForm, max_num=0, extra=0
)
class RegisterAbsenceForm(forms.Form):
layout = Layout(
Fieldset("", "person"),
Fieldset("", Row("date_start", "date_end"), Row("from_period", "to_period")),
Fieldset("", Row("absent", "excused"), Row("excuse_type"), Row("remarks")),
)
person = forms.ModelChoiceField(label=_("Person"), queryset=None, widget=Select2Widget)
date_start = forms.DateField(label=_("Start date"), initial=datetime.today)
date_end = forms.DateField(label=_("End date"), initial=datetime.today)
from_period = forms.ChoiceField(label=_("Start period"))
to_period = forms.ChoiceField(label=_("End period"))
absent = forms.BooleanField(label=_("Absent"), initial=True, required=False)
excused = forms.BooleanField(label=_("Excused"), initial=True, required=False)
excuse_type = forms.ModelChoiceField(
label=_("Excuse type"),
queryset=ExcuseType.objects.all(),
widget=Select2Widget,
required=False,
)
remarks = forms.CharField(label=_("Remarks"), max_length=30, required=False)
def __init__(self, request, *args, **kwargs):
self.request = request
super().__init__(*args, **kwargs)
period_choices = TimePeriod.period_choices
if self.request.user.has_perm("alsijil.register_absence"):
self.fields["person"].queryset = Person.objects.all()
else:
persons_qs = Person.objects.filter(
Q(
pk__in=get_objects_for_user(
self.request.user, "core.register_absence_person", Person
)
)
| Q(primary_group__owners=self.request.user.person)
| Q(
member_of__in=get_objects_for_user(
self.request.user, "core.register_absence_group", Group
)
)
).distinct()
self.fields["person"].queryset = persons_qs
self.fields["from_period"].choices = period_choices
self.fields["to_period"].choices = period_choices
self.fields["from_period"].initial = TimePeriod.period_min
self.fields["to_period"].initial = TimePeriod.period_max
class ExtraMarkForm(forms.ModelForm):
layout = Layout("short_name", "name")
class Meta:
model = ExtraMark
fields = ["short_name", "name"]
class ExcuseTypeForm(forms.ModelForm):
layout = Layout("short_name", "name", "count_as_absent")
class Meta:
model = ExcuseType
fields = ["short_name", "name", "count_as_absent"]
class PersonOverviewForm(ActionForm):
def get_actions(self):
return (
[mark_as_excused, mark_as_unexcused]
+ [
mark_as_excuse_type_generator(excuse_type)
for excuse_type in ExcuseType.objects.all()
]
+ [delete_personal_note]
)
class GroupRoleForm(forms.ModelForm):
layout = Layout("name", "icon", "colour")
......@@ -357,74 +108,3 @@ class GroupRoleAssignmentEditForm(forms.ModelForm):
class Meta:
model = GroupRoleAssignment
fields = ["date_start", "date_end"]
class FilterRegisterObjectForm(forms.Form):
"""Form for filtering register objects in ``RegisterObjectTable``."""
layout = Layout(
Row("school_term", "date_start", "date_end"), Row("has_documentation", "group", "subject")
)
school_term = forms.ModelChoiceField(queryset=None, label=_("School term"))
has_documentation = forms.NullBooleanField(label=_("Has lesson documentation"))
group = forms.ModelChoiceField(queryset=None, label=_("Group"), required=False)
subject = forms.ModelChoiceField(queryset=None, label=_("Subject"), required=False)
date_start = forms.DateField(label=_("Start date"))
date_end = forms.DateField(label=_("End date"))
@classmethod
def get_initial(cls, has_documentation: Optional[bool] = None):
date_end = timezone.now().date()
date_start = date_end - timedelta(days=30)
school_term = SchoolTerm.current
# If there is no current school year, use last known school year.
if not school_term:
school_term = SchoolTerm.objects.all().last()
return {
"school_term": school_term,
"date_start": date_start,
"date_end": date_end,
"has_documentation": has_documentation,
}
def __init__(
self,
request: HttpRequest,
*args,
for_person: bool = True,
default_documentation: Optional[bool] = None,
groups: Optional[Sequence[Group]] = None,
**kwargs,
):
self.request = request
person = self.request.user.person
kwargs["initial"] = self.get_initial(has_documentation=default_documentation)
super().__init__(*args, **kwargs)
self.fields["school_term"].queryset = SchoolTerm.objects.all()
if not groups and for_person:
groups = Group.objects.filter(
Q(lessons__teachers=person)
| Q(lessons__lesson_periods__substitutions__teachers=person)
| Q(events__teachers=person)
| Q(extra_lessons__teachers=person)
).distinct()
elif not for_person:
groups = Group.objects.all()
self.fields["group"].queryset = groups
# Filter subjects by selectable groups
subject_qs = Subject.objects.filter(
Q(lessons__groups__in=groups) | Q(extra_lessons__groups__in=groups)
).distinct()
self.fields["subject"].queryset = subject_qs
class RegisterObjectActionForm(ListActionForm):
"""Action form for managing register objects for use with ``RegisterObjectTable``."""
actions = [send_request_to_check_entry]
......@@ -53,6 +53,7 @@
:is="itemComponent"
:extra-marks="extraMarks"
:absence-reasons="absenceReasons"
:subjects="subjects"
:documentation="item"
:affected-query="lastQuery"
:value="(selectedParticipations[item.id] ??= [])"
......@@ -87,6 +88,7 @@ import { extraMarks } from "../extra_marks/extra_marks.graphql";
import DocumentationLoader from "./documentation/DocumentationLoader.vue";
import sendToServerMixin from "./absences/sendToServerMixin";
import { absenceReasons } from "./absences/absenceReasons.graphql";
import { subjects } from "aleksis.apps.cursus/components/subject.graphql";
export default {
name: "Coursebook",
......@@ -157,6 +159,7 @@ export default {
hashUpdater: false,
extraMarks: [],
absenceReasons: [],
subjects: [],
selectedParticipations: {},
};
},
......@@ -169,6 +172,10 @@ export default {
query: absenceReasons,
update: (data) => data.items,
},
subjects: {
query: subjects,
update: (data) => data.items,
},
},
computed: {
// Assertion: Should only fire on page load or selection change.
......
......@@ -67,7 +67,7 @@
v-if="form"
@click="form = false"
:loading="loading"
:disabled="!formValid"
:disabled="!formValid || !absenceReason"
>
{{ $t("actions.continue") }}
<v-icon right>$next</v-icon>
......@@ -93,6 +93,7 @@ import SecondaryActionButton from "aleksis.core/components/generic/buttons/Secon
import loadingMixin from "aleksis.core/mixins/loadingMixin.js";
import permissionsMixin from "aleksis.core/mixins/permissions.js";
import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
import { DateTime } from "luxon";
import { createAbsencesForPersons } from "./absenceCreation.graphql";
......@@ -122,6 +123,7 @@ export default {
},
mounted() {
this.addPermissions(["alsijil.view_register_absence_rule"]);
this.clearForm();
},
methods: {
cancel() {
......@@ -131,8 +133,12 @@ export default {
},
clearForm() {
this.persons = [];
this.startDate = "";
this.endDate = "";
this.startDate = DateTime.now()
.startOf("day")
.toISO({ suppressSeconds: true });
this.endDate = DateTime.now()
.endOf("day")
.toISO({ suppressSeconds: true });
this.comment = "";
this.absenceReason = "";
},
......@@ -142,8 +148,8 @@ export default {
createAbsencesForPersons,
{
persons: this.persons.map((p) => p.id),
start: this.startDate,
end: this.endDate,
start: this.$toUTCISO(this.$parseISODate(this.startDate)),
end: this.$toUTCISO(this.$parseISODate(this.endDate)),
comment: this.comment,
reason: this.absenceReason,
},
......
......@@ -26,9 +26,10 @@
<v-row>
<v-col cols="12" :sm="6" class="pl-0">
<div aria-required="true">
<date-field
<date-time-field
:label="$t('forms.labels.start')"
:max="endDate"
:max-date="endDate"
:max-time="maxStartTime"
:rules="$rules().required.build()"
:value="startDate"
@input="$emit('start-date', $event)"
......@@ -37,9 +38,10 @@
</v-col>
<v-col cols="12" :sm="6" class="pr-0">
<div aria-required="true">
<date-field
<date-time-field
:label="$t('forms.labels.end')"
:min="startDate"
:min-date="startDate"
:min-time="minEndTime"
:rules="$rules().required.build()"
:value="endDate"
@input="$emit('end-date', $event)"
......@@ -69,15 +71,16 @@
<script>
import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue";
import DateField from "aleksis.core/components/generic/forms/DateField.vue";
import DateTimeField from "aleksis.core/components/generic/forms/DateTimeField.vue";
import { persons } from "./absenceCreation.graphql";
import formRulesMixin from "aleksis.core/mixins/formRulesMixin.js";
import { DateTime } from "luxon";
export default {
name: "AbsenceCreationForm",
components: {
AbsenceReasonGroupSelect,
DateField,
DateTimeField,
},
mixins: [formRulesMixin],
emits: [
......@@ -113,5 +116,25 @@ export default {
required: true,
},
},
computed: {
maxStartTime() {
// Only if on the same day
const start = DateTime.fromISO(this.startDate);
const end = DateTime.fromISO(this.endDate);
if (start.day !== end.day) return;
return end.minus({ minutes: 5 }).toFormat("HH:mm");
},
minEndTime() {
// Only if on the same day
const start = DateTime.fromISO(this.startDate);
const end = DateTime.fromISO(this.endDate);
if (start.day !== end.day) return;
return start.plus({ minutes: 5 }).toFormat("HH:mm");
},
},
};
</script>
......@@ -15,6 +15,7 @@
<lesson-notes class="span-2" v-bind="documentationPartProps" />
<participation-list
v-if="documentation.canEditParticipationStatus"
:include-present="false"
class="participation-list"
v-bind="documentationPartProps"
......
......@@ -3,28 +3,40 @@ import AbsenceReasonButtons from "aleksis.apps.kolego/components/AbsenceReasonBu
import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue";
import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue";
import DialogCloseButton from "aleksis.core/components/generic/buttons/DialogCloseButton.vue";
import SecondaryActionButton from "aleksis.core/components/generic/buttons/SecondaryActionButton.vue";
import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue";
import updateParticipationMixin from "./updateParticipationMixin.js";
import deepSearchMixin from "aleksis.core/mixins/deepSearchMixin.js";
import LessonInformation from "../documentation/LessonInformation.vue";
import {
extendParticipationStatuses,
updateParticipationStatuses,
} from "./participationStatus.graphql";
import SlideIterator from "aleksis.core/components/generic/SlideIterator.vue";
import PersonalNotes from "../personal_notes/PersonalNotes.vue";
import PersonalNoteChip from "../personal_notes/PersonalNoteChip.vue";
import ExtraMarkChip from "../../extra_marks/ExtraMarkChip.vue";
import TardinessChip from "./TardinessChip.vue";
import TardinessField from "./TardinessField.vue";
import ExtraMarkButtons from "../../extra_marks/ExtraMarkButtons.vue";
import MessageBox from "aleksis.core/components/generic/MessageBox.vue";
export default {
name: "ManageStudentsDialog",
extends: MobileFullscreenDialog,
components: {
ExtraMarkButtons,
TardinessChip,
ExtraMarkChip,
AbsenceReasonChip,
AbsenceReasonGroupSelect,
AbsenceReasonButtons,
PersonalNotes,
PersonalNoteChip,
LessonInformation,
MessageBox,
MobileFullscreenDialog,
SecondaryActionButton,
SlideIterator,
TardinessField,
DialogCloseButton,
......@@ -37,6 +49,14 @@ export default {
loadSelected: false,
selected: [],
isExpanded: false,
markAsAbsentDay: {
showAlert: false,
num: 0,
reason: "no reason",
name: "nobody",
participationIDs: [],
loading: false,
},
};
},
props: {
......@@ -57,9 +77,9 @@ export default {
},
},
methods: {
handleMultipleAction(absenceReasonId) {
handleMultipleAction(field, id) {
this.loadSelected = true;
this.sendToServer(this.selected, "absenceReason", absenceReasonId);
this.sendToServer(this.selected, field, id);
this.$once("save", this.resetMultipleAction);
},
resetMultipleAction() {
......@@ -67,6 +87,71 @@ export default {
this.$set(this.selected, []);
this.$refs.iterator.selected = [];
},
activateFullDayDialog(items) {
const itemIds = items.map((item) => item.id);
const participations = this.documentation.participations.filter((part) =>
itemIds.includes(part.id),
);
if (this.markAsAbsentDay.num === 1) {
this.markAsAbsentDay.name = participations[0].person.firstName;
}
this.$set(this.markAsAbsentDay, "participationIDs", itemIds);
this.markAsAbsentDay.loading = false;
this.markAsAbsentDay.showAlert = true;
},
beforeSendToServer() {
this.markAsAbsentDay.showAlert = false;
this.markAsAbsentDay.participationIDs = [];
},
duringUpdateSendToServer(
_participations,
_field,
_value,
incomingStatuses,
) {
this.markAsAbsentDay.reason = incomingStatuses[0].absenceReason?.name;
this.markAsAbsentDay.num = incomingStatuses.length;
},
afterSendToServer(_participations, field, value) {
if (field === "absenceReason" && value !== "present") {
this.$once("save", this.activateFullDayDialog);
}
},
markAsAbsentDayClick() {
this.markAsAbsentDay.loading = true;
this.mutate(
extendParticipationStatuses,
{
input: this.markAsAbsentDay.participationIDs,
},
(storedDocumentations, incomingStatuses) => {
const documentation = storedDocumentations.find(
(doc) => doc.id === this.documentation.id,
);
incomingStatuses.forEach((newStatus) => {
const participationStatus = documentation.participations.find(
(part) => part.id === newStatus.id,
);
participationStatus.baseAbsence = newStatus.baseAbsence;
participationStatus.isOptimistic = newStatus.isOptimistic;
});
this.markAsAbsentDay.reason = "no reason";
this.markAsAbsentDay.num = 0;
this.markAsAbsentDay.participationIDs = [];
this.markAsAbsentDay.loading = false;
this.markAsAbsentDay.showAlert = false;
return storedDocumentations;
},
);
},
},
};
</script>
......@@ -104,6 +189,40 @@ export default {
class="pt-4 full-width"
/>
</v-scroll-x-transition>
<message-box
v-model="markAsAbsentDay.showAlert"
color="success"
icon="$success"
transition="slide-y-transition"
dismissible
class="mt-4 mb-0 full-width"
>
<div class="text-subtitle-2">
{{
$tc(
"alsijil.coursebook.mark_as_absent_day.title",
markAsAbsentDay.num,
markAsAbsentDay,
)
}}
</div>
<p class="text-body-2 pa-0 ma-0" style="word-break: break-word">
{{
$t(
"alsijil.coursebook.mark_as_absent_day.description",
markAsAbsentDay,
)
}}
</p>
<secondary-action-button
color="success"
i18n-key="alsijil.coursebook.mark_as_absent_day.action_button"
class="mt-2"
:loading="markAsAbsentDay.loading"
@click="markAsAbsentDayClick"
/>
</message-box>
</template>
<template #content>
<slide-iterator
......@@ -138,18 +257,12 @@ export default {
small
:absence-reason="item.absenceReason"
/>
<v-chip
<personal-note-chip
v-for="note in item.notesWithNote"
:key="'text-note-note-overview-' + note.id"
:note="note"
small
>
<v-avatar left>
<v-icon small>mdi-note-outline</v-icon>
</v-avatar>
<span class="text-truncate" style="max-width: 30ch">
{{ note.note }}
</span>
</v-chip>
/>
<extra-mark-chip
v-for="note in item.notesWithExtraMark"
:key="'extra-mark-note-overview-' + note.id"
......@@ -175,6 +288,25 @@ export default {
<span v-t="'actions.back_to_overview'" />
</v-tooltip>
{{ item.person.fullName }}
<v-spacer />
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-btn
v-bind="attrs"
v-on="on"
icon
:to="{
name: 'core.personById',
params: {
id: item.person.id,
},
}"
>
<v-icon>mdi-open-in-new</v-icon>
</v-btn>
</template>
{{ $t("actions.open_person_page", item.person) }}
</v-tooltip>
</v-card-title>
<v-card-text>
<absence-reason-group-select
......@@ -188,7 +320,7 @@ export default {
v-bind="documentationPartProps"
:loading="loading"
:disabled="loading"
:participation="item"
:participations="[item]"
:value="item.tardiness"
@input="sendToServer([item], 'tardiness', $event)"
/>
......@@ -209,11 +341,26 @@ export default {
<template #actions>
<v-scroll-y-reverse-transition>
<div v-show="selected.length > 0" class="full-width">
<h4>{{ $t("alsijil.coursebook.participation_status") }}</h4>
<absence-reason-buttons
class="mb-1"
allow-empty
empty-value="present"
:custom-absence-reasons="absenceReasons"
@input="handleMultipleAction"
@input="handleMultipleAction('absenceReason', $event)"
/>
<h4>{{ $t("alsijil.extra_marks.title_plural") }}</h4>
<extra-mark-buttons
@input="handleMultipleAction('extraMark', $event)"
/>
<h4>{{ $t("alsijil.personal_notes.tardiness") }}</h4>
<tardiness-field
v-bind="documentationPartProps"
:loading="loading"
:disabled="loading"
:value="0"
:participations="selected"
@input="handleMultipleAction('tardiness', $event)"
/>
</div>
</v-scroll-y-reverse-transition>
......
......@@ -57,6 +57,17 @@ export default {
);
},
},
computed: {
showLabel() {
return !!this.labelKey || !this.canOpenParticipation;
},
innerLabelKey() {
if (this.documentation.futureNoticeParticipationStatus) {
return "alsijil.coursebook.notes.future";
}
return this.labelKey;
},
},
};
</script>
......@@ -77,9 +88,9 @@ export default {
v-on="on"
@click="touchDocumentation"
>
<v-icon :left="!!labelKey">mdi-account-edit-outline</v-icon>
<template v-if="labelKey">
{{ $t(labelKey) }}
<v-icon :left="showLabel">mdi-account-edit-outline</v-icon>
<template v-if="showLabel">
{{ $t(innerLabelKey) }}
</template>
</v-chip>
</template>
......
......@@ -2,20 +2,20 @@
import { DateTime } from "luxon";
import documentationPartMixin from "../documentation/documentationPartMixin";
import ConfirmDialog from "aleksis.core/components/generic/dialogs/ConfirmDialog.vue";
import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue";
import formRulesMixin from "aleksis.core/mixins/formRulesMixin.js";
export default {
name: "TardinessField",
components: { ConfirmDialog, PositiveSmallIntegerField },
mixins: [documentationPartMixin],
components: { ConfirmDialog },
mixins: [documentationPartMixin, formRulesMixin],
props: {
value: {
type: Number,
default: null,
required: false,
},
participation: {
type: Object,
participations: {
type: Array,
required: true,
},
},
......@@ -27,6 +27,40 @@ export default {
let diff = lessonEnd.diff(lessonStart, "minutes");
return diff.toObject().minutes;
},
defaultTimes() {
const lessonStart = DateTime.fromISO(this.documentation.datetimeStart);
const lessonEnd = DateTime.fromISO(this.documentation.datetimeEnd);
const now = DateTime.now();
let current = [];
if (now >= lessonStart && now <= lessonEnd) {
const diff = parseInt(
now.diff(lessonStart, "minutes").toObject().minutes,
);
current.push({
text: diff,
value: diff,
current: true,
});
}
return current.concat([
{
text: 5,
value: 5,
},
{
text: 10,
value: 10,
},
{
text: 15,
value: 15,
},
]);
},
},
methods: {
lessonLengthRule(time) {
......@@ -46,14 +80,23 @@ export default {
cancel() {
this.saveValue(this.previousValue);
},
processValueObjectOptional(value) {
if (Object.hasOwn(value, "value")) {
return value.value;
}
return value;
},
set(newValue) {
if (!newValue) {
newValue = this.processValueObjectOptional(newValue);
if (!newValue || parseInt(newValue) === 0) {
// this is a DELETE action, show the dialog, ...
this.showDeleteConfirm = true;
return;
}
this.saveValue(newValue);
this.saveValue(parseInt(newValue));
},
},
data() {
......@@ -69,17 +112,40 @@ export default {
</script>
<template>
<positive-small-integer-field
<v-combobox
outlined
class="mt-1"
prepend-inner-icon="mdi-clock-alert-outline"
:suffix="$t('time.minutes')"
:label="$t('alsijil.personal_notes.tardiness')"
:rules="[lessonLengthRule]"
:rules="
$rules()
.isANumber.isAWholeNumber.isGreaterThan(0)
.build([lessonLengthRule])
.map((f) => (v) => f(this.processValueObjectOptional(v)))
"
:items="defaultTimes"
:value="value"
@change="set($event)"
v-bind="$attrs"
>
<template #item="{ item }">
<v-list-item-icon v-if="item.current">
<v-icon>mdi-shimmer</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{
$tc(
item.current
? "alsijil.personal_notes.minutes_late_current"
: "time.minutes_n",
item.value,
)
}}
</v-list-item-title>
</v-list-item-content>
</template>
<template #append>
<confirm-dialog
v-model="showDeleteConfirm"
......@@ -93,13 +159,13 @@ export default {
{{
$t("alsijil.personal_notes.confirm_delete_tardiness", {
tardiness: previousValue,
name: participation.person.fullName,
name: participations.map((p) => p.person.firstName).join(", "),
})
}}
</template>
</confirm-dialog>
</template>
</positive-small-integer-field>
</v-combobox>
</template>
<style scoped>
......
......@@ -6,7 +6,7 @@ query persons {
}
}
query lessonsForPersons($persons: [ID]!, $start: Date!, $end: Date!) {
query lessonsForPersons($persons: [ID]!, $start: DateTime!, $end: DateTime!) {
items: lessonsForPersons(persons: $persons, start: $start, end: $end) {
id
lessons {
......@@ -31,8 +31,8 @@ query lessonsForPersons($persons: [ID]!, $start: Date!, $end: Date!) {
# Use absencesInputType?
mutation createAbsencesForPersons(
$persons: [ID]!
$start: Date!
$end: Date!
$start: DateTime!
$end: DateTime!
$comment: String
$reason: ID!
) {
......
......@@ -64,3 +64,14 @@ mutation touchDocumentation($documentationId: ID!) {
}
}
}
mutation extendParticipationStatuses($input: [ID]!) {
extendParticipationStatuses(input: $input) {
items: participations {
id
}
absences {
id
}
}
}
/**
* Mixin to provide shared functionality needed to send updated participation data to the server
*/
import { createPersonalNotes } from "../personal_notes/personal_notes.graphql";
import { updateParticipationStatuses } from "./participationStatus.graphql";
import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
......@@ -18,11 +19,17 @@ export default {
fieldValue = {
tardiness: value,
};
} else if (field === "extraMark") {
// Too much different logic → own method
this.addExtraMarks(participations, value);
return;
} else {
console.error(`Wrong field '${field}' for sendToServer`);
return;
}
this.beforeSendToServer(participations, field, value);
this.mutate(
updateParticipationStatuses,
{
......@@ -46,9 +53,63 @@ export default {
participationStatus.isOptimistic = newStatus.isOptimistic;
});
this.duringUpdateSendToServer(
participations,
field,
value,
incomingStatuses,
);
return storedDocumentations;
},
);
this.afterSendToServer(participations, field, value);
},
addExtraMarks(participations, extraMarkId) {
// Get all participation statuses without this extra mark and get the respective person ids
const participants = participations
.filter(
(participation) =>
!participation.notesWithExtraMark.some(
(note) => note.extraMark.id === extraMarkId,
),
)
.map((participation) => participation.person.id);
// CREATE new personal note
this.mutate(
createPersonalNotes,
{
input: participants.map((person) => ({
documentation: this.documentation.id,
person: person,
extraMark: extraMarkId,
})),
},
(storedDocumentations, incomingPersonalNotes) => {
const documentation = storedDocumentations.find(
(doc) => doc.id === this.documentation.id,
);
incomingPersonalNotes.forEach((note, index) => {
const participationStatus = documentation.participations.find(
(part) => part.person.id === participants[index],
);
participationStatus.notesWithExtraMark.push(note);
});
return storedDocumentations;
},
);
},
beforeSendToServer(_participations, _field, _value) {
// Noop hook
},
duringUpdateSendToServer(_participations, _field, _value, _incoming) {
// Noop hook
},
afterSendToServer(_participations, _field, _value) {
// Noop hook
},
},
};
......@@ -112,6 +112,9 @@ query documentationsForCoursebook(
canEdit
futureNotice
canDelete
futureNoticeParticipationStatus
canEditParticipationStatus
canViewParticipationStatus
}
}
......
......@@ -29,6 +29,7 @@
@open="$emit('open')"
@loading="loading = $event"
@save="$emit('close')"
@dirty="dirty = $event"
/>
<lesson-notes v-bind="documentationPartProps" />
</v-card-text>
......@@ -45,6 +46,7 @@
v-if="documentation.canEdit"
@click="save"
:loading="loading"
:disabled="!dirty"
/>
<cancel-button
v-if="!documentation.canEdit"
......@@ -76,12 +78,13 @@ export default {
SaveButton,
CancelButton,
},
emits: ["open", "close"],
emits: ["open", "close", "dirty"],
mixins: [documentationPartMixin],
data() {
return {
loading: false,
documentationsMutation: createOrUpdateDocumentations,
dirty: false,
};
},
methods: {
......@@ -90,6 +93,11 @@ export default {
this.$emit("close");
},
},
watch: {
dirty(dirty) {
this.$emit("dirty", dirty);
},
},
};
</script>
......@@ -100,6 +108,6 @@ export default {
gap: 1em;
}
.vertical {
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
}
</style>
<template>
<v-card outlined dense rounded="lg" v-bind="$attrs" v-on="$listeners">
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.topic.label") }}:
</div>
<div class="text-truncate">{{ documentation.topic || "" }}</div>
<template v-if="documentation.topic">
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.topic.label") }}:
</div>
<div class="text-truncate">{{ documentation.topic }}</div>
</template>
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.homework.label") }}:
</div>
<div class="text-truncate">{{ documentation.homework || "" }}</div>
<template v-if="documentation.homework">
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.homework.label") }}:
</div>
<div class="text-truncate">{{ documentation.homework }}</div>
</template>
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.group_note.label") }}:
</div>
<div class="text-truncate">{{ documentation.groupNote || "" }}</div>
<template v-if="documentation.groupNote">
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.group_note.label") }}:
</div>
<div class="text-truncate">{{ documentation.groupNote }}</div>
</template>
</v-card>
</template>
......
......@@ -10,13 +10,23 @@
<v-card-title class="text-subtitle-2 pb-1 font-weight-medium">
{{ $t("alsijil.coursebook.summary.homework.label") }}
</v-card-title>
<v-card-text>{{ documentation.homework || "" }}</v-card-text>
<v-card-text>
{{
documentation.homework ||
$t("alsijil.coursebook.summary.homework.empty_yet")
}}
</v-card-text>
</v-card>
<v-card outlined dense rounded="lg">
<v-card-title class="text-subtitle-2 pb-1 font-weight-medium">
{{ $t("alsijil.coursebook.summary.group_note.label") }}
</v-card-title>
<v-card-text>{{ documentation.groupNote || "" }}</v-card-text>
<v-card-text>
{{
documentation.groupNote ||
$t("alsijil.coursebook.summary.group_note.empty")
}}
</v-card-text>
</v-card>
</div>
</template>
......
<!-- Wrapper around Documentation.vue -->
<!-- That uses it either as list item or as editable modal dialog. -->
<template>
<mobile-fullscreen-dialog v-model="popup" max-width="500px">
<mobile-fullscreen-dialog
v-model="popup"
max-width="500px"
:persistent="dirty"
>
<template #activator="activator">
<!-- list view -> activate dialog -->
<documentation
......@@ -10,6 +14,7 @@
:dialog-activator="activator"
:extra-marks="extraMarks"
:absence-reasons="absenceReasons"
:subjects="subjects"
/>
</template>
<!-- dialog view -> deactivate dialog -->
......@@ -18,7 +23,9 @@
v-bind="$attrs"
:extra-marks="extraMarks"
:absence-reasons="absenceReasons"
:subjects="subjects"
@close="popup = false"
@dirty="dirty = $event"
/>
</mobile-fullscreen-dialog>
</template>
......@@ -36,6 +43,7 @@ export default {
data() {
return {
popup: false,
dirty: false,
};
},
props: {
......@@ -47,6 +55,10 @@ export default {
type: Array,
required: true,
},
subjects: {
type: Array,
required: true,
},
},
};
</script>