feat: add granular permissions for editing and payments
This commit is contained in:
@@ -117,6 +117,16 @@ class UserManagementForm(forms.Form):
|
||||
is_staff = forms.BooleanField(required=False, initial=True, label=_("Administratör (staff)"))
|
||||
grant_view = forms.BooleanField(required=False, initial=True, label=_("Ge behörighet att se utlägg"))
|
||||
grant_change = forms.BooleanField(required=False, initial=True, label=_("Ge behörighet att besluta utlägg"))
|
||||
grant_edit = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label=_("Ge behörighet att redigera utläggsdetaljer"),
|
||||
)
|
||||
grant_pay = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label=_("Ge behörighet att markera betalningar"),
|
||||
)
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data["username"]
|
||||
@@ -148,6 +158,8 @@ class UserPermissionForm(forms.Form):
|
||||
is_staff = forms.BooleanField(required=False, label=_("Admin/staff"))
|
||||
grant_view = forms.BooleanField(required=False, label=_("Får se utlägg"))
|
||||
grant_change = forms.BooleanField(required=False, label=_("Får besluta utlägg"))
|
||||
grant_edit = forms.BooleanField(required=False, label=_("Får redigera utlägg"))
|
||||
grant_pay = forms.BooleanField(required=False, label=_("Får markera betalningar"))
|
||||
|
||||
|
||||
class DeleteUserForm(forms.Form):
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-11 19:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('claims', '0007_delete_systemsetting_alter_claim_receipt'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='claim',
|
||||
options={'ordering': ['-created_at'], 'permissions': [('mark_claim_paid', 'Can mark claims as paid'), ('edit_claim_details', 'Can edit claim details')]},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='claimlog',
|
||||
name='action',
|
||||
field=models.CharField(choices=[('created', 'Submitted'), ('status_changed', 'Status changed'), ('marked_paid', 'Marked as paid'), ('project_changed', 'Project changed'), ('details_edited', 'Details edited')], max_length=32),
|
||||
),
|
||||
]
|
||||
@@ -81,6 +81,10 @@ class Claim(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
permissions = [
|
||||
("mark_claim_paid", _("Can mark claims as paid")),
|
||||
("edit_claim_details", _("Can edit claim details")),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
project = f" [{self.project}]" if self.project else ""
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
<span class="text-xs text-gray-500">{% trans "Ej markerad som betald" %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if can_change and claim.status == 'pending' %}
|
||||
{% if can_edit_claim and claim.status == 'pending' %}
|
||||
<button type="button"
|
||||
data-open-edit="{{ claim.id }}"
|
||||
class="rounded-full border border-gray-300 px-3 py-1 text-xs font-semibold text-gray-700 transition hover:bg-gray-100">
|
||||
@@ -186,15 +186,21 @@
|
||||
</details>
|
||||
{% if payments_enabled and not claim.is_paid %}
|
||||
<div class="flex flex-col items-start gap-3 md:items-end">
|
||||
<form method="post" class="w-full max-w-xs" onsubmit="return confirm('{% trans "Är du säker på att du har lagt upp betalningen? Markera endast som betald om beloppet skickas till banken." %}');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action_type" value="payment">
|
||||
<input type="hidden" name="payment_claim_id" value="{{ claim.id }}">
|
||||
<button type="submit" class="flex w-full items-center justify-center gap-2 rounded-2xl bg-emerald-600 px-4 py-3 text-xs font-semibold uppercase tracking-wide text-white transition hover:bg-emerald-700">
|
||||
{% trans "Markera som betald" %}
|
||||
</button>
|
||||
</form>
|
||||
<p class="text-[11px] text-green-700">{% trans "Dubbelkolla belopp och kontonummer i panelen innan du bekräftar." %}</p>
|
||||
{% if can_mark_paid %}
|
||||
<form method="post" class="w-full max-w-xs" onsubmit="return confirm('{% trans "Är du säker på att du har lagt upp betalningen? Markera endast som betald om beloppet skickas till banken." %}');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action_type" value="payment">
|
||||
<input type="hidden" name="payment_claim_id" value="{{ claim.id }}">
|
||||
<button type="submit" class="flex w-full items-center justify-center gap-2 rounded-2xl bg-emerald-600 px-4 py-3 text-xs font-semibold uppercase tracking-wide text-white transition hover:bg-emerald-700">
|
||||
{% trans "Markera som betald" %}
|
||||
</button>
|
||||
</form>
|
||||
<p class="text-[11px] text-green-700">{% trans "Dubbelkolla belopp och kontonummer i panelen innan du bekräftar." %}</p>
|
||||
{% else %}
|
||||
<p class="rounded-2xl bg-white/80 px-4 py-3 text-xs text-green-800">
|
||||
{% trans "Du saknar behörighet att markera betalningar. Kontakta en administratör." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif not payments_enabled %}
|
||||
<div class="flex flex-col items-start gap-3 md:items-end">
|
||||
@@ -486,7 +492,7 @@
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% if can_change %}
|
||||
{% if can_edit_claim %}
|
||||
{% for claim in claims %}
|
||||
{% if claim.status == 'pending' %}
|
||||
<div class="fixed inset-0 z-40 hidden items-center justify-center bg-slate-900/80 p-4"
|
||||
|
||||
@@ -65,9 +65,11 @@
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1 h-2 w-2 rounded-full bg-brand-400"></span>
|
||||
<span class="min-w-0">
|
||||
{% blocktrans %}Behörigheterna <code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.view_claim</code>
|
||||
och <code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.change_claim</code>
|
||||
styr åtkomst till adminvyn respektive beslutsflödet.{% endblocktrans %}
|
||||
{% blocktrans %}Behörigheterna <code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.view_claim</code>,
|
||||
<code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.change_claim</code>,
|
||||
<code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.edit_claim_details</code>
|
||||
och <code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.mark_claim_paid</code>
|
||||
styr åtkomst till adminvyn, beslutsflödet, redigering samt betalningspanelen.{% endblocktrans %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
|
||||
@@ -81,7 +81,9 @@ class DashboardViewTests(TestCase):
|
||||
self.user = User.objects.create_user(username="admin", password="test123", email="admin@example.com")
|
||||
view_perm = Permission.objects.get(codename="view_claim")
|
||||
change_perm = Permission.objects.get(codename="change_claim")
|
||||
self.user.user_permissions.add(view_perm, change_perm)
|
||||
edit_perm = Permission.objects.get(codename="edit_claim_details")
|
||||
pay_perm = Permission.objects.get(codename="mark_claim_paid")
|
||||
self.user.user_permissions.add(view_perm, change_perm, edit_perm, pay_perm)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def _create_claim(self, **kwargs):
|
||||
@@ -197,3 +199,43 @@ class DashboardViewTests(TestCase):
|
||||
claim.refresh_from_db()
|
||||
self.assertNotEqual(claim.full_name, "Blocked")
|
||||
self.assertFalse(claim.logs.filter(action=ClaimLog.Action.DETAILS_EDITED).exists())
|
||||
|
||||
def test_edit_requires_permission(self):
|
||||
self.user.user_permissions.remove(Permission.objects.get(codename="edit_claim_details"))
|
||||
claim = self._create_claim()
|
||||
response = self.client.post(
|
||||
reverse("claims:admin-list"),
|
||||
{
|
||||
"action_type": "edit",
|
||||
"edit_claim_id": claim.id,
|
||||
"full_name": "Nope",
|
||||
"email": "nope@example.com",
|
||||
"account_number": "456",
|
||||
"amount": "200",
|
||||
"currency": Claim.Currency.SEK,
|
||||
"project": "",
|
||||
"description": "Should fail",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
claim.refresh_from_db()
|
||||
self.assertNotEqual(claim.full_name, "Nope")
|
||||
self.assertFalse(claim.logs.filter(action=ClaimLog.Action.DETAILS_EDITED).exists())
|
||||
|
||||
@override_settings(CLAIMS_ENABLE_INTERNAL_PAYMENTS=True)
|
||||
def test_mark_paid_requires_permission(self):
|
||||
claim = self._create_claim(status=Claim.Status.APPROVED)
|
||||
self.user.user_permissions.remove(Permission.objects.get(codename="mark_claim_paid"))
|
||||
response = self.client.post(
|
||||
reverse("claims:admin-list"),
|
||||
{
|
||||
"action_type": "payment",
|
||||
"payment_claim_id": claim.id,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
claim.refresh_from_db()
|
||||
self.assertIsNone(claim.paid_at)
|
||||
self.assertFalse(claim.logs.filter(action=ClaimLog.Action.MARKED_PAID).exists())
|
||||
|
||||
@@ -160,6 +160,8 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
context["status_choices"] = Claim.Status.choices
|
||||
context["decision_choices"] = ClaimDecisionForm().fields["action"].choices
|
||||
context["can_change"] = self.request.user.has_perm("claims.change_claim")
|
||||
context["can_edit_claim"] = self.request.user.has_perm("claims.edit_claim_details")
|
||||
context["can_mark_paid"] = self.request.user.has_perm("claims.mark_claim_paid")
|
||||
context["payments_enabled"] = getattr(settings, "CLAIMS_ENABLE_INTERNAL_PAYMENTS", False)
|
||||
context["summary"] = self._build_summary()
|
||||
context["project_options"] = Project.objects.filter(is_active=True).order_by("name")
|
||||
@@ -234,8 +236,8 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
if not getattr(settings, "CLAIMS_ENABLE_INTERNAL_PAYMENTS", False):
|
||||
messages.error(request, _("Betalningshantering är inte aktiverad."))
|
||||
return redirect(request.get_full_path())
|
||||
if not request.user.has_perm("claims.change_claim"):
|
||||
messages.error(request, _("Du har inte behörighet att uppdatera utlägg."))
|
||||
if not request.user.has_perm("claims.mark_claim_paid"):
|
||||
messages.error(request, _("Du har inte behörighet att markera betalningar i systemet."))
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
claim = get_object_or_404(Claim, pk=request.POST.get("payment_claim_id"))
|
||||
@@ -258,8 +260,8 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
def _handle_edit(self, request):
|
||||
if not request.user.has_perm("claims.change_claim"):
|
||||
messages.error(request, _("Du har inte behörighet att uppdatera utlägg."))
|
||||
if not request.user.has_perm("claims.edit_claim_details"):
|
||||
messages.error(request, _("Du har inte behörighet att redigera utlägg."))
|
||||
return redirect(request.get_full_path())
|
||||
claim = get_object_or_404(Claim, pk=request.POST.get("edit_claim_id"))
|
||||
if claim.status != Claim.Status.PENDING:
|
||||
@@ -384,6 +386,8 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
"is_staff": user.is_staff,
|
||||
"grant_view": user.has_perm("claims.view_claim"),
|
||||
"grant_change": user.has_perm("claims.change_claim"),
|
||||
"grant_edit": user.has_perm("claims.edit_claim_details"),
|
||||
"grant_pay": user.has_perm("claims.mark_claim_paid"),
|
||||
}
|
||||
),
|
||||
"delete_form": None
|
||||
@@ -413,6 +417,8 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
)
|
||||
self._set_perm(user, "claims.view_claim", form.cleaned_data.get("grant_view", False))
|
||||
self._set_perm(user, "claims.change_claim", form.cleaned_data.get("grant_change", False))
|
||||
self._set_perm(user, "claims.edit_claim_details", form.cleaned_data.get("grant_edit", False))
|
||||
self._set_perm(user, "claims.mark_claim_paid", form.cleaned_data.get("grant_pay", False))
|
||||
messages.success(request, _("Användaren %(user)s skapades.") % {"user": user.username})
|
||||
return redirect(request.path)
|
||||
return self.render_to_response(self.get_context_data(create_form=form))
|
||||
@@ -430,6 +436,8 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
user.save(update_fields=["is_staff"])
|
||||
self._set_perm(user, "claims.view_claim", form.cleaned_data["grant_view"])
|
||||
self._set_perm(user, "claims.change_claim", form.cleaned_data["grant_change"])
|
||||
self._set_perm(user, "claims.edit_claim_details", form.cleaned_data["grant_edit"])
|
||||
self._set_perm(user, "claims.mark_claim_paid", form.cleaned_data["grant_pay"])
|
||||
messages.success(request, _("Behörigheter uppdaterades för %(user)s.") % {"user": user.username})
|
||||
else:
|
||||
messages.error(request, _("Kunde inte uppdatera behörigheter."))
|
||||
|
||||
Reference in New Issue
Block a user