From 2bf3e6621e6909ac8e01da9a7d3a95c818e9cfa3 Mon Sep 17 00:00:00 2001 From: Rick Reilly Date: Mon, 8 Apr 2019 16:42:30 -0400 Subject: [PATCH] Add ProgramEnrollment app and model for masters --- .../program_enrollments/__init__.py | 0 lms/djangoapps/program_enrollments/admin.py | 17 +++++ lms/djangoapps/program_enrollments/apps.py | 18 ++++++ .../migrations/0001_initial.py | 59 +++++++++++++++++ .../migrations/__init__.py | 0 lms/djangoapps/program_enrollments/models.py | 63 +++++++++++++++++++ .../program_enrollments/tests/__init__.py | 0 .../program_enrollments/tests/test_models.py | 56 +++++++++++++++++ lms/djangoapps/program_enrollments/views.py | 9 +++ setup.py | 3 +- 10 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/program_enrollments/__init__.py create mode 100644 lms/djangoapps/program_enrollments/admin.py create mode 100644 lms/djangoapps/program_enrollments/apps.py create mode 100644 lms/djangoapps/program_enrollments/migrations/0001_initial.py create mode 100644 lms/djangoapps/program_enrollments/migrations/__init__.py create mode 100644 lms/djangoapps/program_enrollments/models.py create mode 100644 lms/djangoapps/program_enrollments/tests/__init__.py create mode 100644 lms/djangoapps/program_enrollments/tests/test_models.py create mode 100644 lms/djangoapps/program_enrollments/views.py diff --git a/lms/djangoapps/program_enrollments/__init__.py b/lms/djangoapps/program_enrollments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/program_enrollments/admin.py b/lms/djangoapps/program_enrollments/admin.py new file mode 100644 index 0000000000..11c28dfcbb --- /dev/null +++ b/lms/djangoapps/program_enrollments/admin.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" +Admin tool for the Program Enrollments models +""" +from __future__ import unicode_literals + +from django.contrib import admin + +from lms.djangoapps.program_enrollments.models import ProgramEnrollment + + +class ProgramEnrollmentAdmin(admin.ModelAdmin): + """ + Admin tool for the ProgramEnrollment model + """ + +admin.site.register(ProgramEnrollment, ProgramEnrollmentAdmin) diff --git a/lms/djangoapps/program_enrollments/apps.py b/lms/djangoapps/program_enrollments/apps.py new file mode 100644 index 0000000000..aa2de26923 --- /dev/null +++ b/lms/djangoapps/program_enrollments/apps.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +ProgramEnrollments Application Configuration +""" +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class ProgramEnrollmentsConfig(AppConfig): + """ + Application configuration for ProgramEnrollment + """ + name = 'lms.djangoapps.program_enrollments' + + plugin_app = { + 'url_config': {}, + } diff --git a/lms/djangoapps/program_enrollments/migrations/0001_initial.py b/lms/djangoapps/program_enrollments/migrations/0001_initial.py new file mode 100644 index 0000000000..7a75c1801f --- /dev/null +++ b/lms/djangoapps/program_enrollments/migrations/0001_initial.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-09 19:32 +from __future__ import unicode_literals + +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 simple_history.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalProgramEnrollment', + fields=[ + ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, 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')), + ('external_user_key', models.CharField(db_index=True, max_length=255, null=True)), + ('program_uuid', models.UUIDField(db_index=True)), + ('curriculum_uuid', models.UUIDField(db_index=True)), + ('status', models.CharField(choices=[('enrolled', 'enrolled'), ('pending', 'pending'), ('suspended', 'suspended'), ('withdrawn', 'withdrawn')], max_length=9)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + 'verbose_name': 'historical program enrollment', + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='ProgramEnrollment', + 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')), + ('external_user_key', models.CharField(db_index=True, max_length=255, null=True)), + ('program_uuid', models.UUIDField(db_index=True)), + ('curriculum_uuid', models.UUIDField(db_index=True)), + ('status', models.CharField(choices=[('enrolled', 'enrolled'), ('pending', 'pending'), ('suspended', 'suspended'), ('withdrawn', 'withdrawn')], max_length=9)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/lms/djangoapps/program_enrollments/migrations/__init__.py b/lms/djangoapps/program_enrollments/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/program_enrollments/models.py b/lms/djangoapps/program_enrollments/models.py new file mode 100644 index 0000000000..b2d76c5dc4 --- /dev/null +++ b/lms/djangoapps/program_enrollments/models.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" +Django model specifications for the Program Enrollments API +""" +from __future__ import unicode_literals + +from django.contrib.auth.models import User +from django.db import models +from model_utils.models import TimeStampedModel +from simple_history.models import HistoricalRecords + + +class ProgramEnrollment(TimeStampedModel): # pylint: disable=model-missing-unicode + """ + This is a model for Program Enrollments from the registrar service + + .. pii: PII is found in the external key for a program enrollment + .. pii_types: other + .. pii_retirement: local_api + """ + STATUSES = ( + ('enrolled', 'enrolled'), + ('pending', 'pending'), + ('suspended', 'suspended'), + ('withdrawn', 'withdrawn'), + ) + + class Meta(object): + app_label = "program_enrollments" + + user = models.ForeignKey( + User, + null=True, + blank=True + ) + external_user_key = models.CharField( + db_index=True, + max_length=255, + null=True + ) + program_uuid = models.UUIDField(db_index=True, null=False) + curriculum_uuid = models.UUIDField(db_index=True, null=False) + status = models.CharField(max_length=9, choices=STATUSES) + historical_records = HistoricalRecords() + + @classmethod + def retire_user(cls, user_id): + """ + With the parameter user_id, retire the external_user_key field + + Return True if there is data that was retired + Return False if there is no matching data + """ + + enrollments = cls.objects.filter(user=user_id) + if not enrollments: + return False + + for enrollment in enrollments: + enrollment.historical_records.update(external_user_key=None) + + enrollments.update(external_user_key=None) + return True diff --git a/lms/djangoapps/program_enrollments/tests/__init__.py b/lms/djangoapps/program_enrollments/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/program_enrollments/tests/test_models.py b/lms/djangoapps/program_enrollments/tests/test_models.py new file mode 100644 index 0000000000..2d36aaa153 --- /dev/null +++ b/lms/djangoapps/program_enrollments/tests/test_models.py @@ -0,0 +1,56 @@ +""" +Unit tests for ProgramEnrollment models. +""" +from __future__ import unicode_literals + +from uuid import uuid4 + +from django.test import TestCase + +from lms.djangoapps.program_enrollments.models import ProgramEnrollment +from student.tests.factories import UserFactory + + +class ProgramEnrollmentModelTests(TestCase): + """ + Tests for the ProgramEnrollment model. + """ + def setUp(self): + """ + Set up the test data used in the specific tests + """ + super(ProgramEnrollmentModelTests, self).setUp() + self.user = UserFactory.create() + self.enrollment = ProgramEnrollment.objects.create( + user=self.user, + external_user_key='abc', + program_uuid=uuid4(), + curriculum_uuid=uuid4(), + status='enrolled' + ) + + def test_user_retirement(self): + """ + Test that the external_user_key is uccessfully retired for a user's program enrollments and history. + """ + new_status = 'withdrawn' + + self.enrollment.status = new_status + self.enrollment.save() + + # Ensure that all the records had values for external_user_key + self.assertEquals(self.enrollment.external_user_key, 'abc') + + self.assertTrue(self.enrollment.historical_records.all()) + for record in self.enrollment.historical_records.all(): + self.assertEquals(record.external_user_key, 'abc') + + ProgramEnrollment.retire_user(self.user.id) + self.enrollment.refresh_from_db() + + # Ensure those values are retired + self.assertEquals(self.enrollment.external_user_key, None) + + self.assertTrue(self.enrollment.historical_records.all()) + for record in self.enrollment.historical_records.all(): + self.assertEquals(record.external_user_key, None) diff --git a/lms/djangoapps/program_enrollments/views.py b/lms/djangoapps/program_enrollments/views.py new file mode 100644 index 0000000000..dbcb7fb359 --- /dev/null +++ b/lms/djangoapps/program_enrollments/views.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +""" +ProgramEnrollment Views +""" +from __future__ import unicode_literals + +# from django.shortcuts import render + +# Create your views here. diff --git a/setup.py b/setup.py index 8e9822aaba..0088a6fe23 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,8 @@ setup( "zendesk_proxy = openedx.core.djangoapps.zendesk_proxy.apps:ZendeskProxyConfig", "instructor = lms.djangoapps.instructor.apps:InstructorConfig", "password_policy = openedx.core.djangoapps.password_policy.apps:PasswordPolicyConfig", - "user_authn = openedx.core.djangoapps.user_authn.apps:UserAuthnConfig" + "user_authn = openedx.core.djangoapps.user_authn.apps:UserAuthnConfig", + "program_enrollments = lms.djangoapps.program_enrollments.apps:ProgramEnrollmentsConfig", ], "cms.djangoapp": [ "announcements = openedx.features.announcements.apps:AnnouncementsConfig",