Browse Source

multilanguage setup for course app added , old values set to the language map schema as default english values .

master
Mohsen Taba 2 months ago
parent
commit
7c0ccbd06e
  1. 2
      apps/course/admin/course.py
  2. 24
      apps/course/admin/lesson.py
  3. 247
      apps/course/migrations/0008_remove_course_course_cour_status_57ffd9_idx_and_more.py
  4. 130
      apps/course/models/course.py
  5. 28
      apps/course/models/lesson.py
  6. 31
      apps/course/serializers/admin.py
  7. 128
      apps/course/serializers/course.py
  8. 35
      apps/course/serializers/lesson.py
  9. 11
      apps/course/signals.py
  10. 6
      apps/course/views/admin.py
  11. 38
      apps/course/views/course.py
  12. 4
      apps/course/views/lesson.py
  13. 6
      apps/course/views/live_session.py
  14. 5
      apps/course/views/participant.py

2
apps/course/admin/course.py

@ -276,8 +276,6 @@ class CourseAdmin(DirectCourseAdmin):
radio_fields = {
"video_type": admin.HORIZONTAL,
"status": admin.HORIZONTAL,
"level": admin.HORIZONTAL,
}
conditional_fields = {

24
apps/course/admin/lesson.py

@ -32,19 +32,9 @@ class CourseLessonForm(forms.ModelForm):
class LessonAdmin(ModelAdmin):
form = LessonForm
list_display = ('title', 'display_duration', 'content_type')
list_filter = (
('content_type', ChoicesDropdownFilter),
)
search_fields = ('title',)
ordering = ('title',)
list_filter_submit = True
radio_fields = {
"content_type": admin.HORIZONTAL,
}
conditional_fields = {
'content_file': "content_type == 'video_file'",
'video_link': "content_type == 'youtube_link'",
}
fieldsets = (
(None, {
@ -55,20 +45,6 @@ class LessonAdmin(ModelAdmin):
}),
)
def get_form(self, request, obj=None, change=False, **kwargs):
form = super().get_form(request, obj, change, **kwargs)
form.base_fields["content_type"].widget = UnfoldAdminRadioSelectWidget(
choices=Lesson.ContentTypeChoices.choices,
radio_style=admin.HORIZONTAL,
attrs={
"class": "radio-inline flex gap-4 p-2 rounded-lg bg-gray-50 shadow-sm",
"option_class": "flex items-center p-2 rounded-md hover:bg-white hover:shadow-sm transition-all duration-200",
"label_class": "ml-2 font-medium text-gray-700 cursor-pointer",
"input_class": "form-radio h-5 w-5 text-blue-600 transition duration-150 ease-in-out cursor-pointer",
},
)
return form
@display(description=_("Duration"))
def display_duration(self, obj):
return format_html('<span class="badge badge-info">{} {}</span>', obj.duration, _("min"))

247
apps/course/migrations/0008_remove_course_course_cour_status_57ffd9_idx_and_more.py

@ -0,0 +1,247 @@
# Generated by Django 5.2.12 on 2026-06-03 15:30
import json
from django.db import migrations, models
def convert_to_json_format(apps, schema_editor):
Attachment = apps.get_model('course', 'Attachment')
Course = apps.get_model('course', 'Course')
CourseCategory = apps.get_model('course', 'CourseCategory')
CourseChapter = apps.get_model('course', 'CourseChapter')
CourseLesson = apps.get_model('course', 'CourseLesson')
Glossary = apps.get_model('course', 'Glossary')
Lesson = apps.get_model('course', 'Lesson')
def wrap_val(val):
if not val:
return "[]"
if isinstance(val, str):
stripped = val.strip()
if stripped.startswith('[') and stripped.endswith(']'):
try:
# Already JSON
json.loads(val)
return val
except Exception:
pass
# Wrap as JSON string
return json.dumps([{"title": val, "language_code": "en"}], ensure_ascii=False)
return json.dumps([{"title": str(val), "language_code": "en"}], ensure_ascii=False)
# 1. Attachment
for obj in Attachment.objects.all():
obj.title = wrap_val(obj.title)
obj.save()
# 2. Course
for obj in Course.objects.all():
obj.title = wrap_val(obj.title)
obj.slug = wrap_val(obj.slug)
obj.level = wrap_val(obj.level)
obj.status = wrap_val(obj.status)
obj.timing = wrap_val(obj.timing)
obj.features = wrap_val(obj.features)
obj.save()
# 3. CourseCategory
for obj in CourseCategory.objects.all():
obj.name = wrap_val(obj.name)
obj.slug = wrap_val(obj.slug)
obj.save()
# 4. CourseChapter
for obj in CourseChapter.objects.all():
obj.title = wrap_val(obj.title)
obj.save()
# 5. CourseLesson
for obj in CourseLesson.objects.all():
obj.title = wrap_val(obj.title)
obj.save()
# 6. Glossary
for obj in Glossary.objects.all():
obj.description = wrap_val(obj.description)
obj.save()
# 7. Lesson
for obj in Lesson.objects.all():
obj.title = wrap_val(obj.title)
obj.content_type = wrap_val(obj.content_type)
obj.save()
class Migration(migrations.Migration):
dependencies = [
('course', '0007_coursechapter_alter_courselesson_options_and_more'),
]
operations = [
# Remove indexes
migrations.RemoveIndex(
model_name='course',
name='course_cour_status_57ffd9_idx',
),
migrations.RemoveIndex(
model_name='course',
name='course_cour_slug_235a66_idx',
),
migrations.RemoveIndex(
model_name='course',
name='course_cour_status_bfcd24_idx',
),
migrations.RemoveIndex(
model_name='course',
name='course_cour_categor_26bb4d_idx',
),
migrations.RemoveIndex(
model_name='course',
name='course_cour_profess_5eae9a_idx',
),
migrations.RemoveIndex(
model_name='lesson',
name='course_less_content_e1cf57_idx',
),
# Phase 1: Temporary convert columns to TextField to lift length limits
migrations.AlterField(
model_name='attachment',
name='title',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='course',
name='title',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='course',
name='slug',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='course',
name='level',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='course',
name='status',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='coursecategory',
name='name',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='coursecategory',
name='slug',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='coursechapter',
name='title',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='courselesson',
name='title',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='glossary',
name='description',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='lesson',
name='content_type',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='lesson',
name='title',
field=models.TextField(blank=True, null=True),
),
# Phase 2: Run Python conversion logic to format as JSON strings
migrations.RunPython(
convert_to_json_format,
reverse_code=migrations.RunPython.noop
),
# Phase 3: Final conversion to JSONField database columns
migrations.AlterField(
model_name='attachment',
name='title',
field=models.JSONField(default=list, verbose_name='Attachment Title'),
),
migrations.AlterField(
model_name='course',
name='features',
field=models.JSONField(blank=True, default=list, null=True, verbose_name='Course features'),
),
migrations.AlterField(
model_name='course',
name='level',
field=models.JSONField(blank=True, default=list, null=True, verbose_name='Course Level'),
),
migrations.AlterField(
model_name='course',
name='slug',
field=models.JSONField(blank=True, default=list, null=True, verbose_name='Slug'),
),
migrations.AlterField(
model_name='course',
name='status',
field=models.JSONField(blank=True, default=list, null=True, verbose_name='Course Status'),
),
migrations.AlterField(
model_name='course',
name='timing',
field=models.JSONField(blank=True, default=list, null=True, verbose_name='Timing'),
),
migrations.AlterField(
model_name='course',
name='title',
field=models.JSONField(default=list, verbose_name='Course Title'),
),
migrations.AlterField(
model_name='coursecategory',
name='name',
field=models.JSONField(default=list, verbose_name='Category Name'),
),
migrations.AlterField(
model_name='coursecategory',
name='slug',
field=models.JSONField(blank=True, default=list, null=True, verbose_name='Slug'),
),
migrations.AlterField(
model_name='coursechapter',
name='title',
field=models.JSONField(default=list, verbose_name='Chapter Title'),
),
migrations.AlterField(
model_name='courselesson',
name='title',
field=models.JSONField(blank=True, default=list, null=True, verbose_name='Sub-lesson Title'),
),
migrations.AlterField(
model_name='glossary',
name='description',
field=models.JSONField(default=list, verbose_name='Description'),
),
migrations.AlterField(
model_name='lesson',
name='content_type',
field=models.JSONField(default=list, verbose_name='Content Type'),
),
migrations.AlterField(
model_name='lesson',
name='title',
field=models.JSONField(default=list, verbose_name='Lesson Title'),
),
]

130
apps/course/models/course.py

@ -7,9 +7,73 @@ from django.utils.translation import gettext_lazy as _
from apps.account.models import ProfessorUser
from utils.schema import default_timing
from utils import generate_slug_for_model
from utils import generate_slug_for_model, generate_language_slugs
from django.core.validators import MinValueValidator, MaxValueValidator
def extract_text_from_json(value):
if not value:
return ""
if isinstance(value, list):
for item in value:
if isinstance(item, dict):
text = item.get('title') or item.get('value') or item.get('text') or item.get('name')
if text:
return str(text)
else:
if item:
return str(item)
return ""
if isinstance(value, dict):
for lang in ("fa", "en", "ru"):
if lang in value and value[lang]:
v = value[lang]
if isinstance(v, dict):
return str(v.get('title') or v.get('value') or v.get('text') or v.get('name') or "")
return str(v)
for v in value.values():
if isinstance(v, dict):
txt = v.get('title') or v.get('value') or v.get('text') or v.get('name')
if txt:
return str(txt)
elif v:
return str(v)
return ""
if isinstance(value, (str, int, float)):
return str(value)
return ""
def is_multilingual_list(value):
if not isinstance(value, list):
return False
if len(value) == 0:
return True
return all(isinstance(item, dict) and 'language_code' in item for item in value)
def format_multilingual_field(value):
if value is None:
return []
if is_multilingual_list(value):
return value
if isinstance(value, dict) and any(k in value for k in ("fa", "en", "ru", "ar", "tr")):
return [{"title": v, "language_code": k} for k, v in value.items() if v]
return [{"title": value, "language_code": "en"}]
def get_localized_field(lang, field_value):
try:
if isinstance(field_value, list) and field_value:
for tr in field_value:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('title') or tr.get('text') or tr.get('value') or tr.get('name')
return extract_text_from_json(field_value)
return extract_text_from_json(field_value)
except Exception as exp:
print(f'---> Error in get_localized_field: {exp}')
return None
def course_file_upload_to(instance, filename):
@ -27,16 +91,22 @@ def course_attachment_file_upload_to(instance, filename):
class CourseCategory(models.Model):
name = models.CharField(max_length=255, verbose_name=_('Category Name'))
name = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Category Name'))
slug = models.SlugField(unique=True, max_length=255)
slug = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Slug'))
def __str__(self):
return self.name
return extract_text_from_json(self.name)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug_for_model(CourseCategory, self.name)
self.name = format_multilingual_field(self.name)
if not self.slug or self.slug == []:
try:
self.slug = generate_language_slugs(self.name)
except Exception:
self.slug = []
else:
self.slug = format_multilingual_field(self.slug)
super().save(*args, **kwargs)
@property
@ -63,8 +133,8 @@ class Course(models.Model):
title = models.CharField(max_length=255, verbose_name=_('Course Title'))
slug = models.SlugField(allow_unicode=True, unique=True)
title = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Course Title'))
slug = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Slug'))
category = models.ForeignKey(CourseCategory, on_delete=models.CASCADE, related_name='courses', verbose_name=_('Category'))
professor = models.ForeignKey(
ProfessorUser,
@ -89,13 +159,13 @@ class Course(models.Model):
is_online = models.BooleanField(default=False, verbose_name=_('Is Online Course'))
online_link = models.CharField(max_length=500, null=True, blank=True, verbose_name=_('Online Class Link'))
level = models.CharField(max_length=10, choices=LevelChoices.choices, verbose_name=_('Course Level'))
level = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Course Level'))
duration = models.PositiveIntegerField(verbose_name=_('Duration (in hours)'))
lessons_count = models.PositiveIntegerField(verbose_name=_('Number of Lessons'))
description = models.TextField(verbose_name=_('Course Description'))
short_description = models.CharField(max_length=500, blank=True, null=True, verbose_name=_("Short Description"))
status = models.CharField(max_length=15, choices=StatusChoices.choices, default=StatusChoices.INACTIVE, verbose_name=_('Course Status'))
status = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Course Status'))
is_free = models.BooleanField(default=True, verbose_name=_('Is Free'))
price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name=_('Course Price'))
discount_percentage = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], verbose_name=_('Discount Percentage'))
@ -113,14 +183,14 @@ class Course(models.Model):
verbose_name=_('Lock Private Chats with Professor')
)
timing = models.JSONField(blank=True, null=True, default=default_timing, verbose_name=_("Timing"))
features = models.JSONField(verbose_name=_('Course features'), default=dict, blank=True, null=True)
timing = models.JSONField(blank=True, null=True, default=list, verbose_name=_("Timing"))
features = models.JSONField(verbose_name=_('Course features'), default=list, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self):
return self.title
return extract_text_from_json(self.title)
def get_completed_lessons_count(self, student):
return self.lessons.filter(completions__student=student).count()
@ -147,8 +217,18 @@ class Course(models.Model):
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug_for_model(Course, self.title)
self.title = format_multilingual_field(self.title)
if not self.slug or self.slug == []:
try:
self.slug = generate_language_slugs(self.title)
except Exception:
self.slug = []
else:
self.slug = format_multilingual_field(self.slug)
self.level = format_multilingual_field(self.level)
self.status = format_multilingual_field(self.status)
self.timing = format_multilingual_field(self.timing)
self.features = format_multilingual_field(self.features)
# Ensure consistency: if price is 0, set is_free to True and discount_percentage to 0
if self.price == 0:
@ -174,13 +254,8 @@ class Course(models.Model):
verbose_name_plural = _("Courses")
indexes = [
models.Index(fields=['status']),
models.Index(fields=['is_free']),
models.Index(fields=['created_at']),
models.Index(fields=['slug']),
models.Index(fields=['status', 'created_at']),
models.Index(fields=['category', 'status']),
models.Index(fields=['professor', 'status']),
]
@ -189,13 +264,17 @@ class Glossary(models.Model):
Base Glossary model that contains the actual content
"""
title = models.CharField(max_length=555, verbose_name=_('Glossary Title'))
description = models.TextField(verbose_name=_('Description'))
description = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Description'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self):
return self.title
def save(self, *args, **kwargs):
self.description = format_multilingual_field(self.description)
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Glossary")
verbose_name_plural = _("Glossaries")
@ -212,7 +291,7 @@ class CourseGlossary(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self):
return f"{self.course.title} - {self.glossary.title}"
return f"{extract_text_from_json(self.course.title)} - {self.glossary.title}"
@property
def title(self):
@ -233,7 +312,7 @@ class Attachment(models.Model):
"""
Base Attachment model that contains the actual file
"""
title = models.CharField(max_length=255, verbose_name=_('Attachment Title'))
title = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Attachment Title'))
file = models.FileField(
upload_to=attachment_file_upload_to,
verbose_name=_('Attachment File')
@ -243,13 +322,14 @@ class Attachment(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def save(self, *args, **kwargs):
self.title = format_multilingual_field(self.title)
# Calculate the file size before saving
if self.file and not self.file_size:
self.file_size = self.file.size
super().save(*args, **kwargs)
def __str__(self):
return self.title
return extract_text_from_json(self.title)
class Meta:
verbose_name = _("Attachment")
@ -267,7 +347,7 @@ class CourseAttachment(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self):
return f"{self.course.title} - {self.attachment.title}"
return f"{extract_text_from_json(self.course.title)} - {extract_text_from_json(self.attachment.title)}"
@property
def title(self):

28
apps/course/models/lesson.py

@ -6,6 +6,7 @@ from filer.fields.image import FilerImageField
from filer.fields.file import FilerFileField
from apps.account.models import StudentUser
from apps.course.models.course import extract_text_from_json, format_multilingual_field, get_localized_field
def lesson_file_upload_to(instance, filename):
@ -21,8 +22,8 @@ class Lesson(models.Model):
YOUTUBE_LINK = 'youtube_link', _('Youtube Link')
VIDEO_FILE = 'video_file', _('Video File')
title = models.CharField(max_length=255, verbose_name=_('Lesson Title'))
content_type = models.CharField(max_length=50, choices=ContentTypeChoices.choices, verbose_name=_('Content Type'))
title = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Lesson Title'))
content_type = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Content Type'))
content_file = models.FileField(
null=True,
@ -36,14 +37,18 @@ class Lesson(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self):
return self.title
return extract_text_from_json(self.title)
def save(self, *args, **kwargs):
self.title = format_multilingual_field(self.title)
self.content_type = format_multilingual_field(self.content_type)
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Lesson")
verbose_name_plural = _("Lessons")
indexes = [
models.Index(fields=['content_type']),
models.Index(fields=['created_at']),
]
@ -52,16 +57,17 @@ class CourseChapter(models.Model):
V2 Grouping Layer: Groups lessons together into chapters/sections.
"""
course = models.ForeignKey("course.Course", on_delete=models.CASCADE, related_name='chapters', verbose_name=_('Course'))
title = models.CharField(max_length=255, verbose_name=_('Chapter Title'))
title = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Chapter Title'))
priority = models.IntegerField(null=True, blank=True, verbose_name=_('Priority'))
is_active = models.BooleanField(default=True, verbose_name=_('Is Active'))
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.course.title} - {self.title}"
return f"{extract_text_from_json(self.course.title)} - {extract_text_from_json(self.title)}"
def save(self, *args, **kwargs):
self.title = format_multilingual_field(self.title)
if self.priority is None:
max_priority = self.course.chapters.aggregate(max_p=models.Max('priority'))['max_p']
self.priority = (max_priority or 0) + 1
@ -85,7 +91,7 @@ class CourseLesson(models.Model):
course = models.ForeignKey("course.Course", on_delete=models.CASCADE, related_name='lessons', verbose_name=_('Course'))
chapter = models.ForeignKey(CourseChapter, on_delete=models.CASCADE, related_name='lessons', verbose_name=_('Chapter') , null=True , blank=True)
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='course_lessons', verbose_name=_('Lesson'))
title = models.CharField(max_length=255, verbose_name=_('Sub-lesson Title'), null=True, blank=True)
title = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Sub-lesson Title'))
priority = models.IntegerField(null=True, blank=True, verbose_name=_('Priority'))
is_active = models.BooleanField(default=True, verbose_name=_('Is Active'))
@ -93,8 +99,8 @@ class CourseLesson(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self):
title = self.title or self.lesson.title
return f"{self.chapter.title} - {title}"
title = extract_text_from_json(self.title) or extract_text_from_json(self.lesson.title)
return f"{extract_text_from_json(self.chapter.title)} - {title}"
def is_completed_by(self, student):
return self.completions.filter(student=student).exists()
@ -120,9 +126,11 @@ class CourseLesson(models.Model):
if self.chapter and getattr(self, 'course_id', None) != self.chapter.course_id:
self.course = self.chapter.course
if not self.title:
if not self.title or self.title == []:
self.title = self.lesson.title
self.title = format_multilingual_field(self.title)
if self.priority is None:
max_priority = self.course.lessons.aggregate(max_priority=models.Max('priority'))['max_priority']
self.priority = (max_priority or 0) + 1

31
apps/course/serializers/admin.py

@ -15,6 +15,7 @@ from apps.course.models import (
LiveSessionRecording
)
from apps.account.models import ProfessorUser
from apps.course.models.course import extract_text_from_json
class AdminLessonSerializer(serializers.ModelSerializer):
class Meta:
@ -40,14 +41,14 @@ def serialize_file(value, serializer):
return field.to_representation(value)
class AdminCourseCategorySerializer(serializers.ModelSerializer):
slug = serializers.SlugField(required=False, allow_blank=True)
slug = serializers.JSONField(required=False, allow_null=True)
class Meta:
model = CourseCategory
fields = ['id', 'name', 'slug']
class AdminCourseListSerializer(serializers.ModelSerializer):
category_name = serializers.CharField(source='category.name', read_only=True)
category_name = serializers.SerializerMethodField()
professor_name = serializers.CharField(source='professor.fullname', read_only=True)
participant_count = serializers.SerializerMethodField()
thumbnail = serializers.SerializerMethodField()
@ -82,10 +83,13 @@ class AdminCourseListSerializer(serializers.ModelSerializer):
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))
def get_category_name(self, obj):
return extract_text_from_json(obj.category.name) if obj.category else ""
class AdminCourseDetailSerializer(serializers.ModelSerializer):
slug = serializers.SlugField(required=False, allow_blank=True)
category_name = serializers.CharField(source='category.name', read_only=True)
slug = serializers.JSONField(required=False, allow_null=True)
category_name = serializers.SerializerMethodField()
professor_name = serializers.CharField(source='professor.fullname', read_only=True)
thumbnail = FileFieldSerializer(required=False, allow_null=True)
video_file = FileFieldSerializer(required=False, allow_null=True)
@ -130,6 +134,9 @@ class AdminCourseDetailSerializer(serializers.ModelSerializer):
def get_participant_count(self, obj):
return obj.participants.count()
def get_category_name(self, obj):
return extract_text_from_json(obj.category.name) if obj.category else ""
class AdminCourseChapterSerializer(serializers.ModelSerializer):
class Meta:
@ -143,12 +150,12 @@ class AdminCourseLessonSerializer(serializers.Serializer):
course = serializers.IntegerField(required=False, read_only=True)
chapter = serializers.IntegerField()
lesson = serializers.IntegerField(required=False, write_only=True, allow_null=True)
title = serializers.CharField(max_length=255, required=False, allow_blank=True, allow_null=True)
title = serializers.JSONField(required=False, allow_null=True)
priority = serializers.IntegerField(required=False, allow_null=True)
is_active = serializers.BooleanField(default=True)
# Lesson (Base Content) fields
content_type = serializers.ChoiceField(choices=Lesson.ContentTypeChoices.choices, required=False, allow_null=True)
content_type = serializers.JSONField(required=False, allow_null=True)
content_file = FileFieldSerializer(required=False, allow_null=True)
video_link = serializers.CharField(max_length=500, required=False, allow_blank=True, allow_null=True)
duration = serializers.IntegerField(min_value=0, required=False, default=0)
@ -166,10 +173,10 @@ class AdminCourseLessonSerializer(serializers.Serializer):
'course': instance.course_id,
'chapter': instance.chapter_id,
'lesson': instance.lesson_id,
'title': instance.title or (lesson_obj.title if lesson_obj else ""),
'title': instance.title or (lesson_obj.title if lesson_obj else []),
'priority': instance.priority,
'is_active': instance.is_active,
'content_type': lesson_obj.content_type if lesson_obj else None,
'content_type': lesson_obj.content_type if lesson_obj else [],
'content_file': serialize_file(lesson_obj.content_file, self) if lesson_obj and lesson_obj.content_file else None,
'video_link': lesson_obj.video_link if lesson_obj else "",
'duration': lesson_obj.duration if lesson_obj else 0,
@ -260,7 +267,7 @@ class AdminCourseAttachmentSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
course = serializers.IntegerField()
attachment = serializers.IntegerField(required=False, write_only=True, allow_null=True)
title = serializers.CharField(max_length=255, required=False, allow_blank=True, allow_null=True)
title = serializers.JSONField(required=False, allow_null=True)
file = FileFieldSerializer(required=False, allow_null=True)
file_size = serializers.IntegerField(read_only=True)
@ -270,7 +277,7 @@ class AdminCourseAttachmentSerializer(serializers.Serializer):
'id': instance.id,
'course': instance.course_id,
'attachment': instance.attachment_id,
'title': attachment_obj.title if attachment_obj else "",
'title': attachment_obj.title if attachment_obj else [],
'file': serialize_file(attachment_obj.file, self) if attachment_obj and attachment_obj.file else None,
'file_size': attachment_obj.file_size if attachment_obj else 0
}
@ -328,7 +335,7 @@ class AdminCourseGlossarySerializer(serializers.Serializer):
course = serializers.IntegerField()
glossary = serializers.IntegerField(required=False, write_only=True, allow_null=True)
title = serializers.CharField(max_length=555, required=False, allow_blank=True, allow_null=True)
description = serializers.CharField(required=False, allow_blank=True, allow_null=True)
description = serializers.JSONField(required=False, allow_null=True)
def to_representation(self, instance):
glossary_obj = instance.glossary
@ -337,7 +344,7 @@ class AdminCourseGlossarySerializer(serializers.Serializer):
'course': instance.course_id,
'glossary': instance.glossary_id,
'title': glossary_obj.title if glossary_obj else "",
'description': glossary_obj.description if glossary_obj else ""
'description': glossary_obj.description if glossary_obj else []
}
def create(self, validated_data):

128
apps/course/serializers/course.py

@ -2,6 +2,7 @@ from rest_framework import serializers
# from dj_filer.admin import get_thumbs
from utils import get_thumbs
from apps.course.models import Course, CourseCategory, Attachment, Glossary, LessonCompletion, Participant, Lesson, CourseAttachment, CourseGlossary, CourseLesson
from apps.course.models.course import get_localized_field
from apps.chat.models import RoomMessage
from apps.account.serializers import UserProfileSerializer
@ -9,11 +10,23 @@ from apps.account.serializers import UserProfileSerializer
class CourseCategorySerializer(serializers.ModelSerializer):
course_count = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
slug = serializers.SerializerMethodField()
class Meta:
model = CourseCategory
fields = ['id', 'name', 'slug', 'course_count']
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_name(self, obj):
return get_localized_field(self._lang(), obj.name)
def get_slug(self, obj):
return get_localized_field(self._lang(), obj.slug)
def get_course_count(self, obj):
return obj.course_count
@ -27,6 +40,10 @@ class CourseListSerializer(serializers.ModelSerializer):
discount_percentage = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
slug = serializers.SerializerMethodField()
level = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
class Meta:
model = Course
@ -50,6 +67,22 @@ class CourseListSerializer(serializers.ModelSerializer):
'final_price',
]
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)
def get_slug(self, obj):
return get_localized_field(self._lang(), obj.slug)
def get_level(self, obj):
return get_localized_field(self._lang(), obj.level)
def get_status(self, obj):
return get_localized_field(self._lang(), obj.status)
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))
@ -105,6 +138,12 @@ class CourseDetailSerializer(serializers.ModelSerializer):
final_price = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField()
is_professor = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
slug = serializers.SerializerMethodField()
level = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
timing = serializers.SerializerMethodField()
features = serializers.SerializerMethodField()
class Meta:
model = Course
@ -358,6 +397,43 @@ class CourseDetailSerializer(serializers.ModelSerializer):
def get_is_free(self, obj):
return obj.is_free or obj.price == 0
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)
def get_slug(self, obj):
return get_localized_field(self._lang(), obj.slug)
def get_level(self, obj):
return get_localized_field(self._lang(), obj.level)
def get_status(self, obj):
return get_localized_field(self._lang(), obj.status)
def get_timing(self, obj):
import json
val = get_localized_field(self._lang(), obj.timing)
if isinstance(val, str):
try:
return json.loads(val)
except Exception:
pass
return val
def get_features(self, obj):
import json
val = get_localized_field(self._lang(), obj.features)
if isinstance(val, str):
try:
return json.loads(val)
except Exception:
pass
return val
class MyCourseListSerializer(serializers.ModelSerializer):
@ -365,6 +441,9 @@ class MyCourseListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
lessons_count = serializers.SerializerMethodField()
lessons_complated_count = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
slug = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
class Meta:
model = Course
@ -441,15 +520,37 @@ class MyCourseListSerializer(serializers.ModelSerializer):
course_lesson__course=course
).count()
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)
def get_slug(self, obj):
return get_localized_field(self._lang(), obj.slug)
def get_status(self, obj):
return get_localized_field(self._lang(), obj.status)
class AttachmentSerializer(serializers.ModelSerializer):
title = serializers.SerializerMethodField()
class Meta:
model = Attachment
fields = ['id', 'title', 'file', 'file_size']
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)
class CourseAttachmentSerializer(serializers.ModelSerializer):
title = serializers.CharField(source='attachment.title', read_only=True)
title = serializers.SerializerMethodField()
file = serializers.FileField(source='attachment.file', read_only=True)
file_size = serializers.IntegerField(source='attachment.file_size', read_only=True)
@ -457,17 +558,40 @@ class CourseAttachmentSerializer(serializers.ModelSerializer):
model = CourseAttachment
fields = ['id', 'title', 'file', 'file_size']
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_title(self, obj):
return get_localized_field(self._lang(), obj.attachment.title)
class GlossarySerializer(serializers.ModelSerializer):
description = serializers.SerializerMethodField()
class Meta:
model = Glossary
fields = ['id', 'title', 'description']
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_description(self, obj):
return get_localized_field(self._lang(), obj.description)
class CourseGlossarySerializer(serializers.ModelSerializer):
title = serializers.CharField(source='glossary.title', read_only=True)
description = serializers.CharField(source='glossary.description', read_only=True)
description = serializers.SerializerMethodField()
class Meta:
model = CourseGlossary
fields = ['id', 'title', 'description']
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_description(self, obj):
return get_localized_field(self._lang(), obj.glossary.description)

35
apps/course/serializers/lesson.py

@ -2,20 +2,35 @@ from rest_framework import serializers
from apps.course.models import Lesson, CourseLesson, Participant, LessonCompletion
from apps.course.access import user_has_course_access
from apps.quiz.serializers import QuizListSerializer
from apps.course.models.course import get_localized_field
class LessonSerializer(serializers.ModelSerializer):
title = serializers.SerializerMethodField()
content_type = serializers.SerializerMethodField()
class Meta:
model = Lesson
fields = ['id', 'title', 'content_type', 'content_file', 'video_link', 'duration']
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)
def get_content_type(self, obj):
return get_localized_field(self._lang(), obj.content_type)
class CourseLessonSerializer(serializers.ModelSerializer):
is_active = serializers.SerializerMethodField()
is_complated = serializers.SerializerMethodField()
quizs = serializers.SerializerMethodField()
permission = serializers.SerializerMethodField()
content_type = serializers.CharField(source='lesson.content_type', read_only=True)
title = serializers.SerializerMethodField()
content_type = serializers.SerializerMethodField()
content_file = serializers.FileField(source='lesson.content_file', read_only=True)
video_link = serializers.CharField(source='lesson.video_link', read_only=True)
duration = serializers.IntegerField(source='lesson.duration', read_only=True)
@ -24,6 +39,16 @@ class CourseLessonSerializer(serializers.ModelSerializer):
model = CourseLesson
fields = ['id', 'title', 'priority', 'is_active', 'permission', 'duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quizs']
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)
def get_content_type(self, obj):
return get_localized_field(self._lang(), obj.lesson.content_type)
def get_is_active(self, obj):
return obj.is_active and self._has_access_for_object(obj)
@ -93,11 +118,19 @@ class CourseChapterSerializer(serializers.ModelSerializer):
"""
is_active = serializers.SerializerMethodField()
lessons = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
class Meta:
model = CourseChapter
fields = ['id', 'title', 'priority', 'is_active', 'lessons']
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)
def get_is_active(self, obj):
if not obj.is_active:
return False

11
apps/course/signals.py

@ -1,6 +1,6 @@
from apps.course.models import Course
from apps.chat.models import RoomMessage
from apps.course.models.course import Course
from apps.course.models.course import Course, extract_text_from_json
from apps.course.models.lesson import CourseLesson
from django.db.models import Q
@ -14,10 +14,11 @@ UserModel = get_user_model()
@receiver(post_save, sender=Course)
def handle_room_message_for_course(sender, instance, created, **kwargs):
title_str = extract_text_from_json(instance.title)
if created: # فقط برای موارد جدید اجرا شود
RoomMessage.objects.create(
name=f"{instance.title} - Group",
description=f"Group chat for course: {instance.title}",
name=f"{title_str} - Group",
description=f"Group chat for course: {title_str}",
initiator=instance.professor, # استاد به‌عنوان سازنده اتاق
course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP
@ -28,8 +29,8 @@ def handle_room_message_for_course(sender, instance, created, **kwargs):
course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP
).update(
name=f"{instance.title} - Group",
description=f"Group chat for course: {instance.title}",
name=f"{title_str} - Group",
description=f"Group chat for course: {title_str}",
initiator=instance.professor
)

6
apps/course/views/admin.py

@ -78,11 +78,11 @@ class AdminCourseViewSet(viewsets.ModelViewSet):
level = self.request.query_params.get('level', None)
if level:
queryset = queryset.filter(level=level)
queryset = queryset.filter(level__contains=[{"title": level}])
status_param = self.request.query_params.get('status', None)
if status_param:
queryset = queryset.filter(status=status_param)
queryset = queryset.filter(status__contains=[{"title": status_param}])
is_free_param = self.request.query_params.get('is_free', None)
if is_free_param is not None:
@ -300,7 +300,7 @@ class AdminLessonViewSet(viewsets.ModelViewSet):
content_type = self.request.query_params.get('content_type', None)
if content_type:
queryset = queryset.filter(content_type=content_type)
queryset = queryset.filter(content_type__contains=[{"title": content_type}])
return queryset

38
apps/course/views/course.py

@ -121,7 +121,7 @@ class CourseListAPIView(ListAPIView):
queryset = Course.objects.select_related(
'category',
'professor'
).exclude(status=Course.StatusChoices.INACTIVE)
).exclude(status__contains=[{"title": Course.StatusChoices.INACTIVE}])
request = self.request
filters = request.query_params
@ -129,12 +129,18 @@ class CourseListAPIView(ListAPIView):
# Handle category_slug with multiple values separated by commas
if category_slugs := filters.get('category_slug'):
category_slugs_list = category_slugs.split(',')
queryset = queryset.filter(category__slug__in=category_slugs_list)
category_q = Q()
for slug_item in category_slugs_list:
category_q |= Q(category__slug__contains=[{"title": slug_item}])
queryset = queryset.filter(category_q)
# Handle status with multiple values separated by commas
if statuses := filters.get('status'):
statuses_list = statuses.split(',')
queryset = queryset.filter(status__in=statuses_list)
status_q = Q()
for status_item in statuses_list:
status_q |= Q(status__contains=[{"title": status_item}])
queryset = queryset.filter(status_q)
if is_free := filters.get('is_free'):
is_free = is_free.lower() == 'true'
@ -186,6 +192,16 @@ class CourseDetailAPIView(RetrieveAPIView):
'room_messages'
)
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
slug_value = self.kwargs[lookup_url_kwarg]
obj = queryset.filter(slug__contains=[{"title": slug_value}]).first()
if not obj:
raise NotFound("Course not found")
self.check_object_permissions(self.request, obj)
return obj
@swagger_auto_schema(
operation_description=doc_course_detail(),
tags=['Imam-Javad - Course'],
@ -231,7 +247,7 @@ class MyCourseListAPIView(ListAPIView):
'chapters__lessons__lesson',
'chapters__lessons__completions',
'participants__student'
).exclude(status=Course.StatusChoices.INACTIVE)
).exclude(status__contains=[{"title": Course.StatusChoices.INACTIVE}])
request = self.request
filters = request.query_params
@ -316,9 +332,8 @@ class AttachmentListAPIView(ListAPIView):
Optimized queryset with select_related for attachment relationship
"""
course_slug = self.kwargs.get('slug')
try:
course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist:
course = Course.objects.filter(slug__contains=[{"title": course_slug}]).first()
if not course:
raise NotFound("Course not found")
return CourseAttachment.objects.select_related(
'course',
@ -368,9 +383,8 @@ class GlossaryListAPIView(ListAPIView):
Optimized queryset with select_related for glossary relationship
"""
course_slug = self.kwargs.get('slug')
try:
course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist:
course = Course.objects.filter(slug__contains=[{"title": course_slug}]).first()
if not course:
raise NotFound("Course not found")
return CourseGlossary.objects.select_related(
@ -494,7 +508,9 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
detail_view = CourseDetailAPIView()
queryset = detail_view.get_queryset()
course = get_object_or_404(queryset, slug=slug)
course = queryset.filter(slug__contains=[{"title": slug}]).first()
if not course:
raise NotFound("Course not found")
user = request.user
print(f"[Online Validate GET] Course found - course_id={course.id} slug={slug} is_online={course.is_online}")

4
apps/course/views/lesson.py

@ -39,7 +39,7 @@ class LessonListView(ListAPIView):
Optimized queryset with select_related and prefetch_related for lesson relationships
"""
course_slug = self.kwargs.get('slug')
course = get_object_or_404(Course, slug=course_slug)
course = get_object_or_404(Course, slug__contains=[{"title": course_slug}])
return CourseLesson.objects.select_related(
'chapter', # 👇 Route through chapter
@ -316,7 +316,7 @@ class LessonListV2APIView(ListAPIView):
def get_queryset(self):
course_slug = self.kwargs.get('slug')
course = get_object_or_404(Course, slug=course_slug)
course = get_object_or_404(Course, slug__contains=[{"title": course_slug}])
# We query the Chapters, and prefetch the lessons to avoid N+1 query problems
return CourseChapter.objects.prefetch_related(

6
apps/course/views/live_session.py

@ -46,7 +46,7 @@ class CourseLiveSessionRoomCreateAPIView(GenericAPIView):
def post(self, request, slug, *args, **kwargs):
# 1. Standard Permissions Logic
course = get_object_or_404(Course, slug=slug)
course = get_object_or_404(Course, slug__contains=[{"title": slug}])
if not request.user.can_manage_course(course):
raise AppAPIException({'message': 'Permission denied'}, status_code=403)
@ -301,7 +301,9 @@ class CourseLiveSessionTokenAPIView(GenericAPIView):
logger.info(f"[LiveSession Token] Request from user_id={user.id} for course={course_slug}")
try:
course = Course.objects.get(slug=course_slug)
course = Course.objects.filter(slug__contains=[{"title": course_slug}]).first()
if not course:
raise Course.DoesNotExist
except Course.DoesNotExist:
logger.warning(f"[LiveSession Token] Course not found - course={course_slug} user_id={user.id}")
raise AppAPIException({'message': 'Course not found.'}, status_code=status.HTTP_404_NOT_FOUND)

5
apps/course/views/participant.py

@ -32,9 +32,8 @@ class CourseParticipantsView(generics.ListAPIView):
for the admin course participants table.
"""
course_slug = self.kwargs.get('slug')
try:
course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist:
course = Course.objects.filter(slug__contains=[{"title": course_slug}]).first()
if not course:
raise AppAPIException({'message': "Course not found"})
return StudentUser.objects.select_related().filter(

Loading…
Cancel
Save