import os import hashlib from django.contrib import admin from django.contrib import messages from django import forms from django.utils.translation import gettext_lazy as _ from django.db import models from django.utils.html import format_html from django.shortcuts import redirect from django.urls import reverse_lazy from unfold.admin import ModelAdmin, StackedInline, TabularInline from unfold.decorators import action, display from unfold.contrib.forms.widgets import WysiwygWidget from unfold.sections import TableSection from unfold.contrib.filters.admin import ( ChoicesDropdownFilter, MultipleRelatedDropdownFilter, RangeDateFilter, RangeNumericFilter, 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 utils.admin import project_admin_site from utils.json_editor_field import JsonEditorWidget from apps.course.models import Course, Glossary, Attachment, CourseCategory, Participant, CourseGlossary, CourseAttachment from apps.course.models.lesson import Lesson, CourseLesson from apps.account.models import StudentUser, User from utils.schema import get_weekly_timing_schema, get_course_feature_schema class CourseTableSection(TableSection): verbose_name = _("Course Categories") related_name = "courses" height = 380 fields = [ "title", "status", "edit_link" ] def edit_link(self, instance): from django.utils.html import format_html return format_html( '' 'visibility' '', instance.id ) edit_link.short_description = _("Edit") class CourseCategoryAdmin(ModelAdmin): list_display = ('name', 'slug', 'course_count') search_fields = ('name',) # exclude = ('slug', ) list_sections = [CourseTableSection] fieldsets = ( (None, { 'fields': ('name', 'slug') }), ) @display(description=_("Courses")) def course_count(self, obj): count = obj.courses.all().count() return format_html( '{}', count ) class CourseForm(forms.ModelForm): class Meta: model = Course fields = '__all__' exclude = ('slug',) widgets = { 'timing': JsonEditorWidget(attrs={ 'schema': get_weekly_timing_schema(), 'title': _('Course Weekly Schedule'), }), 'features': JsonEditorWidget(attrs={ 'schema': get_course_feature_schema(), 'title': _('Course Features'), }), } help_texts = { 'status': 'If set to inactive, the course will not be displayed.', } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Make short_description required self.fields['short_description'].required = True class CourseAttachmentInline(StackedInline): model = CourseAttachment extra = 0 fields = ('attachment',) tab = True autocomplete_fields = ('attachment',) class CourseGlossaryInline(StackedInline): model = CourseGlossary fields = ('glossary',) extra = 0 tab = True show_change_link = True autocomplete_fields = ('glossary',) class CourseLessonInline(StackedInline): model = CourseLesson fields = ('lesson', 'title', 'is_active', 'priority',) extra = 0 tab = True show_change_link = True ordering_field = "priority" autocomplete_fields = ('lesson',) 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): model = Participant fields = ('student', 'joined_date', ) readonly_fields = ('joined_date', 'student') extra = 0 tab = True verbose_name = _("Participant") verbose_name_plural = _("Participants") show_change_link = True autocomplete_fields = ('student',) def get_queryset(self, request): qs = super().get_queryset(request) return qs.order_by('-joined_date') def has_add_permission(self, request, obj): return False def has_change_permission(self, request, obj=None): return False def has_delete_permission(self, request, obj=None): 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): student = forms.ModelChoiceField( queryset=User.objects.filter(is_active=True), label=_("Select Student"), widget=UnfoldAdminSelectWidget, required=True ) class CourseAdmin(DirectCourseAdmin): form = CourseForm inlines = [CourseLessonInline, CourseAttachmentInline, CourseGlossaryInline, ParticipantInline] list_display = ('display_header', 'category', 'display_professor', 'status', 'display_price', 'is_online') list_filter = [ ('status', ChoicesDropdownFilter), ('level', ChoicesDropdownFilter), 'is_online', 'is_free', ('category', MultipleRelatedDropdownFilter), ('price', RangeNumericFilter), ] save_as = True warn_unsaved_form = True # compressed_fields = True search_fields = ('id','title', 'description') exclude = ('slug', ) readonly_fields = ('final_price',) autocomplete_fields = ('category', 'professor',) list_filter_submit = True change_form_show_cancel_button = True radio_fields = { "video_type": admin.HORIZONTAL, "status": admin.HORIZONTAL, "level": admin.HORIZONTAL, } # formfield_overrides = { # models.TextField: { # "widget": WysiwygWidget, # }, # } conditional_fields = { 'price': "is_free == false", 'discount_percentage': "is_free == false", 'final_price': "is_free == false", 'online_link': "is_online", 'video_file': "video_type == 'video_file'", 'video_link': "video_type == 'youtube_link'", } fieldsets = ( (None, { 'fields': ('title', 'category', 'professor', 'thumbnail') }), (_('Status'), { 'fields': ('status', 'is_online', 'online_link'), }), (_('Course Details'), { 'fields': ('description', 'short_description', 'level', 'duration', 'lessons_count',), # 'classes': ['tab'], }), (_('Media'), { 'fields': ('video_type', 'video_file', 'video_link'), }), (_('Pricing'), { 'fields': ('is_free', 'price', 'discount_percentage', 'final_price'), }), (_('Timing & Features'), { 'fields': ('timing', 'features'), # 'classes': ['tab'], }), ) @display(description=_("Course"), header=True) def display_header(self, instance): from django.templatetags.static import static thumbnail_path = instance.thumbnail.url if instance.thumbnail else None return [ instance.title, # instance.short_description or _("No description"), None, None, { "path": thumbnail_path, "height": 40, "width": 60, "squared": True, "borderless": True, }, ] @display(description=_("Professor")) def display_professor(self, instance): return instance.professor.fullname @display(description=_("Price")) def display_price(self, instance): if instance.is_free: return format_html('{}', _("Free")) if instance.discount_percentage > 0: return format_html( '${}' '${}', instance.price, instance.final_price ) return format_html('${}', 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) def has_is_course_professor_permission(self, request, object_id=None): try: if request.user.is_staff: return True 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) except Exception as e: print(e) return False @action( description=_("Add Student to Course"), icon="person_add", permissions=[ "is_course_professor", ], ) 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) if not course: messages.error(request, _("Course not found")) return redirect(reverse("admin:course_course_changelist")) if request.method == 'POST': form = AddStudentForm(request.POST) if form.is_valid(): student = form.cleaned_data['student'] # Check if the student is already a participant if Participant.objects.filter(student=student, course=course).exists(): messages.warning(request, _(f"Student {student.fullname} is already enrolled in this course")) else: # اطمینان از اینکه کاربر نقش student دارد if not student.has_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")) else: form = AddStudentForm() return render( request, "course/add_student_form.html", { "form": form, "object": object, "title": _("Change detail action for {}").format(object), **self.admin_site.each_context(request), }, ) class GlossaryAdmin(AttachmentGlossaryBaseAdmin): list_display = ('title', 'description') search_fields = ('title', 'description') ordering = ('-id',) def is_used_in_professor_courses(self, user, obj): """آیا این glossary در دوره‌های استاد استفاده شده؟""" return obj.courseglossary_set.filter(course__professor=user).exists() def filter_by_professor_usage(self, user, queryset): """فیلتر کردن glossary ها بر اساس استفاده در دوره‌های استاد""" return queryset.filter(courseglossary__course__professor=user).distinct() class CourseGlossaryAdmin(CourseRelatedAdmin): list_display = ('course', 'glossary_title', 'glossary_description') list_filter = ('course',) search_fields = ('glossary__title', 'glossary__description', 'course__title') ordering = ('-id',) autocomplete_fields = ('course', 'glossary') @admin.display(description=_("Title")) def glossary_title(self, obj): return obj.glossary.title @admin.display(description=_("Description")) def glossary_description(self, obj): return obj.glossary.description class AttachmentAdminForm(forms.ModelForm): class Meta: model = Attachment fields = '__all__' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if 'file' in self.data or 'file' in self.files: file = self.files.get('file') if file: file.name = self._shorten_file_name(file.name) def _shorten_file_name(self, file_name): max_length = 100 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}" # ترکیب بخش اصلی و هش با پسوند return file_name class AttachmentAdmin(AttachmentGlossaryBaseAdmin): form = AttachmentAdminForm list_display = ('title', 'file', 'file_size') search_fields = ('title', 'file') def save_model(self, request, obj, form, change): if obj.file: obj.file_size = obj.file.size super().save_model(request, obj, form, change) def is_used_in_professor_courses(self, user, obj): """آیا این attachment در دوره‌های استاد استفاده شده؟""" return obj.courseattachment_set.filter(course__professor=user).exists() def filter_by_professor_usage(self, user, queryset): """فیلتر کردن attachment ها بر اساس استفاده در دوره‌های استاد""" return queryset.filter(courseattachment__course__professor=user).distinct() class CourseAttachmentAdmin(CourseRelatedAdmin): list_display = ('course', 'attachment_title', 'attachment_file', 'attachment_file_size') list_filter = ('course',) search_fields = ('attachment__title', 'course__title') autocomplete_fields = ('course', 'attachment') @admin.display(description=_("Title")) def attachment_title(self, obj): return obj.attachment.title @admin.display(description=_("File")) def attachment_file(self, obj): return obj.attachment.file @admin.display(description=_("File Size")) def attachment_file_size(self, obj): return obj.attachment.file_size # Register with both admin sites for autocomplete support from django.contrib import admin as django_admin django_admin.site.register(Course, CourseAdmin) django_admin.site.register(CourseCategory, CourseCategoryAdmin) django_admin.site.register(Glossary, GlossaryAdmin) django_admin.site.register(Attachment, AttachmentAdmin) # Register with the project admin site project_admin_site.register(Course, CourseAdmin) project_admin_site.register(CourseCategory, CourseCategoryAdmin) project_admin_site.register(Glossary, GlossaryAdmin) project_admin_site.register(CourseGlossary, CourseGlossaryAdmin) project_admin_site.register(Attachment, AttachmentAdmin) project_admin_site.register(CourseAttachment, CourseAttachmentAdmin) project_admin_site.register(Participant, ParticipantAdmin) # مدل‌های ProfessorUser و StudentUser قبلاً در admin های مربوطه ثبت شده‌اند