From 672ee4e3d94c785057f47d70fd188a1abad7c6b5 Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Sun, 25 Jul 2021 21:15:46 +0200 Subject: [PATCH] Add views for managing automatic plans --- aleksis/apps/chronos/admin.py | 3 + aleksis/apps/chronos/forms.py | 12 +- aleksis/apps/chronos/menus.py | 11 ++ aleksis/apps/chronos/rules.py | 28 +++++ aleksis/apps/chronos/tables.py | 25 ++++ .../chronos/automatic_plan/create.html | 15 +++ .../chronos/automatic_plan/edit.html | 15 +++ .../chronos/automatic_plan/list.html | 18 +++ aleksis/apps/chronos/urls.py | 21 ++++ aleksis/apps/chronos/views.py | 115 +++++++++++++++--- 10 files changed, 245 insertions(+), 18 deletions(-) create mode 100644 aleksis/apps/chronos/templates/chronos/automatic_plan/create.html create mode 100644 aleksis/apps/chronos/templates/chronos/automatic_plan/edit.html create mode 100644 aleksis/apps/chronos/templates/chronos/automatic_plan/list.html diff --git a/aleksis/apps/chronos/admin.py b/aleksis/apps/chronos/admin.py index 34b8df69..8bcc9d9f 100644 --- a/aleksis/apps/chronos/admin.py +++ b/aleksis/apps/chronos/admin.py @@ -8,6 +8,7 @@ from guardian.admin import GuardedModelAdmin from .models import ( Absence, AbsenceReason, + AutomaticPlan, Break, Event, ExtraLesson, @@ -212,3 +213,5 @@ class ValidityRangeAdmin(admin.ModelAdmin): admin.site.register(ValidityRange, ValidityRangeAdmin) + +admin.site.register(AutomaticPlan) diff --git a/aleksis/apps/chronos/forms.py b/aleksis/apps/chronos/forms.py index 3233cf89..b628c2d5 100644 --- a/aleksis/apps/chronos/forms.py +++ b/aleksis/apps/chronos/forms.py @@ -2,11 +2,11 @@ from django import forms from django.utils.translation import gettext_lazy as _ from django_select2.forms import ModelSelect2MultipleWidget -from material import Fieldset +from material import Fieldset, Layout from aleksis.core.forms import AnnouncementForm -from .models import LessonSubstitution +from .models import AutomaticPlan, LessonSubstitution class LessonSubstitutionForm(forms.ModelForm): @@ -27,3 +27,11 @@ class LessonSubstitutionForm(forms.ModelForm): AnnouncementForm.add_node_to_layout(Fieldset(_("Options for timetables"), "show_in_timetables")) + + +class AutomaticPlanForm(forms.ModelForm): + layout = Layout("slug", "name", "number_of_days", "show_header_box") + + class Meta: + model = AutomaticPlan + fields = ["slug", "name", "number_of_days", "show_header_box"] diff --git a/aleksis/apps/chronos/menus.py b/aleksis/apps/chronos/menus.py index 89fb6d8d..ec6337f7 100644 --- a/aleksis/apps/chronos/menus.py +++ b/aleksis/apps/chronos/menus.py @@ -56,6 +56,17 @@ MENUS = { ), ], }, + { + "name": _("Automatic plans"), + "url": "automatic_plans", + "icon": "update", + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "chronos.view_automaticplans", + ), + ], + }, ], } ] diff --git a/aleksis/apps/chronos/rules.py b/aleksis/apps/chronos/rules.py index d34a3f49..9947a565 100644 --- a/aleksis/apps/chronos/rules.py +++ b/aleksis/apps/chronos/rules.py @@ -51,3 +51,31 @@ add_perm("chronos.view_substitutions_rule", view_substitutions_predicate) # View room (timetable) view_room_predicate = has_person & has_room_timetable_perm add_perm("chronos.view_room_rule", view_room_predicate) + +# View automatic plan list +view_automatic_plans_predicate = has_person & has_global_perm("chronos.view_automaticplan") +add_perm("chronos.view_automaticplans_rule", view_automatic_plans_predicate) + +# View automatic plan +view_automatic_plan_predicate = has_person & ( + has_global_perm("chronos.view_automaticplan") | has_object_perm("chronos.view_automaticplan") +) +add_perm("chronos.view_automaticplan_rule", view_automatic_plan_predicate) + +# Add automatic plan +add_automatic_plan_predicate = view_automatic_plans_predicate & has_global_perm( + "chronos.add_automaticplan" +) +add_perm("chronos.add_automaticplan_rule", add_automatic_plan_predicate) + +# Edit automatic plan +edit_automatic_plan_predicate = view_automatic_plans_predicate & has_global_perm( + "chronos.change_automaticplan" +) +add_perm("chronos.edit_automaticplan_rule", edit_automatic_plan_predicate) + +# Delete automatic plan +delete_automatic_plan_predicate = view_automatic_plans_predicate & has_global_perm( + "chronos.delete_automaticplan" +) +add_perm("chronos.delete_automaticplan_rule", delete_automatic_plan_predicate) diff --git a/aleksis/apps/chronos/tables.py b/aleksis/apps/chronos/tables.py index 6caff934..bb699dcb 100644 --- a/aleksis/apps/chronos/tables.py +++ b/aleksis/apps/chronos/tables.py @@ -42,3 +42,28 @@ class LessonsTable(tables.Table): attrs={"a": {"class": "btn-flat waves-effect waves-orange"}}, verbose_name=_("Manage substitution"), ) + + +class AutomaticPlanTable(tables.Table): + """Table for automatic plans.""" + + class Meta: + attrs = {"class": "highlight"} + + name = tables.LinkColumn("edit_automatic_plan", args=[A("id")]) + filename = tables.LinkColumn("show_automatic_plan", args=[A("slug")]) + local_path = tables.Column() + last_update = tables.DateTimeColumn() + last_update_triggered_manually = tables.BooleanColumn() + edit = tables.LinkColumn( + "edit_automatic_plan", + args=[A("id")], + text=_("Edit"), + attrs={"a": {"class": "btn-flat waves-effect waves-orange orange-text"}}, + ) + delete = tables.LinkColumn( + "delete_automatic_plan", + args=[A("id")], + text=_("Delete"), + attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}}, + ) diff --git a/aleksis/apps/chronos/templates/chronos/automatic_plan/create.html b/aleksis/apps/chronos/templates/chronos/automatic_plan/create.html new file mode 100644 index 00000000..7e41d220 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/automatic_plan/create.html @@ -0,0 +1,15 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block browser_title %}{% blocktrans %}Create automatic plan{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Create automatic plan{% endblocktrans %}{% endblock %} + +{% block content %} + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> +{% endblock %} diff --git a/aleksis/apps/chronos/templates/chronos/automatic_plan/edit.html b/aleksis/apps/chronos/templates/chronos/automatic_plan/edit.html new file mode 100644 index 00000000..db002486 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/automatic_plan/edit.html @@ -0,0 +1,15 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block browser_title %}{% blocktrans %}Edit automatic plan{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Edit automatic plan{% endblocktrans %}{% endblock %} + +{% block content %} + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> +{% endblock %} diff --git a/aleksis/apps/chronos/templates/chronos/automatic_plan/list.html b/aleksis/apps/chronos/templates/chronos/automatic_plan/list.html new file mode 100644 index 00000000..a62447c5 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/automatic_plan/list.html @@ -0,0 +1,18 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} + +{% load i18n %} +{% load render_table from django_tables2 %} + +{% block browser_title %}{% blocktrans %}Automatic plans{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Autoamtic plans{% endblocktrans %}{% endblock %} + +{% block content %} + <a class="btn green waves-effect waves-light" href="{% url 'create_automatic_plan' %}"> + <i class="material-icons left">add</i> + {% trans "Create automatic plan" %} + </a> + + {% render_table table %} +{% endblock %} diff --git a/aleksis/apps/chronos/urls.py b/aleksis/apps/chronos/urls.py index fc6d9a86..765db990 100644 --- a/aleksis/apps/chronos/urls.py +++ b/aleksis/apps/chronos/urls.py @@ -54,4 +54,25 @@ urlpatterns = [ {"is_print": True}, name="substitutions_print_by_date", ), + path("automatic_plans/", views.AutomaticPlanListView.as_view(), name="automatic_plans"), + path( + "automatic_plans/create/", + views.AutomaticPlanCreateView.as_view(), + name="create_automatic_plan", + ), + path( + "automatic_plans/<int:pk>/edit/", + views.AutomaticPlanEditView.as_view(), + name="edit_automatic_plan", + ), + path( + "automatic_plans/<int:pk>/delete/", + views.AutomaticPlanDeleteView.as_view(), + name="delete_automatic_plan", + ), + path( + "automatic_plans/<str:slug>.pdf", + views.AutomaticPlanShowView.as_view(), + name="show_automatic_plan", + ), ] diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py index 67e256a3..2829ddc4 100644 --- a/aleksis/apps/chronos/views.py +++ b/aleksis/apps/chronos/views.py @@ -1,26 +1,31 @@ from datetime import datetime, timedelta -from typing import Optional +from typing import Any, Optional from django.db.models import Q -from django.http import HttpRequest, HttpResponse, HttpResponseNotFound +from django.http import FileResponse, HttpRequest, HttpResponse, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse +from django.urls import reverse, reverse_lazy from django.utils import timezone +from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _ +from django.views import View from django.views.decorators.cache import never_cache +from django.views.generic.detail import SingleObjectMixin -from django_tables2 import RequestConfig -from rules.contrib.views import permission_required +from django_tables2 import RequestConfig, SingleTableView +from reversion.views import RevisionMixin +from rules.contrib.views import PermissionRequiredMixin, permission_required +from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView from aleksis.core.models import Announcement, Group from aleksis.core.util import messages from aleksis.core.util.core_helpers import get_site_preferences, has_person from aleksis.core.util.pdf import render_pdf -from .forms import LessonSubstitutionForm +from .forms import AutomaticPlanForm, LessonSubstitutionForm from .managers import TimetableType -from .models import Absence, Holiday, LessonPeriod, LessonSubstitution, TimePeriod -from .tables import LessonsTable +from .models import Absence, AutomaticPlan, Holiday, LessonPeriod, LessonSubstitution, TimePeriod +from .tables import AutomaticPlanTable, LessonsTable from .util.build import build_substitutions_list, build_timetable, build_weekdays from .util.chronos_helpers import ( get_classes, @@ -288,15 +293,16 @@ def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpRespon return redirect("lessons_day_by_date", year=date.year, month=date.month, day=date.day) -@permission_required("chronos.view_substitutions_rule") -def substitutions( - request: HttpRequest, +def get_substitutions_context_data( + request: Optional[HttpRequest] = None, year: Optional[int] = None, month: Optional[int] = None, day: Optional[int] = None, is_print: bool = False, -) -> HttpResponse: - """View all substitutions on a spcified day.""" + number_of_days: Optional[int] = None, + show_header_box: Optional[bool] = None, +): + """Get context data for the substitutions table.""" context = {} if day: @@ -305,7 +311,14 @@ def substitutions( else: wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time()) - day_number = get_site_preferences()["chronos__substitutions_print_number_of_days"] + day_number = ( + number_of_days or get_site_preferences()["chronos__substitutions_print_number_of_days"] + ) + show_header_box = ( + show_header_box + if show_header_box is not None + else get_site_preferences()["chronos__substitutions_show_header_box"] + ) day_contexts = {} if is_print: @@ -324,7 +337,7 @@ def substitutions( Announcement.for_timetables().on_date(day).filter(show_in_timetables=True) ) - if get_site_preferences()["chronos__substitutions_show_header_box"]: + if show_header_box: subs = LessonSubstitution.objects.on_day(day).order_by( "lesson_period__lesson__groups", "lesson_period__period" ) @@ -353,8 +366,78 @@ def substitutions( wanted_day, "substitutions_by_date" ) - return render(request, "chronos/substitutions.html", context) else: context["days"] = day_contexts + return context + + +@permission_required("chronos.view_substitutions_rule") +def substitutions( + request: HttpRequest, + year: Optional[int] = None, + month: Optional[int] = None, + day: Optional[int] = None, + is_print: bool = False, +) -> HttpResponse: + """View all substitutions on a specified day.""" + context = get_substitutions_context_data(request, year, month, day, is_print) + if not is_print: + return render(request, "chronos/substitutions.html", context) + else: return render_pdf(request, "chronos/substitutions_print.html", context) + + +class AutomaticPlanListView(PermissionRequiredMixin, SingleTableView): + """Table of all automatic plans.""" + + model = AutomaticPlan + table_class = AutomaticPlanTable + permission_required = "chronos.view_automaticplans_rule" + template_name = "chronos/automatic_plan/list.html" + + +@method_decorator(never_cache, name="dispatch") +class AutomaticPlanCreateView(PermissionRequiredMixin, AdvancedCreateView): + """Create view for automatic plans.""" + + model = AutomaticPlan + form_class = AutomaticPlanForm + permission_required = "chronos.add_automaticplan_rule" + template_name = "chronos/automatic_plan/create.html" + success_url = reverse_lazy("automatic_plans") + success_message = _("The automatic plan has been created.") + + +@method_decorator(never_cache, name="dispatch") +class AutomaticPlanEditView(PermissionRequiredMixin, AdvancedEditView): + """Edit view for automatic plans.""" + + model = AutomaticPlan + form_class = AutomaticPlanForm + permission_required = "chronos.edit_automaticplan_rule" + template_name = "chronos/automatic_plan/edit.html" + success_url = reverse_lazy("automatic_plans") + success_message = _("The automatic plan has been saved.") + + +@method_decorator(never_cache, name="dispatch") +class AutomaticPlanDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView): + """Delete view for automatic plans.""" + + model = AutomaticPlan + permission_required = "chronos.delete_automaticplan_rule" + template_name = "core/pages/delete.html" + success_url = reverse_lazy("automatic_plans") + success_message = _("The automatic plan has been deleted.") + + +class AutomaticPlanShowView(PermissionRequiredMixin, SingleObjectMixin, View): + """Show the current version of the automatic plan.""" + + model = AutomaticPlan + permission_required = "resint.view_automaticplan_rule" + + def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> FileResponse: + automatic_plan = self.get_object() + return FileResponse(automatic_plan.get_current_file(), content_type="application/pdf") -- GitLab