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 (36)
Showing
with 1153 additions and 265 deletions
<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>
{{ event.meta.subject[attr] }}
</span>
<span v-else-if="event.meta.subject">
{{ event.meta.subject[attr] }}
</span>
<span v-else-if="event.amended && event.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>
<template>
<base-calendar-feed-details
v-bind="$props"
:color="currentSubject ? currentSubject.colour_bg : null"
>
<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>
</template>
</base-calendar-feed-details>
</template>
<script>
import calendarFeedDetailsMixin from "aleksis.core/mixins/calendarFeedDetails.js";
import BaseCalendarFeedDetails from "aleksis.core/components/calendar/BaseCalendarFeedDetails.vue";
import CalendarStatusChip from "aleksis.core/components/calendar/CalendarStatusChip.vue";
import CancelledCalendarStatusChip from "aleksis.core/components/calendar/CancelledCalendarStatusChip.vue";
import LessonRelatedObjectChip from "../../LessonRelatedObjectChip.vue";
import lessonEvent from "../mixins/lessonEvent";
import LessonEventSubject from "../../LessonEventSubject.vue";
export default {
name: "LessonDetails",
components: {
LessonEventSubject,
LessonRelatedObjectChip,
BaseCalendarFeedDetails,
CalendarStatusChip,
CancelledCalendarStatusChip,
},
mixins: [calendarFeedDetailsMixin, lessonEvent],
};
</script>
<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",
meta: {
inMenu: true,
titleKey: "chronos.timetable.menu_title_all",
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"),
path: "timetable/",
component: Timetable,
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",
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,
titleKey: "chronos.timetable.menu_title",
icon: "mdi-grid",
permission: "chronos.view_timetables_rule",
},
},
{
path: "supervisions/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.supervisionsDay",
path: "timetable/:type/:id/",
component: Timetable,
name: "chronos.timetableWithId",
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_timetables_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,11 @@
},
"supervisions": {
"menu_title_daily": "Daily supervisions"
},
"event": {
"no_teacher": "No teacher",
"no_room": "No room",
"current_changes": "Current changes"
}
}
}
......@@ -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",
......
# Generated by Django 4.1.8 on 2023-06-29 14:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("sites", "0002_alter_domain_unique"),
("core", "0051_calendarevent_and_holiday"),
("cursus", "0001_initial"),
("chronos", "0013_move_room_to_core"),
]
operations = [
migrations.CreateModel(
name="LessonEvent",
fields=[
(
"calendarevent_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.calendarevent",
),
),
(
"title",
models.CharField(blank=True, max_length=255, verbose_name="Name"),
),
(
"cancelled",
models.BooleanField(default=False, verbose_name="Cancelled"),
),
("comment", models.TextField(blank=True, verbose_name="Comment")),
(
"course",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="cursus.course",
verbose_name="Course",
),
),
(
"groups",
models.ManyToManyField(
blank=True,
related_name="lesson_events",
to="core.group",
verbose_name="Groups",
),
),
(
"rooms",
models.ManyToManyField(
blank=True,
related_name="lesson_events",
to="core.room",
verbose_name="Rooms",
),
),
(
"subject",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="lesson_events",
to="cursus.subject",
verbose_name="Subject",
),
),
(
"teachers",
models.ManyToManyField(
blank=True,
related_name="lesson_events_as_teacher",
to="core.person",
verbose_name="Teachers",
),
),
],
options={
"verbose_name": "Lesson Event",
"verbose_name_plural": "Lesson Events",
},
bases=("core.calendarevent",),
),
migrations.CreateModel(
name="SupervisionEvent",
fields=[
(
"lessonevent_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="chronos.lessonevent",
),
),
],
options={
"abstract": False,
},
bases=("chronos.lessonevent",),
),
]
# Generated by Django 4.2.3 on 2023-07-27 13:35
import aleksis.core.managers
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('sites', '0002_alter_domain_unique'),
('chronos', '0014_lessonevent'),
]
operations = [
migrations.AddField(
model_name='absence',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='absencereason',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='break',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='event',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='exam',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='extralesson',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='holiday',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='lesson',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='lessonperiod',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='lessonsubstitution',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='subject',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='supervision',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='supervisionarea',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='supervisionsubstitution',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='timeperiod',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
migrations.AddField(
model_name='validityrange',
name='managed_by_app_label',
field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
),
]