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. 341
      config/settings/base.py
  2. 46
      templates/admin/index.html
  3. 279
      utils/admin.py

341
config/settings/base.py

@ -205,7 +205,6 @@ THUMBNAIL_ALIASES = {
LANGUAGES = [ LANGUAGES = [
('en', _('English')), ('en', _('English')),
('fa', _('Persian')),
('ru', _('Russia')), ('ru', _('Russia')),
] ]
LOCALE_PATHS = [ 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 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 = { UNFOLD = {
# "SITE_TITLE": _("Imam Jawad Admin"), # "SITE_TITLE": _("Imam Jawad Admin"),
# "SITE_HEADER": _("Imam Jawad Admin"), # "SITE_HEADER": _("Imam Jawad Admin"),
@ -341,6 +356,7 @@ UNFOLD = {
# ], # ],
"SITE_SYMBOL": "settings", "SITE_SYMBOL": "settings",
"ALLOW_UNICODE_SLUGS": True,
"SHOW_HISTORY": True, "SHOW_HISTORY": True,
"SHOW_LANGUAGES": True, "SHOW_LANGUAGES": True,
"ENVIRONMENT": "utils.environment_callback", "ENVIRONMENT": "utils.environment_callback",
@ -359,7 +375,7 @@ UNFOLD = {
# - FormulaAdminSite: پالت سبز برای امام جواد # - FormulaAdminSite: پالت سبز برای امام جواد
# - DovoodiAdminSite: پالت آبی-تیره برای داوودی (مطابق فرانت) # - DovoodiAdminSite: پالت آبی-تیره برای داوودی (مطابق فرانت)
"STYLES": [ "STYLES": [
# lambda request: static("css/styles.css"),
lambda request: static("css/styles.css"),
], ],
"SCRIPTS": [ "SCRIPTS": [
# lambda request: static("js/chart.min.js"), # lambda request: static("js/chart.min.js"),
@ -470,19 +486,19 @@ UNFOLD = {
"title": _("Course Lessons"), "title": _("Course Lessons"),
"icon": "menu_book", "icon": "menu_book",
"link": lambda request: admin_url_generator(request, "course_courselesson_changelist"), "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"), "title": _("Course Attachments"),
"icon": "attach_file", "icon": "attach_file",
"link": lambda request: admin_url_generator(request, "course_courseattachment_changelist"), "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"), "title": _("Course Glossary"),
"icon": "book", "icon": "book",
"link": lambda request: admin_url_generator(request, "course_courseglossary_changelist"), "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": [ "items": [
{ {
"title": _("Course Onlines"),
"title": _("Live Sessions"),
"icon": "video_call", "icon": "video_call",
"link": lambda request: admin_url_generator(request, "course_courselivesession_changelist"), "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"), "title": _("Session Users"),
"icon": "groups", "icon": "groups",
"link": lambda request: admin_url_generator(request, "course_livesessionuser_changelist"), "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"), "title": _("Session Recordings"),
"icon": "play_circle", "icon": "play_circle",
"link": lambda request: admin_url_generator(request, "course_livesessionrecording_changelist"), "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": { "SIDEBAR": {
"show_search": True, "show_search": True,
"show_all_applications": True, "show_all_applications": True,
"navigation": [ "navigation": [
# --- 1. OVERVIEW ---
{ {
"title": _(""), "title": _(""),
"separator": True,
"collapsible": True,
"separator": False,
"items": [ "items": [
{ {
"title": _("Dashboard"), "title": _("Dashboard"),
"icon": "dashboard", "icon": "dashboard",
"link": lambda request: admin_url_generator(request, "index"), "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": _(""), "title": _(""),
"separator": True,
"items": [ "items": [
{ {
"title": _("Authentication"), "title": _("Authentication"),
@ -559,172 +605,117 @@ UNFOLD = {
"link": lambda request: admin_url_generator(request, "auth_group_changelist"), "link": lambda request: admin_url_generator(request, "auth_group_changelist"),
"permission": lambda request: request.user.is_staff, "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"), "link": lambda request: admin_url_generator(request, "account_user_changelist"),
"permission": lambda request: request.user.is_staff, "permission": lambda request: request.user.is_staff,
}, },
],
},
{
"title": _(""),
"items": [
{
{
"title": _("Students"), "title": _("Students"),
"icon": "school",
"icon": "face",
"link": lambda request: admin_url_generator(request, "account_studentuser_changelist"), "link": lambda request: admin_url_generator(request, "account_studentuser_changelist"),
"permission": is_main_panel, "permission": is_main_panel,
},
]
},
{
"title": _(""),
"items": [
{
},
{
"title": _("Professors"), "title": _("Professors"),
"icon": "person_book",
"icon": "history_edu",
"link": lambda request: admin_url_generator(request, "account_professoruser_changelist"), "link": lambda request: admin_url_generator(request, "account_professoruser_changelist"),
"permission": is_main_panel, "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"), "title": _("Courses"),
"collapsible": True, "collapsible": True,
"separator": True, "separator": True,
"permission":is_main_panel,
"permission": is_main_panel,
"items": [ "items": [
{ {
"title": _("Categories"), "title": _("Categories"),
"icon": "category", "icon": "category",
"link": lambda request: admin_url_generator(request, "course_coursecategory_changelist"), "link": lambda request: admin_url_generator(request, "course_coursecategory_changelist"),
"permission":is_main_panel,
"permission": is_main_panel,
}, },
{ {
"title": _("Courses"), "title": _("Courses"),
"icon": "school", "icon": "school",
"link": lambda request: admin_url_generator(request, "course_course_changelist"), "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"), "title": _("Live Sessions"),
"icon": "video_call",
"icon": "video_camera_front",
"link": lambda request: admin_url_generator(request, "course_courselivesession_changelist"), "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"), "title": _("Certificates"),
"icon": "workspace_premium", "icon": "workspace_premium",
"link": lambda request: admin_url_generator(request, "certificate_certificate_changelist"), "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, "separator": True,
"permission":is_main_panel,
"permission": is_main_panel,
"items": [ "items": [
{ {
"title": _("Quizzes"), "title": _("Quizzes"),
"icon": "quiz", "icon": "quiz",
"link": lambda request: admin_url_generator(request, "quiz_quiz_changelist"), "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"), "title": _("Transactions"),
"icon": "payments", "icon": "payments",
"link": lambda request: admin_url_generator(request, "transaction_transactionparticipant_changelist"), "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"), "title": _("Libraries"),
"collapsible": True, "collapsible": True,
"separator": True, "separator": True,
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
"items": [ "items": [
{ {
"title": _("Books"), "title": _("Books"),
"icon": "menu_book", "icon": "menu_book",
"link": lambda request: admin_url_generator(request, "library_book_changelist"), "link": lambda request: admin_url_generator(request, "library_book_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Categories"), "title": _("Categories"),
"icon": "category", "icon": "category",
"link": lambda request: admin_url_generator(request, "library_category_changelist"), "link": lambda request: admin_url_generator(request, "library_category_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Collections"), "title": _("Collections"),
"icon": "view_module", "icon": "view_module",
"link": lambda request: admin_url_generator(request, "library_pinnedbookcollection_changelist"), "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"), "title": _("Videos"),
"collapsible": True, "collapsible": True,
"separator": True, "separator": True,
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
"items": [ "items": [
{ {
"title": _("Videos"), "title": _("Videos"),
"icon": "live_tv", "icon": "live_tv",
"link": lambda request: admin_url_generator(request, "video_video_changelist"), "link": lambda request: admin_url_generator(request, "video_video_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Categories"), "title": _("Categories"),
"icon": "category", "icon": "category",
"link": lambda request: admin_url_generator(request, "video_videocategory_changelist"), "link": lambda request: admin_url_generator(request, "video_videocategory_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Collections"), "title": _("Collections"),
"icon": "view_module", "icon": "view_module",
"link": lambda request: admin_url_generator(request, "video_pinnedvideocollection_changelist"), "link": lambda request: admin_url_generator(request, "video_pinnedvideocollection_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Playlists"), "title": _("Playlists"),
"icon": "playlist_play", "icon": "playlist_play",
"link": lambda request: admin_url_generator(request, "video_videoplaylist_changelist"), "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"), "title": _("Articles"),
"collapsible": True, "collapsible": True,
"separator": True, "separator": True,
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
"items": [ "items": [
{ {
"title": _("Articles"), "title": _("Articles"),
"icon": "article", "icon": "article",
"link": lambda request: admin_url_generator(request, "article_article_changelist"), "link": lambda request: admin_url_generator(request, "article_article_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Categories"), "title": _("Categories"),
"icon": "category", "icon": "category",
"link": lambda request: admin_url_generator(request, "article_articlecategory_changelist"), "link": lambda request: admin_url_generator(request, "article_articlecategory_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Pinned Collections"), "title": _("Pinned Collections"),
"icon": "collections_bookmark", "icon": "collections_bookmark",
"link": lambda request: admin_url_generator(request, "article_pinnedarticlecollection_changelist"), "link": lambda request: admin_url_generator(request, "article_pinnedarticlecollection_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Regular Collections"), "title": _("Regular Collections"),
"icon": "view_module", "icon": "view_module",
"link": lambda request: admin_url_generator(request, "article_middlearticlecollection_changelist"), "link": lambda request: admin_url_generator(request, "article_middlearticlecollection_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Article Contents"), "title": _("Article Contents"),
"icon": "text_snippet", "icon": "text_snippet",
"link": lambda request: admin_url_generator(request, "article_articlecontent_changelist"), "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"), "title": _("Podcasts"),
"collapsible": True, "collapsible": True,
"separator": True, "separator": True,
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
"items": [ "items": [
{ {
"title": _("Podcasts"), "title": _("Podcasts"),
"icon": "headset", "icon": "headset",
"link": lambda request: admin_url_generator(request, "podcast_podcast_changelist"), "link": lambda request: admin_url_generator(request, "podcast_podcast_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Categories"), "title": _("Categories"),
"icon": "category", "icon": "category",
"link": lambda request: admin_url_generator(request, "podcast_podcastcategory_changelist"), "link": lambda request: admin_url_generator(request, "podcast_podcastcategory_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Pinned Collections"), "title": _("Pinned Collections"),
"icon": "collections_bookmark", "icon": "collections_bookmark",
"link": lambda request: admin_url_generator(request, "podcast_pinnedpodcastcollection_changelist"), "link": lambda request: admin_url_generator(request, "podcast_pinnedpodcastcollection_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Regular Collections"), "title": _("Regular Collections"),
"icon": "view_module", "icon": "view_module",
"link": lambda request: admin_url_generator(request, "podcast_middlepodcastcollection_changelist"), "link": lambda request: admin_url_generator(request, "podcast_middlepodcastcollection_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Playlists"), "title": _("Playlists"),
"icon": "playlist_play", "icon": "playlist_play",
"link": lambda request: admin_url_generator(request, "podcast_podcastplaylist_changelist"), "link": lambda request: admin_url_generator(request, "podcast_podcastplaylist_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("User Playlists"), "title": _("User Playlists"),
"icon": "person_add", "icon": "person_add",
"link": lambda request: admin_url_generator(request, "podcast_userplaylist_changelist"), "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"), "title": _("Hadis"),
"collapsible": True, "collapsible": True,
"separator": True, "separator": True,
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
"items": [ "items": [
{ {
"title": _("Hadis Sects"), "title": _("Hadis Sects"),
"icon": "account_tree", "icon": "account_tree",
"link": lambda request: admin_url_generator(request, "hadis_hadissect_changelist"), "link": lambda request: admin_url_generator(request, "hadis_hadissect_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Hadis Categories"), "title": _("Hadis Categories"),
"icon": "category", "icon": "category",
"link": lambda request: admin_url_generator(request, "hadis_hadiscategory_changelist"), "link": lambda request: admin_url_generator(request, "hadis_hadiscategory_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Hadis"), "title": _("Hadis"),
"icon": "format_quote", "icon": "format_quote",
"link": lambda request: admin_url_generator(request, "hadis_hadis_changelist"), "link": lambda request: admin_url_generator(request, "hadis_hadis_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Hadis References"), "title": _("Hadis References"),
"icon": "link", "icon": "link",
"link": lambda request: admin_url_generator(request, "hadis_hadisreference_changelist"), "link": lambda request: admin_url_generator(request, "hadis_hadisreference_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Hadis Tags"), "title": _("Hadis Tags"),
"icon": "label", "icon": "label",
"link": lambda request: admin_url_generator(request, "hadis_hadistag_changelist"), "link": lambda request: admin_url_generator(request, "hadis_hadistag_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Hadis Status"), "title": _("Hadis Status"),
"icon": "flag", "icon": "flag",
"link": lambda request: admin_url_generator(request, "hadis_hadisstatus_changelist"), "link": lambda request: admin_url_generator(request, "hadis_hadisstatus_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Transmitters"), "title": _("Transmitters"),
"icon": "person", "icon": "person",
"link": lambda request: admin_url_generator(request, "hadis_transmitters_changelist"), "link": lambda request: admin_url_generator(request, "hadis_transmitters_changelist"),
"permission":is_dovoodi_panel,
"permission": is_dovoodi_panel,
}, },
{ {
"title": _("Hadis Transmitters"), "title": _("Hadis Transmitters"),
"icon": "group", "icon": "group",
"link": lambda request: admin_url_generator(request, "hadis_hadistransmitter_changelist"), "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": [ "items": [
{ {
"title": _("App Versions"), "title": _("App Versions"),
"icon": "system_update", "icon": "system_update",
"link": lambda request: admin_url_generator(request, "api_appversion_changelist"), "link": lambda request: admin_url_generator(request, "api_appversion_changelist"),
}, },
],
},
{
"title": "",
"items": [
{ {
"title": _("Global Preferences"), "title": _("Global Preferences"),
"icon": "settings",
"icon": "tune",
"link": lambda request: admin_url_generator(request, "dynamic_preferences_globalpreferencemodel_changelist"), "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_DEFAULT_FRAGMENT = "color-schemes"
UNFOLD_STUDIO_PERMISSION = lambda request: request.user.is_authenticated UNFOLD_STUDIO_PERMISSION = lambda request: request.user.is_authenticated
PLAUSIBLE_DOMAIN = env("PLAUSIBLE_DOMAIN") 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( sentry_sdk.init(
dsn="https://31aaeeb3a42f9a8c1b26272a0cb8ad3e@o4507991743725568.ingest.us.sentry.io/4511127356768256", 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, 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 %} {% load i18n unfold %}
{% block breadcrumbs %}{% endblock %} {% block breadcrumbs %}{% endblock %}
{% block title %} {% 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 %} {% endblock %}
{% block branding %} {% block branding %}
{% include "unfold/helpers/site_branding.html" %}
{% include "unfold/helpers/site_branding.html" %}
{% endblock %} {% endblock %}
{% block content %} {% 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 %}

279
utils/admin.py

@ -13,6 +13,24 @@ from django.utils.translation import get_language
from django.http import JsonResponse from django.http import JsonResponse
from django.views.decorators.http import require_POST 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 # Unfold Imports
from unfold.sites import UnfoldAdminSite from unfold.sites import UnfoldAdminSite
@ -36,40 +54,43 @@ def is_main_panel(request):
def admin_url_generator(request, url_name): def admin_url_generator(request, url_name):
""" """
Dynamically generates admin URLs based on the current active panel. 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): if is_dovoodi_panel(request):
namespace = 'dovoodi_admin' namespace = 'dovoodi_admin'
else: else:
namespace = 'imam_javad_admin' namespace = 'imam_javad_admin'
# 2. Construct the view name
full_view_name = f"{namespace}:{url_name}" full_view_name = f"{namespace}:{url_name}"
# 3. Try Django URL reversal
try: try:
return reverse(full_view_name) return reverse(full_view_name)
except Exception: except Exception:
return "#" 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): def variables(request):
return {"plausible_domain": getattr(settings, 'PLAUSIBLE_DOMAIN', '')} return {"plausible_domain": getattr(settings, 'PLAUSIBLE_DOMAIN', '')}
# Toggle sidebar view for Unfold compatibility
@require_POST @require_POST
def toggle_sidebar(request): def toggle_sidebar(request):
"""Toggle sidebar state for Unfold admin interface""" """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'}) return JsonResponse({'status': 'success'})
# --------------------------------------------------------- # ---------------------------------------------------------
@ -81,7 +102,6 @@ class LoginForm:
@staticmethod @staticmethod
def get_form(): def get_form():
# Import AuthenticationForm only when needed
from unfold.forms import AuthenticationForm from unfold.forms import AuthenticationForm
class CustomLoginForm(AuthenticationForm): class CustomLoginForm(AuthenticationForm):
@ -89,7 +109,6 @@ class LoginForm:
def __init__(self, request=None, *args, **kwargs): def __init__(self, request=None, *args, **kwargs):
super().__init__(request, *args, **kwargs) super().__init__(request, *args, **kwargs)
# Change the label of the username field to "Email"
self.fields["username"].label = "Email" self.fields["username"].label = "Email"
return CustomLoginForm return CustomLoginForm
@ -107,16 +126,13 @@ class FormulaAdminSite(UnfoldAdminSite):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Set login form after initialization to avoid circular import
self.login_form = LoginForm.get_form() self.login_form = LoginForm.get_form()
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
"""Override to ensure form is properly initialized"""
form = super().get_form(request, obj, **kwargs) form = super().get_form(request, obj, **kwargs)
return form return form
def each_context(self, request): def each_context(self, request):
"""Override to provide site-specific dropdown"""
context = super().each_context(request) context = super().each_context(request)
context["site_dropdown"] = [ context["site_dropdown"] = [
{ {
@ -145,13 +161,9 @@ class FormulaAdminSite(UnfoldAdminSite):
return custom_urls + urls return custom_urls + urls
def _get_colors(self, key, *args): 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": if key != "COLORS":
return super()._get_colors(key, *args) return super()._get_colors(key, *args)
# پالت رنگی سبز برای امام جواد
imam_javad_colors = { imam_javad_colors = {
"base": { "base": {
"50": "249 250 251", "50": "249 250 251",
@ -172,7 +184,7 @@ class FormulaAdminSite(UnfoldAdminSite):
"200": "167 247 216", "200": "167 247 216",
"300": "110 240 189", "300": "110 240 189",
"400": "37 213 152", "400": "37 213 152",
"500": "37 208 118", # #25D076 - سبز اصلی
"500": "37 208 118",
"600": "29 166 94", "600": "29 166 94",
"700": "25 136 80", "700": "25 136 80",
"800": "22 108 66", "800": "22 108 66",
@ -185,7 +197,7 @@ class FormulaAdminSite(UnfoldAdminSite):
"200": "153 246 228", "200": "153 246 228",
"300": "94 234 212", "300": "94 234 212",
"400": "45 212 191", "400": "45 212 191",
"500": "1 53 59", # #01353B - پس‌زمینه تیره
"500": "1 53 59",
"600": "1 43 48", "600": "1 43 48",
"700": "1 36 40", "700": "1 36 40",
"800": "1 30 34", "800": "1 30 34",
@ -201,7 +213,6 @@ class FormulaAdminSite(UnfoldAdminSite):
"important-dark": "255 255 255", "important-dark": "255 255 255",
}, },
} }
return imam_javad_colors return imam_javad_colors
class DovoodiAdminSite(UnfoldAdminSite): class DovoodiAdminSite(UnfoldAdminSite):
@ -213,16 +224,13 @@ class DovoodiAdminSite(UnfoldAdminSite):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Set login form after initialization to avoid circular import
self.login_form = LoginForm.get_form() self.login_form = LoginForm.get_form()
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
"""Override to ensure form is properly initialized"""
form = super().get_form(request, obj, **kwargs) form = super().get_form(request, obj, **kwargs)
return form return form
def each_context(self, request): def each_context(self, request):
"""Override to provide site-specific dropdown"""
context = super().each_context(request) context = super().each_context(request)
context["site_dropdown"] = [ context["site_dropdown"] = [
{ {
@ -251,51 +259,44 @@ class DovoodiAdminSite(UnfoldAdminSite):
return custom_urls + urls return custom_urls + urls
def _get_colors(self, key, *args): 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": if key != "COLORS":
return super()._get_colors(key, *args) return super()._get_colors(key, *args)
# پالت رنگی آبی-تیره برای داوودی (مطابق با فرانت)
dovoodi_colors = { dovoodi_colors = {
"base": { "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", "500": "107 114 128",
"600": "75 85 99", "600": "75 85 99",
"700": "55 65 81", "700": "55 65 81",
"800": "31 41 55", "800": "31 41 55",
"900": "17 24 39", # #111827
"900": "17 24 39",
"950": "3 7 18", "950": "3 7 18",
}, },
"primary": { "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", "900": "30 41 91",
"950": "15 20 45", "950": "15 20 45",
}, },
"secondary": { "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", "700": "0 15 17",
"800": "0 12 14", "800": "0 12 14",
"900": "0 8 10", "900": "0 8 10",
@ -310,21 +311,17 @@ class DovoodiAdminSite(UnfoldAdminSite):
"important-dark": "255 255 255", "important-dark": "255 255 255",
}, },
} }
return dovoodi_colors return dovoodi_colors
# Simple admin site placeholders that will be replaced after Django setup
class AdminSitePlaceholder(UnfoldAdminSite): class AdminSitePlaceholder(UnfoldAdminSite):
"""Placeholder that behaves like an admin site until Django is fully loaded""" """Placeholder that behaves like an admin site until Django is fully loaded"""
def __init__(self, site_class, name): def __init__(self, site_class, name):
# 1. Store config for lazy loading
self._site_class = site_class self._site_class = site_class
self._name = name self._name = name
self._real_instance = None 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_header = getattr(site_class, 'site_header', 'Django Admin')
self.site_title = getattr(site_class, 'site_title', 'Django Site') self.site_title = getattr(site_class, 'site_title', 'Django Site')
self.index_title = getattr(site_class, 'index_title', 'Site Administration') self.index_title = getattr(site_class, 'index_title', 'Site Administration')
@ -332,22 +329,17 @@ class AdminSitePlaceholder(UnfoldAdminSite):
def _get_real_instance(self): def _get_real_instance(self):
if self._real_instance is None: 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) 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_form = self._real_instance.login_form
self.login_template = self._real_instance.login_template 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']: for attr in ['site_header', 'site_title', 'index_title', 'site_subheader']:
if hasattr(self._real_instance, attr): if hasattr(self._real_instance, attr):
setattr(self, attr, getattr(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'): if hasattr(self, '_registry'):
for model, admin_class in self._registry.items(): for model, admin_class in self._registry.items():
self._real_instance.register(model, admin_class) self._real_instance.register(model, admin_class)
# Replace the global reference with the real instance
import sys import sys
current_module = sys.modules[__name__] current_module = sys.modules[__name__]
if hasattr(current_module, self._name): if hasattr(current_module, self._name):
@ -356,11 +348,9 @@ class AdminSitePlaceholder(UnfoldAdminSite):
return self._real_instance return self._real_instance
def __getattr__(self, name): 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) return getattr(self._get_real_instance(), name)
def get_form(self, request, obj=None, **kwargs): 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) return self._get_real_instance().get_form(request, obj, **kwargs)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
@ -377,37 +367,30 @@ class AdminSitePlaceholder(UnfoldAdminSite):
return self._get_real_instance().each_context(request) return self._get_real_instance().each_context(request)
def register(self, model_or_iterable, admin_class=None, **options): 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)): if isinstance(model_or_iterable, (list, tuple)):
for model in model_or_iterable: for model in model_or_iterable:
self.register(model, admin_class, **options) self.register(model, admin_class, **options)
else: else:
model = model_or_iterable model = model_or_iterable
if model in self._registry: if model in self._registry:
# If already registered, update the admin class
self._registry[model] = admin_class self._registry[model] = admin_class
else: else:
self._registry[model] = admin_class self._registry[model] = admin_class
# Also register with the real instance if it exists
if self._real_instance is not None: if self._real_instance is not None:
self._real_instance.register(model, admin_class, **options) self._real_instance.register(model, admin_class, **options)
# Create lazy-loading admin site instances that properly inherit from AdminSite
class LazyAdminSite(UnfoldAdminSite): class LazyAdminSite(UnfoldAdminSite):
def __init__(self, site_class, name): def __init__(self, site_class, name):
# Don't call super().__init__() to avoid creating the real instance yet
self._site_class = site_class self._site_class = site_class
self._name = name self._name = name
self._instance = None self._instance = None
# Set basic attributes that Django expects for isinstance checks
self.name = name self.name = name
def _force_init(self): def _force_init(self):
"""Force initialization immediately"""
if self._instance is None: if self._instance is None:
self._instance = self._site_class(name=self._name) self._instance = self._site_class(name=self._name)
# Copy all attributes to this instance
for attr in dir(self._instance): for attr in dir(self._instance):
if not attr.startswith('_') and attr not in ('register', 'unregister', 'is_registered'): if not attr.startswith('_') and attr not in ('register', 'unregister', 'is_registered'):
try: try:
@ -415,10 +398,8 @@ class LazyAdminSite(UnfoldAdminSite):
except (AttributeError, TypeError): except (AttributeError, TypeError):
pass pass
def _ensure_instance(self): def _ensure_instance(self):
"""Ensure the real instance exists"""
if self._instance is None: if self._instance is None:
self._instance = self._site_class(name=self._name) 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'] essential_attrs = ['site_header', 'site_title', 'index_title', 'site_url', 'login_template']
for attr in essential_attrs: for attr in essential_attrs:
if hasattr(self._instance, attr): if hasattr(self._instance, attr):
@ -437,23 +418,19 @@ class LazyAdminSite(UnfoldAdminSite):
@property @property
def urls(self): def urls(self):
"""Ensure URLs are accessed to create the instance"""
return self._get_instance().urls return self._get_instance().urls
def get_urls(self): def get_urls(self):
"""Delegate get_urls to ensure proper URL registration"""
return self._get_instance().get_urls() return self._get_instance().get_urls()
def register(self, model_or_iterable, admin_class=None, **options): def register(self, model_or_iterable, admin_class=None, **options):
"""Register models with the real admin site instance"""
self._ensure_instance() self._ensure_instance()
return self._instance.register(model_or_iterable, admin_class, **options) return self._instance.register(model_or_iterable, admin_class, **options)
# Create lazy admin site instances
project_admin_site = LazyAdminSite(FormulaAdminSite, 'imam_javad_admin') project_admin_site = LazyAdminSite(FormulaAdminSite, 'imam_javad_admin')
dovoodi_admin_site = LazyAdminSite(DovoodiAdminSite, 'dovoodi_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(): def replace_placeholders_with_real_sites():
global project_admin_site, dovoodi_admin_site global project_admin_site, dovoodi_admin_site
if isinstance(project_admin_site, AdminSitePlaceholder): if isinstance(project_admin_site, AdminSitePlaceholder):
@ -461,55 +438,105 @@ def replace_placeholders_with_real_sites():
if isinstance(dovoodi_admin_site, AdminSitePlaceholder): if isinstance(dovoodi_admin_site, AdminSitePlaceholder):
dovoodi_admin_site = DovoodiAdminSite(name='dovoodi_admin') 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): 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): def get_redirect_url(self, *args, **kwargs):
# دریافت زبان فعلی (پیش‌فرض: en)
language = get_language() or '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/' 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'<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"}]}),
},
],
"chart": json.dumps({
"labels": [WEEKDAYS[day % 7] for day in range(1, 28)],
"datasets": [
{"label": "Revenue", "data": positive, "backgroundColor": "var(--color-primary-700)"},
],
}),
}
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('<strong class="text-green-500 font-medium">Platform Users</strong>'),
},
{
"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