diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue index 3dc66363684c38a0ae02fbfc3a5a9992b8562e9c..13fcd9ddcdb8872b96330f7a9748a45bcd42e55e 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue @@ -22,32 +22,46 @@ </div> </v-row> <v-row> - <v-col cols="12" :sm="6" class="pl-0"> + <v-col cols="12" :sm="startPeriods ? 4 : 6" class="pl-0"> <div aria-required="true"> <date-time-field :label="$t('forms.labels.start')" - :max-date="maxStartDate" - :max-time="maxStartTime" - :limit-selectable-range="false" :rules="$rules().required.build()" - :value="startDate" + :value="start.toISO()" @input="handleStartDate" /> </div> </v-col> - <v-col cols="12" :sm="6" class="pr-0"> + <v-col cols="12" :sm="2" v-if="startPeriods" align-self="end"> + <v-select + :label="$t('lesrooster.slot.period')" + :items="startPeriods" + item-text="period" + :value="startSlot" + @input="handleStartSlot" + return-object + /> + </v-col> + <v-col cols="12" :sm="endPeriods ? 4 : 6" class="pr-0"> <div aria-required="true"> <date-time-field :label="$t('forms.labels.end')" - :min-date="minEndDate" - :min-time="minEndTime" - :limit-selectable-range="false" :rules="$rules().required.build()" - :value="endDate" + :value="end.toISO()" @input="handleEndDate" /> </div> </v-col> + <v-col cols="12" :sm="2" v-if="endPeriods" align-self="end"> + <v-select + :label="$t('lesrooster.slot.period')" + :items="endPeriods" + item-text="period" + :value="endSlot" + @input="handleEndSlot" + return-object + /> + </v-col> </v-row> <v-row> <v-text-field @@ -76,7 +90,7 @@ import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue"; import DateTimeField from "aleksis.core/components/generic/forms/DateTimeField.vue"; import PersonField from "aleksis.core/components/generic/forms/PersonField.vue"; -import { gqlPersons } from "./absenceCreation.graphql"; +import { gqlPersons, periodsByDay } from "./absenceCreation.graphql"; import formRulesMixin from "aleksis.core/mixins/formRulesMixin.js"; import { DateTime } from "luxon"; @@ -96,10 +110,14 @@ export default { "comment", "absence-reason", ], - data() { - return { - gqlQuery: gqlPersons, - }; + apollo: { + periodsByDay: { + query: periodsByDay, + result(_) { + this.handleStartDate(this.start.toISO()); + this.handleEndDate(this.end.toISO()); + }, + }, }, props: { persons: { @@ -127,60 +145,98 @@ export default { required: true, }, }, + data() { + return { + gqlQuery: gqlPersons, + startDT: DateTime.fromISO(this.startDate), + endDT: DateTime.fromISO(this.endDate), + startPeriods: false, + endPeriods: false, + startSlot: undefined, + endSlot: undefined, + }; + }, 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"); + start: { + get() { + return this.startDT; + }, + set(dt) { + this.startDT = dt; + if (dt >= this.end) { + this.end = dt.plus({ minutes: 5 }); + } + this.$emit("start-date", dt.toISO()); + }, }, - maxStartDate() { - const end = DateTime.fromISO(this.endDate); - return end.toISODate(); - }, - minEndDate() { - const start = DateTime.fromISO(this.startDate); - return start.toISODate(); + end: { + get() { + return this.endDT; + }, + set(dt) { + this.endDT = dt; + if (dt <= this.start) { + this.start = dt.minus({ minutes: 5 }); + } + this.$emit("end-date", dt.toISO()); + }, }, }, methods: { - handleStartDate(startDate) { - const parsedStart = DateTime.fromISO(startDate); - const parsedEnd = DateTime.fromISO(this.endDate); - if (parsedStart >= parsedEnd) { - this.$emit( - "end-date", - parsedStart.plus({ minutes: 5 }).toISO({ suppressSeconds: true }), + getPeriodsForWeekday(weekday) { + // Adapt from python conventions + const pythonWeekday = weekday - 1; + let periodsForWeekday = this.periodsByDay.find( + (period) => period.weekday === pythonWeekday, + ); + if (!periodsForWeekday) return false; + return periodsForWeekday.periods; + }, + handleStartDate(date) { + this.start = DateTime.fromISO(date); + + if (this.periodsByDay && this.periodsByDay.length > 0) { + // Select periods for day + this.startPeriods = this.getPeriodsForWeekday(this.start.weekday); + if (!this.startPeriods) return; + // Sync PeriodSelect + const startTime = this.start.toFormat("HH:mm:ss"); + this.startSlot = this.startPeriods.find( + (period) => period.timeStart === startTime, ); } - this.$emit("start-date", startDate); - this.$refs.form.resetValidation(); - this.$refs.form.validate(); }, - handleEndDate(endDate) { - const parsedStart = DateTime.fromISO(this.startDate); - const parsedEnd = DateTime.fromISO(endDate); - if (parsedEnd <= parsedStart) { - this.$emit( - "start-date", - parsedEnd.minus({ minutes: 5 }).toISO({ suppressSeconds: true }), + handleEndDate(date) { + this.end = DateTime.fromISO(date); + + if (this.periodsByDay && this.periodsByDay.length > 0) { + // Select periods for day + this.endPeriods = this.getPeriodsForWeekday(this.end.weekday); + if (!this.endPeriods) return; + // Sync PeriodSelect + const endTime = this.end.toFormat("HH:mm:ss"); + this.endSlot = this.endPeriods.find( + (period) => period.endTime === endTime, ); } - this.$emit("end-date", endDate); - this.$refs.form.resetValidation(); - this.$refs.form.validate(); + }, + handleStartSlot(slot) { + // Sync TimeSelect + const startTime = DateTime.fromISO(slot.timeStart); + this.start = this.start.set({ + hour: startTime.hour, + minute: startTime.minute, + second: startTime.second, + }); + }, + handleEndSlot(slot) { + // Sync TimeSelect + const endTime = DateTime.fromISO(slot.timeEnd); + this.end = this.end.set({ + hour: endTime.hour, + minute: endTime.minute, + second: endTime.second, + }); }, }, }; diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql index 9ca58dd7bc5d58b269210d316c47641a952cdcba..34a22cef6431fd15450e48b40d7ce1d94553fb29 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql @@ -7,6 +7,17 @@ query gqlPersons { } } +query periodsByDay { + periodsByDay: periodsByDay { + weekday + periods { + period + timeStart + timeEnd + } + } +} + query lessonsForPersons($persons: [ID]!, $start: DateTime!, $end: DateTime!) { items: lessonsForPersons(persons: $persons, start: $start, end: $end) { id diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py index 975b8930e3e184aba8523795a2d86d3b77cd468e..821ff026fdbb5b925445d48b8925a716c2dce951 100644 --- a/aleksis/apps/alsijil/schema/__init__.py +++ b/aleksis/apps/alsijil/schema/__init__.py @@ -1,5 +1,7 @@ +from collections import defaultdict from datetime import datetime +from django.apps import apps from django.db.models import BooleanField, ExpressionWrapper, Q import graphene @@ -53,6 +55,17 @@ from .personal_note import ( from .statistics import StatisticsByPersonType +class PeriodType(graphene.ObjectType): + period = graphene.Int() + time_start = graphene.Time() + time_end = graphene.Time() + + +class WeekdayType(graphene.ObjectType): + weekday = graphene.Int() + periods = graphene.List(PeriodType) + + class Query(graphene.ObjectType): documentations_by_course_id = FilterOrderList( DocumentationType, course_id=graphene.ID(required=True) @@ -100,6 +113,8 @@ class Query(graphene.ObjectType): group=graphene.ID(required=True), ) + periods_by_day = graphene.List(WeekdayType) + def resolve_documentations_by_course_id(root, info, course_id, **kwargs): documentations = Documentation.objects.filter( pk__in=Documentation.objects.filter(course_id=course_id) @@ -355,6 +370,35 @@ class Query(graphene.ObjectType): ) return graphene_django_optimizer.query(statistics, info) + @staticmethod + def resolve_periods_by_day(root, info): + if apps.is_installed("aleksis.apps.lesrooster"): + Slot = apps.get_model("lesrooster", "Slot") + ValidityRange = apps.get_model("lesrooster", "ValidityRange") + slots = ( + Slot.objects.filter( + time_grid__validity_range=ValidityRange.current, period__isnull=False + ) + .order_by("weekday") + .values("weekday", "period", "time_start", "time_end") + ) + # Key by weekday + by_weekday = defaultdict(list) + for slot in slots: + # return nested dicts: {weekday periods { period time_* }} + # sort periods by period + by_weekday[slot["weekday"]].append(slot) + # Nest and sort periods + periods = [] + for weekday, slots in by_weekday.items(): + periods.append( + {"weekday": weekday, "periods": sorted(slots, key=lambda slot: slot["period"])} + ) + + return periods + else: + return [] + class Mutation(graphene.ObjectType): create_or_update_documentations = DocumentationBatchCreateOrUpdateMutation.Field() diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/person_overview.html b/aleksis/apps/alsijil/templates/alsijil/print/partials/person_overview.html similarity index 100% rename from aleksis/apps/alsijil/templates/alsijil/partials/person_overview.html rename to aleksis/apps/alsijil/templates/alsijil/print/partials/person_overview.html diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/register_abbreviations.html b/aleksis/apps/alsijil/templates/alsijil/print/partials/register_abbreviations.html similarity index 100% rename from aleksis/apps/alsijil/templates/alsijil/partials/register_abbreviations.html rename to aleksis/apps/alsijil/templates/alsijil/print/partials/register_abbreviations.html diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/register_coursebook.html b/aleksis/apps/alsijil/templates/alsijil/print/partials/register_coursebook.html similarity index 100% rename from aleksis/apps/alsijil/templates/alsijil/partials/register_coursebook.html rename to aleksis/apps/alsijil/templates/alsijil/print/partials/register_coursebook.html diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/register_cover.html b/aleksis/apps/alsijil/templates/alsijil/print/partials/register_cover.html similarity index 100% rename from aleksis/apps/alsijil/templates/alsijil/partials/register_cover.html rename to aleksis/apps/alsijil/templates/alsijil/print/partials/register_cover.html diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/register_members_table.html b/aleksis/apps/alsijil/templates/alsijil/print/partials/register_members_table.html similarity index 100% rename from aleksis/apps/alsijil/templates/alsijil/partials/register_members_table.html rename to aleksis/apps/alsijil/templates/alsijil/print/partials/register_members_table.html diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/register_teachers_and_subjects_table.html b/aleksis/apps/alsijil/templates/alsijil/print/partials/register_teachers_and_subjects_table.html similarity index 100% rename from aleksis/apps/alsijil/templates/alsijil/partials/register_teachers_and_subjects_table.html rename to aleksis/apps/alsijil/templates/alsijil/print/partials/register_teachers_and_subjects_table.html diff --git a/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html b/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html index 8e395b7239765b60c0bfa8cd5d4db5a00bb2671b..8b1ea0afb698d94c8df131dfadd932ec54409cca 100644 --- a/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html +++ b/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html @@ -13,42 +13,42 @@ {% block content %} {% for group in groups %} {% if include_cover %} - {% include "alsijil/partials/register_cover.html" with group=group %} + {% include "alsijil/print/partials/register_cover.html" with group=group %} <div class="page-break"> </div> {% endif %} {% if include_abbreviations %} - {% include "alsijil/partials/register_abbreviations.html" with group=group %} + {% include "alsijil/print/partials/register_abbreviations.html" with group=group %} <div class="page-break"> </div> {% endif %} {% if include_members_table %} - {% include "alsijil/partials/register_members_table.html" with group=group %} + {% include "alsijil/print/partials/register_members_table.html" with group=group %} <div class="page-break"> </div> {% endif %} {% if include_teachers_and_subjects_table %} {% if group.courses.all %} <h4>{% trans 'Teachers and lessons in group' %} {{ group.name }}</h4> - {% include "alsijil/partials/register_teachers_and_subjects_table.html" with groups=group.as_list only %} + {% include "alsijil/print/partials/register_teachers_and_subjects_table.html" with groups=group.as_list only %} <div class="page-break"> </div> {% endif %} {% if group.child_groups.all %} <h4>{% trans 'Teachers and lessons in child groups' %}</h4> - {% include "alsijil/partials/register_teachers_and_subjects_table.html" with groups=group.child_groups.all only %} + {% include "alsijil/print/partials/register_teachers_and_subjects_table.html" with groups=group.child_groups.all only %} <div class="page-break"> </div> {% endif %} {% endif %} {% if include_person_overviews %} {% for person in group.members_with_stats %} - {% include "alsijil/partials/person_overview.html" with person=person %} + {% include "alsijil/print/partials/person_overview.html" with person=person %} <div class="page-break"> </div> {% endfor %} {% endif %} {% if include_coursebook %} - {% include "alsijil/partials/register_coursebook.html" with group=group %} + {% include "alsijil/print/partials/register_coursebook.html" with group=group %} {% endif %} {% endfor %} {% endblock %}