diff --git a/README.rst b/README.rst index 61e80f73c24aa9077218aa86a2427463f75f1f0f..e2af874fa42f6e23675156e27fee583be1576b1e 100644 --- a/README.rst +++ b/README.rst @@ -38,6 +38,7 @@ Licence Copyright © 2020, 2021 Julian Leucker <leuckeju@katharineum.de> Copyright © 2020, 2022 Hangzhi Yu <yuha@katharineum.de> Copyright © 2021 Lloyd Meins <meinsll@katharineum.de> + Copyright © 2022 magicfelix <felix@felix-zauberer.de> Licenced under the EUPL, version 1.2 or later, by Teckids e.V. (Bonn, Germany). diff --git a/aleksis/apps/alsijil/apps.py b/aleksis/apps/alsijil/apps.py index b523b38afa08c965628afbfbe61327e8b03f7067..326976e34995942e7431a579c5a6044b29fe4434 100644 --- a/aleksis/apps/alsijil/apps.py +++ b/aleksis/apps/alsijil/apps.py @@ -17,4 +17,5 @@ class AlsijilConfig(AppConfig): ([2020, 2021], "Julian Leucker", "leuckeju@katharineum.de"), ([2020, 2022], "Hangzhi Yu", "yuha@katharineum.de"), ([2021], "Lloyd Meins", "meinsll@katharineum.de"), + ([2022], "magicfelix", "felix@felix-zauberer.de"), ) diff --git a/aleksis/apps/alsijil/assets/UpdateStatuses.js b/aleksis/apps/alsijil/assets/UpdateStatuses.js new file mode 100644 index 0000000000000000000000000000000000000000..bb69013839fa3b83cd69d5e036d82507248ed8a1 --- /dev/null +++ b/aleksis/apps/alsijil/assets/UpdateStatuses.js @@ -0,0 +1,5 @@ +export const + ERROR = "ERROR", // Something went wrong + SAVED = "SAVED", // Everything alright + UPDATING = "UPDATING", // We are sending something to the server + CHANGES = "CHANGES" // the user changed something, but it has not been saved yet diff --git a/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.graphql b/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.graphql new file mode 100644 index 0000000000000000000000000000000000000000..9af1bf9dd9ffb33e16f12ebdd969393f40cab7f1 --- /dev/null +++ b/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.graphql @@ -0,0 +1,80 @@ +query CourseBook($lessonId: ID!) { + excuseTypes { + id + name + shortName + } + lesson: lessonById(id: $lessonId) { + groups { + name + shortName + members { + id + fullName + } + } + subject { + name + } + plannedLessonperiodsDatetimes { + year + week + datetimeStart + lessonPeriod{ + id + period{ + period + } + } + } + } + lessonDocumentations: lessonDocumentationsByLessonId(id: $lessonId) { + id + topic + homework + groupNote + year + week + lessonPeriod { + id + period { + id + period + } + } + event { + id + } + extraLesson { + id + } + period + date + personalNotes { + id + person { + id + fullName + } + tardiness + absent + excused + excuseType { + id + name + shortName + } + remarks + extraMarks { + id + name + shortName + } + } + } + extraMarks { + id + name + shortName + } +} diff --git a/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.vue b/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.vue new file mode 100644 index 0000000000000000000000000000000000000000..6c29cc64e5635cf89b825a0e2809c9bf018eb047 --- /dev/null +++ b/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.vue @@ -0,0 +1,85 @@ +<template> + <ApolloQuery + :query="require('./CourseBook.graphql')" + :variables="{ lessonId: $route.params.lessonId }" + > + <template v-slot="{ result: { loading, error, data } }"> + <!-- Error --> + <message-box v-if="error" type="error">{{ $t("alsijil.error_occurred") }}</message-box> + + <!-- Result --> + <div v-else-if="data" class="result apollo"> + <div class="d-flex justify-space-between"> + <v-btn text color="primary" :href="$root.urls.select_coursebook()"> + <v-icon left>mdi-chevron-left</v-icon> + {{ $t("alsijil.back") }} + </v-btn> + <update-indicator @manual-update="updateManually()" ref="indicator" :status="status"></update-indicator> + </div> + <v-row> + <v-col cols="12"> + <lesson-documentations + :lesson-documentations="data.lessonDocumentations" + :planned-lesson-periods-date-times="data.lesson.plannedLessonperiodsDatetimes" + :groups="data.lesson.groups" + :excuse-types="data.excuseTypes" + :extra-marks="data.extraMarks" + :save-lesson-documentations-per-week="saveLessonDocumentationsPerWeek" + /> + </v-col> + </v-row> + </div> + <!-- No result or Loading --> + <div v-else class="text-center"> + <v-progress-circular + indeterminate + color="primary" + class="ma-auto" + ></v-progress-circular> + </div> + </template> + </ApolloQuery> +</template> + +<script> +import {CHANGES, SAVED, UPDATING} from "../../UpdateStatuses.js"; +import UpdateIndicator from "./UpdateIndicator.vue"; +import LessonDocumentations from "./LessonDocumentations.vue"; + +export default { + components: { + UpdateIndicator, + LessonDocumentations, + }, + props: [ "saveLessonDocumentationsPerWeek" ], + methods: { + processDataChange(event) { + this.status = CHANGES; + // alert("Probably save the data"); + console.log(event); + setTimeout(() => { + this.status = UPDATING; + }, 500) + + setTimeout(() => { + this.status = SAVED; + }, 1000) + + }, + updateManually(event) { + alert("Data sync triggered manually"); + this.status = UPDATING; + setTimeout(() => { + this.status = SAVED; + }, 500) + }, + }, + name: "course-book", + data: () => { + return { + ping: "ping", + status: SAVED, + } + } +} +</script> diff --git a/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.graphql b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.graphql new file mode 100644 index 0000000000000000000000000000000000000000..c591598ae52bd8a72caa67d3e6e974ae78743257 --- /dev/null +++ b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.graphql @@ -0,0 +1,32 @@ +mutation UpdateOrCreateLessonDocumentation($year:Int!, $week:Int!, $lessonPeriodId:ID, $topic:String, $homework:String, $groupNote:String) { + updateOrCreateLessonDocumentation(year:$year, week:$week, lessonPeriodId:$lessonPeriodId, topic:$topic, homework:$homework, groupNote:$groupNote) { + lessonDocumentation{ + id + topic + homework + groupNote + date + personalNotes { + id + person { + id + fullName + } + tardiness + absent + excused + excuseType { + id + name + shortName + } + remarks + extraMarks { + id + name + shortName + } + } + } + } +} diff --git a/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.vue b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.vue new file mode 100644 index 0000000000000000000000000000000000000000..e2679b4ade3b67b44b24ef60f96f2d024d29aeab --- /dev/null +++ b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.vue @@ -0,0 +1,160 @@ +<template> + <ApolloMutation + :mutation="require('./LessonDocumentation.graphql')" + :variables=lessonDocumentationEdit + @done="onDone" + > + <template v-slot="{ mutate, loading, error }"> + <v-card elevation="2" :loading="loading"> + <v-form v-model="valid"> + <v-card-title v-if="saveLessonDocumentationsPerWeek === 'True'"> + <span + v-text="getWeekText(lessonDocumentationEdit)" + class="ma-1 text-h5"> + </span> + </v-card-title> + <v-card-title v-else> + <v-hover v-slot="{ hover }"> + <div> + <v-menu + v-model="showPicker" + :close-on-content-click="false" + transition="scale-transition" + offset-y + min-width="auto" + > + <template v-slot:activator="{ on, attrs }"> + <span> + <span v-text="$d(new Date(lessonDocumentationEdit.date), 'short')" + class="ma-1 text-h5"></span> + <v-btn right v-bind="attrs" v-on="on" icon v-if="hover && dateAndPeriodEditable"> + <v-icon>mdi-pencil-outline</v-icon> + </v-btn> + </span> + </template> + <v-date-picker + scrollable + no-title + @input="showPicker = false; $emit('change-date', $event)" + v-model="lessonDocumentationEdit.date" + ></v-date-picker> + </v-menu> + </div> + </v-hover> + <v-hover v-slot="{ hover }" v-if="!(saveLessonDocumentationsPerWeek === 'True')"> + <div> + <v-menu offset-y> + <template v-slot:activator="{ on, attrs }"> + <span> + <span + v-text="$t('alsijil.period_number', {number: lessonDocumentationEdit.period})" + class="ma-1 text-h5"></span> + <v-btn + right + v-bind="attrs" + v-on="on" + icon + v-if="hover && dateAndPeriodEditable" + > + <v-icon>mdi-pencil-outline</v-icon> + </v-btn> + </span> + </template> + <v-list> + <!-- Fixme: load valid lessons --> + <v-list-item + v-for="(item, index) in [1, 2, 3, 4, 5, 6, 7, 8, 9]" + :key="index" + > + <v-list-item-title>{{ item }}</v-list-item-title> + </v-list-item> + </v-list> + </v-menu> + </div> + </v-hover> + </v-card-title> + <v-card-text> + <v-row> + <v-col cols="12" md="12" lg="12"> + <message-box type="error" v-if="error">{{ $t("alsijil.error_updating") }}</message-box> + <v-textarea + name="input-7-1" + :label="$t('alsijil.lesson_documentation.topic')" + rows="1" + auto-grow + required + + v-model="lessonDocumentationEdit.topic" + ></v-textarea> + <v-textarea + name="input-7-1" + :label="$t('alsijil.lesson_documentation.homework')" + rows="1" + auto-grow + + v-model="lessonDocumentationEdit.homework" + ></v-textarea> + <v-textarea + name="input-7-1" + :label="$t('alsijil.lesson_documentation.group_note')" + rows="1" + auto-grow + + v-model="lessonDocumentationEdit.groupNote" + ></v-textarea> + </v-col> + <v-col v-if="!(saveLessonDocumentationsPerWeek === 'True')" cols="12" md="4" lg="4"> + Personal notes + <personal-notes + :lesson-documentation-id="lessonDocumentationEdit.id" + :groups="groups" + :excuse-types="excuseTypes" + :extra-marks="extraMarks" + + v-model="lessonDocumentationEdit.personalNotes" + @change="$emit('change-personal-notes', $event)" + ></personal-notes> + </v-col> + </v-row> + </v-card-text> + <v-card-actions> + <v-spacer></v-spacer> + <v-btn + color="error" + outlined + @click="$emit('cancel-lesson-documentation-dialog', $event)" + > + {{ $t('alsijil.cancel') }} + </v-btn> + <v-btn + color="success" + @click="mutate()" + > + {{ $t('alsijil.save') }} + </v-btn> + </v-card-actions> + </v-form> + </v-card> + </template> + </ApolloMutation> +</template> + +<script> +import PersonalNotes from "./PersonalNotes.vue"; + +export default { + components: {PersonalNotes}, + props: ["lessonDocumentationEdit", "groups", "excuseTypes", "extraMarks", "saveLessonDocumentationsPerWeek", "getWeekText"], + name: "lesson-documentation", + data() { + return { + dateAndPeriodEditable: false, + showPicker: false, + //lessonDocumentationEdit: {}, + } + }, + //created() { + //this.lessonDocumentationEdit = this.lessonDocumentation + //} +} +</script> diff --git a/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentations.vue b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentations.vue new file mode 100644 index 0000000000000000000000000000000000000000..0fdb51f7768bc8292f65a74ad86d877ef892f5aa --- /dev/null +++ b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentations.vue @@ -0,0 +1,283 @@ +<template><div> + <v-dialog + v-model="dialog" + max-width="800" + > + <template v-slot:activator="{ on, attrs }"> + <v-row> + <v-col cols="12" md="6" class="pb-0 pb-md-3"> + <v-select + v-if="saveLessonDocumentationsPerWeek === 'True'" + :items="emptyLessonPeriods" + :label="$t('alsijil.coursebook.choose_week')" + :item-text="getWeekText" + v-model="selectedLessonPeriodDatetime" + return-object + ></v-select> + <v-select + v-else + :items="emptyLessonPeriods" + :label="$t('alsijil.coursebook.choose_lesson_date')" + :item-text="getLessonText" + v-model="selectedLessonPeriodDatetime" + return-object + ></v-select> + </v-col> + <v-col cols="12" md="6" class="pt-0 pt-md-3"> + <v-btn + color="primary" + dark + v-bind="attrs" + v-on="on" + @click="createLessonDocumentation()" + > + {{ $t("alsijil.coursebook.create_documentation") }} + </v-btn> + </v-col> + </v-row> + </template> + <lesson-documentation + :lesson-documentation-edit="lessonDocumentationEdit" + :groups="groups" + :excuse-types="excuseTypes" + :extra-marks="extraMarks" + :save-lesson-documentations-per-week="saveLessonDocumentationsPerWeek" + :get-week-text="getWeekText" + @cancel-lesson-documentation-dialog="cancelDialog" + /> + </v-dialog> + <v-data-table + :headers="headers" + :items="computedLessonDocumentations" + @click:row="editLessonDocumentation" + class="elevation-1 my-3" + :expanded.sync="expanded" + show-expand + multi-sort + :sort-by="['year','week']" + :sort-desc="[true, true]" + > + <template v-slot:item.period="{ item }"> + <span class="text-no-wrap">{{ (saveLessonDocumentationsPerWeek === "True") ? getWeekText(item) : getLessonText(item) }}</span> + </template> + <template v-slot:expanded-item="{ headers, item }"> + <td :colspan="headers.length"> + <template v-if="saveLessonDocumentationsPerWeek === 'True'" v-for="lessonDocumentation in item.documentations"> + <v-list-item> + <v-list-item-content> + <v-list-item-title>{{ getLessonText(lessonDocumentation) }}</v-list-item-title> + <v-list-item-action> + <personal-notes + :lesson-documentation-id="lessonDocumentation.id" + :groups="groups" + :excuse-types="excuseTypes" + :extra-marks="extraMarks" + + v-model="lessonDocumentation.personalNotes" + @change="$emit('change-personal-notes', $event)" + ></personal-notes> + </v-list-item-action> + </v-list-item-content> + </v-list-item> + <v-divider></v-divider> + </template> + <template v-else v-for="personalNote in item.personalNotes"> +<!-- FIXME: Add edit and delete functionality to personal note chips--> + <v-chip class="ma-1" v-if="personalNoteString(personalNote)"> + {{ personalNote.person.fullName }}: {{ personalNoteString(personalNote) }} + </v-chip> + </template> + </td> + </template> +</v-data-table> +</div></template> + +<script> + import LessonDocumentation from "./LessonDocumentation.vue"; + import PersonalNotes from "./PersonalNotes.vue"; + export default { + components: {LessonDocumentation, PersonalNotes}, + props: [ "lessonDocumentations", "plannedLessonPeriodsDateTimes", "groups", "excuseTypes", "extraMarks", "saveLessonDocumentationsPerWeek" ], + name: "lesson-documentations", + data () { + return { + dialog: false, + expanded: [], + headers: [ + { text: this.$t("alsijil.period"), value: "period" }, + { text: this.$t("alsijil.lesson_documentation.topic"), value: "topic" }, + { text: this.$t("alsijil.lesson_documentation.homework"), value: "homework" }, + { text: this.$t("alsijil.lesson_documentation.group_note"), value: "groupNote" }, + { text: this.$t("alsijil.personal_note.title_plural"), value: "data-table-expand" } + ], + lessonDocumentationEdit: {}, + selectedLessonPeriodDatetime: {}, + recordedWeeks: [], + } + }, + computed: { + emptyLessonPeriods() { + if (this.saveLessonDocumentationsPerWeek === "True") { + let currentDatetime = new Date() + let weeks = {} + let lpdts = this.plannedLessonPeriodsDateTimes.filter(lp => new Date(lp.datetimeStart) > currentDatetime) + for (let ldIndex in lpdts) { + let ld = lpdts[ldIndex] + if (ld.week in weeks) { + weeks[ld.week]["planned"].push(ld) + } else { + weeks[ld.week] = { + "year": ld.year, + "week": ld.week, + "startDate": this.calculateStartDateOfCW(ld.year, ld.week), + "datetimeStart": ld.datetimeStart, + "lessonPeriod": ld.lessonPeriod, + "planned": [ld] + } + } + } + return Object.values(weeks) // FIXME sort by date + } else { + let currentDatetime = new Date() + return this.plannedLessonPeriodsDateTimes.filter(lp => new Date(lp.datetimeStart) > currentDatetime) + } + }, + computedLessonDocumentations() { + if (this.saveLessonDocumentationsPerWeek === "True") { + let weeks = {} + for (let ldIndex in this.lessonDocumentations) { + let ld = this.lessonDocumentations[ldIndex] + if (ld.week in weeks) { + weeks[ld.week]["documentations"].push(ld) + } else { + weeks[ld.week] = { + "id": ld.id, + "startDate": this.calculateStartDateOfCW(ld.year, ld.week), + "year": ld.year, + "week": ld.week, + "topic": ld.topic, + "homework": ld.homework, + "groupNote": ld.groupNote, + "documentations": [ld] + } + } + } + return Object.values(weeks) + } else { + return this.lessonDocumentations + } + } + }, + methods: { + cancelDialog() { + this.dialog = false; + this.lessonDocumentationEdit = {}; + }, + recordDocumentation(item) { + if (this.recordedWeeks.includes(item.week)) { + return false + } + this.recordedWeeks.push(item.week) + return true + }, + async loadLessonDocumentation(item) { + const result = await this.$apollo.mutate({ + mutation: require("./LessonDocumentation.graphql"), + variables: { + year: item.year, + week: item.week, + lessonPeriodId: item.lessonPeriod ? item.lessonPeriod.id : null, + eventId: item.event ? item.event.id : null, + extraLessonId: item.extraLesson ? item.extraLesson.id : null, + }, + }) + let lessonDocumentation = result.data.updateOrCreateLessonDocumentation.lessonDocumentation + this.lessonDocumentationEdit = { + id: lessonDocumentation.id, + year: item.year, + week: item.week, + date: lessonDocumentation.date, + period: item.period, + lessonPeriodId: item.lessonPeriod ? item.lessonPeriod.id : null, + eventId: item.event ? item.event.id : null, + extraLessonId: item.extraLesson ? item.extraLesson.id : null, + topic: lessonDocumentation.topic, + homework: lessonDocumentation.homework, + groupNote: lessonDocumentation.groupNote, + personalNotes: lessonDocumentation.personalNotes, + } + }, + + editLessonDocumentation(item) { + if (this.saveLessonDocumentationsPerWeek === "True") { + this.loadLessonDocumentation(item.documentations[0]) + } else { + this.loadLessonDocumentation(item) + } + this.dialog = true + }, + + createLessonDocumentation() { // FIXME: Update cache to show newly created LessonDocumentation in table + let lessonDocumentation = this.selectedLessonPeriodDatetime + lessonDocumentation["event"] = null + lessonDocumentation["extraLesson"] = null + this.loadLessonDocumentation(lessonDocumentation) + this.dialog = true + }, + + calculateStartDateOfCW(year, week){ + let ld_date = new Date(Date.UTC(year, 0, 1 + (week - 1) * 7)); + let dow = ld_date.getDay(); + let start_date = ld_date; + if (dow <= 4) + return start_date.setDate(ld_date.getDate() - ld_date.getDay() + 1) + else + return start_date.setDate(ld_date.getDate() + 8 - ld_date.getDay()) + }, + + getLessonText(item) { + let date_obj = new Date(item.hasOwnProperty("datetimeStart") ? item.datetimeStart : item.date) + let period = item.lessonPeriod ? ", " + this.$t('alsijil.period_number', {number: item.lessonPeriod.period.period}) : "" // FIXME: Cases without lessonPeriod + return this.$d(date_obj, "short") + period + }, + getWeekText(item) { + if (item.hasOwnProperty("startDate")) { + var start_date = new Date(item.startDate) + } else { + let lesson_date = new Date(item.date) + var start_date = new Date(((lesson_date.getDay() || 7) !== 1) ? lesson_date.setHours(-24 * (lesson_date.getDay() - 1)) : lesson_date) + } + let end_date = new Date(start_date) + end_date.setDate(end_date.getDate() + 6) + return start_date.toLocaleDateString(this.$root.languageCode) + " - " + end_date.toLocaleDateString(this.$root.languageCode) + ", " + this.$root.django.gettext('CW') + " " + item.week + }, + personalNoteString(personalNote) { + let personalNoteString = ""; + if (personalNote.tardiness > 0) { + personalNoteString += personalNote.tardiness + " min. "; + } + if (personalNote.absent) { + personalNoteString += this.$t("absent") + ", "; + } + if (personalNote.excused) { + personalNoteString += this.$t("excused") + ", "; + } + if (personalNote.excuseType) { + personalNoteString += personalNote.excuseType.name; + } + if (personalNote.extraMarks.length > 0) { + personalNoteString += " ("; + personalNote.extraMarks.forEach(item => { + personalNoteString += item.name + ", "; + }); + personalNoteString = personalNoteString.substring(0, personalNoteString.length - 2); + personalNoteString += ") "; + } + if (personalNote.remarks) { + personalNoteString += "\"" + personalNote.remarks + "\" "; + } + return personalNoteString; + }, + } + } +</script> diff --git a/aleksis/apps/alsijil/assets/components/coursebook/PersonalNotes.vue b/aleksis/apps/alsijil/assets/components/coursebook/PersonalNotes.vue new file mode 100644 index 0000000000000000000000000000000000000000..a7b8e3f5d304b32652793e90247a0bee28724725 --- /dev/null +++ b/aleksis/apps/alsijil/assets/components/coursebook/PersonalNotes.vue @@ -0,0 +1,359 @@ +<template> + <v-dialog + v-model="dialog" + max-width="600px" + @click:outside="cancelDialog" + > + <template v-slot:activator="{ on, attrs }"> + <div> + <template v-for="personalNote in personalNotes"> + <v-chip class="ma-1" close @click="editPersonalNote(personalNote.person.id)" + @click:close="removePersonalNote(personalNote.person.id)" v-if="personalNoteString(personalNote)"> + {{ personalNote.person.fullName }}: {{ personalNoteString(personalNote) }} + </v-chip> + </template> + </div> + <v-tooltip bottom> + <template v-slot:activator="{ on, attrs }"> + <v-btn + class="ma-1" + color="primary" + icon + outlined + v-bind="attrs" + v-on="on" + @click="createPersonalNote" + > + <v-icon> + mdi-plus + </v-icon> + </v-btn> + </template> + <span v-text="$t('alsijil.coursebook.add_personal_note')"></span> + </v-tooltip> + </template> + <v-card> + <v-card-title> + <span class="text-h5">Personal Note</span> + </v-card-title> + <v-card-text> + <v-container> + <v-select + item-text="fullName" + item-value="id" + :items="persons" + :label="$t('alsijil.personal_note.person')" + v-model="editedPersonID" + @input="updatePersonalNote" + ></v-select> + <v-text-field + :label="$t('alsijil.personal_note.tardiness')" + suffix="min" type="number" + min="0" + :disabled="editedPersonID === ID_NO_PERSON" + v-model="editedTardiness" + ></v-text-field> + <v-checkbox + :label="$t('alsijil.personal_note.absent')" + v-model="editedAbsent" + :disabled="editedPersonID === ID_NO_PERSON" + @change="editedExcused = false; editedExcuseType = null" + ></v-checkbox> + <v-checkbox + :label="$t('alsijil.personal_note.excused')" + v-model="editedExcused" + :disabled="editedPersonID === ID_NO_PERSON || !editedAbsent" + @change="editedExcuseType = null" + ></v-checkbox> + <v-select + :label="$t('alsijil.personal_note.excuse_type')" + v-model="editedExcuseType" + :items="excuseTypes" + item-text="name" + return-object + :disabled="editedPersonID === ID_NO_PERSON || !editedAbsent || !editedExcused" + ></v-select> + <v-select + :label="$t('alsijil.personal_note.extra_marks')" + v-model="editedExtraMarks" + :items="extraMarks" + item-text="name" + return-object + :disabled="editedPersonID === ID_NO_PERSON" + multiple + chips + ></v-select> + <v-text-field + :label="$t('alsijil.personal_note.remarks')" + v-model="editedRemarks" + :disabled="editedPersonID === ID_NO_PERSON" + ></v-text-field> + </v-container> + </v-card-text> + <v-card-actions> + <v-spacer></v-spacer> + <v-btn + color="error" + outlined + @click="cancelDialog" + > + {{ $t("alsijil.cancel") }} + </v-btn> + <v-btn + color="success" + @click="saveDialog" + :disabled="editedPersonID === ID_NO_PERSON" + > + {{ $t("alsijil.save") }} + </v-btn> + </v-card-actions> + </v-card> + </v-dialog> +</template> + +<script> +import gql from 'graphql-tag'; + +const ID_NO_PERSON = null; + +export default { + model: { + prop: "personalNotes", + event: "change", + }, + created() { + this.ID_NO_PERSON = ID_NO_PERSON; + }, + methods: { + removePersonalNote(personID) { + if (personID === ID_NO_PERSON) { + return + } + console.log("removing personal note of person", personID); + this.editedPersonID = personID; + this.editedTardiness = 0; + this.editedAbsent = false; + this.editedExcused = false; + this.editedExcuseType = null; + this.editedExtraMarks = []; + this.editedRemarks = ""; + + this.savePersonalNote(); + }, + editPersonalNote(personID) { + console.log("editing personal note of person", personID); + this.editedPersonID = personID; + this.updatePersonalNote(); + this.dialog = true; + }, + updatePersonalNote() { + let personalNote = this.personalNoteByStudentID(this.editedPersonID); + this.editedTardiness = personalNote.tardiness || 0; + this.editedAbsent = personalNote.absent || false; + this.editedExcused = personalNote.excused || false; + this.editedExcuseType = personalNote.excuseType || null; + this.editedExtraMarks = personalNote.extraMarks || []; + this.editedRemarks = personalNote.remarks || ""; + + this.newPersonalNote = !!(personalNote && Object.keys(personalNote).length === 0 && Object.getPrototypeOf(personalNote) === Object.prototype); + }, + createPersonalNote() { + this.editedPersonID = ID_NO_PERSON; + this.editedTardiness = 0; + this.editedAbsent = false; + this.editedExcused = false; + this.editedExcuseType = null; + this.editedExtraMarks = []; + this.editedRemarks = ""; + this.newPersonalNote = true; + this.dialog = true; + }, + personalNoteByStudentID(studentID) { + if (this.editedPersonID === ID_NO_PERSON) { + return {}; + } + return this.personalNotes.filter(item => item.person.id === studentID)[0] || {}; + }, + savePersonalNote() { + if (this.editedPersonID === ID_NO_PERSON) { + return + } + + let editedExcuseTypeID = (this.editedExcuseType) ? this.editedExcuseType.id : null; + let editedExtraMarksIDs = []; + this.editedExtraMarks.forEach(item => {editedExtraMarksIDs.push(item.id);}); + + // We save the user input in case of an error + const variables = { + "personId": this.editedPersonID, + "tardiness": this.editedTardiness, + "absent": this.editedAbsent, + "excused": this.editedExcused, + "excuseType": editedExcuseTypeID, + "extraMarks": editedExtraMarksIDs, + "remarks": this.editedRemarks, + "lessonDocumentation": this.lessonDocumentationId, + } + + console.log(variables) + + // Call to the graphql mutation + this.$apollo.mutate({ + // Query + mutation: gql`mutation updateOrCreatePersonalNote( + $personId: ID!, + $lessonDocumentation: ID!, + $tardiness: Int, + $absent: Boolean, + $excused: Boolean, + $excuseType: ID, + $extraMarks: [ID], + $remarks: String + ) { + updateOrCreatePersonalNote(personId: $personId, + lessonDocumentation: $lessonDocumentation, + tardiness: $tardiness, + absent: $absent, + excused: $excused, + excuseType: $excuseType, + extraMarks: $extraMarks, + remarks: $remarks + ) { + personalNote { + id + person { + id + fullName + } + tardiness + remarks + absent + excused + excuseType { + id + } + extraMarks { + id + } + } + } + } + `, + // Parameters + variables: variables, + }).then((data) => { + // Result + console.log(data) + // FIXME: check if data changed (?), display success message + }).catch((error) => { + // Error + console.error(error) + // FIXME: Notify the user about the error, maybe retry + }) + + if (this.newPersonalNote) { + this.personalNotes.push({ + person: { + id: this.editedPersonID, + fullName: this.studentNameByID(this.editedPersonID) + }, + tardiness: this.editedTardiness, + absent: this.editedAbsent, + excused: this.editedExcused, + excuseType: this.editedExcuseType, + extraMarks: this.editedExtraMarks, + remarks: this.editedRemarks, + }); + } else { + // Loop through all personal notes and update the ones that match the editedPersonID + this.personalNotes.forEach(item => { + if (item.person.id === this.editedPersonID) { + item.tardiness = this.editedTardiness; + item.absent = this.editedAbsent; + item.excused = this.editedExcused; + item.excuseType = this.editedExcuseType; + item.extraMarks = this.editedExtraMarks; + item.remarks = this.editedRemarks; + } + }); + } + this.$emit('change', this.personalNotes) + }, + cancelDialog() { + this.dialog = false; + this.editedPersonID = ID_NO_PERSON; + }, + saveDialog() { + this.savePersonalNote(); + this.dialog = false; + this.editedPersonID = ID_NO_PERSON; + }, + personalNoteString(personalNote) { + let personalNoteString = ""; + if (personalNote.tardiness > 0) { + personalNoteString += personalNote.tardiness + " min. "; + } + if (personalNote.absent) { + personalNoteString += $t("alsijil.absent") + " "; + } + if (personalNote.excused) { + personalNoteString += $t("alsijil.excused") + " "; + } + if (personalNote.excuseType) { + personalNoteString += personalNote.excuseType.name; + } + if (personalNote.extraMarks.length > 0) { + personalNoteString += " ("; + personalNote.extraMarks.forEach(item => { + personalNoteString += item.name + ", "; + }); + personalNoteString = personalNoteString.substring(0, personalNoteString.length - 2); + personalNoteString += ") "; + } + if (personalNote.remarks) { + personalNoteString += "\"" + personalNote.remarks + "\" "; + } + return personalNoteString; + }, + studentNameByID(studentID) { + try { + return this.persons.filter(item => item.id === studentID)[0].fullName; + } catch (TypeError) { + return ""; + } + } + }, + props: ["lessonDocumentationId", "personalNotes", "groups", "excuseTypes", "extraMarks"], + name: "personal-notes", + data: () => { + return { + dialog: false, + // Absent versp. exc. type hw note + editPersonalNoteId: null, + editedPersonID: ID_NO_PERSON, + editedTardiness: 0, + editedAbsent: false, + editedExcused: false, + editedExcuseType: null, + editedExtraMarks: [], + editedRemarks: "", + newPersonalNote: false, + } + }, + computed: { + persons() { + // go through each group and get the students + // use the group names as headers for the v-select + + return this.groups.map( + group => { + return [ + {header: group.name, id: group.shortName}, + group.members + ] + } + ).flat(2); + } + } +} +</script> diff --git a/aleksis/apps/alsijil/assets/components/coursebook/UpdateIndicator.vue b/aleksis/apps/alsijil/assets/components/coursebook/UpdateIndicator.vue new file mode 100644 index 0000000000000000000000000000000000000000..4ae2d9518d0be459cf71b80df738cbb97a5fc617 --- /dev/null +++ b/aleksis/apps/alsijil/assets/components/coursebook/UpdateIndicator.vue @@ -0,0 +1,80 @@ +<template> + <v-tooltip bottom> + <template v-slot:activator="{ on, attrs }"> + <v-btn + right + icon + + v-bind="attrs" + v-on="on" + + @click="() => {isAbleToClick ? $emit('manual-update') : null}" + :loading="status === UPDATING" + > + <v-icon + v-if="status !== UPDATING" + :color="color" + > + {{ icon }} + </v-icon> + </v-btn> + </template> + <span>{{ text }}</span> + </v-tooltip> +</template> + +<script> +import {CHANGES, ERROR, SAVED, UPDATING} from "../../UpdateStatuses.js"; + +export default { + created() { + this.ERROR = ERROR; + this.SAVED = SAVED; + this.UPDATING = UPDATING; + this.CHANGES = CHANGES; + }, + name: "update-indicator", + emits: ["manual-update"], + props: ["status"], + computed: { + text() { + switch (this.status) { + case SAVED: + return this.$t("alsijil.coursebook.sync.saved"); + case UPDATING: + return this.$t("alsijil.coursebook.sync.updating"); + case CHANGES: + return this.$t("alsijil.coursebook.sync.changes"); + default: + return this.$t("alsijil.coursebook.sync.error"); + } + }, + color() { + switch (this.status) { + case SAVED: + return "success"; + case CHANGES: + return "secondary"; + case UPDATING: + return "secondary"; + default: + return "error"; + } + }, + icon() { + // FIXME use app sdhasdhahsdhsadhsadh + switch (this.status) { + case SAVED: + return "mdi-check-circle-outline"; + case CHANGES: + return "mdi-dots-horizontal"; + default: + return "mdi-alert-outline"; + } + }, + isAbleToClick() { + return this.status === CHANGES || this.status === ERROR; + } + }, +} +</script> diff --git a/aleksis/apps/alsijil/assets/index.js b/aleksis/apps/alsijil/assets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..77d41966e5d3688accb0916b457b6be9ea390c73 --- /dev/null +++ b/aleksis/apps/alsijil/assets/index.js @@ -0,0 +1,3 @@ +export default [ + { path: "/coursebook/:lessonId", component: () => import("./components/coursebook/CourseBook.vue"), props: true }, +]; diff --git a/aleksis/apps/alsijil/assets/messages.json b/aleksis/apps/alsijil/assets/messages.json new file mode 100644 index 0000000000000000000000000000000000000000..ecf5f79e4ece66dbe7b7a35f21af2a773f944a44 --- /dev/null +++ b/aleksis/apps/alsijil/assets/messages.json @@ -0,0 +1,53 @@ +{ + "en": { + "alsijil": { + "coursebook": { + "title": "Coursebook", + "create_documentation": "Create documentation", + "choose_week": "Choose week", + "choose_lesson_date": "Choose lesson date", + "sync": { + "saved": "All changes are saved.", + "updating": "Changes are being synced.", + "changes": "You have unsaved changes. Click to save them immediately.", + "error": "There has been an error while saving the latest changes." + } + }, + "period": "Period", + "period_number": "{number}. period", + "lesson_documentation": { + "topic": "Topic", + "homework": "Homework", + "group_note": "Group note" + }, + "calendar_week": "Calendar week", + "calendar_week_short": "Week", + "personal_note": { + "title": "Personal Note", + "title_plural": "Personal Notes", + "absent_title": "Absent", + "excused_title": "Excused", + "absent": "absent", + "excused": "excused", + "person": "Person", + "tardiness": "Tardiness", + "excuse_type": "Excuse type", + "extra_marks": "Extra marks", + "remarks": "Remarks", + "actions": { + "add": "Add personal note" + } + }, + "error_occurred": "An error occurred", + "error_updating": "Error updating data", + "cancel": "Cancel", + "save": "Save", + "back": "Back" + } + }, + "de": { + "coursebook": { + "title": "Kursbuch" + } + } +} diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py index 4ef9b4e24e7315c75cd6106b72f3853410e9ef7b..3c5630cbfc1653db99107159e1b8834507a27a6d 100644 --- a/aleksis/apps/alsijil/forms.py +++ b/aleksis/apps/alsijil/forms.py @@ -47,7 +47,7 @@ class LessonDocumentationForm(forms.ModelForm): self.fields["homework"].label = _("Homework for the next lesson") if ( self.instance.lesson_period - and get_site_preferences()["alsijil__allow_carry_over_same_week"] + and get_site_preferences()["alsijil__save_lesson_documentations_by_week"] ): self.fields["carry_over_week"] = forms.BooleanField( label=_("Carry over data to all other lessons with the same subject in this week"), @@ -58,7 +58,7 @@ class LessonDocumentationForm(forms.ModelForm): def save(self, **kwargs): lesson_documentation = super(LessonDocumentationForm, self).save(commit=True) if ( - get_site_preferences()["alsijil__allow_carry_over_same_week"] + get_site_preferences()["alsijil__save_lesson_documentations_by_week"] and self.cleaned_data["carry_over_week"] and ( lesson_documentation.topic @@ -68,7 +68,7 @@ class LessonDocumentationForm(forms.ModelForm): and lesson_documentation.lesson_period ): lesson_documentation.carry_over_data( - LessonPeriod.objects.filter(lesson=lesson_documentation.lesson_period.lesson) + LessonPeriod.objects.filter(lesson=lesson_documentation.lesson_period.lesson), True ) diff --git a/aleksis/apps/alsijil/menus.py b/aleksis/apps/alsijil/menus.py index fcf14e7cc8f8ea51eabee00abb5a75c61de42af8..bd351495f7e5aa4159f47ac50e5df40ed17698d1 100644 --- a/aleksis/apps/alsijil/menus.py +++ b/aleksis/apps/alsijil/menus.py @@ -6,16 +6,30 @@ MENUS = { "name": _("Class register"), "url": "#", "svg_icon": "mdi:book-open-outline", + "vuetify_icon": "mdi-book-open-outline", "root": True, "validators": [ "menu_generator.validators.is_authenticated", "aleksis.core.util.core_helpers.has_person", ], "submenu": [ + { + "name": _("Coursebook"), + "url": "select_coursebook", + "svg_icon": "mdi:book-education-outline", + "vuetify_icon": "mdi-book-education-outline", + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "alsijil.view_coursebook_rule", + ), + ], + }, { "name": _("Current lesson"), "url": "lesson_period", "svg_icon": "mdi:alarm", + "vuetify_icon": "mdi-alarm", "validators": [ ( "aleksis.core.util.predicates.permission_validator", @@ -27,6 +41,7 @@ MENUS = { "name": _("Current week"), "url": "week_view", "svg_icon": "mdi:view-week-outline", + "vuetify_icon": "mdi-view-week-outline", "validators": [ ( "aleksis.core.util.predicates.permission_validator", @@ -38,6 +53,7 @@ MENUS = { "name": _("My groups"), "url": "my_groups", "svg_icon": "mdi:account-multiple-outline", + "vuetify_icon": "mdi-account-multiple-outline", "validators": [ ( "aleksis.core.util.predicates.permission_validator", @@ -49,6 +65,7 @@ MENUS = { "name": _("My overview"), "url": "overview_me", "svg_icon": "mdi:chart-box-outline", + "vuetify_icon": "mdi-chart-box-outline", "validators": [ ( "aleksis.core.util.predicates.permission_validator", @@ -60,6 +77,7 @@ MENUS = { "name": _("My students"), "url": "my_students", "svg_icon": "mdi:account-school-outline", + "vuetify_icon": "mdi-account-school-outline", "validators": [ ( "aleksis.core.util.predicates.permission_validator", @@ -71,6 +89,7 @@ MENUS = { "name": _("Assign group role"), "url": "assign_group_role_multiple", "svg_icon": "mdi:clipboard-account-outline", + "vuetify_icon": "mdi-clipboard-account-outline", "validators": [ ( "aleksis.core.util.predicates.permission_validator", @@ -82,6 +101,7 @@ MENUS = { "name": _("All lessons"), "url": "all_register_objects", "svg_icon": "mdi:format-list-text", + "vuetify_icon": "mdi-format-list-text", "validators": [ ( "aleksis.core.util.predicates.permission_validator", @@ -92,7 +112,8 @@ MENUS = { { "name": _("Register absence"), "url": "register_absence", - "icon": "rate_review", + "svg_icon": "mdi:message-draw", + "vuetify_icon": "mdi-message-draw", "validators": [ ( "aleksis.core.util.predicates.permission_validator", @@ -104,6 +125,7 @@ MENUS = { "name": _("Excuse types"), "url": "excuse_types", "svg_icon": "mdi:label-outline", + "vuetify_icon": "mdi-label-outline", "validators": [ ( "aleksis.core.util.predicates.permission_validator", @@ -115,6 +137,7 @@ MENUS = { "name": _("Extra marks"), "url": "extra_marks", "svg_icon": "mdi:label-variant-outline", + "vuetify_icon": "mdi-label-variant-outline", "validators": [ ( "aleksis.core.util.predicates.permission_validator", @@ -126,6 +149,7 @@ MENUS = { "name": _("Manage group roles"), "url": "group_roles", "svg_icon": "mdi:clipboard-plus-outline", + "vuetify_icon": "mdi-clipboard-plus-outline", "validators": [ ( "aleksis.core.util.predicates.permission_validator", diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py index 2c5420200e3dd9a6c60ad562e495d198be4a8b39..60504010d83ddcedb7a73646a28a2eb421e2fa62 100644 --- a/aleksis/apps/alsijil/model_extensions.py +++ b/aleksis/apps/alsijil/model_extensions.py @@ -461,7 +461,9 @@ def generate_person_list_with_class_register_statistics( ), tardiness=Sum("filtered_personal_notes__tardiness"), tardiness_count=Count( - "filtered_personal_notes", filter=Q(filtered_personal_notes__tardiness__gt=0), distinct=True + "filtered_personal_notes", + filter=Q(filtered_personal_notes__tardiness__gt=0), + distinct=True, ), ) diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index b504a294164a6388099148fda5149091767577a7..2cd7b24c64ef7314a47557158e7c8af09d1abf6f 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -347,11 +347,13 @@ class LessonDocumentation(RegisterObjectRelatedMixin, ExtensibleModel): homework = models.CharField(verbose_name=_("Homework"), max_length=200, blank=True) group_note = models.CharField(verbose_name=_("Group note"), max_length=200, blank=True) - def carry_over_data(self, all_periods_of_lesson: LessonPeriod): - """Carry over data to given periods in this lesson if data is not already set. + def carry_over_data(self, all_periods_of_lesson: LessonPeriod, force: bool): + """Carry over data to given periods in this lesson. + + Does overwrite existing data in case ``force`` is set to ``True``. Both forms of carrying over data can be deactivated using site preferences - ``alsijil__carry_over_next_periods`` and ``alsijil__allow_carry_over_same_week`` + ``alsijil__carry_over_next_periods`` and ``alsijil__save_lesson_documentations_by_week`` respectively. """ for period in all_periods_of_lesson: @@ -361,15 +363,15 @@ class LessonDocumentation(RegisterObjectRelatedMixin, ExtensibleModel): changed = False - if not lesson_documentation.topic: + if not lesson_documentation.topic or force: lesson_documentation.topic = self.topic changed = True - if not lesson_documentation.homework: + if not lesson_documentation.homework or force: lesson_documentation.homework = self.homework changed = True - if not lesson_documentation.group_note: + if not lesson_documentation.group_note or force: lesson_documentation.group_note = self.group_note changed = True @@ -390,7 +392,8 @@ class LessonDocumentation(RegisterObjectRelatedMixin, ExtensibleModel): LessonPeriod.objects.filter( lesson=self.lesson_period.lesson, period__weekday=self.lesson_period.period.weekday, - ) + ), + False, ) super().save(*args, **kwargs) diff --git a/aleksis/apps/alsijil/preferences.py b/aleksis/apps/alsijil/preferences.py index 2fd34fa7fc4f802a52ead1d2d0291789be4fb9ce..014435f1b3072ab8804e2e57d1fdb6c7d135bca7 100644 --- a/aleksis/apps/alsijil/preferences.py +++ b/aleksis/apps/alsijil/preferences.py @@ -67,17 +67,11 @@ class CarryOverDataToNextPeriods(BooleanPreference): @site_preferences_registry.register -class AllowCarryOverLessonDocumentationToCurrentWeek(BooleanPreference): +class SaveLessonDocumentationsPerWeek(BooleanPreference): section = alsijil - name = "allow_carry_over_same_week" + name = "save_lesson_documentations_by_week" default = False - verbose_name = _( - "Allow carrying over data from any lesson period to all other lesson \ - periods with the same lesson and in the same week" - ) - help_text = _( - "This will carry over data only if the data in the aforementioned periods are empty." - ) + verbose_name = _("Save lesson documentations per week instead of per lesson period") @site_preferences_registry.register diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py index e9011c6c2b33ed58a21b6412f6c7e366b1b5e49e..83af497c900ea9ed54df5d3f8e4f7b63828d2ebe 100644 --- a/aleksis/apps/alsijil/rules.py +++ b/aleksis/apps/alsijil/rules.py @@ -204,6 +204,11 @@ view_students_list_predicate = view_my_groups_predicate & ( ) add_perm("alsijil.view_students_list_rule", view_students_list_predicate) +# View CourseBook +view_coursebook_predicate = has_person & is_teacher +add_perm("alsijil.view_coursebook_rule", view_my_students_predicate) + + # View person overview view_person_overview_predicate = has_person & ( (is_current_person & is_site_preference_set("alsijil", "view_own_personal_notes")) diff --git a/aleksis/apps/alsijil/schema.py b/aleksis/apps/alsijil/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..d7f746170a13985dd7f5b012f71017dfb50c303e --- /dev/null +++ b/aleksis/apps/alsijil/schema.py @@ -0,0 +1,249 @@ +from datetime import datetime + +import graphene +from graphene_django import DjangoObjectType + +from aleksis.apps.chronos.models import Lesson +from aleksis.core.models import Group, Person +from aleksis.core.util.core_helpers import get_site_preferences + +from .models import ( + Event, + ExcuseType, + ExtraLesson, + ExtraMark, + LessonDocumentation, + LessonPeriod, + PersonalNote, +) + + +class ExcuseTypeType(DjangoObjectType): + class Meta: + model = ExcuseType + + +class PersonalNoteType(DjangoObjectType): + class Meta: + model = PersonalNote + + +class LessonDocumentationType(DjangoObjectType): + class Meta: + model = LessonDocumentation + + personal_notes = graphene.List(PersonalNoteType) + date = graphene.Field(graphene.Date) + period = graphene.Field(graphene.Int) + + def resolve_personal_notes(root: LessonDocumentation, info, **kwargs): + persons = Person.objects.filter( + member_of__in=Group.objects.filter(pk__in=root.register_object.get_groups().all()) + ) + return PersonalNote.objects.filter( + week=root.week, + year=root.year, + lesson_period=root.lesson_period, + person__in=persons, + ) + + def resolve_period(root: LessonDocumentation, info, **kwargs): + return root.period.period + + def resolve_date(root: LessonDocumentation, info, **kwargs): + return root.date + + +class ExtraMarkType(DjangoObjectType): + class Meta: + model = ExtraMark + + +class LessonDocumentationMutation(graphene.Mutation): + class Arguments: + year = graphene.Int(required=True) + week = graphene.Int(required=True) + + lesson_period_id = graphene.ID(required=False) + event_id = graphene.ID(required=False) + extra_lesson_id = graphene.ID(required=False) + + lesson_documentation_id = graphene.ID(required=False) + + topic = graphene.String(required=False) + homework = graphene.String(required=False) + group_note = graphene.String(required=False) + + lesson_documentation = graphene.Field(LessonDocumentationType) + + @classmethod + def mutate( + cls, + root, + info, + year, + week, + lesson_period_id=None, + event_id=None, + extra_lesson_id=None, + lesson_documentation_id=None, + topic=None, + homework=None, + group_note=None, + ): + + lesson_period = LessonPeriod.objects.filter(pk=lesson_period_id).first() + event = Event.objects.filter(pk=event_id).first() + extra_lesson = ExtraLesson.objects.filter(pk=extra_lesson_id).first() + + lesson_documentation, created = LessonDocumentation.objects.get_or_create( + year=year, + week=week, + lesson_period=lesson_period, + event=event, + extra_lesson=extra_lesson, + ) + + if topic is not None: + lesson_documentation.topic = topic + if homework is not None: + lesson_documentation.homework = homework + if group_note is not None: + lesson_documentation.group_note = group_note + + lesson_documentation.save() + + if ( + get_site_preferences()["alsijil__save_lesson_documentations_by_week"] + and ( + lesson_documentation.topic + or lesson_documentation.homework + or lesson_documentation.group_note + ) + and lesson_documentation.lesson_period + ): + lesson_documentation.carry_over_data( + LessonPeriod.objects.filter(lesson=lesson_documentation.lesson_period.lesson), True + ) + + return LessonDocumentationMutation(lesson_documentation=lesson_documentation) + + +class PersonalNoteMutation(graphene.Mutation): + class Arguments: + person_id = graphene.ID(required=True) + lesson_documentation = graphene.ID(required=True) + + personal_note_id = graphene.ID(required=False) # Update or create personal note + + late = graphene.Int(required=False) + absent = graphene.Boolean(required=False) + excused = graphene.Boolean(required=False) + excuse_type = graphene.ID(required=False) + remarks = graphene.String(required=False) + extra_marks = graphene.List(graphene.ID, required=False) + + personal_note = graphene.Field(PersonalNoteType) + + @classmethod + def mutate( + cls, + root, + info, + person_id, + lesson_documentation, + personal_note_id=None, + late=None, + absent=None, + excused=None, + excuse_type=None, + remarks=None, + extra_marks=None, + ): + person = Person.objects.get(pk=person_id) + lesson_documentation = LessonDocumentation.objects.get(pk=lesson_documentation) + + personal_note, created = PersonalNote.objects.get_or_create( + person=person, + event=lesson_documentation.event, + extra_lesson=lesson_documentation.extra_lesson, + lesson_period=lesson_documentation.lesson_period, + week=lesson_documentation.week, + year=lesson_documentation.year, + ) + if late is not None: + personal_note.late = late + if absent is not None: + personal_note.absent = absent + if excused is not None: + personal_note.excused = excused + if excuse_type is not None: + personal_note.excuse_type = ExcuseType.objects.get(pk=excuse_type) + if remarks is not None: + personal_note.remarks = remarks + + if created: + personal_note.groups_of_person.set(person.member_of.all()) + + personal_note.save() + + if extra_marks is not None: + extra_marks = ExtraMark.objects.filter(pk__in=extra_marks) + personal_note.extra_marks.set(extra_marks) + personal_note.save() + return PersonalNoteMutation(personal_note=personal_note) + + +class Mutation(graphene.ObjectType): + update_or_create_lesson_documentation = LessonDocumentationMutation.Field() + update_or_create_personal_note = PersonalNoteMutation.Field() + # update_personal_note = PersonalNoteMutation.Field() + + +class Query(graphene.ObjectType): + excuse_types = graphene.List(ExcuseTypeType) + lesson_documentations = graphene.List(LessonDocumentationType) + lesson_documentation_by_id = graphene.Field(LessonDocumentationType, id=graphene.ID()) + lesson_documentations_by_lesson_id = graphene.List(LessonDocumentationType, id=graphene.ID()) + personal_notes = graphene.List(PersonalNoteType) + extra_marks = graphene.List(ExtraMarkType) + + def resolve_excuse_types(root, info, **kwargs): + # FIXME do permission stuff + return ExcuseType.objects.all() + + def resolve_lesson_documentations(root, info, **kwargs): + # FIXME do permission stuff + return LessonDocumentation.objects.all().order_by( + "-year", "-week", "-lesson_period__period__weekday", "-lesson_period__period__period" + ) + + def resolve_lesson_documentation_by_id(root, info, id, **kwargs): # noqa + return LessonDocumentation.objects.get(id=id) + + def resolve_lesson_documentations_by_lesson_id(root, info, id, **kwargs): # noqa + lesson = Lesson.objects.get(id=id) + now = datetime.now() + for equal_lesson in lesson._equal_lessons: + for planned in equal_lesson.planned_lessonperiods_datetimes: + if planned["datetime_start"] <= now: + LessonDocumentation.objects.get_or_create( + week=planned["week"], + year=planned["year"], + lesson_period=planned["lesson_period"], + ) # FIXME: Queries shouldn't alter data + + return LessonDocumentation.objects.filter( + lesson_period_id__in=LessonPeriod.objects.filter( + lesson__in=lesson._equal_lessons + ).values_list("id", flat=True) + ).order_by( + "-year", "-week", "-lesson_period__period__weekday", "-lesson_period__period__period" + ) + + def resolve_personal_notes(root, info, **kwargs): + # FIXME do permission stuff + return PersonalNote.objects.all() + + def resolve_extra_marks(root, info, **kwargs): + return ExtraMark.objects.all() diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/coursebook.html b/aleksis/apps/alsijil/templates/alsijil/class_register/coursebook.html new file mode 100644 index 0000000000000000000000000000000000000000..80378c66a06cbac1951cbf38f1d6d49bea896b49 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/coursebook.html @@ -0,0 +1,16 @@ +{% extends "core/vue_base.html" %} +{% load static i18n %} +{% load render_bundle from webpack_loader %} + +{% block page_title %} + {% trans "Coursebook" %} +{% endblock %} +{% block browser_title %}{% trans "Coursebook" %} {{ lesson }}{% endblock %} +{% block content %} +<div class="text-h5">{{ lesson }}</div> +<router-view save-lesson-documentations-per-week={{ SITE_PREFERENCES.alsijil__save_lesson_documentations_by_week }} /> +{% endblock %} + +{% block extra_body %} + {% render_bundle "aleksis.apps.alsijil" %} +{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/select_coursebook.html b/aleksis/apps/alsijil/templates/alsijil/class_register/select_coursebook.html new file mode 100644 index 0000000000000000000000000000000000000000..57114df23ebd2e1631f56596fd89c220740fdad8 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/select_coursebook.html @@ -0,0 +1,53 @@ +{% extends "core/vue_base.html" %} +{% load static i18n %} +{% load render_bundle from webpack_loader %} + +{% block page_title %}{% trans "Select Coursebook" %}{% endblock %} +{% block browser_title %}{% trans "Select Coursebook" %}{% endblock %} +{% block content %} + <v-row> + {% for lesson in lessons %} + <v-col xs="12" sm="6" md="6" lg="4" xl="3" class="d-flex"> + <v-card class="flex-grow-1"> + <v-card-title> + {% for group in lesson.groups.all %}{{ group.short_name }}{% if not forloop.last %},{% endif %}{% endfor %} + · {{ lesson.subject.name }} + </v-card-title> + <v-card-subtitle> + {{ lesson.validity.date_start }}-{{ lesson.validity.date_end }} + </v-card-subtitle> + <v-card-text> + {{ lesson.teachers.all|join:"," }} + </v-card-text> +{# <v-spacer></v-spacer>#} + <v-card-actions> + <v-btn :href="urls.coursebook({{ lesson.pk }})" text color="secondary"> + <v-icon left>mdi-book-search-outline</v-icon> + {% trans "Open in coursebook" %} + </v-btn> + </v-card-actions> + </v-card> + </v-col> + {% empty %} + <v-container + class="text-center fill-height" + style="height: calc(100vh - 58px);" + > + <v-row align="center"> + <v-col> + <h1 class="text-h3 primary--text"> + <v-icon color="error" x-large>mdi-book-off-outline</v-icon> + {% trans "No Coursebook" %} + </h1> + + <p>{% trans "There are no courses where you are a teacher." %} </p> + </v-col> + </v-row> + </v-container> + {% endfor %} + </v-row> +{% endblock %} + +{% block extra_body %} + {% render_bundle "aleksis.apps.alsijil" %} +{% endblock %} diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py index b06aebd352774727bb6bddca9dec8a0e28846a69..2cbc63fbbbe9932e3f7fb6e312980971a1fc20dc 100644 --- a/aleksis/apps/alsijil/urls.py +++ b/aleksis/apps/alsijil/urls.py @@ -3,6 +3,8 @@ from django.urls import path from . import views urlpatterns = [ + path("coursebook/", views.SelectCoursebookView.as_view(), name="select_coursebook"), + path("coursebook/<int:pk>/", views.CoursebookView.as_view(), name="coursebook"), path("lesson", views.register_object, {"model": "lesson"}, name="lesson_period"), path( "lesson/<int:year>/<int:week>/<int:id_>", diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index 487a27d3b51d1ced83275403e1537a84a7925b4f..55fc21792f9500fc7d9de0c92dc0d38244a4337f 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -17,7 +17,7 @@ from django.utils.http import url_has_allowed_host_and_scheme from django.utils.translation import gettext as _ from django.views import View from django.views.decorators.cache import never_cache -from django.views.generic import DetailView +from django.views.generic import DetailView, TemplateView import reversion from calendarweek import CalendarWeek @@ -28,9 +28,20 @@ from reversion.views import RevisionMixin from rules.contrib.views import PermissionRequiredMixin, permission_required from aleksis.apps.chronos.managers import TimetableType -from aleksis.apps.chronos.models import Event, ExtraLesson, Holiday, LessonPeriod, TimePeriod +from aleksis.apps.chronos.models import ( + Event, + ExtraLesson, + Holiday, + Lesson, + LessonPeriod, + TimePeriod, +) from aleksis.apps.chronos.util.build import build_weekdays -from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date +from aleksis.apps.chronos.util.date import ( + get_current_year, + get_weeks_for_year, + week_weekday_to_date, +) from aleksis.core.mixins import ( AdvancedCreateView, AdvancedDeleteView, @@ -1349,3 +1360,54 @@ class AllRegisterObjectsView(PermissionRequiredMixin, View): if self.action_form.is_valid(): self.action_form.execute() return render(request, "alsijil/class_register/all_objects.html", context) + + +class CoursebookView(PermissionRequiredMixin, DetailView): + model = Lesson + template_name = "alsijil/class_register/coursebook.html" + permission_required = "alsijil.view_coursebook_rule" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + return context + + +class SelectCoursebookView(PermissionRequiredMixin, TemplateView): + template_name = "alsijil/class_register/select_coursebook.html" + permission_required = "alsijil.view_coursebook_rule" # FIXME + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + person = self.request.user.person + + current_week = CalendarWeek.current_week() + current_year = get_current_year() + # Show all future and the ones of last week + + last_week, last_week_year = ( + (current_week - 1, current_year) + if current_week >= 2 + else (CalendarWeek.get_last_week_of_year(current_year - 1).week, current_year - 1) + ) + + last_week_query = Q( + lesson_periods__substitutions__week=last_week, + lesson_periods__substitutions__year=last_week_year, + ) + this_week_query = Q( + lesson_periods__substitutions__week__gte=current_week, + lesson_periods__substitutions__year=current_year, + ) + next_year_query = Q(lesson_periods__substitutions__year__gt=current_year) + context["lessons"] = ( + Lesson.objects.filter( + Q(teachers=person) + | ( + Q(lesson_periods__substitutions__teachers=person) + & (last_week_query | this_week_query | next_year_query) + ) + ) + .for_current_or_all() + .distinct() + ) + return context diff --git a/docs/conf.py b/docs/conf.py index 59ca213449c1f46b86ef5340aaac7c21447cb4e8..e4b22f267c295959eede8facc6a4d3a2cf26f9d2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,9 +29,9 @@ copyright = "2019-2022 The AlekSIS team" author = "The AlekSIS Team" # The short X.Y version -version = "2.1" +version = "3.0" # The full version, including alpha/beta/rc tags -release = "2.2.dev0" +release = "3.0.dev1" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index a3a4aea8d9b8f3cfd286cab366900bcce9ee5b9c..70cac0640e8dfb2b045ae566d77fbf23d78d5f0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "AlekSIS-App-Alsijil" -version = "2.2.dev0" +version = "3.0.dev1" packages = [ { include = "aleksis" } ] @@ -22,7 +22,8 @@ authors = [ "Hangzhi Yu <yuha@katharineum.de>", "Lloyd Meins <meinsll@katharineum.de>", "mirabilos <thorsten.glaser@teckids.org>", - "Tom Teichler <tom.teichler@teckids.org>" + "Tom Teichler <tom.teichler@teckids.org>", + "magicfelix <felix@felix-zauberer.de>" ] maintainers = [ "Dominik George <dominik.george@teckids.org>", @@ -48,9 +49,9 @@ secondary = true [tool.poetry.dependencies] python = "^3.9" -aleksis-core = "^2.12" -aleksis-app-chronos = "^2.2" -aleksis-app-stoelindeling = { version = "^1.0", optional = true } +aleksis-core = "^2.12.2" +aleksis-app-chronos = "^3.0.0.dev0" +aleksis-app-stoelindeling = { version = "^2.0.0.dev0", optional = true } [tool.poetry.dev-dependencies] aleksis-builddeps = "*"