Repository: ephios-dev/ephios

Issue:
Allow users to be marked as staff user
add a checkbox to UserProfileForm that sets the `is_staff` flag on the user. Only show this to othter staff-users.
Being a staff user is currently required for some setting-views


Discussion:

Resolving patch:
diff --git a/ephios/core/forms/users.py b/ephios/core/forms/users.py
--- a/ephios/core/forms/users.py
+++ b/ephios/core/forms/users.py
@@ -3,6 +3,7 @@
 from crispy_forms.layout import Field, Fieldset, Layout, Submit
 from django import forms
 from django.contrib.auth.models import Group
+from django.core.exceptions import ValidationError
 from django.db.models import Q
 from django.forms import (
     CheckboxSelectMultiple,
@@ -223,6 +224,11 @@ def __init__(self, *args, **kwargs):
                 self.fields["groups"].help_text = _(
                     "The user is also member of <b>{groups}</b>, but you are not allowed to manage memberships for those groups."
                 ).format(groups=", ".join((group.name for group in self.locked_groups)))
+        if not request.user.is_staff:
+            self.fields["is_staff"].disabled = True
+            self.fields["is_staff"].help_text += " " + _(
+                "Only other technical administrators can change this."
+            )
 
     field_order = [
         "email",
@@ -232,17 +238,37 @@ def __init__(self, *args, **kwargs):
         "phone",
         "groups",
         "is_active",
+        "is_staff",
     ]
 
     class Meta:
         model = UserProfile
-        fields = ["email", "first_name", "last_name", "date_of_birth", "phone", "is_active"]
+        fields = [
+            "email",
+            "first_name",
+            "last_name",
+            "date_of_birth",
+            "phone",
+            "is_active",
+            "is_staff",
+        ]
         widgets = {"date_of_birth": CustomDateInput}
         help_texts = {
             "is_active": _("Inactive users cannot log in and do not get any notifications.")
         }
         labels = {"is_active": _("Active user")}
 
+    def clean_is_staff(self):
+        if self.initial.get("is_staff", False) and not self.cleaned_data["is_staff"]:
+            other_staff = UserProfile.objects.filter(is_staff=True).exclude(pk=self.instance.pk)
+            if not other_staff.exists():
+                raise ValidationError(
+                    _(
+                        "At least one user must be technical administrator. Please promote another user before demoting this one."
+                    )
+                )
+        return self.cleaned_data["is_staff"]
+
     def save(self, commit=True):
         userprofile = super().save(commit)
         userprofile.groups.set(
diff --git a/ephios/core/models/users.py b/ephios/core/models/users.py
--- a/ephios/core/models/users.py
+++ b/ephios/core/models/users.py
@@ -97,7 +97,11 @@ class UserProfile(guardian.mixins.GuardianUserMixin, PermissionsMixin, AbstractB
     email = EmailField(_("email address"), unique=True)
     is_active = BooleanField(default=True, verbose_name=_("Active"))
     is_visible = BooleanField(default=True, verbose_name=_("Visible"))
-    is_staff = BooleanField(default=False, verbose_name=_("Staff user"))
+    is_staff = BooleanField(
+        default=False,
+        verbose_name=_("Technical Administrator"),
+        help_text=_("If checked, this user can change technical ephios settings."),
+    )
     first_name = CharField(_("first name"), max_length=254)
     last_name = CharField(_("last name"), max_length=254)
     date_of_birth = DateField(_("date of birth"), null=True, blank=False)


Test patch:
diff --git a/tests/core/test_views_userprofile.py b/tests/core/test_views_userprofile.py
--- a/tests/core/test_views_userprofile.py
+++ b/tests/core/test_views_userprofile.py
@@ -141,7 +141,9 @@ def test_userprofile_password_reset(self, django_app, groups, volunteer, manager
         response.form.submit()
         assert response.status_code == 200
 
-    def test_userprofile_edit_by_hr_allowed(self, django_app, volunteer, hr_group, groups):
+    def test_userprofile_group_membership_edit_by_hr_allowed(
+        self, django_app, volunteer, hr_group, groups
+    ):
         managers, planners, volunteers = groups
         form = django_app.get(
             reverse("core:userprofile_edit", kwargs={"pk": volunteer.id}), user=volunteer
@@ -151,7 +153,9 @@ def test_userprofile_edit_by_hr_allowed(self, django_app, volunteer, hr_group, g
         assert response.status_code == 302
         assert set(volunteer.groups.all()) == {volunteers}
 
-    def test_userprofile_edit_by_hr_forbidden(self, django_app, volunteer, hr_group, groups):
+    def test_userprofile_group_membership_edit_by_hr_forbidden(
+        self, django_app, volunteer, hr_group, groups
+    ):
         managers, planners, volunteers = groups
         assert set(volunteer.groups.all()) == {hr_group, volunteers}
         form = django_app.get(
@@ -161,3 +165,22 @@ def test_userprofile_edit_by_hr_forbidden(self, django_app, volunteer, hr_group,
         response = form.submit()
         assert response.status_code == 200
         assert set(volunteer.groups.all()) == {hr_group, volunteers}
+
+    def test_is_staff_flag_cannot_be_changed_by_non_staff_user(
+        self, django_app, volunteer, manager, groups
+    ):
+        form = django_app.get(
+            reverse("core:userprofile_edit", kwargs={"pk": volunteer.id}), user=manager
+        ).form
+        form["is_staff"] = True
+        response = form.submit()
+        assert not manager.is_staff
+        assert not UserProfile.objects.get(id=volunteer.id).is_staff
+
+    def test_staffuser_can_change_is_staff_flag(self, django_app, volunteer, superuser, groups):
+        form = django_app.get(
+            reverse("core:userprofile_edit", kwargs={"pk": volunteer.id}), user=superuser
+        ).form
+        form["is_staff"] = True
+        form.submit()
+        assert UserProfile.objects.get(id=volunteer.id).is_staff