You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

538 lines
18 KiB

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(
'<a href="/admin/course/course/{}/change/" class="leading-none">'
'<span class="material-symbols-outlined leading-none text-base-500">visibility</span>'
'</a>',
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(
'<span class="badge badge-primary">{}</span>',
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('<span class="text-green-600 font-medium">{}</span>', _("Free"))
if instance.discount_percentage > 0:
return format_html(
'<span class="line-through text-gray-400 mr-2">${}</span>'
'<span class="text-green-600 font-medium">${}</span>',
instance.price,
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)
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 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 های مربوطه ثبت شده‌اند