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

Add group role assignments overview and managing views

Includes all related stuff like permissions, urls, forms and preferences
parent 13d91129
No related branches found
No related tags found
1 merge request!131Resolve "Add support for assinging group roles"
Pipeline #5612 passed
Showing
with 546 additions and 17 deletions
......@@ -6,20 +6,21 @@ 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 django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, 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.core_helpers import get_site_preferences
from aleksis.core.util.predicates import check_global_permission
from .models import (
GroupRole,
GroupRoleAssignment,
ExcuseType,
ExtraMark,
GroupRole,
GroupRoleAssignment,
LessonDocumentation,
PersonalNote,
)
......@@ -177,3 +178,76 @@ class GroupRoleForm(forms.ModelForm):
class Meta:
model = GroupRole
fields = ["name", "icon", "colour"]
class AssignGroupRoleForm(forms.ModelForm):
layout_base = ["groups", "person", "role", Row("date_start", "date_end")]
groups = forms.ModelMultipleChoiceField(
label=_("Group"),
required=True,
queryset=Group.objects.all(),
widget=ModelSelect2MultipleWidget(
model=Group,
search_fields=["name__icontains", "short_name__icontains"],
attrs={"data-minimum-input-length": 0, "class": "browser-default",},
),
)
person = forms.ModelChoiceField(
label=_("Person"),
required=True,
queryset=Person.objects.all(),
widget=ModelSelect2Widget(
model=Person,
dependent_fields={"groups": "member_of"},
search_fields=[
"first_name__icontains",
"last_name__icontains",
"short_name__icontains",
],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
)
def __init__(self, request, *args, **kwargs):
self.request = request
initial = kwargs.get("initial", {})
# Build layout with or without groups field
base_layout = self.layout_base[:]
if "groups" in initial:
base_layout.remove("groups")
self.layout = Layout(*base_layout)
super().__init__(*args, **kwargs)
if "groups" in initial:
self.fields["groups"].required = False
# Filter persons by permissions
if not self.request.user.has_perm("alsijil.assign_grouprole"): # Global permission
persons = Person.objects
if initial.get("groups"):
persons = persons.filter(member_of__in=initial["groups"])
if get_site_preferences()["alsijil__group_owners_can_assign_roles_to_parents"]:
persons = persons.filter(
Q(member_of__owners=self.request.user.person)
| Q(children__member_of__owners=self.request.user.person)
)
else:
persons = persons.filter(member_of__owners=self.request.user.person)
self.fields["person"].queryset = persons
def clean_groups(self):
"""Ensure that only permitted groups are used."""
return self.initial["groups"] if "groups" in self.initial else self.cleaned_data["groups"]
class Meta:
model = GroupRoleAssignment
fields = ["groups", "person", "role", "date_start", "date_end"]
class GroupRoleAssignmentEditForm(forms.ModelForm):
class Meta:
model = GroupRoleAssignment
fields = ["date_start", "date_end"]
......@@ -13,6 +13,7 @@ from aleksis.apps.alsijil.data_checks import (
PersonalNoteOnHolidaysDataCheck,
)
from aleksis.apps.alsijil.managers import PersonalNoteManager
from aleksis.apps.chronos.managers import GroupPropertiesMixin
from aleksis.apps.chronos.mixins import WeekRelatedMixin
from aleksis.apps.chronos.models import LessonPeriod
from aleksis.apps.chronos.util.date import get_current_year
......@@ -246,7 +247,7 @@ class GroupRole(ExtensibleModel):
verbose_name_plural = _("Group roles")
class GroupRoleAssignment(ExtensibleModel):
class GroupRoleAssignment(GroupPropertiesMixin, ExtensibleModel):
role = models.ForeignKey(
GroupRole,
on_delete=models.CASCADE,
......@@ -260,9 +261,7 @@ class GroupRoleAssignment(ExtensibleModel):
verbose_name=_("Assigned person"),
)
groups = models.ManyToManyField(
"core.Group",
related_name="group_roles",
verbose_name=_("Groups"),
"core.Group", related_name="group_roles", verbose_name=_("Groups"),
)
date_start = models.DateField(verbose_name=_("Start date"))
date_end = models.DateField(
......
......@@ -74,3 +74,13 @@ class ActivateGroupRoles(BooleanPreference):
name = "activate_group_roles"
default = True
verbose_name = _("Activate support for creating and assigning group roles")
@site_preferences_registry.register
class GroupOwnersCanAssignRolesToParents(BooleanPreference):
section = alsijil
name = "group_owners_can_assign_roles_to_parents"
default = False
verbose_name = _(
"Allow group owners to assign group roles to the parents of the group's members"
)
......@@ -14,6 +14,7 @@ from .util.predicates import (
has_personal_note_group_perm,
is_group_member,
is_group_owner,
is_group_role_assignment_group_owner,
is_lesson_parent_group_owner,
is_lesson_participant,
is_lesson_teacher,
......@@ -219,12 +220,13 @@ add_perm("alsijil.edit_extramark", edit_extramark_predicate)
delete_extramark_predicate = view_extramarks_predicate & has_global_perm("alsijil.delete_extramark")
add_perm("alsijil.delete_extramark", delete_extramark_predicate)
group_roles_activated_predicate = has_person & is_site_preference_set(
"alsijil", "activate_group_roles"
)
# View group role list
view_group_roles_predicate = (
has_person
& is_site_preference_set("alsijil", "activate_group_roles")
& has_global_perm("alsijil.view_grouprole")
view_group_roles_predicate = group_roles_activated_predicate & has_global_perm(
"alsijil.view_grouprole"
)
add_perm("alsijil.view_grouproles", view_group_roles_predicate)
......@@ -241,3 +243,31 @@ delete_group_role_predicate = view_group_roles_predicate & has_global_perm(
"alsijil.delete_grouprole"
)
add_perm("alsijil.delete_grouprole", delete_group_role_predicate)
view_assigned_group_roles_predicate = group_roles_activated_predicate & (
is_group_owner
| has_global_perm("alsjil.assign_grouprole")
| has_object_perm("alsijil.assign_grouprole")
)
add_perm("alsijil.view_assigned_grouproles", view_assigned_group_roles_predicate)
assign_group_role_person_predicate = group_roles_activated_predicate & (
is_person_group_owner | has_global_perm("alsjil.assign_grouprole")
)
add_perm("alsijil.assign_grouprole_to_person", assign_group_role_person_predicate)
assign_group_role_group_predicate = view_assigned_group_roles_predicate
add_perm("alsijil.assign_grouprole_for_group", assign_group_role_group_predicate)
edit_group_role_assignment_predicate = group_roles_activated_predicate & (
has_global_perm("alsjil.assign_grouprole") | is_group_role_assignment_group_owner
)
add_perm("alsijil.edit_grouproleassignment", edit_group_role_assignment_predicate)
stop_group_role_assignment_predicate = edit_group_role_assignment_predicate
add_perm("alsijil.stop_grouproleassignment", stop_group_role_assignment_predicate)
delete_group_role_assignment_predicate = group_roles_activated_predicate & (
has_global_perm("alsjil.assign_grouprole") | is_group_role_assignment_group_owner
)
add_perm("alsijil.delete_grouproleassignment", delete_group_role_assignment_predicate)
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load i18n static %}
{% load i18n static rules %}
{% block browser_title %}{% blocktrans %}My groups{% endblocktrans %}{% endblock %}
......@@ -38,6 +38,13 @@
<i class="material-icons left">view_week</i>
{% trans "Week view" %}
</a>
{% has_perm "alsijil.view_assigned_grouproles" user group as can_view_assigned_group_roles %}
{% if can_view_assigned_group_roles %}
<a class="btn primary waves-effect waves-light" href="{% url 'assigned_group_roles' group.pk %}">
<i class="material-icons left">assignment_ind</i>
{% trans "Roles" %}
</a>
{% endif %}
<a class="btn primary waves-effect waves-light" href="{% url "full_register_group" group.pk %}"
target="_blank">
<i class="material-icons left">print</i>
......@@ -75,6 +82,15 @@
{% trans "Week view" %}
</a>
</p>
{% has_perm "alsijil.view_assigned_grouproles" user group as can_view_assigned_group_roles %}
{% if can_view_assigned_group_roles %}
<p>
<a class="btn primary waves-effect waves-light" href="{% url 'assigned_group_roles' group.pk %}">
<i class="material-icons left">assignment_ind</i>
{% trans "Roles" %}
</a>
</p>
{% endif %}
<p>
<a class="btn primary waves-effect waves-light" href="{% url "full_register_group" group.pk %}"
target="_blank">
......
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load i18n rules any_js material_form %}
{% block browser_title %}
{% blocktrans with group=group.name %}Assign group role for {{ group }}{% endblocktrans %}
{% endblock %}
{% block page_title %}
{% blocktrans with group=group.name %}Assign group role for {{ group }}{% endblocktrans %}
{% endblock %}
{% block extra_head %}
{{ form.media.css }}
{% include_css "select2-materialize" %}
{% endblock %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
{% form form=form %}{% endform %}
<button type="submit" class="btn green waves-effect waves-light">
<i class="material-icons left">assignment_ind</i>
{% trans "Assign" %}
</button>
</form>
{% include_js "select2-materialize" %}
{{ form.media.js }}
{% endblock %}
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load i18n rules any_js material_form static %}
{% load render_table from django_tables2 %}
{% block browser_title %}
{% blocktrans with group=object.name %}Group roles for {{ group }}{% endblocktrans %}
{% endblock %}
{% block page_title %}
{% blocktrans with group=object.name %}Group roles for {{ group }}{% endblocktrans %}
{% endblock %}
{% block extra_head %}
{{ block.super }}
<link rel="stylesheet" href="{% static "css/alsijil/alsijil.css" %}"/>
{% endblock %}
{% block content %}
{% url "assigned_group_roles" object.pk as back_url %}
<p>
{% has_perm "alsijil.view_my_groups" user as can_view_group_overview %}
{% if can_view_group_overview %}
<a class="btn waves-effect waves-light" href="{% url "my_groups" %}">
<i class="material-icons left">arrow_back</i>
{% trans "Back to my groups" %}
</a>
{% endif %}
{% has_perm "alsijil.assign_grouprole_for_group" user object as can_assign_group_role %}
{% if can_assign_group_role %}
<a class="btn green waves-effect waves-light" href="{% url "assign_group_role" object.pk %}">
<i class="material-icons left">assignment_ind</i>
{% trans "Assign a role to a person" %}
</a>
{% endif %}
</p>
<div class="row">
<div class="col s12">
<ul class="tabs">
<li class="tab">
<a class="active" href="#current">{% trans "Current roles" %} ({{ today|date:"SHORT_DATE_FORMAT" }})</a>
</li>
<li class="tab">
<a href="#all">{% trans "All assignments" %}</a>
</li>
</ul>
</div>
<div id="current" class="col s12">
<div class="collection">
{% for role in roles %}
<div class="collection-item">
<div class="row no-margin">
<div class="col s12 m5 l4 xl3 no-padding">
{% if can_assign_group_role %}
<a class="btn waves-effect waves-light right hide-on-med-and-up"
href="{% url "assign_group_role" object.pk role.pk %}">
<i class="material-icons center">add</i>
</a>
{% endif %}
<div class="btn-margin">
{% include "alsijil/group_role/chip.html" with role=role %}
</div>
</div>
<div class="col s12 m7 l8 xl9 no-padding">
{% if can_assign_group_role %}
<a class="btn waves-effect waves-light right hide-on-small-only"
href="{% url "assign_group_role" object.pk role.pk %}">
<i class="material-icons center">add</i>
</a>
{% endif %}
{% for assignment in role.assignments.all %}
<a class="chip dropdown-trigger" href="#"
data-target="dropdown-{{ assignment.pk }}" title="{{ assignment }}">{{ assignment.person }}
{% if object not in assignment.groups.all %}
<small>({{ assignment.group_names }})</small>
{% endif %}
</a>
{% include "alsijil/group_role/assignment_options.html" with assignment=assignment back_url=back_url %}
{% empty %}
<div class="grey-text darken-3">{% trans "No one assigned." %}</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="alert primary">
<div>
<i class="material-icons left">info</i>
{% blocktrans %}
You can get some additional actions for each group role assignment if you click on the name of the
corresponding person.
{% endblocktrans %}
</div>
</div>
</div>
<div class="col s12 " id="all">
<table class="responsive-table">
<thead>
<tr>
<th class="chip-height">{% trans "Group role" %}</th>
<th>{% trans "Person" %}</th>
<th>{% trans "Start date" %}</th>
<th>{% trans "End date" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
{% for assignment in assignments %}
<tr>
<td>
{% include "alsijil/group_role/chip.html" with role=assignment.role %}
</td>
<td>
{{ assignment.person }}
</td>
<td>{{ assignment.date_start }}</td>
<td>{{ assignment.date_end|default:"–" }}</td>
<td>
<a class="btn waves-effect waves-light dropdown-trigger" href="#"
data-target="dropdown-{{ assignment.pk }}-d2">
<i class="material-icons left">list</i>
{% trans "Actions" %}
</a>
{% include "alsijil/group_role/assignment_options.html" with assignment=assignment back_url=back_url suffix="-d2" %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock %}
{# -*- engine:django -*- #}
{% load i18n rules %}
{% has_perm "alsijil.edit_grouproleassignment" user assignment as can_edit %}
{% has_perm "alsijil.stop_grouproleassignment" user assignment as can_stop %}
{% has_perm "alsijil.delete_grouproleassignment" user assignment as can_delete %}
<ul id="dropdown-{{ assignment.pk }}{{ suffix }}" class="dropdown-content">
{% if can_edit %}
<li>
<a href="{% url "edit_group_role_assignment" assignment.pk %}?next={{ back_url }}">
<i class="material-icons left">edit</i> {% trans "Edit" %}
</a>
</li>
{% endif %}
{% if not assignment.date_end and can_stop %}
<li>
<a href="#">
<i class="material-icons left">stop</i> {% trans "Stop" %}
</a>
</li>
{% endif %}
{% if can_delete %}
<li>
<a href="{% url "delete_group_role_assignment" assignment.pk %}?next={{ back_url }}" class="red-text">
<i class="material-icons left">delete</i> {% trans "Delete" %}
</a>
</li>
{% endif %}
</ul>
\ No newline at end of file
{# -*- engine:django -*- #}
<div class="chip white-text {{ role.colour|default:"black" }}">
<i class="material-icons left">{{ role.icon|default:"assignment_ind" }}</i>
{{ role.name }}
</div>
\ No newline at end of file
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load i18n rules material_form %}
{% block browser_title %}{% blocktrans %}Edit group role assignment{% endblocktrans %}{% endblock %}
{% block page_title %}{% blocktrans %}Edit group role assignment{% endblocktrans %}{% endblock %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
{% form form=form %}{% endform %}
{% include "core/partials/save_button.html" %}
</form>
{% endblock %}
......@@ -57,4 +57,34 @@ urlpatterns = [
views.GroupRoleDeleteView.as_view(),
name="delete_group_role",
),
path(
"groups/<int:pk>/group_roles/",
views.AssignedGroupRolesView.as_view(),
name="assigned_group_roles",
),
path(
"groups/<int:pk>/group_roles/assign/",
views.AssignGroupRoleView.as_view(),
name="assign_group_role",
),
path(
"groups/<int:pk>/group_roles/<int:role_pk>/assign/",
views.AssignGroupRoleView.as_view(),
name="assign_group_role",
),
path(
"group_roles/assignments/<int:pk>/edit/",
views.GroupRoleAssignmentEditView.as_view(),
name="edit_group_role_assignment",
),
path(
"group_roles/assignments/<int:pk>/stop/",
views.GroupRoleAssignmentStopView.as_view(),
name="stop_group_role_assignment",
),
path(
"group_roles/assignments/<int:pk>/delete/",
views.GroupRoleAssignmentDeleteView.as_view(),
name="delete_group_role_assignment",
),
]
......@@ -246,3 +246,18 @@ def is_personal_note_lesson_parent_group_owner(user: User, obj: PersonalNote) ->
def is_teacher(user: User, obj: Person) -> bool:
"""Predicate which checks if the provided object is a teacher."""
return user.person.is_teacher
@predicate
def is_group_role_assignment_group_owner(user: User, obj: Union[Group, Person]) -> bool:
"""Predicate for group owners of a group role assignment.
Checks whether the person linked to the user is the owner of the groups
linked to the given group role assignment.
If there isn't provided a group role assignment, it will return `False`.
"""
if obj:
for group in obj.groups.all():
if user.person in list(group.owners.all()):
return True
return False
......@@ -6,6 +6,7 @@ from django.db.models import Count, Exists, OuterRef, Prefetch, Q, Subquery, Sum
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect, render
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.decorators.cache import never_cache
......@@ -21,30 +22,36 @@ from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import Holiday, LessonPeriod, TimePeriod
from aleksis.apps.chronos.util.build import build_weekdays
from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date
from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
from aleksis.core.mixins import (
AdvancedCreateView,
AdvancedDeleteView,
AdvancedEditView,
SuccessNextMixin,
)
from aleksis.core.models import Group, Person, SchoolTerm
from aleksis.core.util import messages
from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional
from .forms import (
AssignGroupRoleForm,
GroupRoleForm,
ExcuseTypeForm,
ExtraMarkForm,
GroupRoleAssignmentEditForm,
GroupRoleForm,
LessonDocumentationForm,
PersonalNoteFormSet,
RegisterAbsenceForm,
SelectForm,
)
from .models import (
GroupRole,
GroupRoleAssignment,
ExcuseType,
ExtraMark,
GroupRole,
GroupRoleAssignment,
LessonDocumentation,
PersonalNote,
)
from .tables import GroupRoleTable, ExcuseTypeTable, ExtraMarkTable
from .tables import ExcuseTypeTable, ExtraMarkTable, GroupRoleTable
from .util.alsijil_helpers import get_lesson_period_by_pk, get_timetable_instance_by_pk
......@@ -902,3 +909,117 @@ class GroupRoleDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelete
template_name = "core/pages/delete.html"
success_url = reverse_lazy("group_roles")
success_message = _("The group role has been deleted.")
class AssignedGroupRolesView(PermissionRequiredMixin, DetailView):
permission_required = "alsijil.view_assigned_grouproles"
model = Group
template_name = "alsijil/group_role/assigned_list.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data()
today = timezone.now().date()
context["today"] = today
self.roles = GroupRole.objects.prefetch_related(
Prefetch(
"assignments",
queryset=GroupRoleAssignment.objects.filter(
Q(date_start__lte=today) & (Q(date_end__gte=today) | Q(date_end__isnull=True))
)
.filter(Q(groups=self.object) | Q(groups__child_groups=self.object))
.distinct(),
)
)
context["roles"] = self.roles
assignments = (
GroupRoleAssignment.objects.filter(
Q(groups=self.object) | Q(groups__child_groups=self.object)
)
.distinct()
.order_by("-date_start")
)
context["assignments"] = assignments
return context
@method_decorator(never_cache, name="dispatch")
class AssignGroupRoleView(PermissionRequiredMixin, AdvancedCreateView):
model = GroupRoleAssignment
form_class = AssignGroupRoleForm
permission_required = "alsijil.assign_grouprole_for_group"
template_name = "alsijil/group_role/assign.html"
success_message = _("The group role has been assigned.")
def get_success_url(self) -> str:
return reverse("assigned_group_roles", args=[self.group.pk])
def get_permission_object(self):
self.group = get_object_or_404(Group, pk=self.kwargs.get("pk"))
try:
self.role = GroupRole.objects.get(pk=self.kwargs.get("role_pk"))
except GroupRole.DoesNotExist:
self.role = None
return self.group
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["request"] = self.request
kwargs["initial"] = {"role": self.role, "groups": [self.group]}
return kwargs
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context["role"] = self.role
context["group"] = self.group
return context
@method_decorator(never_cache, name="dispatch")
class GroupRoleAssignmentEditView(PermissionRequiredMixin, SuccessNextMixin, AdvancedEditView):
"""Edit view for group role assignments."""
model = GroupRoleAssignment
form_class = GroupRoleAssignmentEditForm
permission_required = "alsijil.edit_grouproleassignment"
template_name = "alsijil/group_role/edit_assignment.html"
success_message = _("The group role assignment has been saved.")
def get_default_success_url(self) -> str:
pk = self.object.groups.first().pk
return reverse("assigned_group_roles", args=[pk])
@method_decorator(never_cache, "dispatch")
class GroupRoleAssignmentStopView(PermissionRequiredMixin, SuccessNextMixin, DetailView):
model = GroupRoleAssignment
permission_required = "alsijil.stop_grouproleassignment"
def get_default_success_url(self) -> str:
pk = self.object.groups.first().pk
return reverse("assigned_group_roles", args=[pk])
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.date_end:
self.object.date_end = timezone.now().date()
self.object.save()
messages.success(request, _("The group role assignment has been stopped."))
return redirect(self.get_success_url())
@method_decorator(never_cache, "dispatch")
class GroupRoleAssignmentDeleteView(
PermissionRequiredMixin, RevisionMixin, SuccessNextMixin, AdvancedDeleteView
):
"""Delete view for group role assignments."""
model = GroupRoleAssignment
permission_required = "alsijil.delete_grouproleassignment"
template_name = "core/pages/delete.html"
success_message = _("The group role assignment has been deleted.")
def get_default_success_url(self) -> str:
pk = self.object.groups.first().pk
return reverse("assigned_group_roles", args=[pk])
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