diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..ab74d43bf595f02267eb612f3a4a14a6682b7f19 Binary files /dev/null and b/.coverage differ diff --git a/aleksis/apps/chronos/frontend/components/AmendLesson.vue b/aleksis/apps/chronos/frontend/components/AmendLesson.vue new file mode 100644 index 0000000000000000000000000000000000000000..587ccdf093bdab1eae7bc197954607fdb56ad18e --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/AmendLesson.vue @@ -0,0 +1,193 @@ +<template> + <v-card-actions v-if="checkPermission('chronos.edit_substitution_rule')"> + <edit-button + i18n-key="chronos.event.amend.edit_button" + @click="edit = true" + /> + <delete-button + v-if="selectedEvent.meta.amended" + i18n-key="chronos.event.amend.delete_button" + @click="deleteEvent = true" + /> + <dialog-object-form + v-model="edit" + :fields="fields" + :is-create="!selectedEvent.meta.amended" + createItemI18nKey="chronos.event.amend.title" + :gql-create-mutation="gqlCreateMutation" + :get-create-data="transformCreateData" + :default-item="defaultItem" + editItemI18nKey="chronos.event.amend.title" + :gql-patch-mutation="gqlPatchMutation" + :get-patch-data="transformPatchData" + :edit-item="initPatchData" + @cancel="open = false" + @save="updateOnSave()" + > + <template #subject.field="{ attrs, on, item }"> + <v-autocomplete + :disabled="item.cancelled" + :items="amendableSubjects" + item-text="name" + item-value="id" + v-bind="attrs" + v-on="on" + /> + </template> + <template #teachers.field="{ attrs, on, item }"> + <v-autocomplete + :disabled="item.cancelled" + multiple + :items="amendableTeachers" + item-text="fullName" + item-value="id" + v-bind="attrs" + v-on="on" + chips + deletable-chips + /> + </template> + <template #rooms.field="{ attrs, on, item }"> + <v-autocomplete + :disabled="item.cancelled" + multiple + :items="amendableRooms" + item-text="name" + item-value="id" + v-bind="attrs" + v-on="on" + chips + deletable-chips + /> + </template> + <template #cancelled.field="{ attrs, on }"> + <v-checkbox v-bind="attrs" v-on="on" /> + </template> + <template #comment.field="{ attrs, on }"> + <v-textarea v-bind="attrs" v-on="on" /> + </template> + </dialog-object-form> + <delete-dialog + deleteSuccessMessageI18nKey="chronos.event.amend.delete_success" + :gql-mutation="gqlDeleteMutation" + v-model="deleteEvent" + :item="selectedEvent.meta" + @success="updateOnSave()" + > + <template #title> + {{ $t("chronos.event.amend.delete_dialog") }} + </template> + </delete-dialog> + </v-card-actions> +</template> + +<script> +import permissionsMixin from "aleksis.core/mixins/permissions.js"; +import EditButton from "aleksis.core/components/generic/buttons/EditButton.vue"; +import DialogObjectForm from "aleksis.core/components/generic/dialogs/DialogObjectForm.vue"; +import DeleteButton from "aleksis.core/components/generic/buttons/DeleteButton.vue"; +import DeleteDialog from "aleksis.core/components/generic/dialogs/DeleteDialog.vue"; +import { + gqlSubjects, + gqlPersons, + gqlRooms, + createAmendLesson, + patchAmendLesson, + deleteAmendLesson, +} from "./amendLesson.graphql"; + +export default { + name: "AmendLesson", + components: { + EditButton, + DialogObjectForm, + DeleteButton, + DeleteDialog, + }, + mixins: [permissionsMixin], + props: { + selectedEvent: { + type: Object, + required: true, + } + }, + data() { + return { + edit: false, + fields: [ + { + text: this.$t("chronos.event.amend.subject"), + value: "subject", + }, + { + text: this.$t("chronos.event.amend.teachers"), + value: "teachers", + }, + { + text: this.$t("chronos.event.amend.rooms"), + value: "rooms", + }, + { + text: this.$t("chronos.event.amend.cancelled"), + value: "cancelled", + }, + { + text: this.$t("chronos.event.amend.comment"), + value: "comment", + }, + ], + defaultItem: { + cancelled: this.selectedEvent.meta.cancelled, + comment: this.selectedEvent.meta.comment, + }, + gqlCreateMutation: createAmendLesson, + gqlPatchMutation: patchAmendLesson, + deleteEvent: false, + gqlDeleteMutation: deleteAmendLesson, + }; + }, + methods: { + transformCreateData(item) { + return { + ...item, + amends: this.selectedEvent.meta.id, + datetimeStart: this.selectedEvent.startDateTime.toUTC().toISO(), + datetimeEnd: this.selectedEvent.endDateTime.toUTC().toISO(), + }; + }, + transformPatchData(item) { + let { id, __typename, cancelled, ...patchItem } = item; + return { + ...patchItem, + // Normalize cancelled, v-checkbox returns null & does not + // honor false-value. + cancelled: cancelled ? true : false, + }; + }, + updateOnSave() { + this.$emit('refreshCalendar'); + this.model = false; + }, + }, + computed: { + initPatchData() { + return { + id: this.selectedEvent.meta.id, + subject: this.selectedEvent.meta.subject?.id.toString(), + teachers: this.selectedEvent.meta.teachers.map((teacher) => teacher.id.toString()), + rooms: this.selectedEvent.meta.rooms.map((room) => room.id.toString()), + cancelled: this.selectedEvent.meta.cancelled, + comment: this.selectedEvent.meta.comment, + }; + }, + }, + apollo: { + amendableSubjects: gqlSubjects, + amendableTeachers: gqlPersons, + amendableRooms: gqlRooms, + }, + mounted() { + this.addPermissions(["chronos.edit_substitution_rule"]); + }, +}; +</script> diff --git a/aleksis/apps/chronos/frontend/components/LessonEventLinkIterator.vue b/aleksis/apps/chronos/frontend/components/LessonEventLinkIterator.vue new file mode 100644 index 0000000000000000000000000000000000000000..a6a737298d496c3a8b7acdcd7fe4d485c77cace2 --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/LessonEventLinkIterator.vue @@ -0,0 +1,25 @@ +<script> +export default { + name: "LessonEventLinkIterator", + props: { + items: { + type: Array, + required: true, + }, + attr: { + type: String, + required: false, + default: "name", + }, + }, +}; +</script> + +<template> + <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 ? "," : "" }} + </span> + </span> +</template> diff --git a/aleksis/apps/chronos/frontend/components/LessonEventOldNew.vue b/aleksis/apps/chronos/frontend/components/LessonEventOldNew.vue new file mode 100644 index 0000000000000000000000000000000000000000..774dfb8aa3ce47a76da45d0dfc43d3b31208e32e --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/LessonEventOldNew.vue @@ -0,0 +1,42 @@ +<script> +import LessonEventLinkIterator from "./LessonEventLinkIterator.vue"; + +export default { + name: "LessonEventOldNew", + components: { LessonEventLinkIterator }, + props: { + oldItems: { + type: Array, + required: true, + }, + newItems: { + type: Array, + required: true, + }, + attr: { + type: String, + required: false, + default: "name", + }, + }, +}; +</script> + +<template> + <span v-bind="$attrs"> + <span v-if="oldItems.length > 0 && newItems.length > 0"> + <span class="text-decoration-line-through" + ><lesson-event-link-iterator :items="oldItems" :attr="attr" + /></span> + <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text --> + <span> → </span> + <lesson-event-link-iterator :items="newItems" :attr="attr" /> + </span> + <span v-else-if="newItems.length > 0"> + <lesson-event-link-iterator :items="newItems" :attr="attr" /> + </span> + <span v-else> + <lesson-event-link-iterator :items="oldItems" :attr="attr" /> + </span> + </span> +</template> diff --git a/aleksis/apps/chronos/frontend/components/LessonEventSubject.vue b/aleksis/apps/chronos/frontend/components/LessonEventSubject.vue new file mode 100644 index 0000000000000000000000000000000000000000..9a739936f497ad6f63b1d2780d244ab3b5b74092 --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/LessonEventSubject.vue @@ -0,0 +1,42 @@ +<script> +export default { + name: "LessonEventSubject", + props: { + event: { + type: Object, + required: true, + }, + attr: { + type: String, + required: false, + default: "name", + }, + }, +}; +</script> + +<template> + <span v-bind="$attrs"> + <span + v-if=" + event.meta.subject && event.meta.amended && event.meta.amends.subject + " + > + <span class="text-decoration-line-through"> + {{ event.meta.amends.subject[attr] }}</span + > + <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text --> + <span> → </span> + <span>{{ event.meta.subject[attr] }}</span> + </span> + <span v-else-if="event.meta.subject"> + {{ event.meta.subject[attr] }} + </span> + <span v-else-if="event.meta.amended && event.meta.amends.subject"> + {{ event.meta.amends.subject[attr] }} + </span> + <span v-else> + {{ event[attr] }} + </span> + </span> +</template> diff --git a/aleksis/apps/chronos/frontend/components/LessonRelatedObjectChip.vue b/aleksis/apps/chronos/frontend/components/LessonRelatedObjectChip.vue new file mode 100644 index 0000000000000000000000000000000000000000..657cfafd6d27c5da2e890581b53bddcf9841b777 --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/LessonRelatedObjectChip.vue @@ -0,0 +1,32 @@ +<script> +export default { + name: "LessonRelatedObjectChip", + props: { + status: { + type: String, + default: "regular", + validator: (value) => ["new", "removed", "regular"].includes(value), + }, + newIcon: { + type: String, + default: "mdi-plus", + }, + }, +}; +</script> + +<template> + <v-chip + label + outlined + :class="{ + 'mr-2': true, + 'text-decoration-line-through text--secondary': status === 'removed', + }" + :color="status === 'new' ? 'warning' : ''" + > + <v-icon left v-if="status === 'new'">{{ newIcon }}</v-icon> + + <slot></slot> + </v-chip> +</template> diff --git a/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue b/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..84cf21c8498e54de13efd2056117fe478212aae1 --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue @@ -0,0 +1,42 @@ +<script> +export default { + name: "NoTimetableCard", + props: { + titleKey: { + type: String, + required: false, + default: "chronos.timetable.no_timetable_selected.title", + }, + descriptionKey: { + type: String, + required: false, + default: "chronos.timetable.no_timetable_selected.description", + }, + }, +}; +</script> + +<template> + <v-card + class="full-height d-flex align-center justify-center py-10" + v-bind="$attrs" + > + <div class="text-center"> + <v-icon color="secondary" size="60" class="mb-4"> mdi-grid-off </v-icon> + <div class="text-h5 grey--text text--darken-2 mb-2"> + {{ $t(titleKey) }} + </div> + <div + class="text-body-2 grey--text text--darken-2" + v-if="$vuetify.breakpoint.lgAndUp" + > + {{ $t(descriptionKey) }} + </div> + <div v-if="$vuetify.breakpoint.mdAndDown"> + <v-btn color="primary" @click="$emit('selectTimetable')" class="mt-4"> + {{ $t("chronos.timetable.select") }} + </v-btn> + </div> + </div> + </v-card> +</template> diff --git a/aleksis/apps/chronos/frontend/components/SelectTimetable.vue b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue new file mode 100644 index 0000000000000000000000000000000000000000..fb98d1f56c51d4261502df5d8ec2889e39e6445c --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue @@ -0,0 +1,118 @@ +<script> +import timetableTypes from "./timetableTypes"; + +export default { + name: "SelectTimetable", + props: { + value: { + type: String, + required: false, + default: null, + }, + availableTimetables: { + type: Array, + required: true, + }, + }, + data() { + return { + selected: null, + selectedFull: null, + search: "", + selectedTypes: ["GROUP", "TEACHER", "ROOM"], + types: timetableTypes, + }; + }, + watch: { + value(val) { + this.selectedFull = val; + this.selected = val.id; + }, + selectedFull(val) { + this.$emit("input", val); + }, + }, + computed: { + availableTimetablesFiltered() { + // Filter timetables by selected types + return this.availableTimetables.filter((timetable) => { + return this.selectedTypes.indexOf(timetable.type) !== -1; + }); + }, + }, +}; +</script> + +<template> + <div> + <v-card-text class="mb-0"> + <!-- Search field for timetables --> + <v-text-field + search + filled + rounded + clearable + autofocus + v-model="search" + :placeholder="$t('chronos.timetable.search')" + prepend-inner-icon="mdi-magnify" + hide-details="auto" + class="mb-2" + /> + + <!-- Filter by timetable types --> + <v-btn-toggle v-model="selectedTypes" dense block multiple class="d-flex"> + <v-btn + v-for="type in types" + :key="type.id" + class="flex-grow-1" + :value="type.id" + > + {{ type.name }} + </v-btn> + </v-btn-toggle> + </v-card-text> + + <!-- Select of available timetables --> + <v-data-iterator + :items="availableTimetablesFiltered" + item-key="id" + :search="search" + single-expand + disable-pagination + > + <template #default="{ items, isExpanded, expand }"> + <v-list class="scrollable-list"> + <v-list-item-group v-model="selected"> + <v-list-item + v-for="item in items" + @click="selectedFull = item" + :value="item.id" + :key="item.id" + > + <v-list-item-icon color="primary"> + <v-icon v-if="item.type in types" color="secondary"> + {{ types[item.type].icon }} + </v-icon> + <v-icon v-else color="secondary">mdi-grid</v-icon> + </v-list-item-icon> + <v-list-item-content> + <v-list-item-title>{{ item.name }}</v-list-item-title> + </v-list-item-content> + <v-list-item-action> + <v-icon>mdi-chevron-right</v-icon> + </v-list-item-action> + </v-list-item> + </v-list-item-group> + </v-list> + </template> + </v-data-iterator> + </div> +</template> + +<style scoped> +.scrollable-list { + height: 100%; + overflow-y: scroll; +} +</style> diff --git a/aleksis/apps/chronos/frontend/components/Timetable.vue b/aleksis/apps/chronos/frontend/components/Timetable.vue new file mode 100644 index 0000000000000000000000000000000000000000..e4fca5dfb63f26597cd534315fed8f32b9b8a2de --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/Timetable.vue @@ -0,0 +1,203 @@ +<script> +import { gqlAvailableTimetables } from "./timetables.graphql"; +import NoTimetableCard from "./NoTimetableCard.vue"; +import SelectTimetable from "./SelectTimetable.vue"; +import timetableTypes from "./timetableTypes"; + +export default { + name: "Timetable", + components: { NoTimetableCard, SelectTimetable }, + apollo: { + availableTimetables: { + query: gqlAvailableTimetables, + result() { + if ( + !this.selected && + this.$route.params.id && + this.$route.params.type + ) { + this.selectTimetable( + this.availableTimetables.find( + (t) => + t.objId === this.$route.params.id && + t.type.toLowerCase() === this.$route.params.type + ) + ); + } + }, + }, + }, + data() { + return { + availableTimetables: [], + selected: null, + search: "", + selectedTypes: ["GROUP", "TEACHER", "ROOM"], + types: timetableTypes, + selectDialog: false, + }; + }, + watch: { + selected(selected) { + // Align navigation with currently selected timetable + if (!selected) { + this.$router.push({ name: "chronos.timetable" }); + } else if ( + selected.objId !== this.$route.params.id || + selected.type.toLowerCase() !== this.$route.params.type + ) { + this.$router.push({ + name: "chronos.timetableWithId", + params: { + type: selected.type.toLowerCase(), + id: selected.objId, + }, + }); + } + }, + }, + methods: { + findNextTimetable(offset = 1) { + const currentIndex = this.availableTimetablesIds.indexOf( + this.selected.id + ); + const newIndex = currentIndex + offset; + if (newIndex < 0 || newIndex >= this.availableTimetablesIds.length) { + return null; + } + return this.availableTimetables[newIndex]; + }, + selectTimetable(timetable) { + this.selected = timetable; + }, + }, + computed: { + selectedTypesFull() { + return this.selectedTypes.map((type) => { + return this.types[type]; + }); + }, + availableTimetablesFiltered() { + // Filter timetables by selected types + return this.availableTimetables.filter((timetable) => { + return this.selectedTypes.indexOf(timetable.type) !== -1; + }); + }, + availableTimetablesIds() { + return this.availableTimetables.map((timetable) => timetable.id); + }, + prevTimetable() { + return this.findNextTimetable(-1); + }, + nextTimetable() { + return this.findNextTimetable(1); + }, + }, +}; +</script> + +<template> + <div> + <v-row> + <v-dialog + v-model="selectDialog" + fullscreen + hide-overlay + transition="dialog-bottom-transition" + > + <v-card> + <v-toolbar dark color="primary"> + <v-toolbar-title>{{ + $t("chronos.timetable.select") + }}</v-toolbar-title> + <v-spacer></v-spacer> + </v-toolbar> + <select-timetable + v-model="selected" + @input="selectDialog = false" + :available-timetables="availableTimetables" + /> + </v-card> + </v-dialog> + + <v-col md="3" lg="3" xl="3" v-if="$vuetify.breakpoint.lgAndUp"> + <v-card> + <select-timetable + v-model="selected" + :available-timetables="availableTimetables" + /> + </v-card> + </v-col> + <v-col sm="12" md="12" lg="9" xl="9" class="full-height"> + <!-- No timetable card--> + <no-timetable-card + v-if="selected == null" + @selectTimetable="selectDialog = true" + /> + + <!-- Calendar card--> + <v-card v-else> + <div class="d-flex flex-column" v-if="$vuetify.breakpoint.smAndDown"> + <v-card-title class="pt-2"> + <v-btn + icon + :disabled="!prevTimetable" + @click="selectTimetable(prevTimetable)" + :title="$t('chronos.timetable.prev')" + class="mr-1" + > + <v-icon>mdi-chevron-left</v-icon> + </v-btn> + <v-spacer /> + <v-chip outlined color="secondary" @click="selectDialog = true"> + {{ selected.name }} + <v-icon right>mdi-chevron-down</v-icon> + </v-chip> + <v-spacer /> + <v-btn + icon + :disabled="!nextTimetable" + @click="selectTimetable(nextTimetable)" + :title="$t('chronos.timetable.next')" + class="ml-1 float-right" + > + <v-icon>mdi-chevron-right</v-icon> + </v-btn> + </v-card-title> + </div> + + <div class="d-flex flex-wrap justify-space-between mb-2" v-else> + <v-card-title> + {{ selected.name }} + </v-card-title> + <div class="pa-2 mt-1"> + <v-btn + icon + :disabled="!prevTimetable" + @click="selectTimetable(prevTimetable)" + :title="$t('chronos.timetable.prev')" + > + <v-icon>mdi-chevron-left</v-icon> + </v-btn> + <v-chip label color="secondary" outlined class="mx-1">{{ + selected.shortName + }}</v-chip> + <v-btn + icon + :disabled="!nextTimetable" + @click="selectTimetable(nextTimetable)" + :title="$t('chronos.timetable.next')" + > + <v-icon>mdi-chevron-right</v-icon> + </v-btn> + </div> + </div> + <calendar-with-controls + :calendar-feeds="[{ name: 'lesson' }, { name: 'supervision' }]" + :params="{ type: selected.type, id: selected.objId }" + /> + </v-card> + </v-col> + </v-row> + </div> +</template> diff --git a/aleksis/apps/chronos/frontend/components/amendLesson.graphql b/aleksis/apps/chronos/frontend/components/amendLesson.graphql new file mode 100644 index 0000000000000000000000000000000000000000..a93e89186df05efdfa77473b7f1a1751eb614c24 --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/amendLesson.graphql @@ -0,0 +1,75 @@ +query gqlSubjects { + amendableSubjects: subjects { + id + name + } +} + +query gqlPersons { + amendableTeachers: persons { + id + fullName + } +} + +query gqlRooms { + amendableRooms: rooms { + id + name + } +} + +mutation createAmendLesson($input: CreateLessonEventInput!) { + createAmendLesson(input: $input) { + lessonEvent { + id + amends { + id + } + datetimeStart + datetimeEnd + subject { + id + } + teachers { + id + } + groups { + id + } + rooms { + id + } + cancelled + comment + } + } +} + +mutation patchAmendLesson($input: PatchLessonEventInput!, $id: ID!) { + patchAmendLesson(input: $input, id: $id) { + lessonEvent { + id + subject { + id + } + teachers { + id + } + groups { + id + } + rooms { + id + } + cancelled + comment + } + } +} + +mutation deleteAmendLesson($id: ID!) { + deleteAmendLesson(id: $id) { + ok + } +} diff --git a/aleksis/apps/chronos/frontend/components/calendar_feeds/details/LessonDetails.vue b/aleksis/apps/chronos/frontend/components/calendar_feeds/details/LessonDetails.vue new file mode 100644 index 0000000000000000000000000000000000000000..0c9311450f0c7d8c3e922743f1403382da1148ce --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/calendar_feeds/details/LessonDetails.vue @@ -0,0 +1,133 @@ +<template> + <base-calendar-feed-details + v-bind="$props" + :color="currentSubject ? currentSubject.colour_bg : null" + without-location + > + <template #title> + <div + :style="{ + color: currentSubject ? currentSubject.colour_fg || 'white' : 'white', + }" + > + <lesson-event-subject :event="selectedEvent" /> + </div> + </template> + <template #badge> + <cancelled-calendar-status-chip + v-if="selectedEvent.meta.cancelled" + class="ml-4" + /> + <calendar-status-chip + color="warning" + icon="mdi-clipboard-alert-outline" + v-else-if="selectedEvent.meta.amended" + class="ml-4" + > + {{ $t("chronos.event.current_changes") }} + </calendar-status-chip> + </template> + <template #description> + <v-divider inset /> + <v-list-item v-if="selectedEvent.meta.groups.length > 0"> + <v-list-item-icon> + <v-icon color="primary">mdi-account-group-outline</v-icon> + </v-list-item-icon> + <v-list-item-content> + <v-list-item-title> + <lesson-related-object-chip + v-for="group in selectedEvent.meta.groups" + :key="group.id" + >{{ group.name }}</lesson-related-object-chip + > + </v-list-item-title> + </v-list-item-content> + </v-list-item> + <v-list-item> + <v-list-item-icon> + <v-icon color="primary">mdi-human-male-board </v-icon> + </v-list-item-icon> + <v-list-item-content> + <v-list-item-title> + <span v-if="teachers.length === 0" class="body-2 text--secondary">{{ + $t("chronos.event.no_teacher") + }}</span> + <lesson-related-object-chip + v-for="teacher in teachers" + :status="teacher.status" + :key="teacher.id" + new-icon="mdi-account-plus-outline" + >{{ teacher.full_name }}</lesson-related-object-chip + > + </v-list-item-title> + </v-list-item-content> + </v-list-item> + <v-list-item> + <v-list-item-icon> + <v-icon color="primary">mdi-door </v-icon> + </v-list-item-icon> + <v-list-item-content> + <v-list-item-title> + <span v-if="rooms.length === 0" class="body-2 text--secondary">{{ + $t("chronos.event.no_room") + }}</span> + <lesson-related-object-chip + v-for="room in rooms" + :status="room.status" + :key="room.id" + new-icon="mdi-door-open" + >{{ room.name }}</lesson-related-object-chip + > + </v-list-item-title> + </v-list-item-content> + </v-list-item> + <v-divider inset /> + <v-list-item v-if="selectedEvent.meta.comment"> + <v-list-item-content> + <v-list-item-title> + <v-alert + dense + outlined + type="warning" + icon="mdi-information-outline" + > + {{ selectedEvent.meta.comment }} + </v-alert> + </v-list-item-title> + </v-list-item-content> + </v-list-item> + <amend-lesson + v-if="selectedEvent" + :selected-event="selectedEvent" + @refreshCalendar="$emit('refreshCalendar')" + /> + </template> + </base-calendar-feed-details> +</template> + +<script> +import calendarFeedDetailsMixin from "aleksis.core/mixins/calendarFeedDetails.js"; +import BaseCalendarFeedDetails from "aleksis.core/components/calendar/BaseCalendarFeedDetails.vue"; +import CalendarStatusChip from "aleksis.core/components/calendar/CalendarStatusChip.vue"; +import CancelledCalendarStatusChip from "aleksis.core/components/calendar/CancelledCalendarStatusChip.vue"; + +import LessonRelatedObjectChip from "../../LessonRelatedObjectChip.vue"; + +import lessonEvent from "../mixins/lessonEvent"; +import LessonEventSubject from "../../LessonEventSubject.vue"; + +import AmendLesson from "../../AmendLesson.vue"; + +export default { + name: "LessonDetails", + components: { + LessonEventSubject, + LessonRelatedObjectChip, + BaseCalendarFeedDetails, + CalendarStatusChip, + CancelledCalendarStatusChip, + AmendLesson, + }, + mixins: [calendarFeedDetailsMixin, lessonEvent], +}; +</script> diff --git a/aleksis/apps/chronos/frontend/components/calendar_feeds/details/SupervisionDetails.vue b/aleksis/apps/chronos/frontend/components/calendar_feeds/details/SupervisionDetails.vue new file mode 100644 index 0000000000000000000000000000000000000000..7d6455a054fd8d7a46433ea5ad4693e4c40ff43e --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/calendar_feeds/details/SupervisionDetails.vue @@ -0,0 +1,109 @@ +<template> + <base-calendar-feed-details + v-bind="$props" + :color="currentSubject ? currentSubject.colour_bg : null" + without-location + > + <template #title> + <div + :style="{ + color: currentSubject ? currentSubject.colour_fg || 'white' : 'white', + }" + > + <lesson-event-subject :event="selectedEvent" /> + </div> + </template> + <template #badge> + <cancelled-calendar-status-chip + v-if="selectedEvent.meta.cancelled" + class="ml-4" + /> + <calendar-status-chip + color="warning" + icon="mdi-clipboard-alert-outline" + v-else-if="selectedEvent.meta.amended" + class="ml-4" + > + {{ $t("chronos.event.current_changes") }} + </calendar-status-chip> + </template> + <template #description> + <v-divider inset /> + <v-list-item> + <v-list-item-icon> + <v-icon color="primary">mdi-human-male-board </v-icon> + </v-list-item-icon> + <v-list-item-content> + <v-list-item-title> + <span v-if="teachers.length === 0" class="body-2 text--secondary">{{ + $t("chronos.event.no_teacher") + }}</span> + <lesson-related-object-chip + v-for="teacher in teachers" + :status="teacher.status" + :key="teacher.id" + new-icon="mdi-account-plus-outline" + >{{ teacher.full_name }}</lesson-related-object-chip + > + </v-list-item-title> + </v-list-item-content> + </v-list-item> + <v-list-item> + <v-list-item-icon> + <v-icon color="primary">mdi-door </v-icon> + </v-list-item-icon> + <v-list-item-content> + <v-list-item-title> + <span v-if="rooms.length === 0" class="body-2 text--secondary">{{ + $t("chronos.event.no_room") + }}</span> + <lesson-related-object-chip + v-for="room in rooms" + :status="room.status" + :key="room.id" + new-icon="mdi-door-open" + >{{ room.name }}</lesson-related-object-chip + > + </v-list-item-title> + </v-list-item-content> + </v-list-item> + <v-divider inset /> + <v-list-item v-if="selectedEvent.meta.comment"> + <v-list-item-content> + <v-list-item-title> + <v-alert + dense + outlined + type="warning" + icon="mdi-information-outline" + > + {{ selectedEvent.meta.comment }} + </v-alert> + </v-list-item-title> + </v-list-item-content> + </v-list-item> + </template> + </base-calendar-feed-details> +</template> + +<script> +import calendarFeedDetailsMixin from "aleksis.core/mixins/calendarFeedDetails.js"; +import BaseCalendarFeedDetails from "aleksis.core/components/calendar/BaseCalendarFeedDetails.vue"; +import CalendarStatusChip from "aleksis.core/components/calendar/CalendarStatusChip.vue"; +import CancelledCalendarStatusChip from "aleksis.core/components/calendar/CancelledCalendarStatusChip.vue"; + +import LessonRelatedObjectChip from "../../LessonRelatedObjectChip.vue"; +import lessonEvent from "../mixins/lessonEvent"; +import LessonEventSubject from "../../LessonEventSubject.vue"; +export default { + name: "LessonDetails", + components: { + LessonEventSubject, + LessonRelatedObjectChip, + BaseCalendarFeedDetails, + CalendarStatusChip, + CancelledCalendarStatusChip, + }, + mixins: [calendarFeedDetailsMixin, lessonEvent], +}; +</script> diff --git a/aleksis/apps/chronos/frontend/components/calendar_feeds/event_bar/LessonEventBar.vue b/aleksis/apps/chronos/frontend/components/calendar_feeds/event_bar/LessonEventBar.vue new file mode 100644 index 0000000000000000000000000000000000000000..1f0eb6527d0859ead2edb1cf120ec5ef84b5c6c3 --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/calendar_feeds/event_bar/LessonEventBar.vue @@ -0,0 +1,103 @@ +<template> + <base-calendar-feed-event-bar + :with-padding="false" + :without-time="true" + v-bind="$props" + > + <template #icon> </template> + + <template #title> + <div + class="d-flex justify-start" + :class="{ + 'px-1': true, + 'orange-border': + selectedEvent.meta.amended && !selectedEvent.meta.cancelled, + 'red-border': selectedEvent.meta.cancelled, + }" + :style="{ + color: currentSubject ? currentSubject.colour_fg || 'white' : 'white', + height: '100%', + borderRadius: '4px', + }" + > + <span + v-if="calendarType === 'month' && eventParsed.start.hasTime" + class="mr-1 font-weight-bold ml-1" + > + {{ eventParsed.start.time }} + </span> + <div + class="d-flex justify-center align-center flex-grow-1 text-truncate" + > + <div class="d-flex justify-center align-center flex-wrap text"> + <lesson-event-link-iterator + v-if="!selectedEvent.meta.is_member" + :items="selectedEvent.meta.groups" + attr="short_name" + class="mr-1" + /> + <lesson-event-old-new + v-if="!selectedEvent.meta.is_teacher || newTeachers.length > 0" + :new-items="newTeachers" + :old-items="oldTeachers" + attr="short_name" + class="mr-1" + /> + + <lesson-event-subject + :event="selectedEvent" + attr="short_name" + class="font-weight-medium mr-1" + /> + <lesson-event-old-new + :new-items="newRooms" + :old-items="oldRooms" + attr="short_name" + /> + </div> + </div> + </div> + </template> + </base-calendar-feed-event-bar> +</template> + +<script> +import calendarFeedEventBarMixin from "aleksis.core/mixins/calendarFeedEventBar.js"; +import BaseCalendarFeedEventBar from "aleksis.core/components/calendar/BaseCalendarFeedEventBar.vue"; +import lessonEvent from "../mixins/lessonEvent"; +import LessonEventSubject from "../../LessonEventSubject.vue"; +import LessonEventLinkIterator from "../../LessonEventLinkIterator.vue"; +import LessonEventOldNew from "../../LessonEventOldNew.vue"; + +export default { + name: "LessonEventBar", + components: { + LessonEventOldNew, + LessonEventLinkIterator, + LessonEventSubject, + BaseCalendarFeedEventBar, + }, + computed: { + selectedEvent() { + return this.event; + }, + }, + mixins: [calendarFeedEventBarMixin, lessonEvent], +}; +</script> + +<style scoped> +.orange-border { + border: 3px orange solid; +} + +.red-border { + border: 3px red solid; +} + +.text { + line-height: 1.1; + font-size: 12px; +} +</style> diff --git a/aleksis/apps/chronos/frontend/components/calendar_feeds/mixins/lessonEvent.js b/aleksis/apps/chronos/frontend/components/calendar_feeds/mixins/lessonEvent.js new file mode 100644 index 0000000000000000000000000000000000000000..64d2b60a12ca67c5b586f9ed9b9e029ad00948ec --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/calendar_feeds/mixins/lessonEvent.js @@ -0,0 +1,78 @@ +/** + * Mixin with common used API for custom lesson event calendar components + */ +const lessonEvent = { + methods: { + addStatuses(attr) { + let oldItems = this.getOldItems(attr); + let newItems = this.getNewItems(attr); + let oldIds = oldItems.map((item) => item.id); + let newIds = newItems.map((item) => item.id); + let itemsWithStatus = oldItems.concat(newItems).map((item) => { + let status = "regular"; + if (newIds.includes(item.id) && !oldIds.includes(item.id)) { + status = "new"; + } else if ( + newIds.length > 0 && + !newIds.includes(item.id) && + oldIds.includes(item.id) + ) { + status = "removed"; + } + return { ...item, status: status }; + }); + return itemsWithStatus; + }, + getOldItems(attr) { + let oldItems = []; + if (this.selectedEvent.meta.amended) { + oldItems = this.selectedEvent.meta.amends[attr]; + } else { + oldItems = this.selectedEvent.meta[attr]; + } + return oldItems; + }, + getNewItems(attr) { + let newItems = []; + if (this.selectedEvent.meta.amended) { + newItems = this.selectedEvent.meta[attr]; + } + return newItems; + }, + }, + computed: { + teachers() { + return this.addStatuses("teachers"); + }, + oldTeachers() { + return this.getOldItems("teachers"); + }, + newTeachers() { + return this.getNewItems("teachers"); + }, + + rooms() { + return this.addStatuses("rooms"); + }, + oldRooms() { + return this.getOldItems("rooms"); + }, + newRooms() { + return this.getNewItems("rooms"); + }, + currentSubject() { + if (this.selectedEvent.meta.subject) { + return this.selectedEvent.meta.subject; + } else if ( + this.selectedEvent.meta.amended && + this.selectedEvent.meta.amends.subject + ) { + return this.selectedEvent.meta.amends.subject; + } else { + return null; + } + }, + }, +}; + +export default lessonEvent; diff --git a/aleksis/apps/chronos/frontend/components/timetableTypes.js b/aleksis/apps/chronos/frontend/components/timetableTypes.js new file mode 100644 index 0000000000000000000000000000000000000000..692854f18411cc2f3ed1abf35069d28c9b18e51c --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/timetableTypes.js @@ -0,0 +1,13 @@ +export default { + GROUP: { + name: "Groups", + id: "GROUP", + icon: "mdi-account-group-outline", + }, + TEACHER: { + name: "Teachers", + id: "TEACHER", + icon: "mdi-account-outline", + }, + ROOM: { name: "Rooms", id: "ROOM", icon: "mdi-door" }, +}; diff --git a/aleksis/apps/chronos/frontend/components/timetables.graphql b/aleksis/apps/chronos/frontend/components/timetables.graphql new file mode 100644 index 0000000000000000000000000000000000000000..5bbe46f734f1981ad1c2648d50a8a6e8d7bd04fc --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/timetables.graphql @@ -0,0 +1,9 @@ +query gqlAvailableTimetables { + availableTimetables { + id + objId + type + name + shortName + } +} diff --git a/aleksis/apps/chronos/frontend/index.js b/aleksis/apps/chronos/frontend/index.js index 501071992b10bfad99480b80d2810628d19b48aa..30451f93c2f0dfcb769a4ab92385b50fe1bfe61c 100644 --- a/aleksis/apps/chronos/frontend/index.js +++ b/aleksis/apps/chronos/frontend/index.js @@ -1,4 +1,5 @@ import { hasPersonValidator } from "aleksis.core/routeValidators"; +import Timetable from "./components/Timetable.vue"; export default { meta: { @@ -12,185 +13,22 @@ export default { }, children: [ { - path: "", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.allTimetables", + path: "timetable/", + component: Timetable, + name: "chronos.timetable", meta: { inMenu: true, - titleKey: "chronos.timetable.menu_title_all", + titleKey: "chronos.timetable.menu_title", icon: "mdi-grid", permission: "chronos.view_timetable_overview_rule", }, - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "timetable/my/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.myTimetable", - meta: { - inMenu: true, - titleKey: "chronos.timetable.menu_title_my", - icon: "mdi-account-outline", - permission: "chronos.view_my_timetable_rule", - }, - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "timetable/my/:year/:month/:day/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.myTimetableByDate", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "timetable/:type_/:pk/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.timetable", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "timetable/:type_/:pk/:year/:week/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.timetableByWeek", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "timetable/:type_/:pk/print/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.timetablePrint", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "timetable/:type_/:pk/:regular/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.timetableRegular", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "lessons/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.lessonsDay", - meta: { - inMenu: true, - titleKey: "chronos.lessons.menu_title_daily", - icon: "mdi-calendar-outline", - permission: "chronos.view_lessons_day_rule", - }, - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "lessons/:year/:month/:day/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.lessonsDayByDate", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, }, { - path: "lessons/:id_/:week/substitution/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.editSubstitution", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "lessons/:id_/:week/substitution/delete/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.deleteSubstitution", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "substitutions/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.substitutions", + path: "timetable/:type/:id/", + component: Timetable, + name: "chronos.timetableWithId", meta: { - inMenu: true, - titleKey: "chronos.substitutions.menu_title", - icon: "mdi-update", - permission: "chronos.view_substitutions_rule", - }, - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "substitutions/print/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.substitutionsPrint", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "substitutions/:year/:month/:day/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.substitutionsByDate", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "substitutions/:year/:month/:day/print/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.substitutionsPrintByDate", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "supervisions/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.supervisionsDay", - meta: { - inMenu: true, - titleKey: "chronos.supervisions.menu_title_daily", - icon: "mdi-calendar-outline", - permission: "chronos.view_supervisions_day_rule", - }, - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "supervisions/:year/:month/:day/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.supervisionsDayByDate", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "supervisions/:id_/:week/substitution/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.editSupervisionSubstitution", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - }, - { - path: "supervisions/:id_/:week/substitution/delete/", - component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), - name: "chronos.deleteSupervisionSubstitution", - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, + permission: "chronos.view_timetable_overview_rule", }, }, ], diff --git a/aleksis/apps/chronos/frontend/messages/de.json b/aleksis/apps/chronos/frontend/messages/de.json index eb3592afd13a36cc3b8f9cf2e30e6ef032b60099..512498ccfae594033f7634e09bfba992233612fe 100644 --- a/aleksis/apps/chronos/frontend/messages/de.json +++ b/aleksis/apps/chronos/frontend/messages/de.json @@ -2,8 +2,17 @@ "chronos": { "menu_title": "Stundenpläne", "timetable": { + "menu_title": "Stundenpläne", "menu_title_all": "Alle Stundenpläne", - "menu_title_my": "Mein Stundenplan" + "menu_title_my": "Mein Stundenplan", + "no_timetable_selected": { + "title": "Kein Stundenplan ausgewählt", + "description": "Wählen Sie auf der linken Seite einen Stundenplan aus, um ihn hier anzuzeigen" + }, + "search": "Stundenpläne suchen", + "prev": "Vorheriger Stundenplan", + "next": "Nächster Stundenplan", + "select": "Stundenplan auswählen" }, "lessons": { "menu_title_daily": "Tagesstunden" @@ -12,7 +21,12 @@ "menu_title": "Vertretungen" }, "supervisions": { - "menu_title_daily": "Tagesvertretungen" + "menu_title_daily": "Aufsichten" + }, + "event": { + "no_teacher": "Keine Lehrkraft", + "no_room": "Kein Raum", + "current_changes": "Aktuelle Änderungen" } } } diff --git a/aleksis/apps/chronos/frontend/messages/en.json b/aleksis/apps/chronos/frontend/messages/en.json index d824f79087540ca0b92d4efe18e3755d562c370f..b995b749a1677cdfbdbfef9888f4bd427062717c 100644 --- a/aleksis/apps/chronos/frontend/messages/en.json +++ b/aleksis/apps/chronos/frontend/messages/en.json @@ -2,8 +2,17 @@ "chronos": { "menu_title": "Timetables", "timetable": { + "menu_title": "Timetables", "menu_title_all": "All timetables", - "menu_title_my": "My timetable" + "menu_title_my": "My timetable", + "no_timetable_selected": { + "title": "No Timetable Selected", + "description": "Select a timetable on the left side to show it in this place" + }, + "search": "Search Timetables", + "prev": "Previous Timetable", + "next": "Next Timetable", + "select": "Select Timetable" }, "lessons": { "menu_title_daily": "Daily lessons" @@ -13,6 +22,23 @@ }, "supervisions": { "menu_title_daily": "Daily supervisions" + }, + "event": { + "no_teacher": "No teacher", + "no_room": "No room", + "current_changes": "Current changes", + "amend": { + "edit_button": "Change", + "delete_button": "Reset", + "delete_dialog": " Are you sure you want to delete this substitution?", + "delete_success": "The substitution was deleted successfully.", + "title": "Change lesson", + "subject": "Subject", + "teachers": "Teachers", + "rooms": "Rooms", + "cancelled": "Cancelled", + "comment": "Comment" + } } } } diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py index 486409f95052b51431fcaf74121690dafcb31d7e..79bd043f4b28300ca5d6df961d43e820ae92a40b 100644 --- a/aleksis/apps/chronos/managers.py +++ b/aleksis/apps/chronos/managers.py @@ -9,10 +9,11 @@ from django.db.models.fields import DateField from django.db.models.functions import Concat from calendarweek import CalendarWeek +from polymorphic.managers import PolymorphicQuerySet from aleksis.apps.chronos.util.date import week_weekday_from_date, week_weekday_to_date from aleksis.core.managers import DateRangeQuerySetMixin, SchoolTermRelatedQuerySet -from aleksis.core.models import Group, Person +from aleksis.core.models import Group, Person, Room from aleksis.core.util.core_helpers import get_site_preferences @@ -799,16 +800,20 @@ class ExtraLessonQuerySet(TimetableQuerySet, SchoolTermRelatedQuerySet, GroupByP class GroupPropertiesMixin: """Mixin for common group properties. - Needed field: `groups` + Necessary method: `get_groups` """ @property def group_names(self, sep: Optional[str] = ", ") -> str: - return sep.join([group.short_name for group in self.groups.all()]) + return sep.join([group.short_name for group in self.get_groups()]) + + @property + def group_short_names(self, sep: Optional[str] = ", ") -> str: + return sep.join([group.short_name for group in self.get_groups()]) @property def groups_to_show(self) -> models.QuerySet: - groups = self.groups.all() + groups = self.get_groups() if ( groups.count() == 1 and groups[0].parent_groups.all() @@ -822,17 +827,68 @@ class GroupPropertiesMixin: def groups_to_show_names(self, sep: Optional[str] = ", ") -> str: return sep.join([group.short_name for group in self.groups_to_show]) + @property + def groups_to_show_short_names(self, sep: Optional[str] = ", ") -> str: + return sep.join([group.short_name for group in self.groups_to_show]) + class TeacherPropertiesMixin: """Mixin for common teacher properties. - Needed field: `teacher` + Necessary method: `get_teachers` """ @property def teacher_names(self, sep: Optional[str] = ", ") -> str: - return sep.join([teacher.full_name for teacher in self.get_teachers().all()]) + return sep.join([teacher.full_name for teacher in self.get_teachers()]) @property def teacher_short_names(self, sep: str = ", ") -> str: - return sep.join([teacher.short_name for teacher in self.get_teachers().all()]) + return sep.join([teacher.short_name for teacher in self.get_teachers()]) + + +class RoomPropertiesMixin: + """Mixin for common room properties. + + Necessary method: `get_rooms` + """ + + @property + def room_names(self, sep: Optional[str] = ", ") -> str: + return sep.join([room.name for room in self.get_rooms()]) + + @property + def room_short_names(self, sep: str = ", ") -> str: + return sep.join([room.short_name for room in self.get_rooms()]) + + +class LessonEventQuerySet(PolymorphicQuerySet): + """Queryset with special query methods for lesson events.""" + + def for_teacher(self, teacher: Union[int, Person]): + amended = self.filter(Q(amended_by__isnull=False) & (Q(teachers=teacher))).values_list( + "amended_by__pk", flat=True + ) + return self.filter(Q(teachers=teacher) | Q(pk__in=amended)).distinct() + + def for_group(self, group: Union[int, Group]): + amended = self.filter( + Q(amended_by__isnull=False) & (Q(groups=group) | Q(groups__parent_groups=group)) + ).values_list("amended_by__pk", flat=True) + return self.filter( + Q(groups=group) | Q(groups__parent_groups=group) | Q(pk__in=amended) + ).distinct() + + def for_room(self, room: Union[int, Room]): + amended = self.filter(Q(amended_by__isnull=False) & (Q(rooms=room))).values_list( + "amended_by__pk", flat=True + ) + return self.filter(Q(rooms=room) | Q(pk__in=amended)).distinct() + + def for_person(self, person: Union[int, Person]): + amended = self.filter( + Q(amended_by__isnull=False) & (Q(teachers=person) | Q(groups__members=person)) + ).values_list("amended_by__pk", flat=True) + return self.filter( + Q(teachers=person) | Q(groups__members=person) | Q(pk__in=amended) + ).distinct() diff --git a/aleksis/apps/chronos/menus.py b/aleksis/apps/chronos/menus.py deleted file mode 100644 index 810e88c6d75a4cefb95210234fb70d690bb32ea1..0000000000000000000000000000000000000000 --- a/aleksis/apps/chronos/menus.py +++ /dev/null @@ -1,73 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -MENUS = { - "NAV_MENU_CORE": [ - { - "name": _("Timetables"), - "url": "#", - "svg_icon": "mdi:school-outline", - "root": True, - "validators": [ - "menu_generator.validators.is_authenticated", - "aleksis.core.util.core_helpers.has_person", - ], - "submenu": [ - { - "name": _("My timetable"), - "url": "my_timetable", - "svg_icon": "mdi:account-outline", - "validators": [ - ( - "aleksis.core.util.predicates.permission_validator", - "chronos.view_my_timetable_rule", - ), - ], - }, - { - "name": _("All timetables"), - "url": "all_timetables", - "svg_icon": "mdi:grid", - "validators": [ - ( - "aleksis.core.util.predicates.permission_validator", - "chronos.view_timetable_overview_rule", - ), - ], - }, - { - "name": _("Daily lessons"), - "url": "lessons_day", - "svg_icon": "mdi:calendar-outline", - "validators": [ - ( - "aleksis.core.util.predicates.permission_validator", - "chronos.view_lessons_day_rule", - ), - ], - }, - { - "name": _("Daily supervisions"), - "url": "supervisions_day", - "svg_icon": "mdi:calendar-outline", - "validators": [ - ( - "aleksis.core.util.predicates.permission_validator", - "chronos.view_supervisions_day_rule", - ), - ], - }, - { - "name": _("Substitutions"), - "url": "substitutions", - "svg_icon": "mdi:update", - "validators": [ - ( - "aleksis.core.util.predicates.permission_validator", - "chronos.view_substitutions_rule", - ), - ], - }, - ], - } - ] -} diff --git a/aleksis/apps/chronos/migrations/0001_initial.py b/aleksis/apps/chronos/migrations/0001_initial.py index 883b25bbf97f4bb0660e787d0a787b754b5efec4..023ea06eb613a00e8b24b0f90046d92ae906a126 100644 --- a/aleksis/apps/chronos/migrations/0001_initial.py +++ b/aleksis/apps/chronos/migrations/0001_initial.py @@ -1,13 +1,14 @@ # Generated by Django 3.0.5 on 2020-05-04 14:16 import django.contrib.postgres.fields.jsonb -import django.contrib.sites.managers import django.db.models.deletion from django.db import migrations, models import calendarweek.calendarweek import colorfield.fields +import aleksis.core.managers + import aleksis.apps.chronos.managers @@ -49,7 +50,7 @@ class Migration(migrations.Migration): ), "managed": False, }, - managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),], + managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),], ), migrations.CreateModel( name="Break", @@ -80,7 +81,7 @@ class Migration(migrations.Migration): "verbose_name_plural": "Breaks", "ordering": ["after_period"], }, - managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),], + managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),], ), migrations.CreateModel( name="Lesson", @@ -119,7 +120,7 @@ class Migration(migrations.Migration): aleksis.apps.chronos.managers.GroupPropertiesMixin, aleksis.apps.chronos.managers.TeacherPropertiesMixin, ), - managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),], + managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),], ), migrations.CreateModel( name="LessonPeriod", @@ -251,7 +252,7 @@ class Migration(migrations.Migration): "verbose_name_plural": "Time periods", "ordering": ["weekday", "period"], }, - managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),], + managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),], ), migrations.CreateModel( name="SupervisionSubstitution", @@ -353,7 +354,7 @@ class Migration(migrations.Migration): "verbose_name_plural": "Supervision areas", "ordering": ["name"], }, - managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),], + managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),], ), migrations.AddField( model_name="supervision", @@ -458,7 +459,7 @@ class Migration(migrations.Migration): "verbose_name_plural": "Subjects", "ordering": ["name", "short_name"], }, - managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),], + managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),], ), migrations.CreateModel( name="Room", @@ -500,7 +501,7 @@ class Migration(migrations.Migration): "verbose_name_plural": "Rooms", "ordering": ["name", "short_name"], }, - managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),], + managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),], ), migrations.CreateModel( name="LessonSubstitution", @@ -871,7 +872,7 @@ class Migration(migrations.Migration): "verbose_name_plural": "Exams", "ordering": ["date"], }, - managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),], + managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),], ), migrations.CreateModel( name="Event", @@ -1032,7 +1033,7 @@ class Migration(migrations.Migration): "verbose_name": "Absence reason", "verbose_name_plural": "Absence reasons", }, - managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),], + managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),], ), migrations.CreateModel( name="Absence", diff --git a/aleksis/apps/chronos/migrations/0014_lessonevent.py b/aleksis/apps/chronos/migrations/0014_lessonevent.py new file mode 100644 index 0000000000000000000000000000000000000000..7b0e0cbac8478b850d847d067591b6885d606d2a --- /dev/null +++ b/aleksis/apps/chronos/migrations/0014_lessonevent.py @@ -0,0 +1,114 @@ +# Generated by Django 4.1.8 on 2023-06-29 14:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("sites", "0002_alter_domain_unique"), + ("core", "0051_calendarevent_and_holiday"), + ("cursus", "0001_initial"), + ("chronos", "0013_move_room_to_core"), + ] + + operations = [ + migrations.CreateModel( + name="LessonEvent", + fields=[ + ( + "calendarevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="core.calendarevent", + ), + ), + ( + "title", + models.CharField(blank=True, max_length=255, verbose_name="Name"), + ), + ( + "cancelled", + models.BooleanField(default=False, verbose_name="Cancelled"), + ), + ("comment", models.TextField(blank=True, verbose_name="Comment")), + ( + "course", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="cursus.course", + verbose_name="Course", + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + related_name="lesson_events", + to="core.group", + verbose_name="Groups", + ), + ), + ( + "rooms", + models.ManyToManyField( + blank=True, + related_name="lesson_events", + to="core.room", + verbose_name="Rooms", + ), + ), + ( + "subject", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="lesson_events", + to="cursus.subject", + verbose_name="Subject", + ), + ), + ( + "teachers", + models.ManyToManyField( + blank=True, + related_name="lesson_events_as_teacher", + to="core.person", + verbose_name="Teachers", + ), + ), + ], + options={ + "verbose_name": "Lesson Event", + "verbose_name_plural": "Lesson Events", + }, + bases=("core.calendarevent",), + ), + migrations.CreateModel( + name="SupervisionEvent", + fields=[ + ( + "lessonevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="chronos.lessonevent", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("chronos.lessonevent",), + ), + ] diff --git a/aleksis/apps/chronos/migrations/0015_add_managed_by_app_label.py b/aleksis/apps/chronos/migrations/0015_add_managed_by_app_label.py new file mode 100644 index 0000000000000000000000000000000000000000..d4906fcd7eefb3fffd36136c75fa8d4feee76cc8 --- /dev/null +++ b/aleksis/apps/chronos/migrations/0015_add_managed_by_app_label.py @@ -0,0 +1,96 @@ +# Generated by Django 4.2.3 on 2023-07-27 13:35 + +import aleksis.core.managers +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ('chronos', '0014_lessonevent'), + ] + + operations = [ + migrations.AddField( + model_name='absence', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='absencereason', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='break', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='event', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='exam', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='extralesson', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='holiday', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='lesson', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='lessonperiod', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='lessonsubstitution', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='subject', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='supervision', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='supervisionarea', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='supervisionsubstitution', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='timeperiod', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + migrations.AddField( + model_name='validityrange', + name='managed_by_app_label', + field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'), + ), + ] diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index ed6905004d257c6e89bcf22ef0b87cd1a1a248e5..11ba2702e648d0eff7e6eb5c29a3748a8f8329c5 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -1,7 +1,7 @@ # flake8: noqa: DJ01 - from __future__ import annotations +import itertools import os from datetime import date, datetime, time, timedelta from itertools import chain @@ -42,6 +42,7 @@ from aleksis.apps.chronos.managers import ( ExtraLessonQuerySet, GroupPropertiesMixin, HolidayQuerySet, + LessonEventQuerySet, LessonPeriodManager, LessonPeriodQuerySet, LessonSubstitutionManager, @@ -60,14 +61,16 @@ from aleksis.apps.chronos.mixins import ( from aleksis.apps.chronos.util.change_tracker import _get_substitution_models, substitutions_changed from aleksis.apps.chronos.util.date import get_current_year from aleksis.apps.chronos.util.format import format_m2m +from aleksis.apps.cursus import models as cursus_models +from aleksis.apps.cursus.models import Course from aleksis.apps.resint.models import LiveDocument -from aleksis.core.managers import CurrentSiteManagerWithoutMigrations +from aleksis.core.managers import CurrentSiteManagerWithoutMigrations, PolymorphicCurrentSiteManager from aleksis.core.mixins import ( ExtensibleModel, GlobalPermissionModel, SchoolTermRelatedExtensibleModel, ) -from aleksis.core.models import DashboardWidget, Group, Room, SchoolTerm +from aleksis.core.models import CalendarEvent, DashboardWidget, Group, Person, Room, SchoolTerm from aleksis.core.util.core_helpers import has_person from aleksis.core.util.pdf import generate_pdf_from_template @@ -1362,3 +1365,274 @@ class ChronosGlobalPermissions(GlobalPermissionModel): ("view_lessons_day", _("Can view all lessons per day")), ("view_supervisions_day", _("Can view all supervisions per day")), ) + + +class LessonEvent(CalendarEvent): + name = "lesson" + verbose_name = _("Lessons") + + objects = PolymorphicCurrentSiteManager.from_queryset(LessonEventQuerySet)() + + title = models.CharField(verbose_name=_("Name"), max_length=255, blank=True) + + course = models.ForeignKey( + Course, on_delete=models.CASCADE, verbose_name=_("Course"), null=True, blank=True + ) + + groups = models.ManyToManyField( + Group, + related_name="lesson_events", + verbose_name=_("Groups"), + blank=True, + ) + + rooms = models.ManyToManyField( + Room, + verbose_name=_("Rooms"), + related_name="lesson_events", + blank=True, + ) + teachers = models.ManyToManyField( + Person, + verbose_name=_("Teachers"), + related_name="lesson_events_as_teacher", + blank=True, + ) + subject = models.ForeignKey( + cursus_models.Subject, + on_delete=models.CASCADE, + verbose_name=_("Subject"), + related_name="lesson_events", + blank=True, + null=True, + ) + + cancelled = models.BooleanField( + default=False, + verbose_name=_("Cancelled"), + ) + + comment = models.TextField( + verbose_name=_("Comment"), + blank=True, + ) + + @property + def actual_groups(self: "LessonEvent"): + return self.groups.all() if self.amends else self.real_amends.groups.all() + + @property + def all_members(self: "LessonEvent") -> list[Person]: + return list(itertools.chain(*[list(g.members.all()) for g in self.actual_groups])) + + @property + def all_teachers(self: "LessonEvent") -> list[Person]: + all_teachers = list(self.teachers.all()) + if self.amends: + all_teachers += list(self.real_amends.teachers.all()) + return all_teachers + + @property + def group_names(self: "LessonEvent") -> str: + return ", ".join([g.name for g in self.actual_groups]) + + @property + def teacher_names(self: "LessonEvent") -> str: + return ", ".join([t.full_name for t in self.teachers.all()]) + + @property + def room_names(self: "LessonEvent") -> str: + return ", ".join([r.name for r in self.rooms.all()]) + + @property + def room_names_with_amends(self: "LessonEvent") -> str: + my_room_names = self.room_names + amended_room_names = self.real_amends.room_names if self.amends else "" + + if my_room_names and amended_room_names: + return _("{} (instead of {})").format(my_room_names, amended_room_names) + elif not my_room_names and amended_room_names: + return amended_room_names + return my_room_names + + @property + def teacher_names_with_amends(self: "LessonEvent") -> str: + my_teacher_names = self.teacher_names + amended_teacher_names = self.real_amends.teacher_names if self.amends else "" + + if my_teacher_names and amended_teacher_names: + return _("{} (instead of {})").format(my_teacher_names, amended_teacher_names) + elif not my_teacher_names and amended_teacher_names: + return amended_teacher_names + return my_teacher_names + + @property + def subject_name_with_amends(self: "LessonEvent") -> str: + my_subject = self.subject.name if self.subject else "" + amended_subject = self.real_amends.subject.name if self.amends else "" + + if my_subject and amended_subject: + return _("{} (instead of {})").format(my_subject, amended_subject) + elif not my_subject and amended_subject: + return amended_subject + elif my_subject: + return my_subject + return _("Lesson") + + @property + def real_amends(self: "LessonEvent") -> "LessonEvent": + # FIXME THIS IS AWFUL SLOW + if self.amends: + return LessonEvent.objects.get(pk=self.amends.pk) + return self + + @classmethod + def value_title(cls, reference_object: "LessonEvent", request) -> str: + """Get the title of the event.""" + if reference_object.title: + return reference_object.title + elif reference_object.subject or ( + reference_object.amends and reference_object.real_amends.subject + ): + title = reference_object.subject_name_with_amends + if request.user.person in reference_object.teachers.all(): + title += " · " + reference_object.group_names + else: + title += " · " + reference_object.teacher_names_with_amends + if reference_object.rooms.all().exists(): + title += " · " + reference_object.room_names_with_amends + return title + + return _("Lesson") + + @classmethod + def value_description(cls, reference_object: "LessonEvent", request) -> str: + return render_to_string("chronos/lesson_event_description.txt", {"event": reference_object}) + + @classmethod + def value_color(cls, reference_object: "LessonEvent", request) -> str: + """Get the color of the event.""" + if reference_object.cancelled: + return "#eeeeee" + if reference_object.subject: + return reference_object.subject.colour_bg + if reference_object.amends and reference_object.real_amends.subject: + return reference_object.real_amends.subject.colour_bg + return super().value_color(reference_object, request) + + @classmethod + def value_attendee(cls, reference_object: "LessonEvent", request) -> str: + """Get the attendees of the event.""" + attendees = [t.get_vcal_address(role="CHAIR") for t in reference_object.teachers.all()] + return [a for a in attendees if a] + + @classmethod + def value_location(cls, reference_object: "LessonEvent", request) -> str: + """Get the location of the event.""" + return ", ".join([r.name for r in reference_object.rooms.all()]) + + @classmethod + def value_status(cls, reference_object: "LessonEvent", request) -> str: + """Get the status of the event.""" + if reference_object.cancelled: + return "CANCELLED" + return "CONFIRMED" + + @classmethod + def value_meta(cls, reference_object: "LessonEvent", request) -> str: + """Get the meta of the event.""" + real_amends = reference_object.real_amends + + return { + "id": reference_object.id, + "amended": bool(reference_object.amends), + "amends": cls.value_meta(real_amends, request) if reference_object.amends else None, + "teachers": [ + { + "id": t.pk, + "first_name": t.first_name, + "last_name": t.last_name, + "full_name": t.full_name, + "short_name": t.short_name, + } + for t in reference_object.teachers.all() + ], + "is_teacher": request.user.person in reference_object.all_teachers, + "groups": [ + {"id": g.pk, "name": g.name, "short_name": g.short_name} + for g in reference_object.actual_groups + ], + "is_member": request.user.person in reference_object.all_members, + "rooms": [ + {"id": r.pk, "name": r.name, "short_name": r.short_name} + for r in reference_object.rooms.all() + ], + "subject": { + "id": reference_object.subject.pk, + "name": reference_object.subject.name, + "short_name": reference_object.subject.short_name, + "colour_fg": reference_object.subject.colour_fg, + "colour_bg": reference_object.subject.colour_bg, + } + if reference_object.subject + else None, + "comment": reference_object.comment, + "cancelled": reference_object.cancelled, + } + + @classmethod + def get_objects(cls, request, params=None) -> Iterable: + """Return all objects that should be included in the calendar.""" + objs = super().get_objects(request, params).not_instance_of(SupervisionEvent) + if params: + obj_id = int(params.get("id", 0)) + type = params.get("type", None) + + if type and obj_id: + if type == "TEACHER": + return objs.for_teacher(obj_id) + elif type == "GROUP": + return objs.for_group(obj_id) + elif type == "ROOM": + return objs.for_room(obj_id) + return objs.for_person(request.user.person) + + class Meta: + verbose_name = _("Lesson Event") + verbose_name_plural = _("Lesson Events") + + +class SupervisionEvent(LessonEvent): + name = "supervision" + verbose_name = _("Supervisions") + + objects = PolymorphicCurrentSiteManager.from_queryset(LessonEventQuerySet)() + + @classmethod + def value_title(cls, reference_object: "LessonEvent", request) -> str: + """Get the title of the event.""" + + return _("Supervision: {}").format(reference_object.room_names) + + @classmethod + def value_description(cls, reference_object: "LessonEvent", request) -> str: + return render_to_string( + "chronos/supervision_event_description.txt", {"event": reference_object} + ) + + @classmethod + def get_objects(cls, request, params=None) -> Iterable: + """Return all objects that should be included in the calendar.""" + objs = cls.objects.instance_of(cls) + if params: + obj_id = int(params.get("id", 0)) + type = params.get("type", None) + + if type and obj_id: + if type == "TEACHER": + return objs.for_teacher(obj_id) + elif type == "GROUP": + return objs.for_group(obj_id) + elif type == "ROOM": + return objs.for_room(obj_id) + return objs.for_person(request.user.person) diff --git a/aleksis/apps/chronos/schema/__init__.py b/aleksis/apps/chronos/schema/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f7cc25ff46c7fe7018876cc782fb6ad4e92e4eca --- /dev/null +++ b/aleksis/apps/chronos/schema/__init__.py @@ -0,0 +1,193 @@ +from datetime import timezone + +import graphene +from graphene_django import DjangoObjectType +from graphene_django_cud.mutations import DjangoCreateMutation, DjangoPatchMutation + +from aleksis.core.models import CalendarEvent, Group, Person, Room +from aleksis.core.schema.base import DeleteMutation + +from ..models import LessonEvent +from ..util.chronos_helpers import get_classes, get_rooms, get_teachers + + +class TimetablePersonType(DjangoObjectType): + class Meta: + model = Person + fields = ("id", "first_name", "last_name", "short_name") + skip_registry = True + + +class TimetableGroupType(DjangoObjectType): + class Meta: + model = Group + fields = ("id", "name", "short_name") + skip_registry = True + + +class TimetableRoomType(DjangoObjectType): + class Meta: + model = Room + fields = ("id", "name", "short_name") + skip_registry = True + + +# There is another unrelated CalendarEventType in aleksis/core/schema/calendar +# This CalendarEventType is needed for the inherited amends field of LessonEvent +# to work in the graphql query. +class CalendarEventForLessonEventType(DjangoObjectType): + class Meta: + model = CalendarEvent + fields = ("id", "amends", "datetime_start", "datetime_end") + + +class LessonEventType(DjangoObjectType): + class Meta: + model = LessonEvent + fields = ( + "id", + "amends", + "datetime_start", + "datetime_end", + "subject", + "teachers", + "groups", + "rooms", + "cancelled", + "comment", + ) + + +class DatetimeTimezoneMixin: + """Handle datetimes for mutations with CalendarEvent objects. + + This is necessary because the client sends timezone information as + ISO string which only includes an offset (+00:00 UTC) and an + offset is not a valid timezone. Instead we set UTC as timezone + here directly. + """ + + @classmethod + def handle_datetime_start(cls, value, name, info) -> int: + value = value.replace(tzinfo=timezone.utc) + return value + + @classmethod + def handle_datetime_end(cls, value, name, info) -> int: + value = value.replace(tzinfo=timezone.utc) + return value + + @classmethod + def before_save(cls, root, info, input, obj, patch_obj=False): + # before_save has different signatures for different mutations + # This handles create & patch + # https://graphene-django-cud.readthedocs.io/en/latest/guide/other-hooks.html?highlight=before_save#before-save + + if patch_obj: + obj = patch_obj + + obj.timezone = obj.amends.timezone + return obj + + +class AmendLessonCreateMutation(DatetimeTimezoneMixin, DjangoCreateMutation): + class Meta: + model = LessonEvent + permissions = ("chronos.edit_substitution_rule",) + only_fields = ( + "amends", + "datetime_start", + "datetime_end", + "subject", + "teachers", + "groups", + "rooms", + "cancelled", + "comment", + ) + + +class AmendLessonPatchMutation(DatetimeTimezoneMixin, DjangoPatchMutation): + class Meta: + model = LessonEvent + permissions = ("chronos.edit_substitution_rule",) + only_fields = ("subject", "teachers", "groups", "rooms", "cancelled", "comment") + + +class AmendLessonDeleteMutation(DeleteMutation): + klass = LessonEvent + permission_required = "chronos.edit_substitution_rule" + + +class TimetableType(graphene.Enum): + TEACHER = "teacher" + GROUP = "group" + ROOM = "room" + + +class TimetableObjectType(graphene.ObjectType): + id = graphene.String() # noqa + obj_id = graphene.String() + name = graphene.String() + short_name = graphene.String() + type = graphene.Field(TimetableType) # noqa + + def resolve_obj_id(root, info, **kwargs): + return root.id + + def resolve_id(root, info, **kwargs): + return f"{root.type.value}-{root.id}" + + +class Query(graphene.ObjectType): + timetable_teachers = graphene.List(TimetablePersonType) + timetable_groups = graphene.List(TimetableGroupType) + timetable_rooms = graphene.List(TimetableRoomType) + available_timetables = graphene.List(TimetableObjectType) + + def resolve_timetable_teachers(self, info, **kwargs): + return get_teachers(info.context.user) + + def resolve_timetable_groups(self, info, **kwargs): + return get_classes(info.context.user) + + def resolve_timetable_rooms(self, info, **kwargs): + return get_rooms(info.context.user) + + def resolve_available_timetables(self, info, **kwargs): + all_timetables = [] + for group in get_classes(info.context.user): + all_timetables.append( + TimetableObjectType( + id=group.id, + name=group.name, + short_name=group.short_name, + type=TimetableType.GROUP, + ) + ) + + for teacher in get_teachers(info.context.user): + print(teacher.full_name) + all_timetables.append( + TimetableObjectType( + id=teacher.id, + name=teacher.full_name, + short_name=teacher.short_name, + type=TimetableType.TEACHER, + ) + ) + + for room in get_rooms(info.context.user): + all_timetables.append( + TimetableObjectType( + id=room.id, name=room.name, short_name=room.short_name, type=TimetableType.ROOM + ) + ) + + return all_timetables + + +class Mutation(graphene.ObjectType): + create_amend_lesson = AmendLessonCreateMutation.Field() + patch_amend_lesson = AmendLessonPatchMutation.Field() + delete_amend_lesson = AmendLessonDeleteMutation.Field() diff --git a/aleksis/apps/chronos/templates/chronos/lesson_event_description.txt b/aleksis/apps/chronos/templates/chronos/lesson_event_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..573c92354535f0f4a5ed029e9efab63bc71c3639 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/lesson_event_description.txt @@ -0,0 +1,6 @@ +{% load i18n %}{% trans "Groups" %}: {{ event.group_names|default:"–" }}{% if event.subject %} +{% trans "Subject" %}: {{ event.subject_name_with_amends }}{% endif %} +{% trans "Teachers" %}: {{ event.teacher_names_with_amends|default:"–" }} +{% trans "Rooms" %}: {{ event.room_names_with_amends|default:"–" }}{% if event.comment %} + +{{ event.comment }}{% endif %} \ No newline at end of file diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/room.html b/aleksis/apps/chronos/templates/chronos/partials/subs/room.html index a80afb98487308b20733e1f23cee8a29821b1eb0..94f2d3574992d23dffe3d6f9b03ffdd3e75cb042 100644 --- a/aleksis/apps/chronos/templates/chronos/partials/subs/room.html +++ b/aleksis/apps/chronos/templates/chronos/partials/subs/room.html @@ -1,6 +1,6 @@ {% if type == "substitution" %} {% if el.cancelled or el.cancelled_for_teachers %} - {# Canceled lesson: no room #} + {# Cancelled lesson: no room #} {% elif el.room and el.lesson_period.room %} {# New and old room available #} <span class="tooltipped" data-position="bottom" diff --git a/aleksis/apps/chronos/templates/chronos/supervision_event_description.txt b/aleksis/apps/chronos/templates/chronos/supervision_event_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..d2f311f240e5cf527033bc08de15869085dc0ee5 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/supervision_event_description.txt @@ -0,0 +1,2 @@ +{% load i18n %}{% trans "Teachers" %}: {{ event.teacher_names }} +{% trans "Areas" %}: {{ event.room_names }} \ No newline at end of file diff --git a/aleksis/apps/chronos/tests/regression/test_regression.py b/aleksis/apps/chronos/tests/regression/test_regression.py deleted file mode 100644 index 0b4a71838cdfb1fafe2600022868558d70b9bd14..0000000000000000000000000000000000000000 --- a/aleksis/apps/chronos/tests/regression/test_regression.py +++ /dev/null @@ -1,76 +0,0 @@ -from datetime import time, timedelta - -from django.contrib.auth import get_user_model -from django.utils import timezone - -import pytest - -from aleksis.apps.chronos.util.chronos_helpers import get_rooms, get_teachers -from aleksis.core.models import Group, Person, Room, SchoolTerm - -pytestmark = pytest.mark.django_db - - -from aleksis.apps.chronos.models import Lesson, LessonPeriod, Subject, TimePeriod, ValidityRange - - -def test_rooms_teachers_only_from_current_school_term(): - User = get_user_model() - - user = User.objects.create(username="test", is_staff=True, is_superuser=True) - person_user = Person.objects.create(user=user, first_name="Test", last_name="User") - - correct_school_term = SchoolTerm.objects.create( - date_start=timezone.now() - timedelta(days=1), - date_end=timezone.now() + timedelta(days=1), - name="Correct school term", - ) - wrong_school_term = SchoolTerm.objects.create( - date_start=timezone.now() - timedelta(days=3), - date_end=timezone.now() - timedelta(days=2), - name="Wrong school term", - ) - - correct_validity = ValidityRange.objects.create( - school_term=correct_school_term, - date_start=correct_school_term.date_start, - date_end=correct_school_term.date_end, - name="Correct validity", - ) - wrong_validity = ValidityRange.objects.create( - school_term=wrong_school_term, - date_start=wrong_school_term.date_start, - date_end=wrong_school_term.date_end, - name="Wrong validity", - ) - - subject = Subject.objects.create(name="Test subject", short_name="TS") - time_period = TimePeriod.objects.create( - weekday=0, period=1, time_start=time(8, 0), time_end=time(9, 0) - ) - - correct_person = Person.objects.create(first_name="Correct", last_name="Person") - wrong_person = Person.objects.create(first_name="Wrong", last_name="Person") - - correct_lesson = Lesson.objects.create(validity=correct_validity, subject=subject) - correct_lesson.teachers.add(correct_person) - wrong_lesson = Lesson.objects.create(validity=wrong_validity, subject=subject) - wrong_lesson.teachers.add(wrong_person) - - correct_room = Room.objects.create(name="Correct room", short_name="cr") - wrong_room = Room.objects.create(name="Wrong room", short_name="wr") - - correct_lesson_period = LessonPeriod.objects.create( - lesson=correct_lesson, period=time_period, room=correct_room - ) - wrong_lesson_period = LessonPeriod.objects.create( - lesson=wrong_lesson, period=time_period, room=wrong_room - ) - - rooms = get_rooms(user) - assert correct_room in rooms - assert wrong_room not in rooms - - teachers = get_teachers(user) - assert correct_person in teachers - assert wrong_person not in teachers diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py index 17d98c54a88e28c9be5ca81d8561924d4155c83a..5664c0b97e13e68ab18120a2cadfd39396eaa8a4 100644 --- a/aleksis/apps/chronos/util/chronos_helpers.py +++ b/aleksis/apps/chronos/util/chronos_helpers.py @@ -9,7 +9,7 @@ from django.utils import timezone from guardian.core import ObjectPermissionChecker -from aleksis.core.models import Announcement, Group, Person, Room, SchoolTerm +from aleksis.core.models import Announcement, Group, Person, Room from aleksis.core.util.core_helpers import get_site_preferences from aleksis.core.util.predicates import check_global_permission @@ -74,10 +74,8 @@ def get_teachers(user: "User"): """Get the teachers whose timetables are allowed to be seen by current user.""" checker = ObjectPermissionChecker(user) - school_term = SchoolTerm.current - school_term_q = Q(lessons_as_teacher__validity__school_term=school_term) if school_term else Q() teachers = ( - Person.objects.annotate(lessons_count=Count("lessons_as_teacher", filter=school_term_q)) + Person.objects.annotate(lessons_count=Count("lesson_events_as_teacher")) .filter(lessons_count__gt=0) .order_by("short_name", "last_name") ) @@ -93,6 +91,8 @@ def get_teachers(user: "User"): teachers = teachers.filter(Q(pk=user.person.pk) | Q(pk__in=wanted_teachers)) + teachers = teachers.distinct() + return teachers @@ -103,8 +103,8 @@ def get_classes(user: "User"): classes = ( Group.objects.for_current_school_term_or_all() .annotate( - lessons_count=Count("lessons"), - child_lessons_count=Count("child_groups__lessons"), + lessons_count=Count("lesson_events"), + child_lessons_count=Count("child_groups__lesson_events"), ) .filter( Q(lessons_count__gt=0, parent_groups=None) @@ -128,6 +128,8 @@ def get_classes(user: "User"): if user.person.primary_group: classes = classes.filter(Q(pk=user.person.primary_group.pk)) + classes = classes.distinct() + return classes @@ -135,13 +137,8 @@ def get_rooms(user: "User"): """Get the rooms whose timetables are allowed to be seen by current user.""" checker = ObjectPermissionChecker(user) - school_term = SchoolTerm.current - school_term_q = ( - Q(lesson_periods__lesson__validity__school_term=school_term) if school_term else Q() - ) - rooms = ( - Room.objects.annotate(lessons_count=Count("lesson_periods", filter=school_term_q)) + Room.objects.annotate(lessons_count=Count("lesson_events")) .filter(lessons_count__gt=0) .order_by("short_name", "name") ) @@ -157,6 +154,8 @@ def get_rooms(user: "User"): rooms = rooms.filter(Q(pk__in=wanted_rooms)) + rooms = rooms.distinct() + return rooms diff --git a/fixtures/highschool-de-20212022_short.json b/fixtures/highschool-de-20212022_short.json new file mode 100644 index 0000000000000000000000000000000000000000..1faf32e3bbc6607475a42e8d3ca714fdd653e87a --- /dev/null +++ b/fixtures/highschool-de-20212022_short.json @@ -0,0 +1,3088 @@ +[ + { + "model": "auth.user", + "pk": 2, + "fields": { + "password": "!kOVrghAiFaFTsTSTrU96OGEFB4vvhe4KLo9ASH4o", + "last_login": null, + "is_superuser": false, + "username": "?", + "first_name": "?", + "last_name": "?", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:04.997Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 3, + "fields": { + "password": "!B4NXmwHzWuvGMfdtRx0agEYe2UEOrJBmPfr6cHDs", + "last_login": null, + "is_superuser": false, + "username": "alb", + "first_name": "Carl", + "last_name": "Albrecht", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.045Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 4, + "fields": { + "password": "!GGy8AQjJcmPtqGtZKfeUyE4glGfvZEL6g4CmCObW", + "last_login": null, + "is_superuser": false, + "username": "ald", + "first_name": "Tobias", + "last_name": "Arnold", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.048Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 5, + "fields": { + "password": "!QrBCVfLfVPTJHdQIDAcgynKvqk82MZnKHt23ajEY", + "last_login": null, + "is_superuser": false, + "username": "bar", + "first_name": "Anna", + "last_name": "Bauer", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.050Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 6, + "fields": { + "password": "!TEPmv5Y6SG2pSB2l3v4dTOR8GB6I3k1ph2B21VQ9", + "last_login": null, + "is_superuser": false, + "username": "bau", + "first_name": "Anna-Lena", + "last_name": "Baumann", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.053Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 7, + "fields": { + "password": "!H2w4tzXb9USp2lccHanAqdgcIAWpk1fDr4OHKutg", + "last_login": null, + "is_superuser": false, + "username": "bam", + "first_name": "Werner", + "last_name": "Baumann", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.056Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 8, + "fields": { + "password": "!R2SMsUgP2XE5UCVELhCwaAXVkRuBU4CQfbh5YdWq", + "last_login": null, + "is_superuser": false, + "username": "bec", + "first_name": "Ella", + "last_name": "Beck", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.059Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 9, + "fields": { + "password": "!vkBLfDfinfpcUgoQ20hXQYjBTU93oaTKX9DQVNN3", + "last_login": null, + "is_superuser": false, + "username": "bek", + "first_name": "Herrmann", + "last_name": "Becker", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.062Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 10, + "fields": { + "password": "!Ku1Shj80attBZOufvRUXP3U908WAh2Wxhe6Lt6UC", + "last_login": null, + "is_superuser": false, + "username": "beg", + "first_name": "Georg", + "last_name": "Berger", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.065Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 11, + "fields": { + "password": "!dI923sXrtSeZlW6TSpNmGABySLEi1iFtaNW9ZtaS", + "last_login": null, + "is_superuser": false, + "username": "ber", + "first_name": "Stefan", + "last_name": "Berger", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.068Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 12, + "fields": { + "password": "!sTXSXu2O0QITR2t5sfPEqdn8J63bW0mcZvTW4kiP", + "last_login": null, + "is_superuser": false, + "username": "kam", + "first_name": "Stefan", + "last_name": "Berger", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.071Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 13, + "fields": { + "password": "!F05GGoRuP83XO4UdrTdwsCv2zabOWolsr95UrIET", + "last_login": null, + "is_superuser": false, + "username": "bem", + "first_name": "Johanna", + "last_name": "Bergmann", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.075Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 14, + "fields": { + "password": "!MKF0i9NGbRximlktRwwetz76hNo2ck0wNeyieMqU", + "last_login": null, + "is_superuser": false, + "username": "boh", + "first_name": "Ilse", + "last_name": "Bohr", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.078Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 15, + "fields": { + "password": "!krWVzD4EcFUEvDvUd2oWK7eJdJoafDm7ktEEt1RD", + "last_login": null, + "is_superuser": false, + "username": "bra", + "first_name": "Helene", + "last_name": "Brandt", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.080Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 16, + "fields": { + "password": "!IDWlZkdySSkpNJuD9oTc3KWhGjWMYYWxx28il6cC", + "last_login": null, + "is_superuser": false, + "username": "bdt", + "first_name": "Philipp", + "last_name": "Brandt", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.082Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 17, + "fields": { + "password": "!gWHQ2ClDCw9MIT6g4kCKMDzOMymGVS0qw8NAFias", + "last_login": null, + "is_superuser": false, + "username": "bru", + "first_name": "Clara", + "last_name": "Braun", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.086Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 18, + "fields": { + "password": "!hBCFGb0NuJyHjJxo0a3QkZDunzYZ0JFBAj6MAdSs", + "last_login": null, + "is_superuser": false, + "username": "bus", + "first_name": "Frieda", + "last_name": "Busch", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.088Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 19, + "fields": { + "password": "!q5B1aQZXgA6c7vqBelhF9fxFDKobu5W0Z1AtBcep", + "last_login": null, + "is_superuser": false, + "username": "die", + "first_name": "Ursula", + "last_name": "Dietrich", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.091Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 20, + "fields": { + "password": "!TFE3JM6nZgwGab0omHCFuXQZWsllVfOkskrEnk4m", + "last_login": null, + "is_superuser": false, + "username": "eng", + "first_name": "Lotte", + "last_name": "Engel", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.093Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 21, + "fields": { + "password": "!Rn25F7BOjekvA0W4DqHxE9hHuTyne8yPkLkPhJ2G", + "last_login": null, + "is_superuser": false, + "username": "ern", + "first_name": "Kurt", + "last_name": "Ernst", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.096Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 22, + "fields": { + "password": "!h5gXbGGCuJGlhDjFvfFbXRCOBeyLwaOVaOOGmHH0", + "last_login": null, + "is_superuser": false, + "username": "fis", + "first_name": "Alexander", + "last_name": "Fischer", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.098Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 23, + "fields": { + "password": "!iWthkBJYSKH0IgR8qHVDQUQxFlO9MTzbtTx6xkr4", + "last_login": null, + "is_superuser": false, + "username": "fra", + "first_name": "Lene", + "last_name": "Frank", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.100Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 24, + "fields": { + "password": "!FEMVVShg0YpqhxWYGbn7kKjWnAXuCkWGTHEwC9DP", + "last_login": null, + "is_superuser": false, + "username": "fre", + "first_name": "Sebastian", + "last_name": "Franke", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.102Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 25, + "fields": { + "password": "!VE1EIxTsx24laMGTynWoAoP5oAGryluDPwz42Or0", + "last_login": null, + "is_superuser": false, + "username": "fri", + "first_name": "Gabriele", + "last_name": "Friedrich", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.105Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 26, + "fields": { + "password": "!bBO37upTrg6DZ9Hp74Up3iQBeS4QT8YN1KfbLxNZ", + "last_login": null, + "is_superuser": false, + "username": "fuc", + "first_name": "Karsten", + "last_name": "Fuchs", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.109Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 27, + "fields": { + "password": "!i69z1Y7rxPhs8l19cDLJoWuFwdkd3hhaXLeQ0ydS", + "last_login": null, + "is_superuser": false, + "username": "gra", + "first_name": "Maja", + "last_name": "Graf", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.112Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 28, + "fields": { + "password": "!1Txl1BNrE4tgoIvqtTA3WZp4BGSz7NQJMBEq3uPF", + "last_login": null, + "is_superuser": false, + "username": "gue", + "first_name": "Nina", + "last_name": "G\u00fcnther", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.114Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 29, + "fields": { + "password": "!jXSLoz6BEASc9zqMdPF2KcmN3Cwatm5VATZ565rM", + "last_login": null, + "is_superuser": false, + "username": "haa", + "first_name": "Greta", + "last_name": "Haas", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.116Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 30, + "fields": { + "password": "!hP5qJsooLG28yfnRymEjJF0qHzLUVnqIsyNB3CMe", + "last_login": null, + "is_superuser": false, + "username": "hah", + "first_name": "Michael", + "last_name": "Hahn", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.119Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 31, + "fields": { + "password": "!DWb4yp3P1Xh3S2FLH1AhxqsTuWDtWhtw5cCl2ZY2", + "last_login": null, + "is_superuser": false, + "username": "hei", + "first_name": "Constanze", + "last_name": "Heinrich", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.121Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 32, + "fields": { + "password": "!PWkxrNP1FPx2094FcRwUbohVyz9cVv82koiKXvVo", + "last_login": null, + "is_superuser": false, + "username": "her", + "first_name": "Karla", + "last_name": "Hermann", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.124Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 33, + "fields": { + "password": "!FFljZ20cxb4AKPyU4sw4hJhSYJJxEcYWNx9XOeui", + "last_login": null, + "is_superuser": false, + "username": "hor", + "first_name": "Lukas", + "last_name": "Horstmann", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.126Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 34, + "fields": { + "password": "!g2BwbxLeNA7alWX1lV0Xmx4bZ7UxgayusAiaDt0Q", + "last_login": null, + "is_superuser": false, + "username": "hub", + "first_name": "Frank", + "last_name": "Hubert", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:30:05.129Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 35, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "jan", + "first_name": "Markus", + "last_name": "Jansen", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.698Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 36, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "jen", + "first_name": "Christiane", + "last_name": "Jenner", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.709Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 37, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "jun", + "first_name": "Christoph", + "last_name": "Jung", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.714Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 38, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "kai", + "first_name": "Ida", + "last_name": "Kaiser", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.720Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 39, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "kel", + "first_name": "Oliver", + "last_name": "Keller", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.728Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 40, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "koc", + "first_name": "Norman", + "last_name": "Koch", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.733Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 41, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "krs", + "first_name": "Peter", + "last_name": "Kraus", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.738Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 42, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "kra", + "first_name": "Justina", + "last_name": "Krause", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.743Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 43, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "kr\u00fc", + "first_name": "Barbara", + "last_name": "Kr\u00fcger", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.750Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 44, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "kuh", + "first_name": "Nadine", + "last_name": "Kuhn", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.756Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 45, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "lan", + "first_name": "Ruth", + "last_name": "Lang", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.762Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 46, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "lor", + "first_name": "Matthias", + "last_name": "Lorenz", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.766Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 47, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "lud", + "first_name": "Hans", + "last_name": "Ludwig", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.773Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 48, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "lut", + "first_name": "Artur", + "last_name": "Lutz", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.779Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 49, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "may", + "first_name": "Elisa", + "last_name": "Mayer", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.784Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 50, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "nag", + "first_name": "Sven", + "last_name": "Nagel", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.791Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 51, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "ott", + "first_name": "Thomas", + "last_name": "Ott", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.797Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 52, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "pet", + "first_name": "Felicitas", + "last_name": "Peters", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.801Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 53, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "pol", + "first_name": "Diana", + "last_name": "Pohl", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.807Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 54, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "rot", + "first_name": "Laura", + "last_name": "Roth", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.813Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 55, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "sol", + "first_name": "Katja", + "last_name": "Scholz", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.819Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 56, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "sub", + "first_name": "Miriam", + "last_name": "Schubert", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.826Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 57, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "slt", + "first_name": "Kathrin", + "last_name": "Schulte", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.831Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 58, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "sul", + "first_name": "Paul", + "last_name": "Schulze", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.838Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 59, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "sus", + "first_name": "Lisa", + "last_name": "Schuster", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.842Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 60, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "swe", + "first_name": "Vera", + "last_name": "Schwenck", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.848Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 61, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "sei", + "first_name": "Patrick", + "last_name": "Seidel", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.854Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 62, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "sim", + "first_name": "Michaela", + "last_name": "Simon", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.859Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 63, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "som", + "first_name": "Dennis", + "last_name": "Sommer", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.863Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 64, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "stn", + "first_name": "Sarah", + "last_name": "Stein", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.868Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 65, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "vot", + "first_name": "Melanie", + "last_name": "Vogt", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.874Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 66, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "wal", + "first_name": "Marie", + "last_name": "Walter", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.879Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 67, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "wei", + "first_name": "Margarete", + "last_name": "Wei\u00df", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.886Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 68, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "win", + "first_name": "Horst", + "last_name": "Winkler", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.891Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "auth.user", + "pk": 69, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "zie", + "first_name": "Julia", + "last_name": "Ziegler", + "email": "", + "is_staff": false, + "date_joined": "2021-10-12T16:31:17.895Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "core.person", + "pk": 1, + "fields": { + "site": 1, + "extended_data": {}, + "user": 1, + "first_name": "Admin", + "last_name": "istrator", + "additional_name": "", + "short_name": null, + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 69, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 1 + }, + "user": null, + "first_name": "Anna", + "last_name": "Bauer", + "additional_name": "", + "short_name": "BAR", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 70, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 2 + }, + "user": null, + "first_name": "Carl", + "last_name": "Albrecht", + "additional_name": "", + "short_name": "ALB", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 71, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 3 + }, + "user": null, + "first_name": "Werner", + "last_name": "Baumann", + "additional_name": "", + "short_name": "BAM", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 72, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 4 + }, + "user": null, + "first_name": "Herrmann", + "last_name": "Becker", + "additional_name": "", + "short_name": "BEK", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 73, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 5 + }, + "user": null, + "first_name": "Georg", + "last_name": "Berger", + "additional_name": "", + "short_name": "BEG", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 74, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 6 + }, + "user": null, + "first_name": "Johanna", + "last_name": "Bergmann", + "additional_name": "", + "short_name": "BEM", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 75, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 8 + }, + "user": null, + "first_name": "Ilse", + "last_name": "Bohr", + "additional_name": "", + "short_name": "BOH", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 76, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 10 + }, + "user": null, + "first_name": "Helene", + "last_name": "Brandt", + "additional_name": "", + "short_name": "BRA", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 77, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 11 + }, + "user": null, + "first_name": "Clara", + "last_name": "Braun", + "additional_name": "", + "short_name": "BRU", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 78, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 12 + }, + "user": null, + "first_name": "Frieda", + "last_name": "Busch", + "additional_name": "", + "short_name": "BUS", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 79, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 13 + }, + "user": null, + "first_name": "Ursula", + "last_name": "Dietrich", + "additional_name": "", + "short_name": "DIE", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 80, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 14 + }, + "user": null, + "first_name": "Katja", + "last_name": "Scholz", + "additional_name": "", + "short_name": "SOL", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 81, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 15 + }, + "user": null, + "first_name": "Lotte", + "last_name": "Engel", + "additional_name": "", + "short_name": "ENG", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 82, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 16 + }, + "user": null, + "first_name": "Kurt", + "last_name": "Ernst", + "additional_name": "", + "short_name": "ERN", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 83, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 17 + }, + "user": null, + "first_name": "Alexander", + "last_name": "Fischer", + "additional_name": "", + "short_name": "FIS", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 84, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 18 + }, + "user": null, + "first_name": "Lene", + "last_name": "Frank", + "additional_name": "", + "short_name": "FRA", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 85, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 19 + }, + "user": null, + "first_name": "Sebastian", + "last_name": "Franke", + "additional_name": "", + "short_name": "FRE", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 86, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 20 + }, + "user": null, + "first_name": "Gabriele", + "last_name": "Friedrich", + "additional_name": "", + "short_name": "FRI", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 87, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 21 + }, + "user": null, + "first_name": "Karsten", + "last_name": "Fuchs", + "additional_name": "", + "short_name": "FUC", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 88, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 22 + }, + "user": null, + "first_name": "Maja", + "last_name": "Graf", + "additional_name": "", + "short_name": "GRA", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 89, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 23 + }, + "user": null, + "first_name": "Nina", + "last_name": "G\u00fcnther", + "additional_name": "", + "short_name": "GUE", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 90, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 25 + }, + "user": null, + "first_name": "Greta", + "last_name": "Haas", + "additional_name": "", + "short_name": "HAA", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 91, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 27 + }, + "user": null, + "first_name": "Michael", + "last_name": "Hahn", + "additional_name": "", + "short_name": "HAH", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 92, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 28 + }, + "user": null, + "first_name": "Constanze", + "last_name": "Heinrich", + "additional_name": "", + "short_name": "HEI", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 93, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 30 + }, + "user": null, + "first_name": "Christiane", + "last_name": "Jenner", + "additional_name": "", + "short_name": "JEN", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 94, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 31 + }, + "user": null, + "first_name": "Barbara", + "last_name": "Kr\u00fcger", + "additional_name": "", + "short_name": "KR\u00dc", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 95, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 32 + }, + "user": null, + "first_name": "Norman", + "last_name": "Koch", + "additional_name": "", + "short_name": "KOC", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 96, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 33 + }, + "user": null, + "first_name": "Justina", + "last_name": "Krause", + "additional_name": "", + "short_name": "KRA", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 97, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 34 + }, + "user": null, + "first_name": "Oliver", + "last_name": "Keller", + "additional_name": "", + "short_name": "KEL", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 98, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 35 + }, + "user": null, + "first_name": "Christoph", + "last_name": "Jung", + "additional_name": "", + "short_name": "JUN", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 99, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 37 + }, + "user": null, + "first_name": "Frank", + "last_name": "Hubert", + "additional_name": "", + "short_name": "HUB", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 100, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 38 + }, + "user": null, + "first_name": "Karla", + "last_name": "Hermann", + "additional_name": "", + "short_name": "HER", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 101, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 39 + }, + "user": null, + "first_name": "Paul", + "last_name": "Schulze", + "additional_name": "", + "short_name": "SUL", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 102, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 40 + }, + "user": null, + "first_name": "Ida", + "last_name": "Kaiser", + "additional_name": "", + "short_name": "KAI", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 103, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 41 + }, + "user": null, + "first_name": "Ruth", + "last_name": "Lang", + "additional_name": "", + "short_name": "LAN", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 104, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 43 + }, + "user": null, + "first_name": "Felicitas", + "last_name": "Peters", + "additional_name": "", + "short_name": "PET", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 105, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 44 + }, + "user": null, + "first_name": "Miriam", + "last_name": "Schubert", + "additional_name": "", + "short_name": "SUB", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 106, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 45 + }, + "user": null, + "first_name": "Laura", + "last_name": "Roth", + "additional_name": "", + "short_name": "ROT", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 107, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 46 + }, + "user": null, + "first_name": "Stefan", + "last_name": "Berger", + "additional_name": "", + "short_name": "BER", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 108, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 47 + }, + "user": null, + "first_name": "Horst", + "last_name": "Winkler", + "additional_name": "", + "short_name": "WIN", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 109, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 48 + }, + "user": null, + "first_name": "Anna-Lena", + "last_name": "Baumann", + "additional_name": "", + "short_name": "BAU", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 110, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 49 + }, + "user": null, + "first_name": "Peter", + "last_name": "Kraus", + "additional_name": "", + "short_name": "KRS", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 111, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 50 + }, + "user": null, + "first_name": "Michaela", + "last_name": "Simon", + "additional_name": "", + "short_name": "SIM", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 112, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 51 + }, + "user": null, + "first_name": "Hans", + "last_name": "Ludwig", + "additional_name": "", + "short_name": "LUD", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 113, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 52 + }, + "user": null, + "first_name": "Melanie", + "last_name": "Vogt", + "additional_name": "", + "short_name": "VOT", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 114, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 53 + }, + "user": null, + "first_name": "Sarah", + "last_name": "Stein", + "additional_name": "", + "short_name": "STN", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 115, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 54 + }, + "user": null, + "first_name": "Dennis", + "last_name": "Sommer", + "additional_name": "", + "short_name": "SOM", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 116, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 55 + }, + "user": null, + "first_name": "Patrick", + "last_name": "Seidel", + "additional_name": "", + "short_name": "SEI", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 117, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 56 + }, + "user": null, + "first_name": "Julia", + "last_name": "Ziegler", + "additional_name": "", + "short_name": "ZIE", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 118, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 57 + }, + "user": null, + "first_name": "Philipp", + "last_name": "Brandt", + "additional_name": "", + "short_name": "BDT", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 119, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 58 + }, + "user": null, + "first_name": "Nadine", + "last_name": "Kuhn", + "additional_name": "", + "short_name": "KUH", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 120, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 59 + }, + "user": null, + "first_name": "Kathrin", + "last_name": "Schulte", + "additional_name": "", + "short_name": "SLT", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 121, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 60 + }, + "user": null, + "first_name": "Diana", + "last_name": "Pohl", + "additional_name": "", + "short_name": "POL", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 122, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 62 + }, + "user": null, + "first_name": "Tobias", + "last_name": "Arnold", + "additional_name": "", + "short_name": "ALD", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 123, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 63 + }, + "user": null, + "first_name": "Markus", + "last_name": "Jansen", + "additional_name": "", + "short_name": "JAN", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 124, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 65 + }, + "user": null, + "first_name": "Thomas", + "last_name": "Ott", + "additional_name": "", + "short_name": "OTT", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 125, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 66 + }, + "user": null, + "first_name": "Sven", + "last_name": "Nagel", + "additional_name": "", + "short_name": "NAG", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 126, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 67 + }, + "user": null, + "first_name": "Artur", + "last_name": "Lutz", + "additional_name": "", + "short_name": "LUT", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 127, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 68 + }, + "user": null, + "first_name": "?", + "last_name": "?", + "additional_name": "", + "short_name": "?", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 128, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 69 + }, + "user": null, + "first_name": "Marie", + "last_name": "Walter", + "additional_name": "", + "short_name": "WAL", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 129, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 70 + }, + "user": null, + "first_name": "Lukas", + "last_name": "Horstmann", + "additional_name": "", + "short_name": "HOR", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 130, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 71 + }, + "user": null, + "first_name": "Ella", + "last_name": "Beck", + "additional_name": "", + "short_name": "BEC", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 131, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 72 + }, + "user": null, + "first_name": "Matthias", + "last_name": "Lorenz", + "additional_name": "", + "short_name": "LOR", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 132, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 74 + }, + "user": null, + "first_name": "Elisa", + "last_name": "Mayer", + "additional_name": "", + "short_name": "MAY", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 133, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 75 + }, + "user": null, + "first_name": "Margarete", + "last_name": "Wei\u00df", + "additional_name": "", + "short_name": "WEI", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 134, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 76 + }, + "user": null, + "first_name": "Lisa", + "last_name": "Schuster", + "additional_name": "", + "short_name": "SUS", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 135, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 80 + }, + "user": null, + "first_name": "Stefan", + "last_name": "Berger", + "additional_name": "", + "short_name": "KAM", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + }, + { + "model": "core.person", + "pk": 137, + "fields": { + "site": 1, + "extended_data": { + "import_ref_untis": 77 + }, + "user": null, + "first_name": "Vera", + "last_name": "Schwenck", + "additional_name": "", + "short_name": "SWE", + "street": "", + "housenumber": "", + "postal_code": "", + "place": "", + "phone_number": "", + "mobile_number": "", + "email": "", + "date_of_birth": null, + "sex": "", + "photo": "", + "primary_group": null, + "description": "", + "guardians": [] + } + } +] diff --git a/pyproject.toml b/pyproject.toml index 631675cb3535b98703b00078556adcf3519b6d0b..2d526d3c9f01fae6b832e332c0dbed0dfb926be4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "AlekSIS-App-Chronos" -version = "3.0.3.dev0" +version = "4.0.dev1" packages = [ { include = "aleksis" } ] @@ -50,8 +50,9 @@ priority = "supplemental" [tool.poetry.dependencies] python = "^3.9" calendarweek = "^0.5.0" -aleksis-core = "^3.0" -aleksis-app-resint = "^3.0" +aleksis-core = "^4.0.0.dev0" +aleksis-app-resint = "^4.0.dev0" +aleksis-app-cursus = "^0.1.dev0" [tool.poetry.plugins."aleksis.app"] chronos = "aleksis.apps.chronos.apps:ChronosConfig"