From ab4f62777c33de9f3966dd148dee834c4df76af7 Mon Sep 17 00:00:00 2001 From: Jansen Kantor Date: Fri, 23 Feb 2024 11:56:04 -0500 Subject: [PATCH] feat: add db models for course reset feature (#34282) * feat: add db models for course reset feature * style: quality * fix: read only fields when creating / updating model --- lms/djangoapps/support/admin.py | 42 ++++++++++++++++ .../support/migrations/0003_course_reset.py | 49 ++++++++++++++++++ lms/djangoapps/support/models.py | 50 +++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 lms/djangoapps/support/admin.py create mode 100644 lms/djangoapps/support/migrations/0003_course_reset.py diff --git a/lms/djangoapps/support/admin.py b/lms/djangoapps/support/admin.py new file mode 100644 index 0000000000..d7bfc57508 --- /dev/null +++ b/lms/djangoapps/support/admin.py @@ -0,0 +1,42 @@ +""" Django admins for support models """ +from django.contrib import admin +from lms.djangoapps.support.models import CourseResetCourseOptIn, CourseResetAudit + + +class CourseResetCourseOptInAdmin(admin.ModelAdmin): + """ Django admin for CourseResetCourseOptIn model """ + list_display = ['course_id', 'active'] + fields = ['course_id', 'active', 'created', 'modified'] + + def get_readonly_fields(self, request, obj=None): + """ + Ensure that 'course_id' cannot be edited after creation. + """ + if obj: + return ['course_id', 'created', 'modified'] + else: + return ['created', 'modified'] + + +class CourseResetAuditAdmin(admin.ModelAdmin): + """ Django admin for CourseResetAudit model """ + + list_display = ['course', 'user', 'status', 'created', 'completed_at', 'reset_by'] + fields = ['created', 'modified', 'status', 'completed_at', 'course', 'user', 'course_enrollment', 'reset_by'] + + def get_readonly_fields(self, request, obj=None): + """ + If we are editing an existing model, we should only be able to change the status, for potential debugging + """ + if obj: + return ['created', 'modified', 'completed_at', 'course', 'user', 'course_enrollment', 'reset_by'] + else: + return ['created', 'modified', 'user'] + + @admin.display(description="user") + def user(self, obj): + return obj.course_enrollment.user + + +admin.site.register(CourseResetCourseOptIn, CourseResetCourseOptInAdmin) +admin.site.register(CourseResetAudit, CourseResetAuditAdmin) diff --git a/lms/djangoapps/support/migrations/0003_course_reset.py b/lms/djangoapps/support/migrations/0003_course_reset.py new file mode 100644 index 0000000000..b7e80d262f --- /dev/null +++ b/lms/djangoapps/support/migrations/0003_course_reset.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.23 on 2024-02-22 14:48 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +import opaque_keys.edx.django.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('student', '0045_auto_20230808_0944'), + ('support', '0002_alter_historicalusersocialauth_options'), + ] + + operations = [ + migrations.CreateModel( + name='CourseResetCourseOptIn', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('course_id', opaque_keys.edx.django.models.CourseKeyField(max_length=255)), + ('active', models.BooleanField()), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CourseResetAudit', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('status', models.CharField(choices=[('in_progress', 'In Progress'), ('complete', 'Complete'), ('enqueued', 'Enqueued'), ('failed', 'Failed')], default='enqueued', max_length=12)), + ('completed_at', models.DateTimeField(blank=True, default=None, null=True)), + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='support.courseresetcourseoptin')), + ('course_enrollment', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='student.courseenrollment')), + ('reset_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/lms/djangoapps/support/models.py b/lms/djangoapps/support/models.py index 3f2693f918..7915d1aa91 100644 --- a/lms/djangoapps/support/models.py +++ b/lms/djangoapps/support/models.py @@ -1,9 +1,59 @@ """ Models used to implement support related models in such as SSO History model """ +from django.contrib.auth import get_user_model +from django.db.models import ForeignKey, DO_NOTHING, CASCADE, TextChoices +from django.db.models.fields import BooleanField, CharField, DateTimeField +from model_utils.models import TimeStampedModel +from opaque_keys.edx.django.models import CourseKeyField from simple_history import register from social_django.models import UserSocialAuth +from common.djangoapps.student.models import CourseEnrollment + +User = get_user_model() + # Registers UserSocialAuth with simple-django-history. register(UserSocialAuth, app=__package__) + + +class CourseResetCourseOptIn(TimeStampedModel): + """ + Model that represents a course which has opted in to the course reset feature. + """ + course_id = CourseKeyField(max_length=255) + active = BooleanField() + + def __str__(self): + return f'{self.course_id} - {"ACTIVE" if self.active else "INACTIVE"}' + + +class CourseResetAudit(TimeStampedModel): + """ + Model which records the course reset action's status and metadata + """ + class CourseResetStatus(TextChoices): + IN_PROGRESS = "in_progress" + COMPLETE = "complete" + ENQUEUED = "enqueued" + FAILED = "failed" + + course = ForeignKey( + CourseResetCourseOptIn, + on_delete=CASCADE + ) + course_enrollment = ForeignKey( + CourseEnrollment, + on_delete=DO_NOTHING + ) + reset_by = ForeignKey( + User, + on_delete=DO_NOTHING + ) + status = CharField( + max_length=12, + choices=CourseResetStatus.choices, + default=CourseResetStatus.ENQUEUED, + ) + completed_at = DateTimeField(default=None, null=True, blank=True)