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

372 lines
14 KiB

import os
from decimal import Decimal
import math
from django.db import models
from django.db.models import TextChoices
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, 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):
return os.path.join(f"courses/{instance.slug}/videos/{filename}")
def attachment_file_upload_to(instance, filename):
return os.path.join(f"attachments/{filename}")
def course_attachment_file_upload_to(instance, filename):
return os.path.join(f"courses/{instance.course.slug}/attachments/{filename}")
class CourseCategory(models.Model):
name = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Category Name'))
slug = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Slug'))
def __str__(self):
return extract_text_from_json(self.name)
def save(self, *args, **kwargs):
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
def course_count(self):
return self.courses.exclude(status="inactive").count()
class Course(models.Model):
class LevelChoices(TextChoices):
BEGINNER = 'beginner', _('Beginner')
MID = 'mid', _('Mid Level')
ADVANCED = 'advanced', _('Advanced')
class StatusChoices(TextChoices):
INACTIVE = 'inactive', _('Inactive') # Not Active (does not show)
UPCOMING = 'upcoming', _('Upcoming') # Upcoming (visible but registration not allowed)-Предстоящие
REGISTERING = 'registering', _('Registering') # Registering (registration is open)-регистрация
ONGOING = 'ongoing', _('Ongoing') # Ongoing (course has started, registration closed)-В процессе
FINISHED = 'finished', _('Finished') # Finished (course has ended)-закончился
class VedioTypeChoices(models.TextChoices):
YOUTUBE_LINK = 'youtube_link', _('Youtube Link')
VIDEO_FILE = 'video_file', _('Video File')
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,
on_delete=models.CASCADE,
related_name="courses",
verbose_name=_("Professor")
)
thumbnail = models.ImageField(upload_to="courses/thumbnails/", verbose_name=_('Thumbnail'))
video_type = models.CharField(
max_length=20,
choices=VedioTypeChoices.choices,
verbose_name=_('Preview Video Type (YouTube Link or File Upload)')
)
video_file = models.FileField(
upload_to=course_file_upload_to,
null=True,
blank=True,
verbose_name=_("Video File")
)
video_link = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Video Link"))
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.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.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'))
final_price = models.DecimalField(
verbose_name=_('Course Final Price'), decimal_places=2, max_digits=10, default=0.00, blank=True,
help_text=_('This field is automatically calculated based on the discount percentage.')
)
is_group_chat_locked = models.BooleanField(
default=False,
verbose_name=_('Lock Group Chat')
)
is_professor_chat_locked = models.BooleanField(
default=False,
verbose_name=_('Lock Private Chats with Professor')
)
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 extract_text_from_json(self.title)
def get_completed_lessons_count(self, student):
return self.lessons.filter(completions__student=student).count()
def is_student_participant(self, student):
return self.participants.filter(student=student).exists()
@classmethod
def recalculate_lessons_count_for_course(cls, course_id):
if not course_id:
return 0
from apps.course.models.lesson import CourseLesson
lessons_count = CourseLesson.objects.filter(
course_id=course_id,
is_active=True,
).count()
cls.objects.filter(pk=course_id).update(lessons_count=lessons_count)
return lessons_count
def recalculate_lessons_count(self):
return self.__class__.recalculate_lessons_count_for_course(self.pk)
def save(self, *args, **kwargs):
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:
self.is_free = True
self.discount_percentage = 0
self.final_price = Decimal('0.00')
elif self.is_free:
self.price = Decimal('0.00')
self.discount_percentage = 0
self.final_price = Decimal('0.00')
elif self.discount_percentage > 0:
discount_amount = (self.price * self.discount_percentage) / 100
final_price = self.price - discount_amount
self.final_price = Decimal(math.ceil(final_price)).quantize(Decimal('0.00'))
else:
self.final_price = Decimal(math.ceil(self.price)).quantize(Decimal('0.00'))
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Course")
verbose_name_plural = _("Courses")
indexes = [
models.Index(fields=['is_free']),
models.Index(fields=['created_at']),
]
class Glossary(models.Model):
"""
Base Glossary model that contains the actual content
"""
title = models.CharField(max_length=555, verbose_name=_('Glossary Title'))
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")
class CourseGlossary(models.Model):
"""
Intermediate model that connects Course with Glossary
"""
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='glossaries', verbose_name=_('Course'))
glossary = models.ForeignKey(Glossary, on_delete=models.CASCADE, related_name='course_glossaries', verbose_name=_('Glossary'))
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 f"{extract_text_from_json(self.course.title)} - {self.glossary.title}"
@property
def title(self):
return self.glossary.title
@property
def description(self):
return self.glossary.description
class Meta:
ordering = ("-id",)
verbose_name = _("Course Glossary")
verbose_name_plural = _("Course Glossaries")
class Attachment(models.Model):
"""
Base Attachment model that contains the actual file
"""
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')
)
file_size = models.PositiveIntegerField(verbose_name=_('File Size (in bytes)'), null=True, blank=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 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 extract_text_from_json(self.title)
class Meta:
verbose_name = _("Attachment")
verbose_name_plural = _("Attachments")
class CourseAttachment(models.Model):
"""
Intermediate model that connects Course with Attachment
"""
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='attachments', verbose_name=_('Course'))
attachment = models.ForeignKey(Attachment, on_delete=models.CASCADE, related_name='course_attachments', verbose_name=_('Attachment'))
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 f"{extract_text_from_json(self.course.title)} - {extract_text_from_json(self.attachment.title)}"
@property
def title(self):
return self.attachment.title
@property
def file(self):
return self.attachment.file
@property
def file_size(self):
return self.attachment.file_size
class Meta:
ordering = ("-id",)
verbose_name = _("Course Attachment")
verbose_name_plural = _("Course Attachments")
indexes = [
models.Index(fields=['course']),
models.Index(fields=['attachment']),
]