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 (66)
Showing
with 717 additions and 89 deletions
<!-- AlekSIS is developed on EduGit. GitHub only serves as
backup mirror and to help people find the project. If
possible, please submit your merge request on EduGit!
EduGit accepts logins with GitHub accounts.
-->
[ ] I have read the above and have no way to contribute on EduGit
[ ] I understand that GitHub's terms of service exclude young and
learning contributors, but still cannot contribute on EduGit
instead.
......@@ -9,6 +9,56 @@ and this project adheres to `Semantic Versioning`_.
Unreleased
----------
Added
~~~~~
* Add support for operation with SPA.
Changed
~~~~~~~
* Improve rendering of substituted or cancelled items on daily lessons/supervisions pages.
Fixed
~~~~~
* Migrations wouldn't run on new installations due to a faulty reference to SchoolTerm.
* The daily lessons page did not work correctly due to faulty pre-filtering of lessons.
* Substitution form teacher selections also included students
* Getting the max and min periods for events failed due to using always the current school term.
Typically, that caused problems when the schedule changed (more or less periods on a day).
`2.5`_ - 2022-11-12
-------------------
Added
~~~~~
* Add overview page of all daily supervisions.
* Add form to add substitutions to supervisions.
* Add filter to daily lessons page.
* Display initial lesson data with substituted lessons in daily lessons table.
`2.4.2`_ - 2022-11-02
---------------------
Fixed
~~~~~
* The date picker did not work with some date formats.
* Send notifications for changes done via daily lessons page.
* Lessons with same subject and groups but different teachers were not considered equal.
* Lessons without any groups were displayed as cancelled if there was an event in the same time period.
`2.4.1`_ - 2022-08-31
---------------------
Fixed
~~~~~
* The week and lesson period fields in the edit substitution form could be changed
and the comment field was missing.
`2.4`_ - 2022-06-23
-------------------
......@@ -313,3 +363,6 @@ Fixed
.. _2.2.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/2.2.1
.. _2.3: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/2.3
.. _2.4: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/2.4
.. _2.4.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/2.4.1
.. _2.4.2: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/2.4.2
.. _2.5: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/2.5
......@@ -5,6 +5,8 @@ from django.utils.html import format_html
from guardian.admin import GuardedModelAdmin
from aleksis.core.models import Room
from .models import (
Absence,
AbsenceReason,
......@@ -16,7 +18,6 @@ from .models import (
Lesson,
LessonPeriod,
LessonSubstitution,
Room,
Subject,
Supervision,
SupervisionArea,
......
from typing 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
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget
from material import Layout
from .models import AutomaticPlan, LessonSubstitution
from .models import AutomaticPlan, LessonSubstitution, SupervisionSubstitution
from .util.chronos_helpers import get_teachers
class LessonSubstitutionForm(forms.ModelForm):
......@@ -11,7 +12,7 @@ class LessonSubstitutionForm(forms.ModelForm):
class Meta:
model = LessonSubstitution
fields = ["week", "lesson_period", "subject", "teachers", "room", "cancelled"]
fields = ["subject", "teachers", "room", "cancelled", "comment"]
widgets = {
"teachers": ModelSelect2MultipleWidget(
search_fields=[
......@@ -20,9 +21,37 @@ class LessonSubstitutionForm(forms.ModelForm):
"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")
......
import { hasPersonValidator } from "aleksis.core/routeValidators";
export default
{
meta: {
inMenu: true,
titleKey: "chronos.menu_title",
icon: "mdi-school-outline",
validators: [
hasPersonValidator
]
},
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",
},
},
{
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",
},
},
{
path: "timetable/my/:year/:month/:day/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.myTimetableByDate",
},
{
path: "timetable/:type_/:pk/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.timetable",
},
{
path: "timetable/:type_/:pk/:year/:week/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.timetableByWeek",
},
{
path: "timetable/:type_/:pk/print/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.timetablePrint",
},
{
path: "timetable/:type_/:pk/:regular/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.timetableRegular",
},
{
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",
},
},
{
path: "lessons/:year/:month/:day/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.lessonsDayByDate",
},
{
path: "lessons/:id_/:week/substitution/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.editSubstitution",
},
{
path: "lessons/:id_/:week/substitution/delete/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.deleteSubstitution",
},
{
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",
},
},
{
path: "substitutions/print/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.substitutionsPrint",
},
{
path: "substitutions/:year/:month/:day/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.substitutionsByDate",
},
{
path: "substitutions/:year/:month/:day/print/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.substitutionsPrintByDate",
},
{
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",
},
},
{
path: "supervisions/:year/:month/:day/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.supervisionsDayByDate",
},
{
path: "supervisions/:id_/:week/substitution/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.editSupervisionSubstitution",
},
{
path: "supervisions/:id_/:week/substitution/delete/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.deleteSupervisionSubstitution",
},
],
}
\ No newline at end of file
{
"chronos": {
"menu_title": "Stundenpläne"
}
}
\ No newline at end of file
{
"chronos": {
"menu_title": "Timetables",
"timetable": {
"menu_title_all": "All timetables",
"menu_title_my": "My timetable"
},
"lessons": {
"menu_title_daily": "Daily lessons"
},
"substitutions": {
"menu_title": "Substitutions"
},
"supervisions": {
"menu_title_daily": "Daily supervisions"
}
}
}
\ No newline at end of file
......@@ -45,6 +45,17 @@ MENUS = {
),
],
},
{
"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",
......
# Generated by Django 3.2.13 on 2022-11-09 23:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('chronos', '0011_exam'),
]
operations = [
migrations.AlterModelOptions(
name='chronosglobalpermissions',
options={'managed': False, 'permissions': (('view_all_room_timetables', 'Can view all room timetables'), ('view_all_group_timetables', 'Can view all group timetables'), ('view_all_person_timetables', 'Can view all person timetables'), ('view_timetable_overview', 'Can view timetable overview'), ('view_lessons_day', 'Can view all lessons per day'), ('view_supervisions_day', 'Can view all supervisions per day'))},
),
]
# Generated by Django 3.2.16 on 2022-11-30 17:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0047_add_room_model'),
('chronos', '0012_add_supervision_global_permission'),
]
operations = [
migrations.AlterField(
model_name='absence',
name='room',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='core.room', verbose_name='Room'),
),
migrations.AlterField(
model_name='event',
name='rooms',
field=models.ManyToManyField(related_name='events', to='core.Room', verbose_name='Rooms'),
),
migrations.AlterField(
model_name='extralesson',
name='room',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='extra_lessons', to='core.room', verbose_name='Room'),
),
migrations.AlterField(
model_name='lessonperiod',
name='room',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lesson_periods', to='core.room', verbose_name='Room'),
),
migrations.AlterField(
model_name='lessonsubstitution',
name='room',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.room', verbose_name='Room'),
),
migrations.AlterField(
model_name='timeperiod',
name='weekday',
field=models.PositiveSmallIntegerField(choices=[(0, 'Montag'), (1, 'Dienstag'), (2, 'Mittwoch'), (3, 'Donnerstag'), (4, 'Freitag'), (5, 'Samstag'), (6, 'Sonntag')], verbose_name='Week day'),
),
migrations.DeleteModel(
name='Room',
),
]
......@@ -67,7 +67,7 @@ from aleksis.core.mixins import (
GlobalPermissionModel,
SchoolTermRelatedExtensibleModel,
)
from aleksis.core.models import DashboardWidget, Group, SchoolTerm
from aleksis.core.models import DashboardWidget, Group, Room, SchoolTerm
from aleksis.core.util.core_helpers import has_person
from aleksis.core.util.pdf import generate_pdf_from_template
......@@ -360,28 +360,6 @@ class Subject(ExtensibleModel):
]
class Room(ExtensibleModel):
short_name = models.CharField(verbose_name=_("Short name"), max_length=255)
name = models.CharField(verbose_name=_("Long name"), max_length=255)
def __str__(self) -> str:
return f"{self.name} ({self.short_name})"
def get_absolute_url(self) -> str:
return reverse("timetable", args=["room", self.id])
class Meta:
permissions = (("view_room_timetable", _("Can view room timetable")),)
ordering = ["name", "short_name"]
verbose_name = _("Room")
verbose_name_plural = _("Rooms")
constraints = [
models.UniqueConstraint(
fields=["site_id", "short_name"], name="unique_short_name_per_site_room"
),
]
class Lesson(ValidityRangeRelatedExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
subject = models.ForeignKey(
"Subject",
......@@ -415,6 +393,18 @@ class Lesson(ValidityRangeRelatedExtensibleModel, GroupPropertiesMixin, TeacherP
"""Get teachers relation."""
return self.teachers
@property
def _equal_lessons(self):
"""Get all lesson periods with equal lessons in the whole school term."""
qs = Lesson.objects.filter(
subject=self.subject,
validity__school_term=self.validity.school_term,
)
for group in self.groups.all():
qs = qs.filter(groups=group)
return qs
def __str__(self):
return f"{format_m2m(self.groups)}, {self.subject.short_name}, {format_m2m(self.teachers)}"
......@@ -451,7 +441,9 @@ class LessonSubstitution(ExtensibleModel, TeacherPropertiesMixin, WeekRelatedMix
blank=True,
verbose_name=_("Teachers"),
)
room = models.ForeignKey("Room", models.CASCADE, null=True, blank=True, verbose_name=_("Room"))
room = models.ForeignKey(
"core.Room", models.CASCADE, null=True, blank=True, verbose_name=_("Room")
)
cancelled = models.BooleanField(default=False, verbose_name=_("Cancelled?"))
cancelled_for_teachers = models.BooleanField(
......@@ -526,7 +518,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
)
room = models.ForeignKey(
"Room",
"core.Room",
models.CASCADE,
null=True,
related_name="lesson_periods",
......@@ -581,18 +573,10 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
return f"{self.period}, {self.lesson}"
@property
def _equal_lessons(self):
def _equal_lesson_periods(self):
"""Get all lesson periods with equal lessons in the whole school term."""
qs = LessonPeriod.objects.filter(
lesson__subject=self.lesson.subject,
lesson__validity__school_term=self.lesson.validity.school_term,
)
for group in self.lesson.groups.all():
qs = qs.filter(lesson__groups=group)
for teacher in self.lesson.teachers.all():
qs = qs.filter(lesson__teachers=teacher)
return qs
return LessonPeriod.objects.filter(lesson__in=self.lesson._equal_lessons)
@property
def next(self) -> "LessonPeriod":
......@@ -601,7 +585,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
.. warning::
To use this property, the provided lesson period must be annotated with a week.
"""
return self._equal_lessons.next_lesson(self)
return self._equal_lesson_periods.next_lesson(self)
@property
def prev(self) -> "LessonPeriod":
......@@ -610,7 +594,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
.. warning::
To use this property, the provided lesson period must be annotated with a week.
"""
return self._equal_lessons.next_lesson(self, -1)
return self._equal_lesson_periods.next_lesson(self, -1)
def is_replaced_by_event(
self, events: Iterable[Event], groups: Optional[Iterable[Group]] = None
......@@ -627,6 +611,10 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
else:
groups_lesson_period = set(self.lesson.groups.all())
# The lesson period isn't replacable if the lesson has no groups at all
if not groups_lesson_period:
return False
# This lesson period is replaced by an event ...
# ... if all groups of this lesson period are a part of the event ...
if groups_lesson_period.issubset(groups_of_event):
......@@ -743,7 +731,7 @@ class Absence(SchoolTermRelatedExtensibleModel):
verbose_name=_("Group"),
)
room = models.ForeignKey(
"Room",
"core.Room",
on_delete=models.CASCADE,
related_name="absences",
null=True,
......@@ -980,6 +968,11 @@ class Supervision(ValidityRangeRelatedExtensibleModel, WeekAnnotationMixin):
year += 1
return year
def get_calendar_week(self, week: int):
year = self.get_year(week)
return CalendarWeek(year=year, week=week)
def get_substitution(
self, week: Optional[CalendarWeek] = None
) -> Optional[SupervisionSubstitution]:
......@@ -1075,7 +1068,7 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope
)
groups = models.ManyToManyField("core.Group", related_name="events", verbose_name=_("Groups"))
rooms = models.ManyToManyField("Room", related_name="events", verbose_name=_("Rooms"))
rooms = models.ManyToManyField("core.Room", related_name="events", verbose_name=_("Rooms"))
teachers = models.ManyToManyField(
"core.Person", related_name="events", verbose_name=_("Teachers")
)
......@@ -1086,6 +1079,21 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope
else:
return _("Event {pk}").format(pk=self.pk)
def get_period_min(self, day) -> int:
return (
TimePeriod.objects.on_day(day)
.aggregate(period__min=Coalesce(Min("period"), 1))
.get("period__min")
)
def get_period_max(self, day) -> int:
return (
TimePeriod.objects.on_day(day)
.aggregate(period__max=Coalesce(Max("period"), 7))
.get("period__max")
)
@property
def raw_period_from_on_day(self) -> TimePeriod:
"""Get start period on the annotated day (as TimePeriod object).
......@@ -1094,7 +1102,7 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope
"""
day = getattr(self, "_date", timezone.now().date())
if day != self.date_start:
return TimePeriod.from_period(TimePeriod.period_min, day)
return TimePeriod.from_period(self.get_period_min(day), day)
else:
return self.period_from
......@@ -1106,7 +1114,7 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope
"""
day = getattr(self, "_date", timezone.now().date())
if day != self.date_end:
return TimePeriod.from_period(TimePeriod.period_max, day)
return TimePeriod.from_period(self.get_period_max(day), day)
else:
return self.period_to
......@@ -1206,7 +1214,7 @@ class ExtraLesson(
verbose_name=_("Teachers"),
)
room = models.ForeignKey(
"Room",
"core.Room",
models.CASCADE,
null=True,
related_name="extra_lessons",
......@@ -1354,4 +1362,5 @@ class ChronosGlobalPermissions(GlobalPermissionModel):
("view_all_person_timetables", _("Can view all person timetables")),
("view_timetable_overview", _("Can view timetable overview")),
("view_lessons_day", _("Can view all lessons per day")),
("view_supervisions_day", _("Can view all supervisions per day")),
)
......@@ -17,7 +17,7 @@ class UseParentGroups(BooleanPreference):
default = False
verbose_name = _("Use parent groups in timetable views")
help_text = _(
"If an lesson or substitution has only one group"
"If a lesson or substitution has only one group"
" and this group has parent groups,"
" show the parent groups instead of the original group."
)
......@@ -39,7 +39,7 @@ class ShortenGroupsLimit(IntegerPreference):
default = 4
verbose_name = _("Limit of groups for shortening of groups")
help_text = _(
"If an user activates shortening of groups,"
"If a user activates shortening of groups,"
"they will be collapsed if there are more groups than this limit."
)
......
......@@ -48,6 +48,22 @@ view_substitutions_predicate = has_person & (
)
add_perm("chronos.view_substitutions_rule", view_substitutions_predicate)
# View all supervisions per day
view_supervisions_day_predicate = has_person & has_global_perm("chronos.view_supervisions_day")
add_perm("chronos.view_supervisions_day_rule", view_supervisions_day_predicate)
# Edit supervision substitution
edit_supervision_substitution_predicate = has_person & (
has_global_perm("chronos.change_supervisionsubstitution")
)
add_perm("chronos.edit_supervision_substitution_rule", edit_supervision_substitution_predicate)
# Delete supervision substitution
delete_supervision_substitution_predicate = has_person & (
has_global_perm("chronos.delete_supervisionsubstitution")
)
add_perm("chronos.delete_supervision_substitution_rule", delete_supervision_substitution_predicate)
# View room (timetable)
view_room_predicate = has_person & has_room_timetable_perm
add_perm("chronos.view_room_rule", view_room_predicate)
from aleksis.core.util.search import Indexable, SearchIndex
from .models import Room
class RoomIndex(SearchIndex, Indexable):
"""Haystack index for searching rooms."""
model = Room
......@@ -10,10 +10,7 @@ function loadNew() {
}
function onDateChanged() {
var str = $("#date").val();
var split = str.split(".");
activeDate = new Date(split[2], split[1] - 1, split[0]);
updateDatepicker();
activeDate = M.Datepicker.getInstance($("#date")).date;
loadNew();
}
......
from __future__ import annotations
from typing import Optional
from typing import Optional, Union
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
from django_tables2.utils import A, Accessor
from .models import LessonPeriod
from .models import LessonPeriod, Supervision
def _css_class_from_lesson_state(
record: Optional[LessonPeriod] = None, table: Optional[LessonsTable] = None
def _title_attr_from_lesson_or_supervision_state(
record: Optional[Union[LessonPeriod, Supervision]] = None,
table: Optional[Union[LessonsTable, SupervisionsTable]] = None,
) -> str:
"""Return CSS class depending on lesson state."""
"""Return HTML title depending on lesson or supervision state."""
if record.get_substitution():
if record.get_substitution().cancelled:
return "success"
if hasattr(record.get_substitution(), "cancelled") and record.get_substitution().cancelled:
return _("Lesson cancelled")
else:
return "warning"
return _("Substituted")
else:
return ""
class SubstitutionColumn(tables.Column):
def render(self, value, record: Optional[Union[LessonPeriod, Supervision]] = 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: Optional[Union[LessonPeriod, Supervision]] = 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"}
row_attrs = {"class": _css_class_from_lesson_state}
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"))
lesson__teachers = tables.Column(accessor="lesson__teacher_names", verbose_name=_("Teachers"))
lesson__subject = tables.Column(accessor="lesson__subject")
room = tables.Column(accessor="room")
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")],
......@@ -42,3 +96,29 @@ class LessonsTable(tables.Table):
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"),
)
......@@ -12,6 +12,7 @@
{% block page_title %}{% blocktrans %}Edit substitution{% endblocktrans %}{% endblock %}
{% block content %}
<p class="flow-text">{{ date }}: {{ lesson_period }}</p>
<form method="post">
{% csrf_token %}
......@@ -19,7 +20,6 @@
{% include "core/partials/save_button.html" %}
{% if substitution %}
{# FIXME Respect year as well #}
<a href="{% url 'delete_substitution' substitution.lesson_period.id substitution.week %}"
class="btn red waves-effect waves-light">
<i class="material-icons iconify left" data-icon="mdi:delete-outline"></i> {% trans "Delete" %}
......
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load material_form i18n any_js %}
{% block extra_head %}
{{ edit_supervision_substitution_form.media.css }}
{% include_css "select2-materialize" %}
{% endblock %}
{% block browser_title %}{% blocktrans %}Edit substitution.{% endblocktrans %}{% endblock %}
{% block page_title %}{% blocktrans %}Edit substitution{% endblocktrans %}{% endblock %}
{% block content %}
<p class="flow-text">{{ date }}: {{ supervision }}</p>
<form method="post">
{% csrf_token %}
{% form form=edit_supervision_substitution_form %}{% endform %}
{% include "core/partials/save_button.html" %}
{% if substitution %}
<a href="{% url 'delete_supervision_substitution' substitution.supervision.id week %}"
class="btn red waves-effect waves-light">
<i class="material-icons iconify left" data-icon="mdi:delete-outline"></i> {% trans "Delete" %}
</a>
{% endif %}
</form>
{% include_js "select2-materialize" %}
{{ edit_supervision_substitution_form.media.js }}
{% endblock %}
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load i18n %}
{% load i18n material_form any_js %}
{% load render_table from django_tables2 %}
{% block extra_head %}
{{ lesson_periods_filter.form.media.css }}
{% include_css "select2-materialize" %}
{% endblock %}
{% block browser_title %}{% blocktrans %}Lessons{% endblocktrans %}{% endblock %}
{% block no_page_title %}{% endblock %}
......@@ -14,6 +19,13 @@
var dest = Urls.lessonsDay();
</script>
<h2>{% trans "Filter lessons" %}</h2>
<form method="get">
{% form form=lesson_periods_filter.form %}{% endform %}
{% trans "Search" as caption %}
{% include "core/partials/save_button.html" with caption=caption icon="mdi:search" %}
</form>
<div class="row no-margin">
<div class="col s12 m6 l8 no-padding">
<h1>{% blocktrans %}Lessons{% endblocktrans %} {{ day|date:"l" }}, {{ day }}</h1>
......@@ -24,4 +36,7 @@
</div>
{% render_table lessons_table %}
{% include_js "select2-materialize" %}
{{ lesson_periods_filter.form.media.js }}
{% endblock %}