@ -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> 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