fix: update program enrollments to be case insensitive on external_user_key (#29646)
UTAsutin is an example partner who would use mixed casing on their external_user_key references for program enrollment upload and matriculation. Update the system to be case insensitive on external_user_key Co-authored-by: Simon Chen <schen@edX-C02FW0GUML85.local>
This commit is contained in:
@@ -11,6 +11,7 @@ import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import IntegrityError, transaction
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.api import get_access_role_by_role_name
|
||||
@@ -158,10 +159,10 @@ def _get_program_enrollments_by_ext_key(program_uuid, external_user_keys):
|
||||
).prefetch_related(
|
||||
'program_course_enrollments'
|
||||
).select_related('user')
|
||||
return {
|
||||
return CaseInsensitiveDict({
|
||||
program_enrollment.external_user_key: program_enrollment
|
||||
for program_enrollment in program_enrollments
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def _get_lms_users(lms_usernames):
|
||||
|
||||
@@ -5,6 +5,7 @@ Outside of this subpackage, import these functions
|
||||
from `lms.djangoapps.program_enrollments.api`.
|
||||
"""
|
||||
|
||||
import re
|
||||
from functools import reduce
|
||||
from operator import or_
|
||||
|
||||
@@ -62,7 +63,7 @@ def get_program_enrollment(
|
||||
raise ValueError(_STUDENT_ARG_ERROR_MESSAGE)
|
||||
filters = {
|
||||
"user": user,
|
||||
"external_user_key": external_user_key,
|
||||
"external_user_key__iexact": external_user_key,
|
||||
"curriculum_uuid": curriculum_uuid,
|
||||
}
|
||||
return ProgramEnrollment.objects.get(
|
||||
@@ -100,7 +101,7 @@ def get_program_course_enrollment(
|
||||
raise ValueError(_STUDENT_ARG_ERROR_MESSAGE)
|
||||
filters = {
|
||||
"program_enrollment__user": user,
|
||||
"program_enrollment__external_user_key": external_user_key,
|
||||
"program_enrollment__external_user_key__iexact": external_user_key,
|
||||
"program_enrollment__curriculum_uuid": curriculum_uuid,
|
||||
}
|
||||
return ProgramCourseEnrollment.objects.get(
|
||||
@@ -142,10 +143,13 @@ def fetch_program_enrollments(
|
||||
raise ValueError(
|
||||
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
|
||||
)
|
||||
external_user_key_regex = None
|
||||
if external_user_keys:
|
||||
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
|
||||
filters = {
|
||||
"curriculum_uuid__in": curriculum_uuids,
|
||||
"user__in": users,
|
||||
"external_user_key__in": external_user_keys,
|
||||
"external_user_key__iregex": external_user_key_regex,
|
||||
"status__in": program_enrollment_statuses,
|
||||
}
|
||||
if realized_only:
|
||||
@@ -202,10 +206,13 @@ def fetch_program_course_enrollments(
|
||||
raise ValueError(
|
||||
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
|
||||
)
|
||||
external_user_key_regex = None
|
||||
if external_user_keys:
|
||||
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
|
||||
filters = {
|
||||
"program_enrollment__curriculum_uuid__in": curriculum_uuids,
|
||||
"program_enrollment__user__in": users,
|
||||
"program_enrollment__external_user_key__in": external_user_keys,
|
||||
"program_enrollment__external_user_key__iregex": external_user_key_regex,
|
||||
"program_enrollment__status__in": program_enrollment_statuses,
|
||||
"program_enrollment__in": program_enrollments,
|
||||
}
|
||||
@@ -301,9 +308,12 @@ def fetch_program_enrollments_by_students(
|
||||
raise ValueError(
|
||||
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
|
||||
)
|
||||
external_user_key_regex = None
|
||||
if external_user_keys:
|
||||
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
|
||||
filters = {
|
||||
"user__in": users,
|
||||
"external_user_key__in": external_user_keys,
|
||||
"external_user_key__iregex": external_user_key_regex,
|
||||
"status__in": program_enrollment_statuses,
|
||||
}
|
||||
if realized_only:
|
||||
@@ -357,9 +367,12 @@ def fetch_program_course_enrollments_by_students(
|
||||
raise ValueError(
|
||||
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
|
||||
)
|
||||
external_user_key_regex = None
|
||||
if external_user_keys:
|
||||
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
|
||||
filters = {
|
||||
"program_enrollment__user__in": users,
|
||||
"program_enrollment__external_user_key__in": external_user_keys,
|
||||
"program_enrollment__external_user_key__iregex": external_user_key_regex,
|
||||
"program_enrollment__program_uuid__in": program_uuids,
|
||||
"program_enrollment__curriculum_uuid__in": curriculum_uuids,
|
||||
"course_key__in": course_keys,
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
Tests for account linking Python API.
|
||||
"""
|
||||
|
||||
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -98,7 +98,7 @@ class TestLinkProgramEnrollmentsMixin:
|
||||
if refresh:
|
||||
user.refresh_from_db()
|
||||
enrollment = user.programenrollment_set.get(
|
||||
program_uuid=program_uuid, external_user_key=external_user_key
|
||||
program_uuid=program_uuid, external_user_key__iexact=external_user_key
|
||||
)
|
||||
assert enrollment is not None
|
||||
|
||||
@@ -128,6 +128,7 @@ class TestLinkProgramEnrollmentsMixin:
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
|
||||
""" Tests for linking behavior """
|
||||
|
||||
@@ -153,6 +154,22 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
|
||||
|
||||
self._assert_no_user(another_program_enrollment)
|
||||
|
||||
def test_link_mixed_case_external_user_key(self):
|
||||
"""
|
||||
Test that when linking the program enrollment with same external user key,
|
||||
but the casing on external_user_key is mixed, the linking is still successful
|
||||
"""
|
||||
program_enrollment = self._create_waiting_enrollment(self.program, 'student-43')
|
||||
self._create_waiting_course_enrollment(program_enrollment, self.fruit_course)
|
||||
self._create_waiting_course_enrollment(program_enrollment, self.animal_course)
|
||||
|
||||
link_program_enrollments(self.program, {'STUDEnt-43': self.user_1.username})
|
||||
|
||||
self._assert_program_enrollment(self.user_1, self.program, 'STUDEnt-43')
|
||||
self._assert_user_enrolled_in_program_courses(
|
||||
self.user_1, self.program, self.fruit_course, self.animal_course
|
||||
)
|
||||
|
||||
def test_inactive_waiting_course_enrollment(self):
|
||||
"""
|
||||
Test that when a waiting program enrollment has waiting program course enrollments with a
|
||||
@@ -272,14 +289,24 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
|
||||
for course_enrollment in course_enrollments:
|
||||
assert course_enrollment.mode == course_keys_to_mode[course_enrollment.course.id]
|
||||
|
||||
@ddt.data(
|
||||
('001', '001'),
|
||||
('learner-2', 'LEArneR-2'),
|
||||
)
|
||||
@ddt.unpack
|
||||
@patch('lms.djangoapps.program_enrollments.api.linking.CourseMode.modes_for_course_dict')
|
||||
def test_update_linking_enrollment_to_another_user(self, mock_modes_for_course_dict):
|
||||
def test_update_linking_enrollment_to_another_user(
|
||||
self,
|
||||
linked_external_key,
|
||||
updated_external_key,
|
||||
mock_modes_for_course_dict,
|
||||
):
|
||||
"""
|
||||
Test that when link_program_enrollments is called with a program and an external_user_key,
|
||||
user pair and that program is already linked to a different user with the same external_user_key
|
||||
that the original user's link is removed and replaced by a link with the new user.
|
||||
"""
|
||||
program_enrollment = self._create_waiting_enrollment(self.program, '0001')
|
||||
program_enrollment = self._create_waiting_enrollment(self.program, linked_external_key)
|
||||
|
||||
self._create_waiting_course_enrollment(program_enrollment, self.fruit_course)
|
||||
self._create_waiting_course_enrollment(program_enrollment, self.animal_course)
|
||||
@@ -296,9 +323,9 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
|
||||
mock_modes_for_course_dict.side_effect = mocked_modes_for_course_dict
|
||||
|
||||
# do the initial link of user_1 to the program enrollment
|
||||
link_program_enrollments(self.program, {'0001': self.user_1.username})
|
||||
link_program_enrollments(self.program, {linked_external_key: self.user_1.username})
|
||||
|
||||
self._assert_program_enrollment(self.user_1, self.program, '0001', refresh=False)
|
||||
self._assert_program_enrollment(self.user_1, self.program, linked_external_key, refresh=False)
|
||||
self._assert_no_program_enrollment(self.user_2, self.program, refresh=False)
|
||||
|
||||
# grab the user's original course enrollment before the link between the program
|
||||
@@ -310,12 +337,12 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
|
||||
errors = link_program_enrollments(
|
||||
self.program,
|
||||
{
|
||||
'0001': self.user_2.username,
|
||||
updated_external_key: self.user_2.username,
|
||||
}
|
||||
)
|
||||
|
||||
assert not errors
|
||||
self._assert_program_enrollment(self.user_2, self.program, '0001')
|
||||
self._assert_program_enrollment(self.user_2, self.program, updated_external_key)
|
||||
self._assert_no_program_enrollment(self.user_1, self.program)
|
||||
# assert that all of user_1's course enrollments as part of the program
|
||||
# are inactive
|
||||
@@ -332,7 +359,7 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
|
||||
self._assert_course_enrollments_in_mode(course_enrollments_for_user_1, course_keys_to_mode)
|
||||
|
||||
# assert that user_2 has been successfully linked to the program
|
||||
self._assert_program_enrollment(self.user_2, self.program, '0001')
|
||||
self._assert_program_enrollment(self.user_2, self.program, updated_external_key)
|
||||
self._assert_user_enrolled_in_program_courses(self.user_2, self.program, self.fruit_course, self.animal_course)
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ Tests for program enrollment reading Python API.
|
||||
|
||||
|
||||
from uuid import UUID
|
||||
import pytest
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
@@ -157,6 +158,10 @@ class ProgramEnrollmentReadingTests(TestCase):
|
||||
# enrollments in one curriculum, so it's not ambiguous).
|
||||
(program_uuid_y, None, None, ext_6, 6),
|
||||
(program_uuid_y, None, username_2, None, 10),
|
||||
# use mixed casing for external_user_id
|
||||
(program_uuid_x, curriculum_uuid_b, None, 'STUDENT-4', 4),
|
||||
(program_uuid_x, curriculum_uuid_b, None, 'STUDent-5', 5),
|
||||
(program_uuid_y, None, None, 'STudENT-6', 6),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_get_program_enrollment(
|
||||
@@ -193,6 +198,10 @@ class ProgramEnrollmentReadingTests(TestCase):
|
||||
# because each user-course pairing can only have one
|
||||
# program-course enrollment.
|
||||
(program_uuid_y, curriculum_uuid_c, course_key_r, None, ext_6, 10),
|
||||
# Use mixed casing for external_user_key
|
||||
(program_uuid_x, None, course_key_p, username_3, 'stuDENT-3', 5),
|
||||
(program_uuid_y, None, course_key_p, None, 'STudenT-4', 7),
|
||||
(program_uuid_x, None, course_key_p, None, 'STUDENT-5', 6),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_get_program_course_enrollment(
|
||||
@@ -253,6 +262,15 @@ class ProgramEnrollmentReadingTests(TestCase):
|
||||
{'program_uuid': program_uuid_x, 'waiting_only': True},
|
||||
{5},
|
||||
),
|
||||
# Use mixed casing on external_user_key
|
||||
(
|
||||
{
|
||||
'program_uuid': program_uuid_x,
|
||||
'usernames': {username_1, username_2, username_3, username_4},
|
||||
'external_user_keys': {'studeNT-3', 'STUdent-4', 'STudenT-5'}
|
||||
},
|
||||
{3, 4},
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_fetch_program_enrollments(self, kwargs, expected_enrollment_ids):
|
||||
@@ -312,6 +330,16 @@ class ProgramEnrollmentReadingTests(TestCase):
|
||||
},
|
||||
{10},
|
||||
),
|
||||
# Use mixed casing on external_user_key
|
||||
(
|
||||
{
|
||||
'program_uuid': program_uuid_x,
|
||||
'course_key': course_key_p,
|
||||
'usernames': {username_2, username_3},
|
||||
'external_user_keys': {'STudENt-3', 'stuDENt-5'}
|
||||
},
|
||||
{5},
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_fetch_program_course_enrollments(self, kwargs, expected_enrollment_ids):
|
||||
@@ -363,6 +391,11 @@ class ProgramEnrollmentReadingTests(TestCase):
|
||||
{'external_user_key': ext_4, 'waiting_only': True},
|
||||
{8},
|
||||
),
|
||||
# Use mixed casing on external_user_key
|
||||
(
|
||||
{'external_user_key': 'STudeNT-4', 'realized_only': True},
|
||||
{4},
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_fetch_program_enrollments_by_student(self, kwargs, expected_enrollment_ids):
|
||||
@@ -408,6 +441,11 @@ class ProgramEnrollmentReadingTests(TestCase):
|
||||
{'external_user_keys': [ext_4], 'waiting_only': True},
|
||||
{8},
|
||||
),
|
||||
# Use mixed casing on external_user_key
|
||||
(
|
||||
{'external_user_keys': ['STUdenT-4'], 'waiting_only': True},
|
||||
{8},
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_fetch_program_enrollments_by_students(self, kwargs, expected_enrollment_ids):
|
||||
@@ -461,6 +499,11 @@ class ProgramEnrollmentReadingTests(TestCase):
|
||||
},
|
||||
{8},
|
||||
),
|
||||
# Use mixed casing on external_user_key
|
||||
(
|
||||
{'external_user_keys': ['STUDENT-4'], 'realized_only': True},
|
||||
set(),
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_fetch_program_course_enrollments_by_students(self, kwargs, expected_enrollment_ids):
|
||||
|
||||
@@ -118,32 +118,44 @@ class EnrollmentTestMixin(CacheIsolationTestCase):
|
||||
return self.create_program_course_enrollment(program_enrollment, course_status=course_status)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class WritingProgramEnrollmentTest(EnrollmentTestMixin):
|
||||
"""
|
||||
Test cases for program enrollment writing functions.
|
||||
"""
|
||||
def test_write_program_enrollments_status_ended(self):
|
||||
@ddt.data(
|
||||
('learner-1', 'learner-1', PEStatuses.ENDED),
|
||||
# Test mixing the external_user_key casing
|
||||
('learner-1', 'LEARNER-1', PEStatuses.ENROLLED),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_write_program_enrollments_status_ended(
|
||||
self,
|
||||
external_key_1,
|
||||
external_key_2,
|
||||
target_status
|
||||
):
|
||||
"""
|
||||
Successfully updates program enrollment to status ended if requested.
|
||||
Successfully updates program enrollment to status if requested.
|
||||
This also validates history records are created on both create and update.
|
||||
"""
|
||||
assert ProgramEnrollment.objects.count() == 0
|
||||
assert ProgramEnrollment.historical_records.count() == 0 # pylint: disable=no-member
|
||||
write_program_enrollments(self.program_uuid, [{
|
||||
'external_user_key': 'learner-1',
|
||||
'external_user_key': external_key_1,
|
||||
'status': PEStatuses.PENDING,
|
||||
'curriculum_uuid': self.curriculum_uuid_a,
|
||||
}], True, False)
|
||||
assert ProgramEnrollment.objects.count() == 1
|
||||
assert ProgramEnrollment.historical_records.count() == 1 # pylint: disable=no-member
|
||||
write_program_enrollments(self.program_uuid, [{
|
||||
'external_user_key': 'learner-1',
|
||||
'status': PEStatuses.ENDED,
|
||||
result = write_program_enrollments(self.program_uuid, [{
|
||||
'external_user_key': external_key_2,
|
||||
'status': target_status,
|
||||
'curriculum_uuid': self.curriculum_uuid_a,
|
||||
}], False, True)
|
||||
assert ProgramEnrollment.objects.count() == 1
|
||||
assert ProgramEnrollment.historical_records.count() == 2 # pylint: disable=no-member
|
||||
assert ProgramEnrollment.objects.filter(status=PEStatuses.ENDED).exists()
|
||||
assert ProgramEnrollment.objects.filter(status=target_status).exists()
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -166,7 +178,7 @@ class WriteProgramCourseEnrollmentTest(EnrollmentTestMixin):
|
||||
and potentially that a CourseEnrollment also exists
|
||||
"""
|
||||
enrollment = ProgramCourseEnrollment.objects.get(
|
||||
program_enrollment__external_user_key=external_user_key,
|
||||
program_enrollment__external_user_key__iexact=external_user_key,
|
||||
program_enrollment__program_uuid=self.program_uuid
|
||||
)
|
||||
assert expected_status == enrollment.status
|
||||
@@ -306,6 +318,48 @@ class WriteProgramCourseEnrollmentTest(EnrollmentTestMixin):
|
||||
self.assert_program_course_enrollment('learner-5', CourseStatuses.ACTIVE, True)
|
||||
self.assert_program_course_enrollment('learner-6', CourseStatuses.ACTIVE, False)
|
||||
|
||||
def test_create_or_update_with_mixed_cased_external_user_key(self):
|
||||
"""
|
||||
Test writing enrollments with both create and update flags true.
|
||||
However, this time, the external_user_keys are mixed cased.
|
||||
Existing enrollments should be updated. If no matching enrollment is found, create one.
|
||||
"""
|
||||
# learners 1-4 are already enrolled in courses, 5-6 only have a program enrollment
|
||||
self.setup_change_test_data([
|
||||
CourseStatuses.ACTIVE, CourseStatuses.ACTIVE,
|
||||
CourseStatuses.ACTIVE, CourseStatuses.ACTIVE]
|
||||
)
|
||||
self.create_program_enrollment('LEArneR-5')
|
||||
self.create_program_enrollment('leARnER-6', user=None)
|
||||
|
||||
course_enrollment_requests = [
|
||||
self.course_enrollment_request('leaRNER-1', CourseStatuses.INACTIVE),
|
||||
self.course_enrollment_request('LEarner-2', CourseStatuses.ACTIVE),
|
||||
self.course_enrollment_request('learner-5', CourseStatuses.ACTIVE),
|
||||
self.course_enrollment_request('learner-6', CourseStatuses.ACTIVE),
|
||||
]
|
||||
|
||||
result = write_program_course_enrollments(
|
||||
self.program_uuid,
|
||||
self.course_id,
|
||||
course_enrollment_requests,
|
||||
True,
|
||||
True,
|
||||
)
|
||||
self.assertDictEqual(
|
||||
{
|
||||
'leaRNER-1': CourseStatuses.INACTIVE,
|
||||
'LEarner-2': CourseStatuses.ACTIVE,
|
||||
'learner-5': CourseStatuses.ACTIVE,
|
||||
'learner-6': CourseStatuses.ACTIVE,
|
||||
},
|
||||
result,
|
||||
)
|
||||
self.assert_program_course_enrollment('learner-1', CourseStatuses.INACTIVE, True)
|
||||
self.assert_program_course_enrollment('learner-2', CourseStatuses.ACTIVE, True)
|
||||
self.assert_program_course_enrollment('LEArneR-5', CourseStatuses.ACTIVE, True)
|
||||
self.assert_program_course_enrollment('leARnER-6', CourseStatuses.ACTIVE, False)
|
||||
|
||||
def test_create_conflicting_enrollment(self):
|
||||
"""
|
||||
The program enrollments application already has a program_course_enrollment
|
||||
@@ -322,6 +376,22 @@ class WriteProgramCourseEnrollmentTest(EnrollmentTestMixin):
|
||||
)
|
||||
self.assertDictEqual({'learner-1': CourseStatuses.CONFLICT}, result)
|
||||
|
||||
def test_create_conflicting_enrollment_mixed_case_external_user_key(self):
|
||||
"""
|
||||
The program enrollments application already has a program_course_enrollment
|
||||
record for this user with a mixed cased external_user_key and course
|
||||
"""
|
||||
self.create_program_and_course_enrollments('learner-1')
|
||||
course_enrollment_requests = [self.course_enrollment_request('LeArnER-1')]
|
||||
result = write_program_course_enrollments(
|
||||
self.program_uuid,
|
||||
self.course_id,
|
||||
course_enrollment_requests,
|
||||
True,
|
||||
False,
|
||||
)
|
||||
self.assertDictEqual({'LeArnER-1': CourseStatuses.CONFLICT}, result)
|
||||
|
||||
def test_update_nonexistent_enrollment(self):
|
||||
self.create_program_enrollment('learner-1')
|
||||
result = write_program_course_enrollments(
|
||||
@@ -374,7 +444,13 @@ class WriteProgramCourseEnrollmentTest(EnrollmentTestMixin):
|
||||
)
|
||||
self.assertDictEqual({'learner-1': CourseStatuses.NOT_IN_PROGRAM}, result)
|
||||
|
||||
def test_create_enrollments_and_assign_staff(self):
|
||||
@ddt.data(
|
||||
'learner',
|
||||
'LEARNer',
|
||||
'learNER',
|
||||
'LEARNER',
|
||||
)
|
||||
def test_create_enrollments_and_assign_staff(self, request_user_key_prefix):
|
||||
"""
|
||||
Successfully creates both waiting and linked program course enrollments with the course staff role.
|
||||
"""
|
||||
@@ -386,9 +462,15 @@ class WriteProgramCourseEnrollmentTest(EnrollmentTestMixin):
|
||||
self.create_program_enrollment('learner-3', user=self.student_2)
|
||||
|
||||
course_enrollment_requests = [
|
||||
self.course_enrollment_request('learner-1', CourseStatuses.ACTIVE, True),
|
||||
self.course_enrollment_request('learner-2', CourseStatuses.ACTIVE, True),
|
||||
self.course_enrollment_request('learner-3', CourseStatuses.ACTIVE, True),
|
||||
self.course_enrollment_request(
|
||||
'{}-1'.format(request_user_key_prefix), CourseStatuses.ACTIVE, True
|
||||
),
|
||||
self.course_enrollment_request(
|
||||
'{}-2'.format(request_user_key_prefix), CourseStatuses.ACTIVE, True
|
||||
),
|
||||
self.course_enrollment_request(
|
||||
'{}-3'.format(request_user_key_prefix), CourseStatuses.ACTIVE, True
|
||||
),
|
||||
]
|
||||
write_program_course_enrollments(
|
||||
self.program_uuid,
|
||||
|
||||
@@ -8,6 +8,7 @@ from `lms.djangoapps.program_enrollments.api`.
|
||||
|
||||
import logging
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from simple_history.utils import bulk_create_with_history
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
@@ -61,7 +62,7 @@ def write_program_enrollments(program_uuid, enrollment_requests, create, update)
|
||||
existing_enrollments = fetch_program_enrollments(
|
||||
program_uuid=program_uuid, external_user_keys=external_keys
|
||||
)
|
||||
existing_enrollments_by_key = {key: None for key in external_keys}
|
||||
existing_enrollments_by_key = CaseInsensitiveDict({key: None for key in external_keys})
|
||||
existing_enrollments_by_key.update({
|
||||
enrollment.external_user_key: enrollment
|
||||
for enrollment in existing_enrollments
|
||||
@@ -210,9 +211,9 @@ def write_program_course_enrollments(
|
||||
program_uuid=program_uuid,
|
||||
external_user_keys=processable_external_keys,
|
||||
).prefetch_related('program_course_enrollments')
|
||||
program_enrollments_by_key = {
|
||||
program_enrollments_by_key = CaseInsensitiveDict({
|
||||
enrollment.external_user_key: enrollment for enrollment in program_enrollments
|
||||
}
|
||||
})
|
||||
|
||||
# Fetch enrollments regardless of anchored Program Enrollments
|
||||
existing_course_enrollments = fetch_program_course_enrollments_by_students(
|
||||
@@ -239,7 +240,9 @@ def write_program_course_enrollments(
|
||||
program_enrollment__program_uuid=program_uuid
|
||||
)
|
||||
|
||||
existing_course_enrollments_by_key = {key: None for key in processable_external_keys}
|
||||
existing_course_enrollments_by_key = CaseInsensitiveDict({
|
||||
key: None for key in processable_external_keys
|
||||
})
|
||||
existing_course_enrollments_by_key.update({
|
||||
enrollment.program_enrollment.external_user_key: enrollment
|
||||
for enrollment in existing_course_enrollments_of_program_enrollment
|
||||
@@ -252,7 +255,7 @@ def write_program_course_enrollments(
|
||||
enrollments_to_save = []
|
||||
created_enrollments = []
|
||||
updated_enrollments = []
|
||||
staff_assignments_by_user_key = {}
|
||||
staff_assignments_by_user_key = CaseInsensitiveDict()
|
||||
for external_key, request in requests_by_key.items():
|
||||
course_staff = request['course_staff']
|
||||
status = request['status']
|
||||
@@ -498,7 +501,7 @@ def _organize_requests_by_external_key(enrollment_requests):
|
||||
where requests_by_key is dict[str: dict]
|
||||
and duplicated_keys is set[str].
|
||||
"""
|
||||
requests_by_key = {}
|
||||
requests_by_key = CaseInsensitiveDict()
|
||||
duplicated_keys = set()
|
||||
for request in enrollment_requests:
|
||||
key = request['external_user_key']
|
||||
@@ -534,11 +537,11 @@ def _get_conflicting_active_course_enrollments(
|
||||
Returns:
|
||||
results (dict) with detected conflict entry, or empty dict.
|
||||
"""
|
||||
conflicted_by_user_key = {}
|
||||
conflicted_by_user_key = CaseInsensitiveDict()
|
||||
|
||||
requested_statuses_by_user_key = {
|
||||
requested_statuses_by_user_key = CaseInsensitiveDict({
|
||||
key: request.get('status') for key, request in requests_by_key.items()
|
||||
}
|
||||
})
|
||||
|
||||
for existing_enrollment in existing_course_enrollments:
|
||||
external_user_key = existing_enrollment.program_enrollment.external_user_key
|
||||
|
||||
@@ -5,6 +5,7 @@ Test signal handlers for program_enrollments
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
from django.core.cache import cache
|
||||
from edx_django_utils.cache import RequestCache
|
||||
@@ -12,6 +13,7 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from organizations.tests.factories import OrganizationFactory
|
||||
from social_django.models import UserSocialAuth
|
||||
from testfixtures import LogCapture
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.models import CourseEnrollmentException
|
||||
@@ -27,7 +29,6 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import fake_completed_retirement
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
class ProgramEnrollmentRetireSignalTests(ModuleStoreTestCase):
|
||||
@@ -100,6 +101,7 @@ class ProgramEnrollmentRetireSignalTests(ModuleStoreTestCase):
|
||||
self.assert_enrollment_and_history_retired(enrollment)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase):
|
||||
"""
|
||||
Test post-save handler on UserSocialAuth
|
||||
@@ -110,7 +112,7 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase):
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.external_id = '0000'
|
||||
cls.external_id = 'learner1a'
|
||||
cls.provider_slug = 'uox'
|
||||
cls.course_keys = [
|
||||
CourseKey.from_string('course-v1:edX+DemoX+Test_Course'),
|
||||
@@ -197,13 +199,18 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase):
|
||||
for program_course_enrollment in program_course_enrollments:
|
||||
self._assert_program_course_enrollment(program_course_enrollment)
|
||||
|
||||
def test_waiting_course_enrollments_completed(self):
|
||||
@ddt.data(
|
||||
'learner1a',
|
||||
'LEarnER1A',
|
||||
'LEARNER1A',
|
||||
)
|
||||
def test_waiting_course_enrollments_completed(self, auth_user_key):
|
||||
program_enrollment = self._create_waiting_program_enrollment()
|
||||
program_course_enrollments = self._create_waiting_course_enrollments(program_enrollment)
|
||||
|
||||
UserSocialAuth.objects.create(
|
||||
user=self.user,
|
||||
uid=f'{self.provider_slug}:{self.external_id}'
|
||||
uid=f'{self.provider_slug}:{auth_user_key}'
|
||||
)
|
||||
|
||||
self._assert_program_enrollment_user(program_enrollment, self.user)
|
||||
|
||||
@@ -4,6 +4,7 @@ Unit tests for program_course_enrollments tasks
|
||||
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from django.db.models.base import ObjectDoesNotExist
|
||||
from django.test import TestCase
|
||||
|
||||
Reference in New Issue
Block a user