diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py index 6f4e65b2022192c3cf5c0a7c4b44cc3e1731919e..e325586cbf8f0a1bace8c186fc85e76b309e0d35 100644 --- a/aleksis/apps/alsijil/forms.py +++ b/aleksis/apps/alsijil/forms.py @@ -31,6 +31,7 @@ from .models import ( ExtraMark, GroupRole, GroupRoleAssignment, + Instruction, LessonDocumentation, PersonalNote, ) @@ -383,3 +384,11 @@ class RegisterObjectActionForm(ListActionForm): """Action form for managing register objects for use with ``RegisterObjectTable``.""" actions = [send_request_to_check_entry] + + +class InstructionForm(forms.ModelForm): + layout = Layout("name", "icon", "pdf_file", "groups") + + class Meta: + model = Instruction + fields = ["name", "icon", "pdf_file", "groups"] diff --git a/aleksis/apps/alsijil/migrations/0014_instruction.py b/aleksis/apps/alsijil/migrations/0017_instruction.py similarity index 99% rename from aleksis/apps/alsijil/migrations/0014_instruction.py rename to aleksis/apps/alsijil/migrations/0017_instruction.py index 48525627f2fdbc0950fc1bddfbf2bb10e09eebf1..11e200873c02ce578fda5364804bad0efc363dba 100644 --- a/aleksis/apps/alsijil/migrations/0014_instruction.py +++ b/aleksis/apps/alsijil/migrations/0017_instruction.py @@ -10,7 +10,7 @@ class Migration(migrations.Migration): dependencies = [ ('core', '0019_fix_uniqueness_per_site'), ('sites', '0002_alter_domain_unique'), - ('alsijil', '0013_fix_uniqueness_per_site'), + ('alsijil', '0016_add_not_counted_excuse_types'), ] operations = [ diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index 42189856b660e771f0dd36efd14414450a6485c9..90c2868ba3f1a625d83f045dc9dc0d15a0c31898 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -525,6 +525,7 @@ class Instruction(SchoolTermRelatedExtensibleModel): help_text=_( "The instruction will be shown for the members and owners of the selected groups." ), + related_name="instructions", ) def __str__(self): diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py index 621f3e3b2e6994fc2f06b0081d6fa8c191eca641..aa80713bda0ce2e643e03e1a0f2f484b7aea297b 100644 --- a/aleksis/apps/alsijil/rules.py +++ b/aleksis/apps/alsijil/rules.py @@ -11,12 +11,14 @@ from aleksis.core.util.predicates import ( ) from .util.predicates import ( + has_any_instruction, has_lesson_group_object_perm, has_person_group_object_perm, has_personal_note_group_perm, is_group_member, is_group_owner, is_group_role_assignment_group_owner, + is_instruction_for_person, is_lesson_original_teacher, is_lesson_parent_group_owner, is_lesson_participant, @@ -311,5 +313,27 @@ view_register_objects_list_predicate = has_person & ( ) add_perm("alsijil.view_register_objects_list_rule", view_register_objects_list_predicate) -view_instructions_predicate = has_person +view_instructions_predicate = has_person & ( + has_global_perm("alsijil.view_instruction") | has_any_instruction +) add_perm("alsijil.view_instructions_rule", view_instructions_predicate) + +view_instruction_predicate = has_person & ( + has_global_perm("alsijil.view_instruction") + | is_instruction_for_person + | has_object_perm("alsijil.view_instruction") +) +add_perm("alsijil.view_instruction_rule", view_instruction_predicate) + +add_instruction_predicate = view_instructions_predicate & has_global_perm("alsijil.add_instruction") +add_perm("alsijil.add_instruction_rule", add_instruction_predicate) + +edit_instruction_predicate = view_instructions_predicate & ( + has_global_perm("alsijil.change_instruction") | has_object_perm("alsijil.change_instruction") +) +add_perm("alsijil.edit_instruction_rule", edit_instruction_predicate) + +delete_instruction_predicate = view_instructions_predicate & ( + has_global_perm("alsijil.delete_instruction") | has_object_perm("alsijil.delete_instruction") +) +add_perm("alsijil.delete_instruction_rule", delete_instruction_predicate) diff --git a/aleksis/apps/alsijil/templates/alsijil/instruction/create.html b/aleksis/apps/alsijil/templates/alsijil/instruction/create.html new file mode 100644 index 0000000000000000000000000000000000000000..42761a81361d4e86b085082cfa5500360426ac3a --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/instruction/create.html @@ -0,0 +1,15 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block browser_title %}{% blocktrans %}Create instruction{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Create instruction{% endblocktrans %}{% endblock %} + +{% block content %} + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> +{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/instruction/edit.html b/aleksis/apps/alsijil/templates/alsijil/instruction/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..b9732e4c41c93c7e17f498fb942581921bc3b7f0 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/instruction/edit.html @@ -0,0 +1,17 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block browser_title %}{% blocktrans %}Edit instruction{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Edit instruction{% endblocktrans %}{% endblock %} + +{% block content %} + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + + {{ form.media.js }} +{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/instruction/list.html b/aleksis/apps/alsijil/templates/alsijil/instruction/list.html index da3c384b39440f78e27ac4700124a3ed58d3984b..02320901a0bfc927d3c2e95dd174e26f24904370 100644 --- a/aleksis/apps/alsijil/templates/alsijil/instruction/list.html +++ b/aleksis/apps/alsijil/templates/alsijil/instruction/list.html @@ -2,21 +2,51 @@ {% extends "core/base.html" %} -{% load i18n %} +{% load i18n rules %} {% block browser_title %}{% blocktrans %}Instructions{% endblocktrans %}{% endblock %} {% block page_title %}{% blocktrans %}Instructions{% endblocktrans %}{% endblock %} {% block content %} + {% has_perm "alsijil.add_instruction_rule" user as can_add %} + {% if can_add %} + <a class="btn green waves-effect waves-light" href="{% url 'create_instruction' %}"> + <i class="material-icons left">add</i> + {% trans "Create instruction" %} + </a> + {% endif %} + <ul class="collection"> {% for instruction in instruction_list %} <li class="collection-item avatar"> <i class="material-icons materialize-circle primary-color">{{ instruction.icon|default:"rule" }}</i> <span class="title"> {{ instruction.name }}</span> + <div class="right"> + {% has_perm "alsijil.edit_instruction_rule" user as can_edit %} + {% has_perm "alsijil.delete_instruction_rule" user as can_delete %} + {% if can_edit %} + <a class="btn-flat waves-effect waves-orange orange-text" + href="{% url "edit_instruction" instruction.pk %}"> + <i class="material-icons left">edit</i> + {% trans "Edit" %} + </a> + {% endif %} + {% if can_delete %} + <a class="btn-flat waves-effect waves-red red-text" href="{% url "delete_instruction" instruction.pk %}"> + <i class="material-icons left">delete</i> + {% trans "Delete" %} + </a> + {% endif %} + <a class="btn-flat waves-effect waves-green right" href="{{ instruction.pdf_file.url }}" target="_blank"> <i class="material-icons left">picture_as_pdf</i> {% trans "Show PDF file with instruction" %} </a> + </div> + </li> + {% empty %} + <li class="collection-item"> + {% trans "No instructions available." %} </li> {% endfor %} </ul> diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py index f7551eb8eb53e4bd21b639eb8b67d0a69daf0ced..19f92a2354bbd4810a38cd6e40b5347c240cfd38 100644 --- a/aleksis/apps/alsijil/urls.py +++ b/aleksis/apps/alsijil/urls.py @@ -129,4 +129,13 @@ urlpatterns = [ ), path("all/", views.AllRegisterObjectsView.as_view(), name="all_register_objects"), path("instructions/", views.InstructionsListView.as_view(), name="instructions"), + path("instructions/create/", views.InstructionCreateView.as_view(), name="create_instruction"), + path( + "instructions/<int:pk>/edit/", views.InstructionEditView.as_view(), name="edit_instruction" + ), + path( + "instructions/<int:pk>/delete/", + views.InstructionDeleteView.as_view(), + name="delete_instruction", + ), ] diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py index 1759a5446d204d61c87f5bd1cbe9e3e031d95be2..377779d30e9c01de09f48f8befd9921326dcb010 100644 --- a/aleksis/apps/alsijil/util/predicates.py +++ b/aleksis/apps/alsijil/util/predicates.py @@ -1,6 +1,7 @@ from typing import Any, Union from django.contrib.auth.models import User +from django.db.models import Q from rules import predicate @@ -8,7 +9,7 @@ from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod from aleksis.core.models import Group, Person from aleksis.core.util.predicates import check_object_permission -from ..models import PersonalNote +from ..models import Instruction, PersonalNote @predicate @@ -281,3 +282,22 @@ def is_group_role_assignment_group_owner(user: User, obj: Union[Group, Person]) def is_owner_of_any_group(user: User, obj): """Predicate which checks if the person is group owner of any group.""" return Group.objects.filter(owners=user.person).exists() + + +@predicate +def has_any_instruction(user: User, obj): + """Predicate which checks if the user has any instruction.""" + return Instruction.objects.filter( + Q(groups__members=user.person) | Q(groups__owners=user.person) + ).exists() + + +@predicate +def is_instruction_for_person(user: User, obj: Instruction): + """Predicate which checks if the instruction is for the person.""" + return ( + user + in Person.objects.filter( + Q(member_of__instructions=obj) | Q(owner_of__instructions=obj) + ).exists() + ) diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index 17b6e2dee3b7e0a1d28fea2986ba6a4b8d11571e..f05b90fb4e569920d18465f4537582c1c2f0d138 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -51,6 +51,7 @@ from .forms import ( FilterRegisterObjectForm, GroupRoleAssignmentEditForm, GroupRoleForm, + InstructionForm, LessonDocumentationForm, PersonalNoteFormSet, PersonOverviewForm, @@ -1405,8 +1406,56 @@ class AllRegisterObjectsView(PermissionRequiredMixin, View): class InstructionsListView(PermissionRequiredMixin, ListView): - """Table of all excuse types.""" + """Table of all instructions.""" model = Instruction permission_required = "alsijil.view_instructions_rule" template_name = "alsijil/instruction/list.html" + + def get_queryset(self): + if self.request.user.has_perm("alsijil.view_instruction"): + return super().get_queryset() + return ( + super() + .get_queryset() + .filter( + Q(groups__members=self.request.user.person) + | Q(groups__owners=self.request.user.person) + ) + .distinct() + ) + + +@method_decorator(never_cache, name="dispatch") +class InstructionCreateView(PermissionRequiredMixin, AdvancedCreateView): + """Create view for instructions.""" + + model = Instruction + form_class = InstructionForm + permission_required = "alsijil.add_instruction_rule" + template_name = "alsijil/instruction/create.html" + success_url = reverse_lazy("instructions") + success_message = _("The instruction has been created.") + + +@method_decorator(never_cache, name="dispatch") +class InstructionEditView(PermissionRequiredMixin, AdvancedEditView): + """Edit view for instructions.""" + + model = Instruction + form_class = InstructionForm + permission_required = "alsijil.edit_instruction_rule" + template_name = "alsijil/instruction/edit.html" + success_url = reverse_lazy("instructions") + success_message = _("The instruction has been saved.") + + +@method_decorator(never_cache, "dispatch") +class InstructionDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView): + """Delete view for instructions.""" + + model = Instruction + permission_required = "alsijil.delete_instruction_rule" + template_name = "core/pages/delete.html" + success_url = reverse_lazy("instructions") + success_message = _("The instruction has been deleted.")