Skip to content
Snippets Groups Projects
Verified Commit 672ee4e3 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Add views for managing automatic plans

parent c110a68a
No related branches found
No related tags found
1 merge request!191Resolve "Automatically create a PDF file of the substitution plan"
Pipeline #23762 failed
...@@ -8,6 +8,7 @@ from guardian.admin import GuardedModelAdmin ...@@ -8,6 +8,7 @@ from guardian.admin import GuardedModelAdmin
from .models import ( from .models import (
Absence, Absence,
AbsenceReason, AbsenceReason,
AutomaticPlan,
Break, Break,
Event, Event,
ExtraLesson, ExtraLesson,
...@@ -212,3 +213,5 @@ class ValidityRangeAdmin(admin.ModelAdmin): ...@@ -212,3 +213,5 @@ class ValidityRangeAdmin(admin.ModelAdmin):
admin.site.register(ValidityRange, ValidityRangeAdmin) admin.site.register(ValidityRange, ValidityRangeAdmin)
admin.site.register(AutomaticPlan)
...@@ -2,11 +2,11 @@ from django import forms ...@@ -2,11 +2,11 @@ from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_select2.forms import ModelSelect2MultipleWidget from django_select2.forms import ModelSelect2MultipleWidget
from material import Fieldset from material import Fieldset, Layout
from aleksis.core.forms import AnnouncementForm from aleksis.core.forms import AnnouncementForm
from .models import LessonSubstitution from .models import AutomaticPlan, LessonSubstitution
class LessonSubstitutionForm(forms.ModelForm): class LessonSubstitutionForm(forms.ModelForm):
...@@ -27,3 +27,11 @@ class LessonSubstitutionForm(forms.ModelForm): ...@@ -27,3 +27,11 @@ class LessonSubstitutionForm(forms.ModelForm):
AnnouncementForm.add_node_to_layout(Fieldset(_("Options for timetables"), "show_in_timetables")) 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"]
...@@ -56,6 +56,17 @@ MENUS = { ...@@ -56,6 +56,17 @@ MENUS = {
), ),
], ],
}, },
{
"name": _("Automatic plans"),
"url": "automatic_plans",
"icon": "update",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"chronos.view_automaticplans",
),
],
},
], ],
} }
] ]
......
...@@ -51,3 +51,31 @@ add_perm("chronos.view_substitutions_rule", view_substitutions_predicate) ...@@ -51,3 +51,31 @@ add_perm("chronos.view_substitutions_rule", view_substitutions_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)
# 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)
...@@ -42,3 +42,28 @@ class LessonsTable(tables.Table): ...@@ -42,3 +42,28 @@ 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 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"}},
)
{# -*- 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 %}
{# -*- 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 %}
{# -*- 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 %}
...@@ -54,4 +54,25 @@ urlpatterns = [ ...@@ -54,4 +54,25 @@ urlpatterns = [
{"is_print": True}, {"is_print": True},
name="substitutions_print_by_date", 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",
),
] ]
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional from typing import Any, Optional
from django.db.models import Q 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.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 import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views import View
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.generic.detail import SingleObjectMixin
from django_tables2 import RequestConfig from django_tables2 import RequestConfig, SingleTableView
from rules.contrib.views import permission_required 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.models import Announcement, Group
from aleksis.core.util import messages from aleksis.core.util import messages
from aleksis.core.util.core_helpers import get_site_preferences, has_person from aleksis.core.util.core_helpers import get_site_preferences, has_person
from aleksis.core.util.pdf import render_pdf from aleksis.core.util.pdf import render_pdf
from .forms import LessonSubstitutionForm from .forms import AutomaticPlanForm, LessonSubstitutionForm
from .managers import TimetableType from .managers import TimetableType
from .models import Absence, Holiday, LessonPeriod, LessonSubstitution, TimePeriod from .models import Absence, AutomaticPlan, Holiday, LessonPeriod, LessonSubstitution, TimePeriod
from .tables import LessonsTable from .tables import AutomaticPlanTable, LessonsTable
from .util.build import build_substitutions_list, build_timetable, build_weekdays from .util.build import build_substitutions_list, build_timetable, build_weekdays
from .util.chronos_helpers import ( from .util.chronos_helpers import (
get_classes, get_classes,
...@@ -288,15 +293,16 @@ def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpRespon ...@@ -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) return redirect("lessons_day_by_date", year=date.year, month=date.month, day=date.day)
@permission_required("chronos.view_substitutions_rule") def get_substitutions_context_data(
def substitutions( request: Optional[HttpRequest] = None,
request: HttpRequest,
year: Optional[int] = None, year: Optional[int] = None,
month: Optional[int] = None, month: Optional[int] = None,
day: Optional[int] = None, day: Optional[int] = None,
is_print: bool = False, is_print: bool = False,
) -> HttpResponse: number_of_days: Optional[int] = None,
"""View all substitutions on a spcified day.""" show_header_box: Optional[bool] = None,
):
"""Get context data for the substitutions table."""
context = {} context = {}
if day: if day:
...@@ -305,7 +311,14 @@ def substitutions( ...@@ -305,7 +311,14 @@ def substitutions(
else: else:
wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time()) 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 = {} day_contexts = {}
if is_print: if is_print:
...@@ -324,7 +337,7 @@ def substitutions( ...@@ -324,7 +337,7 @@ def substitutions(
Announcement.for_timetables().on_date(day).filter(show_in_timetables=True) 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( subs = LessonSubstitution.objects.on_day(day).order_by(
"lesson_period__lesson__groups", "lesson_period__period" "lesson_period__lesson__groups", "lesson_period__period"
) )
...@@ -353,8 +366,78 @@ def substitutions( ...@@ -353,8 +366,78 @@ def substitutions(
wanted_day, "substitutions_by_date" wanted_day, "substitutions_by_date"
) )
return render(request, "chronos/substitutions.html", context)
else: else:
context["days"] = day_contexts 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) 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")
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