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-Chronos
  • sunweaver/AlekSIS-App-Chronos
  • sggua/AlekSIS-App-Chronos
  • tincmeKdenka/AlekSIS-App-Chronos
  • ligquamacti/AlekSIS-App-Chronos
  • 1crotatilhe/AlekSIS-App-Chronos
  • 1compluningi/AlekSIS-App-Chronos
  • starwardcarfi/AlekSIS-App-Chronos
  • ceohecholeg/AlekSIS-App-Chronos
  • 7quecontranchi/AlekSIS-App-Chronos
  • 8evsubcesza/AlekSIS-App-Chronos
  • unscinKibdzu/AlekSIS-App-Chronos
  • delucPchondmu/AlekSIS-App-Chronos
13 results
Show changes
Commits on Source (76)
Showing
with 1233 additions and 264 deletions
<template>
<v-card-actions v-if="checkPermission('chronos.edit_substitution_rule')">
<edit-button
i18n-key="chronos.event.amend.edit_button"
@click="edit = true"
/>
<delete-button
v-if="selectedEvent.meta.amended"
i18n-key="chronos.event.amend.delete_button"
@click="deleteEvent = true"
/>
<dialog-object-form
v-model="edit"
:fields="fields"
:is-create="!selectedEvent.meta.amended"
createItemI18nKey="chronos.event.amend.title"
:gql-create-mutation="gqlCreateMutation"
:get-create-data="transformCreateData"
:default-item="defaultItem"
editItemI18nKey="chronos.event.amend.title"
:gql-patch-mutation="gqlPatchMutation"
:get-patch-data="transformPatchData"
:edit-item="initPatchData"
@cancel="open = false"
@save="updateOnSave()"
>
<template #subject.field="{ attrs, on, item }">
<v-autocomplete
:disabled="item.cancelled"
:items="amendableSubjects"
item-text="name"
item-value="id"
v-bind="attrs"
v-on="on"
/>
</template>
<template #teachers.field="{ attrs, on, item }">
<v-autocomplete
:disabled="item.cancelled"
multiple
:items="amendableTeachers"
item-text="fullName"
item-value="id"
v-bind="attrs"
v-on="on"
chips
deletable-chips
/>
</template>
<template #rooms.field="{ attrs, on, item }">
<v-autocomplete
:disabled="item.cancelled"
multiple
:items="amendableRooms"
item-text="name"
item-value="id"
v-bind="attrs"
v-on="on"
chips
deletable-chips
/>
</template>
<template #cancelled.field="{ attrs, on }">
<v-checkbox v-bind="attrs" v-on="on" />
</template>
<template #comment.field="{ attrs, on }">
<v-textarea v-bind="attrs" v-on="on" />
</template>
</dialog-object-form>
<delete-dialog
deleteSuccessMessageI18nKey="chronos.event.amend.delete_success"
:gql-mutation="gqlDeleteMutation"
v-model="deleteEvent"
:item="selectedEvent.meta"
@success="updateOnSave()"
>
<template #title>
{{ $t("chronos.event.amend.delete_dialog") }}
</template>
</delete-dialog>
</v-card-actions>
</template>
<script>
import permissionsMixin from "aleksis.core/mixins/permissions.js";
import EditButton from "aleksis.core/components/generic/buttons/EditButton.vue";
import DialogObjectForm from "aleksis.core/components/generic/dialogs/DialogObjectForm.vue";
import DeleteButton from "aleksis.core/components/generic/buttons/DeleteButton.vue";
import DeleteDialog from "aleksis.core/components/generic/dialogs/DeleteDialog.vue";
import {
gqlSubjects,
gqlPersons,
gqlRooms,
createAmendLesson,
patchAmendLesson,
deleteAmendLesson,
} from "./amendLesson.graphql";
export default {
name: "AmendLesson",
components: {
EditButton,
DialogObjectForm,
DeleteButton,
DeleteDialog,
},
mixins: [permissionsMixin],
props: {
selectedEvent: {
type: Object,
required: true,
}
},
data() {
return {
edit: false,
fields: [
{
text: this.$t("chronos.event.amend.subject"),
value: "subject",
},
{
text: this.$t("chronos.event.amend.teachers"),
value: "teachers",
},
{
text: this.$t("chronos.event.amend.rooms"),
value: "rooms",
},
{
text: this.$t("chronos.event.amend.cancelled"),
value: "cancelled",
},
{
text: this.$t("chronos.event.amend.comment"),
value: "comment",
},
],
defaultItem: {
cancelled: this.selectedEvent.meta.cancelled,
comment: this.selectedEvent.meta.comment,
},
gqlCreateMutation: createAmendLesson,
gqlPatchMutation: patchAmendLesson,
deleteEvent: false,
gqlDeleteMutation: deleteAmendLesson,
};
},
methods: {
transformCreateData(item) {
return {
...item,
amends: this.selectedEvent.meta.id,
datetimeStart: this.selectedEvent.startDateTime.toUTC().toISO(),
datetimeEnd: this.selectedEvent.endDateTime.toUTC().toISO(),
};
},
transformPatchData(item) {
let { id, __typename, cancelled, ...patchItem } = item;
return {
...patchItem,
// Normalize cancelled, v-checkbox returns null & does not
// honor false-value.
cancelled: cancelled ? true : false,
};
},
updateOnSave() {
this.$emit('refreshCalendar');
this.model = false;
},
},
computed: {
initPatchData() {
return {
id: this.selectedEvent.meta.id,
subject: this.selectedEvent.meta.subject?.id.toString(),
teachers: this.selectedEvent.meta.teachers.map((teacher) => teacher.id.toString()),
rooms: this.selectedEvent.meta.rooms.map((room) => room.id.toString()),
cancelled: this.selectedEvent.meta.cancelled,
comment: this.selectedEvent.meta.comment,
};
},
},
apollo: {
amendableSubjects: gqlSubjects,
amendableTeachers: gqlPersons,
amendableRooms: gqlRooms,
},
mounted() {
this.addPermissions(["chronos.edit_substitution_rule"]);
},
};
</script>
<script>
export default {
name: "LessonEventLinkIterator",
props: {
items: {
type: Array,
required: true,
},
attr: {
type: String,
required: false,
default: "name",
},
},
};
</script>
<template>
<span v-bind="$attrs">
<span v-for="(item, idx) in items" :key="idx">
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
{{ item[attr] }}{{ idx + 1 < items.length ? "," : "" }}
</span>
</span>
</template>
<script>
import LessonEventLinkIterator from "./LessonEventLinkIterator.vue";
export default {
name: "LessonEventOldNew",
components: { LessonEventLinkIterator },
props: {
oldItems: {
type: Array,
required: true,
},
newItems: {
type: Array,
required: true,
},
attr: {
type: String,
required: false,
default: "name",
},
},
};
</script>
<template>
<span v-bind="$attrs">
<span v-if="oldItems.length > 0 && newItems.length > 0">
<span class="text-decoration-line-through"
><lesson-event-link-iterator :items="oldItems" :attr="attr"
/></span>
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
<span></span>
<lesson-event-link-iterator :items="newItems" :attr="attr" />
</span>
<span v-else-if="newItems.length > 0">
<lesson-event-link-iterator :items="newItems" :attr="attr" />
</span>
<span v-else>
<lesson-event-link-iterator :items="oldItems" :attr="attr" />
</span>
</span>
</template>
<script>
export default {
name: "LessonEventSubject",
props: {
event: {
type: Object,
required: true,
},
attr: {
type: String,
required: false,
default: "name",
},
},
};
</script>
<template>
<span v-bind="$attrs">
<span
v-if="
event.meta.subject && event.meta.amended && event.meta.amends.subject
"
>
<span class="text-decoration-line-through">
{{ event.meta.amends.subject[attr] }}</span
>
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
<span></span>
<span>{{ event.meta.subject[attr] }}</span>
</span>
<span v-else-if="event.meta.subject">
{{ event.meta.subject[attr] }}
</span>
<span v-else-if="event.meta.amended && event.meta.amends.subject">
{{ event.meta.amends.subject[attr] }}
</span>
<span v-else>
{{ event[attr] }}
</span>
</span>
</template>
<script>
export default {
name: "LessonRelatedObjectChip",
props: {
status: {
type: String,
default: "regular",
validator: (value) => ["new", "removed", "regular"].includes(value),
},
newIcon: {
type: String,
default: "mdi-plus",
},
},
};
</script>
<template>
<v-chip
label
outlined
:class="{
'mr-2': true,
'text-decoration-line-through text--secondary': status === 'removed',
}"
:color="status === 'new' ? 'warning' : ''"
>
<v-icon left v-if="status === 'new'">{{ newIcon }}</v-icon>
<slot></slot>
</v-chip>
</template>
<script>
export default {
name: "NoTimetableCard",
props: {
titleKey: {
type: String,
required: false,
default: "chronos.timetable.no_timetable_selected.title",
},
descriptionKey: {
type: String,
required: false,
default: "chronos.timetable.no_timetable_selected.description",
},
},
};
</script>
<template>
<v-card
class="full-height d-flex align-center justify-center py-10"
v-bind="$attrs"
>
<div class="text-center">
<v-icon color="secondary" size="60" class="mb-4"> mdi-grid-off </v-icon>
<div class="text-h5 grey--text text--darken-2 mb-2">
{{ $t(titleKey) }}
</div>
<div
class="text-body-2 grey--text text--darken-2"
v-if="$vuetify.breakpoint.lgAndUp"
>
{{ $t(descriptionKey) }}
</div>
<div v-if="$vuetify.breakpoint.mdAndDown">
<v-btn color="primary" @click="$emit('selectTimetable')" class="mt-4">
{{ $t("chronos.timetable.select") }}
</v-btn>
</div>
</div>
</v-card>
</template>
<script>
import timetableTypes from "./timetableTypes";
export default {
name: "SelectTimetable",
props: {
value: {
type: String | null,
required: true,
},
availableTimetables: {
type: Array,
required: true,
},
},
data() {
return {
selected: null,
selectedFull: null,
search: "",
selectedTypes: ["GROUP", "TEACHER", "ROOM"],
types: timetableTypes,
};
},
watch: {
value(val) {
this.selectedFull = val;
this.selected = val.id;
},
selectedFull(val) {
this.$emit("input", val);
},
},
computed: {
availableTimetablesFiltered() {
// Filter timetables by selected types
return this.availableTimetables.filter((timetable) => {
return this.selectedTypes.indexOf(timetable.type) !== -1;
});
},
},
};
</script>
<template>
<div>
<v-card-text class="mb-0">
<!-- Search field for timetables -->
<v-text-field
search
filled
rounded
clearable
autofocus
v-model="search"
:placeholder="$t('chronos.timetable.search')"
prepend-inner-icon="mdi-magnify"
hide-details="auto"
class="mb-2"
/>
<!-- Filter by timetable types -->
<v-btn-toggle v-model="selectedTypes" dense block multiple class="d-flex">
<v-btn
v-for="type in types"
:key="type.id"
class="flex-grow-1"
:value="type.id"
>
{{ type.name }}
</v-btn>
</v-btn-toggle>
</v-card-text>
<!-- Select of available timetables -->
<v-data-iterator
:items="availableTimetablesFiltered"
item-key="id"
:search="search"
single-expand
disable-pagination
>
<template #default="{ items, isExpanded, expand }">
<v-list class="scrollable-list">
<v-list-item-group v-model="selected">
<v-list-item
v-for="item in items"
@click="selectedFull = item"
:value="item.id"
:key="item.id"
>
<v-list-item-icon color="primary">
<v-icon v-if="item.type in types" color="secondary">
{{ types[item.type].icon }}
</v-icon>
<v-icon v-else color="secondary">mdi-grid</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>mdi-chevron-right</v-icon>
</v-list-item-action>
</v-list-item>
</v-list-item-group>
</v-list>
</template>
</v-data-iterator>
</div>
</template>
<style scoped>
.scrollable-list {
height: 100%;
overflow-y: scroll;
}
</style>
<script>
import { gqlAvailableTimetables } from "./timetables.graphql";
import NoTimetableCard from "./NoTimetableCard.vue";
import SelectTimetable from "./SelectTimetable.vue";
import timetableTypes from "./timetableTypes";
export default {
name: "Timetable",
components: { NoTimetableCard, SelectTimetable },
apollo: {
availableTimetables: {
query: gqlAvailableTimetables,
result() {
if (
!this.selected &&
this.$route.params.id &&
this.$route.params.type
) {
this.selectTimetable(
this.availableTimetables.find(
(t) =>
t.objId === this.$route.params.id &&
t.type.toLowerCase() === this.$route.params.type
)
);
}
},
},
},
data() {
return {
availableTimetables: [],
selected: null,
search: "",
selectedTypes: ["GROUP", "TEACHER", "ROOM"],
types: timetableTypes,
selectDialog: false,
};
},
watch: {
selected(selected) {
// Align navigation with currently selected timetable
if (!selected) {
this.$router.push({ name: "chronos.timetable" });
} else if (
selected.objId !== this.$route.params.id ||
selected.type.toLowerCase() !== this.$route.params.type
) {
this.$router.push({
name: "chronos.timetableWithId",
params: {
type: selected.type.toLowerCase(),
id: selected.objId,
},
});
}
},
},
methods: {
findNextTimetable(offset = 1) {
const currentIndex = this.availableTimetablesIds.indexOf(
this.selected.id
);
const newIndex = currentIndex + offset;
if (newIndex < 0 || newIndex >= this.availableTimetablesIds.length) {
return null;
}
return this.availableTimetables[newIndex];
},
selectTimetable(timetable) {
this.selected = timetable;
},
},
computed: {
selectedTypesFull() {
return this.selectedTypes.map((type) => {
return this.types[type];
});
},
availableTimetablesFiltered() {
// Filter timetables by selected types
return this.availableTimetables.filter((timetable) => {
return this.selectedTypes.indexOf(timetable.type) !== -1;
});
},
availableTimetablesIds() {
return this.availableTimetables.map((timetable) => timetable.id);
},
prevTimetable() {
return this.findNextTimetable(-1);
},
nextTimetable() {
return this.findNextTimetable(1);
},
},
};
</script>
<template>
<div>
<v-row>
<v-dialog
v-model="selectDialog"
fullscreen
hide-overlay
transition="dialog-bottom-transition"
>
<v-card>
<v-toolbar dark color="primary">
<v-toolbar-title>{{
$t("chronos.timetable.select")
}}</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<select-timetable
v-model="selected"
@input="selectDialog = false"
:availableTimetables="availableTimetables"
/>
</v-card>
</v-dialog>
<v-col md="3" lg="3" xl="3" v-if="$vuetify.breakpoint.lgAndUp">
<v-card>
<select-timetable
v-model="selected"
:availableTimetables="availableTimetables"
/>
</v-card>
</v-col>
<v-col sm="12" md="12" lg="9" xl="9" class="full-height">
<!-- No timetable card-->
<no-timetable-card
v-if="selected == null"
@selectTimetable="selectDialog = true"
/>
<!-- Calendar card-->
<v-card v-else>
<div class="d-flex flex-column" v-if="$vuetify.breakpoint.smAndDown">
<v-card-title class="pt-2">
<v-btn
icon
:disabled="!prevTimetable"
@click="selectTimetable(prevTimetable)"
:title="$t('chronos.timetable.prev')"
class="mr-1"
>
<v-icon>mdi-chevron-left</v-icon>
</v-btn>
<v-spacer />
<v-chip outlined color="secondary" @click="selectDialog = true">
{{ selected.name }}
<v-icon right>mdi-chevron-down</v-icon>
</v-chip>
<v-spacer />
<v-btn
icon
:disabled="!nextTimetable"
@click="selectTimetable(nextTimetable)"
:title="$t('chronos.timetable.next')"
class="ml-1 float-right"
>
<v-icon>mdi-chevron-right</v-icon>
</v-btn>
</v-card-title>
</div>
<div class="d-flex flex-wrap justify-space-between mb-2" v-else>
<v-card-title>
{{ selected.name }}
</v-card-title>
<div class="pa-2 mt-1">
<v-btn
icon
:disabled="!prevTimetable"
@click="selectTimetable(prevTimetable)"
:title="$t('chronos.timetable.prev')"
>
<v-icon>mdi-chevron-left</v-icon>
</v-btn>
<v-chip label color="secondary" outlined class="mx-1">{{
selected.shortName
}}</v-chip>
<v-btn
icon
:disabled="!nextTimetable"
@click="selectTimetable(nextTimetable)"
:title="$t('chronos.timetable.next')"
>
<v-icon>mdi-chevron-right</v-icon>
</v-btn>
</div>
</div>
<calendar-with-controls
:calendar-feeds="[{ name: 'lesson' }]"
:params="{ type: selected.type, id: selected.objId }"
/>
</v-card>
</v-col>
</v-row>
</div>
</template>
query gqlSubjects {
amendableSubjects: subjects {
id
name
}
}
query gqlPersons {
amendableTeachers: persons {
id
fullName
}
}
query gqlRooms {
amendableRooms: rooms {
id
name
}
}
mutation createAmendLesson($input: CreateLessonEventInput!) {
createAmendLesson(input: $input) {
lessonEvent {
id
amends {
id
}
datetimeStart
datetimeEnd
subject {
id
}
teachers {
id
}
groups {
id
}
rooms {
id
}
cancelled
comment
}
}
}
mutation patchAmendLesson($input: PatchLessonEventInput!, $id: ID!) {
patchAmendLesson(input: $input, id: $id) {
lessonEvent {
id
subject {
id
}
teachers {
id
}
groups {
id
}
rooms {
id
}
cancelled
comment
}
}
}
mutation deleteAmendLesson($id: ID!) {
deleteAmendLesson(id: $id) {
ok
}
}
<template>
<base-calendar-feed-details
v-bind="$props"
:color="currentSubject ? currentSubject.colour_bg : null"
without-location
>
<template #title>
<div
:style="{
color: currentSubject ? currentSubject.colour_fg || 'white' : 'white',
}"
>
<lesson-event-subject :event="selectedEvent" />
</div>
</template>
<template #badge>
<cancelled-calendar-status-chip
v-if="selectedEvent.meta.cancelled"
class="ml-4"
/>
<calendar-status-chip
color="warning"
icon="mdi-clipboard-alert-outline"
v-else-if="selectedEvent.meta.amended"
class="ml-4"
>
{{ $t("chronos.event.current_changes") }}
</calendar-status-chip>
</template>
<template #description>
<v-divider inset />
<v-list-item v-if="selectedEvent.meta.groups.length > 0">
<v-list-item-icon>
<v-icon color="primary">mdi-account-group-outline</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
<lesson-related-object-chip
v-for="group in selectedEvent.meta.groups"
:key="group.id"
>{{ group.name }}</lesson-related-object-chip
>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon color="primary">mdi-human-male-board </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
<span v-if="teachers.length === 0" class="body-2 text--secondary">{{
$t("chronos.event.no_teacher")
}}</span>
<lesson-related-object-chip
v-for="teacher in teachers"
:status="teacher.status"
:key="teacher.id"
new-icon="mdi-account-plus-outline"
>{{ teacher.full_name }}</lesson-related-object-chip
>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon color="primary">mdi-door </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
<span v-if="rooms.length === 0" class="body-2 text--secondary">{{
$t("chronos.event.no_room")
}}</span>
<lesson-related-object-chip
v-for="room in rooms"
:status="room.status"
:key="room.id"
new-icon="mdi-door-open"
>{{ room.name }}</lesson-related-object-chip
>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-divider inset />
<v-list-item v-if="selectedEvent.meta.comment">
<v-list-item-content>
<v-list-item-title>
<v-alert
dense
outlined
type="warning"
icon="mdi-information-outline"
>
{{ selectedEvent.meta.comment }}
</v-alert>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<amend-lesson
v-if="selectedEvent"
:selected-event="selectedEvent"
@refreshCalendar="$emit('refreshCalendar')"
/>
</template>
</base-calendar-feed-details>
</template>
<script>
import calendarFeedDetailsMixin from "aleksis.core/mixins/calendarFeedDetails.js";
import BaseCalendarFeedDetails from "aleksis.core/components/calendar/BaseCalendarFeedDetails.vue";
import CalendarStatusChip from "aleksis.core/components/calendar/CalendarStatusChip.vue";
import CancelledCalendarStatusChip from "aleksis.core/components/calendar/CancelledCalendarStatusChip.vue";
import LessonRelatedObjectChip from "../../LessonRelatedObjectChip.vue";
import lessonEvent from "../mixins/lessonEvent";
import LessonEventSubject from "../../LessonEventSubject.vue";
import AmendLesson from "../../AmendLesson.vue";
export default {
name: "LessonDetails",
components: {
LessonEventSubject,
LessonRelatedObjectChip,
BaseCalendarFeedDetails,
CalendarStatusChip,
CancelledCalendarStatusChip,
AmendLesson,
},
mixins: [calendarFeedDetailsMixin, lessonEvent],
};
</script>
<template>
<base-calendar-feed-event-bar
:with-padding="false"
:without-time="true"
v-bind="$props"
>
<template #icon> </template>
<template #title>
<div
class="d-flex justify-start"
:class="{
'px-1': true,
'orange-border':
selectedEvent.meta.amended && !selectedEvent.meta.cancelled,
'red-border': selectedEvent.meta.cancelled,
}"
:style="{
color: currentSubject ? currentSubject.colour_fg || 'white' : 'white',
height: '100%',
borderRadius: '4px',
}"
>
<span
v-if="calendarType === 'month' && eventParsed.start.hasTime"
class="mr-1 font-weight-bold ml-1"
>
{{ eventParsed.start.time }}
</span>
<div
class="d-flex justify-center align-center flex-grow-1 text-truncate"
>
<div class="d-flex justify-center align-center flex-wrap text">
<lesson-event-link-iterator
v-if="!selectedEvent.meta.is_member"
:items="selectedEvent.meta.groups"
attr="short_name"
class="mr-1"
/>
<lesson-event-old-new
v-if="!selectedEvent.meta.is_teacher || newTeachers.length > 0"
:new-items="newTeachers"
:old-items="oldTeachers"
attr="short_name"
class="mr-1"
/>
<lesson-event-subject
:event="selectedEvent"
attr="short_name"
class="font-weight-medium mr-1"
/>
<lesson-event-old-new
:new-items="newRooms"
:old-items="oldRooms"
attr="short_name"
/>
</div>
</div>
</div>
</template>
</base-calendar-feed-event-bar>
</template>
<script>
import calendarFeedEventBarMixin from "aleksis.core/mixins/calendarFeedEventBar.js";
import BaseCalendarFeedEventBar from "aleksis.core/components/calendar/BaseCalendarFeedEventBar.vue";
import lessonEvent from "../mixins/lessonEvent";
import LessonEventSubject from "../../LessonEventSubject.vue";
import LessonEventLinkIterator from "../../LessonEventLinkIterator.vue";
import LessonEventOldNew from "../../LessonEventOldNew.vue";
export default {
name: "LessonEventBar",
components: {
LessonEventOldNew,
LessonEventLinkIterator,
LessonEventSubject,
BaseCalendarFeedEventBar,
},
computed: {
selectedEvent() {
return this.event;
},
},
mixins: [calendarFeedEventBarMixin, lessonEvent],
};
</script>
<style scoped>
.orange-border {
border: 3px orange solid;
}
.red-border {
border: 3px red solid;
}
.text {
line-height: 1.1;
font-size: 12px;
}
</style>
/**
* Mixin with common used API for custom lesson event calendar components
*/
const lessonEvent = {
methods: {
addStatuses(attr) {
let oldItems = this.getOldItems(attr);
let newItems = this.getNewItems(attr);
let oldIds = oldItems.map((item) => item.id);
let newIds = newItems.map((item) => item.id);
let itemsWithStatus = oldItems.concat(newItems).map((item) => {
let status = "regular";
if (newIds.includes(item.id) && !oldIds.includes(item.id)) {
status = "new";
} else if (
newIds.length > 0 &&
!newIds.includes(item.id) &&
oldIds.includes(item.id)
) {
status = "removed";
}
return { ...item, status: status };
});
return itemsWithStatus;
},
getOldItems(attr) {
let oldItems = [];
if (this.selectedEvent.meta.amended) {
oldItems = this.selectedEvent.meta.amends[attr];
} else {
oldItems = this.selectedEvent.meta[attr];
}
return oldItems;
},
getNewItems(attr) {
let newItems = [];
if (this.selectedEvent.meta.amended) {
newItems = this.selectedEvent.meta[attr];
}
return newItems;
},
},
computed: {
teachers() {
return this.addStatuses("teachers");
},
oldTeachers() {
return this.getOldItems("teachers");
},
newTeachers() {
return this.getNewItems("teachers");
},
rooms() {
return this.addStatuses("rooms");
},
oldRooms() {
return this.getOldItems("rooms");
},
newRooms() {
return this.getNewItems("rooms");
},
currentSubject() {
if (this.selectedEvent.meta.subject) {
return this.selectedEvent.meta.subject;
} else if (
this.selectedEvent.meta.amended &&
this.selectedEvent.meta.amends.subject
) {
return this.selectedEvent.meta.amends.subject;
} else {
return null;
}
},
},
};
export default lessonEvent;
export default {
GROUP: {
name: "Groups",
id: "GROUP",
icon: "mdi-account-group-outline",
},
TEACHER: {
name: "Teachers",
id: "TEACHER",
icon: "mdi-account-outline",
},
ROOM: { name: "Rooms", id: "ROOM", icon: "mdi-door" },
};
query gqlAvailableTimetables {
availableTimetables {
id
objId
type
name
shortName
}
}
import { hasPersonValidator } from "aleksis.core/routeValidators";
import Timetable from "./components/Timetable.vue";
export default {
meta: {
......@@ -12,185 +13,22 @@ export default {
},
children: [
{
path: "",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.allTimetables",
path: "timetable/",
component: Timetable,
name: "chronos.timetable",
meta: {
inMenu: true,
titleKey: "chronos.timetable.menu_title_all",
titleKey: "chronos.timetable.menu_title",
icon: "mdi-grid",
permission: "chronos.view_timetable_overview_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "timetable/my/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.myTimetable",
meta: {
inMenu: true,
titleKey: "chronos.timetable.menu_title_my",
icon: "mdi-account-outline",
permission: "chronos.view_my_timetable_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "timetable/my/:year/:month/:day/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.myTimetableByDate",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "timetable/:type_/:pk/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.timetable",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "timetable/:type_/:pk/:year/:week/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.timetableByWeek",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "timetable/:type_/:pk/print/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.timetablePrint",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "timetable/:type_/:pk/:regular/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.timetableRegular",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "lessons/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.lessonsDay",
meta: {
inMenu: true,
titleKey: "chronos.lessons.menu_title_daily",
icon: "mdi-calendar-outline",
permission: "chronos.view_lessons_day_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "lessons/:year/:month/:day/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.lessonsDayByDate",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "lessons/:id_/:week/substitution/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.editSubstitution",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "lessons/:id_/:week/substitution/delete/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.deleteSubstitution",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "substitutions/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.substitutions",
path: "timetable/:type/:id/",
component: Timetable,
name: "chronos.timetableWithId",
meta: {
inMenu: true,
titleKey: "chronos.substitutions.menu_title",
icon: "mdi-update",
permission: "chronos.view_substitutions_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "substitutions/print/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.substitutionsPrint",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "substitutions/:year/:month/:day/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.substitutionsByDate",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "substitutions/:year/:month/:day/print/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.substitutionsPrintByDate",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "supervisions/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.supervisionsDay",
meta: {
inMenu: true,
titleKey: "chronos.supervisions.menu_title_daily",
icon: "mdi-calendar-outline",
permission: "chronos.view_supervisions_day_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "supervisions/:year/:month/:day/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.supervisionsDayByDate",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "supervisions/:id_/:week/substitution/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.editSupervisionSubstitution",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "supervisions/:id_/:week/substitution/delete/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.deleteSupervisionSubstitution",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
permission: "chronos.view_timetable_overview_rule",
},
},
],
......
......@@ -2,8 +2,17 @@
"chronos": {
"menu_title": "Stundenpläne",
"timetable": {
"menu_title": "Stundenpläne",
"menu_title_all": "Alle Stundenpläne",
"menu_title_my": "Mein Stundenplan"
"menu_title_my": "Mein Stundenplan",
"no_timetable_selected": {
"title": "Kein Stundenplan ausgewählt",
"description": "Wählen Sie auf der linken Seite einen Stundenplan aus, um ihn hier anzuzeigen"
},
"search": "Stundenpläne suchen",
"prev": "Vorheriger Stundenplan",
"next": "Nächster Stundenplan",
"select": "Stundenplan auswählen"
},
"lessons": {
"menu_title_daily": "Tagesstunden"
......@@ -12,7 +21,12 @@
"menu_title": "Vertretungen"
},
"supervisions": {
"menu_title_daily": "Tagesvertretungen"
"menu_title_daily": "Aufsichten"
},
"event": {
"no_teacher": "Keine Lehrkraft",
"no_room": "Kein Raum",
"current_changes": "Aktuelle Änderungen"
}
}
}
......@@ -2,8 +2,17 @@
"chronos": {
"menu_title": "Timetables",
"timetable": {
"menu_title": "Timetables",
"menu_title_all": "All timetables",
"menu_title_my": "My timetable"
"menu_title_my": "My timetable",
"no_timetable_selected": {
"title": "No Timetable Selected",
"description": "Select a timetable on the left side to show it in this place"
},
"search": "Search Timetables",
"prev": "Previous Timetable",
"next": "Next Timetable",
"select": "Select Timetable"
},
"lessons": {
"menu_title_daily": "Daily lessons"
......@@ -13,6 +22,23 @@
},
"supervisions": {
"menu_title_daily": "Daily supervisions"
},
"event": {
"no_teacher": "No teacher",
"no_room": "No room",
"current_changes": "Current changes",
"amend": {
"edit_button": "Change",
"delete_button": "Reset",
"delete_dialog": " Are you sure you want to delete this substitution?",
"delete_success": "The substitution was deleted successfully.",
"title": "Change lesson",
"subject": "Subject",
"teachers": "Teachers",
"rooms": "Rooms",
"cancelled": "Cancelled",
"comment": "Comment"
}
}
}
}
......@@ -9,10 +9,11 @@ from django.db.models.fields import DateField
from django.db.models.functions import Concat
from calendarweek import CalendarWeek
from polymorphic.managers import PolymorphicQuerySet
from aleksis.apps.chronos.util.date import week_weekday_from_date, week_weekday_to_date
from aleksis.core.managers import DateRangeQuerySetMixin, SchoolTermRelatedQuerySet
from aleksis.core.models import Group, Person
from aleksis.core.models import Group, Person, Room
from aleksis.core.util.core_helpers import get_site_preferences
......@@ -799,16 +800,20 @@ class ExtraLessonQuerySet(TimetableQuerySet, SchoolTermRelatedQuerySet, GroupByP
class GroupPropertiesMixin:
"""Mixin for common group properties.
Needed field: `groups`
Necessary method: `get_groups`
"""
@property
def group_names(self, sep: Optional[str] = ", ") -> str:
return sep.join([group.short_name for group in self.groups.all()])
return sep.join([group.short_name for group in self.get_groups()])
@property
def group_short_names(self, sep: Optional[str] = ", ") -> str:
return sep.join([group.short_name for group in self.get_groups()])
@property
def groups_to_show(self) -> models.QuerySet:
groups = self.groups.all()
groups = self.get_groups()
if (
groups.count() == 1
and groups[0].parent_groups.all()
......@@ -822,17 +827,68 @@ class GroupPropertiesMixin:
def groups_to_show_names(self, sep: Optional[str] = ", ") -> str:
return sep.join([group.short_name for group in self.groups_to_show])
@property
def groups_to_show_short_names(self, sep: Optional[str] = ", ") -> str:
return sep.join([group.short_name for group in self.groups_to_show])
class TeacherPropertiesMixin:
"""Mixin for common teacher properties.
Needed field: `teacher`
Necessary method: `get_teachers`
"""
@property
def teacher_names(self, sep: Optional[str] = ", ") -> str:
return sep.join([teacher.full_name for teacher in self.get_teachers().all()])
return sep.join([teacher.full_name for teacher in self.get_teachers()])
@property
def teacher_short_names(self, sep: str = ", ") -> str:
return sep.join([teacher.short_name for teacher in self.get_teachers().all()])
return sep.join([teacher.short_name for teacher in self.get_teachers()])
class RoomPropertiesMixin:
"""Mixin for common room properties.
Necessary method: `get_rooms`
"""
@property
def room_names(self, sep: Optional[str] = ", ") -> str:
return sep.join([room.name for room in self.get_rooms()])
@property
def room_short_names(self, sep: str = ", ") -> str:
return sep.join([room.short_name for room in self.get_rooms()])
class LessonEventQuerySet(PolymorphicQuerySet):
"""Queryset with special query methods for lesson events."""
def for_teacher(self, teacher: Union[int, Person]):
amended = self.filter(Q(amended_by__isnull=False) & (Q(teachers=teacher))).values_list(
"amended_by__pk", flat=True
)
return self.filter(Q(teachers=teacher) | Q(pk__in=amended)).distinct()
def for_group(self, group: Union[int, Group]):
amended = self.filter(
Q(amended_by__isnull=False) & (Q(groups=group) | Q(groups__parent_groups=group))
).values_list("amended_by__pk", flat=True)
return self.filter(
Q(groups=group) | Q(groups__parent_groups=group) | Q(pk__in=amended)
).distinct()
def for_room(self, room: Union[int, Room]):
amended = self.filter(Q(amended_by__isnull=False) & (Q(rooms=room))).values_list(
"amended_by__pk", flat=True
)
return self.filter(Q(rooms=room) | Q(pk__in=amended)).distinct()
def for_person(self, person: Union[int, Person]):
amended = self.filter(
Q(amended_by__isnull=False) & (Q(teachers=person) | Q(groups__members=person))
).values_list("amended_by__pk", flat=True)
return self.filter(
Q(teachers=person) | Q(groups__members=person) | Q(pk__in=amended)
).distinct()
from django.utils.translation import gettext_lazy as _
MENUS = {
"NAV_MENU_CORE": [
{
"name": _("Timetables"),
"url": "#",
"svg_icon": "mdi:school-outline",
"root": True,
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
],
"submenu": [
{
"name": _("My timetable"),
"url": "my_timetable",
"svg_icon": "mdi:account-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"chronos.view_my_timetable_rule",
),
],
},
{
"name": _("All timetables"),
"url": "all_timetables",
"svg_icon": "mdi:grid",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"chronos.view_timetable_overview_rule",
),
],
},
{
"name": _("Daily lessons"),
"url": "lessons_day",
"svg_icon": "mdi:calendar-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"chronos.view_lessons_day_rule",
),
],
},
{
"name": _("Daily supervisions"),
"url": "supervisions_day",
"svg_icon": "mdi:calendar-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"chronos.view_supervisions_day_rule",
),
],
},
{
"name": _("Substitutions"),
"url": "substitutions",
"svg_icon": "mdi:update",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"chronos.view_substitutions_rule",
),
],
},
],
}
]
}
# Generated by Django 3.0.5 on 2020-05-04 14:16
import django.contrib.postgres.fields.jsonb
import django.contrib.sites.managers
import django.db.models.deletion
from django.db import migrations, models
import calendarweek.calendarweek
import colorfield.fields
import aleksis.core.managers
import aleksis.apps.chronos.managers
......@@ -49,7 +50,7 @@ class Migration(migrations.Migration):
),
"managed": False,
},
managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),],
managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),],
),
migrations.CreateModel(
name="Break",
......@@ -80,7 +81,7 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Breaks",
"ordering": ["after_period"],
},
managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),],
managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),],
),
migrations.CreateModel(
name="Lesson",
......@@ -119,7 +120,7 @@ class Migration(migrations.Migration):
aleksis.apps.chronos.managers.GroupPropertiesMixin,
aleksis.apps.chronos.managers.TeacherPropertiesMixin,
),
managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),],
managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),],
),
migrations.CreateModel(
name="LessonPeriod",
......@@ -251,7 +252,7 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Time periods",
"ordering": ["weekday", "period"],
},
managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),],
managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),],
),
migrations.CreateModel(
name="SupervisionSubstitution",
......@@ -353,7 +354,7 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Supervision areas",
"ordering": ["name"],
},
managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),],
managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),],
),
migrations.AddField(
model_name="supervision",
......@@ -458,7 +459,7 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Subjects",
"ordering": ["name", "short_name"],
},
managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),],
managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),],
),
migrations.CreateModel(
name="Room",
......@@ -500,7 +501,7 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Rooms",
"ordering": ["name", "short_name"],
},
managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),],
managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),],
),
migrations.CreateModel(
name="LessonSubstitution",
......@@ -871,7 +872,7 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Exams",
"ordering": ["date"],
},
managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),],
managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),],
),
migrations.CreateModel(
name="Event",
......@@ -1032,7 +1033,7 @@ class Migration(migrations.Migration):
"verbose_name": "Absence reason",
"verbose_name_plural": "Absence reasons",
},
managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),],
managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),],
),
migrations.CreateModel(
name="Absence",
......