feat: allow editing user profile info via modal
This commit is contained in:
@@ -160,6 +160,42 @@ class UserPermissionForm(forms.Form):
|
||||
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"))
|
||||
first_name = forms.CharField(
|
||||
max_length=150,
|
||||
required=False,
|
||||
label=_("Förnamn"),
|
||||
widget=forms.TextInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
last_name = forms.CharField(
|
||||
max_length=150,
|
||||
required=False,
|
||||
label=_("Efternamn"),
|
||||
widget=forms.TextInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
email = forms.EmailField(
|
||||
required=False,
|
||||
label=_("E-post"),
|
||||
widget=forms.EmailInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
new_password1 = forms.CharField(
|
||||
required=False,
|
||||
label=_("Nytt lösenord"),
|
||||
widget=forms.PasswordInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
new_password2 = forms.CharField(
|
||||
required=False,
|
||||
label=_("Bekräfta nytt lösenord"),
|
||||
widget=forms.PasswordInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
pwd1 = cleaned.get("new_password1")
|
||||
pwd2 = cleaned.get("new_password2")
|
||||
if pwd1 or pwd2:
|
||||
if pwd1 != pwd2:
|
||||
self.add_error("new_password2", _("Lösenorden matchar inte."))
|
||||
return cleaned
|
||||
|
||||
|
||||
class DeleteUserForm(forms.Form):
|
||||
|
||||
@@ -111,17 +111,32 @@
|
||||
<div class="rounded-2xl bg-slate-50 p-4">
|
||||
<p class="text-sm font-semibold text-gray-700">{% trans "Behörigheter" %}</p>
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-xs">
|
||||
<span class="rounded-full px-3 py-1 {% if row.permission_flags.is_staff %}bg-emerald-100 text-emerald-800{% else %}bg-slate-200 text-slate-600{% endif %}">{% trans "Admin/staff" %}</span>
|
||||
<span class="rounded-full px-3 py-1 {% if row.permission_flags.view %}bg-blue-100 text-blue-800{% else %}bg-slate-200 text-slate-600{% endif %}">{% trans "Får se utlägg" %}</span>
|
||||
<span class="rounded-full px-3 py-1 {% if row.permission_flags.change %}bg-indigo-100 text-indigo-800{% else %}bg-slate-200 text-slate-600{% endif %}">{% trans "Får besluta utlägg" %}</span>
|
||||
<span class="rounded-full px-3 py-1 {% if row.permission_flags.edit %}bg-purple-100 text-purple-800{% else %}bg-slate-200 text-slate-600{% endif %}">{% trans "Får redigera utlägg" %}</span>
|
||||
<span class="rounded-full px-3 py-1 {% if row.permission_flags.pay %}bg-amber-100 text-amber-800{% else %}bg-slate-200 text-slate-600{% endif %}">{% trans "Får markera betalningar" %}</span>
|
||||
{% if row.permission_flags.is_staff %}
|
||||
<span class="rounded-full px-3 py-1 bg-emerald-100 text-emerald-800">{% trans "Admin/staff" %}</span>
|
||||
{% endif %}
|
||||
{% if row.permission_flags.view %}
|
||||
<span class="rounded-full px-3 py-1 bg-blue-100 text-blue-800">{% trans "Får se utlägg" %}</span>
|
||||
{% endif %}
|
||||
{% if row.permission_flags.change %}
|
||||
<span class="rounded-full px-3 py-1 bg-indigo-100 text-indigo-800">{% trans "Får besluta utlägg" %}</span>
|
||||
{% endif %}
|
||||
{% if row.permission_flags.edit %}
|
||||
<span class="rounded-full px-3 py-1 bg-purple-100 text-purple-800">{% trans "Får redigera utlägg" %}</span>
|
||||
{% endif %}
|
||||
{% if row.permission_flags.pay %}
|
||||
<span class="rounded-full px-3 py-1 bg-amber-100 text-amber-800">{% trans "Får markera betalningar" %}</span>
|
||||
{% endif %}
|
||||
{% if not row.permission_flags.is_staff and not row.permission_flags.view and not row.permission_flags.change and not row.permission_flags.edit and not row.permission_flags.pay %}
|
||||
<span class="rounded-full bg-slate-200 px-3 py-1 text-slate-600">{% trans "Inga behörigheter tilldelade" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="button"
|
||||
data-open-permission-edit="{{ user.id }}"
|
||||
class="mt-4 inline-flex items-center gap-2 rounded-full bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700">
|
||||
{% trans "Redigera behörigheter" %}
|
||||
</button>
|
||||
{% if can_change_users %}
|
||||
<button type="button"
|
||||
data-open-permission-edit="{{ user.id }}"
|
||||
class="mt-4 inline-flex items-center gap-2 rounded-full bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700">
|
||||
{% trans "Redigera behörigheter" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="rounded-2xl border border-red-100 bg-red-50 p-4 text-sm text-red-800">
|
||||
<p class="font-semibold">{% trans "Ta bort konto" %}</p>
|
||||
@@ -155,8 +170,9 @@
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% for row in user_rows %}
|
||||
{% with user=row.user form=row.permission_form %}
|
||||
{% if can_change_users %}
|
||||
{% for row in user_rows %}
|
||||
{% with user=row.user form=row.permission_form %}
|
||||
<div class="fixed inset-0 z-40 hidden items-center justify-center bg-slate-900/80 p-4"
|
||||
data-permission-modal="{{ user.id }}"
|
||||
aria-hidden="true"
|
||||
@@ -178,7 +194,55 @@
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="update">
|
||||
{{ form.user_id }}
|
||||
<div class="space-y-3 text-sm text-gray-800">
|
||||
<div class="space-y-5 text-sm text-gray-800">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Kontaktuppgifter" %}</p>
|
||||
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-gray-600" for="{{ form.first_name.id_for_label }}">{{ form.first_name.label }}</label>
|
||||
{{ form.first_name }}
|
||||
{% for error in form.first_name.errors %}
|
||||
<p class="text-xs text-rose-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-gray-600" for="{{ form.last_name.id_for_label }}">{{ form.last_name.label }}</label>
|
||||
{{ form.last_name }}
|
||||
{% for error in form.last_name.errors %}
|
||||
<p class="text-xs text-rose-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="text-xs font-semibold text-gray-600" for="{{ form.email.id_for_label }}">{{ form.email.label }}</label>
|
||||
{{ form.email }}
|
||||
{% for error in form.email.errors %}
|
||||
<p class="text-xs text-rose-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Lösenord" %}</p>
|
||||
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-gray-600" for="{{ form.new_password1.id_for_label }}">{{ form.new_password1.label }}</label>
|
||||
{{ form.new_password1 }}
|
||||
{% for error in form.new_password1.errors %}
|
||||
<p class="text-xs text-rose-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-gray-600" for="{{ form.new_password2.id_for_label }}">{{ form.new_password2.label }}</label>
|
||||
{{ form.new_password2 }}
|
||||
{% for error in form.new_password2.errors %}
|
||||
<p class="text-xs text-rose-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">{% trans "Lämna fälten tomma för att behålla nuvarande lösenord." %}</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Behörigheter" %}</p>
|
||||
<label class="flex items-center gap-3" for="{{ form.is_staff.id_for_label }}">
|
||||
{{ form.is_staff }}
|
||||
<span>{% trans "Admin/staff" %}</span>
|
||||
@@ -199,6 +263,7 @@
|
||||
{{ form.grant_pay }}
|
||||
<span>{% trans "Får markera betalningar" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button type="button"
|
||||
@@ -213,12 +278,14 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
{% if can_change_users %}
|
||||
<script>
|
||||
(function () {
|
||||
function lockScroll() {
|
||||
@@ -286,4 +353,5 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -239,3 +239,40 @@ class DashboardViewTests(TestCase):
|
||||
claim.refresh_from_db()
|
||||
self.assertIsNone(claim.paid_at)
|
||||
self.assertFalse(claim.logs.filter(action=ClaimLog.Action.MARKED_PAID).exists())
|
||||
|
||||
|
||||
class UserManagementViewTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.admin = User.objects.create_user(username="manager", password="test123", email="manager@example.com")
|
||||
perms = Permission.objects.filter(codename__in=["view_user", "change_user"])
|
||||
self.admin.user_permissions.add(*perms)
|
||||
self.client.force_login(self.admin)
|
||||
self.target = User.objects.create_user(username="editor", password="oldpass123", email="old@example.com")
|
||||
|
||||
def test_admin_can_update_profile_and_password(self):
|
||||
response = self.client.post(
|
||||
reverse("claims:user-manage"),
|
||||
{
|
||||
"action": "update",
|
||||
"user_id": self.target.id,
|
||||
"is_staff": "on",
|
||||
"grant_view": "on",
|
||||
"grant_change": "",
|
||||
"grant_edit": "on",
|
||||
"grant_pay": "on",
|
||||
"first_name": "New",
|
||||
"last_name": "Name",
|
||||
"email": "new@example.com",
|
||||
"new_password1": "StrongPass123!",
|
||||
"new_password2": "StrongPass123!",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
target = get_user_model().objects.get(pk=self.target.pk)
|
||||
self.assertEqual(target.first_name, "New")
|
||||
self.assertEqual(target.last_name, "Name")
|
||||
self.assertEqual(target.email, "new@example.com")
|
||||
self.assertTrue(target.is_staff)
|
||||
self.assertTrue(target.check_password("StrongPass123!"))
|
||||
|
||||
@@ -3,7 +3,7 @@ from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth import get_user_model, password_validation, update_session_auth_hash
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||
from django.db.models import Sum
|
||||
@@ -14,6 +14,7 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
from django.views.generic import ListView, TemplateView
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from .forms import (
|
||||
ClaimDecisionForm,
|
||||
@@ -390,11 +391,14 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
"permission_form": UserPermissionForm(
|
||||
initial={
|
||||
"user_id": user.id,
|
||||
"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"),
|
||||
"is_staff": perms["is_staff"],
|
||||
"grant_view": perms["view"],
|
||||
"grant_change": perms["change"],
|
||||
"grant_edit": perms["edit"],
|
||||
"grant_pay": perms["pay"],
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"email": user.email,
|
||||
}
|
||||
),
|
||||
"permission_flags": perms,
|
||||
@@ -405,6 +409,7 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
)
|
||||
context["user_rows"] = rows
|
||||
context["create_form"] = kwargs.get("create_form") or UserManagementForm()
|
||||
context["can_change_users"] = self.request.user.has_perm("auth.change_user")
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -437,11 +442,35 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
form = UserPermissionForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = get_object_or_404(User, pk=form.cleaned_data["user_id"])
|
||||
if user == request.user and not form.cleaned_data["is_staff"]:
|
||||
new_is_staff = form.cleaned_data["is_staff"]
|
||||
if user == request.user and not new_is_staff:
|
||||
messages.error(request, _("Du kan inte ta bort din egen staff-status."))
|
||||
return redirect(request.path)
|
||||
user.is_staff = form.cleaned_data["is_staff"]
|
||||
user.save(update_fields=["is_staff"])
|
||||
update_fields = set()
|
||||
if user.is_staff != new_is_staff:
|
||||
user.is_staff = new_is_staff
|
||||
update_fields.add("is_staff")
|
||||
for attr in ("first_name", "last_name", "email"):
|
||||
new_value = form.cleaned_data.get(attr)
|
||||
if new_value is None:
|
||||
continue
|
||||
if getattr(user, attr) != new_value:
|
||||
setattr(user, attr, new_value)
|
||||
update_fields.add(attr)
|
||||
new_password = form.cleaned_data.get("new_password1")
|
||||
if new_password:
|
||||
try:
|
||||
password_validation.validate_password(new_password, user)
|
||||
except ValidationError as exc:
|
||||
for error in exc:
|
||||
messages.error(request, error)
|
||||
return redirect(request.path)
|
||||
user.set_password(new_password)
|
||||
update_fields.add("password")
|
||||
if update_fields:
|
||||
user.save(update_fields=list(update_fields))
|
||||
if new_password and user == request.user:
|
||||
update_session_auth_hash(request, user)
|
||||
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"])
|
||||
|
||||
Reference in New Issue
Block a user