diff --git a/apps/account/admin/user.py b/apps/account/admin/user.py index 198d569..ea41791 100644 --- a/apps/account/admin/user.py +++ b/apps/account/admin/user.py @@ -117,6 +117,12 @@ class UserAdmin(ModelAdmin, BaseUserAdmin): ("role", "user_type"), ("is_active", "is_staff", "is_superuser"), "groups", + ), + }), + (_("Granular Permissions"), { + "classes": ["tab"], + "description": _("Low-level Django database permissions assigned directly to this user. Recommended to manage permissions via Roles or Groups instead."), + "fields": ( "user_permissions", ), }), diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html index e49fe9f..3437024 100644 --- a/templates/admin/base_site.html +++ b/templates/admin/base_site.html @@ -205,6 +205,60 @@ } }); }); + + // Modern Dual-Listbox with Real-time Counters and Badges + (function() { + function setupListboxEnhancements() { + const selectors = document.querySelectorAll('.selector'); + if (!selectors.length) return; + + selectors.forEach(selector => { + const availableSelect = selector.querySelector('.selector-available select'); + const chosenSelect = selector.querySelector('.selector-chosen select'); + const availableH2 = selector.querySelector('.selector-available h2'); + const chosenH2 = selector.querySelector('.selector-chosen h2'); + + function updateCounters() { + if (availableSelect && availableH2) { + const count = availableSelect.options.length; + let badge = availableH2.querySelector('.counter-badge'); + if (!badge) { + badge = document.createElement('span'); + badge.className = 'counter-badge text-xs px-2.5 py-0.5 rounded-full font-medium ml-2.5 bg-slate-200 text-slate-700 dark:bg-slate-700 dark:text-slate-300'; + availableH2.appendChild(badge); + } + badge.textContent = count + ' available'; + } + if (chosenSelect && chosenH2) { + const count = chosenSelect.options.length; + let badge = chosenH2.querySelector('.counter-badge'); + if (!badge) { + badge = document.createElement('span'); + badge.className = 'counter-badge text-xs px-2.5 py-0.5 rounded-full font-semibold ml-2.5 bg-indigo-100 text-indigo-700 dark:bg-indigo-900/60 dark:text-indigo-300'; + chosenH2.appendChild(badge); + } + badge.textContent = count + ' assigned'; + } + } + + updateCounters(); + selector.addEventListener('click', function () { + setTimeout(updateCounters, 100); + }); + if (availableSelect) availableSelect.addEventListener('change', updateCounters); + if (chosenSelect) chosenSelect.addEventListener('change', updateCounters); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupListboxEnhancements); + } else { + setupListboxEnhancements(); + } + window.addEventListener('load', function() { + setTimeout(setupListboxEnhancements, 100); + }); + })(); {% if plausible_domain %}