diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index abd45aabdee3f9d933376a0fc034f59218a8523f..208cbff93f76b7d44451550b73d3b5659ba4aa59 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -9,8 +9,8 @@ from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget from dynamic_preferences.forms import PreferenceForm from material import Fieldset, Layout, Row -from .mixins import ExtensibleForm -from .models import AdditionalField, Announcement, Group, GroupType, Person +from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm +from .models import AdditionalField, Announcement, Group, GroupType, Person, SchoolTerm from .registries import ( group_preferences_registry, person_preferences_registry, @@ -121,10 +121,11 @@ class EditPersonForm(ExtensibleForm): return PersonAccountForm.clean(self) -class EditGroupForm(ExtensibleForm): +class EditGroupForm(SchoolTermRelatedExtensibleForm): """Form to edit an existing group in the frontend.""" layout = Layout( + Fieldset(_("School term"), "school_term"), Fieldset(_("Common data"), "name", "short_name", "group_type"), Fieldset(_("Persons"), "members", "owners", "parent_groups"), Fieldset(_("Additional data"), "additional_fields"), @@ -170,7 +171,7 @@ class AnnouncementForm(ExtensibleForm): persons = forms.ModelMultipleChoiceField( Person.objects.all(), label=_("Persons"), required=False ) - groups = forms.ModelMultipleChoiceField(Group.objects.all(), label=_("Groups"), required=False) + groups = forms.ModelMultipleChoiceField(queryset=None, label=_("Groups"), required=False) layout = Layout( Fieldset( @@ -205,6 +206,8 @@ class AnnouncementForm(ExtensibleForm): super().__init__(*args, **kwargs) + self.fields["groups"].queryset = Group.objects.for_current_school_term_or_all() + def clean(self): data = super().clean() @@ -298,3 +301,13 @@ class EditGroupTypeForm(forms.ModelForm): class Meta: model = GroupType exclude = [] + + +class SchoolTermForm(ExtensibleForm): + """Form for managing school years.""" + + layout = Layout("name", Row("date_start", "date_end")) + + class Meta: + model = SchoolTerm + exclude = [] diff --git a/aleksis/core/managers.py b/aleksis/core/managers.py new file mode 100644 index 0000000000000000000000000000000000000000..a0871078015df8e117a09e23a593b384f7b25dc2 --- /dev/null +++ b/aleksis/core/managers.py @@ -0,0 +1,81 @@ +from datetime import date +from typing import Union + +from django.contrib.sites.managers import CurrentSiteManager as _CurrentSiteManager +from django.db.models import QuerySet + +from calendarweek import CalendarWeek + + +class CurrentSiteManagerWithoutMigrations(_CurrentSiteManager): + """CurrentSiteManager for auto-generating managers just by query sets.""" + + use_in_migrations = False + + +class DateRangeQuerySetMixin: + """QuerySet with custom query methods for models with date ranges. + + Filterable fields: date_start, date_end + """ + + def within_dates(self, start: date, end: date): + """Filter for all objects within a date range.""" + return self.filter(date_start__lte=end, date_end__gte=start) + + def in_week(self, wanted_week: CalendarWeek): + """Filter for all objects within a calendar week.""" + return self.within_dates(wanted_week[0], wanted_week[6]) + + def on_day(self, day: date): + """Filter for all objects on a certain day.""" + return self.within_dates(day, day) + + +class SchoolTermQuerySet(QuerySet, DateRangeQuerySetMixin): + """Custom query set for school terms.""" + + +class SchoolTermRelatedQuerySet(QuerySet): + """Custom query set for all models related to school terms.""" + + def within_dates(self, start: date, end: date) -> "SchoolTermRelatedQuerySet": + """Filter for all objects within a date range.""" + return self.filter(school_term__date_start__lte=end, school_term__date_end__gte=start) + + def in_week(self, wanted_week: CalendarWeek) -> "SchoolTermRelatedQuerySet": + """Filter for all objects within a calendar week.""" + return self.within_dates(wanted_week[0], wanted_week[6]) + + def on_day(self, day: date) -> "SchoolTermRelatedQuerySet": + """Filter for all objects on a certain day.""" + return self.within_dates(day, day) + + def for_school_term(self, school_term: "SchoolTerm") -> "SchoolTermRelatedQuerySet": + return self.filter(school_term=school_term) + + def for_current_school_term_or_all(self) -> "SchoolTermRelatedQuerySet": + """Get all objects related to current school term. + + If there is no current school term, it will return all objects. + """ + from aleksis.core.models import SchoolTerm + + current_school_term = SchoolTerm.current + if current_school_term: + return self.for_school_term(current_school_term) + else: + return self + + def for_current_school_term_or_none(self) -> Union["SchoolTermRelatedQuerySet", None]: + """Get all objects related to current school term. + + If there is no current school term, it will return `None`. + """ + from aleksis.core.models import SchoolTerm + + current_school_term = SchoolTerm.current + if current_school_term: + return self.for_school_term(current_school_term) + else: + return None diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py index c1b19bb3970eac18d756ec0eae6bdaf8ae285d07..2e71c357384c1bc7e4ffa4ff1485f4edc4ffc7de 100644 --- a/aleksis/core/menus.py +++ b/aleksis/core/menus.py @@ -82,6 +82,17 @@ MENUS = { ), ], }, + { + "name": _("School terms"), + "url": "school_terms", + "icon": "date_range", + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "core.view_schoolterm", + ), + ], + }, { "name": _("Data management"), "url": "data_management", diff --git a/aleksis/core/migrations/0002_school_term.py b/aleksis/core/migrations/0002_school_term.py new file mode 100644 index 0000000000000000000000000000000000000000..456f78ebf7e16f32b92b954d36ad39f5d323e866 --- /dev/null +++ b/aleksis/core/migrations/0002_school_term.py @@ -0,0 +1,64 @@ +# Generated by Django 3.0.7 on 2020-06-13 14:34 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SchoolTerm', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)), + ('name', models.CharField(max_length=255, unique=True, verbose_name='Name')), + ('date_start', models.DateField(verbose_name='Start date')), + ('date_end', models.DateField(verbose_name='End date')), + ], + options={ + 'verbose_name': 'School term', + 'verbose_name_plural': 'School terms', + }, + ), + migrations.AlterModelManagers( + name='group', + managers=[ + ], + ), + migrations.AlterField( + model_name='group', + name='name', + field=models.CharField(max_length=255, verbose_name='Long name'), + ), + migrations.AlterField( + model_name='group', + name='short_name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Short name'), + ), + migrations.AddField( + model_name='group', + name='school_term', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='+', to='core.SchoolTerm', verbose_name='Linked school term'), + ), + migrations.AddConstraint( + model_name='group', + constraint=models.UniqueConstraint(fields=('school_term', 'name'), name='unique_school_term_name'), + ), + migrations.AddConstraint( + model_name='group', + constraint=models.UniqueConstraint(fields=('school_term', 'short_name'), name='unique_school_term_short_name'), + ), + migrations.AddField( + model_name='schoolterm', + name='site', + field=models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site'), + ), + ] diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py index 6b52fb3820c366c1040905eed1226cdc1d4c0e3f..8bff9408655273aaad6b05901f8f1dd3e38706c1 100644 --- a/aleksis/core/mixins.py +++ b/aleksis/core/mixins.py @@ -4,13 +4,19 @@ from datetime import datetime from typing import Any, Callable, List, Optional, Tuple, Union from django.conf import settings +from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.contrib.sites.managers import CurrentSiteManager from django.contrib.sites.models import Site from django.db import models from django.db.models import QuerySet +from django.forms.forms import BaseForm from django.forms.models import ModelForm, ModelFormMetaclass +from django.http import HttpResponse from django.utils.functional import lazy +from django.utils.translation import gettext as _ +from django.views.generic import CreateView, UpdateView +from django.views.generic.edit import ModelFormMixin import reversion from easyaudit.models import CRUDEvent @@ -19,6 +25,8 @@ from jsonstore.fields import JSONField, JSONFieldMixin from material.base import Layout, LayoutNode from rules.contrib.admin import ObjectPermissionsModelAdmin +from aleksis.core.managers import CurrentSiteManagerWithoutMigrations, SchoolTermRelatedQuerySet + class _ExtensibleModelBase(models.base.ModelBase): """Ensure predefined behaviour on model creation. @@ -279,3 +287,55 @@ class BaseModelAdmin(GuardedModelAdmin, ObjectPermissionsModelAdmin): """A base class for ModelAdmin combining django-guardian and rules.""" pass + + +class SuccessMessageMixin(ModelFormMixin): + success_message: Optional[str] = None + + def form_valid(self, form: BaseForm) -> HttpResponse: + if self.success_message: + messages.success(self.request, self.success_message) + return super().form_valid(form) + + +class AdvancedCreateView(CreateView, SuccessMessageMixin): + pass + + +class AdvancedEditView(UpdateView, SuccessMessageMixin): + pass + + +class SchoolTermRelatedExtensibleModel(ExtensibleModel): + """Add relation to school term.""" + + objects = CurrentSiteManagerWithoutMigrations.from_queryset(SchoolTermRelatedQuerySet)() + + school_term = models.ForeignKey( + "core.SchoolTerm", + on_delete=models.CASCADE, + related_name="+", + verbose_name=_("Linked school term"), + blank=True, + null=True, + ) + + class Meta: + abstract = True + + +class SchoolTermRelatedExtensibleForm(ExtensibleForm): + """Extensible form for school term related data. + + .. warning:: + This doesn't automatically include the field `school_term` in `fields` or `layout`, + it just sets an initial value. + """ + + def __init__(self, *args, **kwargs): + from aleksis.core.models import SchoolTerm # noqa + + if "instance" not in kwargs: + kwargs["initial"] = {"school_term": SchoolTerm.current} + + super().__init__(*args, **kwargs) diff --git a/aleksis/core/models.py b/aleksis/core/models.py index fe0d63d62713fbdfa6ff9b142844d1226c28be6f..0deffed47c9970ab1ab083fa049e90c1960fe6b8 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -8,11 +8,13 @@ from django.contrib.auth.models import Group as DjangoGroup from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site +from django.core.exceptions import ValidationError from django.db import models from django.db.models import QuerySet from django.forms.widgets import Media from django.urls import reverse from django.utils import timezone +from django.utils.decorators import classproperty from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ @@ -22,7 +24,8 @@ from image_cropping import ImageCropField, ImageRatioField from phonenumber_field.modelfields import PhoneNumberField from polymorphic.models import PolymorphicModel -from .mixins import ExtensibleModel, PureDjangoModel +from .managers import CurrentSiteManagerWithoutMigrations, SchoolTermQuerySet +from .mixins import ExtensibleModel, PureDjangoModel, SchoolTermRelatedExtensibleModel from .tasks import send_notification from .util.core_helpers import get_site_preferences, now_tomorrow from .util.model_helpers import ICONS @@ -43,6 +46,53 @@ FIELD_CHOICES = ( ) +class SchoolTerm(ExtensibleModel): + """School term model. + + This is used to manage start and end times of a school term and link data to it. + """ + + objects = CurrentSiteManagerWithoutMigrations.from_queryset(SchoolTermQuerySet)() + + name = models.CharField(verbose_name=_("Name"), max_length=255, unique=True) + + date_start = models.DateField(verbose_name=_("Start date")) + date_end = models.DateField(verbose_name=_("End date")) + + @classmethod + def get_current(cls, day: Optional[date] = None): + if not day: + day = timezone.now().date() + try: + return cls.objects.on_day(day).first() + except SchoolTerm.DoesNotExist: + return None + + @classproperty + def current(cls): + return cls.get_current() + + def clean(self): + """Ensure there is only one school term at each point of time.""" + if self.date_end < self.date_start: + raise ValidationError(_("The start date must be earlier than the end date.")) + + qs = SchoolTerm.objects.within_dates(self.date_start, self.date_end) + if self.pk: + qs.exclude(pk=self.pk) + if qs.exists(): + raise ValidationError( + _("There is already a school term for this time or a part of this time.") + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _("School term") + verbose_name_plural = _("School terms") + + class Person(ExtensibleModel): """Person model. @@ -255,7 +305,7 @@ class AdditionalField(ExtensibleModel): verbose_name_plural = _("Addtitional fields for groups") -class Group(ExtensibleModel): +class Group(SchoolTermRelatedExtensibleModel): """Group model. Any kind of group of persons in a school, including, but not limited @@ -267,12 +317,18 @@ class Group(ExtensibleModel): verbose_name = _("Group") verbose_name_plural = _("Groups") permissions = (("assign_child_groups_to_groups", _("Can assign child groups to groups")),) + constraints = [ + models.UniqueConstraint(fields=["school_term", "name"], name="unique_school_term_name"), + models.UniqueConstraint( + fields=["school_term", "short_name"], name="unique_school_term_short_name" + ), + ] icon_ = "group" - name = models.CharField(verbose_name=_("Long name"), max_length=255, unique=True) + name = models.CharField(verbose_name=_("Long name"), max_length=255) short_name = models.CharField( - verbose_name=_("Short name"), max_length=255, unique=True, blank=True, null=True # noqa + verbose_name=_("Short name"), max_length=255, blank=True, null=True # noqa ) members = models.ManyToManyField( @@ -315,7 +371,10 @@ class Group(ExtensibleModel): return list(self.members.all()) + list(self.owners.all()) def __str__(self) -> str: - return f"{self.name} ({self.short_name})" + if self.school_term: + return f"{self.name} ({self.short_name}) ({self.school_term})" + else: + return f"{self.name} ({self.short_name})" def save(self, *args, **kwargs): super().save(*args, **kwargs) diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py index 4f97eafddf3cfdb9725cf0e9e0cde05ef83d1ad0..e854c3077dd60e2cf7bd22e02dcd0240689876b3 100644 --- a/aleksis/core/rules.py +++ b/aleksis/core/rules.py @@ -98,14 +98,6 @@ add_perm("core.assign_child_groups_to_groups", assign_child_groups_to_groups_pre edit_school_information_predicate = has_person & has_global_perm("core.change_school") add_perm("core.edit_school_information", edit_school_information_predicate) -# Edit school term -edit_schoolterm_predicate = has_person & has_global_perm("core.change_schoolterm") -add_perm("core.edit_schoolterm", edit_schoolterm_predicate) - -# Manage school -manage_school_predicate = edit_school_information_predicate | edit_schoolterm_predicate -add_perm("core.manage_school", manage_school_predicate) - # Manage data manage_data_predicate = has_person & has_global_perm("core.manage_data") add_perm("core.manage_data", manage_data_predicate) @@ -154,16 +146,6 @@ add_perm( ), ) -# View admin menu -view_admin_menu_predicate = has_person & ( - manage_data_predicate - | manage_school_predicate - | impersonate_predicate - | view_system_status_predicate - | view_announcements_predicate -) -add_perm("core.view_admin_menu", view_admin_menu_predicate) - # View person personal details view_personal_details_predicate = has_person & ( has_global_perm("core.view_personal_details") @@ -246,3 +228,23 @@ view_group_type_predicate = has_person & ( has_global_perm("core.view_grouptype") | has_any_object("core.view_grouptype", GroupType) ) add_perm("core.view_grouptype", view_group_type_predicate) + +# School years +view_school_term_predicate = has_person & has_global_perm("core.view_schoolterm") +add_perm("core.view_schoolterm", view_school_term_predicate) + +create_school_term_predicate = has_person & has_global_perm("core.add_schoolterm") +add_perm("core.create_schoolterm", create_school_term_predicate) + +edit_school_term_predicate = has_person & has_global_perm("core.change_schoolterm") +add_perm("core.edit_schoolterm", edit_school_term_predicate) + +# View admin menu +view_admin_menu_predicate = has_person & ( + manage_data_predicate + | view_school_term_predicate + | impersonate_predicate + | view_system_status_predicate + | view_announcements_predicate +) +add_perm("core.view_admin_menu", view_admin_menu_predicate) diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py index 6f7ba041457f11420cf9155ff0cc8ca5012fd9d1..f562b61a151487c5047ef2861255a00ef7a5f785 100644 --- a/aleksis/core/tables.py +++ b/aleksis/core/tables.py @@ -4,6 +4,24 @@ import django_tables2 as tables from django_tables2.utils import A +class SchoolTermTable(tables.Table): + """Table to list persons.""" + + class Meta: + attrs = {"class": "responsive-table highlight"} + + name = tables.LinkColumn("edit_school_term", args=[A("id")]) + date_start = tables.Column() + date_end = tables.Column() + edit = tables.LinkColumn( + "edit_school_term", + args=[A("id")], + text=_("Edit"), + attrs={"a": {"class": "btn-flat waves-effect waves-orange orange-text"}}, + verbose_name=_("Actions"), + ) + + class PersonsTable(tables.Table): """Table to list persons.""" @@ -22,6 +40,7 @@ class GroupsTable(tables.Table): name = tables.LinkColumn("group_by_id", args=[A("id")]) short_name = tables.LinkColumn("group_by_id", args=[A("id")]) + school_term = tables.Column() class AdditionalFieldsTable(tables.Table): diff --git a/aleksis/core/templates/core/school_term/create.html b/aleksis/core/templates/core/school_term/create.html new file mode 100644 index 0000000000000000000000000000000000000000..06fea51ec6d3196d881e97a61c2a9f2579c8a3c7 --- /dev/null +++ b/aleksis/core/templates/core/school_term/create.html @@ -0,0 +1,17 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block browser_title %}{% blocktrans %}Create school term{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Create school term{% endblocktrans %}{% endblock %} + +{% block content %} + + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/save_button.html" %} + </form> + +{% endblock %} diff --git a/aleksis/core/templates/core/school_term/edit.html b/aleksis/core/templates/core/school_term/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..ea0d0d379a9be4f9a341ced494cc89a8d7382b4a --- /dev/null +++ b/aleksis/core/templates/core/school_term/edit.html @@ -0,0 +1,17 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block browser_title %}{% blocktrans %}Edit school term{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Edit school term{% endblocktrans %}{% endblock %} + +{% block content %} + + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/save_button.html" %} + </form> + +{% endblock %} diff --git a/aleksis/core/templates/core/school_term/list.html b/aleksis/core/templates/core/school_term/list.html new file mode 100644 index 0000000000000000000000000000000000000000..14aa2f0a697a3efd54b27154b431ef3f424923de --- /dev/null +++ b/aleksis/core/templates/core/school_term/list.html @@ -0,0 +1,18 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} + +{% load i18n %} +{% load render_table from django_tables2 %} + +{% block browser_title %}{% blocktrans %}School terms{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}School terms{% endblocktrans %}{% endblock %} + +{% block content %} + <a class="btn green waves-effect waves-light" href="{% url 'create_school_term' %}"> + <i class="material-icons left">add</i> + {% trans "Create school term" %} + </a> + + {% render_table table %} +{% endblock %} diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 766275b5deb78a937b10c93786e29480c0bd2f4b..32082e2703c6f757c2c9ce38637c0113dfb147c7 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -22,6 +22,9 @@ urlpatterns = [ path("status/", views.system_status, name="system_status"), path("", include(tf_urls)), path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"), + path("school_terms/", views.SchoolTermListView.as_view(), name="school_terms"), + path("school_terms/create/", views.SchoolTermCreateView.as_view(), name="create_school_term"), + path("school_terms/<int:pk>/", views.SchoolTermEditView.as_view(), name="edit_school_term"), path("persons", views.persons, name="persons"), path("persons/accounts", views.persons_accounts, name="persons_accounts"), path("person", views.person, name="person"), diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 92f0bf2fc5d5fd863c9427f4acac9a65081c5b5f..4f166c18bca4eafe50bb9af273e013e2e14c1f44 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -2,20 +2,20 @@ from typing import Optional from django.apps import apps from django.conf import settings -from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator from django.http import HttpRequest, HttpResponse, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from django_tables2 import RequestConfig +from django_tables2 import RequestConfig, SingleTableView from dynamic_preferences.forms import preference_form_builder from guardian.shortcuts import get_objects_for_user from haystack.inputs import AutoQuery from haystack.query import SearchQuerySet from haystack.views import SearchView -from rules.contrib.views import permission_required +from rules.contrib.views import PermissionRequiredMixin, permission_required from .filters import GroupFilter from .forms import ( @@ -28,8 +28,10 @@ from .forms import ( GroupPreferenceForm, PersonPreferenceForm, PersonsAccountsFormSet, + SchoolTermForm, SitePreferenceForm, ) +from .mixins import AdvancedCreateView, AdvancedEditView from .models import ( AdditionalField, Announcement, @@ -38,13 +40,20 @@ from .models import ( GroupType, Notification, Person, + SchoolTerm, ) from .registries import ( group_preferences_registry, person_preferences_registry, site_preferences_registry, ) -from .tables import AdditionalFieldsTable, GroupsTable, GroupTypesTable, PersonsTable +from .tables import ( + AdditionalFieldsTable, + GroupsTable, + GroupTypesTable, + PersonsTable, + SchoolTermTable, +) from .util import messages from .util.apps import AppConfig from .util.core_helpers import objectgetter_optional @@ -91,6 +100,37 @@ def about(request: HttpRequest) -> HttpResponse: return render(request, "core/pages/about.html", context) +class SchoolTermListView(SingleTableView, PermissionRequiredMixin): + """Table of all school terms.""" + + model = SchoolTerm + table_class = SchoolTermTable + permission_required = "core.view_schoolterm" + template_name = "core/school_term/list.html" + + +class SchoolTermCreateView(AdvancedCreateView, PermissionRequiredMixin): + """Create view for school terms.""" + + model = SchoolTerm + form_class = SchoolTermForm + permission_required = "core.add_schoolterm" + template_name = "core/school_term/create.html" + success_url = reverse_lazy("school_terms") + success_message = _("The school term has been created.") + + +class SchoolTermEditView(AdvancedEditView, PermissionRequiredMixin): + """Edit view for school terms.""" + + model = SchoolTerm + form_class = SchoolTermForm + permission_required = "core.edit_schoolterm" + template_name = "core/school_term/edit.html" + success_url = reverse_lazy("school_terms") + success_message = _("The school term has been saved.") + + @permission_required("core.view_persons") def persons(request: HttpRequest) -> HttpResponse: """List view listing all persons."""