Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found

Target

Select target project
  • AlekSIS/official/AlekSIS-App-Alsijil
  • sunweaver/AlekSIS-App-Alsijil
  • 8tincsoVluke/AlekSIS-App-Alsijil
  • perfreicpo/AlekSIS-App-Alsijil
  • noifobarep/AlekSIS-App-Alsijil
  • 7ingannisdo/AlekSIS-App-Alsijil
  • unmruntartpa/AlekSIS-App-Alsijil
  • balrorebta/AlekSIS-App-Alsijil
  • comliFdifwa/AlekSIS-App-Alsijil
  • 3ranaadza/AlekSIS-App-Alsijil
10 results
Show changes
Commits on Source (122)
Showing
with 1456 additions and 21 deletions
......@@ -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).
......
......@@ -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"),
)
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
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
}
}
<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>
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
}
}
}
}
}
<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>
<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>
<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>
<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>
export default [
{ path: "/coursebook/:lessonId", component: () => import("./components/coursebook/CourseBook.vue"), props: true },
];
{
"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"
}
}
}
......@@ -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
)
......
......@@ -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",
......
......@@ -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__late__gt=0), distinct=True
"filtered_personal_notes",
filter=Q(filtered_personal_notes__tardiness__gt=0),
distinct=True,
),
)
......
......@@ -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)
......
......@@ -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
......
......@@ -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"))
......
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()
{% 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 %}