15 changed files with 607 additions and 219 deletions
-
2apps/account/templates/account/json_editor_field.html
-
22apps/api/views.py
-
92apps/course/admin/course.py
-
56apps/course/admin/lesson.py
-
0apps/course/management/__init__.py
-
0apps/course/management/commands/__init__.py
-
134apps/course/management/commands/clear_course_data.py
-
132apps/course/migrations/0004_alter_attachment_options_alter_glossary_options_and_more.py
-
88apps/course/models/course.py
-
85apps/course/models/lesson.py
-
21apps/course/serializers/course.py
-
29apps/course/serializers/lesson.py
-
14apps/course/views/course.py
-
69apps/course/views/lesson.py
-
82config/settings/base.py
@ -0,0 +1,134 @@ |
|||
from django.core.management.base import BaseCommand |
|||
from django.db import transaction, connection |
|||
from django.db.models import ProtectedError |
|||
from django.utils.translation import gettext_lazy as _ |
|||
|
|||
from apps.course.models import ( |
|||
Course, CourseCategory, |
|||
Lesson, CourseLesson, LessonCompletion, |
|||
Attachment, CourseAttachment, |
|||
Glossary, CourseGlossary, |
|||
Participant |
|||
) |
|||
|
|||
|
|||
class Command(BaseCommand): |
|||
help = _('Clear all course-related data from the database') |
|||
|
|||
def add_arguments(self, parser): |
|||
parser.add_argument( |
|||
'--force', |
|||
action='store_true', |
|||
dest='force', |
|||
help=_('Force deletion without confirmation'), |
|||
) |
|||
parser.add_argument( |
|||
'--model', |
|||
type=str, |
|||
dest='model', |
|||
help=_('Specify a single model to clear (e.g., "Course", "Lesson", etc.)'), |
|||
) |
|||
parser.add_argument( |
|||
'--legacy-only', |
|||
action='store_true', |
|||
dest='legacy_only', |
|||
help=_('Clear only legacy models (before migration to new structure)'), |
|||
) |
|||
|
|||
def table_exists(self, table_name): |
|||
"""Check if a table exists in the database.""" |
|||
with connection.cursor() as cursor: |
|||
cursor.execute( |
|||
""" |
|||
SELECT EXISTS ( |
|||
SELECT FROM information_schema.tables |
|||
WHERE table_schema = 'public' |
|||
AND table_name = %s |
|||
); |
|||
""", |
|||
[table_name] |
|||
) |
|||
return cursor.fetchone()[0] |
|||
|
|||
def handle(self, *args, **options): |
|||
force = options['force'] |
|||
specific_model = options.get('model') |
|||
legacy_only = options.get('legacy_only') |
|||
|
|||
if not force and not specific_model: |
|||
confirm = input(_('This will delete ALL course-related data. Are you sure? (yes/no): ')) |
|||
if confirm.lower() != 'yes': |
|||
self.stdout.write(self.style.WARNING(_('Operation cancelled.'))) |
|||
return |
|||
|
|||
# Define all models |
|||
all_models = { |
|||
'Course': (Course, 'course_course'), |
|||
'CourseCategory': (CourseCategory, 'course_coursecategory'), |
|||
'Lesson': (Lesson, 'course_lesson'), |
|||
'CourseLesson': (CourseLesson, 'course_courselesson'), |
|||
'LessonCompletion': (LessonCompletion, 'course_lessoncompletion'), |
|||
'Attachment': (Attachment, 'course_attachment'), |
|||
'CourseAttachment': (CourseAttachment, 'course_courseattachment'), |
|||
'Glossary': (Glossary, 'course_glossary'), |
|||
'CourseGlossary': (CourseGlossary, 'course_courseglossary'), |
|||
'Participant': (Participant, 'course_participant'), |
|||
} |
|||
|
|||
# Legacy models (before migration) |
|||
legacy_models = { |
|||
'Course': (Course, 'course_course'), |
|||
'CourseCategory': (CourseCategory, 'course_coursecategory'), |
|||
'Lesson': (Lesson, 'course_lesson'), |
|||
'LessonCompletion': (LessonCompletion, 'course_lessoncompletion'), |
|||
'Attachment': (Attachment, 'course_attachment'), |
|||
'Glossary': (Glossary, 'course_glossary'), |
|||
'Participant': (Participant, 'course_participant'), |
|||
} |
|||
|
|||
models_to_use = legacy_models if legacy_only else all_models |
|||
|
|||
if specific_model: |
|||
# Clear only the specified model |
|||
if specific_model not in models_to_use: |
|||
self.stdout.write(self.style.ERROR(_(f'Unknown model: {specific_model}'))) |
|||
self.stdout.write(self.style.WARNING(_(f'Available models: {", ".join(models_to_use.keys())}'))) |
|||
return |
|||
|
|||
model_info = models_to_use[specific_model] |
|||
models_to_clear = [(specific_model, model_info[0], model_info[1])] |
|||
else: |
|||
# Clear all models in the correct order to avoid foreign key constraints |
|||
models_to_clear = [] |
|||
|
|||
# Order matters for foreign key constraints |
|||
model_order = [ |
|||
'LessonCompletion', 'CourseLesson', 'Lesson', |
|||
'CourseAttachment', 'Attachment', |
|||
'CourseGlossary', 'Glossary', |
|||
'Participant', 'Course', 'CourseCategory' |
|||
] |
|||
|
|||
for model_name in model_order: |
|||
if model_name in models_to_use: |
|||
model_info = models_to_use[model_name] |
|||
models_to_clear.append((model_name, model_info[0], model_info[1])) |
|||
|
|||
# Process each model |
|||
for model_name, model_class, table_name in models_to_clear: |
|||
# Check if the table exists |
|||
if not self.table_exists(table_name): |
|||
self.stdout.write(self.style.WARNING(_(f'Table {table_name} does not exist, skipping {model_name}'))) |
|||
continue |
|||
|
|||
try: |
|||
count = model_class.objects.count() |
|||
model_class.objects.all().delete() |
|||
self.stdout.write(self.style.SUCCESS(_(f'Deleted {count} {model_name} records'))) |
|||
except ProtectedError as e: |
|||
self.stdout.write(self.style.ERROR(_(f'Could not delete {model_name} records due to protected foreign keys'))) |
|||
self.stdout.write(self.style.ERROR(str(e))) |
|||
except Exception as e: |
|||
self.stdout.write(self.style.ERROR(_(f'Error deleting {model_name} records: {str(e)}'))) |
|||
|
|||
self.stdout.write(self.style.SUCCESS(_('Course data clearing completed'))) |
|||
@ -0,0 +1,132 @@ |
|||
# Generated by Django 5.1.8 on 2025-04-13 01:35 |
|||
|
|||
import django.db.models.deletion |
|||
import django.utils.timezone |
|||
from django.db import migrations, models |
|||
|
|||
|
|||
class Migration(migrations.Migration): |
|||
|
|||
dependencies = [ |
|||
('account', '0002_alter_user_phone_number'), |
|||
('course', '0003_alter_course_is_online_alter_course_timing_and_more'), |
|||
] |
|||
|
|||
operations = [ |
|||
migrations.AlterModelOptions( |
|||
name='attachment', |
|||
options={'verbose_name': 'Attachment', 'verbose_name_plural': 'Attachments'}, |
|||
), |
|||
migrations.AlterModelOptions( |
|||
name='glossary', |
|||
options={'verbose_name': 'Glossary', 'verbose_name_plural': 'Glossaries'}, |
|||
), |
|||
migrations.RemoveField( |
|||
model_name='attachment', |
|||
name='course', |
|||
), |
|||
migrations.RemoveField( |
|||
model_name='glossary', |
|||
name='course', |
|||
), |
|||
migrations.RemoveField( |
|||
model_name='lesson', |
|||
name='course', |
|||
), |
|||
migrations.RemoveField( |
|||
model_name='lesson', |
|||
name='is_active', |
|||
), |
|||
migrations.RemoveField( |
|||
model_name='lesson', |
|||
name='priority', |
|||
), |
|||
migrations.AddField( |
|||
model_name='attachment', |
|||
name='created_at', |
|||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created at'), |
|||
preserve_default=False, |
|||
), |
|||
migrations.AddField( |
|||
model_name='attachment', |
|||
name='updated_at', |
|||
field=models.DateTimeField(auto_now=True, verbose_name='Updated At'), |
|||
), |
|||
migrations.AddField( |
|||
model_name='glossary', |
|||
name='created_at', |
|||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created at'), |
|||
preserve_default=False, |
|||
), |
|||
migrations.AddField( |
|||
model_name='glossary', |
|||
name='updated_at', |
|||
field=models.DateTimeField(auto_now=True, verbose_name='Updated At'), |
|||
), |
|||
migrations.AlterField( |
|||
model_name='lesson', |
|||
name='content_type', |
|||
field=models.CharField(choices=[('youtube_link', 'Youtube Link'), ('video_file', 'Video File')], max_length=50, verbose_name='Content Type'), |
|||
), |
|||
migrations.CreateModel( |
|||
name='CourseAttachment', |
|||
fields=[ |
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), |
|||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), |
|||
('attachment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_attachments', to='course.attachment', verbose_name='Attachment')), |
|||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='course.course', verbose_name='Course')), |
|||
], |
|||
options={ |
|||
'verbose_name': 'Course Attachment', |
|||
'verbose_name_plural': 'Course Attachments', |
|||
'ordering': ('-id',), |
|||
}, |
|||
), |
|||
migrations.CreateModel( |
|||
name='CourseGlossary', |
|||
fields=[ |
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), |
|||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), |
|||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='glossaries', to='course.course', verbose_name='Course')), |
|||
('glossary', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_glossaries', to='course.glossary', verbose_name='Glossary')), |
|||
], |
|||
options={ |
|||
'verbose_name': 'Course Glossary', |
|||
'verbose_name_plural': 'Course Glossaries', |
|||
'ordering': ('-id',), |
|||
}, |
|||
), |
|||
migrations.CreateModel( |
|||
name='CourseLesson', |
|||
fields=[ |
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|||
('title', models.CharField(blank=True, max_length=255, null=True, verbose_name='Course Lesson Title')), |
|||
('priority', models.IntegerField(blank=True, null=True, verbose_name='Priority')), |
|||
('is_active', models.BooleanField(default=True, verbose_name='Is Active')), |
|||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), |
|||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), |
|||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='course.course', verbose_name='Course')), |
|||
('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_lessons', to='course.lesson', verbose_name='Lesson')), |
|||
], |
|||
), |
|||
migrations.AlterUniqueTogether( |
|||
name='lessoncompletion', |
|||
unique_together=set(), |
|||
), |
|||
migrations.AddField( |
|||
model_name='lessoncompletion', |
|||
name='course_lesson', |
|||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='completions', to='course.courselesson'), |
|||
preserve_default=False, |
|||
), |
|||
migrations.AlterUniqueTogether( |
|||
name='lessoncompletion', |
|||
unique_together={('student', 'course_lesson')}, |
|||
), |
|||
migrations.RemoveField( |
|||
model_name='lessoncompletion', |
|||
name='lesson', |
|||
), |
|||
] |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue