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