619 lines
22 KiB
Python
619 lines
22 KiB
Python
"""
|
|
Enrollment API for creating, updating, and deleting enrollments. Also provides access to enrollment information at a
|
|
course level, such as available course modes.
|
|
|
|
"""
|
|
|
|
|
|
import importlib
|
|
import logging
|
|
|
|
from django.conf import settings
|
|
from django.core.cache import cache
|
|
from opaque_keys.edx.keys import CourseKey
|
|
|
|
from common.djangoapps.course_modes.models import CourseMode
|
|
from openedx.core.djangoapps.enrollments import errors
|
|
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
DEFAULT_DATA_API = 'openedx.core.djangoapps.enrollments.data'
|
|
|
|
|
|
def get_verified_enrollments(username, include_inactive=False):
|
|
"""Retrieves all the courses in which user is enrolled in a verified mode.
|
|
|
|
Takes a user and retrieves all relative enrollments in which the learner is enrolled in a verified mode.
|
|
Includes information regarding how the user is enrolled
|
|
in the the course.
|
|
|
|
Args:
|
|
username: The username of the user we want to retrieve course enrollment information for.
|
|
include_inactive (bool): Determines whether inactive enrollments will be included
|
|
|
|
Returns:
|
|
A list of enrollment information for the given user.
|
|
|
|
Examples:
|
|
>>> get_verified_enrollments("Bob")
|
|
[
|
|
{
|
|
"created": "2014-10-25T20:18:00Z",
|
|
"mode": "verified",
|
|
"is_active": True,
|
|
"user": "Bob",
|
|
"course_details": {
|
|
"course_id": "edX/edX-Insider/2014T2",
|
|
"course_name": "edX Insider Course",
|
|
"enrollment_end": "2014-12-20T20:18:00Z",
|
|
"enrollment_start": "2014-10-15T20:18:00Z",
|
|
"course_start": "2015-02-03T00:00:00Z",
|
|
"course_end": "2015-05-06T00:00:00Z",
|
|
"course_modes": [
|
|
{
|
|
"slug": "honor",
|
|
"name": "Honor Code Certificate",
|
|
"min_price": 0,
|
|
"suggested_prices": "",
|
|
"currency": "usd",
|
|
"expiration_datetime": null,
|
|
"description": null,
|
|
"sku": null,
|
|
"bulk_sku": null
|
|
}
|
|
],
|
|
"invite_only": True
|
|
}
|
|
}
|
|
]
|
|
|
|
"""
|
|
enrollments = get_enrollments(username, include_inactive)
|
|
enrollments = filter(lambda enrollment: CourseMode.is_verified_slug(enrollment['mode']), enrollments)
|
|
return list(enrollments)
|
|
|
|
|
|
def get_enrollments(username, include_inactive=False):
|
|
"""Retrieves all the courses a user is enrolled in.
|
|
|
|
Takes a user and retrieves all relative enrollments. Includes information regarding how the user is enrolled
|
|
in the the course.
|
|
|
|
Args:
|
|
username: The username of the user we want to retrieve course enrollment information for.
|
|
include_inactive (bool): Determines whether inactive enrollments will be included
|
|
|
|
Returns:
|
|
A list of enrollment information for the given user.
|
|
|
|
Examples:
|
|
>>> get_enrollments("Bob")
|
|
[
|
|
{
|
|
"created": "2014-10-20T20:18:00Z",
|
|
"mode": "honor",
|
|
"is_active": True,
|
|
"user": "Bob",
|
|
"course_details": {
|
|
"course_id": "edX/DemoX/2014T2",
|
|
"course_name": "edX Demonstration Course",
|
|
"enrollment_end": "2014-12-20T20:18:00Z",
|
|
"enrollment_start": "2014-10-15T20:18:00Z",
|
|
"course_start": "2015-02-03T00:00:00Z",
|
|
"course_end": "2015-05-06T00:00:00Z",
|
|
"course_modes": [
|
|
{
|
|
"slug": "honor",
|
|
"name": "Honor Code Certificate",
|
|
"min_price": 0,
|
|
"suggested_prices": "",
|
|
"currency": "usd",
|
|
"expiration_datetime": null,
|
|
"description": null,
|
|
"sku": null,
|
|
"bulk_sku": null
|
|
}
|
|
],
|
|
"invite_only": False
|
|
}
|
|
},
|
|
{
|
|
"created": "2014-10-25T20:18:00Z",
|
|
"mode": "verified",
|
|
"is_active": True,
|
|
"user": "Bob",
|
|
"course_details": {
|
|
"course_id": "edX/edX-Insider/2014T2",
|
|
"course_name": "edX Insider Course",
|
|
"enrollment_end": "2014-12-20T20:18:00Z",
|
|
"enrollment_start": "2014-10-15T20:18:00Z",
|
|
"course_start": "2015-02-03T00:00:00Z",
|
|
"course_end": "2015-05-06T00:00:00Z",
|
|
"course_modes": [
|
|
{
|
|
"slug": "honor",
|
|
"name": "Honor Code Certificate",
|
|
"min_price": 0,
|
|
"suggested_prices": "",
|
|
"currency": "usd",
|
|
"expiration_datetime": null,
|
|
"description": null,
|
|
"sku": null,
|
|
"bulk_sku": null
|
|
}
|
|
],
|
|
"invite_only": True
|
|
}
|
|
}
|
|
]
|
|
|
|
"""
|
|
return _data_api().get_course_enrollments(username, include_inactive)
|
|
|
|
|
|
def get_enrollment(username, course_id):
|
|
"""Retrieves all enrollment information for the user in respect to a specific course.
|
|
|
|
Gets all the course enrollment information specific to a user in a course.
|
|
|
|
Args:
|
|
username: The user to get course enrollment information for.
|
|
course_id (str): The course to get enrollment information for.
|
|
|
|
Returns:
|
|
A serializable dictionary of the course enrollment.
|
|
|
|
Example:
|
|
>>> get_enrollment("Bob", "edX/DemoX/2014T2")
|
|
{
|
|
"created": "2014-10-20T20:18:00Z",
|
|
"mode": "honor",
|
|
"is_active": True,
|
|
"user": "Bob",
|
|
"course_details": {
|
|
"course_id": "edX/DemoX/2014T2",
|
|
"course_name": "edX Demonstration Course",
|
|
"enrollment_end": "2014-12-20T20:18:00Z",
|
|
"enrollment_start": "2014-10-15T20:18:00Z",
|
|
"course_start": "2015-02-03T00:00:00Z",
|
|
"course_end": "2015-05-06T00:00:00Z",
|
|
"course_modes": [
|
|
{
|
|
"slug": "honor",
|
|
"name": "Honor Code Certificate",
|
|
"min_price": 0,
|
|
"suggested_prices": "",
|
|
"currency": "usd",
|
|
"expiration_datetime": null,
|
|
"description": null,
|
|
"sku": null,
|
|
"bulk_sku": null
|
|
}
|
|
],
|
|
"invite_only": False
|
|
}
|
|
}
|
|
|
|
"""
|
|
return _data_api().get_course_enrollment(username, course_id)
|
|
|
|
|
|
def add_enrollment(
|
|
username,
|
|
course_id,
|
|
mode=None,
|
|
is_active=True,
|
|
enrollment_attributes=None,
|
|
enterprise_uuid=None,
|
|
force_enrollment=False,
|
|
include_expired=False
|
|
):
|
|
"""Enrolls a user in a course.
|
|
|
|
Enrolls a user in a course. If the mode is not specified, this will default to `CourseMode.DEFAULT_MODE_SLUG`.
|
|
|
|
Arguments:
|
|
username: The user to enroll.
|
|
course_id (str): The course to enroll the user in.
|
|
mode (str): Optional argument for the type of enrollment to create. Ex. 'audit', 'honor', 'verified',
|
|
'professional'. If not specified, this defaults to the default course mode.
|
|
is_active (boolean): Optional argument for making the new enrollment inactive. If not specified, is_active
|
|
defaults to True.
|
|
enrollment_attributes (list): Attributes to be set the enrollment.
|
|
enterprise_uuid (str): Add course enterprise uuid
|
|
force_enrollment (bool): Enroll user even if course enrollment_end date is expired
|
|
include_expired (bool): Boolean denoting whether expired course modes should be included.
|
|
|
|
Returns:
|
|
A serializable dictionary of the new course enrollment.
|
|
|
|
Example:
|
|
>>> add_enrollment("Bob", "edX/DemoX/2014T2", mode="audit")
|
|
{
|
|
"created": "2014-10-20T20:18:00Z",
|
|
"mode": "audit",
|
|
"is_active": True,
|
|
"user": "Bob",
|
|
"course_details": {
|
|
"course_id": "edX/DemoX/2014T2",
|
|
"course_name": "edX Demonstration Course",
|
|
"enrollment_end": "2014-12-20T20:18:00Z",
|
|
"enrollment_start": "2014-10-15T20:18:00Z",
|
|
"course_start": "2015-02-03T00:00:00Z",
|
|
"course_end": "2015-05-06T00:00:00Z",
|
|
"course_modes": [
|
|
{
|
|
"slug": "audit",
|
|
"name": "Audit",
|
|
"min_price": 0,
|
|
"suggested_prices": "",
|
|
"currency": "usd",
|
|
"expiration_datetime": null,
|
|
"description": null,
|
|
"sku": null,
|
|
"bulk_sku": null
|
|
}
|
|
],
|
|
"invite_only": False
|
|
}
|
|
}
|
|
"""
|
|
if mode is None:
|
|
mode = _default_course_mode(course_id)
|
|
validate_course_mode(course_id, mode, is_active=is_active, include_expired=include_expired)
|
|
enrollment = _data_api().create_course_enrollment(
|
|
username, course_id, mode, is_active, enterprise_uuid, force_enrollment=force_enrollment
|
|
)
|
|
|
|
if enrollment_attributes is not None:
|
|
set_enrollment_attributes(username, course_id, enrollment_attributes)
|
|
|
|
return enrollment
|
|
|
|
|
|
def update_enrollment(
|
|
username, course_id, mode=None, is_active=None, enrollment_attributes=None, include_expired=False
|
|
):
|
|
"""Updates the course mode for the enrolled user.
|
|
|
|
Update a course enrollment for the given user and course.
|
|
|
|
Arguments:
|
|
username: The user associated with the updated enrollment.
|
|
course_id (str): The course associated with the updated enrollment.
|
|
|
|
Keyword Arguments:
|
|
mode (str): The new course mode for this enrollment.
|
|
is_active (bool): Sets whether the enrollment is active or not.
|
|
enrollment_attributes (list): Attributes to be set the enrollment.
|
|
include_expired (bool): Boolean denoting whether expired course modes should be included.
|
|
|
|
Returns:
|
|
A serializable dictionary representing the updated enrollment.
|
|
|
|
Example:
|
|
>>> update_enrollment("Bob", "edX/DemoX/2014T2", "honor")
|
|
{
|
|
"created": "2014-10-20T20:18:00Z",
|
|
"mode": "honor",
|
|
"is_active": True,
|
|
"user": "Bob",
|
|
"course_details": {
|
|
"course_id": "edX/DemoX/2014T2",
|
|
"course_name": "edX Demonstration Course",
|
|
"enrollment_end": "2014-12-20T20:18:00Z",
|
|
"enrollment_start": "2014-10-15T20:18:00Z",
|
|
"course_start": "2015-02-03T00:00:00Z",
|
|
"course_end": "2015-05-06T00:00:00Z",
|
|
"course_modes": [
|
|
{
|
|
"slug": "honor",
|
|
"name": "Honor Code Certificate",
|
|
"min_price": 0,
|
|
"suggested_prices": "",
|
|
"currency": "usd",
|
|
"expiration_datetime": null,
|
|
"description": null,
|
|
"sku": null,
|
|
"bulk_sku": null
|
|
}
|
|
],
|
|
"invite_only": False
|
|
}
|
|
}
|
|
|
|
"""
|
|
log.info('Starting Update Enrollment process for user {user} in course {course} to mode {mode}'.format(
|
|
user=username,
|
|
course=course_id,
|
|
mode=mode,
|
|
))
|
|
if mode is not None:
|
|
validate_course_mode(course_id, mode, is_active=is_active, include_expired=include_expired)
|
|
enrollment = _data_api().update_course_enrollment(username, course_id, mode=mode, is_active=is_active)
|
|
if enrollment is None: # lint-amnesty, pylint: disable=no-else-raise
|
|
msg = f"Course Enrollment not found for user {username} in course {course_id}"
|
|
log.warning(msg)
|
|
raise errors.EnrollmentNotFoundError(msg)
|
|
else:
|
|
if enrollment_attributes is not None:
|
|
set_enrollment_attributes(username, course_id, enrollment_attributes)
|
|
log.info('Course Enrollment updated for user {user} in course {course} to mode {mode}'.format(
|
|
user=username,
|
|
course=course_id,
|
|
mode=mode
|
|
))
|
|
return enrollment
|
|
|
|
|
|
def get_course_enrollment_details(course_id, include_expired=False):
|
|
"""Get the course modes for course. Also get enrollment start and end date, invite only, etc.
|
|
|
|
Given a course_id, return a serializable dictionary of properties describing course enrollment information.
|
|
|
|
Args:
|
|
course_id (str): The Course to get enrollment information for.
|
|
|
|
include_expired (bool): Boolean denoting whether expired course modes
|
|
should be included in the returned JSON data.
|
|
|
|
Returns:
|
|
A serializable dictionary of course enrollment information.
|
|
|
|
Example:
|
|
>>> get_course_enrollment_details("edX/DemoX/2014T2")
|
|
{
|
|
"course_id": "edX/DemoX/2014T2",
|
|
"course_name": "edX Demonstration Course",
|
|
"enrollment_end": "2014-12-20T20:18:00Z",
|
|
"enrollment_start": "2014-10-15T20:18:00Z",
|
|
"course_start": "2015-02-03T00:00:00Z",
|
|
"course_end": "2015-05-06T00:00:00Z",
|
|
"course_modes": [
|
|
{
|
|
"slug": "honor",
|
|
"name": "Honor Code Certificate",
|
|
"min_price": 0,
|
|
"suggested_prices": "",
|
|
"currency": "usd",
|
|
"expiration_datetime": null,
|
|
"description": null,
|
|
"sku": null,
|
|
"bulk_sku": null
|
|
}
|
|
],
|
|
"invite_only": False
|
|
}
|
|
|
|
"""
|
|
cache_key = f'enrollment.course.details.{course_id}.{include_expired}'
|
|
cached_enrollment_data = None
|
|
try:
|
|
cached_enrollment_data = cache.get(cache_key)
|
|
except Exception: # pylint: disable=broad-except
|
|
# The cache backend could raise an exception (for example, memcache keys that contain spaces)
|
|
log.exception("Error occurred while retrieving course enrollment details from the cache")
|
|
|
|
if cached_enrollment_data:
|
|
return cached_enrollment_data
|
|
|
|
course_enrollment_details = _data_api().get_course_enrollment_info(course_id, include_expired)
|
|
|
|
try:
|
|
cache_time_out = getattr(settings, 'ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT', 60)
|
|
cache.set(cache_key, course_enrollment_details, cache_time_out)
|
|
except Exception:
|
|
# Catch any unexpected errors during caching.
|
|
log.exception("Error occurred while caching course enrollment details for course %s", course_id)
|
|
raise errors.CourseEnrollmentError("An unexpected error occurred while retrieving course enrollment details.") # lint-amnesty, pylint: disable=raise-missing-from
|
|
|
|
return course_enrollment_details
|
|
|
|
|
|
def set_enrollment_attributes(username, course_id, attributes):
|
|
"""Set enrollment attributes for the enrollment of given user in the
|
|
course provided.
|
|
|
|
Args:
|
|
course_id: The Course to set enrollment attributes for.
|
|
username: The User to set enrollment attributes for.
|
|
attributes (list): Attributes to be set.
|
|
|
|
Example:
|
|
>>>set_enrollment_attributes(
|
|
"Bob",
|
|
"course-v1-edX-DemoX-1T2015",
|
|
[
|
|
{
|
|
"namespace": "credit",
|
|
"name": "provider_id",
|
|
"value": "hogwarts",
|
|
},
|
|
]
|
|
)
|
|
"""
|
|
_data_api().add_or_update_enrollment_attr(username, course_id, attributes)
|
|
|
|
|
|
def get_enrollment_attributes(username, course_id):
|
|
"""Retrieve enrollment attributes for given user for provided course.
|
|
|
|
Args:
|
|
username: The User to get enrollment attributes for
|
|
course_id: The Course to get enrollment attributes for.
|
|
|
|
Example:
|
|
>>>get_enrollment_attributes("Bob", "course-v1-edX-DemoX-1T2015")
|
|
[
|
|
{
|
|
"namespace": "credit",
|
|
"name": "provider_id",
|
|
"value": "hogwarts",
|
|
},
|
|
]
|
|
|
|
Returns: list
|
|
"""
|
|
return _data_api().get_enrollment_attributes(username, course_id)
|
|
|
|
|
|
def _default_course_mode(course_id):
|
|
"""Return the default enrollment for a course.
|
|
|
|
Special case the default enrollment to return if nothing else is found.
|
|
|
|
Arguments:
|
|
course_id (str): The course to check against for available course modes.
|
|
|
|
Returns:
|
|
str
|
|
"""
|
|
course_modes = CourseMode.modes_for_course(CourseKey.from_string(course_id))
|
|
available_modes = [m.slug for m in course_modes]
|
|
|
|
if CourseMode.DEFAULT_MODE_SLUG in available_modes:
|
|
return CourseMode.DEFAULT_MODE_SLUG
|
|
elif 'audit' in available_modes:
|
|
return 'audit'
|
|
elif 'honor' in available_modes:
|
|
return 'honor'
|
|
|
|
return CourseMode.DEFAULT_MODE_SLUG
|
|
|
|
|
|
def validate_course_mode(course_id, mode, is_active=None, include_expired=False):
|
|
"""Checks to see if the specified course mode is valid for the course.
|
|
|
|
If the requested course mode is not available for the course, raise an error with corresponding
|
|
course enrollment information.
|
|
|
|
Arguments:
|
|
course_id (str): The course to check against for available course modes.
|
|
mode (str): The slug for the course mode specified in the enrollment.
|
|
|
|
Keyword Arguments:
|
|
is_active (bool): Whether the enrollment is to be activated or deactivated.
|
|
include_expired (bool): Boolean denoting whether expired course modes should be included.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Raises:
|
|
CourseModeNotFound: raised if the course mode is not found.
|
|
"""
|
|
# If the client has requested an enrollment deactivation, we want to include expired modes
|
|
# in the set of available modes. This allows us to unenroll users from expired modes.
|
|
# If include_expired is set as True we should not redetermine its value.
|
|
if not include_expired:
|
|
include_expired = not is_active if is_active is not None else False
|
|
|
|
course_enrollment_info = _data_api().get_course_enrollment_info(course_id, include_expired=include_expired)
|
|
course_modes = course_enrollment_info["course_modes"]
|
|
available_modes = [m['slug'] for m in course_modes]
|
|
if mode not in available_modes:
|
|
msg = (
|
|
"Specified course mode '{mode}' unavailable for course {course_id}. "
|
|
"Available modes were: {available}"
|
|
).format(
|
|
mode=mode,
|
|
course_id=course_id,
|
|
available=", ".join(available_modes)
|
|
)
|
|
log.warning(msg)
|
|
raise errors.CourseModeNotFoundError(msg, course_enrollment_info)
|
|
|
|
|
|
def unenroll_user_from_all_courses(username):
|
|
"""
|
|
Unenrolls a specified user from all of the courses they are currently enrolled in.
|
|
:param username: The id of the user being unenrolled.
|
|
:return: The IDs of all of the organizations from which the learner was unenrolled.
|
|
"""
|
|
return _data_api().unenroll_user_from_all_courses(username)
|
|
|
|
|
|
def get_user_roles(username):
|
|
"""
|
|
Returns a list of all roles that this user has.
|
|
:param username: The id of the selected user.
|
|
:return: All roles for all courses that this user has.
|
|
"""
|
|
return _data_api().get_user_roles(username)
|
|
|
|
|
|
def serialize_enrollments(enrollments):
|
|
"""
|
|
Takes a list of CourseEnrollment objects and serializes them.
|
|
|
|
Serialized result will be compatible will the results from `get_enrollments`. If
|
|
the `get_enrollments` function changes to return non-serialized data, this will
|
|
need to change as well.
|
|
|
|
Args:
|
|
enrollments: list of CourseEnrollment objects to be serialized
|
|
|
|
Returns:
|
|
A list of enrollments
|
|
"""
|
|
return _data_api().serialize_enrollments(enrollments)
|
|
|
|
|
|
def is_enrollment_valid_for_proctoring(username, course_id):
|
|
"""
|
|
Returns a boolean value regarding whether user's course enrollment is eligible for proctoring.
|
|
|
|
Returns false if:
|
|
* special exams aren't enabled
|
|
* the enrollment is not active
|
|
* proctored exams aren't enabled for the course
|
|
* the course mode is audit
|
|
|
|
Arguments:
|
|
* username (str): The user associated with the enrollment.
|
|
* course_id (str): The course id associated with the enrollment.
|
|
"""
|
|
if not settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'):
|
|
return False
|
|
|
|
# Verify that the learner's enrollment is active
|
|
enrollment = _data_api().get_course_enrollment(username, str(course_id))
|
|
if not enrollment or not enrollment['is_active']:
|
|
return False
|
|
|
|
# Check that the course has proctored exams enabled
|
|
course_block = modulestore().get_course(course_id)
|
|
if not course_block or not course_block.enable_proctored_exams:
|
|
return False
|
|
|
|
# Only allow verified modes
|
|
appropriate_modes = [
|
|
CourseMode.VERIFIED, CourseMode.MASTERS, CourseMode.PROFESSIONAL, CourseMode.EXECUTIVE_EDUCATION
|
|
]
|
|
|
|
# If the proctoring provider allows learners in honor mode to take exams, include it
|
|
if settings.PROCTORING_BACKENDS.get(course_block.proctoring_provider, {}).get('allow_honor_mode'):
|
|
appropriate_modes.append(CourseMode.HONOR)
|
|
|
|
if enrollment['mode'] not in appropriate_modes:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def _data_api():
|
|
"""Returns a Data API.
|
|
This relies on Django settings to find the appropriate data API.
|
|
|
|
"""
|
|
# We retrieve the settings in-line here (rather than using the
|
|
# top-level constant), so that @override_settings will work
|
|
# in the test suite.
|
|
api_path = getattr(settings, "ENROLLMENT_DATA_API", DEFAULT_DATA_API)
|
|
|
|
try:
|
|
return importlib.import_module(api_path)
|
|
except (ImportError, ValueError):
|
|
log.exception(f"Could not load module at '{api_path}'")
|
|
raise errors.EnrollmentApiLoadError(api_path) # lint-amnesty, pylint: disable=raise-missing-from
|