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. 130
      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 = { radio_fields = {
"video_type": admin.HORIZONTAL, "video_type": admin.HORIZONTAL,
"status": admin.HORIZONTAL,
"level": admin.HORIZONTAL,
} }
conditional_fields = { conditional_fields = {

24
apps/course/admin/lesson.py

@ -32,19 +32,9 @@ class CourseLessonForm(forms.ModelForm):
class LessonAdmin(ModelAdmin): class LessonAdmin(ModelAdmin):
form = LessonForm form = LessonForm
list_display = ('title', 'display_duration', 'content_type') list_display = ('title', 'display_duration', 'content_type')
list_filter = (
('content_type', ChoicesDropdownFilter),
)
search_fields = ('title',) search_fields = ('title',)
ordering = ('title',) ordering = ('title',)
list_filter_submit = True 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 = ( fieldsets = (
(None, { (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")) @display(description=_("Duration"))
def display_duration(self, obj): def display_duration(self, obj):
return format_html('<span class="badge badge-info">{} {}</span>', obj.duration, _("min")) 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 apps.account.models import ProfessorUser
from utils.schema import default_timing 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 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): def course_file_upload_to(instance, filename):
@ -27,16 +91,22 @@ def course_attachment_file_upload_to(instance, filename):
class CourseCategory(models.Model): 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): def __str__(self):
return self.name
return extract_text_from_json(self.name)
def save(self, *args, **kwargs): 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) super().save(*args, **kwargs)
@property @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')) category = models.ForeignKey(CourseCategory, on_delete=models.CASCADE, related_name='courses', verbose_name=_('Category'))
professor = models.ForeignKey( professor = models.ForeignKey(
ProfessorUser, ProfessorUser,
@ -89,13 +159,13 @@ class Course(models.Model):
is_online = models.BooleanField(default=False, verbose_name=_('Is Online Course')) 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')) 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)')) duration = models.PositiveIntegerField(verbose_name=_('Duration (in hours)'))
lessons_count = models.PositiveIntegerField(verbose_name=_('Number of Lessons')) lessons_count = models.PositiveIntegerField(verbose_name=_('Number of Lessons'))
description = models.TextField(verbose_name=_('Course Description')) description = models.TextField(verbose_name=_('Course Description'))
short_description = models.CharField(max_length=500, blank=True, null=True, verbose_name=_("Short 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')) 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')) 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')) 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') 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")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self): def __str__(self):
return self.title
return extract_text_from_json(self.title)
def get_completed_lessons_count(self, student): def get_completed_lessons_count(self, student):
return self.lessons.filter(completions__student=student).count() return self.lessons.filter(completions__student=student).count()
@ -147,8 +217,18 @@ class Course(models.Model):
def save(self, *args, **kwargs): 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 # Ensure consistency: if price is 0, set is_free to True and discount_percentage to 0
if self.price == 0: if self.price == 0:
@ -174,13 +254,8 @@ class Course(models.Model):
verbose_name_plural = _("Courses") verbose_name_plural = _("Courses")
indexes = [ indexes = [
models.Index(fields=['status']),
models.Index(fields=['is_free']), models.Index(fields=['is_free']),
models.Index(fields=['created_at']), 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 Base Glossary model that contains the actual content
""" """
title = models.CharField(max_length=555, verbose_name=_('Glossary Title')) 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")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self): def __str__(self):
return self.title return self.title
def save(self, *args, **kwargs):
self.description = format_multilingual_field(self.description)
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _("Glossary") verbose_name = _("Glossary")
verbose_name_plural = _("Glossaries") verbose_name_plural = _("Glossaries")
@ -212,7 +291,7 @@ class CourseGlossary(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self): def __str__(self):
return f"{self.course.title} - {self.glossary.title}"
return f"{extract_text_from_json(self.course.title)} - {self.glossary.title}"
@property @property
def title(self): def title(self):
@ -233,7 +312,7 @@ class Attachment(models.Model):
""" """
Base Attachment model that contains the actual file 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( file = models.FileField(
upload_to=attachment_file_upload_to, upload_to=attachment_file_upload_to,
verbose_name=_('Attachment File') verbose_name=_('Attachment File')
@ -243,13 +322,14 @@ class Attachment(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.title = format_multilingual_field(self.title)
# Calculate the file size before saving # Calculate the file size before saving
if self.file and not self.file_size: if self.file and not self.file_size:
self.file_size = self.file.size self.file_size = self.file.size
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): def __str__(self):
return self.title
return extract_text_from_json(self.title)
class Meta: class Meta:
verbose_name = _("Attachment") verbose_name = _("Attachment")
@ -267,7 +347,7 @@ class CourseAttachment(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self): 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 @property
def title(self): 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 filer.fields.file import FilerFileField
from apps.account.models import StudentUser 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): def lesson_file_upload_to(instance, filename):
@ -21,8 +22,8 @@ class Lesson(models.Model):
YOUTUBE_LINK = 'youtube_link', _('Youtube Link') YOUTUBE_LINK = 'youtube_link', _('Youtube Link')
VIDEO_FILE = 'video_file', _('Video File') 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( content_file = models.FileField(
null=True, null=True,
@ -36,14 +37,18 @@ class Lesson(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self): 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: class Meta:
verbose_name = _("Lesson") verbose_name = _("Lesson")
verbose_name_plural = _("Lessons") verbose_name_plural = _("Lessons")
indexes = [ indexes = [
models.Index(fields=['content_type']),
models.Index(fields=['created_at']), models.Index(fields=['created_at']),
] ]
@ -52,16 +57,17 @@ class CourseChapter(models.Model):
V2 Grouping Layer: Groups lessons together into chapters/sections. V2 Grouping Layer: Groups lessons together into chapters/sections.
""" """
course = models.ForeignKey("course.Course", on_delete=models.CASCADE, related_name='chapters', verbose_name=_('Course')) 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')) priority = models.IntegerField(null=True, blank=True, verbose_name=_('Priority'))
is_active = models.BooleanField(default=True, verbose_name=_('Is Active')) is_active = models.BooleanField(default=True, verbose_name=_('Is Active'))
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
def __str__(self): 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): def save(self, *args, **kwargs):
self.title = format_multilingual_field(self.title)
if self.priority is None: if self.priority is None:
max_priority = self.course.chapters.aggregate(max_p=models.Max('priority'))['max_p'] max_priority = self.course.chapters.aggregate(max_p=models.Max('priority'))['max_p']
self.priority = (max_priority or 0) + 1 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')) 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) 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')) 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')) priority = models.IntegerField(null=True, blank=True, verbose_name=_('Priority'))
is_active = models.BooleanField(default=True, verbose_name=_('Is Active')) 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")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self): 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): def is_completed_by(self, student):
return self.completions.filter(student=student).exists() 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: if self.chapter and getattr(self, 'course_id', None) != self.chapter.course_id:
self.course = self.chapter.course self.course = self.chapter.course
if not self.title:
if not self.title or self.title == []:
self.title = self.lesson.title self.title = self.lesson.title
self.title = format_multilingual_field(self.title)
if self.priority is None: if self.priority is None:
max_priority = self.course.lessons.aggregate(max_priority=models.Max('priority'))['max_priority'] max_priority = self.course.lessons.aggregate(max_priority=models.Max('priority'))['max_priority']
self.priority = (max_priority or 0) + 1 self.priority = (max_priority or 0) + 1

31
apps/course/serializers/admin.py

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

130
apps/course/serializers/course.py

@ -2,6 +2,7 @@ from rest_framework import serializers
# from dj_filer.admin import get_thumbs # from dj_filer.admin import get_thumbs
from utils 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 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.chat.models import RoomMessage
from apps.account.serializers import UserProfileSerializer from apps.account.serializers import UserProfileSerializer
@ -9,11 +10,23 @@ from apps.account.serializers import UserProfileSerializer
class CourseCategorySerializer(serializers.ModelSerializer): class CourseCategorySerializer(serializers.ModelSerializer):
course_count = serializers.SerializerMethodField() course_count = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
slug = serializers.SerializerMethodField()
class Meta: class Meta:
model = CourseCategory model = CourseCategory
fields = ['id', 'name', 'slug', 'course_count'] 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): def get_course_count(self, obj):
return obj.course_count return obj.course_count
@ -27,6 +40,10 @@ class CourseListSerializer(serializers.ModelSerializer):
discount_percentage = serializers.SerializerMethodField() discount_percentage = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField() final_price = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField() is_free = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
slug = serializers.SerializerMethodField()
level = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
class Meta: class Meta:
model = Course model = Course
@ -50,6 +67,22 @@ class CourseListSerializer(serializers.ModelSerializer):
'final_price', '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): def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request')) return get_thumbs(obj.thumbnail, self.context.get('request'))
@ -105,6 +138,12 @@ class CourseDetailSerializer(serializers.ModelSerializer):
final_price = serializers.SerializerMethodField() final_price = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField() is_free = serializers.SerializerMethodField()
is_professor = 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: class Meta:
model = Course model = Course
@ -358,6 +397,43 @@ class CourseDetailSerializer(serializers.ModelSerializer):
def get_is_free(self, obj): def get_is_free(self, obj):
return obj.is_free or obj.price == 0 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): class MyCourseListSerializer(serializers.ModelSerializer):
@ -365,6 +441,9 @@ class MyCourseListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField() thumbnail = serializers.SerializerMethodField()
lessons_count = serializers.SerializerMethodField() lessons_count = serializers.SerializerMethodField()
lessons_complated_count = serializers.SerializerMethodField() lessons_complated_count = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
slug = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
class Meta: class Meta:
model = Course model = Course
@ -440,16 +519,38 @@ class MyCourseListSerializer(serializers.ModelSerializer):
student=student, student=student,
course_lesson__course=course course_lesson__course=course
).count() ).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): class AttachmentSerializer(serializers.ModelSerializer):
title = serializers.SerializerMethodField()
class Meta: class Meta:
model = Attachment model = Attachment
fields = ['id', 'title', 'file', 'file_size'] 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): 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 = serializers.FileField(source='attachment.file', read_only=True)
file_size = serializers.IntegerField(source='attachment.file_size', read_only=True) file_size = serializers.IntegerField(source='attachment.file_size', read_only=True)
@ -457,17 +558,40 @@ class CourseAttachmentSerializer(serializers.ModelSerializer):
model = CourseAttachment model = CourseAttachment
fields = ['id', 'title', 'file', 'file_size'] 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): class GlossarySerializer(serializers.ModelSerializer):
description = serializers.SerializerMethodField()
class Meta: class Meta:
model = Glossary model = Glossary
fields = ['id', 'title', 'description'] 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): class CourseGlossarySerializer(serializers.ModelSerializer):
title = serializers.CharField(source='glossary.title', read_only=True) title = serializers.CharField(source='glossary.title', read_only=True)
description = serializers.CharField(source='glossary.description', read_only=True)
description = serializers.SerializerMethodField()
class Meta: class Meta:
model = CourseGlossary model = CourseGlossary
fields = ['id', 'title', 'description']
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.models import Lesson, CourseLesson, Participant, LessonCompletion
from apps.course.access import user_has_course_access from apps.course.access import user_has_course_access
from apps.quiz.serializers import QuizListSerializer from apps.quiz.serializers import QuizListSerializer
from apps.course.models.course import get_localized_field
class LessonSerializer(serializers.ModelSerializer): class LessonSerializer(serializers.ModelSerializer):
title = serializers.SerializerMethodField()
content_type = serializers.SerializerMethodField()
class Meta: class Meta:
model = Lesson model = Lesson
fields = ['id', 'title', 'content_type', 'content_file', 'video_link', 'duration'] 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): class CourseLessonSerializer(serializers.ModelSerializer):
is_active = serializers.SerializerMethodField() is_active = serializers.SerializerMethodField()
is_complated = serializers.SerializerMethodField() is_complated = serializers.SerializerMethodField()
quizs = serializers.SerializerMethodField() quizs = serializers.SerializerMethodField()
permission = 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) content_file = serializers.FileField(source='lesson.content_file', read_only=True)
video_link = serializers.CharField(source='lesson.video_link', read_only=True) video_link = serializers.CharField(source='lesson.video_link', read_only=True)
duration = serializers.IntegerField(source='lesson.duration', read_only=True) duration = serializers.IntegerField(source='lesson.duration', read_only=True)
@ -24,6 +39,16 @@ class CourseLessonSerializer(serializers.ModelSerializer):
model = CourseLesson model = CourseLesson
fields = ['id', 'title', 'priority', 'is_active', 'permission', 'duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quizs'] 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): def get_is_active(self, obj):
return obj.is_active and self._has_access_for_object(obj) return obj.is_active and self._has_access_for_object(obj)
@ -93,11 +118,19 @@ class CourseChapterSerializer(serializers.ModelSerializer):
""" """
is_active = serializers.SerializerMethodField() is_active = serializers.SerializerMethodField()
lessons = serializers.SerializerMethodField() lessons = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
class Meta: class Meta:
model = CourseChapter model = CourseChapter
fields = ['id', 'title', 'priority', 'is_active', 'lessons'] 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): def get_is_active(self, obj):
if not obj.is_active: if not obj.is_active:
return False return False

11
apps/course/signals.py

@ -1,6 +1,6 @@
from apps.course.models import Course from apps.course.models import Course
from apps.chat.models import RoomMessage 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 apps.course.models.lesson import CourseLesson
from django.db.models import Q from django.db.models import Q
@ -14,10 +14,11 @@ UserModel = get_user_model()
@receiver(post_save, sender=Course) @receiver(post_save, sender=Course)
def handle_room_message_for_course(sender, instance, created, **kwargs): def handle_room_message_for_course(sender, instance, created, **kwargs):
title_str = extract_text_from_json(instance.title)
if created: # فقط برای موارد جدید اجرا شود if created: # فقط برای موارد جدید اجرا شود
RoomMessage.objects.create( 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, # استاد به‌عنوان سازنده اتاق initiator=instance.professor, # استاد به‌عنوان سازنده اتاق
course=instance, course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP room_type=RoomMessage.RoomTypeChoices.GROUP
@ -28,8 +29,8 @@ def handle_room_message_for_course(sender, instance, created, **kwargs):
course=instance, course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP room_type=RoomMessage.RoomTypeChoices.GROUP
).update( ).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 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) level = self.request.query_params.get('level', None)
if level: if level:
queryset = queryset.filter(level=level)
queryset = queryset.filter(level__contains=[{"title": level}])
status_param = self.request.query_params.get('status', None) status_param = self.request.query_params.get('status', None)
if status_param: 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) is_free_param = self.request.query_params.get('is_free', None)
if is_free_param is not 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) content_type = self.request.query_params.get('content_type', None)
if content_type: if content_type:
queryset = queryset.filter(content_type=content_type)
queryset = queryset.filter(content_type__contains=[{"title": content_type}])
return queryset return queryset

38
apps/course/views/course.py

@ -121,7 +121,7 @@ class CourseListAPIView(ListAPIView):
queryset = Course.objects.select_related( queryset = Course.objects.select_related(
'category', 'category',
'professor' 'professor'
).exclude(status=Course.StatusChoices.INACTIVE)
).exclude(status__contains=[{"title": Course.StatusChoices.INACTIVE}])
request = self.request request = self.request
filters = request.query_params filters = request.query_params
@ -129,12 +129,18 @@ class CourseListAPIView(ListAPIView):
# Handle category_slug with multiple values separated by commas # Handle category_slug with multiple values separated by commas
if category_slugs := filters.get('category_slug'): if category_slugs := filters.get('category_slug'):
category_slugs_list = category_slugs.split(',') 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 # Handle status with multiple values separated by commas
if statuses := filters.get('status'): if statuses := filters.get('status'):
statuses_list = statuses.split(',') 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'): if is_free := filters.get('is_free'):
is_free = is_free.lower() == 'true' is_free = is_free.lower() == 'true'
@ -186,6 +192,16 @@ class CourseDetailAPIView(RetrieveAPIView):
'room_messages' '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( @swagger_auto_schema(
operation_description=doc_course_detail(), operation_description=doc_course_detail(),
tags=['Imam-Javad - Course'], tags=['Imam-Javad - Course'],
@ -231,7 +247,7 @@ class MyCourseListAPIView(ListAPIView):
'chapters__lessons__lesson', 'chapters__lessons__lesson',
'chapters__lessons__completions', 'chapters__lessons__completions',
'participants__student' 'participants__student'
).exclude(status=Course.StatusChoices.INACTIVE)
).exclude(status__contains=[{"title": Course.StatusChoices.INACTIVE}])
request = self.request request = self.request
filters = request.query_params filters = request.query_params
@ -316,9 +332,8 @@ class AttachmentListAPIView(ListAPIView):
Optimized queryset with select_related for attachment relationship Optimized queryset with select_related for attachment relationship
""" """
course_slug = self.kwargs.get('slug') 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") raise NotFound("Course not found")
return CourseAttachment.objects.select_related( return CourseAttachment.objects.select_related(
'course', 'course',
@ -368,9 +383,8 @@ class GlossaryListAPIView(ListAPIView):
Optimized queryset with select_related for glossary relationship Optimized queryset with select_related for glossary relationship
""" """
course_slug = self.kwargs.get('slug') 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") raise NotFound("Course not found")
return CourseGlossary.objects.select_related( return CourseGlossary.objects.select_related(
@ -494,7 +508,9 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
detail_view = CourseDetailAPIView() detail_view = CourseDetailAPIView()
queryset = detail_view.get_queryset() 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 user = request.user
print(f"[Online Validate GET] Course found - course_id={course.id} slug={slug} is_online={course.is_online}") 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 Optimized queryset with select_related and prefetch_related for lesson relationships
""" """
course_slug = self.kwargs.get('slug') 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( return CourseLesson.objects.select_related(
'chapter', # 👇 Route through chapter 'chapter', # 👇 Route through chapter
@ -316,7 +316,7 @@ class LessonListV2APIView(ListAPIView):
def get_queryset(self): def get_queryset(self):
course_slug = self.kwargs.get('slug') 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 # We query the Chapters, and prefetch the lessons to avoid N+1 query problems
return CourseChapter.objects.prefetch_related( 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): def post(self, request, slug, *args, **kwargs):
# 1. Standard Permissions Logic # 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): if not request.user.can_manage_course(course):
raise AppAPIException({'message': 'Permission denied'}, status_code=403) 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}") logger.info(f"[LiveSession Token] Request from user_id={user.id} for course={course_slug}")
try: 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: except Course.DoesNotExist:
logger.warning(f"[LiveSession Token] Course not found - course={course_slug} user_id={user.id}") 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) 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. for the admin course participants table.
""" """
course_slug = self.kwargs.get('slug') 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"}) raise AppAPIException({'message': "Course not found"})
return StudentUser.objects.select_related().filter( return StudentUser.objects.select_related().filter(

Loading…
Cancel
Save