@ -7,12 +7,11 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.db import models
from django.utils.html import format_html
from django.utils.html import format_html
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.shortcuts import redirect , render
from django.urls import reverse_lazy , reverse
from unfold.admin import ModelAdmin , StackedInline , TabularInline
from unfold.admin import ModelAdmin , TabularInline
from unfold.decorators import action , display
from unfold.decorators import action , display
from unfold.contrib.forms.widgets import WysiwygWidget
from unfold.sections import TableSection
from unfold.sections import TableSection
from unfold.contrib.filters.admin import (
from unfold.contrib.filters.admin import (
ChoicesDropdownFilter ,
ChoicesDropdownFilter ,
@ -21,24 +20,14 @@ from unfold.contrib.filters.admin import (
RangeNumericFilter ,
RangeNumericFilter ,
TextFilter ,
TextFilter ,
)
)
from unfold.widgets import (
UnfoldAdminColorInputWidget ,
UnfoldAdminRadioSelectWidget ,
UnfoldAdminSelectWidget ,
UnfoldAdminSplitDateTimeWidget ,
UnfoldAdminTextInputWidget ,
)
from .professor_base import DirectCourseAdmin , CourseRelatedAdmin , AttachmentGlossaryBaseAdmin
from unfold.contrib.forms.widgets import ArrayWidget
from django.contrib.postgres.fields import ArrayField
from unfold.widgets import UnfoldAdminSelectWidget
from utils.admin import project_admin_site , dovoodi_admin_site
from .professor_base import DirectCourseAdmin , CourseRelatedAdmin , AttachmentGlossaryBaseAdmin
from utils.admin import project_admin_site , dovoodi_admin_site
from utils.json_editor_field import JsonEditorWidget
from utils.json_editor_field import JsonEditorWidget
from apps.course.models import Course , Glossary , Attachment , CourseCategory , Participant , CourseGlossary , CourseAttachment
from apps.course.models import Course , Glossary , Attachment , CourseCategory , Participant , CourseGlossary , CourseAttachment
from apps.course.models.lesson import Lesson , CourseLesson
from apps.course.models.lesson import Lesson , CourseLesson
from apps.account.models import StudentUser , User
from apps.account.models import StudentUser , User
from utils.schema import get_weekly_timing_schema , get_course_feature_schema
from utils.schema import get_weekly_timing_schema , get_course_feature_schema
@ -53,23 +42,18 @@ class CourseTableSection(TableSection):
]
]
def edit_link ( self , instance ) :
def edit_link ( self , instance ) :
from django.utils.html import format_html
return format_html (
return format_html (
' <a href= " /admin/course/course/{}/change/ " class= " leading-none " > '
' <a href= " /admin/course/course/{}/change/ " class= " leading-none " > '
' <span class= " material-symbols-outlined leading-none text-base-500 " >visibility</span> '
' <span class= " material-symbols-outlined leading-none text-base-500 " >visibility</span> '
' </a> ' ,
' </a> ' ,
instance . id
instance . id
)
)
edit_link . short_description = _ ( " Edit " )
edit_link . short_description = _ ( " Edit " )
class CourseCategoryAdmin ( ModelAdmin ) :
class CourseCategoryAdmin ( ModelAdmin ) :
list_display = ( ' name ' , ' slug ' , ' course_count ' )
list_display = ( ' name ' , ' slug ' , ' course_count ' )
search_fields = ( ' name ' , )
search_fields = ( ' name ' , )
# exclude = ('slug', )
list_sections = [ CourseTableSection ]
list_sections = [ CourseTableSection ]
fieldsets = (
fieldsets = (
( None , {
( None , {
@ -80,20 +64,14 @@ class CourseCategoryAdmin(ModelAdmin):
@display ( description = _ ( " Courses " ) )
@display ( description = _ ( " Courses " ) )
def course_count ( self , obj ) :
def course_count ( self , obj ) :
count = obj . courses . all ( ) . count ( )
count = obj . courses . all ( ) . count ( )
return format_html (
' <span class= " badge badge-primary " >{}</span> ' ,
count
)
return format_html ( ' <span class= " badge badge-primary " >{}</span> ' , count )
class CourseForm ( forms . ModelForm ) :
class CourseForm ( forms . ModelForm ) :
class Meta :
class Meta :
model = Course
model = Course
fields = ' __all__ '
fields = ' __all__ '
exclude = ( ' slug ' , )
exclude = ( ' slug ' , )
widgets = {
widgets = {
' timing ' : JsonEditorWidget ( attrs = {
' timing ' : JsonEditorWidget ( attrs = {
' schema ' : get_weekly_timing_schema ( ) ,
' schema ' : get_weekly_timing_schema ( ) ,
@ -105,14 +83,12 @@ class CourseForm(forms.ModelForm):
} ) ,
} ) ,
}
}
help_texts = {
help_texts = {
' status ' : ' If set to inactive, the course will not be displayed. ' ,
' status ' : _ ( ' If set to inactive, the course will not be displayed. ' ) ,
}
}
def __init__ ( self , * args , * * kwargs ) :
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
super ( ) . __init__ ( * args , * * kwargs )
# Make short_description required
self . fields [ ' short_description ' ] . required = True
self . fields [ ' short_description ' ] . required = True
# Make thumbnail required (show required star in add/change forms)
if ' thumbnail ' in self . fields :
if ' thumbnail ' in self . fields :
self . fields [ ' thumbnail ' ] . required = True
self . fields [ ' thumbnail ' ] . required = True
@ -121,122 +97,98 @@ class CourseForm(forms.ModelForm):
thumbnail = cleaned_data . get ( ' thumbnail ' )
thumbnail = cleaned_data . get ( ' thumbnail ' )
has_existing_thumbnail = bool ( getattr ( self . instance , ' thumbnail ' , None ) )
has_existing_thumbnail = bool ( getattr ( self . instance , ' thumbnail ' , None ) )
# Disallow clearing the existing thumbnail (must always have a value)
if thumbnail is False :
if thumbnail is False :
self . add_error ( ' thumbnail ' , _ ( ' This field is required and cannot be cleared. ' ) )
self . add_error ( ' thumbnail ' , _ ( ' This field is required and cannot be cleared. ' ) )
return cleaned_data
return cleaned_data
# On create or when no existing thumbnail, require uploading one
if ( thumbnail is None or thumbnail == ' ' ) and not has_existing_thumbnail :
if ( thumbnail is None or thumbnail == ' ' ) and not has_existing_thumbnail :
self . add_error ( ' thumbnail ' , _ ( ' This field is required. ' ) )
self . add_error ( ' thumbnail ' , _ ( ' This field is required. ' ) )
return cleaned_data
return cleaned_data
class CourseAttachmentInline ( StackedInline ) :
# --- WIDTH ENFORCEMENT & PLACEHOLDER TEXT FOR DROPDOWNS ---
class MinWidthInlineForm ( forms . ModelForm ) :
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
target_dropdown_fields = [ ' lesson ' , ' attachment ' , ' glossary ' , ' student ' ]
for field_name , field in self . fields . items ( ) :
if field_name in target_dropdown_fields :
if hasattr ( field . widget , ' attrs ' ) :
existing_class = field . widget . attrs . get ( ' class ' , ' ' )
field . widget . attrs [ ' class ' ] = f " {existing_class} min-w-[250px] w-full "
existing_style = field . widget . attrs . get ( ' style ' , ' ' )
field . widget . attrs [ ' style ' ] = f " {existing_style} min-width: 250px; width: 250px; "
# 🔔 Add the custom placeholder text
if hasattr ( field , ' empty_label ' ) :
field . empty_label = _ ( " Select a value " )
class CourseAttachmentInline ( TabularInline ) :
model = CourseAttachment
model = CourseAttachment
extra = 0
form = MinWidthInlineForm
extra = 1 # Show 1 empty dropdown by default
fields = ( ' attachment ' , )
fields = ( ' attachment ' , )
tab = True
tab = True
autocomplete_fields = ( ' attachment ' , )
# Removed autocomplete_fields to restore Unfold UI
verbose_name = _ ( " Course Attachment " )
verbose_name_plural = _ ( " Course Attachments " )
class CourseGlossaryInline ( StackedInline ) :
class CourseGlossaryInline ( Tabular Inline) :
model = CourseGlossary
model = CourseGlossary
form = MinWidthInlineForm
fields = ( ' glossary ' , )
fields = ( ' glossary ' , )
extra = 0
extra = 1 # Show 1 empty dropdown by default
tab = True
tab = True
tab_id = " glossaries_tab "
show_change_link = True
show_change_link = True
autocomplete_fields = ( ' glossary ' , )
# Removed autocomplete_fields to restore Unfold UI
class CourseLessonInline ( StackedInline ) :
class CourseLessonInline ( Tabular Inline) :
model = CourseLesson
model = CourseLesson
form = MinWidthInlineForm
fields = ( ' lesson ' , ' title ' , ' is_active ' , ' priority ' , )
fields = ( ' lesson ' , ' title ' , ' is_active ' , ' priority ' , )
extra = 0
extra = 1 # Show 1 empty dropdown by default
tab = True
tab = True
tab_id = " lessons_tab "
show_change_link = True
show_change_link = True
ordering_field = " priority "
ordering_field = " priority "
autocomplete_fields = ( ' lesson ' , )
# Removed autocomplete_fields to restore Unfold UI
class ParticipantAdmin ( ModelAdmin ) :
list_display = ( ' student_name ' , ' course_title ' , ' joined_date ' , )
list_filter = (
( ' course ' , MultipleRelatedDropdownFilter ) ,
)
search_fields = ( ' student__email ' , ' student__fullname ' , ' course__title ' )
readonly_fields = ( ' joined_date ' , )
autocomplete_fields = ( ' student ' , ' course ' )
fieldsets = (
( None , {
' fields ' : ( ' student ' , ' course ' , )
} ) ,
( _ ( ' Enrollment Details ' ) , {
' fields ' : ( ' joined_date ' , ' last_activity ' , ' progress ' )
} ) ,
)
@display ( description = _ ( " Student " ) , header = True )
def student_name ( self , instance : StudentUser ) :
from django.templatetags.static import static
# Get avatar image path - use user's avatar if available, otherwise use default
avatar_path = instance . student . avatar . url if instance . student . avatar else static ( " images/reading(1).png " )
return [
instance . student . fullname ,
None ,
None ,
{
" path " : avatar_path ,
" height " : 30 ,
" width " : 36 ,
" borderless " : True ,
# "squared": True,
} ,
]
@admin.display ( description = _ ( " Course " ) )
def course_title ( self , obj ) :
if obj . course :
return obj . course . title
return " - "
class ParticipantInline ( TabularInline ) :
class ParticipantInline ( TabularInline ) :
model = Participant
model = Participant
fields = ( ' student ' , ' joined_date ' , )
form = MinWidthInlineForm
fields = ( ' student ' , ' joined_date ' , )
readonly_fields = ( ' joined_date ' , ' student ' )
readonly_fields = ( ' joined_date ' , ' student ' )
extra = 0
extra = 0 # Remains 0 because users are added via action buttons
tab = True
tab = True
verbose_name = _ ( " Participant " )
verbose_name_plural = _ ( " Participants " )
tab_id = " participants_tab "
verbose_name = _ ( " Recent Participant " )
verbose_name_plural = _ ( " Recent Participants (Latest 10) " )
show_change_link = True
show_change_link = True
autocomplete_fields = ( ' student ' , )
def get_queryset ( self , request ) :
def get_queryset ( self , request ) :
qs = super ( ) . get_queryset ( request )
qs = super ( ) . get_queryset ( request )
return qs . order_by ( ' -joined_date ' )
object_id = request . resolver_match . kwargs . get ( ' object_id ' )
if object_id :
latest_ids = list ( qs . filter ( course_id = object_id ) . order_by ( ' -joined_date ' ) . values_list ( ' id ' , flat = True ) [ : 10 ] )
return qs . filter ( id__in = latest_ids ) . order_by ( ' -joined_date ' )
return qs . none ( )
def has_add_permission ( self , request , obj ) :
def has_add_permission ( self , request , obj ) :
return False
return False
def has_change_permission ( self , request , obj = None ) :
def has_change_permission ( self , request , obj = None ) :
return False
return False
def has_delete_permission ( self , request , obj = None ) :
def has_delete_permission ( self , request , obj = None ) :
return False
return False
from django.urls import reverse
from django import forms
from django.shortcuts import render , redirect
from django.contrib import messages
from unfold.widgets import UnfoldAdminSelectWidget
class AddStudentForm ( forms . Form ) :
class AddStudentForm ( forms . Form ) :
student = forms . ModelChoiceField (
student = forms . ModelChoiceField (
queryset = User . objects . filter ( is_active = True , email__isnull = False ) ,
queryset = User . objects . filter ( is_active = True , email__isnull = False ) ,
@ -249,6 +201,7 @@ class AddStudentForm(forms.Form):
class CourseAdmin ( DirectCourseAdmin ) :
class CourseAdmin ( DirectCourseAdmin ) :
form = CourseForm
form = CourseForm
inlines = [ CourseLessonInline , CourseAttachmentInline , CourseGlossaryInline , ParticipantInline ]
inlines = [ CourseLessonInline , CourseAttachmentInline , CourseGlossaryInline , ParticipantInline ]
list_display = ( ' display_header ' , ' category ' , ' display_professor ' , ' status ' , ' display_price ' , ' is_online ' )
list_display = ( ' display_header ' , ' category ' , ' display_professor ' , ' status ' , ' display_price ' , ' is_online ' )
list_filter = [
list_filter = [
( ' status ' , ChoicesDropdownFilter ) ,
( ' status ' , ChoicesDropdownFilter ) ,
@ -260,23 +213,19 @@ class CourseAdmin(DirectCourseAdmin):
]
]
save_as = True
save_as = True
warn_unsaved_form = True
warn_unsaved_form = True
# compressed_fields = True
search_fields = ( ' id ' , ' title ' , ' description ' )
search_fields = ( ' id ' , ' title ' , ' description ' )
exclude = ( ' slug ' , )
exclude = ( ' slug ' , )
readonly_fields = ( ' final_price ' , )
readonly_fields = ( ' final_price ' , )
autocomplete_fields = ( ' category ' , ' professor ' , )
autocomplete_fields = ( ' category ' , ' professor ' , )
list_filter_submit = True
list_filter_submit = True
change_form_show_cancel_button = True
change_form_show_cancel_button = True
radio_fields = {
radio_fields = {
" video_type " : admin . HORIZONTAL ,
" video_type " : admin . HORIZONTAL ,
" status " : admin . HORIZONTAL ,
" status " : admin . HORIZONTAL ,
" level " : admin . HORIZONTAL ,
" level " : admin . HORIZONTAL ,
}
}
# formfield_overrides = {
# models.TextField: {
# "widget": WysiwygWidget,
# },
# }
conditional_fields = {
conditional_fields = {
' price ' : " is_free == false " ,
' price ' : " is_free == false " ,
' discount_percentage ' : " is_free == false " ,
' discount_percentage ' : " is_free == false " ,
@ -288,27 +237,33 @@ class CourseAdmin(DirectCourseAdmin):
fieldsets = (
fieldsets = (
( None , {
( None , {
' fields ' : ( ' title ' , ' category ' , ' professor ' , ' thumbnail ' )
} ) ,
( _ ( ' Status ' ) , {
' fields ' : ( ' status ' , ' is_online ' , ' online_link ' ) ,
' fields ' : ( ' title ' , ' category ' , ' professor ' , ' thumbnail ' , ' description ' , ' short_description ' )
} ) ,
} ) ,
( _ ( ' Chat Settings ' ) , {
' fields ' : ( ' is_group_chat_locked ' , ' is_professor_chat_locked ' ) ,
} ) ,
( _ ( ' Course Details ' ) , {
' fields ' : ( ' description ' , ' short_description ' , ' level ' , ' duration ' , ' lessons_count ' , ) ,
# 'classes': ['tab'],
( _ ( ' Settings & Status ' ) , {
' fields ' : (
( ' status ' , ' level ' ) ,
( ' duration ' , ' lessons_count ' ) ,
( ' is_group_chat_locked ' , ' is_professor_chat_locked ' ) ,
( ' is_online ' , ' online_link ' )
) ,
' classes ' : [ ' tab ' ] ,
} ) ,
} ) ,
( _ ( ' Media ' ) , {
( _ ( ' Media ' ) , {
' fields ' : ( ' video_type ' , ' video_file ' , ' video_link ' ) ,
' fields ' : (
( ' video_type ' , ' video_file ' , ' video_link ' ) ,
) ,
' classes ' : [ ' tab ' ] ,
} ) ,
} ) ,
( _ ( ' Pricing ' ) , {
( _ ( ' Pricing ' ) , {
' fields ' : ( ' is_free ' , ' price ' , ' discount_percentage ' , ' final_price ' ) ,
' fields ' : (
( ' is_free ' , ' price ' ) ,
( ' discount_percentage ' , ' final_price ' )
) ,
' classes ' : [ ' tab ' ] ,
} ) ,
} ) ,
( _ ( ' Timing & Features ' ) , {
( _ ( ' Advanced Configuration ' ) , {
' fields ' : ( ' timing ' , ' features ' ) ,
' fields ' : ( ' timing ' , ' features ' ) ,
# 'classes': ['tab'],
' classes ': [ ' tab '] ,
} ) ,
} ) ,
)
)
@ -316,14 +271,10 @@ class CourseAdmin(DirectCourseAdmin):
@display ( description = _ ( " Course " ) , header = True )
@display ( description = _ ( " Course " ) , header = True )
def display_header ( self , instance ) :
def display_header ( self , instance ) :
from django.templatetags.static import static
from django.templatetags.static import static
thumbnail_path = instance . thumbnail . url if instance . thumbnail else None
thumbnail_path = instance . thumbnail . url if instance . thumbnail else None
return [
return [
instance . title ,
instance . title ,
# instance.short_description or _("No description"),
None ,
None ,
None , None ,
{
{
" path " : thumbnail_path ,
" path " : thumbnail_path ,
" height " : 40 ,
" height " : 40 ,
@ -349,90 +300,44 @@ class CourseAdmin(DirectCourseAdmin):
instance . price ,
instance . price ,
instance . final_price
instance . final_price
)
)
return format_html ( ' <span>${}</span> ' , instance . final_price )
return format_html ( ' <span>${}</span> ' , instance . final_price )
actions_row = [
" view_course_lessons " ,
" add_student_to_course "
]
actions_detail = [ ' add_student_to_course ' , ]
@action (
description = _ ( " View Lessons " ) ,
icon = " menu_book " ,
url_path = " actions-row-custom-url " ,
permissions = [
" is_course_professor " ,
] ,
)
def view_course_lessons ( self , request , object_id ) :
""" Navigate to the list of lessons for this course. """
course = self . get_object ( request , object_id )
if not course :
messages . error ( request , _ ( " Course not found " ) )
return redirect ( request . META . get ( " HTTP_REFERER " ) or reverse_lazy ( " admin:course_course_changelist " ) )
# Redirect to the lesson list filtered by this course
from django.urls import reverse
url = f " {reverse( ' admin:course_lesson_changelist ' )}?course__id__exact={course.id} "
return redirect ( url )
actions_row = [ " add_student_to_course " ]
actions_detail = [ ' add_student_to_course ' , ' manage_all_students ' ]
def has_is_course_professor_permission ( self , request , object_id = None ) :
def has_is_course_professor_permission ( self , request , object_id = None ) :
try :
try :
if request . user . is_staff :
if request . user . is_staff :
return True
return True
course = self . get_object ( request , object_id )
course = self . get_object ( request , object_id )
# Check if the current user can manage this course
return course and request . user . can_manage_course ( course )
return course and request . user . can_manage_course ( course )
except Exception as e :
except Exception as e :
print ( e )
return False
return False
@action (
@action (
description = _ ( " Add Student to Course " ) ,
description = _ ( " Add Student " ) ,
icon = " person_add " ,
icon = " person_add " ,
permissions = [
" is_course_professor " ,
] ,
permissions = [ " is_course_professor " ] ,
)
)
def add_student_to_course ( self , request , object_id ) :
def add_student_to_course ( self , request , object_id ) :
""" Add a student to this course as a participant. """
course = self . get_object ( request , object_id )
course = self . get_object ( request , object_id )
if not course :
if not course :
messages . error ( request , _ ( " Course not found " ) )
messages . error ( request , _ ( " Course not found " ) )
return redirect ( reverse ( " admin: course_course_changelist" ) )
return redirect ( admin_url_generator ( request , " course_course_changelist " ) )
if request . method == ' POST ' :
if request . method == ' POST ' :
form = AddStudentForm ( request . POST )
form = AddStudentForm ( request . POST )
if form . is_valid ( ) :
if form . is_valid ( ) :
student = form . cleaned_data [ ' student ' ]
student = form . cleaned_data [ ' student ' ]
# Check if the student is already a participant
if Participant . objects . filter ( student = student , course = course ) . exists ( ) :
if Participant . objects . filter ( student = student , course = course ) . exists ( ) :
messages . warning ( request , _ ( f " Student {student.fullname } is already enrolled in this course " ) )
messages . warning ( request , _ ( " Student {} is already enrolled in this course " ) . format ( student . fullname ) )
else :
else :
# اطمینان از اینکه کاربر نقش student دارد
if not student . has_role ( ' student ' ) :
if not student . has_role ( ' student ' ) :
student . add_role ( ' student ' )
student . add_role ( ' student ' )
# Create a new participant
Participant . objects . create (
student = student ,
course = course ,
)
messages . success (
request ,
_ ( f " Student {student.fullname} has been successfully added to {course.title} " )
)
return redirect ( reverse ( " admin:course_course_changelist " ) )
Participant . objects . create ( student = student , course = course )
messages . success ( request , _ ( " Student {} has been successfully added to {} " ) . format ( student . fullname , course . title ) )
return redirect ( admin_url_generator ( request , " course_course_changelist " ) )
else :
else :
form = AddStudentForm ( )
form = AddStudentForm ( )
@ -442,11 +347,25 @@ class CourseAdmin(DirectCourseAdmin):
{
{
" form " : form ,
" form " : form ,
" object " : object ,
" object " : object ,
" title " : _ ( " Change detail action for {}" ) . format ( object ) ,
" title " : _ ( " Add Student to {}" ) . format ( course . title ) ,
* * self . admin_site . each_context ( request ) ,
* * self . admin_site . each_context ( request ) ,
} ,
} ,
)
)
@action (
description = _ ( " Manage All Students " ) ,
icon = " groups " ,
permissions = [ " is_course_professor " ] ,
)
def manage_all_students ( self , request , object_id ) :
course = self . get_object ( request , object_id )
if not course :
messages . error ( request , _ ( " Course not found " ) )
return redirect ( admin_url_generator ( request , " course_course_changelist " ) )
base_url = admin_url_generator ( request , " course_participant_changelist " )
url = f " {base_url}?course__id__exact={object_id} "
return redirect ( url )
class GlossaryAdmin ( AttachmentGlossaryBaseAdmin ) :
class GlossaryAdmin ( AttachmentGlossaryBaseAdmin ) :
list_display = ( ' title ' , ' description ' )
list_display = ( ' title ' , ' description ' )
@ -454,11 +373,9 @@ class GlossaryAdmin(AttachmentGlossaryBaseAdmin):
ordering = ( ' -id ' , )
ordering = ( ' -id ' , )
def is_used_in_professor_courses ( self , user , obj ) :
def is_used_in_professor_courses ( self , user , obj ) :
""" آیا این glossary در دورههای استاد استفاده شده؟ """
return obj . courseglossary_set . filter ( course__professor = user ) . exists ( )
return obj . courseglossary_set . filter ( course__professor = user ) . exists ( )
def filter_by_professor_usage ( self , user , queryset ) :
def filter_by_professor_usage ( self , user , queryset ) :
""" فیلتر کردن glossary ها بر اساس استفاده در دورههای استاد """
return queryset . filter ( courseglossary__course__professor = user ) . distinct ( )
return queryset . filter ( courseglossary__course__professor = user ) . distinct ( )
@ -469,6 +386,10 @@ class CourseGlossaryAdmin(CourseRelatedAdmin):
ordering = ( ' -id ' , )
ordering = ( ' -id ' , )
autocomplete_fields = ( ' course ' , ' glossary ' )
autocomplete_fields = ( ' course ' , ' glossary ' )
# 🔔 REMOVES FROM "ALL APPLICATIONS" MODAL
def has_module_permission ( self , request ) :
return False
@admin.display ( description = _ ( " Title " ) )
@admin.display ( description = _ ( " Title " ) )
def glossary_title ( self , obj ) :
def glossary_title ( self , obj ) :
return obj . glossary . title
return obj . glossary . title
@ -485,28 +406,21 @@ class AttachmentAdminForm(forms.ModelForm):
def __init__ ( self , * args , * * kwargs ) :
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
super ( ) . __init__ ( * args , * * kwargs )
if ' file ' in self . data or ' file ' in self . files :
if ' file ' in self . data or ' file ' in self . files :
file = self . files . get ( ' file ' )
file = self . files . get ( ' file ' )
if file :
if file :
file . name = self . _shorten_file_name ( file . name )
file . name = self . _shorten_file_name ( file . name )
def _shorten_file_name ( self , file_name ) :
def _shorten_file_name ( self , file_name ) :
max_length = 100
max_length = 100
if len ( file_name ) > max_length :
if len ( file_name ) > max_length :
base_name , ext = os . path . splitext ( file_name ) # جدا کردن نام و پسوند
allowed_length = max_length - len ( ext ) # طول مجاز نام بدون پسوند
# 80٪ از نام اصلی و 20٪ هش
base_length = int ( allowed_length * 0.8 ) # 80٪ از طول مجاز
hash_length = allowed_length - base_length # 20٪ از طول مجاز
base_part = base_name [ : base_length ] # 80٪ اول نام اصلی
hash_part = hashlib . sha256 ( base_name . encode ( ' utf-8 ' ) ) . hexdigest ( ) [ : hash_length ] # 20٪ هش
return f " {base_part}{hash_part}{ext} " # ترکیب بخش اصلی و هش با پسوند
base_name , ext = os . path . splitext ( file_name )
allowed_length = max_length - len ( ext )
base_length = int ( allowed_length * 0.8 )
hash_length = allowed_length - base_length
base_part = base_name [ : base_length ]
hash_part = hashlib . sha256 ( base_name . encode ( ' utf-8 ' ) ) . hexdigest ( ) [ : hash_length ]
return f " {base_part}{hash_part}{ext} "
return file_name
return file_name
@ -521,11 +435,9 @@ class AttachmentAdmin(AttachmentGlossaryBaseAdmin):
super ( ) . save_model ( request , obj , form , change )
super ( ) . save_model ( request , obj , form , change )
def is_used_in_professor_courses ( self , user , obj ) :
def is_used_in_professor_courses ( self , user , obj ) :
""" آیا این attachment در دورههای استاد استفاده شده؟ """
return obj . courseattachment_set . filter ( course__professor = user ) . exists ( )
return obj . courseattachment_set . filter ( course__professor = user ) . exists ( )
def filter_by_professor_usage ( self , user , queryset ) :
def filter_by_professor_usage ( self , user , queryset ) :
""" فیلتر کردن attachment ها بر اساس استفاده در دورههای استاد """
return queryset . filter ( courseattachment__course__professor = user ) . distinct ( )
return queryset . filter ( courseattachment__course__professor = user ) . distinct ( )
@ -535,6 +447,10 @@ class CourseAttachmentAdmin(CourseRelatedAdmin):
search_fields = ( ' attachment__title ' , ' course__title ' )
search_fields = ( ' attachment__title ' , ' course__title ' )
autocomplete_fields = ( ' course ' , ' attachment ' )
autocomplete_fields = ( ' course ' , ' attachment ' )
# 🔔 REMOVES FROM "ALL APPLICATIONS" MODAL
def has_module_permission ( self , request ) :
return False
@admin.display ( description = _ ( " Title " ) )
@admin.display ( description = _ ( " Title " ) )
def attachment_title ( self , obj ) :
def attachment_title ( self , obj ) :
return obj . attachment . title
return obj . attachment . title
@ -548,18 +464,62 @@ class CourseAttachmentAdmin(CourseRelatedAdmin):
return obj . attachment . file_size
return obj . attachment . file_size
class ParticipantAdmin ( ModelAdmin ) :
list_display = ( ' student_name ' , ' course_title ' , ' joined_date ' , )
list_filter = (
( ' course ' , MultipleRelatedDropdownFilter ) ,
)
search_fields = ( ' student__email ' , ' student__fullname ' , ' course__title ' )
readonly_fields = ( ' joined_date ' , )
autocomplete_fields = ( ' student ' , ' course ' )
fieldsets = (
( None , {
' fields ' : ( ' student ' , ' course ' , ' is_active ' )
} ) ,
( _ ( ' Enrollment Details ' ) , {
' fields ' : ( ' joined_date ' , ' unread_messages_count ' )
} ) ,
)
@display ( description = _ ( " Student " ) , header = True )
def student_name ( self , instance ) :
from django.templatetags.static import static
avatar_path = instance . student . avatar . url if getattr ( instance . student , ' avatar ' , None ) else static ( " images/reading(1).png " )
return [
instance . student . fullname ,
None ,
None ,
{
" path " : avatar_path ,
" height " : 30 ,
" width " : 36 ,
" borderless " : True ,
} ,
]
@display ( description = _ ( " Course " ) )
def course_title ( self , obj ) :
if obj . course :
return obj . course . title
return " - "
# =========================================================
# REGISTRATIONS
# =========================================================
from django.contrib import admin as django_admin
from django.contrib import admin as django_admin
try :
try :
django_admin . site . register ( Course , CourseAdmin )
django_admin . site . register ( Course , CourseAdmin )
django_admin . site . register ( CourseCategory , CourseCategoryAdmin )
django_admin . site . register ( CourseCategory , CourseCategoryAdmin )
django_admin . site . register ( Glossary , GlossaryAdmin )
django_admin . site . register ( Glossary , GlossaryAdmin )
django_admin . site . register ( Attachment , AttachmentAdmin )
django_admin . site . register ( Attachment , AttachmentAdmin )
except admin . sites . AlreadyRegistered :
except django_ admin. sites . AlreadyRegistered :
pass
pass
# =========================================================
# 2. REGISTER TO PROJECT ADMIN (The Full Admin Panel)
# =========================================================
project_admin_site . register ( Course , CourseAdmin )
project_admin_site . register ( Course , CourseAdmin )
project_admin_site . register ( CourseCategory , CourseCategoryAdmin )
project_admin_site . register ( CourseCategory , CourseCategoryAdmin )
project_admin_site . register ( Glossary , GlossaryAdmin )
project_admin_site . register ( Glossary , GlossaryAdmin )
@ -568,27 +528,11 @@ project_admin_site.register(Attachment, AttachmentAdmin)
project_admin_site . register ( CourseAttachment , CourseAttachmentAdmin )
project_admin_site . register ( CourseAttachment , CourseAttachmentAdmin )
project_admin_site . register ( Participant , ParticipantAdmin )
project_admin_site . register ( Participant , ParticipantAdmin )
# =========================================================
# 3. REGISTER TO DOVOODI ADMIN (The "Ghost" Fix)
# =========================================================
# IMPORTANT: Do NOT inherit from CourseAdmin. Inherit from ModelAdmin directly.
# This prevents dragging in 'inlines' and 'autocomplete_fields' that cause errors.
class HiddenCourseAdmin ( ModelAdmin ) :
class HiddenCourseAdmin ( ModelAdmin ) :
# We only need search_fields so the Autocomplete box works
search_fields = ( ' title ' , ' id ' )
search_fields = ( ' title ' , ' id ' )
# No inlines
# No autocomplete_fields
# No fieldsets
# Hide from the Menu
def has_module_permission ( self , request ) :
def has_module_permission ( self , request ) :
return False
return False
# Disable all permissions
def has_add_permission ( self , request ) :
def has_add_permission ( self , request ) :
return False
return False
def has_change_permission ( self , request , obj = None ) :
def has_change_permission ( self , request , obj = None ) :
@ -596,4 +540,4 @@ class HiddenCourseAdmin(ModelAdmin):
def has_delete_permission ( self , request , obj = None ) :
def has_delete_permission ( self , request , obj = None ) :
return False
return False
dovoodi_admin_site . register ( Course , HiddenCourseAdmin )
dovoodi_admin_site . register ( Course , HiddenCourseAdmin )