Browse Source

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
master
Mohsen Taba 2 weeks ago
parent
commit
038ec4c857
  1. 333
      config/settings/base.py
  2. 46
      templates/admin/index.html
  3. 277
      utils/admin.py

333
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,
},
{
"title": _("Quiz Participants"),
"icon": "group",
"link": lambda request: admin_url_generator(request, "quiz_quizparticipant_changelist"),
"permission":is_main_panel,
},
]
"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,
},
]
},
{
"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,
"permission": is_dovoodi_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,
)

46
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 %}
<script defer data-domain="{{ plausible_domain }}" src="https://plausible.io/js/script.js"></script>
{% 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" %}
<div class="mb-6">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{% trans 'System Overview' %}
</h1>
</div>
<div class="grid gap-4 mb-4 md:grid-cols-2 lg:grid-cols-3">
{% 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 %}
<div class="mt-2 text-sm">
{{ item.footer|safe }}
</div>
{% endif %}
{% endcomponent %}
{% empty %}
<p class="text-gray-500 dark:text-gray-400 col-span-full">
{% trans "No statistics available for this panel." %}
</p>
{% endfor %}
</div>
{% endblock %}

277
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"]
# 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": [
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
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": "Total Revenue",
"metric": f"${intcomma(f'{random.uniform(1000, 9999):.02f}')}",
"footer": mark_safe(f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;progress'),
"chart": json.dumps({"labels": [WEEKDAYS[day % 7] for day in range(1, 28)], "datasets": [{"data": average, "borderColor": "#9333ea"}]}),
"title": _("Active Students"),
"metric": f"{StudentUser.objects.filter(is_active=True).count():,}",
"footer": mark_safe('<strong class="text-green-500 font-medium">Platform Users</strong>'),
},
],
"chart": json.dumps({
"labels": [WEEKDAYS[day % 7] for day in range(1, 28)],
"datasets": [
{"label": "Revenue", "data": positive, "backgroundColor": "var(--color-primary-700)"},
],
}),
}
{
"title": _("Published Courses"),
"metric": f"{Course.objects.exclude(status='inactive').count():,}",
"footer": mark_safe('<strong class="text-blue-500 font-medium">Total Offerings</strong>'),
},
{
"title": _("Pending Certificates"),
"metric": f"{pending_certs:,}",
"footer": mark_safe(f'<strong class="{cert_footer_color} font-medium">{cert_footer_text}</strong>'),
},
]
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('<strong class="text-amber-500 font-medium">Total Records</strong>'),
},
{
"title": _("Books & Articles"),
"metric": f"{total_reading:,}",
"footer": mark_safe('<strong class="text-blue-500 font-medium">Reading Materials</strong>'),
},
{
"title": _("Multimedia"),
"metric": f"{total_multimedia:,}",
"footer": mark_safe('<strong class="text-purple-500 font-medium">Videos & Podcasts</strong>'),
},
]
except Exception as e:
print(f"Dashboard KPI Error (Dovoodi Panel): {e}")
return context
Loading…
Cancel
Save