Skip to content
Snippets Groups Projects
Commit ce943bce authored by Alex Dusenbery's avatar Alex Dusenbery Committed by Alex Dusenbery
Browse files

Add the correct UNIQUE constraints to the ProgramEnrollment model.

parent a40e457f
No related merge requests found
ProgramEnrollment Model Data Integrity
--------------------------------------
Status
======
Accepted (circa August 2019)
Context
=======
For the sake of fundamental data integrity, we are introducing 2 unique
constraints on the ``program_enrollments.ProgramEnrollment`` model.
Decisions
=========
The unique constraints are on the following column sets:
* ``('user', 'program_uuid', 'curriculum_uuid')``
* ``('external_user_key', 'program_uuid', 'curriculum_uuid')``
Note that either the ``user`` column or the ``external_user_key`` column may be null.
In the future, it would be nice to add a validation step at the Django model layer
that restricts a model instance from having null values for both of these fields.
Consequences
============
The first constraint supports the cases in which we save program enrollment records
that don't have any association with an external organization, e.g. our MicroMasters programs.
Non-realized enrollments, where the ``user`` value is null, are not affected by this constraint.
As for the second constraint , we want to disallow the ability of anyone to register a learner,
as identified by ``external_user_key``, into the same program and curriculum more than once.
No enrollment record with a null ``external_user_key`` is affected by this constraint.
Together, these constraints restrict the duplication of learner records in a specific
program/curriculum, where the learner is identified either by their ``auth.User.id`` or
some ``external_user_key``.
This constraint set does NOT support the use case of a single ``auth.User`` being enrolled
in the same program/curriculum with two or more different ``external_user_keys``. Supporting
this case leads to problematic situations, e.g. how to decide which of these program enrollment
records to link to a program-course enrollment? If needed, we could introduce an additional
set of models to support this situation.
# -*- coding: utf-8 -*-
# Generated by Django 1.11.23 on 2019-08-23 15:37
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('program_enrollments', '0005_canceled_not_withdrawn'),
]
operations = [
migrations.AlterUniqueTogether(
name='programenrollment',
unique_together=set([('user', 'program_uuid', 'curriculum_uuid'), ('external_user_key', 'program_uuid', 'curriculum_uuid')]),
),
]
......@@ -39,12 +39,12 @@ class ProgramEnrollment(TimeStampedModel): # pylint: disable=model-missing-unic
class Meta(object):
app_label = "program_enrollments"
unique_together = ('external_user_key', 'program_uuid', 'curriculum_uuid')
# A student enrolled in a given (program, curriculum) should always
# have a non-null ``user`` or ``external_user_key`` field (or both).
unique_together = (
('user', 'external_user_key', 'program_uuid', 'curriculum_uuid'),
('user', 'program_uuid', 'curriculum_uuid'),
('external_user_key', 'program_uuid', 'curriculum_uuid'),
)
user = models.ForeignKey(
......
......@@ -20,8 +20,8 @@ class ProgramEnrollmentFactory(DjangoModelFactory):
user = factory.SubFactory(UserFactory)
external_user_key = None
program_uuid = uuid4()
curriculum_uuid = uuid4()
program_uuid = factory.LazyFunction(uuid4)
curriculum_uuid = factory.LazyFunction(uuid4)
status = 'enrolled'
......
......@@ -6,6 +6,7 @@ from __future__ import absolute_import, unicode_literals
from uuid import uuid4
import ddt
from django.db.utils import IntegrityError
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from six.moves import range
......@@ -32,14 +33,37 @@ class ProgramEnrollmentModelTests(TestCase):
self.user = UserFactory.create()
self.program_uuid = uuid4()
self.other_program_uuid = uuid4()
self.curriculum_uuid = uuid4()
self.enrollment = ProgramEnrollment.objects.create(
user=self.user,
external_user_key='abc',
program_uuid=self.program_uuid,
curriculum_uuid=uuid4(),
curriculum_uuid=self.curriculum_uuid,
status='enrolled'
)
def test_unique_external_key_program_curriculum(self):
""" A record with the same (external_user_key, program_uuid, curriculum_uuid) cannot be duplicated. """
with self.assertRaises(IntegrityError):
_ = ProgramEnrollment.objects.create(
user=None,
external_user_key='abc',
program_uuid=self.program_uuid,
curriculum_uuid=self.curriculum_uuid,
status='pending',
)
def test_unique_user_program_curriculum(self):
""" A record with the same (user, program_uuid, curriculum_uuid) cannot be duplicated. """
with self.assertRaises(IntegrityError):
_ = ProgramEnrollment.objects.create(
user=self.user,
external_user_key=None,
program_uuid=self.program_uuid,
curriculum_uuid=self.curriculum_uuid,
status='suspended',
)
def test_bulk_read_by_student_key(self):
curriculum_a = uuid4()
curriculum_b = uuid4()
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment