diff --git a/lms/djangoapps/program_enrollments/api/__init__.py b/lms/djangoapps/program_enrollments/api/__init__.py index e69de29bb2..6a6aa4c7f2 100644 --- a/lms/djangoapps/program_enrollments/api/__init__.py +++ b/lms/djangoapps/program_enrollments/api/__init__.py @@ -0,0 +1,11 @@ +""" +Python API exposed by the proram_enrollments app to other in-process apps. + +The functions are split into separate files for code organization, but they +are wildcard-imported into here so they can be imported directly from +`lms.djangoapps.program_enrollments.api`. +""" +from __future__ import absolute_import + +from .linking import * # pylint: disable=wildcard-import +from .reading import * # pylint: disable=wildcard-import diff --git a/lms/djangoapps/program_enrollments/api/api.py b/lms/djangoapps/program_enrollments/api/api.py index 438dfab2a0..4aa97478db 100644 --- a/lms/djangoapps/program_enrollments/api/api.py +++ b/lms/djangoapps/program_enrollments/api/api.py @@ -2,17 +2,17 @@ """ ProgramEnrollment internal API intended for Enterprise API. +This is not part of the program_enrollments Python API. + The Enterprise API currently depends on this module being present with these functions, as implemented in ./utils.py. This module will be refactored away in https://openedx.atlassian.net/browse/ENT-2294 """ from __future__ import absolute_import, unicode_literals -from lms.djangoapps.program_enrollments.rest_api.v1.utils import ( - get_due_dates as get_due_dates_util, - get_course_run_url as get_course_run_url_util, - get_emails_enabled as get_emails_enabled_util, -) +from lms.djangoapps.program_enrollments.rest_api.v1.utils import get_course_run_url as get_course_run_url_util +from lms.djangoapps.program_enrollments.rest_api.v1.utils import get_due_dates as get_due_dates_util +from lms.djangoapps.program_enrollments.rest_api.v1.utils import get_emails_enabled as get_emails_enabled_util def get_due_dates(request, course_key, user): diff --git a/lms/djangoapps/program_enrollments/link_program_enrollments.py b/lms/djangoapps/program_enrollments/api/linking.py similarity index 77% rename from lms/djangoapps/program_enrollments/link_program_enrollments.py rename to lms/djangoapps/program_enrollments/api/linking.py index 116a9f31ef..d0334228c8 100644 --- a/lms/djangoapps/program_enrollments/link_program_enrollments.py +++ b/lms/djangoapps/program_enrollments/api/linking.py @@ -1,32 +1,43 @@ -""" Function to link program enrollments and external_student_keys to an LMS user """ +""" +Python API function to link program enrollments and external_student_keys to an +LMS user. + +Outside of this subpackage, import these functions +from `lms.djangoapps.program_enrollments.api`. +""" +from __future__ import absolute_import, unicode_literals + import logging from uuid import UUID from django.contrib.auth import get_user_model from django.db import IntegrityError, transaction -from lms.djangoapps.program_enrollments.models import ProgramEnrollment + from student.models import CourseEnrollmentException +from .reading import fetch_program_enrollments + logger = logging.getLogger(__name__) User = get_user_model() -NO_PROGRAM_ENROLLMENT_TPL = ( - u'No program enrollment found for program uuid={program_uuid} and external student ' + +NO_PROGRAM_ENROLLMENT_TEMPLATE = ( + 'No program enrollment found for program uuid={program_uuid} and external student ' 'key={external_student_key}' ) -NO_LMS_USER_TPL = u'No user found with username {}' -COURSE_ENROLLMENT_ERR_TPL = ( - u'Failed to enroll user {user} with waiting program course enrollment for course {course}' +NO_LMS_USER_TEMPLATE = 'No user found with username {}' +COURSE_ENROLLMENT_ERR_TEMPLATE = ( + 'Failed to enroll user {user} with waiting program course enrollment for course {course}' ) -EXISTING_USER_TPL = ( - u'Program enrollment with external_student_key={external_student_key} is already linked to ' - u'{account_relation} account username={username}' +EXISTING_USER_TEMPLATE = ( + 'Program enrollment with external_student_key={external_student_key} is already linked to ' + '{account_relation} account username={username}' ) @transaction.atomic def link_program_enrollments_to_lms_users(program_uuid, external_keys_to_usernames): - u""" + """ Utility function to link ProgramEnrollments to LMS Users Arguments: @@ -61,24 +72,26 @@ def link_program_enrollments_to_lms_users(program_uuid, external_keys_to_usernam """ _validate_inputs(program_uuid, external_keys_to_usernames) errors = {} - program_enrollments = get_program_enrollments(program_uuid, external_keys_to_usernames.keys()) - users = get_lms_users(external_keys_to_usernames.values()) + program_enrollments = _get_program_enrollments_by_ext_key( + program_uuid, external_keys_to_usernames.keys() + ) + users = _get_lms_users(external_keys_to_usernames.values()) for item in external_keys_to_usernames.items(): external_student_key, username = item user = users.get(username) error_message = None if not user: - error_message = NO_LMS_USER_TPL.format(username) + error_message = NO_LMS_USER_TEMPLATE.format(username) program_enrollment = program_enrollments.get(external_student_key) if not program_enrollment: - error_message = NO_PROGRAM_ENROLLMENT_TPL.format( + error_message = NO_PROGRAM_ENROLLMENT_TEMPLATE.format( program_uuid=program_uuid, external_student_key=external_student_key ) elif program_enrollment.user: - error_message = get_existing_user_message(program_enrollment, user) + error_message = user_already_linked_message(program_enrollment, user) if error_message: logger.warning(error_message) @@ -89,7 +102,7 @@ def link_program_enrollments_to_lms_users(program_uuid, external_keys_to_usernam with transaction.atomic(): link_program_enrollment_to_lms_user(program_enrollment, user) except (CourseEnrollmentException, IntegrityError) as e: - logger.exception(u"Rolling back all operations for {}:{}".format( + logger.exception("Rolling back all operations for {}:{}".format( external_student_key, username, )) @@ -101,39 +114,6 @@ def link_program_enrollments_to_lms_users(program_uuid, external_keys_to_usernam return errors -def _validate_inputs(program_uuid, external_keys_to_usernames): - if None in external_keys_to_usernames or None in external_keys_to_usernames.values(): - raise ValueError('external_user_key or username cannot be None') - UUID(str(program_uuid)) # raises ValueError if invalid - - -def get_program_enrollments(program_uuid, external_student_keys): - """ - Does a bulk read of ProgramEnrollments for a given program and list of external student keys - and returns a dict keyed by external student key - """ - program_enrollments = ProgramEnrollment.bulk_read_by_student_key( - program_uuid, - external_student_keys - ).prefetch_related( - 'program_course_enrollments' - ).select_related('user') - return { - program_enrollment.external_user_key: program_enrollment - for program_enrollment in program_enrollments - } - - -def get_lms_users(lms_usernames): - """ - Does a bulk read of Users by username and returns a dict keyed by username - """ - return { - user.username: user - for user in User.objects.filter(username__in=lms_usernames) - } - - def link_program_enrollment_to_lms_user(program_enrollment, user): """ Attempts to link the given program enrollment to the given user @@ -151,13 +131,59 @@ def link_program_enrollment_to_lms_user(program_enrollment, user): raise +def user_already_linked_message(program_enrollment, user): + """ + Creates an error message that the specified program enrollment is already linked to an lms user + """ + existing_username = program_enrollment.user.username + external_student_key = program_enrollment.external_user_key + return EXISTING_USER_TEMPLATE.format( + external_student_key=external_student_key, + account_relation='target' if program_enrollment.user.id == user.id else 'a different', + username=existing_username, + ) + + +def _validate_inputs(program_uuid, external_keys_to_usernames): + if None in external_keys_to_usernames or None in external_keys_to_usernames.values(): + raise ValueError('external_user_key or username cannot be None') + UUID(str(program_uuid)) # raises ValueError if invalid + + +def _get_program_enrollments_by_ext_key(program_uuid, external_student_keys): + """ + Does a bulk read of ProgramEnrollments for a given program and list of external student keys + and returns a dict keyed by external student key + """ + program_enrollments = fetch_program_enrollments( + program_uuid=program_uuid, + external_user_keys=external_student_keys, + ).prefetch_related( + 'program_course_enrollments' + ).select_related('user') + return { + program_enrollment.external_user_key: program_enrollment + for program_enrollment in program_enrollments + } + + +def _get_lms_users(lms_usernames): + """ + Does a bulk read of Users by username and returns a dict keyed by username + """ + return { + user.username: user + for user in User.objects.filter(username__in=lms_usernames) + } + + def _link_program_enrollment(program_enrollment, user): """ Links program enrollment to user. Raises IntegrityError if ProgramEnrollment is invalid """ - logger.info(u'Linking external student key {} and user {}'.format( + logger.info('Linking external student key {} and user {}'.format( program_enrollment.external_user_key, user.username )) @@ -177,22 +203,9 @@ def _link_course_enrollments(program_enrollment, user): for program_course_enrollment in program_enrollment.program_course_enrollments.all(): program_course_enrollment.enroll(user) except CourseEnrollmentException as e: - error_message = COURSE_ENROLLMENT_ERR_TPL.format( + error_message = COURSE_ENROLLMENT_ERR_TEMPLATE.format( user=user.username, course=program_course_enrollment.course_key ) logger.exception(error_message) raise type(e)(error_message) - - -def get_existing_user_message(program_enrollment, user): - """ - Creates an error message that the specified program enrollment is already linked to an lms user - """ - existing_username = program_enrollment.user.username - external_student_key = program_enrollment.external_user_key - return EXISTING_USER_TPL.format( - external_student_key=external_student_key, - account_relation='target' if program_enrollment.user.id == user.id else 'a different', - username=existing_username, - ) diff --git a/lms/djangoapps/program_enrollments/api/reading.py b/lms/djangoapps/program_enrollments/api/reading.py new file mode 100644 index 0000000000..146766feb3 --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/reading.py @@ -0,0 +1,321 @@ +""" +Python API functions related to reading program enrollments. + +Outside of this subpackage, import these functions +from `lms.djangoapps.program_enrollments.api`. +""" +from __future__ import absolute_import, unicode_literals + +from ..models import ProgramCourseEnrollment, ProgramEnrollment + +_STUDENT_ARG_ERROR_MESSAGE = ( + "user and external_user_key are both None; at least one must be provided." +) +_REALIZED_FILTER_ERROR_TEMPLATE = ( + "{} and {} are mutually exclusive; at most one of them may be passed in as True." +) + + +def get_program_enrollment( + program_uuid, + user=None, + external_user_key=None, + curriculum_uuid=None, +): + """ + Get a single program enrollment. + + Required arguments: + * program_uuid (UUID|str) + * At least one of: + * user (User) + * external_user_key (str) + + Optional arguments: + * curriculum_uuid (UUID|str) [optional] + + Returns: ProgramEnrollment + + Raises: ProgramEnrollment.DoesNotExist, ProgramEnrollment.MultipleObjectsReturned + """ + if not (user or external_user_key): + raise ValueError(_STUDENT_ARG_ERROR_MESSAGE) + filters = { + "user": user, + "external_user_key": external_user_key, + "curriculum_uuid": curriculum_uuid, + } + return ProgramEnrollment.objects.get( + program_uuid=program_uuid, **_remove_none_values(filters) + ) + + +def get_program_course_enrollment( + program_uuid, + course_key, + user=None, + external_user_key=None, + curriculum_uuid=None, +): + """ + Get a single program-course enrollment. + + Required arguments: + * program_uuid (UUID|str) + * course_key (CourseKey|str) + * At least one of: + * user (User) + * external_user_key (str) + + Optional arguments: + * curriculum_uuid (UUID|str) [optional] + + Returns: ProgramCourseEnrollment + + Raises: + * ProgramCourseEnrollment.DoesNotExist + * ProgramCourseEnrollment.MultipleObjectsReturned + """ + if not (user or external_user_key): + raise ValueError(_STUDENT_ARG_ERROR_MESSAGE) + filters = { + "program_enrollment__user": user, + "program_enrollment__external_user_key": external_user_key, + "program_enrollment__curriculum_uuid": curriculum_uuid, + } + return ProgramCourseEnrollment.objects.get( + program_enrollment__program_uuid=program_uuid, + course_key=course_key, + **_remove_none_values(filters) + ) + + +def fetch_program_enrollments( + program_uuid, + curriculum_uuids=None, + users=None, + external_user_keys=None, + program_enrollment_statuses=None, + realized_only=False, + waiting_only=False, +): + """ + Fetch program enrollments for a specific program. + + Required argument: + * program_uuid (UUID|str) + + Optional arguments: + * curriculum_uuids (iterable[UUID|str]) + * users (iterable[User]) + * external_user_keys (iterable[str]) + * program_enrollment_statuses (iterable[str]) + * realized_only (bool) + * waiting_only (bool) + + Optional arguments are used as filtersets if they are not None. + At most one of (realized_only, waiting_only) may be provided. + + Returns: queryset[ProgramEnrollment] + """ + if realized_only and waiting_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only") + ) + filters = { + "curriculum_uuid__in": curriculum_uuids, + "user__in": users, + "external_user_key__in": external_user_keys, + "status__in": program_enrollment_statuses, + } + if realized_only: + filters["user__isnull"] = False + if waiting_only: + filters["user__isnull"] = True + return ProgramEnrollment.objects.filter( + program_uuid=program_uuid, **_remove_none_values(filters) + ) + + +def fetch_program_course_enrollments( + program_uuid, + course_key, + curriculum_uuids=None, + users=None, + external_user_keys=None, + program_enrollment_statuses=None, + active_only=False, + inactive_only=False, + realized_only=False, + waiting_only=False, +): + """ + Fetch program-course enrollments for a specific program and course run. + + Required argument: + * program_uuid (UUID|str) + * course_key (CourseKey|str) + + Optional arguments: + * curriculum_uuids (iterable[UUID|str]) + * users (iterable[User]) + * external_user_keys (iterable[str]) + * program_enrollment_statuses (iterable[str]) + * active_only (bool) + * inactive_only (bool) + * realized_only (bool) + * waiting_only (bool) + + Optional arguments are used as filtersets if they are not None. + At most one of (realized_only, waiting_only) may be provided. + At most one of (active_only, inactive_only) may be provided. + + Returns: queryset[ProgramCourseEnrollment] + """ + if active_only and inactive_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("active_only", "inactive_only") + ) + if realized_only and waiting_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only") + ) + filters = { + "program_enrollment__curriculum_uuid__in": curriculum_uuids, + "program_enrollment__user__in": users, + "program_enrollment__external_user_key__in": external_user_keys, + "program_enrollment__status__in": program_enrollment_statuses, + } + if active_only: + filters["status"] = "active" + if inactive_only: + filters["status"] = "inactive" + if realized_only: + filters["program_enrollment__user__isnull"] = False + if waiting_only: + filters["program_enrollment__user__isnull"] = True + return ProgramCourseEnrollment.objects.filter( + program_enrollment__program_uuid=program_uuid, + course_key=course_key, + **_remove_none_values(filters) + ) + + +def fetch_program_enrollments_by_student( + user=None, + external_user_key=None, + program_uuids=None, + curriculum_uuids=None, + program_enrollment_statuses=None, + realized_only=False, + waiting_only=False, +): + """ + Fetch program enrollments for a specific student. + + Required arguments (at least one must be provided): + * user (User) + * external_user_key (str) + + Optional arguments: + * provided_uuids (iterable[UUID|str]) + * curriculum_uuids (iterable[UUID|str]) + * program_enrollment_statuses (iterable[str]) + * realized_only (bool) + * waiting_only (bool) + + Optional arguments are used as filtersets if they are not None. + At most one of (realized_only, waiting_only) may be provided. + + Returns: queryset[ProgramEnrollment] + """ + if not (user or external_user_key): + raise ValueError(_STUDENT_ARG_ERROR_MESSAGE) + if realized_only and waiting_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only") + ) + filters = { + "user": user, + "external_user_key": external_user_key, + "program_uuid__in": program_uuids, + "curriculum_uuid__in": curriculum_uuids, + "status__in": program_enrollment_statuses, + } + if realized_only: + filters["user__isnull"] = False + if waiting_only: + filters["user__isnull"] = True + return ProgramEnrollment.objects.filter(**_remove_none_values(filters)) + + +def fetch_program_course_enrollments_by_student( + user=None, + external_user_key=None, + program_uuids=None, + curriculum_uuids=None, + course_keys=None, + program_enrollment_statuses=None, + active_only=False, + inactive_only=False, + realized_only=False, + waiting_only=False, +): + """ + Fetch program-course enrollments for a specific student. + + Required arguments (at least one must be provided): + * user (User) + * external_user_key (str) + + Optional arguments: + * provided_uuids (iterable[UUID|str]) + * curriculum_uuids (iterable[UUID|str]) + * course_keys (iterable[CourseKey|str]) + * program_enrollment_statuses (iterable[str]) + * realized_only (bool) + * waiting_only (bool) + + Optional arguments are used as filtersets if they are not None. + At most one of (realized_only, waiting_only) may be provided. + At most one of (active_only, inactive_only) may be provided. + + Returns: queryset[ProgramCourseEnrollment] + """ + if not (user or external_user_key): + raise ValueError(_STUDENT_ARG_ERROR_MESSAGE) + if active_only and inactive_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("active_only", "inactive_only") + ) + if realized_only and waiting_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only") + ) + filters = { + "program_enrollment__user": user, + "program_enrollment__external_user_key": external_user_key, + "program_enrollment__program_uuid__in": program_uuids, + "program_enrollment__curriculum_uuid__in": curriculum_uuids, + "course_key__in": course_keys, + "program_enrollment__status__in": program_enrollment_statuses, + } + if active_only: + filters["status"] = "active" + if inactive_only: + filters["status"] = "inactive" + if realized_only: + filters["program_enrollment__user__isnull"] = False + if waiting_only: + filters["program_enrollment__user__isnull"] = True + return ProgramCourseEnrollment.objects.filter(**_remove_none_values(filters)) + + +def _remove_none_values(dictionary): + """ + Return a dictionary where key-value pairs with `None` as the value + are removed. + """ + return { + key: value for key, value in dictionary.items() if value is not None + } diff --git a/lms/djangoapps/program_enrollments/api/tests/__init__.py b/lms/djangoapps/program_enrollments/api/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/program_enrollments/tests/test_link_program_enrollments.py b/lms/djangoapps/program_enrollments/api/tests/test_linking.py similarity index 86% rename from lms/djangoapps/program_enrollments/tests/test_link_program_enrollments.py rename to lms/djangoapps/program_enrollments/api/tests/test_linking.py index 04297dc5ba..330def045c 100644 --- a/lms/djangoapps/program_enrollments/tests/test_link_program_enrollments.py +++ b/lms/djangoapps/program_enrollments/api/tests/test_linking.py @@ -1,31 +1,32 @@ """ -Tests for link_program_enrollments. +Tests for account linking Python API. """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals from uuid import uuid4 -from testfixtures import LogCapture from django.test import TestCase - from edx_django_utils.cache import RequestCache -from lms.djangoapps.program_enrollments.link_program_enrollments import ( - link_program_enrollments_to_lms_users, - NO_PROGRAM_ENROLLMENT_TPL, - NO_LMS_USER_TPL, - COURSE_ENROLLMENT_ERR_TPL, - get_existing_user_message, -) -from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory from opaque_keys.edx.keys import CourseKey +from testfixtures import LogCapture + +from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from student.tests.factories import UserFactory -LOG_PATH = 'lms.djangoapps.program_enrollments.link_program_enrollments' +from ..linking import ( + COURSE_ENROLLMENT_ERR_TEMPLATE, + NO_LMS_USER_TEMPLATE, + NO_PROGRAM_ENROLLMENT_TEMPLATE, + link_program_enrollments_to_lms_users, + user_already_linked_message +) + +LOG_PATH = 'lms.djangoapps.program_enrollments.api.linking' class TestLinkProgramEnrollmentsMixin(object): - """ Utility methods and test data for testing link_program_enrollments """ + """ Utility methods and test data for testing linking """ @classmethod def setUpTestData(cls): # pylint: disable=missing-docstring @@ -57,7 +58,8 @@ class TestLinkProgramEnrollmentsMixin(object): def _create_waiting_course_enrollment(self, program_enrollment, course_key, status='active'): """ - Create a waiting program course enrollment for the given program enrollment, course key, and optionally status + Create a waiting program course enrollment for the given program enrollment, + course key, and optionally status. """ return ProgramCourseEnrollmentFactory.create( program_enrollment=program_enrollment, @@ -84,19 +86,25 @@ class TestLinkProgramEnrollmentsMixin(object): def _assert_program_enrollment(self, user, program_uuid, external_user_key, refresh=True): """ - Assert that the given user is enrolled in the given program with the given external user key + Assert that the given user is enrolled in the given program with the + given external user key. """ if refresh: user.refresh_from_db() - enrollment = user.programenrollment_set.get(program_uuid=program_uuid, external_user_key=external_user_key) + enrollment = user.programenrollment_set.get( + program_uuid=program_uuid, external_user_key=external_user_key + ) self.assertIsNotNone(enrollment) def _assert_user_enrolled_in_program_courses(self, user, program_uuid, *course_keys): """ - Assert that the given user is has active enrollments in the given courses through the given program + Assert that the given user is has active enrollments in the given courses + through the given program. """ user.refresh_from_db() - program_enrollment = user.programenrollment_set.get(user=user, program_uuid=program_uuid) + program_enrollment = user.programenrollment_set.get( + user=user, program_uuid=program_uuid + ) all_course_enrollments = program_enrollment.program_course_enrollments program_course_enrollments = all_course_enrollments.select_related( 'course_enrollment__course' @@ -107,7 +115,9 @@ class TestLinkProgramEnrollmentsMixin(object): program_course_enrollment.course_enrollment for program_course_enrollment in program_course_enrollments ] - self.assertTrue(all(course_enrollment.is_active for course_enrollment in course_enrollments)) + self.assertTrue( + all(course_enrollment.is_active for course_enrollment in course_enrollments) + ) self.assertCountEqual( course_keys, [course_enrollment.course.id for course_enrollment in course_enrollments] @@ -124,7 +134,7 @@ class TestLinkProgramEnrollmentsMixin(object): class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase): - """ Tests for link_program_enrollments behavior """ + """ Tests for linking behavior """ def test_link_only_specified_program(self): """ @@ -142,14 +152,17 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase): link_program_enrollments_to_lms_users(self.program, {'0001': self.user_1.username}) self._assert_program_enrollment(self.user_1, self.program, '0001') - self._assert_user_enrolled_in_program_courses(self.user_1, self.program, self.fruit_course, self.animal_course) + self._assert_user_enrolled_in_program_courses( + self.user_1, self.program, self.fruit_course, self.animal_course + ) self._assert_no_user(another_program_enrollment) def test_inactive_waiting_course_enrollment(self): """ - Test that when a waiting program enrollment has waiting program course enrollments with a status of 'inactive' - the course enrollment created after calling link_program_enrollments will be inactive + Test that when a waiting program enrollment has waiting program course enrollments with a + status of 'inactive' the course enrollment created after calling link_program_enrollments + will be inactive. """ program_enrollment = self._create_waiting_enrollment(self.program, '0001') active_enrollment = self._create_waiting_course_enrollment( @@ -178,7 +191,7 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase): class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase): - """ Tests for link_program_enrollments error behavior """ + """ Tests for linking error behavior """ def test_program_enrollment_not_found__nonexistant(self): self._create_waiting_enrollment(self.program, '0001') @@ -203,7 +216,7 @@ class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase '0002': self.user_2.username, } ) - expected_error_msg = NO_PROGRAM_ENROLLMENT_TPL.format( + expected_error_msg = NO_PROGRAM_ENROLLMENT_TEMPLATE.format( program_uuid=self.program, external_student_key='0002' ) @@ -225,7 +238,7 @@ class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase '0002': 'nonexistant-user', } ) - expected_error_msg = NO_LMS_USER_TPL.format('nonexistant-user') + expected_error_msg = NO_LMS_USER_TEMPLATE.format('nonexistant-user') logger.check_present((LOG_PATH, 'WARNING', expected_error_msg)) self.assertDictEqual(errors, {('0002', 'nonexistant-user'): expected_error_msg}) @@ -250,7 +263,7 @@ class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase '0002': self.user_2.username } ) - expected_error_msg = get_existing_user_message(program_enrollment, self.user_2) + expected_error_msg = user_already_linked_message(program_enrollment, self.user_2) logger.check_present((LOG_PATH, 'WARNING', expected_error_msg)) self.assertDictEqual(errors, {('0002', self.user_2.username): expected_error_msg}) @@ -277,7 +290,7 @@ class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase '0003': self.user_2.username, } ) - expected_error_msg = get_existing_user_message(enrollment, self.user_2) + expected_error_msg = user_already_linked_message(enrollment, self.user_2) logger.check_present((LOG_PATH, 'WARNING', expected_error_msg)) self.assertDictEqual(errors, {('0003', self.user_2.username): expected_error_msg}) @@ -289,14 +302,20 @@ class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase nonexistant_course = CourseKey.from_string('course-v1:edX+Zilch+Bupkis') program_enrollment_1 = self._create_waiting_enrollment(self.program, '0001') - course_enrollment_1 = self._create_waiting_course_enrollment(program_enrollment_1, nonexistant_course) - course_enrollment_2 = self._create_waiting_course_enrollment(program_enrollment_1, self.animal_course) + course_enrollment_1 = self._create_waiting_course_enrollment( + program_enrollment_1, nonexistant_course + ) + course_enrollment_2 = self._create_waiting_course_enrollment( + program_enrollment_1, self.animal_course + ) program_enrollment_2 = self._create_waiting_enrollment(self.program, '0002') self._create_waiting_course_enrollment(program_enrollment_2, self.fruit_course) self._create_waiting_course_enrollment(program_enrollment_2, self.animal_course) - msg = COURSE_ENROLLMENT_ERR_TPL.format(user=self.user_1.username, course=nonexistant_course) + msg = COURSE_ENROLLMENT_ERR_TEMPLATE.format( + user=self.user_1.username, course=nonexistant_course + ) with LogCapture() as logger: errors = link_program_enrollments_to_lms_users( self.program, @@ -307,7 +326,9 @@ class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase ) logger.check_present((LOG_PATH, 'ERROR', msg)) - self.assertDictEqual(errors, {('0001', self.user_1.username): 'NonExistentCourseError: ' + msg}) + self.assertDictEqual( + errors, {('0001', self.user_1.username): 'NonExistentCourseError: ' + msg} + ) self._assert_no_program_enrollment(self.user_1, self.program) self._assert_no_user(program_enrollment_1) course_enrollment_1.refresh_from_db() @@ -315,7 +336,9 @@ class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase course_enrollment_2.refresh_from_db() self.assertIsNone(course_enrollment_2.course_enrollment) - self._assert_user_enrolled_in_program_courses(self.user_2, self.program, self.animal_course, self.fruit_course) + self._assert_user_enrolled_in_program_courses( + self.user_2, self.program, self.animal_course, self.fruit_course + ) def test_integrity_error(self): existing_program_enrollment = self._create_waiting_enrollment(self.program, 'learner-0') diff --git a/lms/djangoapps/program_enrollments/api/tests/test_reading.py b/lms/djangoapps/program_enrollments/api/tests/test_reading.py new file mode 100644 index 0000000000..0526944ee0 --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/tests/test_reading.py @@ -0,0 +1,427 @@ +""" +Tests for account linking Python API. +""" +from __future__ import absolute_import, unicode_literals + +from uuid import UUID + +import ddt +from django.contrib.auth import get_user_model +from django.test import TestCase +from opaque_keys.edx.keys import CourseKey + +from course_modes.models import CourseMode +from lms.djangoapps.program_enrollments.constants import ProgramCourseEnrollmentStatuses as PCEStatuses +from lms.djangoapps.program_enrollments.constants import ProgramEnrollmentStatuses as PEStatuses +from lms.djangoapps.program_enrollments.models import ProgramEnrollment +from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from student.tests.factories import CourseEnrollmentFactory, UserFactory + +from ..reading import ( + fetch_program_course_enrollments, + fetch_program_course_enrollments_by_student, + fetch_program_enrollments, + fetch_program_enrollments_by_student, + get_program_course_enrollment, + get_program_enrollment +) + +User = get_user_model() + + +@ddt.ddt +class ProgramEnrollmentReadingTests(TestCase): + """ + Tests for program enrollment reading functions. + """ + program_uuid_x = UUID('dddddddd-5f48-493d-9410-84e1d36c657f') + program_uuid_y = UUID('eeeeeeee-f803-43f6-bbf3-5ae15d393649') + program_uuid_z = UUID('ffffffff-89eb-43df-a6b9-c144e7204fd7') # No enrollments + curriculum_uuid_a = UUID('aaaaaaaa-bd26-43d0-94b8-b0063858210b') + curriculum_uuid_b = UUID('bbbbbbbb-145f-43db-ad05-f9ad65eec285') + curriculum_uuid_c = UUID('cccccccc-4577-4559-85f0-4a83e8160a4d') + course_key_p = CourseKey.from_string('course-v1:TestX+ProEnroll+P') + course_key_q = CourseKey.from_string('course-v1:TestX+ProEnroll+Q') + course_key_r = CourseKey.from_string('course-v1:TestX+ProEnroll+R') + username_0 = 'user-0' + username_1 = 'user-1' + username_2 = 'user-2' + username_3 = 'user-3' + username_4 = 'user-4' + ext_3 = 'student-3' + ext_4 = 'student-4' + ext_5 = 'student-5' + ext_6 = 'student-6' + + @classmethod + def setUpTestData(cls): + super(ProgramEnrollmentReadingTests, cls).setUpTestData() + cls.user_0 = UserFactory(username=cls.username_0) # No enrollments + cls.user_1 = UserFactory(username=cls.username_1) + cls.user_2 = UserFactory(username=cls.username_2) + cls.user_3 = UserFactory(username=cls.username_3) + cls.user_4 = UserFactory(username=cls.username_4) + CourseOverviewFactory(id=cls.course_key_p) + CourseOverviewFactory(id=cls.course_key_q) + CourseOverviewFactory(id=cls.course_key_r) + enrollment_test_data = [ # ID + (cls.user_1, None, cls.program_uuid_x, cls.curriculum_uuid_a, PEStatuses.ENROLLED), # 1 + (cls.user_2, None, cls.program_uuid_x, cls.curriculum_uuid_a, PEStatuses.PENDING), # 2 + (cls.user_3, cls.ext_3, cls.program_uuid_x, cls.curriculum_uuid_b, PEStatuses.ENROLLED), # 3 + (cls.user_4, cls.ext_4, cls.program_uuid_x, cls.curriculum_uuid_b, PEStatuses.PENDING), # 4 + (None, cls.ext_5, cls.program_uuid_x, cls.curriculum_uuid_b, PEStatuses.SUSPENDED), # 5 + (None, cls.ext_6, cls.program_uuid_y, cls.curriculum_uuid_c, PEStatuses.CANCELED), # 6 + (cls.user_3, cls.ext_3, cls.program_uuid_y, cls.curriculum_uuid_c, PEStatuses.CANCELED), # 7 + (None, cls.ext_4, cls.program_uuid_y, cls.curriculum_uuid_c, PEStatuses.ENROLLED), # 8 + (cls.user_1, None, cls.program_uuid_x, cls.curriculum_uuid_b, PEStatuses.SUSPENDED), # 9 + ] + for user, external_user_key, program_uuid, curriculum_uuid, status in enrollment_test_data: + ProgramEnrollmentFactory( + user=user, + external_user_key=external_user_key, + program_uuid=program_uuid, + curriculum_uuid=curriculum_uuid, + status=status, + ) + course_enrollment_test_data = [ # ID + (1, cls.course_key_p, PCEStatuses.ACTIVE), # 1 + (1, cls.course_key_q, PCEStatuses.ACTIVE), # 2 + (9, cls.course_key_r, PCEStatuses.ACTIVE), # 3 + (2, cls.course_key_p, PCEStatuses.INACTIVE), # 4 + (3, cls.course_key_p, PCEStatuses.ACTIVE), # 5 + (5, cls.course_key_p, PCEStatuses.INACTIVE), # 6 + (8, cls.course_key_p, PCEStatuses.ACTIVE), # 7 + (8, cls.course_key_q, PCEStatuses.INACTIVE), # 8 + (2, cls.course_key_r, PCEStatuses.INACTIVE), # 9 + (6, cls.course_key_r, PCEStatuses.INACTIVE), # 10 + (8, cls.course_key_r, PCEStatuses.ACTIVE), # 11 + (7, cls.course_key_q, PCEStatuses.ACTIVE), # 12 + + ] + for program_enrollment_id, course_key, status in course_enrollment_test_data: + program_enrollment = ProgramEnrollment.objects.get(id=program_enrollment_id) + course_enrollment = ( + CourseEnrollmentFactory( + course_id=course_key, + user=program_enrollment.user, + mode=CourseMode.MASTERS, + ) + if program_enrollment.user + else None + ) + ProgramCourseEnrollmentFactory( + program_enrollment=program_enrollment, + course_enrollment=course_enrollment, + course_key=course_key, + status=status, + ) + + @ddt.data( + # Realized enrollment, specifying only user. + (program_uuid_x, curriculum_uuid_a, username_1, None, 1), + + # Realized enrollment, specifiying both user and external key. + (program_uuid_x, curriculum_uuid_b, username_3, ext_3, 3), + + # Realized enrollment, specifiying only external key. + (program_uuid_x, curriculum_uuid_b, None, ext_4, 4), + + # Waiting enrollment, specifying external key + (program_uuid_x, curriculum_uuid_b, None, ext_5, 5), + + # Specifying no curriculum (because ext_6 only has Program Y + # enrollments in one curriculum, so it's not ambiguous). + (program_uuid_y, None, None, ext_6, 6), + ) + @ddt.unpack + def test_get_program_enrollment( + self, + program_uuid, + curriculum_uuid, + username, + external_user_key, + expected_enrollment_id, + ): + user = User.objects.get(username=username) if username else None + actual_enrollment = get_program_enrollment( + program_uuid=program_uuid, + curriculum_uuid=curriculum_uuid, + user=user, + external_user_key=external_user_key, + ) + assert actual_enrollment.id == expected_enrollment_id + + @ddt.data( + # Realized enrollment, specifying only user. + (program_uuid_x, None, course_key_p, username_1, None, 1), + + # Realized enrollment, specifiying both user and external key. + (program_uuid_x, None, course_key_p, username_3, ext_3, 5), + + # Realized enrollment, specifiying only external key. + (program_uuid_y, None, course_key_p, None, ext_4, 7), + + # Waiting enrollment, specifying external key + (program_uuid_x, None, course_key_p, None, ext_5, 6), + + # We can specify curriculum, but it shouldn't affect anything, + # 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), + ) + @ddt.unpack + def test_get_program_course_enrollment( + self, + program_uuid, + curriculum_uuid, + course_key, + username, + external_user_key, + expected_enrollment_id, + ): + user = User.objects.get(username=username) if username else None + actual_enrollment = get_program_course_enrollment( + program_uuid=program_uuid, + curriculum_uuid=curriculum_uuid, + course_key=course_key, + user=user, + external_user_key=external_user_key, + ) + assert actual_enrollment.id == expected_enrollment_id + + @ddt.data( + + # Program with no enrollments + ( + {'program_uuid': program_uuid_z}, + set(), + ), + + # Curriculum & status filters + ( + { + 'program_uuid': program_uuid_x, + 'curriculum_uuids': {curriculum_uuid_a, curriculum_uuid_c}, + 'program_enrollment_statuses': {PEStatuses.PENDING, PEStatuses.CANCELED}, + }, + {2}, + ), + + # User & external key filters + ( + { + 'program_uuid': program_uuid_x, + 'usernames': {username_1, username_2, username_3, username_4}, + 'external_user_keys': {ext_3, ext_4, ext_5} + }, + {3, 4}, + ), + + # Realized-only filter + ( + {'program_uuid': program_uuid_x, 'realized_only': True}, + {1, 2, 3, 4, 9}, + ), + + # Waiting-only filter + ( + {'program_uuid': program_uuid_x, 'waiting_only': True}, + {5}, + ), + ) + @ddt.unpack + def test_fetch_program_enrollments(self, kwargs, expected_enrollment_ids): + kwargs = self._usernames_to_users(kwargs) + actual_enrollments = fetch_program_enrollments(**kwargs) + actual_enrollment_ids = {enrollment.id for enrollment in actual_enrollments} + assert actual_enrollment_ids == expected_enrollment_ids + + @ddt.data( + + # Program with no enrollments + ( + {'program_uuid': program_uuid_z, 'course_key': course_key_p}, + set(), + ), + + # Curriculum, status, active-only filters + ( + { + 'program_uuid': program_uuid_x, + 'course_key': course_key_p, + 'curriculum_uuids': {curriculum_uuid_a, curriculum_uuid_c}, + 'program_enrollment_statuses': {PEStatuses.ENROLLED}, + 'active_only': True, + }, + {1}, + ), + + # User and external key filters + ( + { + 'program_uuid': program_uuid_x, + 'course_key': course_key_p, + 'usernames': {username_2, username_3}, + 'external_user_keys': {ext_3, ext_5} + }, + {5}, + ), + + # Realized-only filter + ( + { + 'program_uuid': program_uuid_x, + 'course_key': course_key_p, + 'realized_only': True, + }, + {1, 4, 5}, + ), + + # Waiting-only and inactive-only filters + ( + { + 'program_uuid': program_uuid_y, + 'course_key': course_key_r, + 'waiting_only': True, + 'inactive_only': True, + }, + {10}, + ), + ) + @ddt.unpack + def test_fetch_program_course_enrollments(self, kwargs, expected_enrollment_ids): + kwargs = self._usernames_to_users(kwargs) + actual_enrollments = fetch_program_course_enrollments(**kwargs) + actual_enrollment_ids = {enrollment.id for enrollment in actual_enrollments} + assert actual_enrollment_ids == expected_enrollment_ids + + @ddt.data( + + # User with no enrollments + ( + {'username': username_0}, + set(), + ), + + # Filters + ( + { + 'username': username_3, + 'external_user_key': ext_3, + 'program_uuids': {program_uuid_x}, + 'curriculum_uuids': {curriculum_uuid_b, curriculum_uuid_c}, + 'program_enrollment_statuses': {PEStatuses.ENROLLED, PEStatuses.CANCELED}, + }, + {3}, + ), + + # More filters + ( + { + 'username': username_3, + 'external_user_key': ext_3, + 'program_uuids': {program_uuid_x, program_uuid_y}, + 'curriculum_uuids': {curriculum_uuid_b, curriculum_uuid_c}, + 'program_enrollment_statuses': {PEStatuses.SUSPENDED, PEStatuses.CANCELED}, + }, + {7}, + ), + + # Realized-only filter + ( + {'external_user_key': ext_4, 'realized_only': True}, + {4}, + ), + + # Waiting-only filter + ( + {'external_user_key': ext_4, 'waiting_only': True}, + {8}, + ), + ) + @ddt.unpack + def test_fetch_program_enrollments_by_student(self, kwargs, expected_enrollment_ids): + kwargs = self._username_to_user(kwargs) + actual_enrollments = fetch_program_enrollments_by_student(**kwargs) + actual_enrollment_ids = {enrollment.id for enrollment in actual_enrollments} + assert actual_enrollment_ids == expected_enrollment_ids + + @ddt.data( + + # User with no program enrollments + ( + {'username': username_0}, + set(), + ), + + # Course keys and active-only filters + ( + { + 'external_user_key': ext_4, + 'course_keys': {course_key_p, course_key_q}, + 'active_only': True, + }, + {7}, + ), + + # Curriculum filter + ( + {'username': username_3, 'curriculum_uuids': {curriculum_uuid_b}}, + {5}, + ), + + # Program filter + ( + {'username': username_3, 'program_uuids': {program_uuid_y}}, + {12}, + ), + + # Realized-only filter + ( + {'external_user_key': ext_4, 'realized_only': True}, + set(), + ), + + # Waiting-only and inactive-only filter + ( + { + 'external_user_key': ext_4, + 'waiting_only': True, + 'inactive_only': True, + }, + {8}, + ), + ) + @ddt.unpack + def test_fetch_program_course_enrollments_by_student(self, kwargs, expected_enrollment_ids): + kwargs = self._username_to_user(kwargs) + actual_enrollments = fetch_program_course_enrollments_by_student(**kwargs) + actual_enrollment_ids = {enrollment.id for enrollment in actual_enrollments} + assert actual_enrollment_ids == expected_enrollment_ids + + @staticmethod + def _username_to_user(dictionary): + """ + We can't access the user instances when building `ddt.data`, + so return a dict with the username swapped out for the user themself. + """ + result = dictionary.copy() + if 'username' in result: + result['user'] = User.objects.get(username=result['username']) + del result['username'] + return result + + @staticmethod + def _usernames_to_users(dictionary): + """ + We can't access the user instances when building `ddt.data`, + so return a dict with the usernames swapped out for the users themselves. + """ + result = dictionary.copy() + if 'usernames' in result: + result['users'] = set( + User.objects.filter(username__in=result['usernames']) + ) + del result['usernames'] + return result diff --git a/lms/djangoapps/program_enrollments/constants.py b/lms/djangoapps/program_enrollments/constants.py index f6edee1561..103a5f1e2e 100644 --- a/lms/djangoapps/program_enrollments/constants.py +++ b/lms/djangoapps/program_enrollments/constants.py @@ -2,6 +2,7 @@ Constants used throughout the program_enrollments app and exposed to other in-process apps through api.py. """ +from __future__ import absolute_import, unicode_literals class ProgramEnrollmentStatuses(object): diff --git a/lms/djangoapps/program_enrollments/management/commands/expire_waiting_enrollments.py b/lms/djangoapps/program_enrollments/management/commands/expire_waiting_enrollments.py index fbfd8dfa7e..6887c4abda 100644 --- a/lms/djangoapps/program_enrollments/management/commands/expire_waiting_enrollments.py +++ b/lms/djangoapps/program_enrollments/management/commands/expire_waiting_enrollments.py @@ -1,5 +1,5 @@ """ Management command to cleanup old waiting enrollments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import logging @@ -32,5 +32,5 @@ class Command(BaseCommand): def handle(self, *args, **options): expiration_days = options.get('expiration_days') - logger.info(u'Deleting waiting enrollments unmodified for %s days', expiration_days) + logger.info('Deleting waiting enrollments unmodified for %s days', expiration_days) tasks.expire_waiting_enrollments(expiration_days) diff --git a/lms/djangoapps/program_enrollments/management/commands/link_program_enrollments.py b/lms/djangoapps/program_enrollments/management/commands/link_program_enrollments.py index c0c8e9c13d..fd841411d0 100644 --- a/lms/djangoapps/program_enrollments/management/commands/link_program_enrollments.py +++ b/lms/djangoapps/program_enrollments/management/commands/link_program_enrollments.py @@ -1,51 +1,61 @@ """ Management command to link program enrollments and external student_keys to an LMS user """ +from __future__ import absolute_import, unicode_literals + import logging from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError -from lms.djangoapps.program_enrollments.link_program_enrollments import link_program_enrollments_to_lms_users + +from lms.djangoapps.program_enrollments.api import link_program_enrollments_to_lms_users logger = logging.getLogger(__name__) User = get_user_model() -INCORRECT_PARAMETER_TPL = u'incorrectly formatted argument {}, must be in form :' -DUPLICATE_KEY_TPL = u'external user key {} provided multiple times' +INCORRECT_PARAMETER_TEMPLATE = ( + 'incorrectly formatted argument {}, must be in form :' +) +DUPLICATE_KEY_TEMPLATE = 'external user key {} provided multiple times' class Command(BaseCommand): """ - Management command to manually link ProgramEnrollments without an LMS user to an LMS user by username + Management command to manually link ProgramEnrollments without an LMS user to an LMS user by + username. Usage: ./manage.py lms link_program_enrollments * where a is a string formatted as : - Normally, program enrollments should be linked by the Django Social Auth post_save signal handler - `lms.djangoapps.program_enrollments.signals.matriculate_learner`, but in the case that a partner does not - have an IDP set up for learners to log in through, we need a way to link enrollments + Normally, program enrollments should be linked by the Django Social Auth post_save signal + handler `lms.djangoapps.program_enrollments.signals.matriculate_learner`, but in the case that + a partner does not have an IDP set up for learners to log in through, we need a way to link + enrollments. - Provided a program uuid and a list of external_user_key:lms_username, this command will look up the matching - program enrollments and users, and update the program enrollments with the matching user. If the program - enrollment has course enrollments, we will enroll the user into their waiting program courses. + Provided a program uuid and a list of external_user_key:lms_username, this command will look up + the matching program enrollments and users, and update the program enrollments with the matching + user. If the program enrollment has course enrollments, we will enroll the user into their + waiting program courses. - If an external user key is specified twice, an exception will be raised and no enrollments will be modified. + If an external user key is specified twice, an exception will be raised and no enrollments will + be modified. For each external_user_key:lms_username, if: - The user is not found - No enrollment is found for the given program and external_user_key - The enrollment already has a user - An error message will be logged and the input will be skipped. All other inputs will be processed and - enrollments updated. + An error message will be logged and the input will be skipped. All other inputs will be + processed and enrollments updated. - If there is an error while enrolling a user in a waiting program course enrollment, the error will be - logged, and we will roll back all transactions for that user so that their db state will be the same as - it was before this command was run. This is to allow the re-running of the same command again to correctly enroll - the user once the issue preventing the enrollment has been resolved. + If there is an error while enrolling a user in a waiting program course enrollment, the error + will be logged, and we will roll back all transactions for that user so that their db state will + be the same as it was before this command was run. This is to allow the re-running of the same + command again to correctly enroll the user once the issue preventing the enrollment has been + resolved. No other users will be affected, they will be processed normally. """ - help = u'Manually links ProgramEnrollment records to LMS users' + help = 'Manually links ProgramEnrollment records to LMS users' def add_arguments(self, parser): parser.add_argument( @@ -77,13 +87,13 @@ class Command(BaseCommand): for user_item in user_items: split_args = user_item.split(':') if len(split_args) != 2: - message = (INCORRECT_PARAMETER_TPL).format(user_item) + message = (INCORRECT_PARAMETER_TEMPLATE).format(user_item) raise CommandError(message) external_user_key = split_args[0] lms_username = split_args[1] if external_user_key in result: - raise CommandError(DUPLICATE_KEY_TPL.format(external_user_key)) + raise CommandError(DUPLICATE_KEY_TEMPLATE.format(external_user_key)) result[external_user_key] = lms_username return result diff --git a/lms/djangoapps/program_enrollments/management/commands/reset_enrollment_data.py b/lms/djangoapps/program_enrollments/management/commands/reset_enrollment_data.py index 1f9a029f62..676d77d0ad 100644 --- a/lms/djangoapps/program_enrollments/management/commands/reset_enrollment_data.py +++ b/lms/djangoapps/program_enrollments/management/commands/reset_enrollment_data.py @@ -4,7 +4,7 @@ a side effect of enrolling students. Intented for use in integration sandbox environments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import logging from textwrap import dedent @@ -52,7 +52,7 @@ class Command(BaseCommand): ).delete() log.info( - u'The following records will be deleted:\n%s\n%s\n', + 'The following records will be deleted:\n%s\n%s\n', deleted_course_enrollment_models, deleted_program_enrollment_models, ) @@ -62,4 +62,4 @@ class Command(BaseCommand): if confirmation != 'confirm': raise CommandError('User confirmation required. No records have been modified') - log.info(u'Deleting %s records...', q1_count + q2_count) + log.info('Deleting %s records...', q1_count + q2_count) diff --git a/lms/djangoapps/program_enrollments/management/commands/tests/test_link_program_enrollments.py b/lms/djangoapps/program_enrollments/management/commands/tests/test_link_program_enrollments.py index db11d8b041..2f4c899a1e 100644 --- a/lms/djangoapps/program_enrollments/management/commands/tests/test_link_program_enrollments.py +++ b/lms/djangoapps/program_enrollments/management/commands/tests/test_link_program_enrollments.py @@ -3,27 +3,53 @@ Tests for the link_program_enrollments management command. """ from __future__ import absolute_import +import mock from django.core.management import call_command from django.core.management.base import CommandError from django.test import TestCase -from lms.djangoapps.program_enrollments.tests.test_link_program_enrollments import TestLinkProgramEnrollmentsMixin -from lms.djangoapps.program_enrollments.management.commands.link_program_enrollments import ( - Command, - INCORRECT_PARAMETER_TPL, - DUPLICATE_KEY_TPL, -) +from ..link_program_enrollments import DUPLICATE_KEY_TEMPLATE, INCORRECT_PARAMETER_TEMPLATE, Command -COMMAND_PATH = 'lms.djangoapps.program_enrollments.management.commands.link_program_enrollments' +_COMMAND_PATH = 'lms.djangoapps.program_enrollments.management.commands.link_program_enrollments' -class TestLinkProgramEnrollmentManagementCommand(TestLinkProgramEnrollmentsMixin, TestCase): - """ Tests for exception behavior in the link_program_enrollments command """ +class TestLinkProgramEnrollmentManagementCommand(TestCase): + """ + Test that the command calls link_program_enrollments_to_lms_users + correctly and handles exceptional input correctly. + """ - def test_incorrectly_formatted_input(self): - with self.assertRaisesRegex(CommandError, INCORRECT_PARAMETER_TPL.format('whoops')): - call_command(Command(), self.program, 'learner-01:user-01', 'whoops', 'learner-03:user-03') + program_uuid = 'a32c5da8-fb89-4f1e-97a7-b13de9e6dfa2' - def test_repeated_user_key(self): - with self.assertRaisesRegex(CommandError, DUPLICATE_KEY_TPL.format('learner-01')): - call_command(Command(), self.program, 'learner-01:user-01', 'learner-01:user-02') + _LINKING_FUNCTION_MOCK_PATH = _COMMAND_PATH + ".link_program_enrollments_to_lms_users" + + @mock.patch(_LINKING_FUNCTION_MOCK_PATH, autospec=True) + def test_good_input_calls_linking(self, mock_link): + call_command( + Command(), self.program_uuid, 'learner-01:user-01', 'learner-02:user-02' + ) + mock_link.assert_called_once_with( + self.program_uuid, + { + 'learner-01': 'user-01', + 'learner-02': 'user-02', + }, + ) + + def test_incorrectly_formatted_input_exception(self): + with self.assertRaisesRegex( + CommandError, + INCORRECT_PARAMETER_TEMPLATE.format('whoops') + ): + call_command( + Command(), self.program_uuid, 'learner-01:user-01', 'whoops', 'learner-03:user-03' + ) + + def test_repeated_user_key_exception(self): + with self.assertRaisesRegex( + CommandError, + DUPLICATE_KEY_TEMPLATE.format('learner-01'), + ): + call_command( + Command(), self.program_uuid, 'learner-01:user-01', 'learner-01:user-02' + ) diff --git a/lms/djangoapps/program_enrollments/models.py b/lms/djangoapps/program_enrollments/models.py index 7a7e9827f3..7b89e39aac 100644 --- a/lms/djangoapps/program_enrollments/models.py +++ b/lms/djangoapps/program_enrollments/models.py @@ -63,18 +63,6 @@ class ProgramEnrollment(TimeStampedModel): # pylint: disable=model-missing-unic if not (self.user or self.external_user_key): raise ValidationError(_('One of user or external_user_key must not be null.')) - @classmethod - def bulk_read_by_student_key(cls, program_uuid, student_keys): - """ - args: - program_uuid - The UUID of the program to read enrollment data of. - student_keys - list of student keys - """ - return cls.objects.filter( - program_uuid=program_uuid, - external_user_key__in=student_keys, - ) - @classmethod def retire_user(cls, user_id): """ @@ -204,7 +192,7 @@ class ProgramCourseEnrollment(TimeStampedModel): # pylint: disable=model-missin CourseOverview.get_from_id(self.course_key) except CourseOverview.DoesNotExist: logger.warning( - u"User %s failed to enroll in non-existent course %s", user.id, + "User %s failed to enroll in non-existent course %s", user.id, text_type(self.course_key), ) raise NonExistentCourseError diff --git a/lms/djangoapps/program_enrollments/rest_api/v1/constants.py b/lms/djangoapps/program_enrollments/rest_api/v1/constants.py index 5136c5ff6b..25239fa7d0 100644 --- a/lms/djangoapps/program_enrollments/rest_api/v1/constants.py +++ b/lms/djangoapps/program_enrollments/rest_api/v1/constants.py @@ -1,6 +1,7 @@ """ Constants used throughout the program_enrollments V1 API. """ +from __future__ import absolute_import, unicode_literals from lms.djangoapps.program_enrollments.constants import ProgramCourseEnrollmentStatuses, ProgramEnrollmentStatuses diff --git a/lms/djangoapps/program_enrollments/rest_api/v1/serializers.py b/lms/djangoapps/program_enrollments/rest_api/v1/serializers.py index 3e5368c922..fe1a2d63fb 100644 --- a/lms/djangoapps/program_enrollments/rest_api/v1/serializers.py +++ b/lms/djangoapps/program_enrollments/rest_api/v1/serializers.py @@ -1,7 +1,7 @@ """ API Serializers """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals from rest_framework import serializers from six import text_type @@ -216,4 +216,4 @@ class ProgramCourseGradeError(BaseProgramCourseGrade): super(ProgramCourseGradeError, self).__init__( program_course_enrollment ) - self.error = text_type(exception) if exception else u"Unknown error" + self.error = text_type(exception) if exception else "Unknown error" diff --git a/lms/djangoapps/program_enrollments/rest_api/v1/tests/test_views.py b/lms/djangoapps/program_enrollments/rest_api/v1/tests/test_views.py index f39fc890d0..c0a5668645 100644 --- a/lms/djangoapps/program_enrollments/rest_api/v1/tests/test_views.py +++ b/lms/djangoapps/program_enrollments/rest_api/v1/tests/test_views.py @@ -14,10 +14,10 @@ from django.contrib.auth.models import User from django.core.cache import cache from django.test import override_settings from django.urls import reverse +from django.utils import timezone from freezegun import freeze_time from opaque_keys.edx.keys import CourseKey from organizations.tests.factories import OrganizationFactory as LMSOrganizationFactory -from pytz import UTC from rest_framework import status from rest_framework.test import APITestCase from six import text_type @@ -1367,7 +1367,7 @@ class UserProgramReadOnlyAccessGetTests(EnrollmentsDataMixin, APITestCase): ] cls.course_staff = InstructorFactory.create(password=cls.password, course_key=cls.course_id) - cls.date = datetime(2013, 1, 22, tzinfo=UTC) + cls.date = timezone.make_aware(datetime(2013, 1, 22)) CourseEnrollmentFactory( course_id=cls.course_id, user=cls.course_staff, @@ -1493,8 +1493,8 @@ class ProgramCourseEnrollmentOverviewGetTests( # only freeze time when defining these values and not on the whole test case # as test_multiple_enrollments_all_enrolled relies on actual differences in modified datetimes with freeze_time('2019-01-01'): - cls.yesterday = datetime.utcnow() - timedelta(1) - cls.tomorrow = datetime.utcnow() + timedelta(1) + cls.yesterday = timezone.now() - timedelta(1) + cls.tomorrow = timezone.now() + timedelta(1) cls.relative_certificate_download_url = '/download-the-certificates' cls.absolute_certificate_download_url = 'http://www.certificates.com/' @@ -1825,7 +1825,7 @@ class ProgramCourseEnrollmentOverviewGetTests( # course run has not ended and user has earned a passing certificate more than 30 days ago certificate = self.create_generated_certificate() - certificate.created_date = datetime.utcnow() - timedelta(30) + certificate.created_date = timezone.now() - timedelta(30) certificate.save() mock_has_ended.return_value = False @@ -1859,7 +1859,7 @@ class ProgramCourseEnrollmentOverviewGetTests( # course run has not ended and user has earned a passing certificate fewer than 30 days ago certificate = self.create_generated_certificate() - certificate.created_date = datetime.utcnow() - timedelta(5) + certificate.created_date = timezone.now() - timedelta(5) certificate.save() response = self.client.get(self.get_url(self.program_uuid)) diff --git a/lms/djangoapps/program_enrollments/rest_api/v1/views.py b/lms/djangoapps/program_enrollments/rest_api/v1/views.py index b3553e6e55..8cae739338 100644 --- a/lms/djangoapps/program_enrollments/rest_api/v1/views.py +++ b/lms/djangoapps/program_enrollments/rest_api/v1/views.py @@ -26,6 +26,12 @@ from six import text_type from course_modes.models import CourseMode from lms.djangoapps.certificates.api import get_certificate_for_user from lms.djangoapps.grades.api import CourseGradeFactory, clear_prefetched_course_grades, prefetch_course_grades +from lms.djangoapps.program_enrollments.api import ( + fetch_program_course_enrollments, + fetch_program_enrollments, + fetch_program_enrollments_by_student +) +from lms.djangoapps.program_enrollments.constants import ProgramEnrollmentStatuses from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment from lms.djangoapps.program_enrollments.utils import ( ProviderDoesNotExistException, @@ -45,7 +51,7 @@ from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, PaginatedAP from student.helpers import get_resume_urls_for_enrollments from student.models import CourseEnrollment from student.roles import CourseInstructorRole, CourseStaffRole, UserBasedRole -from util.query import use_read_replica_if_available +from util.query import read_replica_or_default from .constants import ( ENABLE_ENROLLMENT_RESET_FLAG, @@ -272,9 +278,9 @@ class ProgramEnrollmentsView( @verify_program_exists def get(self, request, program_uuid=None): """ Defines the GET list endpoint for ProgramEnrollment objects. """ - enrollments = use_read_replica_if_available( - ProgramEnrollment.objects.filter(program_uuid=program_uuid) - ) + enrollments = fetch_program_enrollments( + program_uuid + ).using(read_replica_or_default()) paginated_enrollments = self.paginate_queryset(enrollments) serializer = ProgramEnrollmentSerializer(paginated_enrollments, many=True) return self.get_paginated_response(serializer.data) @@ -423,10 +429,10 @@ class ProgramEnrollmentsView( def get_existing_program_enrollments(self, program_uuid, student_data): """ Returns the existing program enrollments for the given students and program """ student_keys = [data['student_key'] for data in student_data] - return { - e.external_user_key: e - for e in ProgramEnrollment.bulk_read_by_student_key(program_uuid, student_keys) - } + program_enrollments_qs = fetch_program_enrollments( + program_uuid=program_uuid, external_user_keys=student_keys + ) + return {e.external_user_key: e for e in program_enrollments_qs} def _get_created_or_updated_response(self, response_data, default_status=status.HTTP_201_CREATED): """ @@ -540,14 +546,11 @@ class ProgramCourseEnrollmentsView( """ Get a list of students enrolled in a course within a program. """ - enrollments = use_read_replica_if_available( - ProgramCourseEnrollment.objects.filter( - program_enrollment__program_uuid=program_uuid, - course_key=self.course_key - ).select_related( - 'program_enrollment' - ) - ) + enrollments = fetch_program_course_enrollments( + program_uuid, course_id + ).select_related( + 'program_enrollment' + ).using(read_replica_or_default()) paginated_enrollments = self.paginate_queryset(enrollments) serializer = ProgramCourseEnrollmentSerializer(paginated_enrollments, many=True) return self.get_paginated_response(serializer.data) @@ -667,11 +670,10 @@ class ProgramCourseEnrollmentsView( to that user's existing program enrollment in """ external_user_keys = [e["student_key"] for e in enrollments] - existing_enrollments = ProgramEnrollment.objects.filter( - external_user_key__in=external_user_keys, + existing_enrollments = fetch_program_enrollments( program_uuid=program_uuid, - ) - existing_enrollments = existing_enrollments.prefetch_related('program_course_enrollments') + external_user_keys=external_user_keys, + ).prefetch_related('program_course_enrollments') return {enrollment.external_user_key: enrollment for enrollment in existing_enrollments} def enroll_learner_in_course(self, enrollment_request, program_enrollment, program_course_enrollment): @@ -813,16 +815,14 @@ class ProgramCourseGradesView( Returns: list[BaseProgramCourseGrade] """ - enrollments_qs = use_read_replica_if_available( - ProgramCourseEnrollment.objects.filter( - program_enrollment__program_uuid=program_uuid, - program_enrollment__user__isnull=False, - course_key=course_key, - ).select_related( - 'program_enrollment', - 'program_enrollment__user', - ) - ) + enrollments_qs = fetch_program_course_enrollments( + program_uuid=program_uuid, + course_key=course_key, + realized_only=True, + ).select_related( + 'program_enrollment', + 'program_enrollment__user', + ).using(read_replica_or_default()) paginated_enrollments = self.paginate_queryset(enrollments_qs) if not paginated_enrollments: return [] @@ -961,13 +961,11 @@ class UserProgramReadOnlyAccessView(DeveloperErrorViewMixin, PaginatedAPIView): elif self.is_course_staff(request_user): programs = self.get_programs_user_is_course_staff_for(request_user, requested_program_type) else: - program_enrollments = ProgramEnrollment.objects.filter( + program_enrollments = fetch_program_enrollments_by_student( user=request.user, - status__in=('enrolled', 'pending') + program_enrollment_statuses=ProgramEnrollmentStatuses.__ACTIVE__, ) - uuids = [enrollment.program_uuid for enrollment in program_enrollments] - programs = get_programs(uuids=uuids) or [] programs_in_which_user_has_access = [ @@ -1197,12 +1195,12 @@ class ProgramCourseEnrollmentOverviewView( """ Raises ``PermissionDenied`` if the user is not enrolled in the program with the given UUID. """ - program_enrollments = ProgramEnrollment.objects.filter( + user_enrollment_qs = fetch_program_enrollments( program_uuid=program_uuid, - user=user, - status='enrolled', + users={user}, + program_enrollment_statuses={ProgramEnrollmentStatuses.ENROLLED}, ) - if not program_enrollments: + if not user_enrollment_qs.exists(): raise PermissionDenied diff --git a/lms/djangoapps/program_enrollments/signals.py b/lms/djangoapps/program_enrollments/signals.py index 9d9ef769b4..0fe1a945da 100644 --- a/lms/djangoapps/program_enrollments/signals.py +++ b/lms/djangoapps/program_enrollments/signals.py @@ -1,7 +1,7 @@ """ Signal handlers for program enrollments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import logging @@ -9,12 +9,14 @@ from django.db.models.signals import post_save from django.dispatch import receiver from social_django.models import UserSocialAuth -from lms.djangoapps.program_enrollments.models import ProgramEnrollment from openedx.core.djangoapps.catalog.utils import get_programs from openedx.core.djangoapps.user_api.accounts.signals import USER_RETIRE_LMS_MISC from student.models import CourseEnrollmentException from third_party_auth.models import SAMLProviderConfig +from .api import fetch_program_enrollments_by_student +from .models import ProgramEnrollment + logger = logging.getLogger(__name__) @@ -39,7 +41,7 @@ def listen_for_social_auth_creation(sender, instance, created, **kwargs): # pyl matriculate_learner(instance.user, instance.uid) except Exception as e: logger.warning( - u'Unable to link waiting enrollments for user %s, social auth creation failed: %s', + 'Unable to link waiting enrollments for user %s, social auth creation failed: %s', instance.user.id, e, ) @@ -61,24 +63,24 @@ def matriculate_learner(user, uid): if not authorizing_org: return except (AttributeError, ValueError): - logger.info(u'Ignoring non-saml social auth entry for user=%s', user.id) + logger.info('Ignoring non-saml social auth entry for user=%s', user.id) return except SAMLProviderConfig.DoesNotExist: logger.warning( - u'Got incoming social auth for provider=%s but no such provider exists', provider_slug + 'Got incoming social auth for provider=%s but no such provider exists', provider_slug ) return except SAMLProviderConfig.MultipleObjectsReturned: logger.warning( - u'Unable to activate waiting enrollments for user=%s.' - u' Multiple active SAML configurations found for slug=%s. Expected one.', + 'Unable to activate waiting enrollments for user=%s.' + ' Multiple active SAML configurations found for slug=%s. Expected one.', user.id, provider_slug) return - incomplete_enrollments = ProgramEnrollment.objects.filter( + incomplete_enrollments = fetch_program_enrollments_by_student( external_user_key=external_user_key, - user=None, + waiting_only=True, ).prefetch_related('program_course_enrollments') for enrollment in incomplete_enrollments: @@ -88,8 +90,8 @@ def matriculate_learner(user, uid): continue except (KeyError, TypeError): logger.warning( - u'Failed to complete waiting enrollments for organization=%s.' - u' No catalog programs with matching authoring_organization exist.', + 'Failed to complete waiting enrollments for organization=%s.' + ' No catalog programs with matching authoring_organization exist.', authorizing_org.short_name ) continue @@ -101,7 +103,7 @@ def matriculate_learner(user, uid): program_course_enrollment.enroll(user) except CourseEnrollmentException as e: logger.warning( - u'Failed to enroll user=%s with waiting program_course_enrollment=%s: %s', + 'Failed to enroll user=%s with waiting program_course_enrollment=%s: %s', user.id, program_course_enrollment.id, e, diff --git a/lms/djangoapps/program_enrollments/tasks.py b/lms/djangoapps/program_enrollments/tasks.py index 7ab6a00ab3..a2d86eb396 100644 --- a/lms/djangoapps/program_enrollments/tasks.py +++ b/lms/djangoapps/program_enrollments/tasks.py @@ -1,5 +1,5 @@ """ Tasks for program enrollments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import logging from datetime import timedelta @@ -31,21 +31,21 @@ def expire_waiting_enrollments(expiration_days): for program_enrollment in program_enrollments: program_enrollment_ids.append(program_enrollment.id) log.info( - u'Found expired program_enrollment (id=%s) for program_uuid=%s', + 'Found expired program_enrollment (id=%s) for program_uuid=%s', program_enrollment.id, program_enrollment.program_uuid, ) for course_enrollment in program_enrollment.program_course_enrollments.all(): program_course_enrollment_ids.append(course_enrollment.id) log.info( - u'Found expired program_course_enrollment (id=%s) for program_uuid=%s, course_key=%s', + 'Found expired program_course_enrollment (id=%s) for program_uuid=%s, course_key=%s', course_enrollment.id, program_enrollment.program_uuid, course_enrollment.course_key, ) deleted_enrollments = program_enrollments.delete() - log.info(u'Removed %s expired records: %s', deleted_enrollments[0], deleted_enrollments[1]) + log.info('Removed %s expired records: %s', deleted_enrollments[0], deleted_enrollments[1]) deleted_hist_program_enroll = ProgramEnrollment.historical_records.filter( # pylint: disable=no-member id__in=program_enrollment_ids @@ -54,10 +54,10 @@ def expire_waiting_enrollments(expiration_days): id__in=program_course_enrollment_ids ).delete() log.info( - u'Removed %s historical program_enrollment records with id in %s', + 'Removed %s historical program_enrollment records with id in %s', deleted_hist_program_enroll[0], program_enrollment_ids ) log.info( - u'Removed %s historical program_course_enrollment records with id in %s', + 'Removed %s historical program_course_enrollment records with id in %s', deleted_hist_course_enroll[0], program_course_enrollment_ids ) diff --git a/lms/djangoapps/program_enrollments/tests/test_models.py b/lms/djangoapps/program_enrollments/tests/test_models.py index 524a75c56a..cab82f9ad0 100644 --- a/lms/djangoapps/program_enrollments/tests/test_models.py +++ b/lms/djangoapps/program_enrollments/tests/test_models.py @@ -11,7 +11,6 @@ from django.db.utils import IntegrityError from django.test import TestCase from edx_django_utils.cache import RequestCache from opaque_keys.edx.keys import CourseKey -from six.moves import range from testfixtures import LogCapture from course_modes.models import CourseMode @@ -69,47 +68,6 @@ class ProgramEnrollmentModelTests(TestCase): status='suspended', ) - def test_bulk_read_by_student_key(self): - curriculum_a = uuid4() - curriculum_b = uuid4() - enrollments = [] - student_data = {} - - for i in range(5): - # This will give us 4 program enrollments for self.program_uuid - # and 1 enrollment for self.other_program_uuid - user_curriculum = curriculum_b if i % 2 else curriculum_a - user_status = 'pending' if i % 2 else 'enrolled' - user_program = self.other_program_uuid if i == 4 else self.program_uuid - user_key = 'student-{}'.format(i) - enrollments.append( - ProgramEnrollment.objects.create( - user=None, - external_user_key=user_key, - program_uuid=user_program, - curriculum_uuid=user_curriculum, - status=user_status, - ) - ) - student_data[user_key] = {'curriculum_uuid': user_curriculum} - - enrollment_records = ProgramEnrollment.bulk_read_by_student_key(self.program_uuid, student_data) - - expected = { - 'student-0': {'curriculum_uuid': curriculum_a, 'status': 'enrolled', 'program_uuid': self.program_uuid}, - 'student-1': {'curriculum_uuid': curriculum_b, 'status': 'pending', 'program_uuid': self.program_uuid}, - 'student-2': {'curriculum_uuid': curriculum_a, 'status': 'enrolled', 'program_uuid': self.program_uuid}, - 'student-3': {'curriculum_uuid': curriculum_b, 'status': 'pending', 'program_uuid': self.program_uuid}, - } - assert expected == { - enrollment.external_user_key: { - 'curriculum_uuid': enrollment.curriculum_uuid, - 'status': enrollment.status, - 'program_uuid': enrollment.program_uuid, - } - for enrollment in enrollment_records - } - def test_user_retirement(self): """ Test that the external_user_key is successfully retired for a user's program enrollments diff --git a/lms/djangoapps/program_enrollments/tests/test_signals.py b/lms/djangoapps/program_enrollments/tests/test_signals.py index ff32153fd7..c42fbe77f1 100644 --- a/lms/djangoapps/program_enrollments/tests/test_signals.py +++ b/lms/djangoapps/program_enrollments/tests/test_signals.py @@ -2,7 +2,7 @@ Test signal handlers for program_enrollments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import mock import pytest @@ -312,7 +312,7 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase): ( logger.name, 'WARNING', - u'Got incoming social auth for provider={} but no such provider exists'.format('abc') + 'Got incoming social auth for provider={} but no such provider exists'.format('abc') ) ) @@ -330,8 +330,8 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase): uid='{0}:{1}'.format(self.provider_slug, self.external_id) ) error_template = ( - u'Failed to complete waiting enrollments for organization={}.' - u' No catalog programs with matching authoring_organization exist.' + 'Failed to complete waiting enrollments for organization={}.' + ' No catalog programs with matching authoring_organization exist.' ) log.check_present( ( @@ -353,7 +353,7 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase): user=self.user, uid='{0}:{1}'.format(self.provider_slug, self.external_id) ) - error_template = u'Failed to enroll user={} with waiting program_course_enrollment={}: {}' + error_template = 'Failed to enroll user={} with waiting program_course_enrollment={}: {}' log.check_present( ( logger.name, @@ -379,7 +379,7 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase): user=self.user, uid='{0}:{1}'.format(self.provider_slug, self.external_id), ) - error_template = u'Unable to link waiting enrollments for user {}, social auth creation failed: {}' + error_template = 'Unable to link waiting enrollments for user {}, social auth creation failed: {}' log.check_present( ( logger.name, diff --git a/lms/djangoapps/program_enrollments/tests/test_tasks.py b/lms/djangoapps/program_enrollments/tests/test_tasks.py index c0c3dda58d..d6c8d730b2 100644 --- a/lms/djangoapps/program_enrollments/tests/test_tasks.py +++ b/lms/djangoapps/program_enrollments/tests/test_tasks.py @@ -1,7 +1,7 @@ """ Unit tests for program_course_enrollments tasks """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals from datetime import timedelta @@ -77,9 +77,9 @@ class ExpireWaitingEnrollmentsTest(TestCase): with LogCapture(log.name) as log_capture: expire_waiting_enrollments(60) - program_enrollment_message_tmpl = u'Found expired program_enrollment (id={}) for program_uuid={}' + program_enrollment_message_tmpl = 'Found expired program_enrollment (id={}) for program_uuid={}' course_enrollment_message_tmpl = ( - u'Found expired program_course_enrollment (id={}) for program_uuid={}, course_key={}' + 'Found expired program_course_enrollment (id={}) for program_uuid={}, course_key={}' ) log_capture.check_present( diff --git a/lms/djangoapps/program_enrollments/tests/test_utils.py b/lms/djangoapps/program_enrollments/tests/test_utils.py index 8c8f13b1ef..c067acd6ff 100644 --- a/lms/djangoapps/program_enrollments/tests/test_utils.py +++ b/lms/djangoapps/program_enrollments/tests/test_utils.py @@ -1,7 +1,7 @@ """ Unit tests for program_enrollments utils. """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals from uuid import uuid4 diff --git a/lms/djangoapps/program_enrollments/utils.py b/lms/djangoapps/program_enrollments/utils.py index 052e00d65f..85249bcd4d 100644 --- a/lms/djangoapps/program_enrollments/utils.py +++ b/lms/djangoapps/program_enrollments/utils.py @@ -1,7 +1,7 @@ """ utility functions for program enrollments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import logging @@ -106,11 +106,11 @@ def get_provider_slug(organization): try: provider_config = organization.samlproviderconfig_set.current_set().get(enabled=True) except SAMLProviderConfig.DoesNotExist: - log.error(u'No SAML provider found for organization id [%s]', organization.id) + log.error('No SAML provider found for organization id [%s]', organization.id) raise ProviderDoesNotExistException except SAMLProviderConfig.MultipleObjectsReturned: log.error( - u'Multiple active SAML configurations found for organization=%s. Expected one.', + 'Multiple active SAML configurations found for organization=%s. Expected one.', organization.short_name, ) raise ProviderConfigurationException diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index b39f3093cb..2a1947e40e 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -22,7 +22,7 @@ from pytz import UTC from common.test.utils import disable_signal from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from lms.djangoapps.program_enrollments.link_program_enrollments import NO_PROGRAM_ENROLLMENT_TPL +from lms.djangoapps.program_enrollments.api import NO_PROGRAM_ENROLLMENT_TEMPLATE from lms.djangoapps.verify_student.models import VerificationDeadline from student.models import ENROLLED_TO_ENROLLED, CourseEnrollment, CourseEnrollmentAttribute, ManualEnrollmentAudit from student.roles import GlobalStaff, SupportStaffRole @@ -521,5 +521,5 @@ class SupportViewLinkProgramEnrollmentsTests(SupportViewTestCase): 'program_uuid': self.program_uuid, 'text': text, }) - msg = NO_PROGRAM_ENROLLMENT_TPL.format(program_uuid=self.program_uuid, external_student_key=text) + msg = NO_PROGRAM_ENROLLMENT_TEMPLATE.format(program_uuid=self.program_uuid, external_student_key=text) self._assert_props_list('errors', [msg], response) diff --git a/lms/djangoapps/support/views/program_enrollments.py b/lms/djangoapps/support/views/program_enrollments.py index 31fa6b6a4d..7febaf1c77 100644 --- a/lms/djangoapps/support/views/program_enrollments.py +++ b/lms/djangoapps/support/views/program_enrollments.py @@ -10,7 +10,7 @@ from django.views.generic import View from edxmako.shortcuts import render_to_response from lms.djangoapps.support.decorators import require_support_permission -from lms.djangoapps.program_enrollments.link_program_enrollments import link_program_enrollments_to_lms_users +from lms.djangoapps.program_enrollments.api import link_program_enrollments_to_lms_users TEMPLATE_PATH = 'support/link_program_enrollments.html'