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.
 
 

587 lines
22 KiB

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('<strong class="text-green-500 font-medium">+ {}</strong>', _("Updated Today"))},
{"title": _("Pending Certificates"), "metric": f"{pending_certs:,}", "footer": format_html('<strong class="text-orange-500 font-medium">{}</strong>', _("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