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)