diff --git a/.dev-js/package.json b/.dev-js/package.json index f1a3b8b143115ffb8f0fee2d452ec2830cd888a3..99b0989e1dde9b0f71fdc229bac3aef501c06398 100644 --- a/.dev-js/package.json +++ b/.dev-js/package.json @@ -6,7 +6,7 @@ "eslint": "^8.26.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-vue": "^9.7.0", - "prettier": "^3.0.0", + "prettier": "^3.4.0", "stylelint": "^15.0.0", "stylelint-config-prettier": "^9.0.3", "stylelint-config-standard": "^34.0.0" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 902a10933a40914e7119fec6733cbf139cd7f1d2..d26538a584e9bfb9e1d33f131adda94f7b2be7ea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,14 @@ 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.dev9`_ - 2024-12-08 +-------------------------- + +Added +~~~~~ + +* Substitution planning interface based on teacher absences. + `4.0.0.dev8`_ - 2024-10-17 -------------------------- @@ -432,3 +440,4 @@ Fixed .. _3.0.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/3.0.1 .. _3.0.2: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/3.0.2 .. _4.0.0.dev8: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/4.0.0.dev8 +.. _4.0.0.dev9: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/4.0.0.dev9 diff --git a/aleksis/apps/chronos/frontend/components/LessonEventLinkIterator.vue b/aleksis/apps/chronos/frontend/components/LessonEventLinkIterator.vue index a6a737298d496c3a8b7acdcd7fe4d485c77cace2..2a53a497772191544df5cd65a8c2f14cde897c1a 100644 --- a/aleksis/apps/chronos/frontend/components/LessonEventLinkIterator.vue +++ b/aleksis/apps/chronos/frontend/components/LessonEventLinkIterator.vue @@ -11,6 +11,18 @@ export default { required: false, default: "name", }, + alternativeAttr: { + type: String, + required: false, + default: null, + }, + }, + methods: { + getAttr(item) { + let val = item[this.attr]; + if (val) return val; + return this.alternativeAttr ? item[this.alternativeAttr] : val; + }, }, }; </script> @@ -19,7 +31,7 @@ export default { <span v-bind="$attrs"> <span v-for="(item, idx) in items" :key="idx"> <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text --> - {{ item[attr] }}{{ idx + 1 < items.length ? "," : "" }} + {{ getAttr(item) }}{{ idx + 1 < items.length ? "," : "" }} </span> </span> </template> diff --git a/aleksis/apps/chronos/frontend/components/LessonInformation.vue b/aleksis/apps/chronos/frontend/components/LessonInformation.vue new file mode 100644 index 0000000000000000000000000000000000000000..072745c4c5916a4a71881c77efcf4832a1af7208 --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/LessonInformation.vue @@ -0,0 +1,64 @@ +<script setup> +import CancelledCalendarStatusChip from "aleksis.core/components/calendar/CancelledCalendarStatusChip.vue"; +import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; + +import { DateTime } from "luxon"; +</script> + +<template> + <v-card-text> + <cancelled-calendar-status-chip v-if="lesson.cancelled" class="mr-2" /> + <div + :class="{ + 'text-decoration-line-through': lesson.cancelled, + 'text--secondary': lesson.cancelled, + }" + > + {{ $d(toDateTime(lesson.datetimeStart), "shortTime") }} – + {{ $d(toDateTime(lesson.datetimeEnd), "shortTime") }} + {{ getCourse(lesson)?.name }} + </div> + <subject-chip v-if="getSubject(lesson)" :subject="getSubject(lesson)" /> + </v-card-text> +</template> + +<script> +export default { + name: "LessonInformation", + props: { + lesson: { + type: Object, + required: true, + }, + cancelled: { + type: Boolean, + required: false, + default: false, + }, + }, + methods: { + toDateTime(dateString) { + return DateTime.fromISO(dateString); + }, + getSubject(lesson) { + if (lesson.subject) { + return lesson.subject; + } else if (lesson.course?.subject) { + return lesson.course.subject; + } else if (lesson.amends?.subject) { + return lesson.amends.subject; + } + return undefined; + }, + + getCourse(lesson) { + if (lesson.course) { + return lesson.course; + } else if (lesson.amends?.course) { + return lesson.amends.course; + } + return undefined; + }, + }, +}; +</script> diff --git a/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue b/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue index f11b2ed2ebc60b72895f3e682c4b66cc024fddad..e750d01e98cb1969233e176f42492f4d7900939a 100644 --- a/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue +++ b/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue @@ -25,7 +25,13 @@ export default { v-bind="$attrs" > <div class="text-center"> - <mascot type="timetable" size="60" class="mb-4" /> + <mascot + type="timetable" + class="mb-4" + max-width="min(80vw, 600px)" + max-height="min(50vh, 500px)" + contain + /> <div class="text-h5 grey--text text--darken-2 mb-2"> {{ $t(titleKey) }} </div> diff --git a/aleksis/apps/chronos/frontend/components/SelectTimetable.vue b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue index 76543ac322a63cff1af8e948bbab5f81dd088aa1..5d4166431e14db4fcc412d7ddc7ed4fe69ce6008 100644 --- a/aleksis/apps/chronos/frontend/components/SelectTimetable.vue +++ b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue @@ -1,8 +1,12 @@ <script> import timetableTypes from "./timetableTypes"; +import Mascot from "aleksis.core/components/generic/mascot/Mascot.vue"; export default { name: "SelectTimetable", + components: { + Mascot, + }, props: { value: { type: Object, @@ -13,13 +17,23 @@ export default { type: Array, required: true, }, + loading: { + type: Boolean, + required: false, + default: false, + }, + limitHeight: { + default: false, + required: false, + type: Boolean, + }, }, data() { return { selected: null, selectedFull: null, search: "", - selectedTypes: ["GROUP", "TEACHER", "ROOM"], + selectedType: "GROUP", types: timetableTypes, }; }, @@ -36,7 +50,7 @@ export default { availableTimetablesFiltered() { // Filter timetables by selected types return this.availableTimetables.filter((timetable) => { - return this.selectedTypes.indexOf(timetable.type) !== -1; + return this.selectedType == timetable.type; }); }, }, @@ -49,6 +63,7 @@ export default { <!-- Search field for timetables --> <v-text-field search + dense filled rounded clearable @@ -57,11 +72,11 @@ export default { :placeholder="$t('chronos.timetable.search')" prepend-inner-icon="mdi-magnify" hide-details="auto" - class="mb-2" + class="mb-3" /> <!-- Filter by timetable types --> - <v-btn-toggle v-model="selectedTypes" dense block multiple class="d-flex"> + <v-btn-toggle v-model="selectedType" dense block class="d-flex" mandatory> <v-btn v-for="type in types" :key="type.id" @@ -80,6 +95,10 @@ export default { :search="search" single-expand disable-pagination + hide-default-footer + :loading="loading" + :style="{ height: limitHeight ? '600px' : null }" + :class="{ 'overflow-auto': limitHeight }" > <template #default="{ items, isExpanded, expand }"> <v-list class="scrollable-list"> @@ -107,9 +126,23 @@ export default { </v-list> </template> <template #loading> - <v-skeleton-loader - type="list-item-avatar,list-item-avatar,list-item-avatar" - /> + <v-skeleton-loader type="list-item-avatar@10" /> + </template> + <template #no-results> + <div class="d-flex flex-column align-center justify-center"> + <mascot type="searching" width="33%" min-width="250px" /> + <div class="mb-2"> + {{ $t("$vuetify.dataIterator.noResultsText") }} + </div> + </div> + </template> + <template #no-data> + <div class="d-flex flex-column align-center justify-center"> + <mascot type="ready_for_items" width="33%" min-width="250px" /> + <div class="mb-2"> + {{ $t("chronos.timetable.no_timetables_in_term") }} + </div> + </div> </template> </v-data-iterator> </div> diff --git a/aleksis/apps/chronos/frontend/components/Substitutions.vue b/aleksis/apps/chronos/frontend/components/Substitutions.vue index fd8558fbabc8af1291c05452dc2dc502a184ff6f..0dafca35aed4a2f49770a54ffdca15ffe83fcfa1 100644 --- a/aleksis/apps/chronos/frontend/components/Substitutions.vue +++ b/aleksis/apps/chronos/frontend/components/Substitutions.vue @@ -17,17 +17,23 @@ import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.v :show-select="false" :enable-create="false" :enable-edit="false" + :dense="true" + :items-per-page="-1" + :mobile-breakpoint="0" > <template #title> - <v-row class="d-flex align-center pt-2 pa-2"> - <v-card-title class="text-h4"> - {{ $d(new Date(date), "dateWithWeekday") }} - </v-card-title> + <v-card-title class="full-width flex-nowrap pb-0"> + {{ + $d( + new Date(date), + $vuetify.breakpoint.xs ? "shortWithWeekday" : "dateWithWeekday", + ) + }} <v-spacer /> <primary-action-button - class="mr-4" i18n-key="chronos.substitutions.print" icon-text="$print" + :icon="$vuetify.breakpoint.xs" :to="{ name: 'chronos.printSubstitutionsForDate', params: { @@ -35,50 +41,88 @@ import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.v }, }" /> - </v-row> - <v-card-text> - <div v-if="affectedTeachers.length > 0"> - <strong> - {{ $t("chronos.substitutions.affected_teachers") }}: - </strong> - <person-chip - v-for="teacher in affectedTeachers" - :key="teacher.id" - class="ma-1" - :person="teacher" - :to="{ - name: 'chronos.timetableWithId', - params: { - type: 'person', - id: teacher.id, - }, - }" - /> - </div> - <div v-if="affectedGroups.length > 0"> - <strong> {{ $t("chronos.substitutions.affected_groups") }}: </strong> - <group-chip - v-for="group in affectedGroups" - class="ma-1" - :key="group.id" - :group="group" - format="short" - /> - </div> + </v-card-title> + + <v-card-text + v-if="affectedTeachers.length > 0 || affectedGroups.length > 0" + class="pb-0" + > + <v-expansion-panels accordion multiple flat> + <v-expansion-panel v-if="affectedTeachers.length > 0"> + <v-expansion-panel-header class="px-0"> + <strong>{{ + $t("chronos.substitutions.affected_teachers") + }}</strong> + </v-expansion-panel-header> + <v-expansion-panel-content> + <person-chip + v-for="teacher in affectedTeachers" + :key="teacher.id" + class="ma-1" + :person="teacher" + small + :to="{ + name: 'chronos.timetableWithId', + params: { + type: 'person', + id: teacher.id, + }, + }" + /> + </v-expansion-panel-content> + </v-expansion-panel> + + <v-expansion-panel v-if="affectedGroups.length > 0"> + <v-expansion-panel-header class="px-0"> + <strong> + {{ $t("chronos.substitutions.affected_groups") }}</strong + > + </v-expansion-panel-header> + <!-- TODO: Link to group-timetable as well --> + <!-- as soon as it becomes possible to resolve a --> + <!-- group-timetable from the lesson-event group too. --> + <v-expansion-panel-content class="px-0"> + <group-chip + v-for="group in affectedGroups" + class="ma-1" + :key="group.id" + :group="group" + format="short" + small + /> + </v-expansion-panel-content> + </v-expansion-panel> + </v-expansion-panels> </v-card-text> </template> <!-- TODO: Extract strike -> bold || normal pattern into own --> <!-- component and reuse? --> <template #groups="{ item: { oldGroups, newGroups } }"> <span v-if="newGroups.length > 0"> - <span class="strike-through" v-for="g in oldGroups" :key="g.id">{{ - g.shortName - }}</span> + <span class="strike-through"> + <lesson-event-link-iterator + :items="oldGroups" + attr="shortName" + alternative-attr="name" + /> + </span> <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text --> <span> → </span> - <strong v-for="g in newGroups" :key="g.id">{{ g.shortName }}</strong> + <strong> + <lesson-event-link-iterator + :items="newGroups" + attr="shortName" + alternative-attr="name" + /> + </strong> + </span> + <span v-else> + <lesson-event-link-iterator + :items="oldGroups" + attr="shortName" + alternative-attr="name" + /> </span> - <span v-else v-for="g in oldGroups" :key="g.id">{{ g.shortName }}</span> </template> <template #time="{ item: { startSlot, endSlot, startTime, endTime } }"> <span v-if="startSlot && endSlot && startSlot === endSlot"> @@ -96,17 +140,29 @@ import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.v </template> <template #teachers="{ item: { oldTeachers, newTeachers } }"> <span v-if="newTeachers.length > 0"> - <span class="strike-through" v-for="t in oldTeachers" :key="t.id"> - {{ t.shortName || t.fullName }} + <span class="strike-through"> + <lesson-event-link-iterator + :items="oldTeachers" + attr="shortName" + alternative-attr="fullName" + /> </span> <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text --> <span> → </span> - <strong v-for="t in newTeachers" :key="t.id"> - {{ t.shortName || t.fullName }} + <strong> + <lesson-event-link-iterator + :items="newTeachers" + attr="shortName" + alternative-attr="fullName" + /> </strong> </span> - <span v-else v-for="t in oldTeachers" :key="t.id"> - {{ t.shortName || t.fullName }} + <span v-else> + <lesson-event-link-iterator + :items="oldTeachers" + attr="shortName" + alternative-attr="fullName" + /> </span> </template> <template #subject="{ item: { oldSubject, newSubject } }"> @@ -123,21 +179,33 @@ import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.v </template> <template #rooms="{ item: { oldRooms, newRooms } }"> <span v-if="newRooms.length > 0"> - <span class="strike-through" v-for="r in oldRooms" :key="r.id">{{ - r.shortName || r.name - }}</span> + <span class="strike-through"> + <lesson-event-link-iterator + :items="oldRooms" + attr="shortName" + alternative-attr="name" + /> + </span> <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text --> <span> → </span> - <strong v-for="r in newRooms" :key="r.id">{{ - r.shortName || r.name - }}</strong> + <strong> + <lesson-event-link-iterator + :items="newRooms" + attr="shortName" + alternative-attr="name" + /> + </strong> + </span> + <span v-else> + <lesson-event-link-iterator + :items="oldRooms" + attr="shortName" + alternative-attr="name" + /> </span> - <span v-else v-for="r in oldRooms" :key="r.id">{{ - r.shortName || r.name - }}</span> </template> <template #notes="{ item: { cancelled, notes } }"> - <v-chip v-if="cancelled" color="green" text-color="white"> + <v-chip v-if="cancelled" color="green" text-color="white" small> {{ $t("chronos.substitutions.cancelled") }} </v-chip> {{ notes }} @@ -161,8 +229,11 @@ import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.v import { substitutionsForDate } from "./substitutions.graphql"; import { DateTime } from "luxon"; +import LessonEventLinkIterator from "./LessonEventLinkIterator.vue"; + export default { name: "Substitutions", + components: { LessonEventLinkIterator }, props: { date: { type: String, @@ -230,4 +301,8 @@ export default { .strike-through { text-decoration: line-through; } +.v-expansion-panel-content__wrap { + padding: 0; + padding-bottom: 4px; +} </style> diff --git a/aleksis/apps/chronos/frontend/components/Timetable.vue b/aleksis/apps/chronos/frontend/components/Timetable.vue index 78947059bd524bcd829077d790dec3581bfe42a2..8e4edd36f62c851841cc54e6cbe8897800febe2d 100644 --- a/aleksis/apps/chronos/frontend/components/Timetable.vue +++ b/aleksis/apps/chronos/frontend/components/Timetable.vue @@ -4,6 +4,7 @@ import TimetableWrapper from "./TimetableWrapper.vue"; <script> import { DateTime } from "luxon"; +import { gqlTimetableDays } from "./timetables.graphql"; export default { name: "Timetable", @@ -12,6 +13,7 @@ export default { calendarFocus: "", calendarType: "week", initialRouteFocusSet: false, + timetableDays: [1, 2, 3, 4, 5], }; }, methods: { @@ -79,6 +81,11 @@ export default { }); }, }, + apollo: { + timetableDays: { + query: gqlTimetableDays, + }, + }, }; </script> @@ -93,6 +100,8 @@ export default { ]" :params="{ type: selected.type, id: selected.objId }" ref="calendarWithControls" + :calendar-days-of-week="timetableDays" + scroll-target="first" @changeCalendarFocus="setCalendarFocus" @changeCalendarType="setCalendarType" @calendarReady="setInnerFocusAndType" diff --git a/aleksis/apps/chronos/frontend/components/TimetableWrapper.vue b/aleksis/apps/chronos/frontend/components/TimetableWrapper.vue index d3228681ac045848c0d04c98b96f01e7e7bda508..7c20ad9f35f640761fe780936fd64c817201436a 100644 --- a/aleksis/apps/chronos/frontend/components/TimetableWrapper.vue +++ b/aleksis/apps/chronos/frontend/components/TimetableWrapper.vue @@ -134,6 +134,7 @@ export default { <select-timetable v-model="selected" @input="selectDialog = false" + :loading="$apollo.queries.availableTimetables.loading" :available-timetables="availableTimetables" /> </v-card> @@ -148,7 +149,9 @@ export default { <v-card> <select-timetable v-model="selected" + :loading="$apollo.queries.availableTimetables.loading" :available-timetables="availableTimetables" + limit-height /> </v-card> </v-col> @@ -161,7 +164,7 @@ export default { <!-- Calendar card--> <v-card v-else> - <div class="d-flex flex-column" v-if="$vuetify.breakpoint.smAndDown"> + <div class="d-flex flex-column" v-if="$vuetify.breakpoint.mdAndDown"> <v-card-title class="pt-2"> <v-btn icon diff --git a/aleksis/apps/chronos/frontend/components/amendLesson.graphql b/aleksis/apps/chronos/frontend/components/amendLesson.graphql index 5142b0b84c694333edf8ffce8b905fc4e622b1f3..02e0eea908dbb99244bd871a884088b1c0a5fb05 100644 --- a/aleksis/apps/chronos/frontend/components/amendLesson.graphql +++ b/aleksis/apps/chronos/frontend/components/amendLesson.graphql @@ -2,6 +2,8 @@ query gqlSubjects { amendableSubjects: subjects { id name + colourFg + colourBg } } @@ -12,10 +14,33 @@ query gqlPersons { } } +query gqlTeachers { + amendableTeachers: teachers { + id + fullName + shortName + subjectsAsTeacher { + id + name + shortName + colourFg + colourBg + } + } +} + query gqlRooms { amendableRooms: rooms { id name + shortName + } +} + +query gqlGroups { + groups: timetableGroups { + id + name } } @@ -68,8 +93,193 @@ mutation patchAmendLessons($input: [BatchPatchLessonEventInput]!) { } } +mutation createOrUpdateSubstitutions($input: [SubstitutionInputType]!) { + createOrUpdateSubstitutions(input: $input) { + items: substitutions { + id + oldId + subject { + id + shortName + name + colourFg + colourBg + teachers { + id + shortName + fullName + } + } + teachers { + id + shortName + fullName + } + groups { + id + } + rooms { + id + shortName + name + } + course { + id + subject { + id + shortName + name + colourFg + colourBg + teachers { + id + shortName + fullName + } + } + } + amends { + id + teachers { + id + shortName + fullName + } + subject { + id + shortName + name + colourFg + colourBg + teachers { + id + shortName + fullName + } + } + groups { + id + shortName + } + rooms { + id + shortName + name + } + course { + id + name + } + } + datetimeStart + datetimeEnd + cancelled + comment + } + } +} + mutation deleteAmendLessons($ids: [ID]!) { deleteAmendLessons(ids: $ids) { deletionCount } } + +query amendedLessonsFromAbsences( + $objId: ID + $dateStart: Date! + $dateEnd: Date! + $incomplete: Boolean + $teacher: ID +) { + items: amendedLessonsFromAbsences( + objId: $objId + dateStart: $dateStart + dateEnd: $dateEnd + incomplete: $incomplete + teacher: $teacher + ) { + id + oldId + subject { + id + shortName + name + colourFg + colourBg + teachers { + id + shortName + fullName + } + } + teachers { + id + shortName + fullName + } + groups { + id + } + course { + id + subject { + id + shortName + name + colourFg + colourBg + teachers { + id + shortName + fullName + } + } + name + } + rooms { + id + shortName + name + } + amends { + id + teachers { + id + shortName + fullName + } + subject { + id + shortName + name + colourFg + colourBg + teachers { + id + shortName + fullName + } + } + groups { + id + shortName + } + course { + id + name + } + rooms { + id + shortName + name + } + cancelled + comment + } + datetimeStart + datetimeEnd + cancelled + comment + } +} diff --git a/aleksis/apps/chronos/frontend/components/substitutions.graphql b/aleksis/apps/chronos/frontend/components/substitutions.graphql index f747c6b56552193374115245f552b14bd6584df8..eb4da41ff0cccef96a6481a75c5179446b6dadf0 100644 --- a/aleksis/apps/chronos/frontend/components/substitutions.graphql +++ b/aleksis/apps/chronos/frontend/components/substitutions.graphql @@ -2,6 +2,7 @@ query substitutionsForDate($date: Date!) { items: substitutionsForDate(date: $date) { affectedTeachers { id + avatarContentUrl shortName fullName } diff --git a/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionCard.vue b/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..45bce13d7dd3d04616ef643fd6a62f35573a55cd --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionCard.vue @@ -0,0 +1,430 @@ +<script setup> +import SubstitutionInformation from "./SubstitutionInformation.vue"; +import TeacherField from "aleksis.apps.cursus/components/TeacherField.vue"; +import SubjectChipSelectField from "aleksis.apps.cursus/components/SubjectChipSelectField.vue"; + +import createOrPatchMixin from "aleksis.core/mixins/createOrPatchMixin.js"; +import deleteMixin from "aleksis.core/mixins/deleteMixin.js"; +</script> + +<template> + <v-card class="my-1 full-width" :loading="loading"> + <v-card-text + class="full-width main-body pa-2" + :class="{ + vertical: $vuetify.breakpoint.mobile, + }" + > + <substitution-information :substitution="substitution" /> + + <subject-chip-select-field + :value="subject" + :disabled="loading" + :items="amendableSubjects" + @input="subjectInput" + /> + + <v-autocomplete + :value="substitutionRoomIDs" + multiple + chips + deletable-chips + dense + hide-details + outlined + :items="amendableRooms" + item-text="name" + item-value="id" + :disabled="loading" + :label="$t('chronos.substitutions.overview.rooms.label')" + @input="roomsInput" + > + <template #prepend-inner> + <template + v-if="roomsWithStatus.filter((t) => t.status === 'regular').length" + > + <v-chip + v-for="room in roomsWithStatus.filter( + (t) => t.status === 'regular', + )" + :key="room.id" + class="mb-1" + small + > + {{ room.shortName }} + </v-chip> + </template> + <template + v-if="roomsWithStatus.filter((t) => t.status === 'removed').length" + > + <v-chip + v-for="room in roomsWithStatus.filter( + (t) => t.status === 'removed', + )" + :key="room.id" + outlined + color="error" + class="mb-1" + small + > + <v-icon left small>mdi-cancel</v-icon> + <div class="text-decoration-line-through"> + {{ room.shortName ? room.shortName : room.name }} + </div> + </v-chip> + </template> + </template> + <template #selection="data"> + <v-chip + v-bind="data.attrs" + :input-value="data.selected" + v-if="getRoomStatus(data.item) === 'new'" + close + class="mb-1 mt-1" + small + outlined + color="success" + @click:close="removeRoom(data.item)" + > + <v-icon left small> mdi-plus </v-icon> + {{ data.item.shortName ? data.item.shortName : data.item.name }} + </v-chip> + </template> + </v-autocomplete> + + <teacher-field + v-if="amendableTeachers.length > 0" + :priority-subject="subject" + :show-subjects="true" + :value="substitutionTeacherIDs" + chips + deletable-chips + dense + hide-details + outlined + :disabled="loading" + :label="$t('chronos.substitutions.overview.teacher.label')" + :custom-teachers="amendableTeachers" + @input="teachersInput" + > + <template #prepend-inner> + <v-tooltip bottom> + <template #activator="{ on, attrs }"> + <v-chip + v-for="teacher in teachersWithStatus.filter( + (t) => t.status === 'removed', + )" + :key="teacher.id" + outlined + color="error" + class="mb-1" + small + v-on="on" + v-bind="attrs" + > + <v-icon left small>mdi-account-off-outline</v-icon> + <div class="text-decoration-line-through"> + {{ teacher.fullName }} + </div> + </v-chip> + </template> + <span>{{ + $t("chronos.substitutions.overview.teacher.status.absent") + }}</span> + </v-tooltip> + </template> + <template #selection="data"> + <v-tooltip bottom> + <template #activator="{ on, attrs }"> + <v-chip + v-bind="{ ...data.attrs, ...attrs }" + :input-value="data.selected" + close + class="mb-1 mt-1" + small + :outlined="getTeacherStatus(data.item) === 'new'" + :color="getTeacherStatus(data.item) === 'new' ? 'success' : ''" + @click:close="removeTeacher(data.item)" + v-on="on" + > + <v-icon left small v-if="getTeacherStatus(data.item) === 'new'"> + mdi-account-plus-outline + </v-icon> + {{ data.item.fullName }} + </v-chip> + </template> + <span>{{ + getTeacherStatus(data.item) === "new" + ? $t("chronos.substitutions.overview.teacher.status.new") + : $t("chronos.substitutions.overview.teacher.status.regular") + }}</span> + </v-tooltip> + </template> + </teacher-field> + + <v-text-field + dense + outlined + hide-details + :label="$t('chronos.substitutions.overview.comment')" + :value="substitution.comment" + @input="comment = $event" + @focusout="save(false)" + @keydown.enter="save(false)" + /> + + <v-btn-toggle + mandatory + dense + :color="substitution.cancelled ? 'error' : 'success'" + :disabled="loading" + class="justify-self-end" + :value="substitution.cancelled" + @change="save(false)" + > + <v-btn outlined :value="false" @click="cancelled = false"> + {{ $t("chronos.substitutions.overview.cancel.not_cancelled") }} + </v-btn> + <v-btn outlined :value="true" @click="cancelled = true"> + {{ $t("chronos.substitutions.overview.cancel.cancelled") }} + </v-btn> + </v-btn-toggle> + </v-card-text> + <v-divider /> + </v-card> +</template> + +<script> +export default { + name: "SubstitutionCard", + emits: ["open", "close", "delete"], + mixins: [createOrPatchMixin, deleteMixin], + data() { + return { + loading: false, + teachers: [], + rooms: [], + substitutionSubject: null, + comment: null, + cancelled: null, + }; + }, + props: { + substitution: { + type: Object, + required: true, + }, + amendableRooms: { + type: Array, + required: false, + default: () => [], + }, + amendableSubjects: { + type: Array, + required: false, + default: () => [], + }, + amendableTeachers: { + type: Array, + required: false, + default: () => [], + }, + }, + methods: { + handleUpdateAfterCreateOrPatch(itemId) { + return (cached, incoming) => { + for (const object of incoming) { + console.log("summary: handleUpdateAfterCreateOrPatch", object); + // Replace the current substitution + const index = cached.findIndex( + (o) => o[itemId] === this.substitution.id, + ); + // merged with the incoming partial substitution + // if creation of proper substitution from dummy one, set ID of substitution currently being edited as oldID so that key in overview doesn't change + cached[index] = { + ...this.substitution, + ...object, + oldId: + this.substitution.id !== object.id + ? this.substitution.id + : this.substitution.oldId, + }; + } + return cached; + }; + }, + handleUpdateAfterDelete(ids, itemId) { + return (cached, incoming) => { + for (const id of ids) { + // Remove item from cached data or reset it to old ID, if present + const index = cached.findIndex((o) => o[itemId] === id); + if (cached[index].oldId) { + cached[index].id = cached[index].oldId; + cached[index].oldId = null; + + // Clear dummy substitution + cached[index].teachers = []; + cached[index].rooms = []; + cached[index].subject = null; + cached[index].comment = ""; + cached[index].cancelled = false; + } else { + this.$emit("delete", cached[index].datetimeStart); + } + } + return cached; + }; + }, + getTeacherStatus(teacher) { + return this.teachersWithStatus.find((t) => t.id === teacher.id)?.status; + }, + getRoomStatus(room) { + return this.roomsWithStatus.find((r) => r.id === room.id)?.status; + }, + removeTeacher(teacher) { + this.teachers = this.substitutionTeacherIDs.filter( + (t) => t !== teacher.id, + ); + this.save(true); + }, + removeRoom(room) { + this.rooms = this.substitutionRoomIDs.filter((r) => r !== room.id); + this.save(true); + }, + subjectInput(subject) { + this.substitutionSubject = subject.id; + this.save(); + }, + teachersInput(teachers) { + this.teachers = teachers; + this.save(); + }, + roomsInput(rooms) { + this.rooms = rooms; + this.save(); + }, + cancelledInput(cancelled) { + this.cancelled = cancelled; + this.save(); + }, + save(allowEmpty = false) { + if ( + this.teachers.length || + this.rooms.length || + this.substitutionSubject !== null || + (this.comment !== null && this.comment !== "") || + this.cancelled !== null + ) { + this.createOrPatch([ + { + id: this.substitution.id, + ...((allowEmpty || this.teachers.length) && { + teachers: this.teachers, + }), + ...((allowEmpty || this.rooms.length) && { rooms: this.rooms }), + ...(this.substitutionSubject !== null && { + subject: this.substitutionSubject, + }), + ...(this.comment !== null && + this.comment !== "" && { comment: this.comment }), + ...(this.cancelled !== null && { cancelled: this.cancelled }), + ...((this.teachers.length || this.rooms.length) && { + cancelled: false, + }), + }, + ]); + this.teachers = []; + this.rooms = []; + this.substitutionSubject = null; + this.comment = null; + this.cancelled = null; + } else if (!this.substitution.id.startsWith("DUMMY")) { + this.delete([this.substitution]); + } + }, + }, + computed: { + substitutionTeacherIDs() { + return this.substitution.teachers.map((teacher) => teacher.id); + }, + substitutionRoomIDs() { + return this.substitution.rooms.map((room) => room.id); + }, + // Group teachers by their substitution status (regular, new, removed) + teachersWithStatus() { + // IDs of teachers of amended lesson + const oldIds = this.substitution.amends.teachers.map( + (teacher) => teacher.id, + ); + // IDs of teachers of new substitution lesson + const newIds = this.substitution.teachers.map((teacher) => teacher.id); + const allTeachers = new Set( + this.substitution.amends.teachers.concat(this.substitution.teachers), + ); + let teachersWithStatus = Array.from(allTeachers).map((teacher) => { + let status = "regular"; + if (newIds.includes(teacher.id) && !oldIds.includes(teacher.id)) { + // Mark teacher as being new if they are only linked to the substitution lesson + status = "new"; + } else if ( + !newIds.includes(teacher.id) && + oldIds.includes(teacher.id) + ) { + // Mark teacher as being rremoved if they are only linked to the amended lesson + status = "removed"; + } + return { ...teacher, status: status }; + }); + return teachersWithStatus; + }, + roomsWithStatus() { + const oldIds = this.substitution.amends.rooms.map((room) => room.id); + const newIds = this.substitution.rooms.map((room) => room.id); + const allRooms = new Set( + this.substitution.amends.rooms.concat( + this.substitution.rooms.filter((r) => !oldIds.includes(r.id)), + ), + ); + let roomsWithStatus = Array.from(allRooms).map((room) => { + let status = "regular"; + if (newIds.includes(room.id) && !oldIds.includes(room.id)) { + status = "new"; + } else if ( + newIds.length && + !newIds.includes(room.id) && + oldIds.includes(room.id) + ) { + status = "removed"; + } + return { ...room, status: status }; + }); + return roomsWithStatus; + }, + subject() { + if (this.substitution.subject) { + return this.substitution.subject; + } else if (this.substitution.course?.subject) { + return this.substitution.course.subject; + } else if (this.substitution.amends?.subject) { + return this.substitution.amends.subject; + } + return undefined; + }, + }, +}; +</script> + +<style scoped> +.main-body { + display: grid; + align-items: center; + grid-template-columns: 2fr 1fr 2fr 3fr 1fr 2fr; + gap: 1em; +} +.vertical { + grid-template-columns: 1fr; +} +.justify-self-end { + justify-self: end; +} +</style> diff --git a/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionInformation.vue b/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionInformation.vue new file mode 100644 index 0000000000000000000000000000000000000000..e64eb9c1999b45e5020583ec4b168385d88cf838 --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionInformation.vue @@ -0,0 +1,74 @@ +<script setup> +import SubstitutionStatus from "./SubstitutionStatus.vue"; + +import { DateTime } from "luxon"; +</script> + +<template> + <div class="full-width grid"> + <div class="d-flex"> + <substitution-status :substitution="substitution" /> + <div class="text-right d-flex flex-column fit-content"> + <time :datetime="substitution.datetimeStart" class="text-no-wrap"> + {{ $d(toDateTime(substitution.datetimeStart), "shortTime") }} + </time> + <time :datetime="substitution.datetimeEnd" class="text-no-wrap"> + {{ $d(toDateTime(substitution.datetimeEnd), "shortTime") }} + </time> + </div> + </div> + <span class="text-subtitle-1 font-weight-medium"> + {{ course?.name }} + </span> + </div> +</template> + +<script> +export default { + name: "SubstitutionInformation", + props: { + substitution: { + type: Object, + required: true, + }, + }, + methods: { + toDateTime(dateString) { + return DateTime.fromISO(dateString); + }, + }, + computed: { + course() { + if (this.substitution.course) { + return this.substitution.course; + } else if (this.substitution.amends?.course) { + return this.substitution.amends.course; + } + return undefined; + }, + }, +}; +</script> + +<style scoped> +.grid { + display: grid; + align-items: center; + gap: 1em; + grid-template-columns: 1fr 1fr; + align-content: unset; +} + +.grid:last-child { + justify-self: end; + justify-content: end; +} + +.fit-content { + width: fit-content; +} + +.gap { + gap: 0.25em; +} +</style> diff --git a/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionLoader.vue b/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionLoader.vue new file mode 100644 index 0000000000000000000000000000000000000000..8bb7e60a968e1b2e38b335d10b91a67e4de6250e --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionLoader.vue @@ -0,0 +1,32 @@ +<template> + <v-card class="my-2 full-width"> + <div class="full-width d-flex flex-column align-stretch flex-md-row"> + <v-card-text> + <v-skeleton-loader + type="avatar, heading, chip" + class="d-flex full-width align-center gap" + height="100%" + /> + </v-card-text> + <v-card-text> + <v-skeleton-loader + type="heading" + class="d-flex full-width align-center gap" + height="100%" + /> + </v-card-text> + <v-card-text> + <v-skeleton-loader + type="button" + class="d-flex full-width align-center justify-end gap" + height="100%" + /> + </v-card-text> + </div> + </v-card> +</template> +<script> +export default { + name: "SubstitutionLoader", +}; +</script> diff --git a/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionOverview.vue b/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionOverview.vue new file mode 100644 index 0000000000000000000000000000000000000000..6ec5bcec243090e8c574adf91f8f4727da887a3c --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionOverview.vue @@ -0,0 +1,186 @@ +<script setup> +import SubstitutionCard from "./SubstitutionCard.vue"; +import SubstitutionLoader from "./SubstitutionLoader.vue"; +import InfiniteScrollingDateSortedCRUDIterator from "aleksis.core/components/generic/InfiniteScrollingDateSortedCRUDIterator.vue"; + +import { + amendedLessonsFromAbsences, + createOrUpdateSubstitutions, + deleteAmendLessons, + gqlGroups, + gqlRooms, + gqlSubjects, + gqlTeachers, +} from "../amendLesson.graphql"; + +import { DateTime } from "luxon"; +</script> + +<template> + <infinite-scrolling-date-sorted-c-r-u-d-iterator + :gql-query="gqlQuery" + :gql-additional-query-args="gqlQueryArgs" + :gql-patch-mutation="gqlPatchMutation" + :get-patch-data="gqlGetPatchData" + i18n-key="chronos.substitutions.overview" + :enable-search="true" + :enable-create="false" + :show-create="false" + :enable-delete="false" + :enable-edit="true" + :elevated="false" + :force-model-item-update="true" + :day-increment="3" + empty-icon="mdi-account-multiple-check-outline" + ref="iterator" + > + <template #additionalActions="{ attrs, on }"> + <v-autocomplete + :items="groups" + item-text="name" + item-value="id" + clearable + filled + dense + hide-details + :placeholder="$t('chronos.substitutions.overview.filter.groups')" + :loading="$apollo.queries.groups.loading" + :value="objId" + @input="changeGroup" + @click:clear="changeGroup" + /> + <v-autocomplete + :items="amendableTeachers" + item-text="fullName" + item-value="id" + clearable + filled + dense + hide-details + :placeholder="$t('chronos.substitutions.overview.filter.teachers')" + :loading="$apollo.queries.amendableTeachers.loading" + :value="teacherId" + @input="changeTeacher" + @click:clear="changeTeacher" + /> + <v-switch + :loading="$apollo.queries.groups.loading" + :label="$t('chronos.substitutions.overview.filter.missing')" + v-model="incomplete" + dense + inset + hide-details + class="ml-6" + /> + + <v-alert type="info" outlined dense class="full-width"> + <v-row align="center" no-gutters> + <v-col cols="12" md="9"> + {{ $t("chronos.substitutions.overview.info_alert.text") }} + </v-col> + <v-col cols="12" md="3" align="right"> + <v-btn + color="info" + outlined + small + :to="{ name: 'kolego.absences' }" + > + {{ $t("chronos.substitutions.overview.info_alert.button") }} + </v-btn> + </v-col> + </v-row> + </v-alert> + </template> + + <template #item="{ item, lastQuery }"> + <substitution-card + :substitution="item" + :affected-query="lastQuery" + :is-create="false" + :gql-patch-mutation="gqlPatchMutation" + :gql-delete-mutation="gqlDeleteMutation" + :amendable-rooms="amendableRooms" + :amendable-subjects="amendableSubjects" + :amendable-teachers="amendableTeachers" + @delete="handleDelete" + /> + </template> + + <template #itemLoader> + <substitution-loader /> + </template> + </infinite-scrolling-date-sorted-c-r-u-d-iterator> +</template> + +<script> +export default { + name: "SubstitutionOverview", + props: { + objId: { + type: [Number, String], + required: false, + default: null, + }, + teacherId: { + type: [Number, String], + required: false, + default: null, + }, + }, + data() { + return { + gqlQuery: amendedLessonsFromAbsences, + gqlPatchMutation: createOrUpdateSubstitutions, + gqlDeleteMutation: deleteAmendLessons, + groups: [], + amendableTeachers: [], + incomplete: false, + }; + }, + methods: { + gqlGetPatchData(item) { + return { id: item.id, teachers: item.teachers }; + }, + changeGroup(group) { + this.$router.push({ + name: "chronos.substitutionOverview", + params: { + objId: group ? group : "all", + teacherId: this.teacherId, + }, + hash: this.$route.hash, + }); + this.$refs.iterator.resetDate(); + }, + changeTeacher(teacher) { + this.$router.push({ + name: "chronos.substitutionOverview", + params: { + teacherId: teacher ? teacher : "all", + objId: this.objId, + }, + hash: this.$route.hash, + }); + this.$refs.iterator.resetDate(); + }, + handleDelete(datetime) { + this.$refs.iterator.refetchDay(DateTime.fromISO(datetime).toISODate()); + }, + }, + computed: { + gqlQueryArgs() { + return { + objId: this.objId === "all" ? null : Number(this.objId), + teacher: this.teacherId === "all" ? null : Number(this.teacherId), + incomplete: !!this.incomplete, + }; + }, + }, + apollo: { + groups: gqlGroups, + amendableRooms: gqlRooms, + amendableSubjects: gqlSubjects, + amendableTeachers: gqlTeachers, + }, +}; +</script> diff --git a/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionStatus.vue b/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionStatus.vue new file mode 100644 index 0000000000000000000000000000000000000000..83990f4539b0fbbe185d3f7ad48414d06539cd8c --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/substitutions/SubstitutionStatus.vue @@ -0,0 +1,79 @@ +<template> + <v-tooltip bottom> + <template #activator="{ on, attrs }"> + <v-icon + :color="currentStatus?.color" + class="mr-md-4" + v-on="on" + v-bind="attrs" + >{{ currentStatus?.icon }}</v-icon + > + </template> + <span>{{ currentStatus?.text }}</span> + </v-tooltip> +</template> + +<script> +export default { + name: "SubstitutionStatus", + data() { + return { + statusChoices: [ + { + name: "unedited", + text: this.$t("chronos.substitutions.overview.status.unedited"), + icon: "mdi-calendar-question-outline", + color: "warning", + }, + { + name: "substituted", + text: this.$t("chronos.substitutions.overview.status.substituted"), + icon: "mdi-calendar-edit-outline", + color: "success", + }, + { + name: "cancelled", + text: this.$t("chronos.substitutions.overview.status.cancelled"), + icon: "mdi-cancel", + color: "error", + }, + ], + statusTimeout: null, + currentStatusName: "", + }; + }, + props: { + substitution: { + type: Object, + required: true, + }, + }, + computed: { + currentStatus() { + return this.statusChoices.find((s) => s.name === this.currentStatusName); + }, + }, + methods: { + updateStatus() { + if (this.substitution.id.startsWith("DUMMY")) { + this.currentStatusName = "unedited"; + } else if (this.substitution.cancelled) { + this.currentStatusName = "cancelled"; + } else { + this.currentStatusName = "substituted"; + } + }, + }, + watch: { + substitution: { + handler() { + this.updateStatus(); + }, + deep: true, + }, + }, + mounted() { + this.updateStatus(); + }, +}; +</script> diff --git a/aleksis/apps/chronos/frontend/components/timetables.graphql b/aleksis/apps/chronos/frontend/components/timetables.graphql index 5bbe46f734f1981ad1c2648d50a8a6e8d7bd04fc..6e575f2726e920e31656c2470495b12399532132 100644 --- a/aleksis/apps/chronos/frontend/components/timetables.graphql +++ b/aleksis/apps/chronos/frontend/components/timetables.graphql @@ -7,3 +7,7 @@ query gqlAvailableTimetables { shortName } } + +query gqlTimetableDays { + timetableDays +} diff --git a/aleksis/apps/chronos/frontend/index.js b/aleksis/apps/chronos/frontend/index.js index 1893e4f184e67177fb6f3d0e9d2f8d661c643d55..34a0380b03bc23645f765e08149cb0ce37a9a3a2 100644 --- a/aleksis/apps/chronos/frontend/index.js +++ b/aleksis/apps/chronos/frontend/index.js @@ -1,5 +1,5 @@ import { hasPersonValidator } from "aleksis.core/routeValidators"; -import Timetable from "./components/Timetable.vue"; + import Substitutions from "./components/Substitutions.vue"; import { DateTime } from "luxon"; @@ -14,7 +14,7 @@ export default { children: [ { path: "timetable/", - component: Timetable, + component: () => import("./components/Timetable.vue"), name: "chronos.timetable", meta: { inMenu: true, @@ -27,7 +27,7 @@ export default { }, { path: "timetable/:type/:id/", - component: Timetable, + component: () => import("./components/Timetable.vue"), name: "chronos.timetableWithId", meta: { permission: "chronos.view_timetable_overview_rule", @@ -36,7 +36,7 @@ export default { children: [ { path: ":view(month|week|day)/:year(\\d\\d\\d\\d)/:month(\\d\\d)/:day(\\d\\d)/", - component: Timetable, + component: () => import("./components/Timetable.vue"), name: "chronos.timetableWithIdAndParams", meta: { permission: "chronos.view_timetable_overview_rule", @@ -45,6 +45,41 @@ export default { }, ], }, + { + path: "substitution_overview/", + component: () => + import("./components/substitutions/SubstitutionOverview.vue"), + redirect: () => { + return { + path: "substitution_overview/group/all/person/all/", + hash: "#" + DateTime.now().toISODate(), + }; + }, + name: "chronos.substitutionOverviewLanding", + props: true, + meta: { + inMenu: true, + icon: "mdi-account-convert-outline", + iconActive: "mdi-account-convert", + titleKey: "chronos.substitutions.overview.menu_title", + toolbarTitle: "chronos.substitutions.overview.menu_title", + permission: "chronos.view_substitution_overview_rule", + }, + children: [ + { + path: "group/:objId(all|\\d+)?/person/:teacherId(all|\\d+)?/", + component: () => + import("./components/substitutions/SubstitutionOverview.vue"), + name: "chronos.substitutionOverview", + meta: { + titleKey: "chronos.substitutions.overview.menu_title", + toolbarTitle: "chronos.substitutions.overview.menu_title", + permission: "chronos.view_substitution_overview_rule", + fullWidth: true, + }, + }, + ], + }, { path: "substitutions/print/", component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), diff --git a/aleksis/apps/chronos/frontend/messages/de.json b/aleksis/apps/chronos/frontend/messages/de.json index a7c50a848e25f14b0cc24a36f0cf80df2e0efd94..174e7a1911a1d7c57f7d2da763772cf0d3983c62 100644 --- a/aleksis/apps/chronos/frontend/messages/de.json +++ b/aleksis/apps/chronos/frontend/messages/de.json @@ -22,20 +22,57 @@ }, "menu_title": "Stundenpläne", "substitutions": { + "affected_groups": "Betroffene Gruppen", + "affected_teachers": "Betroffene Lehrkräfte", + "all_day": "Ganztägig", + "cancelled": "Entfällt", + "groups": "Gruppen", "menu_title": "Vertretungen", + "no_substitutions": "Keine Vertretungen", + "notes": "Notizen", + "overview": { + "cancel": { + "cancelled": "Fällt aus", + "not_cancelled": "Findet statt" + }, + "comment": "Kommentar", + "filter": { + "groups": "Nach Gruppen filtern", + "missing": "Nur unvollständige Stunden anzeigen", + "teachers": "Nach Lehrkraft filtern" + }, + "info_alert": { + "button": "Abwesenheiten verwalten", + "text": "Diese Seite zeigt alle Unterrichtsstunden die mit bekannten Abwesenheiten von ihren entsprechenden Lehrkräften kollidieren." + }, + "menu_title": "Vertretungen planen", + "rooms": { + "label": "Räume" + }, + "status": { + "cancelled": "Entfällt", + "substituted": "Vertreten", + "unedited": "Unbearbeitet" + }, + "subject": { + "label": "Fach" + }, + "teacher": { + "label": "Lehrkräfte", + "status": { + "absent": "Abwesend", + "new": "Vertretungslehrkraft", + "regular": "Regulär" + } + }, + "title_plural": "Vertretungen planen" + }, "print": "Drucken", - "groups": "Gruppen", - "time": "Zeit", - "teachers": "Lehrer", - "subject": "Fach", "rooms": "Räume", - "notes": "Notizen", + "subject": "Fach", "supervision": "Aufsicht", - "cancelled": "Entfällt", - "affected_teachers": "Betroffene Lehrer", - "affected_groups": "Betroffene Gruppen", - "all_day": "Ganztägig", - "no_substitutions": "Keine Vertretungen" + "teachers": "Lehrkräfte", + "time": "Zeit" }, "supervisions": { "menu_title_daily": "Aufsichten", @@ -50,6 +87,7 @@ "description": "Wählen Sie auf der linken Seite einen Stundenplan aus, um ihn hier anzuzeigen", "title": "Kein Stundenplan ausgewählt" }, + "no_timetables_in_term": "Keine Stundenpläne im ausgewählten Schuljahr", "prev": "Vorheriger Stundenplan", "search": "Stundenpläne suchen", "select": "Stundenplan auswählen", diff --git a/aleksis/apps/chronos/frontend/messages/en.json b/aleksis/apps/chronos/frontend/messages/en.json index 7ca0d3ee9fcc7774032eb2c2fa13950c361d898f..facaeb7223f8f34551f36ee14563008f31592de6 100644 --- a/aleksis/apps/chronos/frontend/messages/en.json +++ b/aleksis/apps/chronos/frontend/messages/en.json @@ -17,27 +17,12 @@ "groups": "Groups", "teachers": "Teachers", "rooms": "Rooms" - } + }, + "no_timetables_in_term": "No timetables in the selected school term." }, "lessons": { "menu_title_daily": "Daily lessons" }, - "substitutions": { - "menu_title": "Substitutions", - "print": "Print", - "groups": "Groups", - "time": "Time", - "teachers": "Teachers", - "subject": "Subject", - "rooms": "Rooms", - "notes": "Notes", - "supervision": "Supervision", - "cancelled": "Cancelled", - "affected_teachers": "Affected teachers", - "affected_groups": "Affected groups", - "all_day": "All day", - "no_substitutions": "No substitutions" - }, "supervisions": { "title": "Supervision", "menu_title_daily": "Daily supervisions" @@ -58,6 +43,59 @@ "cancelled": "Cancelled", "comment": "Comment" } + }, + "substitutions": { + "menu_title": "Substitutions", + "print": "Print", + "groups": "Groups", + "time": "Time", + "teachers": "Teachers", + "subject": "Subject", + "rooms": "Rooms", + "notes": "Notes", + "supervision": "Supervision", + "cancelled": "Cancelled", + "affected_teachers": "Affected teachers", + "affected_groups": "Affected groups", + "all_day": "All day", + "no_substitutions": "No substitutions", + "overview": { + "menu_title": "Plan substitutions", + "title_plural": "Plan substitutions", + "info_alert": { + "text": "This page shows all lessons that collide with known absences of their respective teachers.", + "button": "Manage absences" + }, + "filter": { + "groups": "Filter by groups", + "teachers": "Filter by teacher", + "missing": "Only show incomplete lessons" + }, + "cancel": { + "cancelled": "Cancelled", + "not_cancelled": "Taking place" + }, + "status": { + "unedited": "Unedited", + "substituted": "Substituted", + "cancelled": "Cancelled" + }, + "teacher": { + "label": "Teachers", + "status": { + "absent": "Absent", + "regular": "Regular", + "new": "Substitution teacher" + } + }, + "rooms": { + "label": "Rooms" + }, + "subject": { + "label": "Subject" + }, + "comment": "Comment" + } } } } diff --git a/aleksis/apps/chronos/frontend/messages/ru.json b/aleksis/apps/chronos/frontend/messages/ru.json index fb663b2ba0a8d4ea0cdb26857b19f8e7856a1816..6f6c5c9a26664c1c2a76a8bd68dfc5cf5137a51b 100644 --- a/aleksis/apps/chronos/frontend/messages/ru.json +++ b/aleksis/apps/chronos/frontend/messages/ru.json @@ -11,7 +11,23 @@ }, "menu_title": "РаÑпиÑаниÑ", "substitutions": { - "menu_title": "Замены" + "cancelled": "Отменено", + "groups": "Группы", + "menu_title": "Замены", + "notes": "Заметки", + "overview": { + "cancel": { + "cancelled": "Отменено" + }, + "rooms": { + "label": "Комнаты" + }, + "status": { + "cancelled": "Отменено" + } + }, + "rooms": "Комнаты", + "time": "ВремÑ" }, "supervisions": { "menu_title_daily": "Ежедневные наблюдениÑ" diff --git a/aleksis/apps/chronos/frontend/messages/uk.json b/aleksis/apps/chronos/frontend/messages/uk.json index 09d0c8ac28b91229ad842db89ed7c0b65514af03..4c18497f9f7f6566b497060c999292f077663d82 100644 --- a/aleksis/apps/chronos/frontend/messages/uk.json +++ b/aleksis/apps/chronos/frontend/messages/uk.json @@ -3,25 +3,76 @@ "event": { "amend": { "cancelled": "СкаÑовано", - "rooms": "Кімнати" - } + "comment": "Коментар", + "delete_button": "Скинути", + "delete_dialog": "Ви дійÑно хочете видалити цю заміну?", + "delete_success": "Заміна уÑпішно видалена.", + "edit_button": "Змінити", + "rooms": "Кімнати", + "subject": "Предмет", + "teachers": "Викладачі", + "title": "Змінити урок" + }, + "current_changes": "Поточні зміни", + "no_room": "Без кімнат", + "no_teacher": "Без викладачів" }, "lessons": { "menu_title_daily": "Щоденні уроки" }, "menu_title": "Розклади", "substitutions": { - "menu_title": "Заміни" + "cancelled": "СкаÑовано", + "groups": "Групи", + "menu_title": "Заміни", + "notes": "Ðотатки", + "overview": { + "cancel": { + "cancelled": "СкаÑовано" + }, + "comment": "Коментар", + "filter": { + "missing": "Показати лише неповні уроки" + }, + "rooms": { + "label": "Кімнати" + }, + "status": { + "cancelled": "СкаÑовано" + }, + "subject": { + "label": "Предмет" + }, + "teacher": { + "label": "Викладачі" + } + }, + "rooms": "Кімнати", + "subject": "Предмет", + "supervision": "Контроль", + "teachers": "Викладачі", + "time": "ЧаÑ" }, "supervisions": { - "menu_title_daily": "Щоденні ÑпоÑтереженнÑ" + "menu_title_daily": "Щоденні ÑпоÑтереженнÑ", + "title": "Контроль" }, "timetable": { + "menu_title": "Розклади", "menu_title_all": "УÑÑ– розклади", "menu_title_my": "Мій розклад", + "next": "ÐаÑтупний розклад", + "no_timetable_selected": { + "description": "Виберіть розклад занÑть ліворуч Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ³Ð»Ñду його тут", + "title": "Розклад не обраний" + }, + "prev": "Попередній розклад", + "search": "Пошук розкладів", + "select": "Оберіть розклад", "types": { "groups": "Групи", - "rooms": "Кімнати" + "rooms": "Кімнати", + "teachers": "Викладачі" } } } diff --git a/aleksis/apps/chronos/locale/ar/LC_MESSAGES/django.po b/aleksis/apps/chronos/locale/ar/LC_MESSAGES/django.po index 67e0bba4ca0e30d6ab451ba309c91fd6048dada5..48f30337626dd0300b7bab3854942507c9f1589f 100644 --- a/aleksis/apps/chronos/locale/ar/LC_MESSAGES/django.po +++ b/aleksis/apps/chronos/locale/ar/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-17 10:48+0200\n" +"POT-Creation-Date: 2024-12-08 13:24+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -18,14 +18,34 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" -#: aleksis/apps/chronos/model_extensions.py:9 +#: aleksis/apps/chronos/model_extensions.py:10 msgid "Can view group timetable" msgstr "" -#: aleksis/apps/chronos/model_extensions.py:13 +#: aleksis/apps/chronos/model_extensions.py:14 +msgid "Can manage group substitutions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:18 msgid "Can view person timetable" msgstr "" +#: aleksis/apps/chronos/model_extensions.py:22 +msgid "Can view group supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:26 +msgid "Can view person supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:30 +msgid "Can view room supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:34 +msgid "Can view course timetable" +msgstr "" + #: aleksis/apps/chronos/models.py:48 msgid "Number of days shown in the plan" msgstr "" @@ -63,88 +83,112 @@ msgid "Can view all person timetables" msgstr "" #: aleksis/apps/chronos/models.py:150 -msgid "Can view timetable overview" +msgid "Can view all course timetables" msgstr "" #: aleksis/apps/chronos/models.py:151 +msgid "Can view timetable overview" +msgstr "" + +#: aleksis/apps/chronos/models.py:152 msgid "Can view substitutions table" msgstr "" -#: aleksis/apps/chronos/models.py:159 +#: aleksis/apps/chronos/models.py:153 +msgid "Can view all room supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:154 +msgid "Can view all group supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:155 +msgid "Can view all person supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:156 +msgid "Can manage all substitutions" +msgstr "" + +#: aleksis/apps/chronos/models.py:164 msgid "Lessons" msgstr "" -#: aleksis/apps/chronos/models.py:163 +#: aleksis/apps/chronos/models.py:168 msgid "Name" msgstr "" -#: aleksis/apps/chronos/models.py:166 +#: aleksis/apps/chronos/models.py:171 msgid "Start slot number" msgstr "" -#: aleksis/apps/chronos/models.py:169 +#: aleksis/apps/chronos/models.py:174 msgid "End slot number" msgstr "" -#: aleksis/apps/chronos/models.py:173 +#: aleksis/apps/chronos/models.py:178 msgid "Course" msgstr "" -#: aleksis/apps/chronos/models.py:179 +#: aleksis/apps/chronos/models.py:184 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:1 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:26 msgid "Groups" msgstr "" -#: aleksis/apps/chronos/models.py:185 +#: aleksis/apps/chronos/models.py:190 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:4 msgid "Rooms" msgstr "" -#: aleksis/apps/chronos/models.py:191 +#: aleksis/apps/chronos/models.py:196 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:3 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:28 #: aleksis/apps/chronos/templates/chronos/supervision_event_description.txt:1 msgid "Teachers" msgstr "" -#: aleksis/apps/chronos/models.py:198 +#: aleksis/apps/chronos/models.py:203 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:2 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:29 msgid "Subject" msgstr "" -#: aleksis/apps/chronos/models.py:206 +#: aleksis/apps/chronos/models.py:211 #: aleksis/apps/chronos/templates/chronos/partials/subs/badge.html:4 msgid "Cancelled" msgstr "" -#: aleksis/apps/chronos/models.py:210 +#: aleksis/apps/chronos/models.py:215 msgid "Comment" msgstr "" -#: aleksis/apps/chronos/models.py:254 aleksis/apps/chronos/models.py:266 -#: aleksis/apps/chronos/models.py:278 +#: aleksis/apps/chronos/models.py:220 +msgid "Is this a current change?" +msgstr "" + +#: aleksis/apps/chronos/models.py:262 aleksis/apps/chronos/models.py:274 +#: aleksis/apps/chronos/models.py:286 msgid "{} (instead of {})" msgstr "" -#: aleksis/apps/chronos/models.py:283 aleksis/apps/chronos/models.py:307 +#: aleksis/apps/chronos/models.py:291 aleksis/apps/chronos/models.py:315 msgid "Lesson" msgstr "" -#: aleksis/apps/chronos/models.py:467 +#: aleksis/apps/chronos/models.py:630 msgid "Lesson Event" msgstr "" -#: aleksis/apps/chronos/models.py:468 +#: aleksis/apps/chronos/models.py:631 msgid "Lesson Events" msgstr "" -#: aleksis/apps/chronos/models.py:475 +#: aleksis/apps/chronos/models.py:638 msgid "Supervisions" msgstr "" -#: aleksis/apps/chronos/models.py:483 +#: aleksis/apps/chronos/models.py:646 msgid "Supervision: {}" msgstr "" @@ -196,6 +240,38 @@ msgstr "" msgid "Supervision calendar feed color" msgstr "" +#: aleksis/apps/chronos/preferences.py:132 +msgid "Days of the week that appear in the timetable" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:134 +msgid "Sunday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:135 +msgid "Monday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:136 +msgid "Tuesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:137 +msgid "Wednesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:138 +msgid "Thursday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:139 +msgid "Friday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:140 +msgid "Saturday" +msgstr "" + #: aleksis/apps/chronos/templates/chronos/partials/headerbox.html:10 msgid "Absent teachers" msgstr "" diff --git a/aleksis/apps/chronos/locale/de_DE/LC_MESSAGES/django.po b/aleksis/apps/chronos/locale/de_DE/LC_MESSAGES/django.po index d53ce3b87c37282efc92294219a0fa18d0cdc4d2..c64f0b51eb786c020e9911d4ebe3c1612b72bd84 100644 --- a/aleksis/apps/chronos/locale/de_DE/LC_MESSAGES/django.po +++ b/aleksis/apps/chronos/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: unnamed project\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-17 10:48+0200\n" +"POT-Creation-Date: 2024-12-08 13:24+0100\n" "PO-Revision-Date: 2024-10-17 11:54+0200\n" "Last-Translator: Jonathan Weth <git@jonathanewth.de>\n" "Language-Team: English\n" @@ -19,14 +19,44 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Gtranslator 47.0\n" -#: aleksis/apps/chronos/model_extensions.py:9 +#: aleksis/apps/chronos/model_extensions.py:10 msgid "Can view group timetable" msgstr "Kann Gruppenstundenpläne sehen" -#: aleksis/apps/chronos/model_extensions.py:13 +#: aleksis/apps/chronos/model_extensions.py:14 +#, fuzzy +#| msgid "Manage substitution" +msgid "Can manage group substitutions" +msgstr "Vertretung verwalten" + +#: aleksis/apps/chronos/model_extensions.py:18 msgid "Can view person timetable" msgstr "Kann Personenstundenpläne sehen" +#: aleksis/apps/chronos/model_extensions.py:22 +#, fuzzy +#| msgid "Can view all supervisions per day" +msgid "Can view group supervisions" +msgstr "Kann alle Tagesaufsichten sehen" + +#: aleksis/apps/chronos/model_extensions.py:26 +#, fuzzy +#| msgid "Can view all supervisions per day" +msgid "Can view person supervisions" +msgstr "Kann alle Tagesaufsichten sehen" + +#: aleksis/apps/chronos/model_extensions.py:30 +#, fuzzy +#| msgid "Can view all supervisions per day" +msgid "Can view room supervisions" +msgstr "Kann alle Tagesaufsichten sehen" + +#: aleksis/apps/chronos/model_extensions.py:34 +#, fuzzy +#| msgid "Can view group timetable" +msgid "Can view course timetable" +msgstr "Kann Gruppenstundenpläne sehen" + #: aleksis/apps/chronos/models.py:48 msgid "Number of days shown in the plan" msgstr "Anzahl der Tage, die im Plan angezeigt werden" @@ -64,88 +94,122 @@ msgid "Can view all person timetables" msgstr "Kann alle Personenstundenpläne sehen" #: aleksis/apps/chronos/models.py:150 +#, fuzzy +#| msgid "Can view all group timetables" +msgid "Can view all course timetables" +msgstr "Kann alle Gruppenstundenpläne sehen" + +#: aleksis/apps/chronos/models.py:151 msgid "Can view timetable overview" msgstr "Kann Stundenplanübersicht sehen" -#: aleksis/apps/chronos/models.py:151 +#: aleksis/apps/chronos/models.py:152 msgid "Can view substitutions table" msgstr "Kann Vertretungstabelle sehen" -#: aleksis/apps/chronos/models.py:159 +#: aleksis/apps/chronos/models.py:153 +#, fuzzy +#| msgid "Can view all supervisions per day" +msgid "Can view all room supervisions" +msgstr "Kann alle Tagesaufsichten sehen" + +#: aleksis/apps/chronos/models.py:154 +#, fuzzy +#| msgid "Can view all supervisions per day" +msgid "Can view all group supervisions" +msgstr "Kann alle Tagesaufsichten sehen" + +#: aleksis/apps/chronos/models.py:155 +#, fuzzy +#| msgid "Can view all supervisions per day" +msgid "Can view all person supervisions" +msgstr "Kann alle Tagesaufsichten sehen" + +#: aleksis/apps/chronos/models.py:156 +#, fuzzy +#| msgid "Manage substitution" +msgid "Can manage all substitutions" +msgstr "Vertretung verwalten" + +#: aleksis/apps/chronos/models.py:164 msgid "Lessons" msgstr "Unterrichtsstunden" -#: aleksis/apps/chronos/models.py:163 +#: aleksis/apps/chronos/models.py:168 msgid "Name" msgstr "Name" -#: aleksis/apps/chronos/models.py:166 +#: aleksis/apps/chronos/models.py:171 msgid "Start slot number" msgstr "Startzeitfenster-Nummer" -#: aleksis/apps/chronos/models.py:169 +#: aleksis/apps/chronos/models.py:174 msgid "End slot number" msgstr "Endzeitfenster-Nummer" -#: aleksis/apps/chronos/models.py:173 +#: aleksis/apps/chronos/models.py:178 msgid "Course" msgstr "Kurs" -#: aleksis/apps/chronos/models.py:179 +#: aleksis/apps/chronos/models.py:184 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:1 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:26 msgid "Groups" msgstr "Gruppen" -#: aleksis/apps/chronos/models.py:185 +#: aleksis/apps/chronos/models.py:190 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:4 msgid "Rooms" msgstr "Räume" -#: aleksis/apps/chronos/models.py:191 +#: aleksis/apps/chronos/models.py:196 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:3 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:28 #: aleksis/apps/chronos/templates/chronos/supervision_event_description.txt:1 msgid "Teachers" msgstr "Lehrkräfte" -#: aleksis/apps/chronos/models.py:198 +#: aleksis/apps/chronos/models.py:203 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:2 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:29 msgid "Subject" msgstr "Fach" -#: aleksis/apps/chronos/models.py:206 +#: aleksis/apps/chronos/models.py:211 #: aleksis/apps/chronos/templates/chronos/partials/subs/badge.html:4 msgid "Cancelled" msgstr "Entfall" -#: aleksis/apps/chronos/models.py:210 +#: aleksis/apps/chronos/models.py:215 msgid "Comment" msgstr "Kommentar" -#: aleksis/apps/chronos/models.py:254 aleksis/apps/chronos/models.py:266 -#: aleksis/apps/chronos/models.py:278 +#: aleksis/apps/chronos/models.py:220 +msgid "Is this a current change?" +msgstr "" + +#: aleksis/apps/chronos/models.py:262 aleksis/apps/chronos/models.py:274 +#: aleksis/apps/chronos/models.py:286 msgid "{} (instead of {})" msgstr "{} (statt {})" -#: aleksis/apps/chronos/models.py:283 aleksis/apps/chronos/models.py:307 +#: aleksis/apps/chronos/models.py:291 aleksis/apps/chronos/models.py:315 msgid "Lesson" msgstr "Unterrichtsstunde" -#: aleksis/apps/chronos/models.py:467 +#: aleksis/apps/chronos/models.py:630 msgid "Lesson Event" msgstr "Unterrichtsstunde" -#: aleksis/apps/chronos/models.py:468 +#: aleksis/apps/chronos/models.py:631 msgid "Lesson Events" msgstr "Unterrichtsstunden" -#: aleksis/apps/chronos/models.py:475 +#: aleksis/apps/chronos/models.py:638 msgid "Supervisions" msgstr "Aufsichten" -#: aleksis/apps/chronos/models.py:483 +#: aleksis/apps/chronos/models.py:646 msgid "Supervision: {}" msgstr "Aufsicht: {}" @@ -158,12 +222,8 @@ msgid "Use parent groups in timetable views" msgstr "Elterngruppen in Stundenplanansichten benutzen" #: aleksis/apps/chronos/preferences.py:31 -msgid "" -"If a lesson or substitution has only one group and this group has parent " -"groups, show the parent groups instead of the original group." -msgstr "" -"Wenn eine Stunde oder Vertretung nur eine Gruppe hat und diese Gruppe " -"Elterngruppen hat, zeige die Elterngruppen anstelle der eigentlichen Gruppe." +msgid "If a lesson or substitution has only one group and this group has parent groups, show the parent groups instead of the original group." +msgstr "Wenn eine Stunde oder Vertretung nur eine Gruppe hat und diese Gruppe Elterngruppen hat, zeige die Elterngruppen anstelle der eigentlichen Gruppe." #: aleksis/apps/chronos/preferences.py:44 msgid "Relevant days for substitution plans" @@ -175,21 +235,15 @@ msgstr "Zeit zu der Vertretungspläne zum nächsten Tag wechseln" #: aleksis/apps/chronos/preferences.py:70 msgid "Number of days shown on substitutions print view" -msgstr "" -"Anzahl der Tage, die in der Druckansicht des Vertretungsplanes angezeigt " -"werden soll" +msgstr "Anzahl der Tage, die in der Druckansicht des Vertretungsplanes angezeigt werden soll" #: aleksis/apps/chronos/preferences.py:78 msgid "Show header box in substitution views" msgstr "Kopfbox in Vertretungsplänen anzeigen" #: aleksis/apps/chronos/preferences.py:88 -msgid "" -"Show parent groups in header box in substitution views instead of original " -"groups" -msgstr "" -"Elterngruppen in der Kopfbox in Vertretungsplänen an Stelle der eigentlichen " -"Gruppen anzeigen" +msgid "Show parent groups in header box in substitution views instead of original groups" +msgstr "Elterngruppen in der Kopfbox in Vertretungsplänen an Stelle der eigentlichen Gruppen anzeigen" #: aleksis/apps/chronos/preferences.py:99 msgid "Group types to show in timetables" @@ -207,6 +261,40 @@ msgstr "Unterrichtsstunden-Kalenderfarbe" msgid "Supervision calendar feed color" msgstr "Aufsichts-Kalenderfarbe" +#: aleksis/apps/chronos/preferences.py:132 +msgid "Days of the week that appear in the timetable" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:134 +msgid "Sunday" +msgstr "Sonntag" + +#: aleksis/apps/chronos/preferences.py:135 +msgid "Monday" +msgstr "Montag" + +#: aleksis/apps/chronos/preferences.py:136 +msgid "Tuesday" +msgstr "Dienstag" + +#: aleksis/apps/chronos/preferences.py:137 +msgid "Wednesday" +msgstr "Mittwoch" + +#: aleksis/apps/chronos/preferences.py:138 +msgid "Thursday" +msgstr "Donnerstag" + +#: aleksis/apps/chronos/preferences.py:139 +#, fuzzy +#| msgid "Holiday" +msgid "Friday" +msgstr "Ferien" + +#: aleksis/apps/chronos/preferences.py:140 +msgid "Saturday" +msgstr "Samstag" + #: aleksis/apps/chronos/templates/chronos/partials/headerbox.html:10 msgid "Absent teachers" msgstr "Abwesende Lehrkräfte" @@ -277,11 +365,8 @@ msgstr "Bereiche" #~ msgid "The validity range must be within the school term." #~ msgstr "Der Gültigkeitsbereich muss innerhalb des Schuljahres liegen." -#~ msgid "" -#~ "There is already a validity range for this time or a part of this time." -#~ msgstr "" -#~ "Es gibt bereits einen Gültigkeitsbereich für diesen Zeitraum oder einen " -#~ "Teil diesen Zeitraumes." +#~ msgid "There is already a validity range for this time or a part of this time." +#~ msgstr "Es gibt bereits einen Gültigkeitsbereich für diesen Zeitraum oder einen Teil diesen Zeitraumes." #~ msgid "Validity range" #~ msgstr "Gültigkeitsbereich" @@ -341,8 +426,7 @@ msgstr "Bereiche" #~ msgstr "Entfällt für Lehrkräfte?" #~ msgid "Lessons can only be either substituted or cancelled." -#~ msgstr "" -#~ "Unterrichtsstunden können nur entweder vertreten werden oder ausfallen." +#~ msgstr "Unterrichtsstunden können nur entweder vertreten werden oder ausfallen." #~ msgid "Lesson substitution" #~ msgstr "Vertretung" @@ -395,9 +479,6 @@ msgstr "Bereiche" #~ msgid "Comments" #~ msgstr "Kommentare" -#~ msgid "Holiday" -#~ msgstr "Ferien" - #~ msgid "Holidays" #~ msgstr "Ferien" @@ -456,61 +537,37 @@ msgstr "Bereiche" #~ msgid "Can view all lessons per day" #~ msgstr "Kann alle Tagesstunden sehen" -#~ msgid "Can view all supervisions per day" -#~ msgstr "Kann alle Tagesaufsichten sehen" - #~ msgid "Shorten groups in timetable views" #~ msgstr "Gruppen in Stundenplanansichten kürzen" #~ msgid "If there are more groups than the set limit, they will be collapsed." -#~ msgstr "" -#~ "Wenn es mehr Gruppen als das gesetzte Limit gibt, werden die " -#~ "Gruppenangaben gekürzt." +#~ msgstr "Wenn es mehr Gruppen als das gesetzte Limit gibt, werden die Gruppenangaben gekürzt." #~ msgid "Limit of groups for shortening of groups" #~ msgstr "Anzahl der Gruppen, ab der gekürzt wird" -#~ msgid "" -#~ "If a user activates shortening of groups,they will be collapsed if there " -#~ "are more groups than this limit." -#~ msgstr "" -#~ "Wenn Benutzer*innen die Kürzung von Gruppen aktiviert hat, werden sie ab " -#~ "diesem Limit gekürzt." +#~ msgid "If a user activates shortening of groups,they will be collapsed if there are more groups than this limit." +#~ msgstr "Wenn Benutzer*innen die Kürzung von Gruppen aktiviert hat, werden sie ab diesem Limit gekürzt." -#~ msgid "" -#~ "How many days in advance users should be notified about timetable changes?" -#~ msgstr "" -#~ "Wie viele Tage im Voraus sollen Benutzer über Stundenplanänderungen " -#~ "benachrichtigt werden?" +#~ msgid "How many days in advance users should be notified about timetable changes?" +#~ msgstr "Wie viele Tage im Voraus sollen Benutzer über Stundenplanänderungen benachrichtigt werden?" #~ msgid "Time for sending notifications about timetable changes" -#~ msgstr "" -#~ "Zeitpunkt zum Senden von Benachrichtigungen über Stundenplanänderungen" +#~ msgstr "Zeitpunkt zum Senden von Benachrichtigungen über Stundenplanänderungen" -#~ msgid "" -#~ "This is only used for scheduling notifications which doesn't affect the " -#~ "time period configured above. All other notifications affecting the next " -#~ "days are sent immediately." -#~ msgstr "" -#~ "Dies wird nur benutzt, um Benachrichtigungen zu planen, welche nicht den " -#~ "oben konfigurierten Zeitraum betreffen. Alle weiteren Benachrichtigungen, " -#~ "die die nächsten Tage betreffen, werden sofort gesendet." +#~ msgid "This is only used for scheduling notifications which doesn't affect the time period configured above. All other notifications affecting the next days are sent immediately." +#~ msgstr "Dies wird nur benutzt, um Benachrichtigungen zu planen, welche nicht den oben konfigurierten Zeitraum betreffen. Alle weiteren Benachrichtigungen, die die nächsten Tage betreffen, werden sofort gesendet." #~ msgid "Send notifications for current timetable changes" #~ msgstr "Benachrichtigungen für aktuelle Stundenplanänderungen verschicken" #, python-brace-format -#~ msgid "" -#~ "The {subject} lesson in the {period}. period on {day} has been cancelled." +#~ msgid "The {subject} lesson in the {period}. period on {day} has been cancelled." #~ msgstr "Die {subject}-Stunde in der {period}. Stunde am {day} fällt aus." #, python-brace-format -#~ msgid "" -#~ "The {subject} lesson in the {period}. period on {day} has some current " -#~ "changes." -#~ msgstr "" -#~ "Die {subject}-Stunde in der {period}. Stunde am {day} hat aktuelle " -#~ "Änderungen." +#~ msgid "The {subject} lesson in the {period}. period on {day} has some current changes." +#~ msgstr "Die {subject}-Stunde in der {period}. Stunde am {day} hat aktuelle Änderungen." #, python-brace-format #~ msgid "The teacher {old} is substituted by {new}." @@ -531,20 +588,12 @@ msgstr "Bereiche" #~ msgstr "Es gibt einen zusätzlichen Hinweis: {comment}." #, python-brace-format -#~ msgid "" -#~ "There is an event that starts on {date_start}, {period_from}. period and " -#~ "ends on {date_end}, {period_to}. period:" -#~ msgstr "" -#~ "Es gibt eine Veranstaltung, die am {date_start} in der {period_from}. " -#~ "Stunde startet und am {date_end} in der {period_to}. Stunde endet:" +#~ msgid "There is an event that starts on {date_start}, {period_from}. period and ends on {date_end}, {period_to}. period:" +#~ msgstr "Es gibt eine Veranstaltung, die am {date_start} in der {period_from}. Stunde startet und am {date_end} in der {period_to}. Stunde endet:" #, python-brace-format -#~ msgid "" -#~ "There is an event on {date} from the {period_from}. period to the " -#~ "{period_to}. period:" -#~ msgstr "" -#~ "Es gibt eine Veranstaltung am {date} von der {period_from}. Stunde bis " -#~ "zur {period_to}. Stunde:" +#~ msgid "There is an event on {date} from the {period_from}. period to the {period_to}. period:" +#~ msgstr "Es gibt eine Veranstaltung am {date} von der {period_from}. Stunde bis zur {period_to}. Stunde:" #, python-brace-format #~ msgid "Groups: {groups}" @@ -575,12 +624,8 @@ msgstr "Bereiche" #~ msgstr "Kommentar: {comment}." #, python-brace-format -#~ msgid "" -#~ "The supervision of {old} on {date} between the {period_from}. period and " -#~ "the {period_to}. period in the area {area} is substituted by {new}." -#~ msgstr "" -#~ "Die Aufsicht von {old} am {date} zwischen der {period_from}. Stunde un " -#~ "der {period_to}. Stunde im Gebiet {area} wird von {new} vertreten." +#~ msgid "The supervision of {old} on {date} between the {period_from}. period and the {period_to}. period in the area {area} is substituted by {new}." +#~ msgstr "Die Aufsicht von {old} am {date} zwischen der {period_from}. Stunde un der {period_to}. Stunde im Gebiet {area} wird von {new} vertreten." #~ msgid "Timetable" #~ msgstr "Stundenplan" @@ -618,9 +663,6 @@ msgstr "Bereiche" #~ msgid "Substitution" #~ msgstr "Vertretung" -#~ msgid "Manage substitution" -#~ msgstr "Vertretung verwalten" - #~ msgid "No teachers timetables available." #~ msgstr "Keine Stundenpläne von Lehrkräften vorhanden." @@ -697,20 +739,11 @@ msgstr "Bereiche" #~ msgid "The substitution has been deleted." #~ msgstr "Die Vertretung wurde gelöscht." -#~ msgid "" -#~ "If an lesson or substitution has only one group and this group has parent " -#~ "groups, show the parent groups instead of the original group." -#~ msgstr "" -#~ "Wenn eine Stunde oder Vertretung nur eine Gruppe hat und diese Gruppe " -#~ "Elterngruppen hat, zeige die Elterngruppen anstelle der eigentlichen " -#~ "Gruppe." +#~ msgid "If an lesson or substitution has only one group and this group has parent groups, show the parent groups instead of the original group." +#~ msgstr "Wenn eine Stunde oder Vertretung nur eine Gruppe hat und diese Gruppe Elterngruppen hat, zeige die Elterngruppen anstelle der eigentlichen Gruppe." -#~ msgid "" -#~ "If an user activates shortening of groups,they will be collapsed if there " -#~ "are more groups than this limit." -#~ msgstr "" -#~ "Wenn ein Benutzer die Kürzung von Gruppen aktiviert hat, werden sie ab " -#~ "diesem Limit gekürzt." +#~ msgid "If an user activates shortening of groups,they will be collapsed if there are more groups than this limit." +#~ msgstr "Wenn ein Benutzer die Kürzung von Gruppen aktiviert hat, werden sie ab diesem Limit gekürzt." #~ msgid "View class register of the current week" #~ msgstr "Klassenbuch der aktuellen Woche anzeigen" @@ -784,34 +817,12 @@ msgstr "Bereiche" #~ msgid "Effective end period of event" #~ msgstr "Effektive Endstunde der Veranstaltung" -#~ msgid "" -#~ "If there are more groups than this limit and CHRONOS_SHORTEN_GROUPS is " -#~ "enabled, add text collapsible." -#~ msgstr "" -#~ "Wenn es mehr Gruppen als dieses Limit gibt und CHRONOS_SHORTEN_GROUPS " -#~ "aktiviert ist, werden die Gruppenangaben gekürzt." +#~ msgid "If there are more groups than this limit and CHRONOS_SHORTEN_GROUPS is enabled, add text collapsible." +#~ msgstr "Wenn es mehr Gruppen als dieses Limit gibt und CHRONOS_SHORTEN_GROUPS aktiviert ist, werden die Gruppenangaben gekürzt." #~ msgid "Abbreviation of subject in timetable" #~ msgstr "Kürzel des Faches im Stundenplan" -#~ msgid "Sunday" -#~ msgstr "Sonntag" - -#~ msgid "Monday" -#~ msgstr "Montag" - -#~ msgid "Tuesday" -#~ msgstr "Dienstag" - -#~ msgid "Wednesday" -#~ msgstr "Mittwoch" - -#~ msgid "Thursday" -#~ msgstr "Donnerstag" - -#~ msgid "Saturday" -#~ msgstr "Samstag" - #~ msgid "Save" #~ msgstr "Speichern" diff --git a/aleksis/apps/chronos/locale/fr/LC_MESSAGES/django.po b/aleksis/apps/chronos/locale/fr/LC_MESSAGES/django.po index cea5c852f6887401bcd06dc8ae9fb07cdb4b222d..b9def12e9b9cd647d3d85f92828114453926638b 100644 --- a/aleksis/apps/chronos/locale/fr/LC_MESSAGES/django.po +++ b/aleksis/apps/chronos/locale/fr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-17 10:48+0200\n" +"POT-Creation-Date: 2024-12-08 13:24+0100\n" "PO-Revision-Date: 2021-06-16 11:59+0000\n" "Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n" "Language-Team: French <https://translate.edugit.org/projects/aleksis/aleksis-app-chronos/fr/>\n" @@ -18,14 +18,34 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.4\n" -#: aleksis/apps/chronos/model_extensions.py:9 +#: aleksis/apps/chronos/model_extensions.py:10 msgid "Can view group timetable" msgstr "" -#: aleksis/apps/chronos/model_extensions.py:13 +#: aleksis/apps/chronos/model_extensions.py:14 +msgid "Can manage group substitutions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:18 msgid "Can view person timetable" msgstr "" +#: aleksis/apps/chronos/model_extensions.py:22 +msgid "Can view group supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:26 +msgid "Can view person supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:30 +msgid "Can view room supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:34 +msgid "Can view course timetable" +msgstr "" + #: aleksis/apps/chronos/models.py:48 msgid "Number of days shown in the plan" msgstr "" @@ -63,92 +83,116 @@ msgid "Can view all person timetables" msgstr "" #: aleksis/apps/chronos/models.py:150 -msgid "Can view timetable overview" +msgid "Can view all course timetables" msgstr "" #: aleksis/apps/chronos/models.py:151 +msgid "Can view timetable overview" +msgstr "" + +#: aleksis/apps/chronos/models.py:152 msgid "Can view substitutions table" msgstr "" -#: aleksis/apps/chronos/models.py:159 +#: aleksis/apps/chronos/models.py:153 +msgid "Can view all room supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:154 +msgid "Can view all group supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:155 +msgid "Can view all person supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:156 +msgid "Can manage all substitutions" +msgstr "" + +#: aleksis/apps/chronos/models.py:164 msgid "Lessons" msgstr "" -#: aleksis/apps/chronos/models.py:163 +#: aleksis/apps/chronos/models.py:168 msgid "Name" msgstr "" -#: aleksis/apps/chronos/models.py:166 +#: aleksis/apps/chronos/models.py:171 msgid "Start slot number" msgstr "" -#: aleksis/apps/chronos/models.py:169 +#: aleksis/apps/chronos/models.py:174 msgid "End slot number" msgstr "" -#: aleksis/apps/chronos/models.py:173 +#: aleksis/apps/chronos/models.py:178 msgid "Course" msgstr "" -#: aleksis/apps/chronos/models.py:179 +#: aleksis/apps/chronos/models.py:184 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:1 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:26 msgid "Groups" msgstr "" -#: aleksis/apps/chronos/models.py:185 +#: aleksis/apps/chronos/models.py:190 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:4 msgid "Rooms" msgstr "" -#: aleksis/apps/chronos/models.py:191 +#: aleksis/apps/chronos/models.py:196 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:3 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:28 #: aleksis/apps/chronos/templates/chronos/supervision_event_description.txt:1 msgid "Teachers" msgstr "Profs" -#: aleksis/apps/chronos/models.py:198 +#: aleksis/apps/chronos/models.py:203 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:2 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:29 msgid "Subject" msgstr "Sujet" -#: aleksis/apps/chronos/models.py:206 +#: aleksis/apps/chronos/models.py:211 #: aleksis/apps/chronos/templates/chronos/partials/subs/badge.html:4 msgid "Cancelled" msgstr "" -#: aleksis/apps/chronos/models.py:210 +#: aleksis/apps/chronos/models.py:215 msgid "Comment" msgstr "" -#: aleksis/apps/chronos/models.py:254 aleksis/apps/chronos/models.py:266 -#: aleksis/apps/chronos/models.py:278 +#: aleksis/apps/chronos/models.py:220 +msgid "Is this a current change?" +msgstr "" + +#: aleksis/apps/chronos/models.py:262 aleksis/apps/chronos/models.py:274 +#: aleksis/apps/chronos/models.py:286 msgid "{} (instead of {})" msgstr "" -#: aleksis/apps/chronos/models.py:283 aleksis/apps/chronos/models.py:307 +#: aleksis/apps/chronos/models.py:291 aleksis/apps/chronos/models.py:315 msgid "Lesson" msgstr "Cours" -#: aleksis/apps/chronos/models.py:467 +#: aleksis/apps/chronos/models.py:630 #, fuzzy #| msgid "Lesson" msgid "Lesson Event" msgstr "Cours" -#: aleksis/apps/chronos/models.py:468 +#: aleksis/apps/chronos/models.py:631 #, fuzzy #| msgid "Lesson" msgid "Lesson Events" msgstr "Cours" -#: aleksis/apps/chronos/models.py:475 +#: aleksis/apps/chronos/models.py:638 msgid "Supervisions" msgstr "" -#: aleksis/apps/chronos/models.py:483 +#: aleksis/apps/chronos/models.py:646 msgid "Supervision: {}" msgstr "" @@ -200,6 +244,38 @@ msgstr "" msgid "Supervision calendar feed color" msgstr "" +#: aleksis/apps/chronos/preferences.py:132 +msgid "Days of the week that appear in the timetable" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:134 +msgid "Sunday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:135 +msgid "Monday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:136 +msgid "Tuesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:137 +msgid "Wednesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:138 +msgid "Thursday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:139 +msgid "Friday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:140 +msgid "Saturday" +msgstr "" + #: aleksis/apps/chronos/templates/chronos/partials/headerbox.html:10 msgid "Absent teachers" msgstr "" diff --git a/aleksis/apps/chronos/locale/la/LC_MESSAGES/django.po b/aleksis/apps/chronos/locale/la/LC_MESSAGES/django.po index 8d0616a26f634a0470c7980ef1529cd4a1642a10..26cde2d780c2ca05a4dbc69569871856efa6f8ce 100644 --- a/aleksis/apps/chronos/locale/la/LC_MESSAGES/django.po +++ b/aleksis/apps/chronos/locale/la/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-17 10:48+0200\n" +"POT-Creation-Date: 2024-12-08 13:24+0100\n" "PO-Revision-Date: 2020-08-23 13:49+0000\n" "Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n" "Language-Team: Latin <https://translate.edugit.org/projects/aleksis/aleksis-app-chronos/la/>\n" @@ -18,14 +18,34 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.1.1\n" -#: aleksis/apps/chronos/model_extensions.py:9 +#: aleksis/apps/chronos/model_extensions.py:10 msgid "Can view group timetable" msgstr "" -#: aleksis/apps/chronos/model_extensions.py:13 +#: aleksis/apps/chronos/model_extensions.py:14 +msgid "Can manage group substitutions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:18 msgid "Can view person timetable" msgstr "" +#: aleksis/apps/chronos/model_extensions.py:22 +msgid "Can view group supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:26 +msgid "Can view person supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:30 +msgid "Can view room supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:34 +msgid "Can view course timetable" +msgstr "" + #: aleksis/apps/chronos/models.py:48 msgid "Number of days shown in the plan" msgstr "" @@ -63,88 +83,112 @@ msgid "Can view all person timetables" msgstr "" #: aleksis/apps/chronos/models.py:150 -msgid "Can view timetable overview" +msgid "Can view all course timetables" msgstr "" #: aleksis/apps/chronos/models.py:151 +msgid "Can view timetable overview" +msgstr "" + +#: aleksis/apps/chronos/models.py:152 msgid "Can view substitutions table" msgstr "" -#: aleksis/apps/chronos/models.py:159 +#: aleksis/apps/chronos/models.py:153 +msgid "Can view all room supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:154 +msgid "Can view all group supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:155 +msgid "Can view all person supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:156 +msgid "Can manage all substitutions" +msgstr "" + +#: aleksis/apps/chronos/models.py:164 msgid "Lessons" msgstr "" -#: aleksis/apps/chronos/models.py:163 +#: aleksis/apps/chronos/models.py:168 msgid "Name" msgstr "" -#: aleksis/apps/chronos/models.py:166 +#: aleksis/apps/chronos/models.py:171 msgid "Start slot number" msgstr "" -#: aleksis/apps/chronos/models.py:169 +#: aleksis/apps/chronos/models.py:174 msgid "End slot number" msgstr "" -#: aleksis/apps/chronos/models.py:173 +#: aleksis/apps/chronos/models.py:178 msgid "Course" msgstr "" -#: aleksis/apps/chronos/models.py:179 +#: aleksis/apps/chronos/models.py:184 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:1 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:26 msgid "Groups" msgstr "" -#: aleksis/apps/chronos/models.py:185 +#: aleksis/apps/chronos/models.py:190 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:4 msgid "Rooms" msgstr "" -#: aleksis/apps/chronos/models.py:191 +#: aleksis/apps/chronos/models.py:196 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:3 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:28 #: aleksis/apps/chronos/templates/chronos/supervision_event_description.txt:1 msgid "Teachers" msgstr "" -#: aleksis/apps/chronos/models.py:198 +#: aleksis/apps/chronos/models.py:203 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:2 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:29 msgid "Subject" msgstr "" -#: aleksis/apps/chronos/models.py:206 +#: aleksis/apps/chronos/models.py:211 #: aleksis/apps/chronos/templates/chronos/partials/subs/badge.html:4 msgid "Cancelled" msgstr "" -#: aleksis/apps/chronos/models.py:210 +#: aleksis/apps/chronos/models.py:215 msgid "Comment" msgstr "" -#: aleksis/apps/chronos/models.py:254 aleksis/apps/chronos/models.py:266 -#: aleksis/apps/chronos/models.py:278 +#: aleksis/apps/chronos/models.py:220 +msgid "Is this a current change?" +msgstr "" + +#: aleksis/apps/chronos/models.py:262 aleksis/apps/chronos/models.py:274 +#: aleksis/apps/chronos/models.py:286 msgid "{} (instead of {})" msgstr "" -#: aleksis/apps/chronos/models.py:283 aleksis/apps/chronos/models.py:307 +#: aleksis/apps/chronos/models.py:291 aleksis/apps/chronos/models.py:315 msgid "Lesson" msgstr "" -#: aleksis/apps/chronos/models.py:467 +#: aleksis/apps/chronos/models.py:630 msgid "Lesson Event" msgstr "" -#: aleksis/apps/chronos/models.py:468 +#: aleksis/apps/chronos/models.py:631 msgid "Lesson Events" msgstr "" -#: aleksis/apps/chronos/models.py:475 +#: aleksis/apps/chronos/models.py:638 msgid "Supervisions" msgstr "" -#: aleksis/apps/chronos/models.py:483 +#: aleksis/apps/chronos/models.py:646 msgid "Supervision: {}" msgstr "" @@ -196,6 +240,38 @@ msgstr "" msgid "Supervision calendar feed color" msgstr "" +#: aleksis/apps/chronos/preferences.py:132 +msgid "Days of the week that appear in the timetable" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:134 +msgid "Sunday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:135 +msgid "Monday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:136 +msgid "Tuesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:137 +msgid "Wednesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:138 +msgid "Thursday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:139 +msgid "Friday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:140 +msgid "Saturday" +msgstr "" + #: aleksis/apps/chronos/templates/chronos/partials/headerbox.html:10 msgid "Absent teachers" msgstr "" diff --git a/aleksis/apps/chronos/locale/nb_NO/LC_MESSAGES/django.po b/aleksis/apps/chronos/locale/nb_NO/LC_MESSAGES/django.po index e7015c68a7c47522f5d628d897256ea935a950ed..fe29db94609091b882fc7088f35760d3e4af793a 100644 --- a/aleksis/apps/chronos/locale/nb_NO/LC_MESSAGES/django.po +++ b/aleksis/apps/chronos/locale/nb_NO/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-17 10:48+0200\n" +"POT-Creation-Date: 2024-12-08 13:24+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -17,14 +17,34 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: aleksis/apps/chronos/model_extensions.py:9 +#: aleksis/apps/chronos/model_extensions.py:10 msgid "Can view group timetable" msgstr "" -#: aleksis/apps/chronos/model_extensions.py:13 +#: aleksis/apps/chronos/model_extensions.py:14 +msgid "Can manage group substitutions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:18 msgid "Can view person timetable" msgstr "" +#: aleksis/apps/chronos/model_extensions.py:22 +msgid "Can view group supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:26 +msgid "Can view person supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:30 +msgid "Can view room supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:34 +msgid "Can view course timetable" +msgstr "" + #: aleksis/apps/chronos/models.py:48 msgid "Number of days shown in the plan" msgstr "" @@ -62,88 +82,112 @@ msgid "Can view all person timetables" msgstr "" #: aleksis/apps/chronos/models.py:150 -msgid "Can view timetable overview" +msgid "Can view all course timetables" msgstr "" #: aleksis/apps/chronos/models.py:151 +msgid "Can view timetable overview" +msgstr "" + +#: aleksis/apps/chronos/models.py:152 msgid "Can view substitutions table" msgstr "" -#: aleksis/apps/chronos/models.py:159 +#: aleksis/apps/chronos/models.py:153 +msgid "Can view all room supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:154 +msgid "Can view all group supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:155 +msgid "Can view all person supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:156 +msgid "Can manage all substitutions" +msgstr "" + +#: aleksis/apps/chronos/models.py:164 msgid "Lessons" msgstr "" -#: aleksis/apps/chronos/models.py:163 +#: aleksis/apps/chronos/models.py:168 msgid "Name" msgstr "" -#: aleksis/apps/chronos/models.py:166 +#: aleksis/apps/chronos/models.py:171 msgid "Start slot number" msgstr "" -#: aleksis/apps/chronos/models.py:169 +#: aleksis/apps/chronos/models.py:174 msgid "End slot number" msgstr "" -#: aleksis/apps/chronos/models.py:173 +#: aleksis/apps/chronos/models.py:178 msgid "Course" msgstr "" -#: aleksis/apps/chronos/models.py:179 +#: aleksis/apps/chronos/models.py:184 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:1 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:26 msgid "Groups" msgstr "" -#: aleksis/apps/chronos/models.py:185 +#: aleksis/apps/chronos/models.py:190 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:4 msgid "Rooms" msgstr "" -#: aleksis/apps/chronos/models.py:191 +#: aleksis/apps/chronos/models.py:196 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:3 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:28 #: aleksis/apps/chronos/templates/chronos/supervision_event_description.txt:1 msgid "Teachers" msgstr "" -#: aleksis/apps/chronos/models.py:198 +#: aleksis/apps/chronos/models.py:203 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:2 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:29 msgid "Subject" msgstr "" -#: aleksis/apps/chronos/models.py:206 +#: aleksis/apps/chronos/models.py:211 #: aleksis/apps/chronos/templates/chronos/partials/subs/badge.html:4 msgid "Cancelled" msgstr "" -#: aleksis/apps/chronos/models.py:210 +#: aleksis/apps/chronos/models.py:215 msgid "Comment" msgstr "" -#: aleksis/apps/chronos/models.py:254 aleksis/apps/chronos/models.py:266 -#: aleksis/apps/chronos/models.py:278 +#: aleksis/apps/chronos/models.py:220 +msgid "Is this a current change?" +msgstr "" + +#: aleksis/apps/chronos/models.py:262 aleksis/apps/chronos/models.py:274 +#: aleksis/apps/chronos/models.py:286 msgid "{} (instead of {})" msgstr "" -#: aleksis/apps/chronos/models.py:283 aleksis/apps/chronos/models.py:307 +#: aleksis/apps/chronos/models.py:291 aleksis/apps/chronos/models.py:315 msgid "Lesson" msgstr "" -#: aleksis/apps/chronos/models.py:467 +#: aleksis/apps/chronos/models.py:630 msgid "Lesson Event" msgstr "" -#: aleksis/apps/chronos/models.py:468 +#: aleksis/apps/chronos/models.py:631 msgid "Lesson Events" msgstr "" -#: aleksis/apps/chronos/models.py:475 +#: aleksis/apps/chronos/models.py:638 msgid "Supervisions" msgstr "" -#: aleksis/apps/chronos/models.py:483 +#: aleksis/apps/chronos/models.py:646 msgid "Supervision: {}" msgstr "" @@ -195,6 +239,38 @@ msgstr "" msgid "Supervision calendar feed color" msgstr "" +#: aleksis/apps/chronos/preferences.py:132 +msgid "Days of the week that appear in the timetable" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:134 +msgid "Sunday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:135 +msgid "Monday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:136 +msgid "Tuesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:137 +msgid "Wednesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:138 +msgid "Thursday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:139 +msgid "Friday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:140 +msgid "Saturday" +msgstr "" + #: aleksis/apps/chronos/templates/chronos/partials/headerbox.html:10 msgid "Absent teachers" msgstr "" diff --git a/aleksis/apps/chronos/locale/ru/LC_MESSAGES/django.po b/aleksis/apps/chronos/locale/ru/LC_MESSAGES/django.po index 05b241d475490fd91d69b5e92ee3d1cf8a598a12..30010aa14a2911c05e72b66b40d920e43ab50bfb 100644 --- a/aleksis/apps/chronos/locale/ru/LC_MESSAGES/django.po +++ b/aleksis/apps/chronos/locale/ru/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-17 10:48+0200\n" +"POT-Creation-Date: 2024-12-08 13:24+0100\n" "PO-Revision-Date: 2023-05-26 04:38+0000\n" "Last-Translator: Serhii Horichenko <m@sgg.im>\n" "Language-Team: Russian <https://translate.edugit.org/projects/aleksis/aleksis-app-chronos/ru/>\n" @@ -18,14 +18,44 @@ msgstr "" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" "X-Generator: Weblate 4.12.1\n" -#: aleksis/apps/chronos/model_extensions.py:9 +#: aleksis/apps/chronos/model_extensions.py:10 msgid "Can view group timetable" msgstr "Может проÑматривать групповые раÑпиÑаниÑ" -#: aleksis/apps/chronos/model_extensions.py:13 +#: aleksis/apps/chronos/model_extensions.py:14 +#, fuzzy +#| msgid "Manage substitution" +msgid "Can manage group substitutions" +msgstr "Управление заменами" + +#: aleksis/apps/chronos/model_extensions.py:18 msgid "Can view person timetable" msgstr "Может проÑматривать личные раÑпиÑаниÑ" +#: aleksis/apps/chronos/model_extensions.py:22 +#, fuzzy +#| msgid "Can view all lessons per day" +msgid "Can view group supervisions" +msgstr "Может проÑматривать вÑе уроки за день" + +#: aleksis/apps/chronos/model_extensions.py:26 +#, fuzzy +#| msgid "Can view all lessons per day" +msgid "Can view person supervisions" +msgstr "Может проÑматривать вÑе уроки за день" + +#: aleksis/apps/chronos/model_extensions.py:30 +#, fuzzy +#| msgid "Can view all lessons per day" +msgid "Can view room supervisions" +msgstr "Может проÑматривать вÑе уроки за день" + +#: aleksis/apps/chronos/model_extensions.py:34 +#, fuzzy +#| msgid "Can view group timetable" +msgid "Can view course timetable" +msgstr "Может проÑматривать групповые раÑпиÑаниÑ" + #: aleksis/apps/chronos/models.py:48 msgid "Number of days shown in the plan" msgstr "КоличеÑтво дней Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð² плане" @@ -63,98 +93,132 @@ msgid "Can view all person timetables" msgstr "Может проÑматривать раÑпиÑÐ°Ð½Ð¸Ñ Ð²Ñех людей" #: aleksis/apps/chronos/models.py:150 +#, fuzzy +#| msgid "Can view all group timetables" +msgid "Can view all course timetables" +msgstr "Может проÑматривать раÑпиÑÐ°Ð½Ð¸Ñ Ð²Ñех групп" + +#: aleksis/apps/chronos/models.py:151 msgid "Can view timetable overview" msgstr "Может проÑматривать общее раÑпиÑание" -#: aleksis/apps/chronos/models.py:151 +#: aleksis/apps/chronos/models.py:152 #, fuzzy #| msgid "No substitutions available." msgid "Can view substitutions table" msgstr "Замены недоÑтупны." -#: aleksis/apps/chronos/models.py:159 +#: aleksis/apps/chronos/models.py:153 +#, fuzzy +#| msgid "Can view all lessons per day" +msgid "Can view all room supervisions" +msgstr "Может проÑматривать вÑе уроки за день" + +#: aleksis/apps/chronos/models.py:154 +#, fuzzy +#| msgid "Can view all lessons per day" +msgid "Can view all group supervisions" +msgstr "Может проÑматривать вÑе уроки за день" + +#: aleksis/apps/chronos/models.py:155 +#, fuzzy +#| msgid "Can view all lessons per day" +msgid "Can view all person supervisions" +msgstr "Может проÑматривать вÑе уроки за день" + +#: aleksis/apps/chronos/models.py:156 +#, fuzzy +#| msgid "Manage substitution" +msgid "Can manage all substitutions" +msgstr "Управление заменами" + +#: aleksis/apps/chronos/models.py:164 msgid "Lessons" msgstr "Уроки" -#: aleksis/apps/chronos/models.py:163 +#: aleksis/apps/chronos/models.py:168 msgid "Name" msgstr "Полное имÑ" -#: aleksis/apps/chronos/models.py:166 +#: aleksis/apps/chronos/models.py:171 #, fuzzy #| msgid "Start time" msgid "Start slot number" msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð½Ð°Ñ‡Ð°Ð»Ð°" -#: aleksis/apps/chronos/models.py:169 +#: aleksis/apps/chronos/models.py:174 #, fuzzy #| msgid "End time" msgid "End slot number" msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð¾ÐºÐ¾Ð½Ñ‡Ð°Ð½Ð¸Ñ" -#: aleksis/apps/chronos/models.py:173 +#: aleksis/apps/chronos/models.py:178 msgid "Course" msgstr "" -#: aleksis/apps/chronos/models.py:179 +#: aleksis/apps/chronos/models.py:184 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:1 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:26 msgid "Groups" msgstr "Группы" -#: aleksis/apps/chronos/models.py:185 +#: aleksis/apps/chronos/models.py:190 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:4 msgid "Rooms" msgstr "Комнаты" -#: aleksis/apps/chronos/models.py:191 +#: aleksis/apps/chronos/models.py:196 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:3 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:28 #: aleksis/apps/chronos/templates/chronos/supervision_event_description.txt:1 msgid "Teachers" msgstr "Преподаватели" -#: aleksis/apps/chronos/models.py:198 +#: aleksis/apps/chronos/models.py:203 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:2 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:29 msgid "Subject" msgstr "Предмет" -#: aleksis/apps/chronos/models.py:206 +#: aleksis/apps/chronos/models.py:211 #: aleksis/apps/chronos/templates/chronos/partials/subs/badge.html:4 msgid "Cancelled" msgstr "Отменено" -#: aleksis/apps/chronos/models.py:210 +#: aleksis/apps/chronos/models.py:215 msgid "Comment" msgstr "Коментарий" -#: aleksis/apps/chronos/models.py:254 aleksis/apps/chronos/models.py:266 -#: aleksis/apps/chronos/models.py:278 +#: aleksis/apps/chronos/models.py:220 +msgid "Is this a current change?" +msgstr "" + +#: aleksis/apps/chronos/models.py:262 aleksis/apps/chronos/models.py:274 +#: aleksis/apps/chronos/models.py:286 msgid "{} (instead of {})" msgstr "" -#: aleksis/apps/chronos/models.py:283 aleksis/apps/chronos/models.py:307 +#: aleksis/apps/chronos/models.py:291 aleksis/apps/chronos/models.py:315 msgid "Lesson" msgstr "Урок" -#: aleksis/apps/chronos/models.py:467 +#: aleksis/apps/chronos/models.py:630 #, fuzzy #| msgid "Lesson" msgid "Lesson Event" msgstr "Урок" -#: aleksis/apps/chronos/models.py:468 +#: aleksis/apps/chronos/models.py:631 #, fuzzy #| msgid "Lessons" msgid "Lesson Events" msgstr "Уроки" -#: aleksis/apps/chronos/models.py:475 +#: aleksis/apps/chronos/models.py:638 msgid "Supervisions" msgstr "Контроли" -#: aleksis/apps/chronos/models.py:483 +#: aleksis/apps/chronos/models.py:646 #, fuzzy #| msgid "Supervision" msgid "Supervision: {}" @@ -216,6 +280,40 @@ msgstr "" msgid "Supervision calendar feed color" msgstr "Зона контролÑ" +#: aleksis/apps/chronos/preferences.py:132 +msgid "Days of the week that appear in the timetable" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:134 +msgid "Sunday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:135 +msgid "Monday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:136 +msgid "Tuesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:137 +msgid "Wednesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:138 +msgid "Thursday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:139 +#, fuzzy +#| msgid "Holiday" +msgid "Friday" +msgstr "Выходной" + +#: aleksis/apps/chronos/preferences.py:140 +msgid "Saturday" +msgstr "" + #: aleksis/apps/chronos/templates/chronos/partials/headerbox.html:10 msgid "Absent teachers" msgstr "ОтÑутÑтвующие преподаватели" @@ -402,9 +500,6 @@ msgstr "" #~ msgid "Comments" #~ msgstr "Комментарии" -#~ msgid "Holiday" -#~ msgstr "Выходной" - #~ msgid "Holidays" #~ msgstr "Выходные" @@ -463,11 +558,6 @@ msgstr "" #~ msgid "Can view all lessons per day" #~ msgstr "Может проÑматривать вÑе уроки за день" -#, fuzzy -#~| msgid "Can view all lessons per day" -#~ msgid "Can view all supervisions per day" -#~ msgstr "Может проÑматривать вÑе уроки за день" - #~ msgid "Shorten groups in timetable views" #~ msgstr "Сокращать группы в обзорах раÑпиÑаниÑ" @@ -598,9 +688,6 @@ msgstr "" #~ msgid "Substitution" #~ msgstr "Замена" -#~ msgid "Manage substitution" -#~ msgstr "Управление заменами" - #~ msgid "No teachers timetables available." #~ msgstr "РаÑпиÑÐ°Ð½Ð¸Ñ Ð¿Ñ€ÐµÐ¿Ð¾Ð´Ð°Ð²Ð°Ñ‚ÐµÐ»ÐµÐ¹ недоÑтупны." diff --git a/aleksis/apps/chronos/locale/tr_TR/LC_MESSAGES/django.po b/aleksis/apps/chronos/locale/tr_TR/LC_MESSAGES/django.po index e7015c68a7c47522f5d628d897256ea935a950ed..fe29db94609091b882fc7088f35760d3e4af793a 100644 --- a/aleksis/apps/chronos/locale/tr_TR/LC_MESSAGES/django.po +++ b/aleksis/apps/chronos/locale/tr_TR/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-17 10:48+0200\n" +"POT-Creation-Date: 2024-12-08 13:24+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -17,14 +17,34 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: aleksis/apps/chronos/model_extensions.py:9 +#: aleksis/apps/chronos/model_extensions.py:10 msgid "Can view group timetable" msgstr "" -#: aleksis/apps/chronos/model_extensions.py:13 +#: aleksis/apps/chronos/model_extensions.py:14 +msgid "Can manage group substitutions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:18 msgid "Can view person timetable" msgstr "" +#: aleksis/apps/chronos/model_extensions.py:22 +msgid "Can view group supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:26 +msgid "Can view person supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:30 +msgid "Can view room supervisions" +msgstr "" + +#: aleksis/apps/chronos/model_extensions.py:34 +msgid "Can view course timetable" +msgstr "" + #: aleksis/apps/chronos/models.py:48 msgid "Number of days shown in the plan" msgstr "" @@ -62,88 +82,112 @@ msgid "Can view all person timetables" msgstr "" #: aleksis/apps/chronos/models.py:150 -msgid "Can view timetable overview" +msgid "Can view all course timetables" msgstr "" #: aleksis/apps/chronos/models.py:151 +msgid "Can view timetable overview" +msgstr "" + +#: aleksis/apps/chronos/models.py:152 msgid "Can view substitutions table" msgstr "" -#: aleksis/apps/chronos/models.py:159 +#: aleksis/apps/chronos/models.py:153 +msgid "Can view all room supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:154 +msgid "Can view all group supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:155 +msgid "Can view all person supervisions" +msgstr "" + +#: aleksis/apps/chronos/models.py:156 +msgid "Can manage all substitutions" +msgstr "" + +#: aleksis/apps/chronos/models.py:164 msgid "Lessons" msgstr "" -#: aleksis/apps/chronos/models.py:163 +#: aleksis/apps/chronos/models.py:168 msgid "Name" msgstr "" -#: aleksis/apps/chronos/models.py:166 +#: aleksis/apps/chronos/models.py:171 msgid "Start slot number" msgstr "" -#: aleksis/apps/chronos/models.py:169 +#: aleksis/apps/chronos/models.py:174 msgid "End slot number" msgstr "" -#: aleksis/apps/chronos/models.py:173 +#: aleksis/apps/chronos/models.py:178 msgid "Course" msgstr "" -#: aleksis/apps/chronos/models.py:179 +#: aleksis/apps/chronos/models.py:184 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:1 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:26 msgid "Groups" msgstr "" -#: aleksis/apps/chronos/models.py:185 +#: aleksis/apps/chronos/models.py:190 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:4 msgid "Rooms" msgstr "" -#: aleksis/apps/chronos/models.py:191 +#: aleksis/apps/chronos/models.py:196 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:3 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:28 #: aleksis/apps/chronos/templates/chronos/supervision_event_description.txt:1 msgid "Teachers" msgstr "" -#: aleksis/apps/chronos/models.py:198 +#: aleksis/apps/chronos/models.py:203 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:2 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:29 msgid "Subject" msgstr "" -#: aleksis/apps/chronos/models.py:206 +#: aleksis/apps/chronos/models.py:211 #: aleksis/apps/chronos/templates/chronos/partials/subs/badge.html:4 msgid "Cancelled" msgstr "" -#: aleksis/apps/chronos/models.py:210 +#: aleksis/apps/chronos/models.py:215 msgid "Comment" msgstr "" -#: aleksis/apps/chronos/models.py:254 aleksis/apps/chronos/models.py:266 -#: aleksis/apps/chronos/models.py:278 +#: aleksis/apps/chronos/models.py:220 +msgid "Is this a current change?" +msgstr "" + +#: aleksis/apps/chronos/models.py:262 aleksis/apps/chronos/models.py:274 +#: aleksis/apps/chronos/models.py:286 msgid "{} (instead of {})" msgstr "" -#: aleksis/apps/chronos/models.py:283 aleksis/apps/chronos/models.py:307 +#: aleksis/apps/chronos/models.py:291 aleksis/apps/chronos/models.py:315 msgid "Lesson" msgstr "" -#: aleksis/apps/chronos/models.py:467 +#: aleksis/apps/chronos/models.py:630 msgid "Lesson Event" msgstr "" -#: aleksis/apps/chronos/models.py:468 +#: aleksis/apps/chronos/models.py:631 msgid "Lesson Events" msgstr "" -#: aleksis/apps/chronos/models.py:475 +#: aleksis/apps/chronos/models.py:638 msgid "Supervisions" msgstr "" -#: aleksis/apps/chronos/models.py:483 +#: aleksis/apps/chronos/models.py:646 msgid "Supervision: {}" msgstr "" @@ -195,6 +239,38 @@ msgstr "" msgid "Supervision calendar feed color" msgstr "" +#: aleksis/apps/chronos/preferences.py:132 +msgid "Days of the week that appear in the timetable" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:134 +msgid "Sunday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:135 +msgid "Monday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:136 +msgid "Tuesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:137 +msgid "Wednesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:138 +msgid "Thursday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:139 +msgid "Friday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:140 +msgid "Saturday" +msgstr "" + #: aleksis/apps/chronos/templates/chronos/partials/headerbox.html:10 msgid "Absent teachers" msgstr "" diff --git a/aleksis/apps/chronos/locale/uk/LC_MESSAGES/django.po b/aleksis/apps/chronos/locale/uk/LC_MESSAGES/django.po index 37c39e991658af7a313c6d082abb5bad239f057c..84a66bce3d50f329a5254a4f7e4409a39b97e179 100644 --- a/aleksis/apps/chronos/locale/uk/LC_MESSAGES/django.po +++ b/aleksis/apps/chronos/locale/uk/LC_MESSAGES/django.po @@ -7,158 +7,226 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-17 10:48+0200\n" +"POT-Creation-Date: 2024-12-08 13:24+0100\n" "PO-Revision-Date: 2022-06-22 19:59+0000\n" "Last-Translator: Serhii Horichenko <m@sgg.im>\n" -"Language-Team: Ukrainian <https://translate.edugit.org/projects/aleksis/aleksis-app-chronos/uk/>\n" +"Language-Team: Ukrainian <https://translate.edugit.org/projects/aleksis/" +"aleksis-app-chronos/uk/>\n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" +"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != " +"11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % " +"100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || " +"(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" "X-Generator: Weblate 4.12.1\n" -#: aleksis/apps/chronos/model_extensions.py:9 +#: aleksis/apps/chronos/model_extensions.py:10 +#: aleksis/apps/chronos/model_extensions.py:145 msgid "Can view group timetable" -msgstr "Може переглÑдати групові розклади" +msgstr "Може бачити групові розклади" + +#: aleksis/apps/chronos/model_extensions.py:14 +#, fuzzy +msgid "Can manage group substitutions" +msgstr "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð°Ð¼Ñ–Ð½Ð°Ð¼Ð¸" -#: aleksis/apps/chronos/model_extensions.py:13 +#: aleksis/apps/chronos/model_extensions.py:18 +#: aleksis/apps/chronos/model_extensions.py:149 msgid "Can view person timetable" -msgstr "Може переглÑдати оÑобиÑті розклади" +msgstr "Може бачити оÑобиÑті розклади" + +#: aleksis/apps/chronos/model_extensions.py:22 +#, fuzzy +msgid "Can view group supervisions" +msgstr "Може переглÑдати уÑÑ– уроки окремого днÑ" + +#: aleksis/apps/chronos/model_extensions.py:26 +#, fuzzy +msgid "Can view person supervisions" +msgstr "Може переглÑдати уÑÑ– уроки окремого днÑ" -#: aleksis/apps/chronos/models.py:48 +#: aleksis/apps/chronos/model_extensions.py:30 +#, fuzzy +msgid "Can view room supervisions" +msgstr "Може переглÑдати уÑÑ– уроки окремого днÑ" + +#: aleksis/apps/chronos/model_extensions.py:34 +#, fuzzy +msgid "Can view course timetable" +msgstr "Може переглÑдати групові розклади" + +#: aleksis/apps/chronos/models.py:48 aleksis/apps/chronos/models.py:1194 msgid "Number of days shown in the plan" msgstr "КількіÑть днів Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ñƒ плані" -#: aleksis/apps/chronos/models.py:52 +#: aleksis/apps/chronos/models.py:52 aleksis/apps/chronos/models.py:1198 msgid "Show header box" msgstr "Показати заголовок" #: aleksis/apps/chronos/models.py:53 aleksis/apps/chronos/preferences.py:79 +#: aleksis/apps/chronos/models.py:1199 aleksis/apps/chronos/preferences.py:100 msgid "The header box shows affected teachers/groups." msgstr "Заголовок показує викладачів/групи на Ñких це впливає." -#: aleksis/apps/chronos/models.py:60 +#: aleksis/apps/chronos/models.py:60 aleksis/apps/chronos/models.py:1206 msgid "Revision which triggered the last update" msgstr "РевізіÑ, Ñка запуÑтила оÑтаннє оновленнÑ" -#: aleksis/apps/chronos/models.py:132 +#: aleksis/apps/chronos/models.py:132 aleksis/apps/chronos/models.py:1278 msgid "Automatic plan" msgstr "Ðвтоматичний план" -#: aleksis/apps/chronos/models.py:133 +#: aleksis/apps/chronos/models.py:133 aleksis/apps/chronos/models.py:1279 msgid "Automatic plans" msgstr "Ðвтоматичні плани" -#: aleksis/apps/chronos/models.py:147 +#: aleksis/apps/chronos/models.py:147 aleksis/apps/chronos/models.py:1293 msgid "Can view all room timetables" -msgstr "Може переглÑдати розклади уÑÑ–Ñ… кімнат" +msgstr "Може бачити розклади уÑÑ–Ñ… кімнат" -#: aleksis/apps/chronos/models.py:148 +#: aleksis/apps/chronos/models.py:148 aleksis/apps/chronos/models.py:1294 msgid "Can view all group timetables" -msgstr "Може переглÑдати розклади уÑÑ–Ñ… груп" +msgstr "Може бачити розклади уÑÑ–Ñ… груп" -#: aleksis/apps/chronos/models.py:149 +#: aleksis/apps/chronos/models.py:149 aleksis/apps/chronos/models.py:1295 msgid "Can view all person timetables" -msgstr "Може переглÑдати розклади уÑÑ–Ñ… оÑіб" +msgstr "Може бачити розклади уÑÑ–Ñ… оÑіб" #: aleksis/apps/chronos/models.py:150 +#, fuzzy +msgid "Can view all course timetables" +msgstr "Може переглÑдати розклади уÑÑ–Ñ… груп" + +#: aleksis/apps/chronos/models.py:151 aleksis/apps/chronos/models.py:1296 msgid "Can view timetable overview" -msgstr "Може переглÑдати загальний розклад" +msgstr "Може бачити загальний розклад" -#: aleksis/apps/chronos/models.py:151 +#: aleksis/apps/chronos/models.py:152 #, fuzzy -#| msgid "No substitutions available." msgid "Can view substitutions table" msgstr "Заміни недоÑтупні." -#: aleksis/apps/chronos/models.py:159 +#: aleksis/apps/chronos/models.py:153 +#, fuzzy +msgid "Can view all room supervisions" +msgstr "Може переглÑдати уÑÑ– уроки окремого днÑ" + +#: aleksis/apps/chronos/models.py:154 +#, fuzzy +msgid "Can view all group supervisions" +msgstr "Може переглÑдати уÑÑ– уроки окремого днÑ" + +#: aleksis/apps/chronos/models.py:155 +#, fuzzy +msgid "Can view all person supervisions" +msgstr "Може переглÑдати уÑÑ– уроки окремого днÑ" + +#: aleksis/apps/chronos/models.py:156 +#, fuzzy +msgid "Can manage all substitutions" +msgstr "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð°Ð¼Ñ–Ð½Ð°Ð¼Ð¸" + +#: aleksis/apps/chronos/models.py:164 aleksis/apps/chronos/models.py:395 +#: aleksis/apps/chronos/models.py:1306 msgid "Lessons" msgstr "Уроки" -#: aleksis/apps/chronos/models.py:163 +#: aleksis/apps/chronos/models.py:168 aleksis/apps/chronos/models.py:90 +#: aleksis/apps/chronos/models.py:626 aleksis/apps/chronos/models.py:1310 msgid "Name" msgstr "Повне ім'Ñ" -#: aleksis/apps/chronos/models.py:166 -#, fuzzy -#| msgid "Start time" +#: aleksis/apps/chronos/models.py:1313 msgid "Start slot number" -msgstr "Ð§Ð°Ñ Ð¿Ð¾Ñ‡Ð°Ñ‚ÐºÑƒ" +msgstr "Ðомер першого Ñлота" -#: aleksis/apps/chronos/models.py:169 -#, fuzzy -#| msgid "End time" +#: aleksis/apps/chronos/models.py:1316 msgid "End slot number" -msgstr "Ð§Ð°Ñ Ð·Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ" +msgstr "Ðомер оÑтаннього Ñлота" -#: aleksis/apps/chronos/models.py:173 +#: aleksis/apps/chronos/models.py:1320 msgid "Course" -msgstr "" +msgstr "КурÑ" -#: aleksis/apps/chronos/models.py:179 +#: aleksis/apps/chronos/models.py:184 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:1 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:26 +#: aleksis/apps/chronos/models.py:360 aleksis/apps/chronos/models.py:995 +#: aleksis/apps/chronos/models.py:1132 aleksis/apps/chronos/models.py:1326 msgid "Groups" msgstr "Групи" -#: aleksis/apps/chronos/models.py:185 +#: aleksis/apps/chronos/models.py:190 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:4 +#: aleksis/apps/chronos/models.py:996 aleksis/apps/chronos/models.py:1332 msgid "Rooms" msgstr "Кімнати" -#: aleksis/apps/chronos/models.py:191 +#: aleksis/apps/chronos/models.py:196 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:3 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:28 #: aleksis/apps/chronos/templates/chronos/supervision_event_description.txt:1 +#: aleksis/apps/chronos/models.py:352 aleksis/apps/chronos/models.py:422 +#: aleksis/apps/chronos/models.py:998 aleksis/apps/chronos/models.py:1137 +#: aleksis/apps/chronos/models.py:1338 msgid "Teachers" msgstr "Викладачі" -#: aleksis/apps/chronos/models.py:198 +#: aleksis/apps/chronos/models.py:203 #: aleksis/apps/chronos/templates/chronos/lesson_event_description.txt:2 #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:29 +#: aleksis/apps/chronos/models.py:340 aleksis/apps/chronos/models.py:349 +#: aleksis/apps/chronos/models.py:416 aleksis/apps/chronos/models.py:1129 +#: aleksis/apps/chronos/models.py:1345 msgid "Subject" msgstr "Предмет" -#: aleksis/apps/chronos/models.py:206 +#: aleksis/apps/chronos/models.py:211 #: aleksis/apps/chronos/templates/chronos/partials/subs/badge.html:4 +#: aleksis/apps/chronos/models.py:1353 msgid "Cancelled" msgstr "СкаÑовано" -#: aleksis/apps/chronos/models.py:210 +#: aleksis/apps/chronos/models.py:215 aleksis/apps/chronos/models.py:433 +#: aleksis/apps/chronos/models.py:692 aleksis/apps/chronos/models.py:734 +#: aleksis/apps/chronos/models.py:1147 aleksis/apps/chronos/models.py:1357 msgid "Comment" msgstr "Коментар" -#: aleksis/apps/chronos/models.py:254 aleksis/apps/chronos/models.py:266 -#: aleksis/apps/chronos/models.py:278 -msgid "{} (instead of {})" +#: aleksis/apps/chronos/models.py:220 +msgid "Is this a current change?" msgstr "" -#: aleksis/apps/chronos/models.py:283 aleksis/apps/chronos/models.py:307 +#: aleksis/apps/chronos/models.py:1401 aleksis/apps/chronos/models.py:1413 +#: aleksis/apps/chronos/models.py:1425 +msgid "{} (instead of {})" +msgstr "{} (заміÑть {})" + +#: aleksis/apps/chronos/models.py:291 aleksis/apps/chronos/models.py:315 +#: aleksis/apps/chronos/models.py:394 aleksis/apps/chronos/models.py:490 +#: aleksis/apps/chronos/models.py:716 aleksis/apps/chronos/models.py:1430 +#: aleksis/apps/chronos/models.py:1454 msgid "Lesson" msgstr "Урок" -#: aleksis/apps/chronos/models.py:467 -#, fuzzy -#| msgid "Lesson" +#: aleksis/apps/chronos/models.py:1614 msgid "Lesson Event" msgstr "Урок" -#: aleksis/apps/chronos/models.py:468 -#, fuzzy -#| msgid "Lessons" +#: aleksis/apps/chronos/models.py:1615 msgid "Lesson Events" msgstr "Уроки" -#: aleksis/apps/chronos/models.py:475 +#: aleksis/apps/chronos/models.py:638 aleksis/apps/chronos/models.py:924 +#: aleksis/apps/chronos/models.py:1622 msgid "Supervisions" -msgstr "Контролі" +msgstr "ÐаглÑдачі" -#: aleksis/apps/chronos/models.py:483 -#, fuzzy -#| msgid "Supervision" +#: aleksis/apps/chronos/models.py:1630 msgid "Supervision: {}" -msgstr "Контроль" +msgstr "Контроль: {}" #: aleksis/apps/chronos/preferences.py:21 msgid "Timetables" @@ -169,52 +237,87 @@ msgid "Use parent groups in timetable views" msgstr "ВикориÑтовувати батьківÑькі групи в оглÑді розкладу" #: aleksis/apps/chronos/preferences.py:31 -#, fuzzy -#| msgid "If an lesson or substitution has only one group and this group has parent groups, show the parent groups instead of the original group." -msgid "If a lesson or substitution has only one group and this group has parent groups, show the parent groups instead of the original group." -msgstr "Якщо урок або заміна має лише одну групу Ñ– Ñ†Ñ Ð³Ñ€ÑƒÐ¿Ð° має батьківÑькі групи, відображати ці батьківÑькі групи заміÑть дійÑної групи." +msgid "" +"If a lesson or substitution has only one group and this group has parent " +"groups, show the parent groups instead of the original group." +msgstr "" +"Якщо урок або заміна має лише одну групу Ñ– Ñ†Ñ Ð³Ñ€ÑƒÐ¿Ð° має батьківÑькі групи, " +"відображати ці батьківÑькі групи заміÑть дійÑної групи." -#: aleksis/apps/chronos/preferences.py:44 -#, fuzzy -#| msgid "Lesson substitutions" +#: aleksis/apps/chronos/preferences.py:65 msgid "Relevant days for substitution plans" -msgstr "Заміни уроків" +msgstr "Відповідні дні Ð´Ð»Ñ Ð¿Ð»Ð°Ð½Ñ–Ð² заміни" -#: aleksis/apps/chronos/preferences.py:61 +#: aleksis/apps/chronos/preferences.py:82 msgid "Time when substitution plans switch to the next day" -msgstr "" +msgstr "Ð§Ð°Ñ Ð¿ÐµÑ€ÐµÐ¼Ð¸ÐºÐ°Ð½Ð½Ñ Ð¿Ð»Ð°Ð½Ñ–Ð² замін на наÑтупний день" #: aleksis/apps/chronos/preferences.py:70 +#: aleksis/apps/chronos/preferences.py:91 msgid "Number of days shown on substitutions print view" msgstr "КількіÑть днів Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ñƒ друкованому виглÑді замін" #: aleksis/apps/chronos/preferences.py:78 +#: aleksis/apps/chronos/preferences.py:99 msgid "Show header box in substitution views" msgstr "Відображати заголовок в оглÑді замін" #: aleksis/apps/chronos/preferences.py:88 -msgid "Show parent groups in header box in substitution views instead of original groups" -msgstr "У заголовку в оглÑді замін відображати батьківÑькі групи заміÑть дійÑних груп" +#: aleksis/apps/chronos/preferences.py:109 +msgid "" +"Show parent groups in header box in substitution views instead of original " +"groups" +msgstr "" +"У заголовку в оглÑді замін відображати батьківÑькі групи заміÑть дійÑних груп" -#: aleksis/apps/chronos/preferences.py:99 -#, fuzzy -#| msgid "Shorten groups in timetable views" +#: aleksis/apps/chronos/preferences.py:158 msgid "Group types to show in timetables" -msgstr "Скорочувати групи в оглÑді розкладу" +msgstr "Типи груп Ð´Ð»Ñ Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð² розкладах" -#: aleksis/apps/chronos/preferences.py:100 +#: aleksis/apps/chronos/preferences.py:159 msgid "If you leave it empty, all groups will be shown." -msgstr "" +msgstr "Якщо залишити порожнім, будуть показані уÑÑ– групи." -#: aleksis/apps/chronos/preferences.py:110 +#: aleksis/apps/chronos/preferences.py:169 msgid "Lesson calendar feed color" +msgstr "Колір ÐºÐ°Ð»ÐµÐ½Ð´Ð°Ñ€Ñ Ð´Ð»Ñ ÑƒÑ€Ð¾ÐºÑ–Ð²" + +#: aleksis/apps/chronos/preferences.py:181 +msgid "Supervision calendar feed color" +msgstr "Колір ÐºÐ°Ð»ÐµÐ½Ð´Ð°Ñ€Ñ Ð´Ð»Ñ Ð½Ð°Ð³Ð»Ñду" + +#: aleksis/apps/chronos/preferences.py:132 +msgid "Days of the week that appear in the timetable" msgstr "" -#: aleksis/apps/chronos/preferences.py:122 +#: aleksis/apps/chronos/preferences.py:134 +msgid "Sunday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:135 +msgid "Monday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:136 +msgid "Tuesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:137 +msgid "Wednesday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:138 +msgid "Thursday" +msgstr "" + +#: aleksis/apps/chronos/preferences.py:139 #, fuzzy -#| msgid "Supervision area" -msgid "Supervision calendar feed color" -msgstr "Зона контролю" +msgid "Friday" +msgstr "Вихідний" + +#: aleksis/apps/chronos/preferences.py:140 +msgid "Saturday" +msgstr "" #: aleksis/apps/chronos/templates/chronos/partials/headerbox.html:10 msgid "Absent teachers" @@ -234,9 +337,10 @@ msgstr "Ðа Ñкі групи впливає" #: aleksis/apps/chronos/templates/chronos/partials/subs/period.html:12 msgid "all day" -msgstr "" +msgstr "увеÑÑŒ день" #: aleksis/apps/chronos/templates/chronos/partials/subs/subject.html:3 +#: aleksis/apps/chronos/models.py:923 aleksis/apps/chronos/models.py:936 msgid "Supervision" msgstr "Контроль" @@ -249,12 +353,12 @@ msgid "Substitutions" msgstr "Заміни" #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:27 -#, fuzzy -#| msgid "Timetable" msgid "Time" -msgstr "Розклад" +msgstr "ЧаÑ" #: aleksis/apps/chronos/templates/chronos/substitutions_print.html:30 +#: aleksis/apps/chronos/models.py:425 aleksis/apps/chronos/models.py:504 +#: aleksis/apps/chronos/models.py:673 aleksis/apps/chronos/models.py:1144 msgid "Room" msgstr "Кімната" @@ -268,305 +372,428 @@ msgstr "Заміни недоÑтупні." #: aleksis/apps/chronos/templates/chronos/supervision_event_description.txt:2 msgid "Areas" -msgstr "" +msgstr "ОблаÑті" -#~ msgid "Linked validity range" -#~ msgstr "Пов'Ñзаний діапазон" +#: aleksis/apps/chronos/mixins.py:25 +msgid "Linked validity range" +msgstr "Пов'Ñзана облаÑть дії" -#~ msgid "School term" -#~ msgstr "Ðавчальний рік" +#: aleksis/apps/chronos/models.py:87 +msgid "School term" +msgstr "Ðавчальний рік" -#~ msgid "Start date" -#~ msgstr "Дата початку" +#: aleksis/apps/chronos/models.py:92 aleksis/apps/chronos/models.py:676 +#: aleksis/apps/chronos/models.py:747 aleksis/apps/chronos/models.py:979 +msgid "Start date" +msgstr "Дата початку" -#~ msgid "End date" -#~ msgstr "Дата закінченнÑ" +#: aleksis/apps/chronos/models.py:93 aleksis/apps/chronos/models.py:677 +#: aleksis/apps/chronos/models.py:748 aleksis/apps/chronos/models.py:980 +msgid "End date" +msgstr "Дата закінченнÑ" -#~ msgid "The start date must be earlier than the end date." -#~ msgstr "Початкова дата повинна бути раніше кінцевої." +#: aleksis/apps/chronos/models.py:112 +msgid "The start date must be earlier than the end date." +msgstr "Початкова дата повинна бути раніше кінцевої дати." -#~ msgid "The validity range must be within the school term." -#~ msgstr "Контрольний період повинен бути в рамках навчального року." +#: aleksis/apps/chronos/models.py:118 +msgid "The validity range must be within the school term." +msgstr "ОблаÑть дії повинна бути в межах навчального року." -#~ msgid "There is already a validity range for this time or a part of this time." -#~ msgstr "Ðа цей Ñ‡Ð°Ñ Ð°Ð±Ð¾ на чаÑтину цього чаÑу контрольний період вже призначений." +#: aleksis/apps/chronos/models.py:125 +msgid "There is already a validity range for this time or a part of this time." +msgstr "Ðа цей Ñ‡Ð°Ñ Ð°Ð±Ð¾ на чаÑтину цього чаÑу облаÑть дії вже призначена." -#~ msgid "Validity range" -#~ msgstr "Контрольний період" - -#~ msgid "Validity ranges" -#~ msgstr "Контрольні періоди" +#: aleksis/apps/chronos/models.py:132 +msgid "Validity range" +msgstr "ОблаÑть дії" -#~ msgid "Week day" -#~ msgstr "День тижнÑ" +#: aleksis/apps/chronos/models.py:133 +msgid "Validity ranges" +msgstr "ОблаÑті дії" -#~ msgid "Number of period" -#~ msgstr "Ðомер урока" +#: aleksis/apps/chronos/models.py:149 +msgid "Week day" +msgstr "День тижнÑ" -#~ msgid "Start time" -#~ msgstr "Ð§Ð°Ñ Ð¿Ð¾Ñ‡Ð°Ñ‚ÐºÑƒ" +#: aleksis/apps/chronos/models.py:151 +msgid "Number of period" +msgstr "Ðомер уроку" -#~ msgid "End time" -#~ msgstr "Ð§Ð°Ñ Ð·Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ" +#: aleksis/apps/chronos/models.py:153 +msgid "Start time" +msgstr "Ð§Ð°Ñ Ð¿Ð¾Ñ‡Ð°Ñ‚ÐºÑƒ" -#~ msgid "Time period" -#~ msgstr "Ð§Ð°Ñ ÑƒÑ€Ð¾ÐºÑƒ" +#: aleksis/apps/chronos/models.py:154 +msgid "End time" +msgstr "Ð§Ð°Ñ Ð·Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ" -#~ msgid "Time periods" -#~ msgstr "Ð§Ð°Ñ ÑƒÑ€Ð¾ÐºÑ–Ð²" +#: aleksis/apps/chronos/models.py:324 aleksis/apps/chronos/models.py:496 +#: aleksis/apps/chronos/models.py:1122 +msgid "Time period" +msgstr "Ð§Ð°Ñ ÑƒÑ€Ð¾ÐºÑƒ" -#~ msgid "Short name" -#~ msgstr "Коротке ім'Ñ" +#: aleksis/apps/chronos/models.py:325 +msgid "Time periods" +msgstr "Ð§Ð°Ñ ÑƒÑ€Ð¾ÐºÑ–Ð²" -#~ msgid "Long name" -#~ msgstr "Довге ім'Ñ" +#: aleksis/apps/chronos/models.py:329 aleksis/apps/chronos/models.py:625 +#: aleksis/apps/chronos/models.py:793 aleksis/apps/chronos/models.py:810 +msgid "Short name" +msgstr "Коротке ім'Ñ" -#~ msgid "Foreground colour" -#~ msgstr "ОÑновний колір" +#: aleksis/apps/chronos/models.py:330 aleksis/apps/chronos/models.py:794 +#: aleksis/apps/chronos/models.py:811 +msgid "Long name" +msgstr "Повна назва" -#~ msgid "Background colour" -#~ msgstr "Колір фону" +#: aleksis/apps/chronos/models.py:332 +msgid "Foreground colour" +msgstr "Колір переднього плану" -#~ msgid "Subjects" -#~ msgstr "Предмети" +#: aleksis/apps/chronos/models.py:333 +msgid "Background colour" +msgstr "Колір фону" -#~ msgid "Periods" -#~ msgstr "Години" +#: aleksis/apps/chronos/models.py:341 +msgid "Subjects" +msgstr "Предмети" -#~ msgid "Week" -#~ msgstr "Тиждень" +#: aleksis/apps/chronos/models.py:358 +msgid "Periods" +msgstr "Години" -#~ msgid "Year" -#~ msgstr "Рік" +#: aleksis/apps/chronos/models.py:403 aleksis/apps/chronos/models.py:1116 +msgid "Week" +msgstr "Тиждень" -#~ msgid "Lesson period" -#~ msgstr "Урок за розкладом" +#: aleksis/apps/chronos/models.py:404 aleksis/apps/chronos/models.py:1117 +msgid "Year" +msgstr "Рік" -#~ msgid "Cancelled?" -#~ msgstr "СкаÑовано?" +#: aleksis/apps/chronos/models.py:407 aleksis/apps/chronos/models.py:620 +msgid "Lesson period" +msgstr "Урок за розкладом" -#~ msgid "Cancelled for teachers?" -#~ msgstr "СкаÑовано Ð´Ð»Ñ Ð²Ð¸ÐºÐ»Ð°Ð´Ð°Ñ‡Ñ–Ð²?" +#: aleksis/apps/chronos/models.py:428 +msgid "Cancelled?" +msgstr "СкаÑовано?" -#~ msgid "Lessons can only be either substituted or cancelled." -#~ msgstr "Уроки можуть бути лише або замінені, або ÑкаÑовані." +#: aleksis/apps/chronos/models.py:430 +msgid "Cancelled for teachers?" +msgstr "СкаÑовано Ð´Ð»Ñ Ð²Ð¸ÐºÐ»Ð°Ð´Ð°Ñ‡Ñ–Ð²?" -#~ msgid "Lesson substitution" -#~ msgstr "Заміна уроку" +#: aleksis/apps/chronos/models.py:437 +msgid "Lessons can only be either substituted or cancelled." +msgstr "Уроки можуть бути лише або замінені, або ÑкаÑовані." -#~ msgid "Lesson substitutions" -#~ msgstr "Заміни уроків" +#: aleksis/apps/chronos/models.py:477 +msgid "Lesson substitution" +msgstr "Заміна уроку" -#~ msgid "Lesson periods" -#~ msgstr "Ðавчальні години" +#: aleksis/apps/chronos/models.py:478 +msgid "Lesson substitutions" +msgstr "Заміни уроків" -#~ msgid "Absence reason" -#~ msgstr "Причина відÑутноÑті" +#: aleksis/apps/chronos/models.py:621 +msgid "Lesson periods" +msgstr "Ðавчальні години" -#~ msgid "Absence reasons" -#~ msgstr "Причини відÑутноÑті" +#: aleksis/apps/chronos/models.py:635 aleksis/apps/chronos/models.py:648 +msgid "Absence reason" +msgstr "Причина відÑутноÑті" -#~ msgid "Teacher" -#~ msgstr "Викладач" +#: aleksis/apps/chronos/models.py:636 +msgid "Absence reasons" +msgstr "Причини відÑутноÑті" -#~ msgid "Group" -#~ msgstr "Група" +#: aleksis/apps/chronos/models.py:657 aleksis/apps/chronos/models.py:889 +#: aleksis/apps/chronos/models.py:943 +msgid "Teacher" +msgstr "Викладач" -#~ msgid "Start period" -#~ msgstr "Початок уроків" +#: aleksis/apps/chronos/models.py:665 +msgid "Group" +msgstr "Група" -#~ msgid "End period" -#~ msgstr "Ð—Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ ÑƒÑ€Ð¾ÐºÑ–Ð²" +#: aleksis/apps/chronos/models.py:681 aleksis/apps/chronos/models.py:723 +msgid "Start period" +msgstr "Початок уроків" -#~ msgid "Unknown absence" -#~ msgstr "ВідÑутніÑть без поÑÑнень" +#: aleksis/apps/chronos/models.py:688 aleksis/apps/chronos/models.py:729 +msgid "End period" +msgstr "Ð—Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ ÑƒÑ€Ð¾ÐºÑ–Ð²" -#~ msgid "Absence" -#~ msgstr "ВідÑутніÑть" +#: aleksis/apps/chronos/models.py:702 +msgid "Unknown absence" +msgstr "ВідÑутніÑть без поÑÑнень" -#~ msgid "Absences" -#~ msgstr "ВідÑутноÑті" +#: aleksis/apps/chronos/models.py:707 +msgid "Absence" +msgstr "ВідÑутніÑть" -#~ msgid "Date of exam" -#~ msgstr "Дата Ñ–Ñпиту" +#: aleksis/apps/chronos/models.py:708 +msgid "Absences" +msgstr "ВідÑутноÑті" -#~ msgid "Title" -#~ msgstr "Ðазва" +#: aleksis/apps/chronos/models.py:719 +msgid "Date of exam" +msgstr "Дата Ñ–Ñпиту" -#~ msgid "Exam" -#~ msgstr "ІÑпит" +#: aleksis/apps/chronos/models.py:733 aleksis/apps/chronos/models.py:746 +#: aleksis/apps/chronos/models.py:977 +msgid "Title" +msgstr "Ðазва" -#~ msgid "Exams" -#~ msgstr "ІÑпити" +#: aleksis/apps/chronos/models.py:739 +msgid "Exam" +msgstr "ІÑпит" -#~ msgid "Comments" -#~ msgstr "Коментарі" +#: aleksis/apps/chronos/models.py:740 +msgid "Exams" +msgstr "ІÑпити" -#~ msgid "Holiday" -#~ msgstr "Вихідний" +#: aleksis/apps/chronos/models.py:749 +msgid "Comments" +msgstr "Коментарі" -#~ msgid "Holidays" -#~ msgstr "Вихідні" +#: aleksis/apps/chronos/models.py:789 +msgid "Holidays" +msgstr "Вихідні" -#~ msgid "Supervision area" -#~ msgstr "Зона контролю" +#: aleksis/apps/chronos/models.py:803 aleksis/apps/chronos/models.py:879 +msgid "Supervision area" +msgstr "Зона контролю" -#~ msgid "Supervision areas" -#~ msgstr "Зони контролю" +#: aleksis/apps/chronos/models.py:804 +msgid "Supervision areas" +msgstr "Зони контролю" -#~ msgid "Time period after break starts" -#~ msgstr "ÐÐ°Ð²Ñ‡Ð°Ð½Ð½Ñ Ð¿Ñ–ÑÐ»Ñ Ð¿ÐµÑ€ÐµÑ€Ð²Ð¸ починаєтьÑÑ" +#: aleksis/apps/chronos/models.py:816 +msgid "Time period after break starts" +msgstr "ÐÐ°Ð²Ñ‡Ð°Ð½Ð½Ñ Ð¿Ñ–ÑÐ»Ñ Ð¿ÐµÑ€ÐµÑ€Ð²Ð¸ починаєтьÑÑ" -#~ msgid "Time period before break ends" -#~ msgstr "ÐÐ°Ð²Ñ‡Ð°Ð½Ð½Ñ Ð¿ÐµÑ€ÐµÐ´ перервою закінчуєтьÑÑ" +#: aleksis/apps/chronos/models.py:824 +msgid "Time period before break ends" +msgstr "ÐÐ°Ð²Ñ‡Ð°Ð½Ð½Ñ Ð¿ÐµÑ€ÐµÐ´ перервою закінчуєтьÑÑ" -#~ msgid "Break" -#~ msgstr "Перерва" +#: aleksis/apps/chronos/models.py:864 aleksis/apps/chronos/models.py:883 +msgid "Break" +msgstr "Перерва" -#~ msgid "Breaks" -#~ msgstr "Перерви" +#: aleksis/apps/chronos/models.py:865 +msgid "Breaks" +msgstr "Перерви" -#~ msgid "Date" -#~ msgstr "Дата" +#: aleksis/apps/chronos/models.py:932 +msgid "Date" +msgstr "Дата" -#~ msgid "Supervision substitution" -#~ msgstr "Заміна контролю" +#: aleksis/apps/chronos/models.py:966 +msgid "Supervision substitution" +msgstr "Заміна контролю" -#~ msgid "Supervision substitutions" -#~ msgstr "Заміни контролю" +#: aleksis/apps/chronos/models.py:967 +msgid "Supervision substitutions" +msgstr "Заміни контролю" -#~ msgid "Start time period" -#~ msgstr "Початок навчаннÑ" +#: aleksis/apps/chronos/models.py:985 +msgid "Start time period" +msgstr "Початок навчаннÑ" -#~ msgid "End time period" -#~ msgstr "Ð—Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ Ð½Ð°Ð²Ñ‡Ð°Ð½Ð½Ñ" +#: aleksis/apps/chronos/models.py:991 +msgid "End time period" +msgstr "Ð—Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ Ð½Ð°Ð²Ñ‡Ð°Ð½Ð½Ñ" +#: aleksis/apps/chronos/models.py:1005 #, python-brace-format -#~ msgid "Event {pk}" -#~ msgstr "ÐŸÐ¾Ð´Ñ–Ñ {pk}" +msgid "Event {pk}" +msgstr "ÐŸÐ¾Ð´Ñ–Ñ {pk}" -#~ msgid "Event" -#~ msgstr "ПодіÑ" +#: aleksis/apps/chronos/models.py:1103 +msgid "Event" +msgstr "ПодіÑ" -#~ msgid "Events" -#~ msgstr "Події" +#: aleksis/apps/chronos/models.py:1104 +msgid "Events" +msgstr "Події" -#~ msgid "Related exam" -#~ msgstr "Пов'Ñзані Ñ–Ñпити" +#: aleksis/apps/chronos/models.py:1152 +msgid "Related exam" +msgstr "Пов'Ñзані Ñ–Ñпити" -#~ msgid "Extra lesson" -#~ msgstr "Додатковий урок" +#: aleksis/apps/chronos/models.py:1181 +msgid "Extra lesson" +msgstr "Додатковий урок" -#~ msgid "Extra lessons" -#~ msgstr "Додаткові уроки" +#: aleksis/apps/chronos/models.py:1182 +msgid "Extra lessons" +msgstr "Додаткові уроки" -#~ msgid "Can view all lessons per day" -#~ msgstr "Може переглÑдати уÑÑ– уроки окремого днÑ" +#: aleksis/apps/chronos/models.py:1297 +msgid "Can view all lessons per day" +msgstr "Може бачити уÑÑ– уроки окремого днÑ" -#, fuzzy -#~| msgid "Can view all lessons per day" -#~ msgid "Can view all supervisions per day" -#~ msgstr "Може переглÑдати уÑÑ– уроки окремого днÑ" - -#~ msgid "Shorten groups in timetable views" -#~ msgstr "Скорочувати групи в оглÑді розкладу" +#: aleksis/apps/chronos/preferences.py:42 +msgid "Shorten groups in timetable views" +msgstr "Скорочувати групи в оглÑді розкладу" -#~ msgid "If there are more groups than the set limit, they will be collapsed." -#~ msgstr "Якщо груп більше за вÑтановлений ліміт, вони будуть згруповані." +#: aleksis/apps/chronos/preferences.py:43 +msgid "If there are more groups than the set limit, they will be collapsed." +msgstr "Якщо груп більше за вÑтановлений ліміт, вони будуть згруповані." -#~ msgid "Limit of groups for shortening of groups" -#~ msgstr "Ліміт груп Ð´Ð»Ñ ÑÐºÐ¾Ñ€Ð¾Ñ‡ÐµÐ½Ð½Ñ Ð³Ñ€ÑƒÐ¿" +#: aleksis/apps/chronos/preferences.py:51 +msgid "Limit of groups for shortening of groups" +msgstr "Ліміт груп Ð´Ð»Ñ ÑÐºÐ¾Ñ€Ð¾Ñ‡ÐµÐ½Ð½Ñ Ð³Ñ€ÑƒÐ¿" -#, fuzzy -#~| msgid "If an user activates shortening of groups,they will be collapsed if there are more groups than this limit." -#~ msgid "If a user activates shortening of groups,they will be collapsed if there are more groups than this limit." -#~ msgstr "Якщо кориÑтувач активує ÑÐºÐ¾Ñ€Ð¾Ñ‡ÐµÐ½Ð½Ñ Ð³Ñ€ÑƒÐ¿, Ñ– кількіÑть груп буде більше за ліміт, вони згруповуватимутьÑÑ." +#: aleksis/apps/chronos/preferences.py:53 +msgid "" +"If a user activates shortening of groups,they will be collapsed if there are " +"more groups than this limit." +msgstr "" +"Якщо кориÑтувач активує ÑÐºÐ¾Ñ€Ð¾Ñ‡ÐµÐ½Ð½Ñ Ð³Ñ€ÑƒÐ¿, Ñ– кількіÑть груп буде більше за " +"ліміт, вони групуватимутьÑÑ." -#~ msgid "How many days in advance users should be notified about timetable changes?" -#~ msgstr "За Ñкільки днів потрібно інформувати кориÑтувачів щодо змін у розкладі?" +#: aleksis/apps/chronos/preferences.py:118 +msgid "" +"How many days in advance users should be notified about timetable changes?" +msgstr "" +"За Ñкільки днів потрібно інформувати кориÑтувачів щодо змін у розкладі?" -#~ msgid "Time for sending notifications about timetable changes" -#~ msgstr "Ð§Ð°Ñ Ð´Ð»Ñ Ð½Ð°Ð´ÑÐ¸Ð»Ð°Ð½Ð½Ñ Ñповіщень щодо змін у розкладі" +#: aleksis/apps/chronos/preferences.py:126 +msgid "Time for sending notifications about timetable changes" +msgstr "Ð§Ð°Ñ Ð´Ð»Ñ Ð½Ð°Ð´ÑÐ¸Ð»Ð°Ð½Ð½Ñ Ñповіщень щодо змін у розкладі" -#~ msgid "This is only used for scheduling notifications which doesn't affect the time period configured above. All other notifications affecting the next days are sent immediately." -#~ msgstr "Це викориÑтовуєтьÑÑ Ð»Ð¸ÑˆÐµ Ð´Ð»Ñ Ð¿Ð»Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ Ñповіщень, Ñкі не впливають на навчальний чаÑ, вÑтановлений вище. УÑÑ– інші ÑповіщеннÑ, Ñкі впливають на найближчі дні, надÑилаютьÑÑ Ð½ÐµÐ³Ð°Ð¹Ð½Ð¾." +#: aleksis/apps/chronos/preferences.py:129 +msgid "" +"This is only used for scheduling notifications which doesn't affect the time " +"period configured above. All other notifications affecting the next days are " +"sent immediately." +msgstr "" +"Це викориÑтовуєтьÑÑ Ð»Ð¸ÑˆÐµ Ð´Ð»Ñ Ð¿Ð»Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ Ñповіщень, Ñкі не впливають на " +"навчальний чаÑ, вÑтановлений вище. УÑÑ– інші ÑповіщеннÑ, Ñкі впливають на " +"найближчі дні, надÑилаютьÑÑ Ð½ÐµÐ³Ð°Ð¹Ð½Ð¾." -#~ msgid "Send notifications for current timetable changes" -#~ msgstr "ÐадіÑлати ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ñ‰Ð¾Ð´Ð¾ поточних змін у розкладі" +#: aleksis/apps/chronos/preferences.py:140 +#: aleksis/apps/chronos/preferences.py:148 +msgid "Send notifications for current timetable changes" +msgstr "ÐадіÑлати ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ñ‰Ð¾Ð´Ð¾ поточних змін у розкладі" +#: aleksis/apps/chronos/util/notifications.py:48 #, python-brace-format -#~ msgid "The {subject} lesson in the {period}. period on {day} has been cancelled." -#~ msgstr "Урок {subject} на {period} уроці у {day} ÑкаÑований." +msgid "" +"The {subject} lesson in the {period}. period on {day} has been cancelled." +msgstr "Урок {subject} на {period} уроці у {day} ÑкаÑований." +#: aleksis/apps/chronos/util/notifications.py:55 #, python-brace-format -#~ msgid "The {subject} lesson in the {period}. period on {day} has some current changes." -#~ msgstr "Урок {subject} на {period} уроці у {day} зараз дещо змінений." +msgid "" +"The {subject} lesson in the {period}. period on {day} has some current " +"changes." +msgstr "Урок {subject} на {period} уроці у {day} зараз дещо змінений." +#: aleksis/apps/chronos/util/notifications.py:64 #, python-brace-format -#~ msgid "The teacher {old} is substituted by {new}." -#~ msgid_plural "The teachers {old} are substituted by {new}." -#~ msgstr[0] "Викладач {old} замінений викладачем {new}." -#~ msgstr[1] "Викладачі {old} замінені викладачами {new}." -#~ msgstr[2] "Викладачі {old} замінені викладачами {new}." -#~ msgstr[3] "Викладачі {old} замінені викладачами {new}." - +msgid "The teacher {old} is substituted by {new}." +msgid_plural "The teachers {old} are substituted by {new}." +msgstr[0] "Викладач {old} замінений викладачем {new}." +msgstr[1] "Викладачі {old} замінені викладачами {new}." +msgstr[2] "Викладачі {old} замінені викладачами {new}." +msgstr[3] "Викладачі {old} замінені викладачами {new}." + +#: aleksis/apps/chronos/util/notifications.py:76 #, python-brace-format -#~ msgid "The subject is changed to {subject}." -#~ msgstr "Предмет замінений на {subject}." +msgid "The subject is changed to {subject}." +msgstr "Предмет замінений на {subject}." +#: aleksis/apps/chronos/util/notifications.py:82 #, python-brace-format -#~ msgid "The lesson is moved from {old} to {new}." -#~ msgstr "Урок перенеÑений з {old} до {new}." +msgid "The lesson is moved from {old} to {new}." +msgstr "Урок перенеÑений з {old} до {new}." +#: aleksis/apps/chronos/util/notifications.py:91 #, python-brace-format -#~ msgid "There is an additional comment: {comment}." -#~ msgstr "Маємо додатковий коментар: {comment}." +msgid "There is an additional comment: {comment}." +msgstr "Маємо додатковий коментар: {comment}." +#: aleksis/apps/chronos/util/notifications.py:99 #, python-brace-format -#~ msgid "There is an event that starts on {date_start}, {period_from}. period and ends on {date_end}, {period_to}. period:" -#~ msgstr "Маємо подію, Ñка розпочинаєтьÑÑ {date_start}, на {period_from}. уроці та закінчуєтьÑÑ {date_end}, на {period_to}. уроці:" +msgid "" +"There is an event that starts on {date_start}, {period_from}. period and " +"ends on {date_end}, {period_to}. period:" +msgstr "" +"Маємо подію, Ñка розпочинаєтьÑÑ {date_start}, на {period_from}. уроці та " +"закінчуєтьÑÑ {date_end}, на {period_to}. уроці:" +#: aleksis/apps/chronos/util/notifications.py:112 #, python-brace-format -#~ msgid "There is an event on {date} from the {period_from}. period to the {period_to}. period:" -#~ msgstr "Маємо подію на {date} з {period_from}. уроку до ÐºÑ–Ð½Ñ†Ñ {period_to}. уроку:" +msgid "" +"There is an event on {date} from the {period_from}. period to the " +"{period_to}. period:" +msgstr "" +"Маємо подію на {date} з {period_from}. уроку до ÐºÑ–Ð½Ñ†Ñ {period_to}. уроку:" +#: aleksis/apps/chronos/util/notifications.py:123 +#: aleksis/apps/chronos/util/notifications.py:143 #, python-brace-format -#~ msgid "Groups: {groups}" -#~ msgstr "Групи: {groups}" +msgid "Groups: {groups}" +msgstr "Групи: {groups}" +#: aleksis/apps/chronos/util/notifications.py:125 +#: aleksis/apps/chronos/util/notifications.py:147 #, python-brace-format -#~ msgid "Teachers: {teachers}" -#~ msgstr "Викладачі: {teachers}" +msgid "Teachers: {teachers}" +msgstr "Викладачі: {teachers}" +#: aleksis/apps/chronos/util/notifications.py:128 #, python-brace-format -#~ msgid "Rooms: {rooms}" -#~ msgstr "Кімнати: {rooms}" +msgid "Rooms: {rooms}" +msgstr "Кімнати: {rooms}" +#: aleksis/apps/chronos/util/notifications.py:135 #, python-brace-format -#~ msgid "There is an extra lesson on {date} in the {period}. period:" -#~ msgstr "Маємо додатковий урок {date} на {period}. уроці:" +msgid "There is an extra lesson on {date} in the {period}. period:" +msgstr "Маємо додатковий урок {date} на {period}. уроці:" +#: aleksis/apps/chronos/util/notifications.py:145 #, python-brace-format -#~ msgid "Subject: {subject}" -#~ msgstr "Предмет: {subject}" +msgid "Subject: {subject}" +msgstr "Предмет: {subject}" +#: aleksis/apps/chronos/util/notifications.py:149 #, python-brace-format -#~ msgid "Room: {room}" -#~ msgstr "Кімната: {room}" +msgid "Room: {room}" +msgstr "Кімната: {room}" +#: aleksis/apps/chronos/util/notifications.py:151 #, python-brace-format -#~ msgid "Comment: {comment}." -#~ msgstr "Коментар: {comment}." +msgid "Comment: {comment}." +msgstr "Коментар: {comment}." +#: aleksis/apps/chronos/util/notifications.py:154 #, python-brace-format -#~ msgid "The supervision of {old} on {date} between the {period_from}. period and the {period_to}. period in the area {area} is substituted by {new}." -#~ msgstr "Контроль {old}, {date} між {period_from}. та {period_to}. уроками у зоні {area} замінений на {new}." +msgid "" +"The supervision of {old} on {date} between the {period_from}. period and the " +"{period_to}. period in the area {area} is substituted by {new}." +msgstr "" +"Контроль {old}, {date} між {period_from}. та {period_to}. уроками у зоні " +"{area} замінений на {new}." + +#: aleksis/apps/chronos/util/notifications.py:204 +msgid "Timetable" +msgstr "Розклад" + +#: aleksis/apps/chronos/util/notifications.py:205 +msgid "There are current changes to your timetable." +msgstr "У Вашому розкладі Ñ” зміни." -#~ msgid "Timetable" -#~ msgstr "Розклад" +#: aleksis/apps/chronos/models.py:788 +msgid "Holiday" +msgstr "Вихідний" -#~ msgid "There are current changes to your timetable." -#~ msgstr "У Вашому розкладі Ñ” зміни." +#: aleksis/apps/chronos/models.py:1298 +msgid "Can view all supervisions per day" +msgstr "Може бачити веÑÑŒ наглÑд окремого днÑ" #~ msgid "Options for timetables" #~ msgstr "Опції Ð´Ð»Ñ Ñ€Ð¾Ð·ÐºÐ»Ð°Ð´Ñ–Ð²" @@ -598,9 +825,6 @@ msgstr "" #~ msgid "Substitution" #~ msgstr "Заміна" -#~ msgid "Manage substitution" -#~ msgstr "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð°Ð¼Ñ–Ð½Ð°Ð¼Ð¸" - #~ msgid "No teachers timetables available." #~ msgstr "Розклади викладачів недоÑтупні." @@ -679,3 +903,6 @@ msgstr "" #~ msgid "View class register of the current week" #~ msgstr "ОглÑд клаÑного журналу поточного тижнÑ" + +#~ msgid "Manage substitution" +#~ msgstr "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð°Ð¼Ñ–Ð½Ð°Ð¼Ð¸" diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py index 07695bb22e7361f3cbfa02b8d5553d2ad6ed5b5d..3fc2ce6dada6aa7a19e182ed46f3b331e33d2f8f 100644 --- a/aleksis/apps/chronos/managers.py +++ b/aleksis/apps/chronos/managers.py @@ -15,7 +15,9 @@ class TimetableType(Enum): GROUP = "group" TEACHER = "teacher" + PARTICIPANT = "participant" ROOM = "room" + COURSE = "course" @classmethod def from_string(cls, s: Optional[str]): @@ -53,6 +55,13 @@ class LessonEventQuerySet(RecurrencePolymorphicQuerySet): ) return Q(pk__in=amended) + def for_teachers(self, teachers: list[Union[int, Person]]) -> "LessonEventQuerySet": + """Get all lesson events for a list of persons as teacher (including amends).""" + amended = self.filter(Q(amended_by__isnull=False) & (Q(teachers__in=teachers))).values_list( + "amended_by__pk", flat=True + ) + return self.filter(Q(teachers__in=teachers) | Q(pk__in=amended)).distinct() + def for_participant(self, person: Union[int, Person]) -> "LessonEventQuerySet": """Get all lesson events the person participates in (including amends).""" return self.filter(self.for_participant_q(person)).distinct() @@ -77,6 +86,17 @@ class LessonEventQuerySet(RecurrencePolymorphicQuerySet): ) return Q(pk__in=amended) + def for_owner_q(self, person: Union[int, Person]) -> Q: + """Get all lesson events the person owns any group of (including amends).""" + amended = self.filter(Q(amended_by__isnull=False) & Q(groups__owners=person)).values_list( + "amended_by__pk", flat=True + ) + return Q(groups__owners=person) | Q(pk__in=amended) + + def for_owner(self, person: Union[int, Person]) -> "LessonEventQuerySet": + """Get all lesson events the person owns any group of (including amends).""" + return self.filter(self.for_owner_q(person)).distinct() + def for_group(self, group: Union[int, Group]) -> "LessonEventQuerySet": """Get all lesson events for a certain group (including amends/as parent group).""" return self.filter(self.for_group_q(group)).distinct() @@ -210,6 +230,15 @@ class LessonEventQuerySet(RecurrencePolymorphicQuerySet): """Get all lesson events that are amending other events.""" return self.filter(self.amending_q()) + @staticmethod + def current_changes_q() -> Q: + """Get all lesson events that are current changes.""" + return Q(amends__isnull=False) | Q(current_change=True) + + def current_changes(self) -> "LessonEventQuerySet": + """Get all lesson events that are current changes.""" + return self.filter(self.current_changes_q()) + class SupervisionEventQuerySet(LessonEventQuerySet): pass diff --git a/aleksis/apps/chronos/migrations/0020_add_global_permissions.py b/aleksis/apps/chronos/migrations/0020_add_global_permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..9fbc5102bd4d4368836ed3a8756b000a8c4ec174 --- /dev/null +++ b/aleksis/apps/chronos/migrations/0020_add_global_permissions.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.7 on 2024-09-13 21:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('chronos', '0019_remove_old_models'), + ] + + operations = [ + migrations.AlterModelOptions( + name='chronosglobalpermissions', + options={'managed': False, 'permissions': (('view_all_room_timetables', 'Can view all room timetables'), ('view_all_group_timetables', 'Can view all group timetables'), ('view_all_person_timetables', 'Can view all person timetables'), ('view_all_course_timetables', 'Can view all course timetables'), ('view_timetable_overview', 'Can view timetable overview'), ('view_substitutions', 'Can view substitutions table'), ('view_all_room_supervisions', 'Can view all room supervisions'), ('view_all_group_supervisions', 'Can view all group supervisions'), ('view_all_person_supervisions', 'Can view all person supervisions'))}, + ), + ] diff --git a/aleksis/apps/chronos/migrations/0021_lessonevent_current_change.py b/aleksis/apps/chronos/migrations/0021_lessonevent_current_change.py new file mode 100644 index 0000000000000000000000000000000000000000..f05654958c804f287a54dabb9d4d5707341ae463 --- /dev/null +++ b/aleksis/apps/chronos/migrations/0021_lessonevent_current_change.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.3 on 2024-11-30 10:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chronos', '0020_add_global_permissions'), + ] + + operations = [ + migrations.AddField( + model_name='lessonevent', + name='current_change', + field=models.BooleanField(default=False, verbose_name='Is this a current change?'), + ), + ] diff --git a/aleksis/apps/chronos/migrations/0022_add_substitution_global_permission.py b/aleksis/apps/chronos/migrations/0022_add_substitution_global_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..8244150f1da622dc6b96efe8d34cf5b25756cccd --- /dev/null +++ b/aleksis/apps/chronos/migrations/0022_add_substitution_global_permission.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.6 on 2024-06-07 14:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('chronos', '0021_lessonevent_current_change'), + ] + + operations = [ + migrations.AlterModelOptions( + name='chronosglobalpermissions', + options={'managed': False, 'permissions': (('view_all_room_timetables', 'Can view all room timetables'), ('view_all_group_timetables', 'Can view all group timetables'), ('view_all_person_timetables', 'Can view all person timetables'), ('view_timetable_overview', 'Can view timetable overview'), ('view_lessons_day', 'Can view all lessons per day'), ('view_supervisions_day', 'Can view all supervisions per day'), ('manage_substitutions', 'Can manage all substitutions'))}, + ), + ] diff --git a/aleksis/apps/chronos/migrations/0020_add_lessoneventalarm.py b/aleksis/apps/chronos/migrations/0023_add_lessoneventalarm.py similarity index 93% rename from aleksis/apps/chronos/migrations/0020_add_lessoneventalarm.py rename to aleksis/apps/chronos/migrations/0023_add_lessoneventalarm.py index 9963d8b54a8c909e9c6a2836f8d99f82186951b1..e8bfac5985463ba76774f24ba837c60869ec5bf1 100644 --- a/aleksis/apps/chronos/migrations/0020_add_lessoneventalarm.py +++ b/aleksis/apps/chronos/migrations/0023_add_lessoneventalarm.py @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('chronos', '0019_remove_old_models'), + ('chronos', '0022_add_substitution_global_permission'), ('core', '0068_add_calendar_alarm'), ] diff --git a/aleksis/apps/chronos/model_extensions.py b/aleksis/apps/chronos/model_extensions.py index 457ef9ef8d0183bf5ddfa5c6799a7e4d14db20d5..61832c4c5eaf81905ec610104d3409cb5912f558 100644 --- a/aleksis/apps/chronos/model_extensions.py +++ b/aleksis/apps/chronos/model_extensions.py @@ -1,14 +1,35 @@ from django.utils.translation import gettext_lazy as _ -from aleksis.core.models import Group, Person +from aleksis.apps.cursus.models import Course +from aleksis.core.models import Group, Person, Room -# Dynamically add extra permissions to Group and Person models in core +# Dynamically add permissions to Group, Person and Room models in core and Course model in cursus # Note: requires migrate afterwards Group.add_permission( "view_group_timetable", _("Can view group timetable"), ) +Group.add_permission( + "manage_group_substitutions", + _("Can manage group substitutions"), +) Person.add_permission( "view_person_timetable", _("Can view person timetable"), ) +Group.add_permission( + "view_group_supervisions", + _("Can view group supervisions"), +) +Person.add_permission( + "view_person_supervisions", + _("Can view person supervisions"), +) +Room.add_permission( + "view_room_supervisions", + _("Can view room supervisions"), +) +Course.add_permission( + "view_course_timetable", + _("Can view course timetable"), +) diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index 95589c4fb155c0cfa24e46235ede803f5969718f..2acfa8d7156eed1f87562cdea574ef014760b005 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -34,6 +34,7 @@ from aleksis.core.mixins import ( ) from aleksis.core.models import CalendarAlarm, CalendarEvent, Group, Person, Room from aleksis.core.util.core_helpers import get_site_preferences, has_person +from aleksis.core.util.predicates import check_global_permission class AutomaticPlan(LiveDocument): @@ -109,7 +110,7 @@ class AutomaticPlan(LiveDocument): # Check if the changed object is relevant for the time period of the PDF file if not version.object.amends: - return + continue if version.object.datetime_start: date_start = version.object.datetime_start.date() @@ -146,8 +147,13 @@ class ChronosGlobalPermissions(GlobalPermissionModel): ("view_all_room_timetables", _("Can view all room timetables")), ("view_all_group_timetables", _("Can view all group timetables")), ("view_all_person_timetables", _("Can view all person timetables")), + ("view_all_course_timetables", _("Can view all course timetables")), ("view_timetable_overview", _("Can view timetable overview")), ("view_substitutions", _("Can view substitutions table")), + ("view_all_room_supervisions", _("Can view all room supervisions")), + ("view_all_group_supervisions", _("Can view all group supervisions")), + ("view_all_person_supervisions", _("Can view all person supervisions")), + ("manage_substitutions", _("Can manage all substitutions")), ) @@ -210,6 +216,9 @@ class LessonEvent(CalendarEvent): blank=True, ) + # current_change=True will show this event in substitutions table + current_change = models.BooleanField(default=False, verbose_name=_("Is this a current change?")) + @property def actual_groups(self: LessonEvent) -> QuerySet[Group]: """Get list of the groups of this lesson event.""" @@ -415,7 +424,7 @@ class LessonEvent(CalendarEvent): return cls.objects.none() q = Q() - + prefetch_absences = False if params: try: obj_id = int(params.get("id", 0)) @@ -423,9 +432,11 @@ class LessonEvent(CalendarEvent): obj_id = None type_ = params.get("type", None) + prefetch_absences = params.get("prefetch_absences", False) not_amended = params.get("not_amended", False) not_amending = params.get("not_amending", False) amending = params.get("amending", False) + current_changes = params.get("current_changes", False) own = params.get("own", False) if not_amended: @@ -437,6 +448,9 @@ class LessonEvent(CalendarEvent): if amending: q = q & LessonEventQuerySet.amending_q() + if current_changes: + q = q & LessonEventQuerySet.current_changes_q() + if request and "own" in params: if own: q = q & LessonEventQuerySet.for_person_q(request.user.person) @@ -444,6 +458,40 @@ class LessonEvent(CalendarEvent): q = q & LessonEventQuerySet.related_to_person_q(request.user.person) if type_ and obj_id: + if request and not ( + ( + type_ == "GROUP" + and check_global_permission( + request.user, "chronos.view_all_group_timetables" + ) + ) + or ( + type_ == "TEACHER" + or type_ == "PARTICIPANT" + or type_ == "OWNER" + and check_global_permission( + request.user, "chronos.view_all_person_timetables" + ) + ) + or ( + type_ == "ROOM" + and check_global_permission( + request.user, "chronos.view_all_room_timetables" + ) + ) + or ( + type_ == "COURSE" + and check_global_permission( + request.user, "chronos.view_all_course_timetables" + ) + ) + ): + # inline import needed to avoid circular import + from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk + + obj = get_el_by_pk(request, type_.lower(), obj_id) + if not request.user.has_perm("chronos.view_timetable_rule", obj): + return LessonEvent.objects.none() if type_ == "TEACHER": q = q & LessonEventQuerySet.for_teacher_q(obj_id) elif type_ == "PARTICIPANT": @@ -454,21 +502,130 @@ class LessonEvent(CalendarEvent): q = q & LessonEventQuerySet.for_room_q(obj_id) elif type_ == "COURSE": q = q & LessonEventQuerySet.for_course_q(obj_id) + elif type_ == "OWNER": + q = q & LessonEventQuerySet.for_owner_q(obj_id) elif request: q = q & LessonEventQuerySet.for_person_q(request.user.person) + prefetch_fields = ["groups", "teachers", "rooms", "groups__members"] + if prefetch_absences: + prefetch_fields.append("teachers__kolego_absences") + objs = super().get_objects( request, params, start_qs=cls.objects.not_instance_of(SupervisionEvent), additional_filter=q, select_related=["subject", "course"], - prefetch_related=["groups", "teachers", "rooms", "groups__members"], + prefetch_related=prefetch_fields, **kwargs, ) return objs + @classmethod + def get_for_substitution_overview( + cls, + date_start: datetime, + date_end: datetime, + request: HttpRequest, + obj_type: str | None = None, + obj_id: str | None = None, + teacher: str | None = None, + incomplete: str | None = False, + ) -> list: + """Get all the amended lessons for an object and a time frame. + + obj_type may be one of TEACHER, GROUP, ROOM, COURSE + """ + + # 1. Find all LessonEvents for all Lessons of this Group in this date range + # which are not themselves amending another lessonEvent + event_params = { + "not_amending": True, + "prefetch_absences": True, + } + + if request.user.has_perm("chronos.manage_substitutions"): + event_params["all"] = True + else: + event_params["own"] = False + + if obj_type is not None and obj_id is not None: + event_params.update( + { + "type": obj_type, + "id": obj_id, + } + ) + elif not request.user.has_perm("chronos.manage_substitutions"): + event_params.update( + { + "type": "OWNER", + "id": request.user.person.id, + } + ) + + event_queryset = LessonEvent.get_objects(request=request, params=event_params) + + if teacher: + event_queryset = event_queryset.for_teacher(teacher) + else: + affected_teachers = Person.objects.filter( + Q(kolego_absences__datetime_start__lte=date_end) + & Q(kolego_absences__datetime_end__gte=date_start) + ) + event_queryset = event_queryset.for_teachers(affected_teachers) + + events = LessonEvent.get_single_events( + start=date_start, + end=date_end, + request=request, + with_reference_object=True, + queryset=event_queryset, + ) + + # 2. For each lessonEvent → check if there are any teachers with absences that overlap + # the lesson & if yes, check if there is already an amendment for that lesson + # If so, add it to a list, if not, create a dummy one + + substitutions = [] + + for event in events: + reference_obj = event["REFERENCE_OBJECT"] + + datetime_start = event["DTSTART"].dt + datetime_end = event["DTEND"].dt + + affected_teachers = reference_obj.teachers.filter( + Q(kolego_absences__datetime_start__lte=datetime_end) + & Q(kolego_absences__datetime_end__gte=datetime_start) + ) + + if affected_teachers.exists(): + existing_substitutions = reference_obj.amended_by.instance_of(cls).filter( + datetime_start=event["DTSTART"].dt, + datetime_end=event["DTEND"].dt, + ) + + if existing_substitutions.exists(): + if incomplete: + continue + substitution = existing_substitutions.first() + substitutions.append(substitution) + + else: + substitutions.append( + cls( + pk=f"DUMMY;{reference_obj.id};{datetime_start.isoformat()};{datetime_end.isoformat()}", + amends=reference_obj, + datetime_start=datetime_start, + datetime_end=datetime_end, + ) + ) + + return substitutions + def save(self, *args, **kwargs): adding_status = self._state.adding @@ -614,6 +771,7 @@ class SupervisionEvent(LessonEvent): not_amended = params.get("not_amended", False) not_amending = params.get("not_amending", False) amending = params.get("amending", False) + current_changes = params.get("current_changes", False) if not_amended: q = q & SupervisionEventQuerySet.not_amended_q() @@ -624,7 +782,36 @@ class SupervisionEvent(LessonEvent): if amending: q = q & SupervisionEventQuerySet.amending_q() + if current_changes: + q = q & SupervisionEventQuerySet.current_changes_q() + if type_ and obj_id: + if request and not ( + ( + type_ == "GROUP" + and check_global_permission( + request.user, "chronos.view_all_group_supervisions" + ) + ) + or ( + type_ == "TEACHER" + and check_global_permission( + request.user, "chronos.view_all_person_supervisions" + ) + ) + or ( + type_ == "ROOM" + and check_global_permission( + request.user, "chronos.view_all_room_supervisions" + ) + ) + ): + # inline import needed to avoid circular import + from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk + + obj = get_el_by_pk(request, type_.lower(), obj_id) + if not request.user.has_perm("chronos.view_supervisions_rule", obj): + return SupervisionEvent.objects.none() if type_ == "TEACHER": q = q & SupervisionEventQuerySet.for_teacher_q(obj_id) elif type_ == "GROUP": diff --git a/aleksis/apps/chronos/preferences.py b/aleksis/apps/chronos/preferences.py index ec8bf739a4238519ae8dc64ca742dab077a505bc..35068ea34ede3068e7eea4ca9435f7f7afa24bff 100644 --- a/aleksis/apps/chronos/preferences.py +++ b/aleksis/apps/chronos/preferences.py @@ -167,4 +167,21 @@ class TimeInAdvanceAlarms(DurationPreference): name = "time_in_advance_alarms" default = timedelta(hours=24) verbose_name = _("How much in advance should lesson event alarms be sent?") + + +@site_preferences_registry.register +class DaysInCalendar(MultipleChoicePreference): + section = chronos + name = "days_in_calendar" + default = ["1", "2", "3", "4", "5"] + verbose_name = _("Days of the week that appear in the timetable") + choices = [ + ("0", _("Sunday")), + ("1", _("Monday")), + ("2", _("Tuesday")), + ("3", _("Wednesday")), + ("4", _("Thursday")), + ("5", _("Friday")), + ("6", _("Saturday")), + ] required = True diff --git a/aleksis/apps/chronos/rules.py b/aleksis/apps/chronos/rules.py index c7d0065c6bc2c6180a0fa81188484472e6ed8af7..2f16ba5434c770eb08fd25767b992722a8ca3416 100644 --- a/aleksis/apps/chronos/rules.py +++ b/aleksis/apps/chronos/rules.py @@ -6,7 +6,14 @@ from aleksis.core.util.predicates import ( has_person, ) -from .util.predicates import has_any_timetable_object, has_timetable_perm +from .util.predicates import ( + has_any_group_substitution_perm, + has_any_timetable_object, + has_group_substitution_perm, + has_substitution_perm_by_group, + has_supervisions_perm, + has_timetable_perm, +) # View timetable overview view_timetable_overview_predicate = has_person & ( @@ -18,23 +25,56 @@ add_perm("chronos.view_timetable_overview_rule", view_timetable_overview_predica view_timetable_predicate = has_person & has_timetable_perm add_perm("chronos.view_timetable_rule", view_timetable_predicate) +# View supervisions for group, person or room +view_supervisions_predicate = has_person & has_supervisions_perm +add_perm("chronos.view_supervisions_rule", view_supervisions_predicate) + +# View substitution management overview page +view_substitution_overview_predicate = has_person & ( + has_global_perm("chronos.manage_substitutions") | has_any_group_substitution_perm +) +add_perm("chronos.view_substitution_overview_rule", view_substitution_overview_predicate) + +# Manage substitutions for a group +manage_substitutions_for_group_predicate = has_person & ( + has_global_perm("chronos.manage_substitutions") | has_group_substitution_perm +) +add_perm("chronos.manage_substitutions_for_group_rule", manage_substitutions_for_group_predicate) + +# Add substitution +add_substitution_predicate = has_person & ( + has_global_perm("chronos.manage_substitutions") + | has_substitution_perm_by_group + | has_global_perm("chronos.add_lessonsubstitution") +) +add_perm("chronos.create_substitution_rule", add_substitution_predicate) # Edit substition edit_substitution_predicate = has_person & ( - has_global_perm("chronos.change_lessonevent") | has_object_perm("chronos.change_lessonevent") + has_global_perm("chronos.manage_substitutions") + | has_substitution_perm_by_group + | has_global_perm("chronos.change_lessonevent") + | has_object_perm("chronos.change_lessonevent") ) add_perm("chronos.edit_substitution_rule", edit_substitution_predicate) # Delete substitution delete_substitution_predicate = has_person & ( - has_global_perm("chronos.delete_lessonevent") | has_object_perm("chronos.delete_lessonevent") + has_global_perm("chronos.manage_substitutions") + | has_substitution_perm_by_group + | has_global_perm("chronos.delete_lessonevent") + | has_object_perm("chronos.delete_lessonevent") ) add_perm("chronos.delete_substitution_rule", delete_substitution_predicate) # View substitutions -view_substitutions_predicate = has_person & (has_global_perm("chronos.view_substitutions")) +view_substitutions_predicate = has_person & ( + has_global_perm("chronos.view_substitutions") | has_global_perm("chronos.manage_substitutions") +) add_perm("chronos.view_substitutions_rule", view_substitutions_predicate) # View parent menu entry -view_menu_predicate = has_person & (view_timetable_overview_predicate) +view_menu_predicate = has_person & ( + view_timetable_overview_predicate | view_substitutions_predicate +) add_perm("chronos.view_menu_rule", view_menu_predicate) diff --git a/aleksis/apps/chronos/schema/__init__.py b/aleksis/apps/chronos/schema/__init__.py index d23eb0c7c8bf056b66f3b7c074f2a73e6b6f1182..5e06c397b2481388c77b23940bdd3256bb0b208a 100644 --- a/aleksis/apps/chronos/schema/__init__.py +++ b/aleksis/apps/chronos/schema/__init__.py @@ -1,19 +1,32 @@ +from datetime import datetime + +from django.core.exceptions import PermissionDenied +from django.db.models import Q + import graphene +import graphene_django_optimizer from graphene_django import DjangoObjectType +from reversion import create_revision, set_comment, set_user +from aleksis.apps.cursus.models import Subject from aleksis.core.models import Group, Person, Room from aleksis.core.schema.base import ( BaseBatchCreateMutation, BaseBatchDeleteMutation, BaseBatchPatchMutation, + FilterOrderList, ) from aleksis.core.schema.group import GroupType from aleksis.core.schema.person import PersonType from aleksis.core.schema.room import RoomType +from aleksis.core.util.core_helpers import ( + get_site_preferences, + has_person, +) from ..models import LessonEvent from ..util.build import build_substitutions_list -from ..util.chronos_helpers import get_groups, get_next_relevant_day, get_rooms, get_teachers +from ..util.chronos_helpers import get_groups, get_rooms, get_teachers class TimetablePersonType(DjangoObjectType): @@ -52,6 +65,7 @@ class LessonEventType(DjangoObjectType): "teachers", "groups", "rooms", + "course", "cancelled", "comment", ) @@ -60,6 +74,35 @@ class LessonEventType(DjangoObjectType): } amends = graphene.Field(lambda: LessonEventType, required=False) + old_id = graphene.ID(required=False) + + @staticmethod + def resolve_teachers(root: LessonEvent, info, **kwargs): + if not str(root.pk).startswith("DUMMY") and hasattr(root, "teachers"): + return root.teachers + elif root.amends: + affected_teachers = root.amends.teachers.filter( + Q(kolego_absences__datetime_start__lte=root.datetime_end) + & Q(kolego_absences__datetime_end__gte=root.datetime_start) + ) + return root.amends.teachers.exclude(pk__in=affected_teachers) + return [] + + @staticmethod + def resolve_groups(root: LessonEvent, info, **kwargs): + if not str(root.pk).startswith("DUMMY") and hasattr(root, "groups"): + return root.groups + elif root.amends: + return root.amends.groups + return [] + + @staticmethod + def resolve_rooms(root: LessonEvent, info, **kwargs): + if not str(root.pk).startswith("DUMMY") and hasattr(root, "rooms"): + return root.rooms + elif root.amends: + return root.amends.rooms + return [] class AmendLessonBatchCreateMutation(BaseBatchCreateMutation): @@ -106,6 +149,87 @@ class AmendLessonBatchDeleteMutation(BaseBatchDeleteMutation): permissions = ("chronos.delete_substitution_rule",) +class SubstitutionInputType(graphene.InputObjectType): + id = graphene.ID(required=True) + subject = graphene.ID(required=False) + teachers = graphene.List(graphene.ID, required=False) + rooms = graphene.List(graphene.ID, required=False) + + comment = graphene.String(required=False) + cancelled = graphene.Boolean(required=False) + + +class SubstitutionBatchCreateOrUpdateMutation(graphene.Mutation): + class Arguments: + input = graphene.List(SubstitutionInputType) + + substitutions = graphene.List(LessonEventType) + + @classmethod + def create_or_update(cls, info, substitution): + _id = substitution.id + + # Sadly, we can't use the update_or_create method since we can't check + # different permissions depending on the operation with it. + if _id.startswith("DUMMY"): + dummy, amended_lesson_event_id, datetime_start_iso, datetime_end_iso = _id.split(";") + amended_lesson_event = LessonEvent.objects.get(id=amended_lesson_event_id) + + datetime_start = datetime.fromisoformat(datetime_start_iso).astimezone( + amended_lesson_event.timezone + ) + datetime_end = datetime.fromisoformat(datetime_end_iso).astimezone( + amended_lesson_event.timezone + ) + + if info.context.user.has_perm("chronos.create_substitution_rule", amended_lesson_event): + obj = LessonEvent.objects.create( + datetime_start=datetime_start, + datetime_end=datetime_end, + amends=amended_lesson_event, + comment=substitution.comment or "", + cancelled=substitution.cancelled or False, + ) + if substitution.subject is not None: + obj.subject = Subject.objects.get(pk=substitution.subject) + if substitution.teachers is not None: + obj.teachers.set(Person.objects.filter(pk__in=substitution.teachers)) + if substitution.rooms is not None: + obj.rooms.set(Room.objects.filter(pk__in=substitution.rooms)) + obj.save() + return obj + raise PermissionDenied() + else: + obj = LessonEvent.objects.get(id=_id) + + if not info.context.user.has_perm("chronos.edit_substitution_rule", obj.amends): + raise PermissionDenied() + + if substitution.subject is not None: + obj.subject = Subject.objects.get(pk=substitution.subject) + if substitution.teachers is not None: + obj.teachers.set(Person.objects.filter(pk__in=substitution.teachers)) + if substitution.rooms is not None: + obj.rooms.set(Room.objects.filter(pk__in=substitution.rooms)) + + if substitution.cancelled is not None: + obj.cancelled = substitution.cancelled + if substitution.comment is not None: + obj.comment = substitution.comment + + obj.save() + return obj + + @classmethod + def mutate(cls, root, info, input): # noqa + with create_revision(): + set_user(info.context.user) + set_comment("Updated in substitution overview") + objs = [cls.create_or_update(info, substitution) for substitution in input] + + return SubstitutionBatchCreateOrUpdateMutation(substitutions=objs) + + class TimetableType(graphene.Enum): TEACHER = "teacher" GROUP = "group" @@ -148,11 +272,13 @@ class SubstitutionType(graphene.ObjectType): def resolve_old_groups(root, info): le = root["REFERENCE_OBJECT"] - return le.amends.groups.all() or le.groups.all() + if le.amends and le.amends.groups.all(): + return le.amends.groups.all() + return le.groups.all() def resolve_new_groups(root, info): le = root["REFERENCE_OBJECT"] - if le.groups.all() and le.amends.groups.all(): + if le.groups.all() and le.amends and le.amends.groups.all(): return le.groups.all() else: return [] @@ -171,11 +297,13 @@ class SubstitutionType(graphene.ObjectType): def resolve_old_teachers(root, info): le = root["REFERENCE_OBJECT"] - return le.amends.teachers.all() or le.teachers.all() + if le.amends and le.amends.teachers.all(): + return le.amends.teachers.all() + return le.teachers.all() def resolve_new_teachers(root, info): le = root["REFERENCE_OBJECT"] - if le.teachers.all() and le.amends.teachers.all(): + if le.teachers.all() and le.amends and le.amends.teachers.all(): return le.teachers.all() else: return [] @@ -184,30 +312,32 @@ class SubstitutionType(graphene.ObjectType): le = root["REFERENCE_OBJECT"] if le.name == "supervision": return "SUPERVISION" - elif not le.amends.subject and not le.subject: - return le.amends.title + elif not (le.amends and le.amends.subject) and not le.subject: + if le.amends: + return le.amends.title + return le.title else: - subject = le.amends.subject or le.subject + subject = le.amends.subject if le.amends and le.amends.subject else le.subject return subject.short_name or subject.name def resolve_new_subject(root, info): le = root["REFERENCE_OBJECT"] if le.name == "supervision": return None - elif not le.amends.subject and not le.subject: - return le.title - elif le.subject and le.amends.subject: + elif le.subject and le.amends and le.amends.subject: return le.subject.short_name or le.subject.name else: return None def resolve_old_rooms(root, info): le = root["REFERENCE_OBJECT"] - return le.amends.rooms.all() or le.rooms.all() + if le.amends and le.amends.rooms.all(): + return le.amends.rooms.all() + return le.rooms.all() def resolve_new_rooms(root, info): le = root["REFERENCE_OBJECT"] - if le.rooms.all() and le.amends.rooms.all(): + if le.rooms.all() and le.amends and le.amends.rooms.all(): return le.rooms.all() else: return [] @@ -216,7 +346,7 @@ class SubstitutionType(graphene.ObjectType): return root["REFERENCE_OBJECT"].cancelled def resolve_notes(root, info): - return root["REFERENCE_OBJECT"].title or root["REFERENCE_OBJECT"].comment + return root["REFERENCE_OBJECT"].comment class SubstitutionsForDateType(graphene.ObjectType): @@ -234,19 +364,34 @@ class Query(graphene.ObjectType): SubstitutionsForDateType, date=graphene.Date(), ) + timetable_days = graphene.List(graphene.Int) + + amended_lessons_from_absences = FilterOrderList( + LessonEventType, + obj_type=graphene.String(required=False), + obj_id=graphene.ID(required=False), + date_start=graphene.Date(required=True), + date_end=graphene.Date(required=True), + teacher=graphene.ID(required=False), + incomplete=graphene.Boolean(required=False), + ) def resolve_timetable_teachers(self, info, **kwargs): - return get_teachers(info.context.user) + return graphene_django_optimizer.query( + get_teachers(info.context.user, request=info.context), info + ) def resolve_timetable_groups(self, info, **kwargs): - return get_groups(info.context.user) + return graphene_django_optimizer.query( + get_groups(info.context.user, request=info.context), info + ) def resolve_timetable_rooms(self, info, **kwargs): - return get_rooms(info.context.user) + return graphene_django_optimizer.query(get_rooms(info.context.user), info) def resolve_available_timetables(self, info, **kwargs): all_timetables = [] - for group in get_groups(info.context.user): + for group in get_groups(info.context.user, request=info.context): all_timetables.append( TimetableObjectType( id=group.id, @@ -256,7 +401,7 @@ class Query(graphene.ObjectType): ) ) - for teacher in get_teachers(info.context.user): + for teacher in get_teachers(info.context.user, request=info.context): all_timetables.append( TimetableObjectType( id=teacher.id, @@ -276,17 +421,65 @@ class Query(graphene.ObjectType): return all_timetables def resolve_substitutions_for_date(root, info, date): - substitutions, affected_teachers, affected_groups = build_substitutions_list( - get_next_relevant_day(date) - ) + substitutions, affected_teachers, affected_groups = build_substitutions_list(date) return SubstitutionsForDateType( affected_teachers=affected_teachers, affected_groups=affected_groups, substitutions=[sub["el"] for sub in substitutions], ) + @staticmethod + def resolve_timetable_days(root, info, **kwargs): + first_day = "default" + + if has_person(info.context): + first_day = info.context.user.person.preferences["calendar__first_day_of_the_week"] + + if first_day == "default": + first_day = get_site_preferences()["calendar__first_day_of_the_week"] + + first_day = int(first_day) + + days = list(map(str, range(7))) + sorted_days = days[first_day:] + days[:first_day] + + allowed_days = get_site_preferences()["chronos__days_in_calendar"] + + return list(map(int, filter(lambda d: d in allowed_days, sorted_days))) + + def resolve_amended_lessons_from_absences( + root, + info, + date_start, + date_end, + obj_type="GROUP", + obj_id=None, + teacher=None, + incomplete=False, + **kwargs, + ): + datetime_start = datetime.combine(date_start, datetime.min.time()) + datetime_end = datetime.combine(date_end, datetime.max.time()) + + if ( + obj_id + and not info.context.user.has_perm( + "chronos.manage_substitutions_for_group_rule", Group.objects.get(id=obj_id) + ) + ) or ( + not obj_id and not info.context.user.has_perm("chronos.view_substitution_overview_rule") + ): + raise PermissionDenied() + + return LessonEvent.get_for_substitution_overview( + datetime_start, datetime_end, info.context, obj_type, obj_id, teacher, incomplete + ) + class Mutation(graphene.ObjectType): create_amend_lessons = AmendLessonBatchCreateMutation.Field() patch_amend_lessons = AmendLessonBatchPatchMutation.Field() + patch_amend_lessons_with_amends = AmendLessonBatchPatchMutation.Field() delete_amend_lessons = AmendLessonBatchDeleteMutation.Field() + + create_or_update_substitutions = SubstitutionBatchCreateOrUpdateMutation.Field() diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/comment.html b/aleksis/apps/chronos/templates/chronos/partials/subs/comment.html index 694a0ce3a430b25f295c2ed75120ef2e0ed9db84..17cb2f2b3d5a5f9a4b5e38bce2a7b7635c3934d9 100644 --- a/aleksis/apps/chronos/templates/chronos/partials/subs/comment.html +++ b/aleksis/apps/chronos/templates/chronos/partials/subs/comment.html @@ -1,5 +1,3 @@ -{% if el.title %} - <em>{{ el.title }}</em> -{% elif el.comment %} +{% if el.comment %} <em>{{ el.comment }}</em> {% endif %} diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html b/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html index 033d7b745a36aa3d859ad95b79e705b9ce4cfb87..6d9f79f28736377bf193099702e1d9a08297ee8f 100644 --- a/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html +++ b/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html @@ -3,10 +3,9 @@ {% trans "Supervision" %} {% elif not el.amends.subject and not el.subject %} {% if el.amends.title %} - <s>{{ el.amends.title }}</s> - {% endif %} - {% if el.title %} - <s>{{ el.title }}</s> + {{ el.amends.title }} + {% elif el.title %} + {{ el.title }} {% endif %} {% elif el.cancelled %} <s>{% include "chronos/partials/subject.html" with subject=el.amends.subject %}</s> diff --git a/aleksis/apps/chronos/util/build.py b/aleksis/apps/chronos/util/build.py index 66168ce2496758b6525c98fb31afbd5c686a84b5..e8f0a2cb7a61d2b854125377bd1f9cb05574de10 100644 --- a/aleksis/apps/chronos/util/build.py +++ b/aleksis/apps/chronos/util/build.py @@ -4,7 +4,7 @@ from aleksis.apps.chronos.models import LessonEvent, SupervisionEvent from aleksis.core.models import Group, Person -def build_substitutions_list(wanted_day: date) -> tuple[list[dict], set[Person], set[Group]]: +def build_substitutions_list(wanted_day: date) -> tuple[list[dict], list[Person], list[Group]]: rows = [] affected_teachers = set() affected_groups = set() @@ -12,19 +12,21 @@ def build_substitutions_list(wanted_day: date) -> tuple[list[dict], set[Person], lesson_events = LessonEvent.get_single_events( datetime.combine(wanted_day, time.min), datetime.combine(wanted_day, time.max), - params={"amending": True}, + params={"current_changes": True}, with_reference_object=True, ) for lesson_event in lesson_events: + ref_object = lesson_event["REFERENCE_OBJECT"] affected_teachers.update(lesson_event["REFERENCE_OBJECT"].teachers.all()) - affected_teachers.update(lesson_event["REFERENCE_OBJECT"].amends.teachers.all()) affected_groups.update(lesson_event["REFERENCE_OBJECT"].groups.all()) - affected_groups.update(lesson_event["REFERENCE_OBJECT"].amends.groups.all()) + if ref_object.amends: + affected_teachers.update(ref_object.amends.teachers.all()) + affected_groups.update(ref_object.amends.groups.all()) row = { "type": "substitution", - "sort_a": lesson_event["REFERENCE_OBJECT"].group_names, + "sort_a": ref_object.group_names, "sort_b": str(lesson_event["DTSTART"]), "el": lesson_event, } @@ -34,14 +36,15 @@ def build_substitutions_list(wanted_day: date) -> tuple[list[dict], set[Person], supervision_events = SupervisionEvent.get_single_events( datetime.combine(wanted_day, time.min), datetime.combine(wanted_day, time.max), - params={"amending": True}, + params={"current_changes": True}, with_reference_object=True, ) - print(supervision_events) for supervision_event in supervision_events: - affected_teachers.update(supervision_event["REFERENCE_OBJECT"].teachers.all()) - affected_teachers.update(supervision_event["REFERENCE_OBJECT"].amends.teachers.all()) + ref_object = supervision_event["REFERENCE_OBJECT"] + affected_teachers.update(ref_object.teachers.all()) + if ref_object.amends: + affected_teachers.update(ref_object.amends.teachers.all()) row = { "type": "supervision_substitution", @@ -54,4 +57,7 @@ def build_substitutions_list(wanted_day: date) -> tuple[list[dict], set[Person], rows.sort(key=lambda row: row["sort_a"] + row["sort_b"]) + affected_teachers = sorted(affected_teachers, key=lambda p: p.short_name or p.last_name) + affected_groups = sorted(affected_groups, key=lambda g: g.short_name or g.name) + return rows, affected_teachers, affected_groups diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py index 961fb01460f8c4fa65504d8726d653d223eeef3b..0adb0bd67b8255b1da528888052a9eeb94f58c94 100644 --- a/aleksis/apps/chronos/util/chronos_helpers.py +++ b/aleksis/apps/chronos/util/chronos_helpers.py @@ -2,12 +2,20 @@ from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, Optional from django.db.models import Count, Q +from django.http import HttpRequest, HttpResponseNotFound +from django.shortcuts import get_object_or_404 from guardian.shortcuts import get_objects_for_user +from aleksis.apps.cursus.models import Course from aleksis.core.models import Announcement, Group, Person, Room -from aleksis.core.util.core_helpers import get_site_preferences +from aleksis.core.util.core_helpers import ( + filter_active_school_term, + get_active_school_term, + get_site_preferences, +) +from ..managers import TimetableType from .build import build_substitutions_list if TYPE_CHECKING: @@ -16,11 +24,39 @@ if TYPE_CHECKING: User = get_user_model() # noqa -def get_teachers(user: "User"): +def get_el_by_pk( + request: HttpRequest, + type_: str, + pk: int, + prefetch: bool = False, + *args, + **kwargs, +): + if type_ == TimetableType.GROUP.value: + return get_object_or_404( + Group.objects.prefetch_related("owners", "parent_groups") if prefetch else Group, + pk=pk, + ) + elif type_ == TimetableType.TEACHER.value or type_ == TimetableType.PARTICIPANT.value: + return get_object_or_404(Person, pk=pk) + elif type_ == TimetableType.ROOM.value: + return get_object_or_404(Room, pk=pk) + elif type_ == TimetableType.COURSE.value: + return get_object_or_404(Course, pk=pk) + else: + return HttpResponseNotFound() + + +def get_teachers(user: "User", request=None): """Get the teachers whose timetables are allowed to be seen by current user.""" + school_term = get_active_school_term(request) + + q = Q(courses_as_teacher__groups__school_term=school_term) | Q( + courses_as_teacher__groups__school_term=None + ) teachers = ( - Person.objects.annotate(course_count=Count("courses_as_teacher")) + Person.objects.annotate(course_count=Count("courses_as_teacher", filter=q)) .filter(course_count__gt=0) .order_by("short_name", "last_name") ) @@ -36,10 +72,10 @@ def get_teachers(user: "User"): return teachers -def get_groups(user: "User"): +def get_groups(user: "User", request=None): """Get the groups whose timetables are allowed to be seen by current user.""" - groups = Group.objects.for_current_school_term_or_all() + groups = filter_active_school_term(request, Group.objects.all()) group_types = get_site_preferences()["chronos__group_types_timetables"] diff --git a/aleksis/apps/chronos/util/predicates.py b/aleksis/apps/chronos/util/predicates.py index 7657ea96becd43be0b92b9ebfda24723b119c44d..370b47f990feb1564ef70e8cb1809030481d7d26 100644 --- a/aleksis/apps/chronos/util/predicates.py +++ b/aleksis/apps/chronos/util/predicates.py @@ -3,9 +3,12 @@ from django.db.models import Model from rules import predicate +from aleksis.apps.cursus.models import Course from aleksis.core.models import Group, Person, Room -from aleksis.core.util.predicates import has_global_perm, has_object_perm +from aleksis.core.util.core_helpers import queryset_rules_filter +from aleksis.core.util.predicates import has_any_object, has_global_perm, has_object_perm +from ..models import LessonEvent from .chronos_helpers import get_groups, get_rooms, get_teachers @@ -23,6 +26,8 @@ def has_timetable_perm(user: User, obj: Model) -> bool: return has_person_timetable_perm(user, obj) elif isinstance(obj, Room): return has_room_timetable_perm(user, obj) + elif isinstance(obj, Course): + return has_course_timetable_perm(user, obj) else: return False @@ -44,6 +49,50 @@ def has_group_timetable_perm(user: User, obj: Group) -> bool: ) +@predicate +def has_substitution_perm_by_group(user: User, obj: LessonEvent) -> bool: + """ + Check if can create/edit substitution based on group. + + Predicate which checks whether the user is allowed + to create/edit the requested substitution. + """ + if not obj: + return False + return ( + obj.groups.filter(pk__in=user.person.owner_of.values_list("id", flat=True)).exists() + or queryset_rules_filter(user, obj.groups.all(), "core.manage_group_substitutions").exists() + ) + + +@predicate +def has_group_substitution_perm(user: User, obj: Group) -> bool: + """ + Check if can access/edit substitutions of given group. + + Predicate which checks whether the user is allowed + to access/edit the substitutions of the given group. + """ + return ( + obj in user.person.owner_of.all() + or has_global_perm("chronos.view_lessonsubstitution")(user) + or has_object_perm("core.manage_group_substitutions")(user, obj) + ) + + +@predicate +def has_any_group_substitution_perm(user: User) -> bool: + """ + Check if can create/edit substitutions of any group. + + Predicate which checks whether the user is allowed + to create/edit any substitutions of any group. + """ + return user.person.owner_of.exists() or has_any_object( + "core.manage_group_substitutions", Group + )(user) + + @predicate def has_person_timetable_perm(user: User, obj: Person) -> bool: """ @@ -72,6 +121,88 @@ def has_room_timetable_perm(user: User, obj: Room) -> bool: )(user, obj) +@predicate +def has_course_timetable_perm(user: User, obj: Course) -> bool: + """ + Check if can access course timetable. + + Predicate which checks whether the user is allowed + to access the requested course timetable. + """ + return ( + user.person in obj.teachers.all() + or obj.groups.all().intersection(user.person.member_of.all()).exists() + or user.person.primary_group in obj.groups.all() + or obj.groups.all().intersection(user.person.owner_of.all()).exists() + or has_global_perm("chronos.view_all_course_timetables")(user) + or has_object_perm("cursus.view_course_timetable")(user, obj) + ) + + +@predicate +def has_supervisions_perm(user: User, obj: Model) -> bool: + """ + Check if can access supervisions of object. + + Predicate which checks whether the user is allowed + to access the requested supervisions of the given + group, person or room. + """ + if isinstance(obj, Group): + return has_group_supervisions_perm(user, obj) + elif isinstance(obj, Person): + return has_person_supervisions_perm(user, obj) + elif isinstance(obj, Room): + return has_room_supervisions_perm(user, obj) + else: + return False + + +@predicate +def has_group_supervisions_perm(user: User, obj: Group) -> bool: + """ + Check if can access group supervisions. + + Predicate which checks whether the user is allowed + to access the requested group supervisions. + """ + return ( + obj in user.person.member_of.all() + or user.person.primary_group == obj + or obj in user.person.owner_of.all() + or has_global_perm("chronos.view_all_group_supervisions")(user) + or has_object_perm("core.view_group_supervisions")(user, obj) + ) + + +@predicate +def has_person_supervisions_perm(user: User, obj: Person) -> bool: + """ + Check if can access person supervisions. + + Predicate which checks whether the user is allowed + to access the requested person supervisions. + """ + return ( + user.person == obj + or has_global_perm("chronos.view_all_person_supervisions")(user) + or has_object_perm("core.view_person_supervisions")(user, obj) + ) + + +@predicate +def has_room_supervisions_perm(user: User, obj: Room) -> bool: + """ + Check if can access room supervisions. + + Predicate which checks whether the user is allowed + to access the requested room supervisions. + """ + return has_global_perm("chronos.view_all_room_supervisions")(user) or has_object_perm( + "core.view_room_supervisions" + )(user, obj) + + @predicate def has_any_timetable_object(user: User) -> bool: """Predicate which checks whether there are any timetables the user is allowed to access.""" diff --git a/pyproject.toml b/pyproject.toml index 923710bea23b988bf914f23afba9c2f6f98c0c3e..5b331df48e3053e2eef4f0e7a6d03905146c463b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "AlekSIS-App-Chronos" -version = "4.0.0.dev9" +version = "4.0.0.dev10" packages = [ { include = "aleksis" } ] @@ -54,6 +54,7 @@ calendarweek = "^0.5.0" aleksis-core = "^4.0.0.dev13" aleksis-app-resint = "^4.0.0.dev1" aleksis-app-cursus = "^0.1.0.dev4" +aleksis-app-kolego = "^0.1.0.dev0" [tool.poetry.plugins."aleksis.app"] diff --git a/tox.ini b/tox.ini index 294e65bc96d4e06262b67e3e6b8a987307b226e4..92f26b55d9ebd6f5b3db425cab8d151bde0c0c69 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] skipsdist = True skip_missing_interpreters = true -envlist = py39,py310,py311 +envlist = py310,py311,py312 [testenv] allowlist_externals =