You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

280 lines
9.4 KiB

import json
from django import forms
from django.conf import settings
from django.contrib.humanize.templatetags.humanize import intcomma
from django.urls import reverse, path
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.views.generic import RedirectView
from django.utils.translation import get_language
from django.http import JsonResponse
from django.views.decorators.http import require_POST
# --- Unicode slugify override for Unfold tabs ---
import django.utils.text
from django.template.defaultfilters import register
_original_slugify = django.utils.text.slugify
def _unicode_slugify(value, allow_unicode=True):
return _original_slugify(value, allow_unicode=True)
django.utils.text.slugify = _unicode_slugify
@register.filter(is_safe=True)
def slugify(value):
return _unicode_slugify(value)
# Unfold Imports
from unfold.sites import UnfoldAdminSite
from unfold.admin import ModelAdmin
# --- Enhance ModelAdmin to ensure clicking on items in changelists always navigates to change view ---
_original_get_list_display_links = ModelAdmin.get_list_display_links
def _smart_get_list_display_links(self, request, list_display):
# If list_display_links is explicitly set (or explicitly set to None), respect that choice
if self.list_display_links or self.list_display_links is None:
return self.list_display_links
# If not set, rather than only list_display[0] which might be empty or a non-text field,
# include all candidate primary identifier fields (excluding action checkboxes and editable fields)
editable = getattr(self, 'list_editable', ()) or ()
candidates = [
f for f in list_display
if f != 'action_checkbox' and f not in editable
]
if not candidates:
return (list_display[0],) if list_display else ()
# Return primary candidate columns (up to first 4) so clicking on any main field navigates to details
return tuple(candidates[:4])
ModelAdmin.get_list_display_links = _smart_get_list_display_links
# ---------------------------------------------------------
# 1. Helper Functions
# ---------------------------------------------------------
def admin_url_generator(request, url_name):
"""
Dynamically generates admin URLs for the active admin site namespace.
"""
namespace = 'admin'
full_view_name = f"{namespace}:{url_name}"
try:
return reverse(full_view_name)
except Exception:
return "#"
def variables(request):
return {"plausible_domain": getattr(settings, 'PLAUSIBLE_DOMAIN', '')}
@require_POST
def toggle_sidebar(request):
"""Toggle sidebar state for Unfold admin interface"""
return JsonResponse({'status': 'success'})
# ---------------------------------------------------------
# 2. Custom Login Form
# ---------------------------------------------------------
class LoginForm:
"""Lazy login form to avoid circular imports during settings loading"""
@staticmethod
def get_form():
from unfold.forms import AuthenticationForm
class CustomLoginForm(AuthenticationForm):
password = forms.CharField(widget=forms.PasswordInput(render_value=True))
def __init__(self, request=None, *args, **kwargs):
super().__init__(request, *args, **kwargs)
self.fields["username"].label = _("Email")
return CustomLoginForm
# ---------------------------------------------------------
# 3. Unified Admin Site Definition
# ---------------------------------------------------------
class AppAdminSite(UnfoldAdminSite):
"""Unified Modern Admin Site with Unfold Theme"""
site_header = _("Management Dashboard")
site_title = _("Admin Portal")
index_title = _("System Administration")
site_subheader = _("Application Console")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.login_form = LoginForm.get_form()
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('toggle_sidebar/', toggle_sidebar, name='toggle_sidebar'),
]
return custom_urls + urls
def _get_colors(self, key, *args):
if key != "COLORS":
return super()._get_colors(key, *args)
# Modern, sleek slate-indigo color palette
return {
"base": {
"50": "248 250 252",
"100": "241 245 249",
"200": "226 232 240",
"300": "203 213 225",
"400": "148 163 184",
"500": "100 116 139",
"600": "71 85 105",
"700": "51 65 85",
"800": "30 41 59",
"900": "15 23 42",
"950": "2 6 23",
},
"primary": {
"50": "238 242 255",
"100": "224 231 255",
"200": "199 210 254",
"300": "165 180 252",
"400": "129 140 248",
"500": "99 102 241",
"600": "79 70 229",
"700": "67 56 202",
"800": "55 48 163",
"900": "49 46 129",
"950": "30 27 75",
},
"font": {
"subtle-light": "var(--color-base-500)",
"subtle-dark": "var(--color-base-400)",
"default-light": "var(--color-base-700)",
"default-dark": "var(--color-base-200)",
"important-light": "var(--color-base-900)",
"important-dark": "255 255 255",
},
}
class LazyAdminSite(UnfoldAdminSite):
def __init__(self, site_class, name):
self._site_class = site_class
self._name = name
self._instance = None
self.name = name
def _sync_default_registry(self):
if self._instance is None:
return
try:
from django.contrib.admin import site as default_admin_site
for model, model_admin in default_admin_site._registry.items():
if model not in self._instance._registry:
self._instance._registry[model] = model_admin
except Exception:
pass
def _ensure_instance(self):
if self._instance is None:
self._instance = self._site_class(name=self._name)
essential_attrs = ['site_header', 'site_title', 'index_title', 'site_url', 'login_template']
for attr in essential_attrs:
if hasattr(self._instance, attr):
setattr(self, attr, getattr(self._instance, attr))
self._sync_default_registry()
def _get_instance(self):
self._ensure_instance()
return self._instance
def __getattr__(self, name):
self._ensure_instance()
return getattr(self._instance, name)
def __call__(self, *args, **kwargs):
return self._get_instance()(*args, **kwargs)
@property
def urls(self):
self._ensure_instance()
return self._get_instance().urls
def get_urls(self):
self._ensure_instance()
return self._get_instance().get_urls()
def each_context(self, request):
return self._get_instance().each_context(request)
def register(self, model_or_iterable, admin_class=None, **options):
self._ensure_instance()
try:
return self._instance.register(model_or_iterable, admin_class, **options)
except Exception:
if admin_class is not None:
# If a custom admin_class was passed, update the registry
if isinstance(model_or_iterable, (list, tuple)):
for model in model_or_iterable:
self._instance._registry[model] = admin_class(model, self._instance)
else:
self._instance._registry[model_or_iterable] = admin_class(model_or_iterable, self._instance)
# Singleton instances
project_admin_site = LazyAdminSite(AppAdminSite, 'admin')
# Compatibility alias
dovoodi_admin_site = project_admin_site
class HomeView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
language = get_language() or 'en'
return f'/{language}/admin/'
# ---------------------------------------------------------
# 4. Dynamic Custom Dashboard
# ---------------------------------------------------------
def dashboard_callback(request, context):
"""Generic Dashboard KPI statistics"""
from django.apps import apps
if context is None:
context = {}
context.update({
"navigation": [{"title": _("Dashboard"), "link": "/admin/", "active": True}],
"kpi": [],
})
if not hasattr(request, "user") or not request.user.is_authenticated:
return context
try:
User = apps.get_model('account', 'User')
LoginHistory = apps.get_model('account', 'LoginHistory')
total_users = User.objects.count()
active_users = User.objects.filter(is_active=True).count()
staff_users = User.objects.filter(is_staff=True).count()
total_logins = LoginHistory.objects.count()
context["kpi"] = [
{"title": _("Total Users"), "metric": f"{total_users:,}"},
{"title": _("Active Users"), "metric": f"{active_users:,}"},
{"title": _("Staff Members"), "metric": f"{staff_users:,}"},
{"title": _("Total Logins"), "metric": f"{total_logins:,}"},
]
except Exception as e:
pass
return context