Skip to content
Snippets Groups Projects
Commit 400c5787 authored by Hangzhi Yu's avatar Hangzhi Yu
Browse files

Merge branch '216-add-preference-for-group-type-to-show-in-timetable-view' into 'master'

Resolve "Add preference for group type to show in timetable view"

Closes #216

See merge request !343
parents 3bec1bec 7274012b
No related branches found
No related tags found
1 merge request!329Introduce substitution to do list
Pipeline #180778 failed
Showing
with 206 additions and 574 deletions
...@@ -3,8 +3,8 @@ include: ...@@ -3,8 +3,8 @@ include:
file: /ci/general.yml file: /ci/general.yml
- project: "AlekSIS/official/AlekSIS" - project: "AlekSIS/official/AlekSIS"
file: /ci/prepare/lock.yml file: /ci/prepare/lock.yml
- project: "AlekSIS/official/AlekSIS" # - project: "AlekSIS/official/AlekSIS"
file: /ci/test/test.yml # file: /ci/test/test.yml
- project: "AlekSIS/official/AlekSIS" - project: "AlekSIS/official/AlekSIS"
file: /ci/test/lint.yml file: /ci/test/lint.yml
- project: "AlekSIS/official/AlekSIS" - project: "AlekSIS/official/AlekSIS"
......
...@@ -23,7 +23,6 @@ from .models import ( ...@@ -23,7 +23,6 @@ from .models import (
SupervisionArea, SupervisionArea,
SupervisionSubstitution, SupervisionSubstitution,
TimePeriod, TimePeriod,
TimetableWidget,
ValidityRange, ValidityRange,
) )
from .util.format import format_date_period, format_m2m from .util.format import format_date_period, format_m2m
...@@ -209,13 +208,6 @@ class TimePeriodAdmin(admin.ModelAdmin): ...@@ -209,13 +208,6 @@ class TimePeriodAdmin(admin.ModelAdmin):
admin.site.register(TimePeriod, TimePeriodAdmin) admin.site.register(TimePeriod, TimePeriodAdmin)
class TimetableWidgetAdmin(admin.ModelAdmin):
list_display = ("title", "active")
admin.site.register(TimetableWidget, TimetableWidgetAdmin)
class ValidityRangeAdmin(admin.ModelAdmin): class ValidityRangeAdmin(admin.ModelAdmin):
list_display = ("__str__", "date_start", "date_end") list_display = ("__str__", "date_start", "date_end")
list_display_links = ("__str__", "date_start", "date_end") list_display_links = ("__str__", "date_start", "date_end")
......
from collections.abc import Sequence
from django.db.models import Count, Q
from django.forms import RadioSelect
from django.utils.translation import gettext as _
from django_filters import BooleanFilter, FilterSet, ModelMultipleChoiceFilter
from django_select2.forms import ModelSelect2MultipleWidget
from material import Layout, Row
from aleksis.core.models import Group, Person, Room, SchoolTerm
from .models import Break, Subject, SupervisionArea, TimePeriod
class MultipleModelMultipleChoiceFilter(ModelMultipleChoiceFilter):
"""Filter for filtering multiple fields with one input.
>>> multiple_filter = MultipleModelMultipleChoiceFilter(["room", "substitution_room"])
"""
def filter(self, qs, value): # noqa
if not value:
return qs
if self.is_noop(qs, value):
return qs
q = Q()
for v in set(value):
if v == self.null_value:
v = None
for field in self.lookup_fields:
q = q | Q(**{field: v})
qs = self.get_method(qs)(q)
return qs.distinct() if self.distinct else qs
def __init__(self, lookup_fields: Sequence[str], *args, **kwargs):
self.lookup_fields = lookup_fields
super().__init__(self, *args, **kwargs)
class LessonPeriodFilter(FilterSet):
period = ModelMultipleChoiceFilter(queryset=TimePeriod.objects.all())
lesson__groups = ModelMultipleChoiceFilter(
queryset=Group.objects.all(),
widget=ModelSelect2MultipleWidget(
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
search_fields=[
"name__icontains",
"short_name__icontains",
],
),
)
room = MultipleModelMultipleChoiceFilter(
["room", "current_substitution__room"],
queryset=Room.objects.all(),
label=_("Room"),
widget=ModelSelect2MultipleWidget(
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
search_fields=[
"name__icontains",
"short_name__icontains",
],
),
)
lesson__teachers = MultipleModelMultipleChoiceFilter(
["lesson__teachers", "current_substitution__teachers"],
queryset=Person.objects.none(),
label=_("Teachers"),
widget=ModelSelect2MultipleWidget(
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
search_fields=[
"first_name__icontains",
"last_name__icontains",
"short_name__icontains",
],
),
)
lesson__subject = MultipleModelMultipleChoiceFilter(
["lesson__subject", "current_substitution__subject"],
queryset=Subject.objects.all(),
label=_("Subject"),
widget=ModelSelect2MultipleWidget(
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
search_fields=[
"name__icontains",
"short_name__icontains",
],
),
)
substituted = BooleanFilter(
field_name="current_substitution",
label=_("Substitution status"),
lookup_expr="isnull",
exclude=True,
widget=RadioSelect(
choices=[
("", _("All lessons")),
(True, _("Substituted")),
(False, _("Not substituted")),
]
),
)
def __init__(self, *args, **kwargs):
weekday = kwargs.pop("weekday")
super().__init__(*args, **kwargs)
self.filters["period"].queryset = TimePeriod.objects.filter(weekday=weekday)
self.filters["lesson__teachers"].queryset = (
Person.objects.annotate(
lessons_count=Count(
"lessons_as_teacher",
filter=Q(lessons_as_teacher__validity__school_term=SchoolTerm.current)
if SchoolTerm.current
else Q(),
)
)
.filter(lessons_count__gt=0)
.order_by("short_name", "last_name")
)
self.form.layout = Layout(
Row("period", "lesson__groups", "room"),
Row("lesson__teachers", "lesson__subject", "substituted"),
)
class SupervisionFilter(FilterSet):
break_item = ModelMultipleChoiceFilter(queryset=Break.objects.all())
area = ModelMultipleChoiceFilter(queryset=SupervisionArea.objects.all())
teacher = MultipleModelMultipleChoiceFilter(
["teacher", "current_substitution__teacher"],
queryset=Person.objects.none(),
label=_("Teacher"),
widget=ModelSelect2MultipleWidget(
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
search_fields=[
"first_name__icontains",
"last_name__icontains",
"short_name__icontains",
],
),
)
substituted = BooleanFilter(
field_name="current_substitution",
label=_("Substitution status"),
lookup_expr="isnull",
exclude=True,
widget=RadioSelect(
choices=[
("", _("All supervisions")),
(True, _("Substituted")),
(False, _("Not substituted")),
]
),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters["break_item"].queryset = Break.objects.filter(supervisions__in=self.queryset)
self.filters["teacher"].queryset = (
Person.objects.annotate(
lessons_count=Count(
"lessons_as_teacher",
filter=Q(lessons_as_teacher__validity__school_term=SchoolTerm.current)
if SchoolTerm.current
else Q(),
)
)
.filter(lessons_count__gt=0)
.order_by("short_name", "last_name")
)
self.form.layout = Layout(
Row("break_item", "area"),
Row("teacher", "substituted"),
)
from django import forms
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget
from material import Layout
from .models import AutomaticPlan, LessonSubstitution, SupervisionSubstitution
from .util.chronos_helpers import get_teachers
class LessonSubstitutionForm(forms.ModelForm):
"""Form to manage substitutions."""
class Meta:
model = LessonSubstitution
fields = ["subject", "teachers", "room", "cancelled", "comment"]
widgets = {
"teachers": ModelSelect2MultipleWidget(
search_fields=[
"first_name__icontains",
"last_name__icontains",
"short_name__icontains",
],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
}
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
self.fields["teachers"].queryset = get_teachers(request.user)
class SupervisionSubstitutionForm(forms.ModelForm):
"""Form to manage supervisions substitutions."""
class Meta:
model = SupervisionSubstitution
fields = ["teacher"]
widgets = {
"teacher": ModelSelect2Widget(
search_fields=[
"first_name__icontains",
"last_name__icontains",
"short_name__icontains",
],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
}
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
self.fields["teacher"].queryset = get_teachers(request.user)
class AutomaticPlanForm(forms.ModelForm):
layout = Layout("slug", "name", "number_of_days", "show_header_box")
class Meta:
model = AutomaticPlan
fields = ["slug", "name", "number_of_days", "show_header_box"]
...@@ -77,6 +77,7 @@ ...@@ -77,6 +77,7 @@
:gql-delete-mutation="gqlDeleteMutation" :gql-delete-mutation="gqlDeleteMutation"
v-model="deleteEvent" v-model="deleteEvent"
:items="[selectedEvent.meta]" :items="[selectedEvent.meta]"
:get-name-of-item="getLessonDeleteText"
@save="updateOnSave()" @save="updateOnSave()"
> >
<template #title> <template #title>
...@@ -173,6 +174,12 @@ export default { ...@@ -173,6 +174,12 @@ export default {
this.$emit("refreshCalendar"); this.$emit("refreshCalendar");
this.model = false; this.model = false;
}, },
getLessonDeleteText(item) {
return `${this.selectedEvent.name} · ${this.$d(
this.selectedEvent.start,
"shortDateTime",
)}${this.$d(this.selectedEvent.end, "shortTime")}`;
},
}, },
computed: { computed: {
initPatchData() { initPatchData() {
......
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
{{ event.meta.amends.subject[attr] }} {{ event.meta.amends.subject[attr] }}
</span> </span>
<span v-else> <span v-else>
{{ event[attr] }} {{ event["name"] }}
</span> </span>
</span> </span>
</template> </template>
...@@ -106,6 +106,11 @@ export default { ...@@ -106,6 +106,11 @@ export default {
</v-list-item-group> </v-list-item-group>
</v-list> </v-list>
</template> </template>
<template #loading>
<v-skeleton-loader
type="list-item-avatar,list-item-avatar,list-item-avatar"
/>
</template>
</v-data-iterator> </v-data-iterator>
</div> </div>
</template> </template>
......
...@@ -5,13 +5,17 @@ ...@@ -5,13 +5,17 @@
without-location without-location
> >
<template #title> <template #title>
<div <slot name="title">
:style="{ <div
color: currentSubject ? currentSubject.colour_fg || 'white' : 'white', :style="{
}" color: currentSubject
> ? currentSubject.colour_fg || 'white'
<lesson-event-subject :event="selectedEvent" /> : 'white',
</div> }"
>
<lesson-event-subject :event="selectedEvent" />
</div>
</slot>
</template> </template>
<template #badge> <template #badge>
<cancelled-calendar-status-chip <cancelled-calendar-status-chip
......
<template> <template>
<base-calendar-feed-details <lesson-details v-bind="$attrs" v-on="$listeners">
v-bind="$props"
:color="currentSubject ? currentSubject.colour_bg : null"
without-location
>
<template #title> <template #title>
<div <v-icon class="mr-1">mdi-coffee</v-icon>
:style="{ {{ $t("chronos.supervisions.title") }}
color: currentSubject ? currentSubject.colour_fg || 'white' : 'white',
}"
>
<lesson-event-subject :event="selectedEvent" />
</div>
</template> </template>
<template #badge> </lesson-details>
<cancelled-calendar-status-chip
v-if="selectedEvent.meta.cancelled"
class="ml-4"
/>
<calendar-status-chip
color="warning"
icon="mdi-clipboard-alert-outline"
v-else-if="selectedEvent.meta.amended"
class="ml-4"
>
{{ $t("chronos.event.current_changes") }}
</calendar-status-chip>
</template>
<template #description>
<v-divider inset />
<v-list-item>
<v-list-item-icon>
<v-icon color="primary">mdi-human-male-board </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
<span v-if="teachers.length === 0" class="body-2 text--secondary">{{
$t("chronos.event.no_teacher")
}}</span>
<lesson-related-object-chip
v-for="teacher in teachers"
:status="teacher.status"
:key="teacher.id"
new-icon="mdi-account-plus-outline"
>{{ teacher.full_name }}</lesson-related-object-chip
>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon color="primary">mdi-door </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
<span v-if="rooms.length === 0" class="body-2 text--secondary">{{
$t("chronos.event.no_room")
}}</span>
<lesson-related-object-chip
v-for="room in rooms"
:status="room.status"
:key="room.id"
new-icon="mdi-door-open"
>{{ room.name }}</lesson-related-object-chip
>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-divider inset />
<v-list-item v-if="selectedEvent.meta.comment">
<v-list-item-content>
<v-list-item-title>
<v-alert
dense
outlined
type="warning"
icon="mdi-information-outline"
>
{{ selectedEvent.meta.comment }}
</v-alert>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</base-calendar-feed-details>
</template> </template>
<script> <script>
import calendarFeedDetailsMixin from "aleksis.core/mixins/calendarFeedDetails.js"; import LessonDetails from "./LessonDetails.vue";
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 { export default {
name: "LessonDetails", name: "SupervisionDetails",
extends: [LessonDetails],
components: { components: {
LessonEventSubject, LessonDetails,
LessonRelatedObjectChip,
BaseCalendarFeedDetails,
CalendarStatusChip,
CancelledCalendarStatusChip,
}, },
mixins: [calendarFeedDetailsMixin, lessonEvent],
}; };
</script> </script>
...@@ -11,9 +11,10 @@ ...@@ -11,9 +11,10 @@
class="d-flex justify-start" class="d-flex justify-start"
:class="{ :class="{
'px-1': true, 'px-1': true,
'orange-border': 'current-changes':
selectedEvent.meta.amended && !selectedEvent.meta.cancelled, selectedEvent.meta.amended && !selectedEvent.meta.cancelled,
'red-border': selectedEvent.meta.cancelled, cancelled: selectedEvent.meta.cancelled,
'text-decoration-line-through': selectedEvent.meta.cancelled,
}" }"
:style="{ :style="{
color: currentSubject ? currentSubject.colour_fg || 'white' : 'white', color: currentSubject ? currentSubject.colour_fg || 'white' : 'white',
...@@ -31,6 +32,8 @@ ...@@ -31,6 +32,8 @@
class="d-flex justify-center align-center flex-grow-1 text-truncate" class="d-flex justify-center align-center flex-grow-1 text-truncate"
> >
<div class="d-flex justify-center align-center flex-wrap text"> <div class="d-flex justify-center align-center flex-wrap text">
<slot name="additionalElements"></slot>
<lesson-event-link-iterator <lesson-event-link-iterator
v-if="!selectedEvent.meta.is_member" v-if="!selectedEvent.meta.is_member"
:items="selectedEvent.meta.groups" :items="selectedEvent.meta.groups"
...@@ -46,6 +49,7 @@ ...@@ -46,6 +49,7 @@
/> />
<lesson-event-subject <lesson-event-subject
v-if="withSubject"
:event="selectedEvent" :event="selectedEvent"
attr="short_name" attr="short_name"
class="font-weight-medium mr-1" class="font-weight-medium mr-1"
...@@ -83,16 +87,23 @@ export default { ...@@ -83,16 +87,23 @@ export default {
return this.event; return this.event;
}, },
}, },
props: {
withSubject: {
type: Boolean,
default: true,
required: false,
},
},
mixins: [calendarFeedEventBarMixin, lessonEvent], mixins: [calendarFeedEventBarMixin, lessonEvent],
}; };
</script> </script>
<style scoped> <style scoped>
.orange-border { .current-changes {
border: 3px orange solid; border: 3px orange solid;
} }
.red-border { .cancelled {
border: 3px red solid; border: 3px red solid;
} }
......
<template>
<lesson-event-bar :with-subject="false" v-bind="$attrs" v-on="$listeners">
<template #additionalElements>
<v-icon size="12" color="white" class="mr-1">mdi-coffee</v-icon>
</template>
</lesson-event-bar>
</template>
<script>
import LessonEventBar from "./LessonEventBar.vue";
export default {
name: "SupervisionEventBar",
components: {
LessonEventBar,
},
extends: [LessonEventBar],
};
</script>
...@@ -25,6 +25,7 @@ export default { ...@@ -25,6 +25,7 @@ export default {
titleKey: "chronos.timetable.menu_title", titleKey: "chronos.timetable.menu_title",
icon: "mdi-grid", icon: "mdi-grid",
permission: "chronos.view_timetable_overview_rule", permission: "chronos.view_timetable_overview_rule",
fullWidth: true,
}, },
}, },
{ {
...@@ -33,6 +34,7 @@ export default { ...@@ -33,6 +34,7 @@ export default {
name: "chronos.timetableWithId", name: "chronos.timetableWithId",
meta: { meta: {
permission: "chronos.view_timetable_overview_rule", permission: "chronos.view_timetable_overview_rule",
fullWidth: true,
}, },
}, },
{ {
......
{ {
"chronos": { "chronos": {
"menu_title": "Stundenpläne", "event": {
"timetable": { "amend": {
"menu_title": "Stundenpläne", "cancelled": "Fällt aus",
"menu_title_all": "Alle Stundenpläne", "comment": "Kommentar",
"menu_title_my": "Mein Stundenplan", "delete_button": "Zurücksetzen",
"no_timetable_selected": { "delete_dialog": "Sind Sie sicher, dass Sie diese Vertretung löschen wollen?",
"title": "Kein Stundenplan ausgewählt", "delete_success": "Die Vertretung wurde erfolgreich gelöscht.",
"description": "Wählen Sie auf der linken Seite einen Stundenplan aus, um ihn hier anzuzeigen" "edit_button": "Ändern",
}, "rooms": "Räume",
"search": "Stundenpläne suchen", "subject": "Fach",
"prev": "Vorheriger Stundenplan",
"next": "Nächster Stundenplan",
"select": "Stundenplan auswählen",
"types": {
"groups": "Gruppen",
"teachers": "Lehrkräfte", "teachers": "Lehrkräfte",
"rooms": "Räume" "title": "Stunde ändern"
} },
"current_changes": "Aktuelle Änderungen",
"no_room": "Kein Raum",
"no_teacher": "Keine Lehrkraft"
}, },
"lessons": { "lessons": {
"menu_title_daily": "Tagesstunden" "menu_title_daily": "Tagesstunden"
}, },
"menu_title": "Stundenpläne",
"substitutions": { "substitutions": {
"menu_title": "Vertretungen" "menu_title": "Vertretungen"
}, },
"supervisions": { "supervisions": {
"menu_title_daily": "Aufsichten" "menu_title_daily": "Aufsichten",
}, "title": "Aufsicht"
"event": {
"no_teacher": "Keine Lehrkraft",
"no_room": "Kein Raum",
"current_changes": "Aktuelle Änderungen"
}, },
"amend_lesson": { "amend_lesson": {
"overview": { "overview": {
...@@ -45,6 +40,24 @@ ...@@ -45,6 +40,24 @@
"decancel": "Stunde nicht ausfallen lassen" "decancel": "Stunde nicht ausfallen lassen"
} }
} }
},
"timetable": {
"menu_title": "Stundenpläne",
"menu_title_all": "Alle Stundenpläne",
"menu_title_my": "Mein Stundenplan",
"next": "Nächster Stundenplan",
"no_timetable_selected": {
"description": "Wählen Sie auf der linken Seite einen Stundenplan aus, um ihn hier anzuzeigen",
"title": "Kein Stundenplan ausgewählt"
},
"prev": "Vorheriger Stundenplan",
"search": "Stundenpläne suchen",
"select": "Stundenplan auswählen",
"types": {
"groups": "Gruppen",
"rooms": "Räume",
"teachers": "Lehrkräfte"
}
} }
} }
} }
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
"menu_title": "Substitutions" "menu_title": "Substitutions"
}, },
"supervisions": { "supervisions": {
"title": "Supervision",
"menu_title_daily": "Daily supervisions" "menu_title_daily": "Daily supervisions"
}, },
"event": { "event": {
...@@ -35,7 +36,7 @@ ...@@ -35,7 +36,7 @@
"amend": { "amend": {
"edit_button": "Change", "edit_button": "Change",
"delete_button": "Reset", "delete_button": "Reset",
"delete_dialog": " Are you sure you want to delete this substitution?", "delete_dialog": "Are you sure you want to delete this substitution?",
"delete_success": "The substitution was deleted successfully.", "delete_success": "The substitution was deleted successfully.",
"title": "Change lesson", "title": "Change lesson",
"subject": "Subject", "subject": "Subject",
......
{ {
"chronos": { "chronos": {
"timetable": { "event": {
"menu_title_all": "Все расписания", "amend": {
"menu_title_my": "Моё расписание" "cancelled": "Отменено",
"rooms": "Комнаты"
}
}, },
"supervisions": {
"menu_title_daily": "Ежедневные наблюдения"
},
"menu_title": "Расписания",
"lessons": { "lessons": {
"menu_title_daily": "Ежедневные уроки" "menu_title_daily": "Ежедневные уроки"
}, },
"menu_title": "Расписания",
"substitutions": { "substitutions": {
"menu_title": "Замены" "menu_title": "Замены"
},
"supervisions": {
"menu_title_daily": "Ежедневные наблюдения"
},
"timetable": {
"menu_title_all": "Все расписания",
"menu_title_my": "Моё расписание",
"types": {
"groups": "Группы",
"rooms": "Комнаты"
}
} }
} }
} }
{ {
"chronos": { "chronos": {
"menu_title": "Розклади", "event": {
"timetable": { "amend": {
"menu_title_all": "Усі розклади", "cancelled": "Скасовано",
"menu_title_my": "Мій розклад" "rooms": "Кімнати"
}
},
"lessons": {
"menu_title_daily": "Щоденні уроки"
}, },
"menu_title": "Розклади",
"substitutions": { "substitutions": {
"menu_title": "Заміни" "menu_title": "Заміни"
}, },
"supervisions": { "supervisions": {
"menu_title_daily": "Щоденні спостереження" "menu_title_daily": "Щоденні спостереження"
}, },
"lessons": { "timetable": {
"menu_title_daily": "Щоденні уроки" "menu_title_all": "Усі розклади",
"menu_title_my": "Мій розклад",
"types": {
"groups": "Групи",
"rooms": "Кімнати"
}
} }
} }
} }
...@@ -14,7 +14,6 @@ from django.db import models ...@@ -14,7 +14,6 @@ from django.db import models
from django.db.models import Max, Min, Q, QuerySet from django.db.models import Max, Min, Q, QuerySet
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.dispatch import receiver from django.dispatch import receiver
from django.forms import Media
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
...@@ -66,8 +65,8 @@ from aleksis.core.mixins import ( ...@@ -66,8 +65,8 @@ from aleksis.core.mixins import (
GlobalPermissionModel, GlobalPermissionModel,
SchoolTermRelatedExtensibleModel, SchoolTermRelatedExtensibleModel,
) )
from aleksis.core.models import CalendarEvent, DashboardWidget, Group, Person, Room, SchoolTerm from aleksis.core.models import CalendarEvent, Group, Person, Room, SchoolTerm
from aleksis.core.util.core_helpers import has_person from aleksis.core.util.core_helpers import get_site_preferences, has_person
class ValidityRange(ExtensibleModel): class ValidityRange(ExtensibleModel):
...@@ -618,45 +617,6 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) ...@@ -618,45 +617,6 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
verbose_name_plural = _("Lesson periods") verbose_name_plural = _("Lesson periods")
class TimetableWidget(DashboardWidget):
template = "chronos/widget.html"
def get_context(self, request):
from aleksis.apps.chronos.util.build import build_timetable # noqa
context = {"has_plan": True}
wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())
if has_person(request.user):
person = request.user.person
type_ = person.timetable_type
# Build timetable
timetable = build_timetable("person", person, wanted_day)
if type_ is None:
# If no student or teacher, redirect to all timetables
context["has_plan"] = False
else:
context["timetable"] = timetable
context["holiday"] = Holiday.on_day(wanted_day)
context["type"] = type_
context["day"] = wanted_day
context["periods"] = TimePeriod.get_times_dict()
context["smart"] = True
else:
context["has_plan"] = False
return context
media = Media(css={"all": ("css/chronos/timetable.css",)})
class Meta:
proxy = True
verbose_name = _("Timetable widget")
verbose_name_plural = _("Timetable widgets")
class AbsenceReason(ExtensibleModel): class AbsenceReason(ExtensibleModel):
short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True) short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True)
name = models.CharField(verbose_name=_("Name"), blank=True, max_length=255) name = models.CharField(verbose_name=_("Name"), blank=True, max_length=255)
...@@ -1478,6 +1438,10 @@ class LessonEvent(CalendarEvent): ...@@ -1478,6 +1438,10 @@ class LessonEvent(CalendarEvent):
"""Get the description of the lesson event.""" """Get the description of the lesson event."""
return render_to_string("chronos/lesson_event_description.txt", {"event": reference_object}) return render_to_string("chronos/lesson_event_description.txt", {"event": reference_object})
@classmethod
def get_color(cls, request: HttpRequest | None = None) -> str:
return get_site_preferences()["chronos__lesson_color"]
@classmethod @classmethod
def value_color(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str: def value_color(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str:
"""Get the color of the lesson event.""" """Get the color of the lesson event."""
...@@ -1525,6 +1489,7 @@ class LessonEvent(CalendarEvent): ...@@ -1525,6 +1489,7 @@ class LessonEvent(CalendarEvent):
"amends": cls.value_meta(reference_object.amends, request) "amends": cls.value_meta(reference_object.amends, request)
if reference_object.amends if reference_object.amends
else None, else None,
"title": reference_object.title,
"teachers": [ "teachers": [
{ {
"id": t.pk, "id": t.pk,
...@@ -1600,6 +1565,9 @@ class LessonEvent(CalendarEvent): ...@@ -1600,6 +1565,9 @@ class LessonEvent(CalendarEvent):
return objs.for_room(obj_id) return objs.for_room(obj_id)
elif type_ == "COURSE": elif type_ == "COURSE":
return objs.for_course(obj_id) return objs.for_course(obj_id)
if "own" in params:
return objs
if request: if request:
return objs.for_person(request.user.person) return objs.for_person(request.user.person)
return objs return objs
...@@ -1690,6 +1658,10 @@ class SupervisionEvent(LessonEvent): ...@@ -1690,6 +1658,10 @@ class SupervisionEvent(LessonEvent):
"chronos/supervision_event_description.txt", {"event": reference_object} "chronos/supervision_event_description.txt", {"event": reference_object}
) )
@classmethod
def get_color(cls, request: HttpRequest | None = None) -> str:
return get_site_preferences()["chronos__supervision_color"]
@classmethod @classmethod
def get_objects( def get_objects(
cls, request: HttpRequest | None = None, params: dict[str, any] | None = None cls, request: HttpRequest | None = None, params: dict[str, any] | None = None
......
...@@ -2,9 +2,17 @@ from datetime import time ...@@ -2,9 +2,17 @@ from datetime import time
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from colorfield.widgets import ColorWidget
from dynamic_preferences.preferences import Section from dynamic_preferences.preferences import Section
from dynamic_preferences.types import BooleanPreference, IntegerPreference, TimePreference from dynamic_preferences.types import (
BooleanPreference,
IntegerPreference,
ModelMultipleChoicePreference,
StringPreference,
TimePreference,
)
from aleksis.core.models import GroupType
from aleksis.core.registries import person_preferences_registry, site_preferences_registry from aleksis.core.registries import person_preferences_registry, site_preferences_registry
chronos = Section("chronos", verbose_name=_("Timetables")) chronos = Section("chronos", verbose_name=_("Timetables"))
...@@ -107,3 +115,41 @@ class SendNotificationsPerson(BooleanPreference): ...@@ -107,3 +115,41 @@ class SendNotificationsPerson(BooleanPreference):
name = "send_notifications" name = "send_notifications"
default = True default = True
verbose_name = _("Send notifications for current timetable changes") verbose_name = _("Send notifications for current timetable changes")
@site_preferences_registry.register
class GroupTypesTimetables(ModelMultipleChoicePreference):
section = chronos
name = "group_types_timetables"
required = False
default = []
model = GroupType
verbose_name = _("Group types to show in timetables")
help_text = _("If you leave it empty, all groups will be shown.")
def get_queryset(self):
return GroupType.objects.managed_and_unmanaged()
@site_preferences_registry.register
class LessonEventFeedColor(StringPreference):
"""Color for the lesson calendar feed."""
section = chronos
name = "lesson_color"
default = "#a7ffeb"
verbose_name = _("Lesson calendar feed color")
widget = ColorWidget
required = True
@site_preferences_registry.register
class SupervisionEventFeedColor(StringPreference):
"""Color for the supervision calendar feed."""
section = chronos
name = "supervision_color"
default = "#e6ee9c"
verbose_name = _("Supervision calendar feed color")
widget = ColorWidget
required = True
...@@ -17,7 +17,7 @@ from aleksis.core.models import CalendarEvent, Group, Person, Room ...@@ -17,7 +17,7 @@ from aleksis.core.models import CalendarEvent, Group, Person, Room
from aleksis.core.schema.base import DeleteMutation, FilterOrderList from aleksis.core.schema.base import DeleteMutation, FilterOrderList
from ..models import LessonEvent from ..models import LessonEvent
from ..util.chronos_helpers import get_classes, get_rooms, get_teachers from ..util.chronos_helpers import get_groups, get_rooms, get_teachers
class TimetablePersonType(DjangoObjectType): class TimetablePersonType(DjangoObjectType):
...@@ -41,15 +41,6 @@ class TimetableRoomType(DjangoObjectType): ...@@ -41,15 +41,6 @@ class TimetableRoomType(DjangoObjectType):
skip_registry = True skip_registry = True
# There is another unrelated CalendarEventType in aleksis/core/schema/calendar
# This CalendarEventType is needed for the inherited amends field of LessonEvent
# to work in the graphql query.
class CalendarEventForLessonEventType(DjangoObjectType):
class Meta:
model = CalendarEvent
fields = ("id", "amends", "datetime_start", "datetime_end")
class LessonEventType(DjangoObjectType): class LessonEventType(DjangoObjectType):
class Meta: class Meta:
model = LessonEvent model = LessonEvent
...@@ -173,14 +164,14 @@ class Query(graphene.ObjectType): ...@@ -173,14 +164,14 @@ class Query(graphene.ObjectType):
return get_teachers(info.context.user) return get_teachers(info.context.user)
def resolve_timetable_groups(self, info, **kwargs): def resolve_timetable_groups(self, info, **kwargs):
return get_classes(info.context.user) return get_groups(info.context.user)
def resolve_timetable_rooms(self, info, **kwargs): def resolve_timetable_rooms(self, info, **kwargs):
return get_rooms(info.context.user) return get_rooms(info.context.user)
def resolve_available_timetables(self, info, **kwargs): def resolve_available_timetables(self, info, **kwargs):
all_timetables = [] all_timetables = []
for group in get_classes(info.context.user): for group in get_groups(info.context.user):
all_timetables.append( all_timetables.append(
TimetableObjectType( TimetableObjectType(
id=group.id, id=group.id,
...@@ -191,7 +182,6 @@ class Query(graphene.ObjectType): ...@@ -191,7 +182,6 @@ class Query(graphene.ObjectType):
) )
for teacher in get_teachers(info.context.user): for teacher in get_teachers(info.context.user):
print(teacher.full_name)
all_timetables.append( all_timetables.append(
TimetableObjectType( TimetableObjectType(
id=teacher.id, id=teacher.id,
......
from __future__ import annotations
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django_tables2.utils import A, Accessor
from .models import LessonPeriod, Supervision
def _title_attr_from_lesson_or_supervision_state(
record: LessonPeriod | Supervision | None = None,
table: LessonsTable | SupervisionsTable | None = None,
) -> str:
"""Return HTML title depending on lesson or supervision state."""
if record.get_substitution():
if hasattr(record.get_substitution(), "cancelled") and record.get_substitution().cancelled:
return _("Lesson cancelled")
else:
return _("Substituted")
else:
return ""
class SubstitutionColumn(tables.Column):
def render(self, value, record: LessonPeriod | Supervision | None = None):
if record.get_substitution():
return (
format_html(
"<s>{}</s> → {}",
value,
self.substitution_accessor.resolve(record.get_substitution()),
)
if self.substitution_accessor.resolve(record.get_substitution())
else format_html(
"<s>{}</s>",
value,
)
)
return value
def __init__(self, *args, **kwargs):
self.substitution_accessor = Accessor(kwargs.pop("substitution_accessor"))
super().__init__(*args, **kwargs)
class LessonStatusColumn(tables.Column):
def render(self, record: LessonPeriod | Supervision | None = None):
if record.get_substitution():
return (
format_html(
'<span class="new badge green">{}</span>',
_("cancelled"),
)
if hasattr(record.get_substitution(), "cancelled")
and record.get_substitution().cancelled
else format_html(
'<span class="new badge orange">{}</span>',
_("substituted"),
)
)
return ""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class LessonsTable(tables.Table):
"""Table for daily lessons and management of substitutions."""
class Meta:
attrs = {"class": "highlight, striped"}
row_attrs = {
"title": _title_attr_from_lesson_or_supervision_state,
}
period__period = tables.Column(accessor="period__period")
lesson__groups = tables.Column(accessor="lesson__group_names", verbose_name=_("Groups"))
status = LessonStatusColumn(verbose_name=_("Status"), empty_values=())
lesson__teachers = SubstitutionColumn(
accessor="lesson__teacher_names",
substitution_accessor="teacher_names",
verbose_name=_("Teachers"),
)
lesson__subject = SubstitutionColumn(
accessor="lesson__subject", substitution_accessor="subject"
)
room = SubstitutionColumn(accessor="room", substitution_accessor="room")
edit_substitution = tables.LinkColumn(
"edit_substitution",
args=[A("id"), A("_week")],
text=_("Substitution"),
attrs={"a": {"class": "btn-flat waves-effect waves-orange"}},
verbose_name=_("Manage substitution"),
)
class SupervisionsTable(tables.Table):
"""Table for daily supervisions and management of substitutions."""
class Meta:
attrs = {"class": "highlight, striped"}
row_attrs = {
"title": _title_attr_from_lesson_or_supervision_state,
}
break_item = tables.Column(accessor="break_item")
status = LessonStatusColumn(verbose_name=_("Status"), empty_values=())
area = tables.Column(accessor="area")
teacher = SubstitutionColumn(
accessor="teacher",
substitution_accessor="teacher",
verbose_name=_("Teachers"),
)
edit_substitution = tables.LinkColumn(
"edit_supervision_substitution",
args=[A("id"), A("_week")],
text=_("Substitution"),
attrs={"a": {"class": "btn-flat waves-effect waves-orange"}},
verbose_name=_("Manage substitution"),
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment