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.
 
 
 
 

117 lines
4.9 KiB

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!"
))