Skip to content
Snippets Groups Projects
Commit 50db5254 authored by Nik | Klampfradler's avatar Nik | Klampfradler
Browse files

Merge branch '73-add-rules-and-permissions' into 'master'

Resolve "Add rules and permissions"

Closes #73 and #79

See merge request !49
parents dad563e6 47ab315d
No related branches found
No related tags found
2 merge requests!95Fix problems with update_or_create and prefetching,!49Resolve "Add rules and permissions"
Pipeline #4074 failed
Showing
with 1148 additions and 306 deletions
......@@ -2,15 +2,18 @@ from datetime import datetime
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count
from django.db.models import Count, Q
from django.utils.translation import gettext_lazy as _
from django_global_request.middleware import get_request
from django_select2.forms import Select2Widget
from guardian.shortcuts import get_objects_for_user
from material import Fieldset, Layout, Row
from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import TimePeriod
from aleksis.core.models import Group, Person
from aleksis.core.util.predicates import check_global_permission
from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
......@@ -51,12 +54,7 @@ class SelectForm(forms.Form):
queryset=None, label=_("Group"), required=False, widget=Select2Widget,
)
teacher = forms.ModelChoiceField(
queryset=Person.objects.annotate(
lessons_count=Count("lessons_as_teacher")
).filter(lessons_count__gt=0),
label=_("Teacher"),
required=False,
widget=Select2Widget,
queryset=None, label=_("Teacher"), required=False, widget=Select2Widget,
)
def clean(self) -> dict:
......@@ -78,8 +76,40 @@ class SelectForm(forms.Form):
return data
def __init__(self, *args, **kwargs):
self.request = get_request()
super().__init__(*args, **kwargs)
self.fields["group"].queryset = Group.get_groups_with_lessons()
person = self.request.user.person
group_qs = Group.get_groups_with_lessons()
# Filter selectable groups by permissions
if not check_global_permission(self.request.user, "alsijil.view_week"):
# 1) All groups the user is allowed to see the week view by object permissions
# 2) All groups the user is a member of an owner of
group_qs = (
group_qs.filter(
pk__in=get_objects_for_user(
self.request.user, "core.view_week_class_register_group", Group
).values_list("pk", flat=True)
)
).union(group_qs.filter(Q(members=person) | Q(owners=person)))
# Flatten query by filtering groups by pk
self.fields["group"].queryset = Group.objects.filter(
pk__in=list(group_qs.values_list("pk", flat=True))
)
teacher_qs = Person.objects.annotate(
lessons_count=Count("lessons_as_teacher")
).filter(lessons_count__gt=0)
# Filter selectable teachers by permissions
if not check_global_permission(self.request.user, "alsijil.view_week"):
# If the user hasn't the global permission, the user is only allowed to see his own person
teacher_qs = teacher_qs.filter(pk=person.pk)
self.fields["teacher"].queryset = teacher_qs
PersonalNoteFormSet = forms.modelformset_factory(
......@@ -95,11 +125,11 @@ class RegisterAbsenceForm(forms.Form):
)
date_start = forms.DateField(label=_("Start date"), initial=datetime.today)
date_end = forms.DateField(label=_("End date"), initial=datetime.today)
from_period = forms.ChoiceField(label=_("Start period"))
to_period = forms.ChoiceField(label=_("End period"))
person = forms.ModelChoiceField(
label=_("Person"), queryset=Person.objects.all(), widget=Select2Widget
label=_("Person"), queryset=None, widget=Select2Widget
)
from_period = forms.ChoiceField(label=_("Start period"))
to_period = forms.ChoiceField(label=_("End period"))
absent = forms.BooleanField(label=_("Absent"), initial=True, required=False)
excused = forms.BooleanField(label=_("Excused"), initial=True, required=False)
excuse_type = forms.ModelChoiceField(
......@@ -111,9 +141,41 @@ class RegisterAbsenceForm(forms.Form):
remarks = forms.CharField(label=_("Remarks"), max_length=30, required=False)
def __init__(self, *args, **kwargs):
self.request = get_request()
super().__init__(*args, **kwargs)
period_choices = TimePeriod.period_choices
# Filter selectable persons by permissions
if check_global_permission(self.request.user, "alsijil.register_absence"):
# Global permission, user can register absences for all persons
self.fields["person"].queryset = Person.objects.all()
else:
# 1) All persons the user is allowed to register an absence for by object permissions
# 2) All persons the user is the primary group owner
# 3) All persons the user is allowed to register an absence for by object permissions of the person's group
persons_qs = (
get_objects_for_user(
self.request.user, "core.register_absence_person", Person
)
.union(
Person.objects.filter(
primary_group__owners=self.request.user.person
)
)
.union(
Person.objects.filter(
member_of__in=get_objects_for_user(
self.request.user, "core.register_absence_group", Group
)
)
)
)
# Flatten query by getting all pks and filter persons
self.fields["person"].queryset = Person.objects.filter(
pk__in=list(persons_qs.values_list("pk", flat=True))
)
self.fields["from_period"].choices = period_choices
self.fields["to_period"].choices = period_choices
self.fields["from_period"].initial = TimePeriod.period_min
......
......@@ -16,49 +16,89 @@ MENUS = {
"name": _("Current lesson"),
"url": "lesson",
"icon": "alarm",
"validators": ["menu_generator.validators.is_authenticated"],
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"alsijil.view_lesson_menu",
),
],
},
{
"name": _("Current week"),
"url": "week_view",
"icon": "view_week",
"validators": ["menu_generator.validators.is_authenticated"],
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"alsijil.view_week_menu",
),
],
},
{
"name": _("My groups"),
"url": "my_groups",
"icon": "people",
"validators": ["menu_generator.validators.is_authenticated"],
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"alsijil.view_my_groups",
),
],
},
{
"name": _("My overview"),
"url": "overview_me",
"icon": "insert_chart",
"validators": ["menu_generator.validators.is_authenticated"],
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"alsijil.view_person_overview_menu",
),
],
},
{
"name": _("My students"),
"url": "my_students",
"icon": "people",
"validators": ["menu_generator.validators.is_authenticated"],
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"alsijil.view_my_students",
),
],
},
{
"name": _("Register absence"),
"url": "register_absence",
"icon": "rate_review",
"validators": ["menu_generator.validators.is_superuser"],
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"alsijil.view_register_absence",
),
],
},
{
"name": _("Excuse types"),
"url": "excuse_types",
"icon": "label",
"validators": ["menu_generator.validators.is_superuser"],
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"alsijil.view_excusetypes",
),
],
},
{
"name": _("Extra marks"),
"url": "extra_marks",
"icon": "label",
"validators": ["menu_generator.validators.is_superuser"],
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"alsijil.view_extramarks",
),
],
},
],
}
......
......@@ -3,6 +3,7 @@ from typing import Dict, Optional, Union
from django.db.models import Exists, OuterRef, Q, QuerySet
from django.db.models.aggregates import Count
from django.utils.translation import gettext as _
import reversion
from calendarweek import CalendarWeek
......@@ -54,16 +55,20 @@ def mark_absent(
continue
with reversion.create_revision():
personal_note, created = PersonalNote.objects.update_or_create(
person=self,
lesson_period=lesson_period,
week=wanted_week.week,
year=wanted_week.year,
defaults={
"absent": absent,
"excused": excused,
"excuse_type": excuse_type,
},
personal_note, created = (
PersonalNote.objects.select_related(None)
.prefetch_related(None)
.update_or_create(
person=self,
lesson_period=lesson_period,
week=wanted_week.week,
year=wanted_week.year,
defaults={
"absent": absent,
"excused": excused,
"excuse_type": excuse_type,
},
)
)
personal_note.groups_of_person.set(self.member_of.all())
......@@ -76,7 +81,7 @@ def mark_absent(
@LessonPeriod.method
def get_personal_notes(self, wanted_week: CalendarWeek):
def get_personal_notes(self, persons: QuerySet, wanted_week: CalendarWeek):
"""Get all personal notes for that lesson in a specified week.
Returns all linked `PersonalNote` objects, filtered by the given weeek,
......@@ -89,7 +94,7 @@ def get_personal_notes(self, wanted_week: CalendarWeek):
- Dominik George <dominik.george@teckids.org>
"""
# Find all persons in the associated groups that do not yet have a personal note for this lesson
missing_persons = Person.objects.annotate(
missing_persons = persons.annotate(
no_personal_notes=~Exists(
PersonalNote.objects.filter(
week=wanted_week.week,
......@@ -119,11 +124,51 @@ def get_personal_notes(self, wanted_week: CalendarWeek):
for personal_note in new_personal_notes:
personal_note.groups_of_person.set(personal_note.person.member_of.all())
return PersonalNote.objects.select_related("person").filter(
lesson_period=self, week=wanted_week.week, year=wanted_week.year
return (
PersonalNote.objects.filter(
lesson_period=self,
week=wanted_week.week,
year=wanted_week.year,
person__in=persons,
)
.select_related(None)
.prefetch_related(None)
.select_related("person", "excuse_type")
.prefetch_related("extra_marks")
)
# Dynamically add extra permissions to Group and Person models in core
# Note: requires migrate afterwards
Group.add_permission(
"view_week_class_register_group",
_("Can view week overview of group class register"),
)
Group.add_permission(
"view_lesson_class_register_group",
_("Can view lesson overview of group class register"),
)
Group.add_permission(
"view_personalnote_group", _("Can view all personal notes of a group")
)
Group.add_permission(
"edit_personalnote_group", _("Can edit all personal notes of a group")
)
Group.add_permission(
"view_lessondocumentation_group", _("Can view all lesson documentation of a group")
)
Group.add_permission(
"edit_lessondocumentation_group", _("Can edit all lesson documentation of a group")
)
Group.add_permission("view_full_register_group", _("Can view full register of a group"))
Group.add_permission(
"register_absence_group", _("Can register an absence for all members of a group")
)
Person.add_permission(
"register_absence_person", _("Can register an absence for a person")
)
@LessonPeriod.method
def get_lesson_documentation(
self, week: Optional[CalendarWeek] = None
......
......@@ -208,3 +208,13 @@ class ExtraMark(ExtensibleModel):
ordering = ["short_name"]
verbose_name = _("Extra mark")
verbose_name_plural = _("Extra marks")
class AlsijilGlobalPermissions(ExtensibleModel):
class Meta:
managed = False
permissions = (
("view_week", _("Can view week overview")),
("register_absence", _("Can register absence")),
("list_personal_note_filters", _("Can list all personal note filters")),
)
......@@ -16,6 +16,24 @@ class BlockPersonalNotesForCancelled(BooleanPreference):
verbose_name = _("Block adding personal notes for cancelled lessons")
@site_preferences_registry.register
class ViewOwnPersonalNotes(BooleanPreference):
section = alsijil
name = "view_own_personal_notes"
default = True
verbose_name = _("Allow users to view their own personal notes")
@site_preferences_registry.register
class RegisterAbsenceAsPrimaryGroupOwner(BooleanPreference):
section = alsijil
name = "register_absence_as_primary_group_owner"
default = True
verbose_name = _(
"Allow primary group owners to register future absences for students in their groups"
)
@site_preferences_registry.register
class CarryOverDataToNextPeriods(BooleanPreference):
section = alsijil
......
from rules import add_perm
from aleksis.core.util.predicates import (
has_global_perm,
has_object_perm,
has_person,
is_current_person,
is_site_preference_set,
)
from .util.predicates import (
has_any_object_absence,
has_lesson_group_object_perm,
has_person_group_object_perm,
has_personal_note_group_perm,
is_group_member,
is_group_owner,
is_lesson_parent_group_owner,
is_lesson_participant,
is_lesson_teacher,
is_none,
is_own_personal_note,
is_person_group_owner,
is_person_primary_group_owner,
is_personal_note_lesson_parent_group_owner,
is_personal_note_lesson_teacher,
is_teacher,
)
# View lesson
view_lesson_predicate = has_person & (
has_global_perm("alsijil.view_lesson")
| is_none # View is opened as "Current lesson"
| is_lesson_teacher
| is_lesson_participant
| is_lesson_parent_group_owner
| has_lesson_group_object_perm("core.view_week_class_register_group")
)
add_perm("alsijil.view_lesson", view_lesson_predicate)
# View lesson in menu
add_perm("alsijil.view_lesson_menu", has_person)
# View lesson personal notes
view_lesson_personal_notes_predicate = view_lesson_predicate & (
has_global_perm("alsijil.view_personalnote")
| ~is_lesson_participant
| has_lesson_group_object_perm("core.view_personalnote_group")
)
add_perm("alsijil.view_lesson_personalnote", view_lesson_personal_notes_predicate)
# Edit personal note
edit_lesson_personal_note_predicate = view_lesson_personal_notes_predicate & (
has_global_perm("alsijil.change_personalnote")
| ~is_lesson_parent_group_owner
| has_lesson_group_object_perm("core.edit_personalnote_group")
)
add_perm("alsijil.edit_lesson_personalnote", edit_lesson_personal_note_predicate)
# View personal note
view_personal_note_predicate = has_person & (
has_global_perm("alsijil.view_personalnote")
| is_personal_note_lesson_teacher
| (
is_own_personal_note
& is_site_preference_set("alsijil", "view_own_personal_notes")
)
| is_personal_note_lesson_parent_group_owner
| has_personal_note_group_perm("core.view_personalnote_group")
)
add_perm("alsijil.view_personalnote", view_personal_note_predicate)
# Edit personal note
edit_personal_note_predicate = view_personal_note_predicate & (
has_global_perm("alsijil.view_personalnote")
| ~is_own_personal_note
| has_personal_note_group_perm("core.edit_personalnote_group")
)
add_perm("alsijil.edit_personalnote", edit_personal_note_predicate)
# View lesson documentation
view_lesson_documentation_predicate = view_lesson_predicate
add_perm("alsijil.view_lessondocumentation", view_lesson_documentation_predicate)
# Edit lesson documentation
edit_lesson_documentation_predicate = view_lesson_predicate & (
has_global_perm("alsijil.change_lessondocumentation")
| is_lesson_teacher
| has_lesson_group_object_perm("core.edit_lessondocumentation_group")
)
add_perm("alsijil.edit_lessondocumentation", edit_lesson_documentation_predicate)
# View week overview
view_week_predicate = has_person & (
has_global_perm("alsijil.view_week")
| is_current_person
| is_group_member
| is_group_owner
| has_object_perm("core.view_week_class_register_group")
)
add_perm("alsijil.view_week", view_week_predicate)
# View week overview in menu
add_perm("alsijil.view_week_menu", has_person)
# View week personal notes
view_week_personal_notes_predicate = has_person & (
has_global_perm("alsijil.view_personalnote")
| has_object_perm("core.view_personalnote_group")
| is_group_owner
| (is_current_person & is_teacher)
)
add_perm("alsijil.view_week_personalnote", view_week_personal_notes_predicate)
# View register absence page
view_register_absence_predicate = has_person & (
has_global_perm("alsijil.register_absence") | has_any_object_absence
)
add_perm("alsijil.view_register_absence", view_register_absence_predicate)
# Register absence
register_absence_predicate = has_person & (
has_global_perm("alsijil.register_absence")
| has_person_group_object_perm("core.register_absence_group")
| has_object_perm("core.register_absence_person")
| (
is_person_primary_group_owner
& is_site_preference_set("alsijil", "register_absence_as_primary_group_owner")
)
)
add_perm("alsijil.register_absence", register_absence_predicate)
# View full register for group
view_full_register_predicate = has_person & (
has_global_perm("alsijil.view_full_register")
| has_object_perm("core.view_full_register_group")
| is_group_owner
)
add_perm("alsijil.view_full_register", view_full_register_predicate)
# View students list
view_my_students_predicate = has_person & is_teacher
add_perm("alsijil.view_my_students", view_my_students_predicate)
# View groups list
view_my_groups_predicate = has_person & is_teacher
add_perm("alsijil.view_my_groups", view_my_groups_predicate)
# View person overview
view_person_overview_predicate = has_person & (
(is_current_person & is_site_preference_set("alsijil", "view_own_personal_notes"))
| is_person_group_owner
)
add_perm("alsijil.view_person_overview", view_person_overview_predicate)
# View person overview
view_person_overview_menu_predicate = has_person
add_perm("alsijil.view_person_overview_menu", view_person_overview_menu_predicate)
# View person overview personal notes
view_person_overview_personal_notes_predicate = view_person_overview_predicate & (
has_global_perm("alsijil.view_personalnote")
| has_person_group_object_perm("core.view_personalnote_group")
| is_person_primary_group_owner
| (is_current_person & is_site_preference_set("alsijil", "view_own_personal_notes"))
)
add_perm(
"alsijil.view_person_overview_personalnote",
view_person_overview_personal_notes_predicate,
)
# Edit person overview personal notes
edit_person_overview_personal_notes_predicate = (
view_person_overview_personal_notes_predicate
& (
has_global_perm("alsijil.edit_personalnote")
| ~is_current_person
| has_person_group_object_perm("core.edit_personalnote_group")
)
)
add_perm(
"alsijil.edit_person_overview_personalnote",
edit_person_overview_personal_notes_predicate,
)
# View person statistics on personal notes
view_person_statistics_personal_notes_predicate = (
view_person_overview_personal_notes_predicate
)
add_perm(
"alsijil.view_person_statistics_personalnote",
view_person_statistics_personal_notes_predicate,
)
# View excuse type list
view_excusetypes_predicate = has_person & has_global_perm("alsijil.view_excusetype")
add_perm("alsijil.view_excusetypes", view_excusetypes_predicate)
# Add excuse type
add_excusetype_predicate = view_excusetypes_predicate & has_global_perm(
"alsijil.add_excusetype"
)
add_perm("alsijil.add_excusetype", add_excusetype_predicate)
# Edit excuse type
edit_excusetype_predicate = view_excusetypes_predicate & has_global_perm(
"alsijil.change_excusetype"
)
add_perm("alsijil.edit_excusetype", edit_excusetype_predicate)
# Delete excuse type
delete_excusetype_predicate = view_excusetypes_predicate & has_global_perm(
"alsijil.delete_excusetype"
)
add_perm("alsijil.delete_excusetype", delete_excusetype_predicate)
# View extra mark list
view_extramarks_predicate = has_person & has_global_perm("alsijil.view_extramark")
add_perm("alsijil.view_extramarks", view_extramarks_predicate)
# Add extra mark
add_extramark_predicate = view_extramarks_predicate & has_global_perm(
"alsijil.add_extramark"
)
add_perm("alsijil.add_extramark", add_extramark_predicate)
# Edit extra mark
edit_extramark_predicate = view_extramarks_predicate & has_global_perm(
"alsijil.change_extramark"
)
add_perm("alsijil.edit_extramark", edit_extramark_predicate)
# Delete extra mark
delete_extramark_predicate = view_extramarks_predicate & has_global_perm(
"alsijil.delete_extramark"
)
add_perm("alsijil.delete_extramark", delete_extramark_predicate)
......@@ -42,3 +42,9 @@ class ExcuseTypeTable(tables.Table):
text=_("Delete"),
attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}},
)
def before_render(self, request):
if not request.user.has_perm("alsijil.edit_excusetype"):
self.columns.hide("edit")
if not request.user.has_perm("alsijil.delete_excusetype"):
self.columns.hide("delete")
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load week_helpers material_form_internal %}
{% load material_form i18n static %}
{% load week_helpers material_form_internal material_form i18n static rules %}
{% block browser_title %}{% blocktrans %}Lesson{% endblocktrans %}{% endblock %}
......@@ -29,6 +28,10 @@
{% endblock %}
{% block content %}
{% has_perm "alsijil.view_lessondocumentation" user lesson_period as can_view_lesson_documentation %}
{% has_perm "alsijil.edit_lessondocumentation" user lesson_period as can_edit_lesson_documentation %}
{% has_perm "alsijil.edit_lesson_personalnote" user lesson_period as can_edit_lesson_personalnote %}
<div class="row">
<div class="col s12">
{% with prev_lesson=lesson_period.prev %}
......@@ -50,7 +53,10 @@
</div>
<form method="post">
<p>{% include "core/partials/save_button.html" %}</p>
{% if can_edit_lesson_documentation or can_edit_lesson_personalnote %}
<p>{% include "core/partials/save_button.html" %}</p>
{% endif %}
{% csrf_token %}
<div class="row">
......@@ -73,7 +79,8 @@
<div class="col s12" id="lesson-documentation">
{% with prev_lesson=lesson_period.prev prev_doc=prev_lesson.get_lesson_documentation %}
{% with prev_doc=prev_lesson.get_lesson_documentation absences=prev_lesson.get_absences tardinesses=prev_lesson.get_tardinesses extra_marks=prev_lesson.get_extra_marks %}
{% if prev_doc %}
{% has_perm "alsijil.view_lessondocumentation" user prev_lesson as can_view_prev_lesson_documentation %}
{% if prev_doc and can_view_prev_lesson_documentation %}
{% weekday_to_date prev_lesson.week prev_lesson.period.weekday as prev_date %}
<div class="card">
......@@ -124,7 +131,10 @@
<th>{{ extra_mark.name }}</th>
<td>
{% for note in notes %}
<span>{{ note.person }}{% if not forloop.last %},{% endif %}</span>
{% has_perm "alsijil.view_personalnote" user note as can_view_personalnote %}
{% if can_view_personalnote %}
<span>{{ note.person }}{% if not forloop.last %},{% endif %}</span>
{% endif %}
{% endfor %}
</td>
</tr>
......@@ -143,7 +153,36 @@
{% blocktrans %}Lesson documentation{% endblocktrans %}
</span>
{% form form=lesson_documentation_form %}{% endform %}
{% if can_edit_lesson_documentation %}
{% form form=lesson_documentation_form %}{% endform %}
{% elif can_view_lesson_documentation %}
<table>
<tr>
<th>
{% trans "Lesson topic" %}
</th>
<td>
{{ lesson_documentation.topic }}
</td>
</tr>
<tr>
<th>
{% trans "Homework" %}
</th>
<td>
{{ lesson_documentation.homework }}
</td>
</tr>
<tr>
<th>
{% trans "Group note" %}
</th>
<td>
{{ lesson_documentation.group_note }}
</td>
</tr>
</table>
{% endif %}
</div>
</div>
</div>
......@@ -152,10 +191,12 @@
<div class="col s12" id="personal-notes">
<div class="card">
<div class="card-content">
<span class="card-title">
{% blocktrans %}Personal notes{% endblocktrans %}
</span>
{% form form=personal_note_formset.management_form %}{% endform %}
<span class="card-title">
{% blocktrans %}Personal notes{% endblocktrans %}
</span>
{% if can_edit_lesson_personalnote %}
{% form form=personal_note_formset.management_form %}{% endform %}
{% endif %}
<table class="striped responsive-table alsijil-table">
<thead>
......@@ -166,94 +207,104 @@
<th>{% blocktrans %}Excused{% endblocktrans %}</th>
<th>{% blocktrans %}Excuse type{% endblocktrans %}</th>
<th>{% blocktrans %}Extra marks{% endblocktrans %}</th>
<th>{% blocktrans %}Remarks{% endblocktrans %}</th>
</tr>
</thead>
<tbody>
{% for form in personal_note_formset %}
<tr>
{{ form.id }}
<td>{{ form.person_name }}{{ form.person_name.value }}</td>
<td class="center-align">
<label>
{{ form.absent }}
<span></span>
</label>
</td>
<td>
<div class="input-field">
{{ form.late }}
<label for="{{ form.absent.id_for_label }}">
{% trans "Tardiness (in m)" %}
{% if can_edit_lesson_personalnote %}
<tr>
{{ form.id }}
<td>{{ form.person_name }}{{ form.person_name.value }}</td>
<td class="center-align">
<label>
{{ form.absent }}
<span></span>
</label>
</div>
</td>
<td class="center-align">
<label>
{{ form.excused }}
<span></span>
</label>
</td>
<td>
<div class="input-field">
{{ form.excuse_type }}
<label for="{{ form.excuse_type.id_for_label }}">
{% trans "Excuse type" %}
</td>
<td>
<div class="input-field">
{{ form.late }}
<label for="{{ form.absent.id_for_label }}">
{% trans "Tardiness (in m)" %}
</label>
</div>
</td>
<td class="center-align">
<label>
{{ form.excused }}
<span></span>
</label>
</div>
</td>
<td>
{% for group, items in form.extra_marks|select_options %}
{% for choice, value, selected in items %}
<label class="{% if selected %} active{% endif %} alsijil-check-box">
<input type="checkbox"
{% if value == None or value == '' %}disabled{% else %}value="{{ value }}"{% endif %}
{% if selected %} checked="checked"{% endif %}
name="{{ form.extra_marks.html_name }}">
<span>{{ choice }}</span>
</td>
<td>
<div class="input-field">
{{ form.excuse_type }}
<label for="{{ form.excuse_type.id_for_label }}">
{% trans "Excuse type" %}
</label>
</div>
</td>
<td>
{% for group, items in form.extra_marks|select_options %}
{% for choice, value, selected in items %}
<label class="{% if selected %} active{% endif %} alsijil-check-box">
<input type="checkbox"
{% if value == None or value == '' %}disabled{% else %}value="{{ value }}"{% endif %}
{% if selected %} checked="checked"{% endif %}
name="{{ form.extra_marks.html_name }}">
<span>{{ choice }}</span>
</label>
{% endfor %}
{% endfor %}
{% endfor %}
</td>
<td>
<div class="input-field">
{{ form.remarks }}
<label for="{{ form.absent.id_for_label }}">
{% trans "Remarks" %}
</label>
</div>
</td>
<td>
<div class="input-field">
{{ form.remarks }}
<label for="{{ form.absent.id_for_label }}">
{% trans "Remarks" %}
</label>
</div>
</td>
</tr>
</td>
<td>
<div class="input-field">
{{ form.remarks }}
<label for="{{ form.absent.id_for_label }}">
{% trans "Remarks" %}
</label>
</div>
</td>
</tr>
{% else %}
<tr>
<td>{{ form.person_name.value }}</td>
<td>{{ form.absent.value }}</td>
<td>{{ form.late.value }}</td>
<td>{{ form.excused.value }}</td>
<td>{{ form.excuse_type.value }}</td>
<td>
{% for extra_mark in form.extra_marks.value %}
{{ extra_mark }}{% if not forloop.last %},{% endif %}
{% endfor %}
</td>
<td>{{ form.remarks.value }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<div class="col s12" id="version-history">
<div class="card">
<div class="card-content">
{% if can_view_lesson_documentation %}
<div class="col s12" id="version-history">
<div class="card">
<div class="card-content">
<span class="card-title">
{% blocktrans %}Change history{% endblocktrans %}
</span>
{% include 'core/partials/crud_events.html' with obj=lesson_documentation %}
{% include 'core/partials/crud_events.html' with obj=lesson_documentation %}
</div>
</div>
</div>
</div>
{% endif %}
</div>
<p>{% include "core/partials/save_button.html" %}</p>
{% if can_edit_lesson_documentation or can_edit_lesson_personalnote %}
<p>{% include "core/partials/save_button.html" %}</p>
{% endif %}
</form>
{% endblock %}
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load rules %}
{% load data_helpers %}
{% load week_helpers %}
{% load i18n %}
......@@ -14,6 +15,8 @@
{% endblock %}
{% block content %}
{% has_perm "alsijil.edit_person_overview_personalnote" user person as can_mark_all_as_excused %}
<div class="row">
<div class="col s12 m12 l6">
<h5>{% trans "Unexcused absences" %}</h5>
......@@ -22,16 +25,19 @@
{% for note in unexcused_absences %}
{% weekday_to_date note.calendar_week note.lesson_period.period.weekday as note_date %}
<li class="collection-item">
<form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;">
{% csrf_token %}
{% trans "Mark as" %}
<input type="hidden" value="{{ note.pk }}" name="personal_note">
{% include "alsijil/partials/mark_as_buttons.html" %}
<a class="btn-flat red-text" title="{% trans "Delete note" %}"
href="{% url "delete_personal_note" note.pk %}">
<i class="material-icons center">cancel</i>
</a>
</form>
{% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %}
{% if can_edit_personal_note %}
<form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;">
{% csrf_token %}
{% trans "Mark as" %}
<input type="hidden" value="{{ note.pk }}" name="personal_note">
{% include "alsijil/partials/mark_as_buttons.html" %}
<a class="btn-flat red-text" title="{% trans "Delete note" %}"
href="{% url "delete_personal_note" note.pk %}">
<i class="material-icons center">cancel</i>
</a>
</form>
{% endif %}
<i class="material-icons left red-text">warning</i>
<p class="no-margin">
<a href="{% url "lesson_by_week_and_period" note.year note.week note.lesson_period.pk %}">{{ note_date }}, {{ note.lesson_period }}</a>
......@@ -39,16 +45,18 @@
{% if note.remarks %}
<p class="no-margin"><em>{{ note.remarks }}</em></p>
{% endif %}
<form action="" method="post" class="hide-on-med-and-up">
{% csrf_token %}
{% trans "Mark as" %}
<input type="hidden" value="{{ note.pk }}" name="personal_note">
{% include "alsijil/partials/mark_as_buttons.html" %}
<a class="btn-flat red-text" title="{% trans "Delete note" %}"
href="{% url "delete_personal_note" note.pk %}">
<i class="material-icons center">cancel</i>
</a>
</form>
{% if can_edit_personal_note %}
<form action="" method="post" class="hide-on-med-and-up">
{% csrf_token %}
{% trans "Mark as" %}
<input type="hidden" value="{{ note.pk }}" name="personal_note">
{% include "alsijil/partials/mark_as_buttons.html" %}
<a class="btn-flat red-text" title="{% trans "Delete note" %}"
href="{% url "delete_personal_note" note.pk %}">
<i class="material-icons center">cancel</i>
</a>
</form>
{% endif %}
</li>
{% empty %}
<li class="collection-item flow-text">
......@@ -56,47 +64,49 @@
</li>
{% endfor %}
</ul>
<h5>{% trans "Statistics on absences, tardiness and remarks" %}</h5>
<ul class="collapsible">
{% for school_term, stat in stats %}
<li {% if forloop.first %}class="active"{% endif %}>
<div class="collapsible-header">
<i class="material-icons">date_range</i>{{ school_term }}</div>
<div class="collapsible-body">
<table>
<tr>
<th colspan="2">{% trans 'Absences' %}</th>
<td>{{ stat.absences_count }}</td>
</tr>
<tr>
<td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-small-only">{% trans "thereof" %}</td>
<td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-med-and-up"></td>
<th class="truncate">{% trans 'Excused' %}</th>
<td>{{ stat.excused }}</td>
</tr>
{% for excuse_type in excuse_types %}
<th>{{ excuse_type.name }}</th>
<td>{{ stat|get_dict:excuse_type.count_label }}</td>
{% endfor %}
<tr>
<th>{% trans 'Unexcused' %}</th>
<td>{{ stat.unexcused }}</td>
</tr>
<tr>
<th colspan="2">{% trans 'Tardiness' %}</th>
<td>{{ stat.tardiness }}'</td>
</tr>
{% for extra_mark in extra_marks %}
{% if stats %}
<h5>{% trans "Statistics on absences, tardiness and remarks" %}</h5>
<ul class="collapsible">
{% for school_term, stat in stats %}
<li {% if forloop.first %}class="active"{% endif %}>
<div class="collapsible-header">
<i class="material-icons">date_range</i>{{ school_term }}</div>
<div class="collapsible-body">
<table>
<tr>
<th colspan="2">{{ extra_mark.name }}</th>
<td>{{ stat|get_dict:extra_mark.count_label }}</td>
<th colspan="2">{% trans 'Absences' %}</th>
<td>{{ stat.absences_count }}</td>
</tr>
{% endfor %}
</table>
</div>
</li>
{% endfor %}
</ul>
<tr>
<td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-small-only">{% trans "thereof" %}</td>
<td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-med-and-up"></td>
<th class="truncate">{% trans 'Excused' %}</th>
<td>{{ stat.excused }}</td>
</tr>
{% for excuse_type in excuse_types %}
<th>{{ excuse_type.name }}</th>
<td>{{ stat|get_dict:excuse_type.count_label }}</td>
{% endfor %}
<tr>
<th>{% trans 'Unexcused' %}</th>
<td>{{ stat.unexcused }}</td>
</tr>
<tr>
<th colspan="2">{% trans 'Tardiness' %}</th>
<td>{{ stat.tardiness }}'</td>
</tr>
{% for extra_mark in extra_marks %}
<tr>
<th colspan="2">{{ extra_mark.name }}</th>
<td>{{ stat|get_dict:extra_mark.count_label }}</td>
</tr>
{% endfor %}
</table>
</div>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="col s12 m12 l6">
<h5>{% trans "Relevant personal notes" %}</h5>
......@@ -121,21 +131,25 @@
{% weekday_to_date note.calendar_week note.lesson_period.period.weekday as note_date %}
{% ifchanged note_date %}
<li class="collection-item">
<form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;">
{% csrf_token %}
{% trans "Mark all as" %}
<input type="hidden" value="{{ note_date|date:"Y-m-d" }}" name="date">
{% include "alsijil/partials/mark_as_buttons.html" %}
</form>
{% if can_mark_all_as_excused %}
<form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;">
{% csrf_token %}
{% trans "Mark all as" %}
<input type="hidden" value="{{ note_date|date:"Y-m-d" }}" name="date">
{% include "alsijil/partials/mark_as_buttons.html" %}
</form>
{% endif %}
<i class="material-icons left">schedule</i>
{{ note_date }}
<form action="" method="post" class="hide-on-med-and-up">
{% csrf_token %}
{% trans "Mark all as" %}
<input type="hidden" value="{{ note_date|date:"Y-m-d" }}" name="date">
{% include "alsijil/partials/mark_as_buttons.html" %}
</form>
{% if can_mark_all_as_excused %}
<form action="" method="post" class="hide-on-med-and-up">
{% csrf_token %}
{% trans "Mark all as" %}
<input type="hidden" value="{{ note_date|date:"Y-m-d" }}" name="date">
{% include "alsijil/partials/mark_as_buttons.html" %}
</form>
{% endif %}
</li>
{% endifchanged %}
......@@ -154,7 +168,8 @@
</div>
<div class="col s12 m7 no-padding">
{% if note.absent and not note.excused %}
{% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %}
{% if note.absent and not note.excused and can_edit_personal_note %}
<form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;">
{% csrf_token %}
{% trans "Mark as" %}
......@@ -165,6 +180,11 @@
<i class="material-icons center">cancel</i>
</a>
</form>
{% elif can_edit_personal_note %}
<a class="btn-flat red-text right hide-on-small-only" title="{% trans "Delete note" %}"
href="{% url "delete_personal_note" note.pk %}">
<i class="material-icons center">cancel</i>
</a>
{% endif %}
{% if note.absent %}
......@@ -196,7 +216,7 @@
</div>
<div class="col s12 hide-on-med-and-up">
{% if note.absent and not note.excused %}
{% if note.absent and not note.excused and can_edit_personal_note %}
<form action="" method="post">
{% csrf_token %}
{% trans "Mark as" %}
......@@ -207,6 +227,12 @@
<i class="material-icons center">cancel</i>
</a>
</form>
{% elif can_edit_personal_note %}
<a class="btn-flat red-text" title="{% trans "Delete note" %}"
href="{% url "delete_personal_note" note.pk %}">
<i class="material-icons left">cancel</i>
{% trans "Delete" %}
</a>
{% endif %}
</div>
</li>
......
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load material_form i18n week_helpers static data_helpers %}
{% load material_form i18n week_helpers static data_helpers rules %}
{% block browser_title %}{% blocktrans %}Week view{% endblocktrans %}{% endblock %}
......@@ -66,38 +66,41 @@
</thead>
<tbody>
{% for period in periods %}
<tr>
<td class="center-align">
{% include "alsijil/partials/lesson_status_icon.html" with period=period %}
</td>
<td class="tr-link">
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.period.period }}.
</a>
</td>
{% if not group %}
{% has_perm "alsijil.view_lessondocumentation" user period as can_view_lesson_documentation %}
{% if can_view_lesson_documentation %}
<tr>
<td class="center-align">
{% include "alsijil/partials/lesson_status_icon.html" with period=period %}
</td>
<td class="tr-link">
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.period.period }}.
</a>
</td>
{% if not group %}
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.lesson.group_names }}
</a>
</td>
{% endif %}
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.lesson.group_names }}
{{ period.get_subject.name }}
</a>
</td>
{% endif %}
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.get_subject.name }}
</a>
</td>
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.get_teacher_names }}
</a>
</td>
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.get_lesson_documentation.topic }}
</a>
</td>
</tr>
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.get_teacher_names }}
</a>
</td>
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.get_lesson_documentation.topic }}
</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
......@@ -113,7 +116,12 @@
</span>
{% for person in persons %}
<h5 class="card-title">
<a href="{% url "overview_person" person.person.pk %}">{{ person.person.full_name }}</a>
{% has_perm "alsijil.view_person_overview" user person.person as can_view_person_overview %}
{% if can_view_person_overview %}
<a href="{% url "overview_person" person.person.pk %}">{{ person.person.full_name }}</a>
{% else %}
{{ person.person.full_name }}
{% endif %}
</h5>
<p class="card-text">
{% trans "Absent" %}: {{ person.person.absences_count }}
......
......@@ -2,7 +2,7 @@
{% extends "core/base.html" %}
{% load i18n %}
{% load i18n rules %}
{% load render_table from django_tables2 %}
{% block browser_title %}{% blocktrans %}Excuse types{% endblocktrans %}{% endblock %}
......@@ -11,10 +11,13 @@
{% block content %}
{% include "alsijil/excuse_type/warning.html" %}
<a class="btn green waves-effect waves-light" href="{% url 'create_excuse_type' %}">
<i class="material-icons left">add</i>
{% trans "Create excuse type" %}
</a>
{% has_perm "alsijil.add_excusetype" user as add_excusetype %}
{% if add_excusetype %}
<a class="btn green waves-effect waves-light" href="{% url 'create_excuse_type' %}">
<i class="material-icons left">add</i>
{% trans "Create excuse type" %}
</a>
{% endif %}
{% render_table table %}
{% endblock %}
{% load i18n %}
{% load i18n rules %}
{% for note in notes %}
<span class="{% if note.excused %}green-text{% else %}red-text{% endif %}">{{ note.person }}
{% if note.excused %}{% if note.excuse_type %}({{ note.excuse_type.short_name }}){% else %}{% trans "(e)" %}{% endif %}{% else %}{% trans "(u)" %}{% endif %}{% if not forloop.last %},{% endif %}
</span>
{% has_perm "alsijil.view_personalnote" user note as can_view_personalnote %}
{% if can_view_personalnote %}
<span class="{% if note.excused %}green-text{% else %}red-text{% endif %}">{{ note.person }}
{% if note.excused %}{% if note.excuse_type %}({{ note.excuse_type.short_name }}){% else %}{% trans "(e)" %}{% endif %}{% else %}{% trans "(u)" %}{% endif %}{% if not forloop.last %},{% endif %}
</span>
{% endif %}
{% endfor %}
{% load rules %}
{% for note in notes %}
<span>{{ note.person }} ({{ note.late }}'){% if not forloop.last %},{% endif %}</span>
{% has_perm "alsijil.view_personalnote" user note as can_view_personalnote %}
{% if can_view_personalnote %}
<span>{{ note.person }} ({{ note.late }}'){% if not forloop.last %},{% endif %}</span>
{% endif %}
{% endfor %}
from typing import Optional
from django.http import HttpRequest
from calendarweek import CalendarWeek
from aleksis.apps.chronos.models import LessonPeriod
from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
def get_lesson_period_by_pk(
request: HttpRequest,
year: Optional[int] = None,
week: Optional[int] = None,
period_id: Optional[int] = None,
):
"""Get LessonPeriod object either by given object_id or by time and current person."""
wanted_week = CalendarWeek(year=year, week=week)
if period_id:
lesson_period = LessonPeriod.objects.annotate_week(wanted_week).get(
pk=period_id
)
elif hasattr(request, "user") and hasattr(request.user, "person"):
if request.user.person.lessons_as_teacher.exists():
lesson_period = (
LessonPeriod.objects.at_time()
.filter_teacher(request.user.person)
.first()
)
else:
lesson_period = (
LessonPeriod.objects.at_time()
.filter_participant(request.user.person)
.first()
)
else:
lesson_period = None
return lesson_period
def get_timetable_instance_by_pk(
request: HttpRequest,
year: Optional[int] = None,
week: Optional[int] = None,
type_: Optional[str] = None,
id_: Optional[int] = None,
):
"""Get timetable object (teacher, room or group) by given type and id or the current person."""
if type_ and id_:
return get_el_by_pk(request, type_, id_)
elif hasattr(request, "user") and hasattr(request.user, "person"):
return request.user.person
from typing import Any, Union
from django.contrib.auth.models import User
from guardian.shortcuts import get_objects_for_user
from rules import predicate
from aleksis.apps.chronos.models import LessonPeriod
from aleksis.core.models import Group, Person
from aleksis.core.util.predicates import check_object_permission
from ..models import PersonalNote
@predicate
def is_none(user: User, obj: Any) -> bool:
"""Predicate that checks if the provided object is None-like."""
return bool(obj)
@predicate
def is_lesson_teacher(user: User, obj: LessonPeriod) -> bool:
"""Predicate for teachers of a lesson.
Checks whether the person linked to the user is a teacher
in the lesson or the substitution linked to the given LessonPeriod.
"""
if obj:
sub = obj.get_substitution()
if sub and sub in user.person.lesson_substitutions.all():
return True
return user.person in obj.lesson.teachers.all()
return False
@predicate
def is_lesson_participant(user: User, obj: LessonPeriod) -> bool:
"""Predicate for participants of a lesson.
Checks whether the person linked to the user is a member in
the groups linked to the given LessonPeriod.
"""
if hasattr(obj, "lesson"):
return obj.lesson.groups.filter(members=user.person).exists()
return False
@predicate
def is_lesson_parent_group_owner(user: User, obj: LessonPeriod) -> bool:
"""
Predicate for parent group owners of a lesson.
Checks whether the person linked to the user is the owner of
any parent groups of any groups of the given LessonPeriods lesson.
"""
if hasattr(obj, "lesson"):
return obj.lesson.groups.filter(parent_groups__owners=user.person).exists()
return False
@predicate
def is_group_owner(user: User, obj: Union[Group, Person]) -> bool:
"""Predicate for group owners of a given group.
Checks whether the person linked to the user is the owner of the given group.
If there isn't provided a group, it will return `False`.
"""
if isinstance(obj, Group):
if obj.owners.filter(pk=user.person.pk).exists():
return True
return False
@predicate
def is_person_group_owner(user: User, obj: Person) -> bool:
"""
Predicate for group owners of any group.
Checks whether the person linked to the user is
the owner of any group of the given person.
"""
if obj:
return obj.member_of.filter(owners=user.person).exists()
return False
@predicate
def is_person_primary_group_owner(user: User, obj: Person) -> bool:
"""
Predicate for group owners of the person's primary group.
Checks whether the person linked to the user is
the owner of the primary group of the given person.
"""
if obj.primary_group:
return user.person in obj.primary_group.owners.all()
return False
def has_person_group_object_perm(perm: str):
"""Predicate builder for permissions on a set of member groups.
Checks whether a user has a permission on any group of a person.
"""
name = f"has_person_group_object_perm:{perm}"
@predicate(name)
def fn(user: User, obj: Person) -> bool:
for group in obj.member_of.all():
if check_object_permission(user, perm, group):
return True
return False
return fn
@predicate
def is_group_member(user: User, obj: Union[Group, Person]) -> bool:
"""Predicate for group membership.
Checks whether the person linked to the user is a member of the given group.
If there isn't provided a group, it will return `False`.
"""
if isinstance(obj, Group):
if obj.members.filter(pk=user.person.pk).exists():
return True
return False
def has_lesson_group_object_perm(perm: str):
"""Predicate builder for permissions on lesson groups.
Checks whether a user has a permission on any group of a LessonPeriod.
"""
name = f"has_lesson_group_object_perm:{perm}"
@predicate(name)
def fn(user: User, obj: LessonPeriod) -> bool:
if hasattr(obj, "lesson"):
for group in obj.lesson.groups.all():
if check_object_permission(user, perm, group):
return True
return False
return True
return fn
def has_personal_note_group_perm(perm: str):
"""Predicate builder for permissions on personal notes
Checks whether a user has a permission on any group of a person of a PersonalNote.
"""
name = f"has_personal_note_person_or_group_perm:{perm}"
@predicate(name)
def fn(user: User, obj: PersonalNote) -> bool:
if hasattr(obj, "person"):
for group in obj.person.member_of.all():
if check_object_permission(user, perm, group):
return True
return False
return fn
@predicate
def is_own_personal_note(user: User, obj: PersonalNote) -> bool:
"""Predicate for users referred to in a personal note
Checks whether the user referred to in a PersonalNote is the active user.
"""
if hasattr(obj, "person") and obj.person is user.person:
return True
return False
@predicate
def is_personal_note_lesson_teacher(user: User, obj: PersonalNote) -> bool:
"""Predicate for teachers of a lesson referred to in the lesson period of a personal note.
Checks whether the person linked to the user is a teacher
in the lesson or the substitution linked to the LessonPeriod of the given PersonalNote.
"""
if hasattr(obj, "lesson_period"):
if hasattr(obj.lesson_period, "lesson"):
sub = obj.lesson_period.get_substitution()
if sub and user.person in Person.objects.filter(
lesson_substitutions=obj.lesson_period.get_substitution()
):
return True
return user.person in obj.lesson_period.lesson.teachers.all()
return False
return False
@predicate
def is_personal_note_lesson_parent_group_owner(user: User, obj: PersonalNote) -> bool:
"""
Predicate for parent group owners of a lesson referred to in the lesson period of a personal note.
Checks whether the person linked to the user is the owner of
any parent groups of any groups of the given LessonPeriod lesson of the given PersonalNote.
"""
if hasattr(obj, "lesson_period"):
if hasattr(obj.lesson_period, "lesson"):
return obj.lesson_period.lesson.groups.filter(
parent_groups__owners=user.person
).exists()
return False
return False
@predicate
def has_any_object_absence(user: User) -> bool:
"""
Predicate which builds a query with all the persons the given users is allowed to register an absence for.
"""
if get_objects_for_user(user, "core.register_absence_person", Person).exists():
return True
if Person.objects.filter(member_of__owners=user.person).exists():
return True
if Person.objects.filter(
member_of__in=get_objects_for_user(user, "core.register_absence_group", Group)
).exists():
return True
@predicate
def is_teacher(user: User, obj: Person) -> bool:
"""Predicate which checks if the provided object is a teacher."""
return user.person.is_teacher
......@@ -13,7 +13,7 @@ import reversion
from calendarweek import CalendarWeek
from django_tables2 import SingleTableView
from reversion.views import RevisionMixin
from rules.contrib.views import PermissionRequiredMixin
from rules.contrib.views import PermissionRequiredMixin, permission_required
from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import LessonPeriod, TimePeriod
......@@ -34,8 +34,10 @@ from .forms import (
)
from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
from .tables import ExcuseTypeTable, ExtraMarkTable
from .util.alsijil_helpers import get_lesson_period_by_pk, get_timetable_instance_by_pk
@permission_required("alsijil.view_lesson", fn=get_lesson_period_by_pk)
def lesson(
request: HttpRequest,
year: Optional[int] = None,
......@@ -44,27 +46,16 @@ def lesson(
) -> HttpResponse:
context = {}
if year and week and period_id:
# Get a specific lesson period if provided in URL
wanted_week = CalendarWeek(year=year, week=week)
lesson_period = LessonPeriod.objects.annotate_week(wanted_week).get(
pk=period_id
)
date_of_lesson = week_weekday_to_date(wanted_week, lesson_period.period.weekday)
lesson_period = get_lesson_period_by_pk(request, year, week, period_id)
if (
date_of_lesson < lesson_period.lesson.validity.date_start
or date_of_lesson > lesson_period.lesson.validity.date_end
):
return HttpResponseNotFound()
else:
# Determine current lesson by current date and time
lesson_period = (
LessonPeriod.objects.at_time().filter_teacher(request.user.person).first()
)
if period_id:
wanted_week = CalendarWeek(year=year, week=week)
elif hasattr(request, "user") and hasattr(request.user, "person"):
wanted_week = CalendarWeek()
else:
wanted_week = None
if not all((year, week, period_id)):
if lesson_period:
return redirect(
"lesson_by_week_and_period",
......@@ -80,6 +71,14 @@ def lesson(
)
)
date_of_lesson = week_weekday_to_date(wanted_week, lesson_period.period.weekday)
if (
date_of_lesson < lesson_period.lesson.validity.date_start
or date_of_lesson > lesson_period.lesson.validity.date_end
):
return HttpResponseNotFound()
if (
datetime.combine(
wanted_week[lesson_period.period.weekday], lesson_period.period.time_start,
......@@ -110,13 +109,20 @@ def lesson(
)
# Create a formset that holds all personal notes for all persons in this lesson
persons_qs = lesson_period.get_personal_notes(wanted_week)
if not request.user.has_perm("alsijil.view_lesson_personalnote", lesson_period):
persons = Person.objects.filter(pk=request.user.person.pk)
else:
persons = Person.objects.all()
persons_qs = lesson_period.get_personal_notes(persons, wanted_week)
personal_note_formset = PersonalNoteFormSet(
request.POST or None, queryset=persons_qs, prefix="personal_notes"
)
if request.method == "POST":
if lesson_documentation_form.is_valid():
if lesson_documentation_form.is_valid() and request.user.has_perm(
"alsijil.edit_lessondocumentation", lesson_period
):
lesson_documentation_form.save()
messages.success(request, _("The lesson documentation has been saved."))
......@@ -126,7 +132,9 @@ def lesson(
not getattr(substitution, "cancelled", False)
or not get_site_preferences()["alsijil__block_personal_notes_for_cancelled"]
):
if personal_note_formset.is_valid():
if personal_note_formset.is_valid() and request.user.has_perm(
"alsijil.edit_lesson_personalnote", lesson_period
):
with reversion.create_revision():
instances = personal_note_formset.save()
......@@ -154,6 +162,7 @@ def lesson(
return render(request, "alsijil/class_register/lesson.html", context)
@permission_required("alsijil.view_week", fn=get_timetable_instance_by_pk)
def week_view(
request: HttpRequest,
year: Optional[int] = None,
......@@ -168,7 +177,9 @@ def week_view(
else:
wanted_week = CalendarWeek()
lesson_periods = LessonPeriod.objects.annotate(
instance = get_timetable_instance_by_pk(request, year, week, type_, id_)
lesson_periods = LessonPeriod.objects.in_week(wanted_week).annotate(
has_documentation=Exists(
LessonDocumentation.objects.filter(
~Q(topic__exact=""),
......@@ -177,23 +188,16 @@ def week_view(
year=wanted_week.year,
)
)
).in_week(wanted_week)
)
group = None
if type_ and id_:
instance = get_el_by_pk(request, type_, id_)
if isinstance(instance, HttpResponseNotFound):
return HttpResponseNotFound()
type_ = TimetableType.from_string(type_)
if type_ == TimetableType.GROUP:
group = instance
lesson_periods = lesson_periods.filter_from_type(type_, instance)
elif hasattr(request, "user") and hasattr(request.user, "person"):
instance = request.user.person
if request.user.person.lessons_as_teacher.exists():
lesson_periods = lesson_periods.filter_teacher(request.user.person)
type_ = TimetableType.TEACHER
......@@ -222,13 +226,20 @@ def week_view(
select_form.cleaned_data["instance"].pk,
)
if type_ == TimetableType.GROUP:
group = instance
else:
group = None
if lesson_periods:
# Aggregate all personal notes for this group and week
lesson_periods_pk = list(lesson_periods.values_list("pk", flat=True))
persons_qs = Person.objects.filter(is_active=True)
if group:
if not request.user.has_perm("alsijil.view_week_personalnote", instance):
persons_qs = persons_qs.filter(pk=request.user.person.pk)
elif group:
persons_qs = persons_qs.filter(member_of=group)
else:
persons_qs = persons_qs.filter(
......@@ -340,6 +351,9 @@ def week_view(
return render(request, "alsijil/class_register/week_view.html", context)
@permission_required(
"alsijil.view_full_register", fn=objectgetter_optional(Group, None, False)
)
def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
context = {}
......@@ -461,6 +475,7 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
return render(request, "alsijil/print/full_register.html", context)
@permission_required("alsijil.view_my_students")
def my_students(request: HttpRequest) -> HttpResponse:
context = {}
relevant_groups = request.user.person.get_owner_groups_with_lessons()
......@@ -469,12 +484,17 @@ def my_students(request: HttpRequest) -> HttpResponse:
return render(request, "alsijil/class_register/persons.html", context)
@permission_required("alsijil.view_my_groups",)
def my_groups(request: HttpRequest) -> HttpResponse:
context = {}
context["groups"] = request.user.person.get_owner_groups_with_lessons()
return render(request, "alsijil/class_register/groups.html", context)
@permission_required(
"alsijil.view_person_overview",
fn=objectgetter_optional(Person, "request.user.person", True),
)
def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
context = {}
person = objectgetter_optional(
......@@ -505,6 +525,11 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
request.POST["date"], "%Y-%m-%d"
).date()
if not request.user.has_perm(
"alsijil.edit_person_overview_personalnote", person
):
raise PermissionDenied()
notes = person.personal_notes.filter(
week=date.isocalendar()[1],
lesson_period__period__weekday=date.weekday(),
......@@ -530,6 +555,8 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
note = PersonalNote.objects.get(
pk=int(request.POST["personal_note"])
)
if not request.user.has_perm("alsijil.edit_personalnote", note):
raise PermissionDenied()
if note.absent:
note.excused = True
note.excuse_type = excuse_type
......@@ -543,10 +570,17 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
person.refresh_from_db()
unexcused_absences = person.personal_notes.filter(absent=True, excused=False)
if request.user.has_perm("alsijil.view_person_overview_personalnote", person):
allowed_personal_notes = person.personal_notes.all()
else:
allowed_personal_notes = person.personal_notes.filter(
lesson_period__lesson__groups__owners=request.user.person
)
unexcused_absences = allowed_personal_notes.filter(absent=True, excused=False)
context["unexcused_absences"] = unexcused_absences
personal_notes = person.personal_notes.filter(
personal_notes = allowed_personal_notes.filter(
Q(absent=True) | Q(late__gt=0) | ~Q(remarks="") | Q(extra_marks__isnull=False)
).order_by(
"-lesson_period__lesson__validity__date_start",
......@@ -557,60 +591,66 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
context["personal_notes"] = personal_notes
context["excuse_types"] = ExcuseType.objects.all()
school_terms = SchoolTerm.objects.all().order_by("-date_start")
stats = []
for school_term in school_terms:
stat = {}
personal_notes = PersonalNote.objects.filter(
person=person, lesson_period__lesson__validity__school_term=school_term
)
if not personal_notes.exists():
continue
stat.update(
personal_notes.filter(absent=True).aggregate(absences_count=Count("absent"))
)
stat.update(
personal_notes.filter(
absent=True, excused=True, excuse_type__isnull=True
).aggregate(excused=Count("absent"))
)
stat.update(
personal_notes.filter(absent=True, excused=False).aggregate(
unexcused=Count("absent")
if request.user.has_perm("alsijil.view_person_statistics_personalnote", person):
school_terms = SchoolTerm.objects.all().order_by("-date_start")
stats = []
for school_term in school_terms:
stat = {}
personal_notes = PersonalNote.objects.filter(
person=person, lesson_period__lesson__validity__school_term=school_term
)
)
stat.update(personal_notes.aggregate(tardiness=Sum("late")))
for extra_mark in ExtraMark.objects.all():
if not personal_notes.exists():
continue
stat.update(
personal_notes.filter(extra_marks=extra_mark).aggregate(
**{extra_mark.count_label: Count("pk")}
personal_notes.filter(absent=True).aggregate(
absences_count=Count("absent")
)
)
for excuse_type in ExcuseType.objects.all():
stat.update(
personal_notes.filter(absent=True, excuse_type=excuse_type).aggregate(
**{excuse_type.count_label: Count("absent")}
personal_notes.filter(
absent=True, excused=True, excuse_type__isnull=True
).aggregate(excused=Count("absent"))
)
stat.update(
personal_notes.filter(absent=True, excused=False).aggregate(
unexcused=Count("absent")
)
)
stat.update(personal_notes.aggregate(tardiness=Sum("late")))
stats.append((school_term, stat))
context["stats"] = stats
for extra_mark in ExtraMark.objects.all():
stat.update(
personal_notes.filter(extra_marks=extra_mark).aggregate(
**{extra_mark.count_label: Count("pk")}
)
)
for excuse_type in ExcuseType.objects.all():
stat.update(
personal_notes.filter(
absent=True, excuse_type=excuse_type
).aggregate(**{excuse_type.count_label: Count("absent")})
)
stats.append((school_term, stat))
context["stats"] = stats
context["excuse_types"] = ExcuseType.objects.all()
context["extra_marks"] = ExtraMark.objects.all()
return render(request, "alsijil/class_register/person.html", context)
@permission_required("alsijil.view_register_absence")
def register_absence(request: HttpRequest) -> HttpResponse:
context = {}
register_absence_form = RegisterAbsenceForm(request.POST or None)
if request.method == "POST":
if register_absence_form.is_valid():
if register_absence_form.is_valid() and request.user.has_perm(
"alsijil.register_absence", register_absence_form.cleaned_data["person"]
):
# Get data from form
person = register_absence_form.cleaned_data["person"]
start_date = register_absence_form.cleaned_data["date_start"]
......@@ -649,9 +689,10 @@ def register_absence(request: HttpRequest) -> HttpResponse:
return render(request, "alsijil/absences/register.html", context)
class DeletePersonalNoteView(DetailView):
class DeletePersonalNoteView(PermissionRequiredMixin, DetailView):
model = PersonalNote
template_name = "core/pages/delete.html"
permission_required = "alsijil.edit_personalnote"
def post(self, request, *args, **kwargs):
note = self.get_object()
......@@ -662,83 +703,83 @@ class DeletePersonalNoteView(DetailView):
return redirect("overview_person", note.person.pk)
class ExtraMarkListView(SingleTableView, PermissionRequiredMixin):
class ExtraMarkListView(PermissionRequiredMixin, SingleTableView):
"""Table of all extra marks."""
model = ExtraMark
table_class = ExtraMarkTable
permission_required = "core.view_extramark"
permission_required = "alsijil.view_extramark"
template_name = "alsijil/extra_mark/list.html"
class ExtraMarkCreateView(AdvancedCreateView, PermissionRequiredMixin):
class ExtraMarkCreateView(PermissionRequiredMixin, AdvancedCreateView):
"""Create view for extra marks."""
model = ExtraMark
form_class = ExtraMarkForm
permission_required = "core.create_extramark"
permission_required = "alsijil.create_extramark"
template_name = "alsijil/extra_mark/create.html"
success_url = reverse_lazy("extra_marks")
success_message = _("The extra mark has been created.")
class ExtraMarkEditView(AdvancedEditView, PermissionRequiredMixin):
class ExtraMarkEditView(PermissionRequiredMixin, AdvancedEditView):
"""Edit view for extra marks."""
model = ExtraMark
form_class = ExtraMarkForm
permission_required = "core.edit_extramark"
permission_required = "alsijil.edit_extramark"
template_name = "alsijil/extra_mark/edit.html"
success_url = reverse_lazy("extra_marks")
success_message = _("The extra mark has been saved.")
class ExtraMarkDeleteView(AdvancedDeleteView, PermissionRequiredMixin, RevisionMixin):
class ExtraMarkDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
"""Delete view for extra marks"""
model = ExtraMark
permission_required = "core.delete_extramark"
permission_required = "alsijil.delete_extramark"
template_name = "core/pages/delete.html"
success_url = reverse_lazy("extra_marks")
success_message = _("The extra mark has been deleted.")
class ExcuseTypeListView(SingleTableView, PermissionRequiredMixin):
class ExcuseTypeListView(PermissionRequiredMixin, SingleTableView):
"""Table of all excuse types."""
model = ExcuseType
table_class = ExcuseTypeTable
permission_required = "core.view_excusetype"
permission_required = "alsijil.view_excusetypes"
template_name = "alsijil/excuse_type/list.html"
class ExcuseTypeCreateView(AdvancedCreateView, PermissionRequiredMixin):
class ExcuseTypeCreateView(PermissionRequiredMixin, AdvancedCreateView):
"""Create view for excuse types."""
model = ExcuseType
form_class = ExcuseTypeForm
permission_required = "core.create_excusetype"
permission_required = "alsijil.add_excusetype"
template_name = "alsijil/excuse_type/create.html"
success_url = reverse_lazy("excuse_types")
success_message = _("The excuse type has been created.")
class ExcuseTypeEditView(AdvancedEditView, PermissionRequiredMixin):
class ExcuseTypeEditView(PermissionRequiredMixin, AdvancedEditView):
"""Edit view for excuse types."""
model = ExcuseType
form_class = ExcuseTypeForm
permission_required = "core.edit_excusetype"
permission_required = "alsijil.edit_excusetype"
template_name = "alsijil/excuse_type/edit.html"
success_url = reverse_lazy("excuse_types")
success_message = _("The excuse type has been saved.")
class ExcuseTypeDeleteView(AdvancedDeleteView, PermissionRequiredMixin, RevisionMixin):
class ExcuseTypeDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
"""Delete view for excuse types"""
model = ExcuseType
permission_required = "core.delete_excusetype"
permission_required = "alsijil.delete_excusetype"
template_name = "core/pages/delete.html"
success_url = reverse_lazy("excuse_types")
success_message = _("The excuse type has been deleted.")
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