Add new ProgramCourseEnrollment uniqueness constriant (#21463)
ProgramCourseEnrollments were already unique on (program_enrollment, course_enrollment) by nature of the OneToOneField on course_enrollment. However, this only affects realized enrollments. For waiting enrollments, we need to add a uniqueness constraint on (program_enrollment, course_key).
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.23 on 2019-08-27 13:11
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('program_enrollments', '0006_add_the_correct_constraints'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='programcourseenrollment',
|
||||
unique_together=set([('program_enrollment', 'course_key')]),
|
||||
),
|
||||
]
|
||||
@@ -127,6 +127,17 @@ class ProgramCourseEnrollment(TimeStampedModel): # pylint: disable=model-missin
|
||||
class Meta(object):
|
||||
app_label = "program_enrollments"
|
||||
|
||||
# For each program enrollment, there may be only one
|
||||
# waiting program-course enrollment per course key.
|
||||
# This same constraint is implicitly enforced for
|
||||
# completed program-course enrollments by the
|
||||
# OneToOneField on `course_enrollment`, which mandates that
|
||||
# there may be at most one program-course enrollment per
|
||||
# (user, course) pair.
|
||||
unique_together = (
|
||||
('program_enrollment', 'course_key'),
|
||||
)
|
||||
|
||||
program_enrollment = models.ForeignKey(
|
||||
ProgramEnrollment,
|
||||
on_delete=models.CASCADE,
|
||||
|
||||
@@ -25,6 +25,11 @@ class ProgramEnrollmentFactory(DjangoModelFactory):
|
||||
status = 'enrolled'
|
||||
|
||||
|
||||
PROGRAM_COURSE_ENROLLMENT_DEFAULT_COURSE_KEY = (
|
||||
CourseKey.from_string("course-v1:edX+DemoX+Demo_Course")
|
||||
)
|
||||
|
||||
|
||||
class ProgramCourseEnrollmentFactory(DjangoModelFactory):
|
||||
""" A factory for the ProgramCourseEnrollment model. """
|
||||
class Meta(object):
|
||||
@@ -32,5 +37,11 @@ class ProgramCourseEnrollmentFactory(DjangoModelFactory):
|
||||
|
||||
program_enrollment = factory.SubFactory(ProgramEnrollmentFactory)
|
||||
course_enrollment = factory.SubFactory(CourseEnrollmentFactory)
|
||||
course_key = CourseKey.from_string("course-v1:edX+DemoX+Demo_Course")
|
||||
course_key = factory.LazyAttribute(
|
||||
lambda pce: (
|
||||
pce.course_enrollment.course_id
|
||||
if pce.course_enrollment
|
||||
else PROGRAM_COURSE_ENROLLMENT_DEFAULT_COURSE_KEY
|
||||
)
|
||||
)
|
||||
status = 'active'
|
||||
|
||||
@@ -155,6 +155,37 @@ class ProgramCourseEnrollmentModelTests(TestCase):
|
||||
self.course_key = CourseKey.from_string(generate_course_run_key())
|
||||
CourseOverviewFactory(id=self.course_key)
|
||||
|
||||
def test_unique_completed_enrollment(self):
|
||||
"""
|
||||
A record with the same (program_enrollment, course_enrollment)
|
||||
cannot be created.
|
||||
"""
|
||||
pce = self._create_completed_program_course_enrollment()
|
||||
with self.assertRaises(IntegrityError):
|
||||
# Purposefully mis-set the course_key in order to test
|
||||
# that there is a constraint on
|
||||
# (program_enrollment, course_enrollment) alone.
|
||||
ProgramCourseEnrollment.objects.create(
|
||||
program_enrollment=pce.program_enrollment,
|
||||
course_key="course-v1:dummy+value+101",
|
||||
course_enrollment=pce.course_enrollment,
|
||||
status="inactive",
|
||||
)
|
||||
|
||||
def test_unique_waiting_enrollment(self):
|
||||
"""
|
||||
A record with the same (program_enrollment, course_key)
|
||||
cannot be created.
|
||||
"""
|
||||
pce = self._create_waiting_program_course_enrollment()
|
||||
with self.assertRaises(IntegrityError):
|
||||
ProgramCourseEnrollment.objects.create(
|
||||
program_enrollment=pce.program_enrollment,
|
||||
course_key=pce.course_key,
|
||||
course_enrollment=None,
|
||||
status="inactive",
|
||||
)
|
||||
|
||||
def _create_completed_program_course_enrollment(self):
|
||||
""" helper function create program course enrollment """
|
||||
course_enrollment = CourseEnrollmentFactory.create(
|
||||
|
||||
@@ -9,36 +9,62 @@ from django.db.models.base import ObjectDoesNotExist
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from freezegun import freeze_time
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from testfixtures import LogCapture
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment
|
||||
from lms.djangoapps.program_enrollments.tasks import expire_waiting_enrollments, log
|
||||
from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
|
||||
from student.tests.factories import UserFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
|
||||
|
||||
class ExpireWaitingEnrollmentsTest(TestCase):
|
||||
""" Test expire_waiting_enrollments task """
|
||||
|
||||
def _setup_enrollments(self, external_user_key, user, created_date):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(cls, ExpireWaitingEnrollmentsTest).setUpClass()
|
||||
cls.timed_course_key = CourseKey.from_string('course-v1:edX+TestExpire+Timed')
|
||||
cls.fresh_course_key = CourseKey.from_string('course-v1:edX+TestExpire+Fresh')
|
||||
CourseOverviewFactory(id=cls.timed_course_key)
|
||||
CourseOverviewFactory(id=cls.fresh_course_key)
|
||||
|
||||
def _set_up_course_enrollment(self, user, program_enrollment, course_key):
|
||||
""" helper function to set up a program course enrollment """
|
||||
if user:
|
||||
ProgramCourseEnrollmentFactory(
|
||||
program_enrollment=program_enrollment,
|
||||
course_enrollment=CourseEnrollmentFactory(
|
||||
course_id=course_key, user=user, mode=CourseMode.MASTERS
|
||||
)
|
||||
)
|
||||
else:
|
||||
ProgramCourseEnrollmentFactory(
|
||||
program_enrollment=program_enrollment,
|
||||
course_key=course_key,
|
||||
)
|
||||
|
||||
def _set_up_enrollments(self, external_user_key, user, created_date):
|
||||
""" helper function to setup enrollments """
|
||||
with freeze_time(created_date):
|
||||
program_enrollment = ProgramEnrollmentFactory(
|
||||
user=user,
|
||||
external_user_key=external_user_key,
|
||||
)
|
||||
ProgramCourseEnrollmentFactory(
|
||||
program_enrollment=program_enrollment
|
||||
self._set_up_course_enrollment(
|
||||
user, program_enrollment, self.timed_course_key
|
||||
)
|
||||
# additional course enrollment that is always fresh
|
||||
ProgramCourseEnrollmentFactory(
|
||||
program_enrollment=program_enrollment
|
||||
self._set_up_course_enrollment(
|
||||
user, program_enrollment, self.fresh_course_key
|
||||
)
|
||||
|
||||
def test_expire(self):
|
||||
self._setup_enrollments('student_expired_waiting', None, timezone.now() - timedelta(60))
|
||||
self._setup_enrollments('student_waiting', None, timezone.now() - timedelta(59))
|
||||
self._setup_enrollments('student_actualized', UserFactory(), timezone.now() - timedelta(90))
|
||||
self._set_up_enrollments('student_expired_waiting', None, timezone.now() - timedelta(60))
|
||||
self._set_up_enrollments('student_waiting', None, timezone.now() - timedelta(59))
|
||||
self._set_up_enrollments('student_actualized', UserFactory(), timezone.now() - timedelta(90))
|
||||
|
||||
expired_program_enrollment = ProgramEnrollment.objects.get(
|
||||
external_user_key='student_expired_waiting'
|
||||
|
||||
Reference in New Issue
Block a user