import json import random from functools import lru_cache 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.utils.html import format_html from django.views.decorators.http import require_POST # --- AGGRESSIVE UNICODE PATCH FOR UNFOLD 0.64.1 TABS --- import django.utils.text from django.template.defaultfilters import register _original_slugify = django.utils.text.slugify def _unicode_slugify(value, allow_unicode=True): # We forcefully pass allow_unicode=True to preserve Russian characters for tab IDs return _original_slugify(value, allow_unicode=True) django.utils.text.slugify = _unicode_slugify # We must also override the template filter explicitly because Unfold calls it directly in 0.64.1 @register.filter(is_safe=True) def slugify(value): return _unicode_slugify(value) # ------------------------------------------------------- # Unfold Imports from unfold.sites import UnfoldAdminSite # --------------------------------------------------------- # 1. Helper Functions # --------------------------------------------------------- def is_dovoodi_panel(request): """ Returns True if the user is accessing the Dovoodi admin panel. Checks if 'dovodi' or 'dovoodi' is in the host domain, or if '/dovoodi/' exists anywhere in the path. """ host = request.get_host() return 'dovodi' in host or 'dovoodi' in host or '/dovoodi/' in request.path def is_main_panel(request): """Returns True if the user is accessing the Main (Imam Javad) admin panel.""" return not is_dovoodi_panel(request) def admin_url_generator(request, url_name): """ Dynamically generates admin URLs based on the current active panel. """ _ = project_admin_site.urls _ = dovoodi_admin_site.urls if is_dovoodi_panel(request): namespace = 'dovoodi_admin' else: namespace = 'imam_javad_admin' full_view_name = f"{namespace}:{url_name}" try: return reverse(full_view_name) except Exception: return "#" def get_pending_certificates_badge(request): """Generates the integer for the sidebar badge""" try: from apps.certificate.models import Certificate qs = Certificate.objects.filter(status='pending') if request.user.is_authenticated and not request.user.is_staff and not getattr(request.user, 'is_superuser', False): qs = qs.filter(course__professor=request.user) count = qs.count() return count if count > 0 else None except Exception as e: print(f"Badge Error: {e}") return None 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. Admin Site Definitions # --------------------------------------------------------- class FormulaAdminSite(UnfoldAdminSite): """Main Admin for Imam Jawad""" site_header = _("Imam Javad Admin") site_title = _("Imam Javad Admin") index_title = _("System Administration") site_subheader = _("Imam Javad School") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.login_form = LoginForm.get_form() def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) return form def each_context(self, request): context = super().each_context(request) context["site_dropdown"] = [ { "title": _("Imam Javad Site"), "link": "https://imamjavad.newhorizonco.uk/", "icon": "diamond", }, { "title": _("Dovoodi Site"), "link": "https://dovodi.newhorizonco.uk/", "icon": "diamond", }, { "title": _("Dovoodi Admin"), "link": "https://dovodi.newhorizonco.uk/admin/", "icon": "diamond", } ] return context 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) imam_javad_colors = { "base": { "50": "249 250 251", "100": "243 244 246", "200": "229 231 235", "300": "209 213 219", "400": "156 163 175", "500": "107 114 128", "600": "75 85 99", "700": "55 65 81", # "800": "31 41 55", # "900": "17 24 39", # "950": "3 7 18", # "800": "12 19 26", # #0C131A (Cards / Sidebar) # "900": "1 31 34", # #011F22 (Main Background) # "950": "1 19 21", # #011315 (Deep Accents) # "800": "2 22 24", # Cards / Sidebar (Slightly elevated dark green) # "900": "0 12 14", # Main Background (Almost pitch black-green) # "950": "0 5 6", # Deepest Accents (Effectively black) "800": "10 36 38", # Cards (Lighter to float above the background) "900": "3 21 22", # Main Background (Lighter dark-green) "950": "0 5 6", # Sidebar (Kept exactly the same!) }, "primary": { "50": "234 253 243", "100": "208 251 232", "200": "167 247 216", "300": "110 240 189", "400": "37 213 152", "500": "37 208 118", "600": "29 166 94", "700": "25 136 80", "800": "22 108 66", "900": "20 89 57", "950": "10 53 34", }, "secondary": { "50": "240 253 250", "100": "204 251 241", "200": "153 246 228", "300": "94 234 212", "400": "45 212 191", "500": "1 53 59", "600": "1 43 48", "700": "1 36 40", "800": "1 30 34", "900": "0 26 29", "950": "0 13 15", }, "font": { "subtle-light": "var(--color-base-500)", "subtle-dark": "var(--color-base-400)", "default-light": "var(--color-secondary-500)", "default-dark": "var(--color-base-300)", "important-light": "var(--color-base-900)", "important-dark": "255 255 255", }, } return imam_javad_colors class DovoodiAdminSite(UnfoldAdminSite): """Secondary Admin for Dovoodi""" site_header = _("Dovoodi Admin") site_title = _("Dovoodi Admin") index_title = _("System Administration") site_subheader = _("Dovodbi Application") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.login_form = LoginForm.get_form() def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) return form def each_context(self, request): context = super().each_context(request) context["site_dropdown"] = [ { "title": _("Dovoodi Site"), "link": "https://dovodi.newhorizonco.uk/", "icon": "diamond", }, { "title": _("Imam Javad Site"), "link": "https://imamjavad.newhorizonco.uk/", "icon": "diamond", }, { "title": _("Imam Javad Admin"), "link": "https://imamjavad.newhorizonco.uk/admin/", "icon": "diamond", } ] return context 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) dovoodi_colors = { "base": { "50": "252 251 250", "100": "246 245 244", "200": "240 236 233", "300": "229 220 211", "400": "191 174 157", "500": "107 114 128", "600": "75 85 99", "700": "55 65 81", "800": "31 41 55", "900": "17 24 39", "950": "3 7 18", }, "primary": { "50": "240 244 255", "100": "224 231 255", "200": "199 210 254", "300": "165 180 252", "400": "129 140 248", "500": "99 102 241", "600": "81 114 225", "700": "59 89 196", "800": "45 68 145", "900": "30 41 91", "950": "15 20 45", }, "secondary": { "50": "210 215 215", "100": "151 163 164", "200": "108 125 127", "300": "44 69 72", "400": "1 31 34", "500": "1 22 24", "600": "1 19 21", "700": "0 15 17", "800": "0 12 14", "900": "0 8 10", "950": "0 4 5", }, "font": { "subtle-light": "var(--color-base-500)", "subtle-dark": "var(--color-base-400)", "default-light": "var(--color-secondary-400)", "default-dark": "var(--color-base-200)", "important-light": "var(--color-base-900)", "important-dark": "255 255 255", }, } return dovoodi_colors class AdminSitePlaceholder(UnfoldAdminSite): """Placeholder that behaves like an admin site until Django is fully loaded""" def __init__(self, site_class, name): self._site_class = site_class self._name = name self._real_instance = None self._registry = {} self.site_header = getattr(site_class, 'site_header', 'Django Admin') self.site_title = getattr(site_class, 'site_title', 'Django Site') self.index_title = getattr(site_class, 'index_title', 'Site Administration') self.site_subheader = getattr(site_class, 'site_subheader', '') def _get_real_instance(self): if self._real_instance is None: self._real_instance = self._site_class(name=self._name) self.login_form = self._real_instance.login_form self.login_template = self._real_instance.login_template for attr in ['site_header', 'site_title', 'index_title', 'site_subheader']: if hasattr(self._real_instance, attr): setattr(self, attr, getattr(self._real_instance, attr)) if hasattr(self, '_registry'): for model, admin_class in self._registry.items(): self._real_instance.register(model, admin_class) import sys current_module = sys.modules[__name__] if hasattr(current_module, self._name): setattr(current_module, self._name, self._real_instance) return self._real_instance def __getattr__(self, name): return getattr(self._get_real_instance(), name) def get_form(self, request, obj=None, **kwargs): return self._get_real_instance().get_form(request, obj, **kwargs) def __call__(self, *args, **kwargs): return self._get_real_instance()(*args, **kwargs) def get_urls(self): return self._get_real_instance().get_urls() @property def urls(self): return self._get_real_instance().urls def each_context(self, request): return self._get_real_instance().each_context(request) def register(self, model_or_iterable, admin_class=None, **options): if isinstance(model_or_iterable, (list, tuple)): for model in model_or_iterable: self.register(model, admin_class, **options) else: model = model_or_iterable if model in self._registry: self._registry[model] = admin_class else: self._registry[model] = admin_class if self._real_instance is not None: self._real_instance.register(model, admin_class, **options) class LazyAdminSite(UnfoldAdminSite): def __init__(self, site_class, name): self._site_class = site_class self._name = name self._instance = None self.name = name def _force_init(self): if self._instance is None: self._instance = self._site_class(name=self._name) for attr in dir(self._instance): if not attr.startswith('_') and attr not in ('register', 'unregister', 'is_registered'): try: setattr(self, attr, getattr(self._instance, attr)) except (AttributeError, TypeError): 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)) 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): return self._get_instance().urls def get_urls(self): return self._get_instance().get_urls() def register(self, model_or_iterable, admin_class=None, **options): self._ensure_instance() return self._instance.register(model_or_iterable, admin_class, **options) project_admin_site = LazyAdminSite(FormulaAdminSite, 'imam_javad_admin') dovoodi_admin_site = LazyAdminSite(DovoodiAdminSite, 'dovoodi_admin') def replace_placeholders_with_real_sites(): global project_admin_site, dovoodi_admin_site if isinstance(project_admin_site, AdminSitePlaceholder): project_admin_site = FormulaAdminSite(name='imam_javad_admin') if isinstance(dovoodi_admin_site, AdminSitePlaceholder): dovoodi_admin_site = DovoodiAdminSite(name='dovoodi_admin') 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): from django.apps import apps from django.db.models import Count, Sum from django.utils import timezone if context is None: context = {} context.update({ "navigation": [{"title": _("Dashboard"), "link": "/", "active": True}], "kpi": [], "top_courses": [], "tx_stats": {}, # New dict for our multi-segment donut chart }) if not hasattr(request, "user") or not request.user.is_authenticated: return context # ------------------------------------------------------------- # 1. IMAM JAVAD PANEL STATS # ------------------------------------------------------------- if is_main_panel(request): try: StudentUser = apps.get_model('account', 'StudentUser') ProfessorUser = apps.get_model('account', 'ProfessorUser') Course = apps.get_model('course', 'Course') Blog = apps.get_model('blog', 'Blog') Certificate = apps.get_model('certificate', 'Certificate') Transaction = apps.get_model('transaction', 'TransactionParticipant') # --- 1. Basic Counts --- active_students = StudentUser.objects.filter(is_active=True).count() active_professors = ProfessorUser.objects.filter(is_active=True).count() active_courses = Course.objects.exclude(status='inactive').count() total_blogs = Blog.objects.count() # --- 2. Certificates --- certs_qs = Certificate.objects.filter(status='pending') if not request.user.is_staff and not getattr(request.user, 'is_superuser', False): certs_qs = certs_qs.filter(course__professor=request.user) pending_certs = certs_qs.count() # --- 3. Revenue (Last 30 Days) --- thirty_days_ago = timezone.now() - timezone.timedelta(days=30) revenue_data = Transaction.objects.filter( status='success', created_at__gte=thirty_days_ago ).aggregate(Sum('price')) revenue = revenue_data['price__sum'] or 0 # --- 4. Transaction Multi-Status Breakdown --- total_tx = Transaction.objects.count() if total_tx > 0: success_count = Transaction.objects.filter(status='success').count() pending_count = Transaction.objects.filter(status='pending').count() waiting_count = Transaction.objects.filter(status='waiting_approval').count() failed_count = Transaction.objects.filter(status='failed').count() # Calculate percentages pct_success = (success_count / total_tx) * 100 pct_pending = (pending_count / total_tx) * 100 pct_waiting = (waiting_count / total_tx) * 100 pct_failed = (failed_count / total_tx) * 100 # Calculate SVG Dash Offsets (Accumulative for overlapping circles) offset_success = 100 - pct_success offset_pending = 100 - (pct_success + pct_pending) offset_waiting = 100 - (pct_success + pct_pending + pct_waiting) offset_failed = 100 - (pct_success + pct_pending + pct_waiting + pct_failed) # Should be 0 else: pct_success = pct_pending = pct_waiting = pct_failed = 0 offset_success = offset_pending = offset_waiting = offset_failed = 100 context["tx_stats"] = { "total": total_tx, "pct_success": round(pct_success, 1), "pct_pending": round(pct_pending, 1), "pct_waiting": round(pct_waiting, 1), "pct_failed": round(pct_failed, 1), # Format as strings to prevent Django from converting dots to commas in Russian "offset_success": f"{offset_success:.2f}", "offset_pending": f"{offset_pending:.2f}", "offset_waiting": f"{offset_waiting:.2f}", } # --- 5. Top 5 Courses --- top_courses = Course.objects.select_related('professor').annotate( participant_count=Count('participants') ).order_by('-participant_count')[:5] context["top_courses"] = top_courses # --- Map to KPIs --- context["kpi"] = [ {"title": _("Active Students"), "metric": f"{active_students:,}"}, {"title": _("Professors"), "metric": f"{active_professors:,}"}, {"title": _("Active Courses"), "metric": f"{active_courses:,}"}, {"title": _("Total Blogs"), "metric": f"{total_blogs:,}"}, {"title": _("30-Day Revenue"), "metric": f"${revenue:,.2f}", "footer": format_html('+ {}', _("Updated Today"))}, {"title": _("Pending Certificates"), "metric": f"{pending_certs:,}", "footer": format_html('{}', _("Requires Action")) if pending_certs > 0 else ""}, ] except Exception as e: print(f"Dashboard KPI Error (Main Panel): {e}") # ------------------------------------------------------------- # 2. DOVOODI PANEL STATS # ------------------------------------------------------------- else: try: Video = apps.get_model('video', 'Video') Book = apps.get_model('library', 'Book') Article = apps.get_model('article', 'Article') Hadis = apps.get_model('hadis', 'Hadis') Podcast = apps.get_model('podcast', 'Podcast') total_multimedia = Video.objects.count() + Podcast.objects.count() total_reading = Book.objects.count() + Article.objects.count() context["kpi"] = [ {"title": _("Hadith Database"), "metric": f"{Hadis.objects.count():,}"}, {"title": _("Books & Articles"), "metric": f"{total_reading:,}"}, {"title": _("Multimedia"), "metric": f"{total_multimedia:,}"}, ] except Exception as e: print(f"Dashboard KPI Error (Dovoodi Panel): {e}") return context