From 81e3c73fdeb36f466daccf7cc42d9d4c44461e9f Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Sat, 6 Jun 2026 09:58:32 +0330 Subject: [PATCH] course fileds updated for multilangual feat . serializers synced and dropdown values handle multilangual fileds statically --- .../0009_alter_course_description_and_more.py | 38 ++++ apps/course/models/course.py | 204 +++++++++++++++++- apps/course/serializers/course.py | 16 ++ apps/course/views/admin.py | 4 + 4 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 apps/course/migrations/0009_alter_course_description_and_more.py diff --git a/apps/course/migrations/0009_alter_course_description_and_more.py b/apps/course/migrations/0009_alter_course_description_and_more.py new file mode 100644 index 0000000..fb59e4b --- /dev/null +++ b/apps/course/migrations/0009_alter_course_description_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.12 on 2026-06-06 09:11 + +from django.db import migrations, models + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0008_remove_course_course_cour_status_57ffd9_idx_and_more'), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunSQL( + sql=[ + "ALTER TABLE course_course ALTER COLUMN description TYPE jsonb USING CASE WHEN description IS NULL OR description = '' THEN '[]'::jsonb ELSE jsonb_build_array(jsonb_build_object('title', description, 'language_code', 'en')) END;", + "ALTER TABLE course_course ALTER COLUMN short_description TYPE jsonb USING CASE WHEN short_description IS NULL OR short_description = '' THEN '[]'::jsonb ELSE jsonb_build_array(jsonb_build_object('title', short_description, 'language_code', 'en')) END;" + ], + reverse_sql=[ + "ALTER TABLE course_course ALTER COLUMN description TYPE text USING CASE WHEN jsonb_array_length(description) > 0 THEN description->0->>'title' ELSE '' END;", + "ALTER TABLE course_course ALTER COLUMN short_description TYPE varchar(500) USING CASE WHEN jsonb_array_length(short_description) > 0 THEN LEFT(short_description->0->>'title', 500) ELSE '' END;" + ] + ) + ], + state_operations=[ + migrations.AlterField( + model_name='course', + name='description', + field=models.JSONField(blank=True, default=list, null=True, verbose_name='Course Description'), + ), + migrations.AlterField( + model_name='course', + name='short_description', + field=models.JSONField(blank=True, default=list, null=True, verbose_name='Short Description'), + ), + ] + ) + ] diff --git a/apps/course/models/course.py b/apps/course/models/course.py index b0bef47..c90e15b 100644 --- a/apps/course/models/course.py +++ b/apps/course/models/course.py @@ -113,6 +113,130 @@ class CourseCategory(models.Model): def course_count(self): return self.courses.exclude(status="inactive").count() +LEVEL_TRANSLATIONS = { + 'beginner': { + 'en': 'Beginner', + 'ru': 'Начальный', + }, + 'mid': { + 'en': 'Mid Level', + 'ru': 'Средний', + }, + 'advanced': { + 'en': 'Advanced', + 'ru': 'Продвинутый', + } +} + +STATUS_TRANSLATIONS = { + 'inactive': { + 'en': 'Inactive', + 'ru': 'Неактивен', + }, + 'upcoming': { + 'en': 'Upcoming', + 'ru': 'Предстоящий', + }, + 'registering': { + 'en': 'Registering', + 'ru': 'Регистрация', + }, + 'ongoing': { + 'en': 'Ongoing', + 'ru': 'В процессе', + }, + 'finished': { + 'en': 'Finished', + 'ru': 'Завершен', + } +} + +def normalize_level(value): + if not value: + return 'beginner' + text = extract_text_from_json(value).lower().strip() + if 'beginner' in text or 'начальн' in text or 'начинающ' in text: + return 'beginner' + if 'mid' in text or 'средн' in text: + return 'mid' + if 'advanced' in text or 'продвинут' in text: + return 'advanced' + return 'beginner' + +def normalize_status(value): + if not value: + return 'inactive' + text = extract_text_from_json(value).lower().strip() + if 'inactive' in text or 'неактив' in text: + return 'inactive' + if 'upcoming' in text or 'предстоящ' in text: + return 'upcoming' + if 'registering' in text or 'регистрац' in text: + return 'registering' + if 'ongoing' in text or 'в процессе' in text: + return 'ongoing' + if 'finished' in text or 'заверш' in text or 'законч' in text: + return 'finished' + return 'inactive' + +WEEKDAY_TRANSLATIONS = { + 'monday': { + 'en': 'Monday', + 'ru': 'Понедельник', + 'fa': 'دوشنبه', + }, + 'tuesday': { + 'en': 'Tuesday', + 'ru': 'Вторник', + 'fa': 'سه‌شنبه', + }, + 'wednesday': { + 'en': 'Wednesday', + 'ru': 'Среда', + 'fa': 'چهارشنبه', + }, + 'thursday': { + 'en': 'Thursday', + 'ru': 'Четверг', + 'fa': 'پنج‌شنبه', + }, + 'friday': { + 'en': 'Friday', + 'ru': 'Пятница', + 'fa': 'جمعه', + }, + 'saturday': { + 'en': 'Saturday', + 'ru': 'Суббота', + 'fa': 'شنبه', + }, + 'sunday': { + 'en': 'Sunday', + 'ru': 'Воскресенье', + 'fa': 'یک‌شنبه', + } +} + +def normalize_weekday_to_key(day_value): + if not day_value: + return 'saturday' + txt = str(day_value).lower().strip() + if 'monday' in txt or 'понедельник' in txt or 'دوشنبه' in txt: + return 'monday' + if 'tuesday' in txt or 'вторник' in txt or 'سه شنبه' in txt or 'سه‌شنبه' in txt: + return 'tuesday' + if 'wednesday' in txt or 'среда' in txt or 'چهارشنبه' in txt: + return 'wednesday' + if 'thursday' in txt or 'четверг' in txt or 'پنج شنبه' in txt or 'پنج‌شنبه' in txt: + return 'thursday' + if 'friday' in txt or 'пятница' in txt or 'جمعه' in txt: + return 'friday' + if 'saturday' in txt or 'суббота' in txt or 'شنبه' in txt: + return 'saturday' + if 'sunday' in txt or 'воскресенье' in txt or 'یک شنبه' in txt or 'یک‌شنبه' in txt: + return 'sunday' + return 'saturday' + class Course(models.Model): class LevelChoices(TextChoices): @@ -163,8 +287,8 @@ class Course(models.Model): 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")) + description = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Course Description')) + short_description = models.JSONField(default=list, null=True, blank=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')) @@ -225,9 +349,79 @@ class Course(models.Model): 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) + + level_key = normalize_level(self.level) + self.level = [ + {"title": LEVEL_TRANSLATIONS[level_key]["en"], "language_code": "en"}, + {"title": LEVEL_TRANSLATIONS[level_key]["ru"], "language_code": "ru"}, + ] + + status_key = normalize_status(self.status) + self.status = [ + {"title": STATUS_TRANSLATIONS[status_key]["en"], "language_code": "en"}, + {"title": STATUS_TRANSLATIONS[status_key]["ru"], "language_code": "ru"}, + ] + + self.description = format_multilingual_field(self.description) + self.short_description = format_multilingual_field(self.short_description) + # Process timing (translate weekdays) + import json + raw_timings = [] + val = self.timing + if val: + if isinstance(val, str): + try: + val = json.loads(val) + except Exception: + pass + + if isinstance(val, list) and all(isinstance(x, dict) and 'language_code' in x for x in val): + # We can extract the timing list from any language item + extracted = [] + for x in val: + title_val = x.get('title') + if title_val: + if isinstance(title_val, str): + try: + title_val = json.loads(title_val) + except Exception: + pass + if isinstance(title_val, list): + extracted = title_val + break + raw_timings = extracted + elif isinstance(val, list): + raw_timings = val + elif isinstance(val, dict): + raw_timings = [] + for k, v in val.items(): + raw_timings.append({"day": k, "time": str(v)}) + + en_timings = [] + ru_timings = [] + fa_timings = [] + for item in raw_timings: + if isinstance(item, dict): + day_key = normalize_weekday_to_key(item.get('day')) + time_val = item.get('time') or item.get('timing') or '' + en_timings.append({ + "day": WEEKDAY_TRANSLATIONS.get(day_key, {}).get('en', item.get('day', '')), + "time": time_val + }) + ru_timings.append({ + "day": WEEKDAY_TRANSLATIONS.get(day_key, {}).get('ru', item.get('day', '')), + "time": time_val + }) + fa_timings.append({ + "day": WEEKDAY_TRANSLATIONS.get(day_key, {}).get('fa', item.get('day', '')), + "time": time_val + }) + + self.timing = [ + {"language_code": "fa", "title": json.dumps(fa_timings)}, + {"language_code": "en", "title": json.dumps(en_timings)}, + {"language_code": "ru", "title": json.dumps(ru_timings)}, + ] self.features = format_multilingual_field(self.features) # Ensure consistency: if price is 0, set is_free to True and discount_percentage to 0 diff --git a/apps/course/serializers/course.py b/apps/course/serializers/course.py index 0e4ca39..eb848c4 100644 --- a/apps/course/serializers/course.py +++ b/apps/course/serializers/course.py @@ -46,6 +46,7 @@ class CourseListSerializer(serializers.ModelSerializer): slug = serializers.SerializerMethodField() level = serializers.SerializerMethodField() status = serializers.SerializerMethodField() + short_description = serializers.SerializerMethodField() class Meta: model = Course @@ -85,6 +86,9 @@ class CourseListSerializer(serializers.ModelSerializer): def get_status(self, obj): return get_localized_field(self._lang(), obj.status) + def get_short_description(self, obj): + return get_localized_field(self._lang(), obj.short_description) + def get_thumbnail(self, obj): return get_thumbs(obj.thumbnail, self.context.get('request')) @@ -146,6 +150,8 @@ class CourseDetailSerializer(serializers.ModelSerializer): status = serializers.SerializerMethodField() timing = serializers.SerializerMethodField() features = serializers.SerializerMethodField() + description = serializers.SerializerMethodField() + short_description = serializers.SerializerMethodField() class Meta: model = Course @@ -415,6 +421,12 @@ class CourseDetailSerializer(serializers.ModelSerializer): def get_status(self, obj): return get_localized_field(self._lang(), obj.status) + def get_description(self, obj): + return get_localized_field(self._lang(), obj.description) + + def get_short_description(self, obj): + return get_localized_field(self._lang(), obj.short_description) + def _parse_builder_payload(self, value): if value is None: return None @@ -483,6 +495,7 @@ class MyCourseListSerializer(serializers.ModelSerializer): title = serializers.SerializerMethodField() slug = serializers.SerializerMethodField() status = serializers.SerializerMethodField() + short_description = serializers.SerializerMethodField() class Meta: model = Course @@ -571,6 +584,9 @@ class MyCourseListSerializer(serializers.ModelSerializer): def get_status(self, obj): return get_localized_field(self._lang(), obj.status) + + def get_short_description(self, obj): + return get_localized_field(self._lang(), obj.short_description) class AttachmentSerializer(serializers.ModelSerializer): diff --git a/apps/course/views/admin.py b/apps/course/views/admin.py index b04b19c..f1125e6 100644 --- a/apps/course/views/admin.py +++ b/apps/course/views/admin.py @@ -91,6 +91,10 @@ class AdminCourseViewSet(viewsets.ModelViewSet): elif is_free_param.lower() == 'false': queryset = queryset.filter(is_free=False) + student_id = self.request.query_params.get('student_id', None) + if student_id: + queryset = queryset.filter(participants__student_id=student_id) + return queryset.order_by('-id') def perform_create(self, serializer):