From 038ec4c85753511ded7df280655251f1f2c0c828 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Mon, 4 May 2026 08:52:20 +0330 Subject: [PATCH] update admin panel appearance and bugs sidebar is now much cleaner than before make slug and urls logic updated to support the unicode slugs in multilanguage panel index.html updated to support dashboards --- config/settings/base.py | 341 ++++++++++++++----------------------- templates/admin/index.html | 46 +++-- utils/admin.py | 279 ++++++++++++++++-------------- 3 files changed, 316 insertions(+), 350 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index 154eb26..3dd3f1f 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -205,7 +205,6 @@ THUMBNAIL_ALIASES = { LANGUAGES = [ ('en', _('English')), - ('fa', _('Persian')), ('ru', _('Russia')), ] LOCALE_PATHS = [ @@ -323,6 +322,22 @@ LOGIN_REDIRECT_URL = reverse_lazy("home") ###################################################################### from utils.admin import admin_url_generator , is_dovoodi_panel , is_main_panel +# --- ENHANCED DYNAMIC BADGE FUNCTION --- +def get_pending_certificates_badge(request): + try: + from apps.certificate.models import Certificate + qs = Certificate.objects.filter(status='pending') + + # If user is a professor (not staff/admin), only show their pending certificates + 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 str(count) if count > 0 else None + except Exception as e: + print(f"Badge Error: {e}") # Fails safely in terminal if DB isn't migrated yet + return None + UNFOLD = { # "SITE_TITLE": _("Imam Jawad Admin"), # "SITE_HEADER": _("Imam Jawad Admin"), @@ -341,6 +356,7 @@ UNFOLD = { # ], "SITE_SYMBOL": "settings", + "ALLOW_UNICODE_SLUGS": True, "SHOW_HISTORY": True, "SHOW_LANGUAGES": True, "ENVIRONMENT": "utils.environment_callback", @@ -359,7 +375,7 @@ UNFOLD = { # - FormulaAdminSite: پالت سبز برای امام جواد # - DovoodiAdminSite: پالت آبی-تیره برای داوودی (مطابق فرانت) "STYLES": [ - # lambda request: static("css/styles.css"), + lambda request: static("css/styles.css"), ], "SCRIPTS": [ # lambda request: static("js/chart.min.js"), @@ -470,19 +486,19 @@ UNFOLD = { "title": _("Course Lessons"), "icon": "menu_book", "link": lambda request: admin_url_generator(request, "course_courselesson_changelist"), - "active": lambda request: request.path.startswith(str(lambda request: admin_url_generator(request, "course_courselesson_changelist"))), + "active": lambda request: request.path.startswith(admin_url_generator(request, "course_courselesson_changelist")), }, { "title": _("Course Attachments"), "icon": "attach_file", "link": lambda request: admin_url_generator(request, "course_courseattachment_changelist"), - "active": lambda request: request.path.startswith(str(lambda request: admin_url_generator(request, "course_courseattachment_changelist"))), + "active": lambda request: request.path.startswith(admin_url_generator(request, "course_courseattachment_changelist")), }, { "title": _("Course Glossary"), "icon": "book", "link": lambda request: admin_url_generator(request, "course_courseglossary_changelist"), - "active": lambda request: request.path.startswith(str(lambda request: admin_url_generator(request, "course_courseglossary_changelist"))), + "active": lambda request: request.path.startswith(admin_url_generator(request, "course_courseglossary_changelist")), }, ], @@ -496,22 +512,22 @@ UNFOLD = { ], "items": [ { - "title": _("Course Onlines"), + "title": _("Live Sessions"), "icon": "video_call", "link": lambda request: admin_url_generator(request, "course_courselivesession_changelist"), - "active": lambda request: request.path.startswith(str(lambda request: admin_url_generator(request, "course_courselivesession_changelist"))), + "active": lambda request: request.path.startswith(admin_url_generator(request, "course_courselivesession_changelist")), }, { "title": _("Session Users"), "icon": "groups", "link": lambda request: admin_url_generator(request, "course_livesessionuser_changelist"), - "active": lambda request: request.path.startswith(str(lambda request: admin_url_generator(request, "course_livesessionuser_changelist"))), + "active": lambda request: request.path.startswith(admin_url_generator(request, "course_livesessionuser_changelist")), }, { "title": _("Session Recordings"), "icon": "play_circle", "link": lambda request: admin_url_generator(request, "course_livesessionrecording_changelist"), - "active": lambda request: request.path.startswith(str(lambda request: admin_url_generator(request, "course_livesessionrecording_changelist"))), + "active": lambda request: request.path.startswith(admin_url_generator(request, "course_livesessionrecording_changelist")), }, ], }, @@ -533,25 +549,55 @@ UNFOLD = { }, ], }, + { + "page": "quizzes", + "models": [ + "quiz.quiz", + "quiz.quizparticipant" + ], + "items": [ + { + "title": _("Quizzes"), + "icon": "quiz", + "link": lambda request: admin_url_generator(request, "quiz_quiz_changelist"), + "active": lambda request: request.path.startswith(admin_url_generator(request, "quiz_quiz_changelist")), + }, + { + "title": _("Quiz Participants"), + "icon": "group", + "link": lambda request: admin_url_generator(request, "quiz_quizparticipant_changelist"), + "active": lambda request: request.path.startswith(admin_url_generator(request, "quiz_quizparticipant_changelist")), + }, + ], + }, ], "SIDEBAR": { "show_search": True, "show_all_applications": True, "navigation": [ + # --- 1. OVERVIEW --- { "title": _(""), - "separator": True, - "collapsible": True, + "separator": False, "items": [ { "title": _("Dashboard"), "icon": "dashboard", "link": lambda request: admin_url_generator(request, "index"), }, + { + "title": _("Calender"), + "icon": "calendar_today", + "link": lambda request: admin_url_generator(request, "dobodbi_calendar_calendaroccasions_changelist"), + "permission": is_dovoodi_panel, + }, ], }, + + # --- 2. USER MANAGEMENT --- { "title": _(""), + "separator": True, "items": [ { "title": _("Authentication"), @@ -559,172 +605,117 @@ UNFOLD = { "link": lambda request: admin_url_generator(request, "auth_group_changelist"), "permission": lambda request: request.user.is_staff, }, - ], - }, - { - "title": _(""), - "items": [ { - "title": _("Users"), - "icon": "person", + "title": _("All Users"), + "icon": "people", "link": lambda request: admin_url_generator(request, "account_user_changelist"), "permission": lambda request: request.user.is_staff, }, - ], - }, - { - "title": _(""), - "items": [ - { + { "title": _("Students"), - "icon": "school", + "icon": "face", "link": lambda request: admin_url_generator(request, "account_studentuser_changelist"), "permission": is_main_panel, - }, - - ] - }, - { - "title": _(""), - "items": [ - { + }, + { "title": _("Professors"), - "icon": "person_book", + "icon": "history_edu", "link": lambda request: admin_url_generator(request, "account_professoruser_changelist"), "permission": is_main_panel, - }, - - ] - }, - { - "title": _(""), - "items": [ - { - "title": _("Calender"), - "icon": "calendar_today", - "link": lambda request: admin_url_generator(request, "dobodbi_calendar_calendaroccasions_changelist"), - "permission": is_dovoodi_panel, }, ], }, + # --- 3. ACADEMICS (Collapsible) --- { "title": _("Courses"), "collapsible": True, "separator": True, - "permission":is_main_panel, + "permission": is_main_panel, "items": [ { "title": _("Categories"), "icon": "category", "link": lambda request: admin_url_generator(request, "course_coursecategory_changelist"), - "permission":is_main_panel, + "permission": is_main_panel, }, { "title": _("Courses"), "icon": "school", "link": lambda request: admin_url_generator(request, "course_course_changelist"), - "permission":is_main_panel, - }, - { - "title": _("Lessons"), - "icon": "menu_book", - "link": lambda request: admin_url_generator(request, "course_lesson_changelist"), - "permission":is_main_panel, - }, - { - "title": _("Attachments"), - "icon": "attach_file", - "link": lambda request: admin_url_generator(request, "course_attachment_changelist"), - "permission":is_main_panel, - }, - { - "title": _("Glossary"), - "icon": "book", - "link": lambda request: admin_url_generator(request, "course_glossary_changelist"), - "permission":is_main_panel, + "permission": is_main_panel, }, { "title": _("Live Sessions"), - "icon": "video_call", + "icon": "video_camera_front", "link": lambda request: admin_url_generator(request, "course_courselivesession_changelist"), - "permission":is_main_panel, - }, - { - "title": _("Session Users"), - "icon": "groups", - "link": lambda request: admin_url_generator(request, "course_livesessionuser_changelist"), - "permission":is_main_panel, - }, - { - "title": _("Session Recordings"), - "icon": "play_circle", - "link": lambda request: admin_url_generator(request, "course_livesessionrecording_changelist"), - "permission":is_main_panel, + "permission": is_main_panel, }, { "title": _("Certificates"), "icon": "workspace_premium", "link": lambda request: admin_url_generator(request, "certificate_certificate_changelist"), - "permission":is_main_panel, + "permission": is_main_panel, + "badge": "utils.admin.get_pending_certificates_badge", }, - ] + ] }, + + # --- 4. ASSESSMENTS --- { - "title": _("Quizzes"), - "collapsible": True, + "title": _(""), "separator": True, - "permission":is_main_panel, + "permission": is_main_panel, "items": [ { "title": _("Quizzes"), "icon": "quiz", "link": lambda request: admin_url_generator(request, "quiz_quiz_changelist"), - "permission":is_main_panel, + "permission": is_main_panel, }, - { - "title": _("Quiz Participants"), - "icon": "group", - "link": lambda request: admin_url_generator(request, "quiz_quizparticipant_changelist"), - "permission":is_main_panel, - }, - ] - }, - { - "title": _("Transactions"), - "collapsible": True, - "separator": True, - "items": [ { "title": _("Transactions"), "icon": "payments", "link": lambda request: admin_url_generator(request, "transaction_transactionparticipant_changelist"), - "permission":is_main_panel, + "permission": is_main_panel, + }, + { + "title": _("Chat Rooms"), + "icon": "forum", + "link": lambda request: admin_url_generator(request, "chat_roommessage_changelist"), + "permission": is_main_panel, + }, + { + "title": _("Blogs"), + "icon": "article", + "link": lambda request: admin_url_generator(request, "blog_blog_changelist"), + "permission": is_main_panel, }, ] }, + # --- DOVOODI SECTIONS --- { "title": _("Libraries"), "collapsible": True, "separator": True, - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, "items": [ { "title": _("Books"), "icon": "menu_book", "link": lambda request: admin_url_generator(request, "library_book_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Categories"), "icon": "category", "link": lambda request: admin_url_generator(request, "library_category_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Collections"), "icon": "view_module", "link": lambda request: admin_url_generator(request, "library_pinnedbookcollection_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, ] }, @@ -732,53 +723,31 @@ UNFOLD = { "title": _("Videos"), "collapsible": True, "separator": True, - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, "items": [ { "title": _("Videos"), "icon": "live_tv", "link": lambda request: admin_url_generator(request, "video_video_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Categories"), "icon": "category", "link": lambda request: admin_url_generator(request, "video_videocategory_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Collections"), "icon": "view_module", "link": lambda request: admin_url_generator(request, "video_pinnedvideocollection_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Playlists"), "icon": "playlist_play", "link": lambda request: admin_url_generator(request, "video_videoplaylist_changelist"), - "permission":is_dovoodi_panel, - # "active": lambda request: "video/videoplaylist" in request.path, - }, - - ] - }, - { - "title": _("Blog"), - "collapsible": True, - "separator": True, - "permission":is_main_panel, - "items": [ - { - "title": _("Comments"), - "icon": "comment", - "link": lambda request: admin_url_generator(request, "api_comment_changelist"), - "permission":is_main_panel, - }, - { - "title": _("Blogs"), - "icon": "article", - "link": lambda request: admin_url_generator(request, "blog_blog_changelist"), - "permission":is_main_panel, + "permission": is_dovoodi_panel, }, ] }, @@ -786,37 +755,37 @@ UNFOLD = { "title": _("Articles"), "collapsible": True, "separator": True, - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, "items": [ { "title": _("Articles"), "icon": "article", "link": lambda request: admin_url_generator(request, "article_article_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Categories"), "icon": "category", "link": lambda request: admin_url_generator(request, "article_articlecategory_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Pinned Collections"), "icon": "collections_bookmark", "link": lambda request: admin_url_generator(request, "article_pinnedarticlecollection_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Regular Collections"), "icon": "view_module", "link": lambda request: admin_url_generator(request, "article_middlearticlecollection_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Article Contents"), "icon": "text_snippet", "link": lambda request: admin_url_generator(request, "article_articlecontent_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, ] }, @@ -824,181 +793,129 @@ UNFOLD = { "title": _("Podcasts"), "collapsible": True, "separator": True, - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, "items": [ { "title": _("Podcasts"), "icon": "headset", "link": lambda request: admin_url_generator(request, "podcast_podcast_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Categories"), "icon": "category", "link": lambda request: admin_url_generator(request, "podcast_podcastcategory_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Pinned Collections"), "icon": "collections_bookmark", "link": lambda request: admin_url_generator(request, "podcast_pinnedpodcastcollection_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Regular Collections"), "icon": "view_module", "link": lambda request: admin_url_generator(request, "podcast_middlepodcastcollection_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Playlists"), "icon": "playlist_play", "link": lambda request: admin_url_generator(request, "podcast_podcastplaylist_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("User Playlists"), "icon": "person_add", "link": lambda request: admin_url_generator(request, "podcast_userplaylist_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, ] }, - { - "title": _("Chats"), - "collapsible": True, - "separator": True, - "permission":is_main_panel, - "items": [ - { - "title": _("Chat Rooms"), - "icon": "forum", - "link": lambda request: admin_url_generator(request, "chat_roommessage_changelist"), - "permission":is_main_panel, - }, - # { - # "title": _("Chat Messages"), - # "icon": "chat", - # "link": lambda request: admin_url_generator(request, "apps_chat_chatmessage_changelist"), - # }, - # { - # "title": _("Read Status"), - # "icon": "mark_chat_read", - # "link": lambda request: admin_url_generator(request, "apps_chat_messagereadstatus_changelist"), - # }, - ] - }, { "title": _("Hadis"), "collapsible": True, "separator": True, - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, "items": [ { "title": _("Hadis Sects"), "icon": "account_tree", "link": lambda request: admin_url_generator(request, "hadis_hadissect_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Hadis Categories"), "icon": "category", "link": lambda request: admin_url_generator(request, "hadis_hadiscategory_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Hadis"), "icon": "format_quote", "link": lambda request: admin_url_generator(request, "hadis_hadis_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Hadis References"), "icon": "link", "link": lambda request: admin_url_generator(request, "hadis_hadisreference_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Hadis Tags"), "icon": "label", "link": lambda request: admin_url_generator(request, "hadis_hadistag_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Hadis Status"), "icon": "flag", "link": lambda request: admin_url_generator(request, "hadis_hadisstatus_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Transmitters"), "icon": "person", "link": lambda request: admin_url_generator(request, "hadis_transmitters_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, { "title": _("Hadis Transmitters"), "icon": "group", "link": lambda request: admin_url_generator(request, "hadis_hadistransmitter_changelist"), - "permission":is_dovoodi_panel, + "permission": is_dovoodi_panel, }, ] }, + + # --- 7. SYSTEM SETTINGS --- { - "title": _(""), + "title": _("System Settings"), + "separator": True, "items": [ { "title": _("App Versions"), "icon": "system_update", "link": lambda request: admin_url_generator(request, "api_appversion_changelist"), }, - ], - }, - { - "title": "", - "items": [ { "title": _("Global Preferences"), - "icon": "settings", + "icon": "tune", "link": lambda request: admin_url_generator(request, "dynamic_preferences_globalpreferencemodel_changelist"), }, - # You can add more preference sections here ], }, - # "STYLES": [ - # lambda request: static("css/styles.css"), - # ], - # "SCRIPTS": [ - # lambda request: static("js/scripts.js"), - # ], ], - }, + } } UNFOLD_STUDIO_DEFAULT_FRAGMENT = "color-schemes" UNFOLD_STUDIO_PERMISSION = lambda request: request.user.is_authenticated PLAUSIBLE_DOMAIN = env("PLAUSIBLE_DOMAIN") -# uncomment it just to check if redis caches and signals works fine locally - -# CACHES = { -# 'default': { -# "BACKEND": "django_redis.cache.RedisCache", -# "LOCATION": "redis://127.0.0.1:6379/1", -# "OPTIONS": { -# "CLIENT_CLASS": "django_redis.client.DefaultClient", -# } -# }, -# 'memory': { -# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', -# 'LOCATION': 'unique-snowflake', -# 'TIMEOUT': 5000, -# }, -# } - sentry_sdk.init( dsn="https://31aaeeb3a42f9a8c1b26272a0cb8ad3e@o4507991743725568.ingest.us.sentry.io/4511127356768256", - # Add data like request headers and IP for users, - # see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info send_default_pii=True, ) \ No newline at end of file diff --git a/templates/admin/index.html b/templates/admin/index.html index c2f6d1a..5a05999 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -1,24 +1,46 @@ -{% extends 'admin/base.html' %} - +{% extends "admin/base_site.html" %} {% load i18n unfold %} {% block breadcrumbs %}{% endblock %} {% block title %} - {% trans 'Dashboard' %} | {{ site_title|default:_('Django site admin') }} -{% endblock %} - -{% block extrahead %} - {% if plausible_domain %} - - {% endif %} +{% trans 'Dashboard' %} | {{ site_title|default:_('Django site admin') }} {% endblock %} {% block branding %} - {% include "unfold/helpers/site_branding.html" %} +{% include "unfold/helpers/site_branding.html" %} {% endblock %} {% block content %} - {% include "unfold/helpers/messages.html" %} -{% endblock %} +{% include "unfold/helpers/messages.html" %} +
+

+ {% trans 'System Overview' %} +

+
+ +
+ {% for item in kpi %} + {% component "unfold/components/card.html" %} + {% component "unfold/components/text.html" %} + {{ item.title }} + {% endcomponent %} + + {% component "unfold/components/title.html" %} + {{ item.metric }} + {% endcomponent %} + + {% if item.footer %} +
+ {{ item.footer|safe }} +
+ {% endif %} + {% endcomponent %} + {% empty %} +

+ {% trans "No statistics available for this panel." %} +

+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/utils/admin.py b/utils/admin.py index f238c1a..7a0f7f9 100644 --- a/utils/admin.py +++ b/utils/admin.py @@ -13,6 +13,24 @@ from django.utils.translation import get_language from django.http import JsonResponse 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 @@ -36,40 +54,43 @@ def is_main_panel(request): def admin_url_generator(request, url_name): """ Dynamically generates admin URLs based on the current active panel. - Usage in settings.py: lambda request: admin_url_generator(request, "app_model_changelist") """ - # Ensure admin sites are created and URLs are registered - _ = project_admin_site.urls # Access URLs to ensure site is created - _ = dovoodi_admin_site.urls # Access URLs to ensure site is created + _ = project_admin_site.urls + _ = dovoodi_admin_site.urls - # 1. Determine the current namespace if is_dovoodi_panel(request): namespace = 'dovoodi_admin' else: namespace = 'imam_javad_admin' - # 2. Construct the view name full_view_name = f"{namespace}:{url_name}" - # 3. Try Django URL reversal try: return reverse(full_view_name) except Exception: return "#" -def dashboard_callback(request, context): - context.update(random_data()) - return context +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', '')} -# Toggle sidebar view for Unfold compatibility @require_POST def toggle_sidebar(request): """Toggle sidebar state for Unfold admin interface""" - # This is a simple view that just returns success - # The actual sidebar state is handled client-side return JsonResponse({'status': 'success'}) # --------------------------------------------------------- @@ -81,7 +102,6 @@ class LoginForm: @staticmethod def get_form(): - # Import AuthenticationForm only when needed from unfold.forms import AuthenticationForm class CustomLoginForm(AuthenticationForm): @@ -89,7 +109,6 @@ class LoginForm: def __init__(self, request=None, *args, **kwargs): super().__init__(request, *args, **kwargs) - # Change the label of the username field to "Email" self.fields["username"].label = "Email" return CustomLoginForm @@ -107,16 +126,13 @@ class FormulaAdminSite(UnfoldAdminSite): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Set login form after initialization to avoid circular import self.login_form = LoginForm.get_form() def get_form(self, request, obj=None, **kwargs): - """Override to ensure form is properly initialized""" form = super().get_form(request, obj, **kwargs) return form def each_context(self, request): - """Override to provide site-specific dropdown""" context = super().each_context(request) context["site_dropdown"] = [ { @@ -145,13 +161,9 @@ class FormulaAdminSite(UnfoldAdminSite): return custom_urls + urls def _get_colors(self, key, *args): - """Override colors for Imam Javad admin panel with green theme""" - from unfold.utils import hex_to_rgb - if key != "COLORS": return super()._get_colors(key, *args) - # پالت رنگی سبز برای امام جواد imam_javad_colors = { "base": { "50": "249 250 251", @@ -172,7 +184,7 @@ class FormulaAdminSite(UnfoldAdminSite): "200": "167 247 216", "300": "110 240 189", "400": "37 213 152", - "500": "37 208 118", # #25D076 - سبز اصلی + "500": "37 208 118", "600": "29 166 94", "700": "25 136 80", "800": "22 108 66", @@ -185,7 +197,7 @@ class FormulaAdminSite(UnfoldAdminSite): "200": "153 246 228", "300": "94 234 212", "400": "45 212 191", - "500": "1 53 59", # #01353B - پس‌زمینه تیره + "500": "1 53 59", "600": "1 43 48", "700": "1 36 40", "800": "1 30 34", @@ -201,7 +213,6 @@ class FormulaAdminSite(UnfoldAdminSite): "important-dark": "255 255 255", }, } - return imam_javad_colors class DovoodiAdminSite(UnfoldAdminSite): @@ -213,16 +224,13 @@ class DovoodiAdminSite(UnfoldAdminSite): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Set login form after initialization to avoid circular import self.login_form = LoginForm.get_form() def get_form(self, request, obj=None, **kwargs): - """Override to ensure form is properly initialized""" form = super().get_form(request, obj, **kwargs) return form def each_context(self, request): - """Override to provide site-specific dropdown""" context = super().each_context(request) context["site_dropdown"] = [ { @@ -251,51 +259,44 @@ class DovoodiAdminSite(UnfoldAdminSite): return custom_urls + urls def _get_colors(self, key, *args): - """Override colors for Dovoodi admin panel with blue/teal theme matching frontend""" - from unfold.utils import hex_to_rgb - if key != "COLORS": return super()._get_colors(key, *args) - # پالت رنگی آبی-تیره برای داوودی (مطابق با فرانت) dovoodi_colors = { "base": { - # استفاده از Wormy scale برای base - "50": "252 251 250", # #FCFBFA - "100": "246 245 244", # #F6F5F4 - "200": "240 236 233", # #F0ECE9 - "300": "229 220 211", # #E5DCD3 - "400": "191 174 157", # #BFAE9D + "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", # #111827 + "900": "17 24 39", "950": "3 7 18", }, "primary": { - # استفاده از رنگ آبی اصلی فرانت - "50": "240 244 255", # #F0F4FF - "100": "224 231 255", # #E0E7FF - "200": "199 210 254", # #C7D2FE - "300": "165 180 252", # #A5B4FC - "400": "129 140 248", # #818CF8 - "500": "99 102 241", # #6366F1 - "600": "81 114 225", # #5172E1 - رنگ اصلی فرانت - "700": "59 89 196", # #3B59C4 - "800": "45 68 145", # #2D4491 + "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": { - # استفاده از Second scale فرانت (تیره سبز-آبی) - "50": "210 215 215", # #D2D7D7 - "100": "151 163 164", # #97A3A4 - "200": "108 125 127", # #6C7D7F - "300": "44 69 72", # #2C4548 - "400": "1 31 34", # #011F22 - "500": "1 22 24", # #011618 - پس‌زمینه اصلی - "600": "1 19 21", # #011315 + "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", @@ -310,21 +311,17 @@ class DovoodiAdminSite(UnfoldAdminSite): "important-dark": "255 255 255", }, } - return dovoodi_colors -# Simple admin site placeholders that will be replaced after Django setup class AdminSitePlaceholder(UnfoldAdminSite): """Placeholder that behaves like an admin site until Django is fully loaded""" def __init__(self, site_class, name): - # 1. Store config for lazy loading self._site_class = site_class self._name = name self._real_instance = None - self._registry = {} # Store registrations until real instance is created + self._registry = {} - # 2. THE FIX: Copy visual attributes immediately so Templates see them! 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') @@ -332,22 +329,17 @@ class AdminSitePlaceholder(UnfoldAdminSite): def _get_real_instance(self): if self._real_instance is None: - # Force creation of real admin site instance for proper CSS loading self._real_instance = self._site_class(name=self._name) - # Copy critical attributes immediately for template access self.login_form = self._real_instance.login_form self.login_template = self._real_instance.login_template - # Copy any other attributes that templates might need 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)) - # Copy any existing registrations from the placeholder to the real instance if hasattr(self, '_registry'): for model, admin_class in self._registry.items(): self._real_instance.register(model, admin_class) - # Replace the global reference with the real instance import sys current_module = sys.modules[__name__] if hasattr(current_module, self._name): @@ -356,11 +348,9 @@ class AdminSitePlaceholder(UnfoldAdminSite): return self._real_instance def __getattr__(self, name): - # Delegate all attribute access to the real instance for proper CSS and template loading return getattr(self._get_real_instance(), name) def get_form(self, request, obj=None, **kwargs): - """Delegate get_form to the real admin site instance""" return self._get_real_instance().get_form(request, obj, **kwargs) def __call__(self, *args, **kwargs): @@ -377,37 +367,30 @@ class AdminSitePlaceholder(UnfoldAdminSite): return self._get_real_instance().each_context(request) def register(self, model_or_iterable, admin_class=None, **options): - """Store registrations in placeholder until real instance is created""" 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: - # If already registered, update the admin class self._registry[model] = admin_class else: self._registry[model] = admin_class - # Also register with the real instance if it exists if self._real_instance is not None: self._real_instance.register(model, admin_class, **options) -# Create lazy-loading admin site instances that properly inherit from AdminSite + class LazyAdminSite(UnfoldAdminSite): def __init__(self, site_class, name): - # Don't call super().__init__() to avoid creating the real instance yet self._site_class = site_class self._name = name self._instance = None - # Set basic attributes that Django expects for isinstance checks self.name = name def _force_init(self): - """Force initialization immediately""" if self._instance is None: self._instance = self._site_class(name=self._name) - # Copy all attributes to this instance for attr in dir(self._instance): if not attr.startswith('_') and attr not in ('register', 'unregister', 'is_registered'): try: @@ -415,10 +398,8 @@ class LazyAdminSite(UnfoldAdminSite): except (AttributeError, TypeError): pass def _ensure_instance(self): - """Ensure the real instance exists""" if self._instance is None: self._instance = self._site_class(name=self._name) - # Copy essential attributes to this lazy wrapper for compatibility essential_attrs = ['site_header', 'site_title', 'index_title', 'site_url', 'login_template'] for attr in essential_attrs: if hasattr(self._instance, attr): @@ -437,23 +418,19 @@ class LazyAdminSite(UnfoldAdminSite): @property def urls(self): - """Ensure URLs are accessed to create the instance""" return self._get_instance().urls def get_urls(self): - """Delegate get_urls to ensure proper URL registration""" return self._get_instance().get_urls() def register(self, model_or_iterable, admin_class=None, **options): - """Register models with the real admin site instance""" self._ensure_instance() return self._instance.register(model_or_iterable, admin_class, **options) -# Create lazy admin site instances + project_admin_site = LazyAdminSite(FormulaAdminSite, 'imam_javad_admin') dovoodi_admin_site = LazyAdminSite(DovoodiAdminSite, 'dovoodi_admin') -# Function to replace placeholders with real instances when Django is ready def replace_placeholders_with_real_sites(): global project_admin_site, dovoodi_admin_site if isinstance(project_admin_site, AdminSitePlaceholder): @@ -461,55 +438,105 @@ def replace_placeholders_with_real_sites(): if isinstance(dovoodi_admin_site, AdminSitePlaceholder): dovoodi_admin_site = DovoodiAdminSite(name='dovoodi_admin') -# The placeholders will be replaced with real instances when first accessed -# This ensures proper CSS loading for admin templates class HomeView(RedirectView): - """ - Redirects /admin/ to the language-prefixed admin URL. - The domain-based routing middleware will handle which admin site to use. - """ def get_redirect_url(self, *args, **kwargs): - # دریافت زبان فعلی (پیش‌فرض: en) language = get_language() or 'en' - - # Now we simply redirect to /language/admin/ - # The SiteMiddleware will route to the correct admin based on domain return f'/{language}/admin/' # --------------------------------------------------------- -# 4. Dummy Data for Dashboard Charts +# 4. Dynamic Custom Dashboard # --------------------------------------------------------- -@lru_cache -def random_data(): - WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] +def dashboard_callback(request, context): + """ + Generates dynamic KPI cards for the custom dashboard. + Shows different stats depending on whether it's Dovoodi or Imam Javad. + """ + from django.apps import apps - # Generate some fake data - positive = [[1, random.randrange(8, 28)] for i in range(1, 28)] - negative = [[-1, -random.randrange(8, 28)] for i in range(1, 28)] - average = [r[1] - random.randint(3, 5) for r in positive] - performance_positive = [[1, random.randrange(8, 28)] for i in range(1, 28)] - performance_negative = [[-1, -random.randrange(8, 28)] for i in range(1, 28)] - - return { - "navigation": [ - {"title": _("Dashboard"), "link": "/", "active": True}, - {"title": _("Analytics"), "link": "#"}, - {"title": _("Settings"), "link": "#"}, - ], - "kpi": [ - { - "title": "Total Revenue", - "metric": f"${intcomma(f'{random.uniform(1000, 9999):.02f}')}", - "footer": mark_safe(f'+{intcomma(f"{random.uniform(1, 9):.02f}")}% progress'), - "chart": json.dumps({"labels": [WEEKDAYS[day % 7] for day in range(1, 28)], "datasets": [{"data": average, "borderColor": "#9333ea"}]}), - }, - ], - "chart": json.dumps({ - "labels": [WEEKDAYS[day % 7] for day in range(1, 28)], - "datasets": [ - {"label": "Revenue", "data": positive, "backgroundColor": "var(--color-primary-700)"}, - ], - }), - } \ No newline at end of file + if context is None: + context = {} + + context.update({ + "navigation": [{"title": _("Dashboard"), "link": "/", "active": True}], + "kpi": [] + }) + + 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') + Course = apps.get_model('course', 'Course') + Certificate = apps.get_model('certificate', 'Certificate') + + # Certificates logic respecting permissions + 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() + + cert_footer_text = "Action Required" if pending_certs > 0 else "All Caught Up" + cert_footer_color = "text-orange-500" if pending_certs > 0 else "text-green-500" + + context["kpi"] = [ + { + "title": _("Active Students"), + "metric": f"{StudentUser.objects.filter(is_active=True).count():,}", + "footer": mark_safe('Platform Users'), + }, + { + "title": _("Published Courses"), + "metric": f"{Course.objects.exclude(status='inactive').count():,}", + "footer": mark_safe('Total Offerings'), + }, + { + "title": _("Pending Certificates"), + "metric": f"{pending_certs:,}", + "footer": mark_safe(f'{cert_footer_text}'), + }, + ] + except Exception as e: + print(f"Dashboard KPI Error (Main Panel): {e}") + + # ------------------------------------------------------------- + # 2. DOVOODI PANEL STATS + # ------------------------------------------------------------- + else: + try: + # Safely fetch Dovoodi specific models + 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():,}", + "footer": mark_safe('Total Records'), + }, + { + "title": _("Books & Articles"), + "metric": f"{total_reading:,}", + "footer": mark_safe('Reading Materials'), + }, + { + "title": _("Multimedia"), + "metric": f"{total_multimedia:,}", + "footer": mark_safe('Videos & Podcasts'), + }, + ] + except Exception as e: + print(f"Dashboard KPI Error (Dovoodi Panel): {e}") + + return context \ No newline at end of file