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`_. ...@@ -9,6 +9,56 @@ and this project adheres to `Semantic Versioning`_.
Unreleased 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 `2.4`_ - 2022-06-23
------------------- -------------------
...@@ -313,3 +363,6 @@ Fixed ...@@ -313,3 +363,6 @@ Fixed
.. _2.2.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos/-/tags/2.2.1 .. _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.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: 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 ...@@ -5,6 +5,8 @@ from django.utils.html import format_html
from guardian.admin import GuardedModelAdmin from guardian.admin import GuardedModelAdmin
from aleksis.core.models import Room
from .models import ( from .models import (
Absence, Absence,
AbsenceReason, AbsenceReason,
...@@ -16,7 +18,6 @@ from .models import ( ...@@ -16,7 +18,6 @@ from .models import (
Lesson, Lesson,
LessonPeriod, LessonPeriod,
LessonSubstitution, LessonSubstitution,
Room,
Subject, Subject,
Supervision, Supervision,
SupervisionArea, 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 import forms
from django_select2.forms import ModelSelect2MultipleWidget from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget
from material import Layout 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): class LessonSubstitutionForm(forms.ModelForm):
...@@ -11,7 +12,7 @@ class LessonSubstitutionForm(forms.ModelForm): ...@@ -11,7 +12,7 @@ class LessonSubstitutionForm(forms.ModelForm):
class Meta: class Meta:
model = LessonSubstitution model = LessonSubstitution
fields = ["week", "lesson_period", "subject", "teachers", "room", "cancelled"] fields = ["subject", "teachers", "room", "cancelled", "comment"]
widgets = { widgets = {
"teachers": ModelSelect2MultipleWidget( "teachers": ModelSelect2MultipleWidget(
search_fields=[ search_fields=[
...@@ -20,9 +21,37 @@ class LessonSubstitutionForm(forms.ModelForm): ...@@ -20,9 +21,37 @@ class LessonSubstitutionForm(forms.ModelForm):
"short_name__icontains", "short_name__icontains",
], ],
attrs={"data-minimum-input-length": 0, "class": "browser-default"}, 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): class AutomaticPlanForm(forms.ModelForm):
layout = Layout("slug", "name", "number_of_days", "show_header_box") 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 = { ...@@ -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"), "name": _("Substitutions"),
"url": "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 ( ...@@ -67,7 +67,7 @@ from aleksis.core.mixins import (
GlobalPermissionModel, GlobalPermissionModel,
SchoolTermRelatedExtensibleModel, 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.core_helpers import has_person
from aleksis.core.util.pdf import generate_pdf_from_template from aleksis.core.util.pdf import generate_pdf_from_template
...@@ -360,28 +360,6 @@ class Subject(ExtensibleModel): ...@@ -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): class Lesson(ValidityRangeRelatedExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
subject = models.ForeignKey( subject = models.ForeignKey(
"Subject", "Subject",
...@@ -415,6 +393,18 @@ class Lesson(ValidityRangeRelatedExtensibleModel, GroupPropertiesMixin, TeacherP ...@@ -415,6 +393,18 @@ class Lesson(ValidityRangeRelatedExtensibleModel, GroupPropertiesMixin, TeacherP
"""Get teachers relation.""" """Get teachers relation."""
return self.teachers 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): def __str__(self):
return f"{format_m2m(self.groups)}, {self.subject.short_name}, {format_m2m(self.teachers)}" return f"{format_m2m(self.groups)}, {self.subject.short_name}, {format_m2m(self.teachers)}"
...@@ -451,7 +441,9 @@ class LessonSubstitution(ExtensibleModel, TeacherPropertiesMixin, WeekRelatedMix ...@@ -451,7 +441,9 @@ class LessonSubstitution(ExtensibleModel, TeacherPropertiesMixin, WeekRelatedMix
blank=True, blank=True,
verbose_name=_("Teachers"), 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 = models.BooleanField(default=False, verbose_name=_("Cancelled?"))
cancelled_for_teachers = models.BooleanField( cancelled_for_teachers = models.BooleanField(
...@@ -526,7 +518,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) ...@@ -526,7 +518,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
) )
room = models.ForeignKey( room = models.ForeignKey(
"Room", "core.Room",
models.CASCADE, models.CASCADE,
null=True, null=True,
related_name="lesson_periods", related_name="lesson_periods",
...@@ -581,18 +573,10 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) ...@@ -581,18 +573,10 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
return f"{self.period}, {self.lesson}" return f"{self.period}, {self.lesson}"
@property @property
def _equal_lessons(self): def _equal_lesson_periods(self):
"""Get all lesson periods with equal lessons in the whole school term.""" """Get all lesson periods with equal lessons in the whole school term."""
qs = LessonPeriod.objects.filter( return LessonPeriod.objects.filter(lesson__in=self.lesson._equal_lessons)
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
@property @property
def next(self) -> "LessonPeriod": def next(self) -> "LessonPeriod":
...@@ -601,7 +585,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) ...@@ -601,7 +585,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
.. warning:: .. warning::
To use this property, the provided lesson period must be annotated with a week. 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 @property
def prev(self) -> "LessonPeriod": def prev(self) -> "LessonPeriod":
...@@ -610,7 +594,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) ...@@ -610,7 +594,7 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
.. warning:: .. warning::
To use this property, the provided lesson period must be annotated with a week. 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( def is_replaced_by_event(
self, events: Iterable[Event], groups: Optional[Iterable[Group]] = None self, events: Iterable[Event], groups: Optional[Iterable[Group]] = None
...@@ -627,6 +611,10 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel) ...@@ -627,6 +611,10 @@ class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel)
else: else:
groups_lesson_period = set(self.lesson.groups.all()) 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 ... # This lesson period is replaced by an event ...
# ... if all groups of this lesson period are a part of the event ... # ... if all groups of this lesson period are a part of the event ...
if groups_lesson_period.issubset(groups_of_event): if groups_lesson_period.issubset(groups_of_event):
...@@ -743,7 +731,7 @@ class Absence(SchoolTermRelatedExtensibleModel): ...@@ -743,7 +731,7 @@ class Absence(SchoolTermRelatedExtensibleModel):
verbose_name=_("Group"), verbose_name=_("Group"),
) )
room = models.ForeignKey( room = models.ForeignKey(
"Room", "core.Room",
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="absences", related_name="absences",
null=True, null=True,
...@@ -980,6 +968,11 @@ class Supervision(ValidityRangeRelatedExtensibleModel, WeekAnnotationMixin): ...@@ -980,6 +968,11 @@ class Supervision(ValidityRangeRelatedExtensibleModel, WeekAnnotationMixin):
year += 1 year += 1
return year return year
def get_calendar_week(self, week: int):
year = self.get_year(week)
return CalendarWeek(year=year, week=week)
def get_substitution( def get_substitution(
self, week: Optional[CalendarWeek] = None self, week: Optional[CalendarWeek] = None
) -> Optional[SupervisionSubstitution]: ) -> Optional[SupervisionSubstitution]:
...@@ -1075,7 +1068,7 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope ...@@ -1075,7 +1068,7 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope
) )
groups = models.ManyToManyField("core.Group", related_name="events", verbose_name=_("Groups")) 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( teachers = models.ManyToManyField(
"core.Person", related_name="events", verbose_name=_("Teachers") "core.Person", related_name="events", verbose_name=_("Teachers")
) )
...@@ -1086,6 +1079,21 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope ...@@ -1086,6 +1079,21 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope
else: else:
return _("Event {pk}").format(pk=self.pk) 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 @property
def raw_period_from_on_day(self) -> TimePeriod: def raw_period_from_on_day(self) -> TimePeriod:
"""Get start period on the annotated day (as TimePeriod object). """Get start period on the annotated day (as TimePeriod object).
...@@ -1094,7 +1102,7 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope ...@@ -1094,7 +1102,7 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope
""" """
day = getattr(self, "_date", timezone.now().date()) day = getattr(self, "_date", timezone.now().date())
if day != self.date_start: if day != self.date_start:
return TimePeriod.from_period(TimePeriod.period_min, day) return TimePeriod.from_period(self.get_period_min(day), day)
else: else:
return self.period_from return self.period_from
...@@ -1106,7 +1114,7 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope ...@@ -1106,7 +1114,7 @@ class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPrope
""" """
day = getattr(self, "_date", timezone.now().date()) day = getattr(self, "_date", timezone.now().date())
if day != self.date_end: if day != self.date_end:
return TimePeriod.from_period(TimePeriod.period_max, day) return TimePeriod.from_period(self.get_period_max(day), day)
else: else:
return self.period_to return self.period_to
...@@ -1206,7 +1214,7 @@ class ExtraLesson( ...@@ -1206,7 +1214,7 @@ class ExtraLesson(
verbose_name=_("Teachers"), verbose_name=_("Teachers"),
) )
room = models.ForeignKey( room = models.ForeignKey(
"Room", "core.Room",
models.CASCADE, models.CASCADE,
null=True, null=True,
related_name="extra_lessons", related_name="extra_lessons",
...@@ -1354,4 +1362,5 @@ class ChronosGlobalPermissions(GlobalPermissionModel): ...@@ -1354,4 +1362,5 @@ class ChronosGlobalPermissions(GlobalPermissionModel):
("view_all_person_timetables", _("Can view all person timetables")), ("view_all_person_timetables", _("Can view all person timetables")),
("view_timetable_overview", _("Can view timetable overview")), ("view_timetable_overview", _("Can view timetable overview")),
("view_lessons_day", _("Can view all lessons per day")), ("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): ...@@ -17,7 +17,7 @@ class UseParentGroups(BooleanPreference):
default = False default = False
verbose_name = _("Use parent groups in timetable views") verbose_name = _("Use parent groups in timetable views")
help_text = _( 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," " and this group has parent groups,"
" show the parent groups instead of the original group." " show the parent groups instead of the original group."
) )
...@@ -39,7 +39,7 @@ class ShortenGroupsLimit(IntegerPreference): ...@@ -39,7 +39,7 @@ class ShortenGroupsLimit(IntegerPreference):
default = 4 default = 4
verbose_name = _("Limit of groups for shortening of groups") verbose_name = _("Limit of groups for shortening of groups")
help_text = _( 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." "they will be collapsed if there are more groups than this limit."
) )
......
...@@ -48,6 +48,22 @@ view_substitutions_predicate = has_person & ( ...@@ -48,6 +48,22 @@ view_substitutions_predicate = has_person & (
) )
add_perm("chronos.view_substitutions_rule", view_substitutions_predicate) 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 (timetable)
view_room_predicate = has_person & has_room_timetable_perm view_room_predicate = has_person & has_room_timetable_perm
add_perm("chronos.view_room_rule", view_room_predicate) 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() { ...@@ -10,10 +10,7 @@ function loadNew() {
} }
function onDateChanged() { function onDateChanged() {
var str = $("#date").val(); activeDate = M.Datepicker.getInstance($("#date")).date;
var split = str.split(".");
activeDate = new Date(split[2], split[1] - 1, split[0]);
updateDatepicker();
loadNew(); loadNew();
} }
......
from __future__ import annotations 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 _ from django.utils.translation import gettext_lazy as _
import django_tables2 as tables 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( def _title_attr_from_lesson_or_supervision_state(
record: Optional[LessonPeriod] = None, table: Optional[LessonsTable] = None record: Optional[Union[LessonPeriod, Supervision]] = None,
table: Optional[Union[LessonsTable, SupervisionsTable]] = None,
) -> str: ) -> 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():
if record.get_substitution().cancelled: if hasattr(record.get_substitution(), "cancelled") and record.get_substitution().cancelled:
return "success" return _("Lesson cancelled")
else: else:
return "warning" return _("Substituted")
else: else:
return "" 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): class LessonsTable(tables.Table):
"""Table for daily lessons and management of substitutions.""" """Table for daily lessons and management of substitutions."""
class Meta: class Meta:
attrs = {"class": "highlight"} attrs = {"class": "highlight, striped"}
row_attrs = {"class": _css_class_from_lesson_state} row_attrs = {
"title": _title_attr_from_lesson_or_supervision_state,
}
period__period = tables.Column(accessor="period__period") period__period = tables.Column(accessor="period__period")
lesson__groups = tables.Column(accessor="lesson__group_names", verbose_name=_("Groups")) lesson__groups = tables.Column(accessor="lesson__group_names", verbose_name=_("Groups"))
lesson__teachers = tables.Column(accessor="lesson__teacher_names", verbose_name=_("Teachers")) status = LessonStatusColumn(verbose_name=_("Status"), empty_values=())
lesson__subject = tables.Column(accessor="lesson__subject") lesson__teachers = SubstitutionColumn(
room = tables.Column(accessor="room") 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 = tables.LinkColumn(
"edit_substitution", "edit_substitution",
args=[A("id"), A("_week")], args=[A("id"), A("_week")],
...@@ -42,3 +96,29 @@ class LessonsTable(tables.Table): ...@@ -42,3 +96,29 @@ class LessonsTable(tables.Table):
attrs={"a": {"class": "btn-flat waves-effect waves-orange"}}, attrs={"a": {"class": "btn-flat waves-effect waves-orange"}},
verbose_name=_("Manage substitution"), 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 @@ ...@@ -12,6 +12,7 @@
{% block page_title %}{% blocktrans %}Edit substitution{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}Edit substitution{% endblocktrans %}{% endblock %}
{% block content %} {% block content %}
<p class="flow-text">{{ date }}: {{ lesson_period }}</p>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
...@@ -19,7 +20,6 @@ ...@@ -19,7 +20,6 @@
{% include "core/partials/save_button.html" %} {% include "core/partials/save_button.html" %}
{% if substitution %} {% if substitution %}
{# FIXME Respect year as well #}
<a href="{% url 'delete_substitution' substitution.lesson_period.id substitution.week %}" <a href="{% url 'delete_substitution' substitution.lesson_period.id substitution.week %}"
class="btn red waves-effect waves-light"> class="btn red waves-effect waves-light">
<i class="material-icons iconify left" data-icon="mdi:delete-outline"></i> {% trans "Delete" %} <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 -*- #} {# -*- engine:django -*- #}
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load i18n %} {% load i18n material_form any_js %}
{% load render_table from django_tables2 %} {% 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 browser_title %}{% blocktrans %}Lessons{% endblocktrans %}{% endblock %}
{% block no_page_title %}{% endblock %} {% block no_page_title %}{% endblock %}
...@@ -14,6 +19,13 @@ ...@@ -14,6 +19,13 @@
var dest = Urls.lessonsDay(); var dest = Urls.lessonsDay();
</script> </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="row no-margin">
<div class="col s12 m6 l8 no-padding"> <div class="col s12 m6 l8 no-padding">
<h1>{% blocktrans %}Lessons{% endblocktrans %} {{ day|date:"l" }}, {{ day }}</h1> <h1>{% blocktrans %}Lessons{% endblocktrans %} {{ day|date:"l" }}, {{ day }}</h1>
...@@ -24,4 +36,7 @@ ...@@ -24,4 +36,7 @@
</div> </div>
{% render_table lessons_table %} {% render_table lessons_table %}
{% include_js "select2-materialize" %}
{{ lesson_periods_filter.form.media.js }}
{% endblock %} {% endblock %}