Create Python API for program_enrollments: Part III
This is the third in a series of commits to create a Python API for the LMS program_enrollments app. It does the following: * Creates api/ folder. * Moves link_program_enrollments.py to api/linking.py * Creates api/reading.py for enrollment-fetching functions. * Updates rest of app to use api/reading.py when it was going directly through the models before. * Other misc. cleanup (isorting, unicode_literals, line breaks, etc). Still to do: * Create api/writing.py and update app to use it instead of going directly through models. * Create api/reset.py and api/expire.py, which the management commands call out to. EDUCATOR-4321
This commit is contained in:
committed by
Kyle McCormick
parent
77aacee6ed
commit
358f989131
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
321
lms/djangoapps/program_enrollments/api/reading.py
Normal file
321
lms/djangoapps/program_enrollments/api/reading.py
Normal file
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
427
lms/djangoapps/program_enrollments/api/tests/test_reading.py
Normal file
427
lms/djangoapps/program_enrollments/api/tests/test_reading.py
Normal file
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <external user key>:<lms username>'
|
||||
DUPLICATE_KEY_TPL = u'external user key {} provided multiple times'
|
||||
INCORRECT_PARAMETER_TEMPLATE = (
|
||||
'incorrectly formatted argument {}, must be in form <external user key>:<lms username>'
|
||||
)
|
||||
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 <program_uuid> <user_item>*
|
||||
where a <user_item> is a string formatted as <external_user_key>:<lms_username>
|
||||
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 <self.program>
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user