5 changed files with 279 additions and 3 deletions
-
33apps/account/serializers/user.py
-
7apps/account/urls.py
-
121apps/account/views/user.py
-
117apps/course/management/commands/redistribute_lessons_to_chapters.py
-
2entrypoint.sh
@ -0,0 +1,117 @@ |
|||
import random |
|||
import sys |
|||
from django.core.management.base import BaseCommand |
|||
from django.db import transaction |
|||
from apps.course.models import Course, CourseChapter, CourseLesson |
|||
|
|||
class Command(BaseCommand): |
|||
help = 'Redistributes course lessons into multiple chapters randomly (maintaining order) for courses with >= 2 lessons.' |
|||
|
|||
def add_arguments(self, parser): |
|||
parser.add_argument( |
|||
'--dry-run', |
|||
action='store_true', |
|||
help='Simulates the redistribution without modifying the database.', |
|||
) |
|||
parser.add_argument( |
|||
'--max-chapters', |
|||
type=int, |
|||
default=5, |
|||
help='Maximum number of chapters to create for a course (default: 5).', |
|||
) |
|||
|
|||
def handle(self, *args, **options): |
|||
# Reconfigure stdout to UTF-8 to prevent UnicodeEncodeError on Windows terminals |
|||
try: |
|||
sys.stdout.reconfigure(encoding='utf-8') |
|||
except AttributeError: |
|||
pass |
|||
|
|||
dry_run = options['dry_run'] |
|||
max_chapters = options['max_chapters'] |
|||
|
|||
if dry_run: |
|||
self.stdout.write(self.style.WARNING("⚠️ DRY RUN MODE: No database changes will be saved.")) |
|||
|
|||
self.stdout.write(self.style.WARNING("Starting lesson redistribution...")) |
|||
|
|||
courses = Course.objects.all() |
|||
courses_updated = 0 |
|||
total_chapters_created = 0 |
|||
total_lessons_redistributed = 0 |
|||
|
|||
# Wrap everything in an atomic transaction to ensure safety |
|||
with transaction.atomic(): |
|||
for course in courses: |
|||
lessons = list(course.lessons.all().order_by('priority')) |
|||
n = len(lessons) |
|||
|
|||
if n < 2: |
|||
self.stdout.write(self.style.NOTICE( |
|||
f"Skipping course: '{course.title}' (ID: {course.id}) - Only has {n} lesson(s)." |
|||
)) |
|||
continue |
|||
|
|||
# Number of chapters C must be between 2 and min(N, max_chapters) |
|||
c = random.randint(2, min(n, max_chapters)) |
|||
|
|||
self.stdout.write(self.style.MIGRATE_HEADING( |
|||
f"\nReorganizing Course: '{course.title}' (ID: {course.id})" |
|||
f"\n Total lessons: {n} | Splitting into {c} chapters..." |
|||
)) |
|||
|
|||
# Partition the ordered lessons list into C slices |
|||
# We randomly pick C-1 unique split boundaries between 1 and n-1 |
|||
split_points = sorted(random.sample(range(1, n), c - 1)) |
|||
|
|||
partitioned_lessons = [] |
|||
last_idx = 0 |
|||
for sp in split_points: |
|||
partitioned_lessons.append(lessons[last_idx:sp]) |
|||
last_idx = sp |
|||
partitioned_lessons.append(lessons[last_idx:]) |
|||
|
|||
if not dry_run: |
|||
# 1. Unlink lessons from chapters first to prevent CASCADE deletion. |
|||
# Using .update() bypasses the save() method and avoids AttributeErrors |
|||
# caused by _adjust_priorities when chapter is None. |
|||
course.lessons.all().update(chapter=None) |
|||
|
|||
# 2. Delete all existing chapters for this course |
|||
course.chapters.all().delete() |
|||
|
|||
# 3. Create the new chapters and assign the sliced lessons |
|||
for i, chapter_lessons in enumerate(partitioned_lessons): |
|||
chapter_title = f"فصل {i+1}" |
|||
priority = i + 1 |
|||
|
|||
self.stdout.write(f" 📂 Chapter '{chapter_title}' (priority {priority}) contains:") |
|||
for idx, lesson in enumerate(chapter_lessons): |
|||
self.stdout.write(f" - [{idx + 1}] {lesson.title} (Original ID: {lesson.id})") |
|||
|
|||
if not dry_run: |
|||
# Create the new chapter |
|||
chapter = CourseChapter.objects.create( |
|||
course=course, |
|||
title=chapter_title, |
|||
priority=priority |
|||
) |
|||
|
|||
# Assign and save each lesson within this chapter |
|||
for idx, lesson in enumerate(chapter_lessons): |
|||
lesson.chapter = chapter |
|||
lesson.priority = idx + 1 |
|||
lesson.save() |
|||
|
|||
total_chapters_created += 1 |
|||
total_lessons_redistributed += len(chapter_lessons) |
|||
|
|||
courses_updated += 1 |
|||
|
|||
if dry_run: |
|||
self.stdout.write(self.style.WARNING("\n⚠️ Dry run complete. No database changes were made.")) |
|||
else: |
|||
self.stdout.write(self.style.SUCCESS( |
|||
f"\n🎉 Successfully redistributed {total_lessons_redistributed} lessons " |
|||
f"across {total_chapters_created} chapters in {courses_updated} courses!" |
|||
)) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue