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 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 from apps.course.models.lesson import Lesson from apps.account.models import StudentUser 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.', } class AttachmentInline(TabularInline): model = Attachment extra = 0 fields = ('title', 'file', 'file_size') tab = True def save_model(self, request, obj, form, change): if obj.file: obj.file_size = obj.file.size super().save_model(request, obj, form, change) class GlossaryInline(StackedInline): model = Glossary fields = ('title', 'description') extra = 0 tab = True show_change_link = True class LessonInline(StackedInline): model = Lesson fields = ('title', 'is_active', 'duration', 'content_type', 'content_file', 'video_link', 'priority',) extra = 0 tab = True show_change_link = True ordering_field = "priority" conditional_fields = { 'content_file': "content_type == 'video_file'", 'video_link': "content_type == 'youtube_link'", } 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=StudentUser.objects.filter(is_active=True), label=_("Select Student"), widget=UnfoldAdminSelectWidget, required=True ) class CourseAdmin(ModelAdmin): form = CourseForm inlines = [LessonInline, AttachmentInline, GlossaryInline, 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, } show_facets = admin.ShowFacets.ALLOW 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, { "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 is the professor of this course return course and hasattr(request.user, 'professor') and course.professor_id == request.user.id 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: # 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(ModelAdmin): list_display = ('title', 'course', 'description') list_filter = ('course',) search_fields = ('title', 'description', 'course__title') ordering = ('-id',) 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(ModelAdmin): form = AttachmentAdminForm list_display = ('title', 'course', 'file', 'file_size') list_filter = ('course',) search_fields = ('title', 'file', 'course__title') def save_model(self, request, obj, form, change): if obj.file: obj.file_size = obj.file.size super().save_model(request, obj, form, change) # 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(Attachment, AttachmentAdmin) project_admin_site.register(Participant, ParticipantAdmin)