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