From 1c145fbfb77b52a7b7adb8793eb99a3c3de5a95b Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Wed, 17 Jun 2026 17:10:54 +0330 Subject: [PATCH] cloned course logic added in the backend for admin panel --- apps/course/serializers/admin.py | 7 +- apps/course/views/admin.py | 154 +++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/apps/course/serializers/admin.py b/apps/course/serializers/admin.py index 2573c3f..b7181cb 100644 --- a/apps/course/serializers/admin.py +++ b/apps/course/serializers/admin.py @@ -270,6 +270,12 @@ class AdminCourseDetailSerializer(serializers.ModelSerializer): def to_representation(self, instance): ret = super().to_representation(instance) lang = get_request_lang(self) + ret['title'] = get_localized_field(lang, instance.title) + ret['slug'] = get_localized_field(lang, instance.slug) + ret['level'] = get_localized_field(lang, instance.level) + ret['status'] = get_localized_field(lang, instance.status) + ret['description'] = get_localized_field(lang, instance.description) + ret['short_description'] = get_localized_field(lang, instance.short_description) # Inject legacy fields first_prof = instance.professors.first() ret['professor'] = first_prof.id if first_prof else None @@ -673,4 +679,3 @@ class AdminLiveSessionDetailSerializer(serializers.ModelSerializer): }) return data - diff --git a/apps/course/views/admin.py b/apps/course/views/admin.py index eefce15..4ef39e1 100644 --- a/apps/course/views/admin.py +++ b/apps/course/views/admin.py @@ -1,4 +1,6 @@ import logging +import re +from copy import deepcopy from django.db import transaction from django.db.models import Count, Q @@ -27,6 +29,8 @@ from apps.course.models import ( LiveSessionUser, LiveSessionRecording ) +from apps.course.models.course import extract_text_from_json +from apps.quiz.models import Quiz, Question from apps.course.serializers.admin import ( AdminCourseCategorySerializer, AdminCourseListSerializer, @@ -134,6 +138,156 @@ class AdminCourseViewSet(viewsets.ModelViewSet): else: serializer.save() + def _build_cloned_title(self, source_course, requested_title=None): + custom_title = str(requested_title or "").strip() + if custom_title: + return [{"language_code": "en", "title": custom_title}] + + source_titles = deepcopy(source_course.title or []) + source_title_text = extract_text_from_json(source_titles).strip() or f"Course {source_course.pk}" + base_title = re.sub(r"\s*\(copied(?:#\d+)?\)$", "", source_title_text).strip() or source_title_text + suffix = "(copied)" + + if isinstance(source_titles, list) and source_titles: + localized_titles = [] + for item in source_titles: + if isinstance(item, dict): + localized_item = deepcopy(item) + for key in ("title", "text", "value", "name"): + if key in localized_item and localized_item.get(key): + cleaned_value = re.sub(r"\s*\(copied(?:#\d+)?\)$", "", str(localized_item[key])).strip() + localized_item[key] = f"{cleaned_value}{suffix}" + break + else: + localized_item["title"] = f"{base_title}{suffix}" + localized_titles.append(localized_item) + elif item: + cleaned_item = re.sub(r"\s*\(copied(?:#\d+)?\)$", "", str(item)).strip() + localized_titles.append(f"{cleaned_item}{suffix}") + + if localized_titles: + return localized_titles + + return [{"language_code": "en", "title": f"{base_title}{suffix}"}] + + @action(detail=True, methods=['post']) + def clone(self, request, pk=None): + source_course = self.get_object() + requested_title = request.data.get('title') + + with transaction.atomic(): + cloned_course = Course.objects.create( + title=self._build_cloned_title(source_course, requested_title), + category=source_course.category, + thumbnail=source_course.thumbnail.name if source_course.thumbnail else None, + video_type=source_course.video_type, + video_file=source_course.video_file.name if source_course.video_file else None, + video_link=source_course.video_link, + is_online=source_course.is_online, + online_link=source_course.online_link, + level=deepcopy(source_course.level), + duration=source_course.duration, + lessons_count=source_course.lessons_count, + description=deepcopy(source_course.description), + short_description=deepcopy(source_course.short_description), + status=deepcopy(source_course.status), + is_free=source_course.is_free, + price=source_course.price, + price_rub=source_course.price_rub, + discount_percentage=source_course.discount_percentage, + is_group_chat_locked=source_course.is_group_chat_locked, + is_professor_chat_locked=source_course.is_professor_chat_locked, + timing=deepcopy(source_course.timing), + features=deepcopy(source_course.features), + ) + cloned_course.professors.set(source_course.professors.all()) + + chapter_map = {} + source_chapters = source_course.chapters.all().order_by('priority', 'id') + for chapter in source_chapters: + cloned_chapter = CourseChapter.objects.create( + course=cloned_course, + title=deepcopy(chapter.title), + priority=chapter.priority, + is_active=chapter.is_active, + ) + chapter_map[chapter.id] = cloned_chapter + + lesson_map = {} + source_lessons = source_course.lessons.select_related('lesson', 'chapter').all().order_by('priority', 'id') + for course_lesson in source_lessons: + base_lesson = course_lesson.lesson + cloned_lesson = Lesson.objects.create( + title=deepcopy(base_lesson.title), + content_type=deepcopy(base_lesson.content_type), + content_file=base_lesson.content_file.name if base_lesson and base_lesson.content_file else None, + video_link=base_lesson.video_link if base_lesson else None, + duration=base_lesson.duration if base_lesson else 0, + ) + cloned_course_lesson = CourseLesson.objects.create( + course=cloned_course, + chapter=chapter_map.get(course_lesson.chapter_id), + lesson=cloned_lesson, + title=deepcopy(course_lesson.title), + priority=course_lesson.priority, + is_active=course_lesson.is_active, + ) + lesson_map[course_lesson.id] = cloned_course_lesson + + source_attachments = source_course.attachments.select_related('attachment').all() + for course_attachment in source_attachments: + base_attachment = course_attachment.attachment + cloned_attachment = Attachment.objects.create( + title=deepcopy(base_attachment.title), + file=base_attachment.file.name if base_attachment and base_attachment.file else None, + file_size=base_attachment.file_size if base_attachment else None, + ) + CourseAttachment.objects.create( + course=cloned_course, + attachment=cloned_attachment, + ) + + source_glossaries = source_course.glossaries.select_related('glossary').all() + for course_glossary in source_glossaries: + base_glossary = course_glossary.glossary + cloned_glossary = Glossary.objects.create( + title=base_glossary.title, + description=deepcopy(base_glossary.description), + ) + CourseGlossary.objects.create( + course=cloned_course, + glossary=cloned_glossary, + ) + + source_quizzes = source_course.quizzes.prefetch_related('questions').all() + for quiz in source_quizzes: + cloned_quiz = Quiz.objects.create( + lesson=lesson_map.get(quiz.lesson_id), + course=cloned_course, + lesson_number=quiz.lesson_number, + title=deepcopy(quiz.title), + description=deepcopy(quiz.description), + each_question_timing=quiz.each_question_timing, + status=quiz.status, + ) + for question in quiz.questions.all(): + Question.objects.create( + quiz=cloned_quiz, + question=deepcopy(question.question), + option1=deepcopy(question.option1), + option2=deepcopy(question.option2), + option3=deepcopy(question.option3), + option4=deepcopy(question.option4), + correct_answer=question.correct_answer, + priority=question.priority, + ) + + cloned_course.recalculate_lessons_count() + cloned_course.refresh_from_db() + + serializer = self.get_serializer(cloned_course) + return Response(serializer.data, status=201) + class AdminChapterViewSet(viewsets.ModelViewSet): """