15 changed files with 607 additions and 219 deletions
-
2apps/account/templates/account/json_editor_field.html
-
24apps/api/views.py
-
92apps/course/admin/course.py
-
54apps/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
-
84apps/course/models/course.py
-
81apps/course/models/lesson.py
-
21apps/course/serializers/course.py
-
23apps/course/serializers/lesson.py
-
14apps/course/views/course.py
-
69apps/course/views/lesson.py
-
80config/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