* feat: Refactor out non REST portions of enrollment api from enrollment POST method For use with edx-enterprise to avoid making REST calls for bulk enrollment and other use cases ENT-4746 * feat: Remove unused test Testing is covered by test_views * refactor: isort isort fixes * docs: ADR for why this change ADR ENT-4746 * test: Fix test failure by restoring course_id to correct object * test: Test fix * refactor: pylint fixes * refactor: raise from to avoid pylint error * refactor: Start to work toward a util in enterprise_support instead of refactoring this endpoint * feat: Add util function in enterprise_support to eventually handle enrollment, only used by bulk enrollment for now * feat: One more revised idea, this time low risk in edx platform and also helps address enterprise specific flow. testing pending * feat: syntax and unused constant * feat: Restore view and add new util function to use in edx-enterprise instead * feat: breakpoint * unused import * feat: don't fail on existing enrollment * docs: ADR update * docs: docstring minor update * test: unit test add_user_to_course_cohort * refactor: imports * feat: remove unused error classes * refactor: lint * test: Test cases * test: Two more tests for negative cases * feat: missing init.py file * test: Fix tests to use correct user mock * unused import * refactor: Review feedback, test fixes, needs rebase now * feat: rebase changes * feat: keep audit_log with similar logic as in the view * refactor: Review feedback, test constant usage
109 lines
4.6 KiB
Python
109 lines
4.6 KiB
Python
"""
|
|
Utils for use in enrollment codebase such as views.
|
|
"""
|
|
import logging
|
|
|
|
from django.core.exceptions import ObjectDoesNotExist # lint-amnesty, pylint: disable=wrong-import-order
|
|
from django.db import transaction
|
|
|
|
from common.djangoapps.student.models import User
|
|
from openedx.core.djangoapps.course_groups.cohorts import CourseUserGroup
|
|
from openedx.core.djangoapps.enrollments import api as enrollment_api
|
|
from openedx.core.djangoapps.enrollments.errors import CourseEnrollmentError, CourseEnrollmentExistsError
|
|
from openedx.core.djangoapps.enrollments.utils import add_user_to_course_cohort
|
|
from openedx.core.lib.log_utils import audit_log
|
|
from openedx.features.enterprise_support.enrollments.exceptions import (
|
|
CourseIdMissingException,
|
|
UserDoesNotExistException
|
|
)
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def lms_enroll_user_in_course(
|
|
username,
|
|
course_id,
|
|
mode,
|
|
enterprise_uuid,
|
|
cohort_name=None,
|
|
is_active=True,
|
|
):
|
|
"""
|
|
Enrollment function meant to be called by edx-enterprise to replace the
|
|
current uses of the EnrollmentApiClient
|
|
The REST enrollment endpoint may also eventually also want to reuse this function
|
|
since it's a subset of what the endpoint handles
|
|
|
|
Unlike the REST endpoint, this function does not check for enterprise enabled, or user api key
|
|
permissions etc. Those concerns are still going to be used by REST endpoint but this function
|
|
is meant for use from within edx-enterprise hence already presume such privileges.
|
|
|
|
Arguments:
|
|
- username (str): User name
|
|
- course_id (obj) : Course key obtained using CourseKey.from_string(course_id_input)
|
|
- mode (CourseMode): course mode
|
|
- enterprise_uuid (str): id to identify the enterprise to enroll under
|
|
- cohort_name (str): Optional. If provided, user will be added to cohort
|
|
- is_active (bool): Optional. A Boolean value that indicates whether the
|
|
enrollment is to be set to inactive (if False). Usually we want a True if enrolling anew.
|
|
|
|
Returns: A serializable dictionary of the new course enrollment. If it hits
|
|
`CourseEnrollmentExistsError` then it logs the error and returns None.
|
|
"""
|
|
user = _validate_enrollment_inputs(username, course_id)
|
|
|
|
with transaction.atomic():
|
|
try:
|
|
response = enrollment_api.add_enrollment(
|
|
username,
|
|
str(course_id),
|
|
mode=mode,
|
|
is_active=is_active,
|
|
enrollment_attributes=None,
|
|
enterprise_uuid=enterprise_uuid,
|
|
)
|
|
add_user_to_course_cohort(cohort_name, course_id, user)
|
|
log.info('The user [%s] has been enrolled in course run [%s].', username, course_id)
|
|
return response
|
|
except CourseEnrollmentExistsError as error:
|
|
log.warning('An enrollment already exists for user [%s] in course run [%s].', username, course_id)
|
|
return None
|
|
except CourseEnrollmentError as error:
|
|
log.exception("An error occurred while creating the new course enrollment for user "
|
|
"[%s] in course run [%s]", username, course_id)
|
|
raise error
|
|
except CourseUserGroup.DoesNotExist as error:
|
|
log.exception('Missing cohort [%s] in course run [%s]', cohort_name, course_id)
|
|
raise error
|
|
finally:
|
|
# Assumes that the ecommerce service uses an API key to authenticate.
|
|
current_enrollment = enrollment_api.get_enrollment(username, str(course_id))
|
|
audit_log(
|
|
'enrollment_change_requested',
|
|
course_id=str(course_id),
|
|
requested_mode=mode,
|
|
actual_mode=current_enrollment['mode'] if current_enrollment else None,
|
|
requested_activation=is_active,
|
|
actual_activation=current_enrollment['is_active'] if current_enrollment else None,
|
|
user_id=user.id
|
|
)
|
|
|
|
|
|
def _validate_enrollment_inputs(username, course_id):
|
|
"""
|
|
Validates username and course_id.
|
|
Raises:
|
|
- UserDoesNotExistException if user not found.
|
|
- CourseIdMissingException if course_id not provided.
|
|
"""
|
|
if not course_id:
|
|
raise CourseIdMissingException("Course ID must be specified to create a new enrollment.")
|
|
if not username:
|
|
raise UserDoesNotExistException('username is a required argument for enrollment')
|
|
try:
|
|
# Lookup the user, instead of using request.user, since request.user may not match the username POSTed.
|
|
user = User.objects.get(username=username)
|
|
except ObjectDoesNotExist as error:
|
|
raise UserDoesNotExistException(f'The user {username} does not exist.') from error
|
|
return user
|