diff --git a/apps/account/admin/user.py b/apps/account/admin/user.py index ea41791..a3ef984 100644 --- a/apps/account/admin/user.py +++ b/apps/account/admin/user.py @@ -40,7 +40,23 @@ class UserAdminCreationForm(UserCreationForm): return email +import json +from apps.account.admin.widgets import GroupPillSelectMultipleWidget, RolePermissionsMatrixWidget + + class UserAdminChangeForm(UserChangeForm): + groups = forms.ModelMultipleChoiceField( + queryset=Group.objects.all(), + widget=GroupPillSelectMultipleWidget, + required=False, + label=_("Assigned Groups") + ) + custom_permissions = forms.CharField( + widget=RolePermissionsMatrixWidget, + required=False, + label=_("Application Permissions Matrix") + ) + class Meta(UserChangeForm.Meta): model = User @@ -49,6 +65,17 @@ class UserAdminChangeForm(UserChangeForm): if 'email' in self.fields: self.fields['email'].required = True + def clean_custom_permissions(self): + val = self.cleaned_data.get('custom_permissions') + if isinstance(val, str): + try: + return json.loads(val) if val.strip() else {} + except Exception: + return {} + elif isinstance(val, dict): + return val + return {} + class UserAdmin(ModelAdmin, BaseUserAdmin): form = UserAdminChangeForm @@ -59,7 +86,7 @@ class UserAdmin(ModelAdmin, BaseUserAdmin): list_display_links = ('first_name', 'last_name', 'fullname', 'email') ordering = ("-id",) search_fields = ('email', 'first_name', 'last_name', 'fullname', 'username',) - filter_horizontal = ('groups', 'user_permissions') + filter_horizontal = ('user_permissions',) list_filter = [ "role", "user_type", @@ -117,22 +144,16 @@ class UserAdmin(ModelAdmin, BaseUserAdmin): ("role", "user_type"), ("is_active", "is_staff", "is_superuser"), "groups", + "custom_permissions", ), }), - (_("Granular Permissions"), { - "classes": ["tab"], - "description": _("Low-level Django database permissions assigned directly to this user. Recommended to manage permissions via Roles or Groups instead."), + (_("Raw Database Permissions"), { + "classes": ["tab", "collapse"], + "description": _("Low-level Django database permissions for internal operations. Application features are managed in the Permissions Matrix above."), "fields": ( "user_permissions", ), }), - (_("Custom Permissions"), { - "classes": ["tab"], - "description": _("Override specific permissions for this individual user (JSON dictionary of {permission_name: True/False}). Overrides take precedence over the role defaults."), - "fields": ( - "custom_permissions", - ), - }), (_("Timestamps & Metadata"), { "classes": ["tab"], "fields": ( diff --git a/apps/account/admin/widgets.py b/apps/account/admin/widgets.py new file mode 100644 index 0000000..243c6b9 --- /dev/null +++ b/apps/account/admin/widgets.py @@ -0,0 +1,414 @@ +import json +from django import forms +from django.utils.html import escape +from django.utils.safestring import mark_safe + +from apps.account.models.role import Role, PERMISSION_CATEGORIES, ALL_PERMISSION_FIELDS + + +CATEGORY_ICONS = { + "Identity & Profiles": "badge", + "Chat & Communications": "forum", + "CMS & Articles": "article", + "LMS & Academics": "school", + "Meetings": "video_camera_front", + "Events": "event", + "Community Projects": "task_alt", + "Donations & Charity": "volunteer_activism", + "Support & Tickets": "confirmation_number", + "Dynamic Forms": "dynamic_form", + "Diplomacy & Institutional": "public", + "Administration & Security": "admin_panel_settings", +} + + +class GroupPillSelectMultipleWidget(forms.CheckboxSelectMultiple): + """ + Renders Django auth Groups as modern, interactive clickable pill cards. + Replaces the cumbersome dual-listbox with instant one-click toggle chips. + """ + def render(self, name, value, attrs=None, renderer=None): + if value is None: + value = [] + elif isinstance(value, str): + value = [value] + else: + value = [str(v) for v in value] + + attrs = attrs or {} + output = [ + '
+ Permissions are automatically inherited from the user's Assigned Role. You can selectively override any permission below. +
+