def checkout_with_ecommerce_service(user, course_key, course_mode, processor): """ Create a new basket and trigger immediate checkout, using the E-Commerce API. """ course_id = unicode(course_key) try: api = ecommerce_api_client(user) # Make an API call to create the order and retrieve the results result = api.baskets.post({ 'products': [{'sku': course_mode.sku}], 'checkout': True, 'payment_processor_name': processor }) # Pass the payment parameters directly from the API response. return result.get('payment_data') except SlumberBaseException: params = {'username': user.username, 'mode': course_mode.slug, 'course_id': course_id} log.exception('Failed to create order for %(username)s %(mode)s mode of %(course_id)s', params) raise finally: audit_log( 'checkout_requested', course_id=course_id, mode=course_mode.slug, processor_name=processor, user_id=user.id )
def checkout_with_ecommerce_service(user, course_key, course_mode, processor): """ Create a new basket and trigger immediate checkout, using the E-Commerce API. """ course_id = unicode(course_key) try: api = ecommerce_api_client(user) # Make an API call to create the order and retrieve the results result = api.baskets.post({ 'products': [{ 'sku': course_mode.sku }], 'checkout': True, 'payment_processor_name': processor }) # Pass the payment parameters directly from the API response. return result.get('payment_data') except SlumberBaseException: params = { 'username': user.username, 'mode': course_mode.slug, 'course_id': course_id } log.exception( 'Failed to create order for %(username)s %(mode)s mode of %(course_id)s', params) raise finally: audit_log('checkout_requested', course_id=course_id, mode=course_mode.slug, processor_name=processor, user_id=user.id)
def test_log_message(self, mock_log): """Verify that log messages are constructed correctly.""" audit_log('foo', qux='quux', bar='baz') # Verify that the logged message contains comma-separated # key-value pairs ordered alphabetically by key. message = 'foo: bar="baz", qux="quux"' self.assertTrue(mock_log.info.called_with(message))
def has_permission(self, request, view): """ Check for permissions by matching the configured API key and header Allow the request if and only if settings.EDX_API_KEY is set and the X-Edx-Api-Key HTTP header is present in the request and matches the setting. """ api_key = getattr(settings, "EDX_API_KEY", None) if api_key is not None and request.META.get("HTTP_X_EDX_API_KEY") == api_key: audit_log("ApiKeyHeaderPermission used", path=request.path, ip=request.META.get("REMOTE_ADDR")) return True return False
def has_permission(self, request, view): """ Check for permissions by matching the configured API key and header Allow the request if and only if settings.EDX_API_KEY is set and the X-Edx-Api-Key HTTP header is present in the request and matches the setting. """ api_key = getattr(settings, "EDX_API_KEY", None) if api_key is not None and request.META.get( "HTTP_X_EDX_API_KEY") == api_key: audit_log("ApiKeyHeaderPermission used", path=request.path, ip=request.META.get("REMOTE_ADDR")) return True return False
def post(self, request): """Enrolls the currently logged-in user in a course. Server-to-server calls may deactivate or modify the mode of existing enrollments. All other requests go through `add_enrollment()`, which allows creation of new and reactivation of old enrollments. """ # Get the User, Course ID, and Mode from the request. username = request.data.get('user', request.user.username) course_id = request.data.get('course_details', {}).get('course_id') if not course_id: return Response( status=status.HTTP_400_BAD_REQUEST, data={"message": u"Course ID must be specified to create a new enrollment."} ) try: course_id = CourseKey.from_string(course_id) except InvalidKeyError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format(course_id=course_id) } ) mode = request.data.get('mode') has_api_key_permissions = self.has_api_key_permissions(request) # Check that the user specified is either the same user, or this is a server-to-server request. if not username: username = request.user.username if username != request.user.username and not has_api_key_permissions: # Return a 404 instead of a 403 (Unauthorized). If one user is looking up # other users, do not let them deduce the existence of an enrollment. return Response(status=status.HTTP_404_NOT_FOUND) 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: return Response( status=status.HTTP_406_NOT_ACCEPTABLE, data={ 'message': u'The user {} does not exist.'.format(username) } ) can_vip_enroll = False if settings.FEATURES.get('ENABLE_MEMBERSHIP_INTEGRATION'): from membership.models import VIPCourseEnrollment can_vip_enroll = VIPCourseEnrollment.can_vip_enroll(user, course_id) is_ecommerce_request = mode not in (CourseMode.AUDIT, CourseMode.HONOR, None) if is_ecommerce_request and not has_api_key_permissions and not can_vip_enroll: return Response( status=status.HTTP_403_FORBIDDEN, data={ "message": u"User does not have permission to create enrollment with mode [{mode}].".format( mode=mode ) } ) embargo_response = embargo_api.get_embargo_response(request, course_id, user) if embargo_response: return embargo_response try: is_active = request.data.get('is_active') # Check if the requested activation status is None or a Boolean if is_active is not None and not isinstance(is_active, bool): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': (u"'{value}' is an invalid enrollment activation status.").format(value=is_active) } ) explicit_linked_enterprise = request.data.get('linked_enterprise_customer') if explicit_linked_enterprise and has_api_key_permissions and enterprise_enabled(): enterprise_api_client = EnterpriseApiServiceClient() consent_client = ConsentApiServiceClient() try: enterprise_api_client.post_enterprise_course_enrollment(username, unicode(course_id), None) except EnterpriseApiException as error: log.exception("An unexpected error occurred while creating the new EnterpriseCourseEnrollment " "for user [%s] in course run [%s]", username, course_id) raise CourseEnrollmentError(text_type(error)) kwargs = { 'username': username, 'course_id': unicode(course_id), 'enterprise_customer_uuid': explicit_linked_enterprise, } consent_client.provide_consent(**kwargs) enrollment_attributes = request.data.get('enrollment_attributes') enrollment = api.get_enrollment(username, unicode(course_id)) mode_changed = enrollment and mode is not None and enrollment['mode'] != mode active_changed = enrollment and is_active is not None and enrollment['is_active'] != is_active missing_attrs = [] audit_with_order = False if enrollment_attributes: actual_attrs = [ u"{namespace}:{name}".format(**attr) for attr in enrollment_attributes ] missing_attrs = set(REQUIRED_ATTRIBUTES.get(mode, [])) - set(actual_attrs) audit_with_order = mode == 'audit' and 'order:order_number' in actual_attrs # Remove audit_with_order when no longer needed - implemented for REV-141 if has_api_key_permissions and (mode_changed or active_changed or audit_with_order): if mode_changed and active_changed and not is_active: # if the requester wanted to deactivate but specified the wrong mode, fail # the request (on the assumption that the requester had outdated information # about the currently active enrollment). msg = u"Enrollment mode mismatch: active mode={}, requested mode={}. Won't deactivate.".format( enrollment["mode"], mode ) log.warning(msg) return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg}) if len(missing_attrs) > 0: msg = u"Missing enrollment attributes: requested mode={} required attributes={}".format( mode, REQUIRED_ATTRIBUTES.get(mode) ) log.warning(msg) return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg}) response = api.update_enrollment( username, unicode(course_id), mode=mode, is_active=is_active, enrollment_attributes=enrollment_attributes, # If we are updating enrollment by authorized api caller, we should allow expired modes include_expired=has_api_key_permissions ) else: # Will reactivate inactive enrollments. response = api.add_enrollment( username, unicode(course_id), mode=mode, is_active=is_active, enrollment_attributes=enrollment_attributes, user=user, is_ecommerce_request=is_ecommerce_request ) cohort_name = request.data.get('cohort') if cohort_name is not None: cohort = get_cohort_by_name(course_id, cohort_name) add_user_to_cohort(cohort, user) email_opt_in = request.data.get('email_opt_in', None) if email_opt_in is not None: org = course_id.org update_email_opt_in(request.user, org, email_opt_in) log.info('The user [%s] has already been enrolled in course run [%s].', username, course_id) return Response(response) except CourseModeNotFoundError as error: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": ( u"The [{mode}] course mode is expired or otherwise unavailable for course run [{course_id}]." ).format(mode=mode, course_id=course_id), "course_details": error.data }) except CourseNotFoundError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format(course_id=course_id) } ) except CourseEnrollmentExistsError as error: log.warning('An enrollment already exists for user [%s] in course run [%s].', username, course_id) return Response(data=error.enrollment) except CourseEnrollmentError: log.exception("An error occurred while creating the new course enrollment for user " "[%s] in course run [%s]", username, course_id) return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": ( u"An error occurred while creating the new course enrollment for user " u"'{username}' in course '{course_id}'" ).format(username=username, course_id=course_id) } ) except CourseUserGroup.DoesNotExist: log.exception('Missing cohort [%s] in course run [%s]', cohort_name, course_id) return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": "An error occured while adding to cohort [%s]" % cohort_name }) finally: # Assumes that the ecommerce service uses an API key to authenticate. if has_api_key_permissions: current_enrollment = api.get_enrollment(username, unicode(course_id)) audit_log( 'enrollment_change_requested', course_id=unicode(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 post(self, request): """Enrolls the currently logged-in user in a course. Server-to-server calls may deactivate or modify the mode of existing enrollments. All other requests go through `add_enrollment()`, which allows creation of new and reactivation of old enrollments. """ # Get the User, Course ID, and Mode from the request. username = request.data.get('user', request.user.username) course_id = request.data.get('course_details', {}).get('course_id') if not course_id: return Response( status=status.HTTP_400_BAD_REQUEST, data={"message": u"Course ID must be specified to create a new enrollment."} ) try: course_id = CourseKey.from_string(course_id) except InvalidKeyError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format(course_id=course_id) } ) mode = request.data.get('mode') has_api_key_permissions = self.has_api_key_permissions(request) # Check that the user specified is either the same user, or this is a server-to-server request. if not username: username = request.user.username if username != request.user.username and not has_api_key_permissions: # Return a 404 instead of a 403 (Unauthorized). If one user is looking up # other users, do not let them deduce the existence of an enrollment. return Response(status=status.HTTP_404_NOT_FOUND) if mode not in (CourseMode.AUDIT, CourseMode.HONOR, None) and not has_api_key_permissions: return Response( status=status.HTTP_403_FORBIDDEN, data={ "message": u"User does not have permission to create enrollment with mode [{mode}].".format( mode=mode ) } ) 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: return Response( status=status.HTTP_406_NOT_ACCEPTABLE, data={ 'message': u'The user {} does not exist.'.format(username) } ) embargo_response = embargo_api.get_embargo_response(request, course_id, user) if embargo_response: return embargo_response try: is_active = request.data.get('is_active') # Check if the requested activation status is None or a Boolean if is_active is not None and not isinstance(is_active, bool): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': (u"'{value}' is an invalid enrollment activation status.").format(value=is_active) } ) enrollment_attributes = request.data.get('enrollment_attributes') enrollment = api.get_enrollment(username, unicode(course_id)) mode_changed = enrollment and mode is not None and enrollment['mode'] != mode active_changed = enrollment and is_active is not None and enrollment['is_active'] != is_active missing_attrs = [] if enrollment_attributes: actual_attrs = [ u"{namespace}:{name}".format(**attr) for attr in enrollment_attributes ] missing_attrs = set(REQUIRED_ATTRIBUTES.get(mode, [])) - set(actual_attrs) if has_api_key_permissions and (mode_changed or active_changed): if mode_changed and active_changed and not is_active: # if the requester wanted to deactivate but specified the wrong mode, fail # the request (on the assumption that the requester had outdated information # about the currently active enrollment). msg = u"Enrollment mode mismatch: active mode={}, requested mode={}. Won't deactivate.".format( enrollment["mode"], mode ) log.warning(msg) return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg}) if len(missing_attrs) > 0: msg = u"Missing enrollment attributes: requested mode={} required attributes={}".format( mode, REQUIRED_ATTRIBUTES.get(mode) ) log.warning(msg) return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg}) response = api.update_enrollment( username, unicode(course_id), mode=mode, is_active=is_active, enrollment_attributes=enrollment_attributes ) else: # Will reactivate inactive enrollments. response = api.add_enrollment(username, unicode(course_id), mode=mode, is_active=is_active) email_opt_in = request.data.get('email_opt_in', None) if email_opt_in is not None: org = course_id.org update_email_opt_in(request.user, org, email_opt_in) log.info('The user [%s] has already been enrolled in course run [%s].', username, course_id) return Response(response) except CourseModeNotFoundError as error: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": ( u"The [{mode}] course mode is expired or otherwise unavailable for course run [{course_id}]." ).format(mode=mode, course_id=course_id), "course_details": error.data }) except CourseNotFoundError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format(course_id=course_id) } ) except CourseEnrollmentExistsError as error: log.warning('An enrollment already exists for user [%s] in course run [%s].', username, course_id) return Response(data=error.enrollment) except CourseEnrollmentError: log.exception("An error occurred while creating the new course enrollment for user " "[%s] in course run [%s]", username, course_id) return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": ( u"An error occurred while creating the new course enrollment for user " u"'{username}' in course '{course_id}'" ).format(username=username, course_id=course_id) } ) finally: # Assumes that the ecommerce service uses an API key to authenticate. if has_api_key_permissions: current_enrollment = api.get_enrollment(username, unicode(course_id)) audit_log( 'enrollment_change_requested', course_id=unicode(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 post(self, request, *args, **kwargs): """ Attempt to create the basket and enroll the user. """ user = request.user valid, course_key, error = self._is_data_valid(request) if not valid: return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE) embargo_response = embargo_api.get_embargo_response( request, course_key, user) if embargo_response: return embargo_response # Don't do anything if an enrollment already exists course_id = unicode(course_key) enrollment = CourseEnrollment.get_enrollment(user, course_key) if enrollment and enrollment.is_active: msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username) return DetailResponse(msg, status=HTTP_409_CONFLICT) # Check to see if enrollment for this course is closed. course = courses.get_course(course_key) if CourseEnrollment.is_enrollment_closed(user, course): msg = Messages.ENROLLMENT_CLOSED.format(course_id=course_id) log.info(u'Unable to enroll user %s in closed course %s.', user.id, course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE) # If there is no audit or honor course mode, this most likely # a Prof-Ed course. Return an error so that the JS redirects # to track selection. honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR) audit_mode = CourseMode.mode_for_course(course_key, CourseMode.AUDIT) # Accept either honor or audit as an enrollment mode to # maintain backwards compatibility with existing courses default_enrollment_mode = audit_mode or honor_mode if not default_enrollment_mode: msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format( course_id=course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE) elif default_enrollment_mode and not default_enrollment_mode.sku: # If there are no course modes with SKUs, enroll the user without contacting the external API. msg = Messages.NO_SKU_ENROLLED.format( enrollment_mode=default_enrollment_mode.slug, course_id=course_id, username=user.username) log.info(msg) self._enroll(course_key, user, default_enrollment_mode.slug) self._handle_marketing_opt_in(request, course_key, user) return DetailResponse(msg) # Setup the API try: api_session = requests.Session() api = ecommerce_api_client(user, session=api_session) except ValueError: self._enroll(course_key, user) msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key)) log.debug(msg) return DetailResponse(msg) response = None # Make the API call try: # Pass along Sailthru campaign id self._add_request_cookie_to_api_session(api_session, request, SAILTHRU_CAMPAIGN_COOKIE) # Pass along UTM tracking info utm_cookie_name = RegistrationCookieConfiguration.current( ).utm_cookie_name self._add_request_cookie_to_api_session(api_session, request, utm_cookie_name) response_data = api.baskets.post({ 'products': [{ 'sku': default_enrollment_mode.sku }], 'checkout': True, }) payment_data = response_data["payment_data"] if payment_data: # Pass data to the client to begin the payment flow. response = JsonResponse(payment_data) elif response_data['order']: # The order was completed immediately because there is no charge. msg = Messages.ORDER_COMPLETED.format( order_number=response_data['order']['number']) log.debug(msg) response = DetailResponse(msg) else: msg = u'Unexpected response from basket endpoint.' log.error( msg + u' Could not enroll user %(username)s in course %(course_id)s.', { 'username': user.id, 'course_id': course_id }, ) raise InvalidResponseError(msg) except (exceptions.SlumberBaseException, exceptions.Timeout) as ex: log.exception(ex.message) return InternalRequestErrorResponse(ex.message) finally: audit_log('checkout_requested', course_id=course_id, mode=default_enrollment_mode.slug, processor_name=None, user_id=user.id) self._handle_marketing_opt_in(request, course_key, user) return response
def post(self, request): """Enrolls the currently logged-in user in a course. Server-to-server calls may deactivate or modify the mode of existing enrollments. All other requests go through `add_enrollment()`, which allows creation of new and reactivation of old enrollments. """ # Get the User, Course ID, and Mode from the request. username = request.data.get('user', request.user.username) course_id = request.data.get('course_details', {}).get('course_id') if not course_id: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"Course ID must be specified to create a new enrollment." }) try: course_id = CourseKey.from_string(course_id) except InvalidKeyError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format( course_id=course_id) }) mode = request.data.get('mode') has_api_key_permissions = self.has_api_key_permissions(request) # Check that the user specified is either the same user, or this is a server-to-server request. if not username: username = request.user.username if username != request.user.username and not has_api_key_permissions: # Return a 404 instead of a 403 (Unauthorized). If one user is looking up # other users, do not let them deduce the existence of an enrollment. return Response(status=status.HTTP_404_NOT_FOUND) if mode not in (CourseMode.AUDIT, CourseMode.HONOR, None) and not has_api_key_permissions: return Response( status=status.HTTP_403_FORBIDDEN, data={ "message": u"User does not have permission to create enrollment with mode [{mode}]." .format(mode=mode) }) 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: return Response(status=status.HTTP_406_NOT_ACCEPTABLE, data={ 'message': u'The user {} does not exist.'.format(username) }) embargo_response = embargo_api.get_embargo_response( request, course_id, user) if embargo_response: return embargo_response try: is_active = request.data.get('is_active') # Check if the requested activation status is None or a Boolean if is_active is not None and not isinstance(is_active, bool): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': (u"'{value}' is an invalid enrollment activation status." ).format(value=is_active) }) enrollment_attributes = request.data.get('enrollment_attributes') enrollment = api.get_enrollment(username, unicode(course_id)) mode_changed = enrollment and mode is not None and enrollment[ 'mode'] != mode active_changed = enrollment and is_active is not None and enrollment[ 'is_active'] != is_active missing_attrs = [] if enrollment_attributes: actual_attrs = [ u"{namespace}:{name}".format(**attr) for attr in enrollment_attributes ] missing_attrs = set(REQUIRED_ATTRIBUTES.get( mode, [])) - set(actual_attrs) if has_api_key_permissions and (mode_changed or active_changed): if mode_changed and active_changed and not is_active: # if the requester wanted to deactivate but specified the wrong mode, fail # the request (on the assumption that the requester had outdated information # about the currently active enrollment). msg = u"Enrollment mode mismatch: active mode={}, requested mode={}. Won't deactivate.".format( enrollment["mode"], mode) log.warning(msg) return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg}) if len(missing_attrs) > 0: msg = u"Missing enrollment attributes: requested mode={} required attributes={}".format( mode, REQUIRED_ATTRIBUTES.get(mode)) log.warning(msg) return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg}) response = api.update_enrollment( username, unicode(course_id), mode=mode, is_active=is_active, enrollment_attributes=enrollment_attributes) else: # Will reactivate inactive enrollments. response = api.add_enrollment(username, unicode(course_id), mode=mode, is_active=is_active) email_opt_in = request.data.get('email_opt_in', None) if email_opt_in is not None: org = course_id.org update_email_opt_in(request.user, org, email_opt_in) log.info( 'The user [%s] has already been enrolled in course run [%s].', username, course_id) return Response(response) except CourseModeNotFoundError as error: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": (u"The [{mode}] course mode is expired or otherwise unavailable for course run [{course_id}]." ).format(mode=mode, course_id=course_id), "course_details": error.data }) except CourseNotFoundError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format( course_id=course_id) }) except CourseEnrollmentExistsError as error: log.warning( 'An enrollment already exists for user [%s] in course run [%s].', username, course_id) return Response(data=error.enrollment) except CourseEnrollmentError: log.exception( "An error occurred while creating the new course enrollment for user " "[%s] in course run [%s]", username, course_id) return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": (u"An error occurred while creating the new course enrollment for user " u"'{username}' in course '{course_id}'").format( username=username, course_id=course_id) }) finally: # Assumes that the ecommerce service uses an API key to authenticate. if has_api_key_permissions: current_enrollment = api.get_enrollment( username, unicode(course_id)) audit_log('enrollment_change_requested', course_id=unicode(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 post(self, request, *args, **kwargs): """ Attempt to create the basket and enroll the user. """ user = request.user valid, course_key, error = self._is_data_valid(request) if not valid: return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE) embargo_response = embargo_api.get_embargo_response(request, course_key, user) if embargo_response: return embargo_response # Don't do anything if an enrollment already exists course_id = unicode(course_key) enrollment = CourseEnrollment.get_enrollment(user, course_key) if enrollment and enrollment.is_active: msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username) return DetailResponse(msg, status=HTTP_409_CONFLICT) # Check to see if enrollment for this course is closed. course = courses.get_course(course_key) if CourseEnrollment.is_enrollment_closed(user, course): msg = Messages.ENROLLMENT_CLOSED.format(course_id=course_id) log.info(u'Unable to enroll user %s in closed course %s.', user.id, course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE) # If there is no audit or honor course mode, this most likely # a Prof-Ed course. Return an error so that the JS redirects # to track selection. honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR) audit_mode = CourseMode.mode_for_course(course_key, CourseMode.AUDIT) # Accept either honor or audit as an enrollment mode to # maintain backwards compatibility with existing courses default_enrollment_mode = audit_mode or honor_mode if not default_enrollment_mode: msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format(course_id=course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE) elif default_enrollment_mode and not default_enrollment_mode.sku: # If there are no course modes with SKUs, enroll the user without contacting the external API. msg = Messages.NO_SKU_ENROLLED.format( enrollment_mode=default_enrollment_mode.slug, course_id=course_id, username=user.username ) log.info(msg) self._enroll(course_key, user, default_enrollment_mode.slug) notify_enrollment_by_email(courses.get_course(course_key), user, request) self._handle_marketing_opt_in(request, course_key, user) return DetailResponse(msg) # Setup the API try: api_session = requests.Session() api = ecommerce_api_client(user, session=api_session) except ValueError: self._enroll(course_key, user) notify_enrollment_by_email(courses.get_course(course_key), user, request) msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key)) log.debug(msg) return DetailResponse(msg) response = None # Make the API call try: # Pass along Sailthru campaign id self._add_request_cookie_to_api_session(api_session, request, SAILTHRU_CAMPAIGN_COOKIE) # Pass along UTM tracking info utm_cookie_name = RegistrationCookieConfiguration.current().utm_cookie_name self._add_request_cookie_to_api_session(api_session, request, utm_cookie_name) response_data = api.baskets.post({ 'products': [{'sku': default_enrollment_mode.sku}], 'checkout': True, }) payment_data = response_data["payment_data"] if payment_data: # Pass data to the client to begin the payment flow. response = JsonResponse(payment_data) elif response_data['order']: # The order was completed immediately because there is no charge. msg = Messages.ORDER_COMPLETED.format(order_number=response_data['order']['number']) log.debug(msg) response = DetailResponse(msg) else: msg = u'Unexpected response from basket endpoint.' log.error( msg + u' Could not enroll user %(username)s in course %(course_id)s.', {'username': user.id, 'course_id': course_id}, ) raise InvalidResponseError(msg) except (exceptions.SlumberBaseException, exceptions.Timeout) as ex: log.exception(ex.message) return InternalRequestErrorResponse(ex.message) finally: audit_log( 'checkout_requested', course_id=course_id, mode=default_enrollment_mode.slug, processor_name=None, user_id=user.id ) self._handle_marketing_opt_in(request, course_key, user) return response
def _create_basket_to_order(self, request, user, course_key, default_enrollment_mode): """ Connect to the ecommerce service to create the basket and the order to do the enrollment """ # Setup the API course_id = unicode(course_key) try: api_session = requests.Session() api = ecommerce_api_client(user, session=api_session) except ValueError: self._enroll(course_key, user) msg = Messages.NO_ECOM_API.format(username=user.username, course_id=course_id) log.debug(msg) return DetailResponse(msg) response = None # Make the API call try: # Pass along Sailthru campaign id self._add_request_cookie_to_api_session(api_session, request, SAILTHRU_CAMPAIGN_COOKIE) # Pass along UTM tracking info utm_cookie_name = RegistrationCookieConfiguration.current( ).utm_cookie_name self._add_request_cookie_to_api_session(api_session, request, utm_cookie_name) response_data = api.baskets.post({ 'products': [{ 'sku': default_enrollment_mode.sku }], 'checkout': True, }) payment_data = response_data["payment_data"] if payment_data: # Pass data to the client to begin the payment flow. response = JsonResponse(payment_data) elif response_data['order']: # The order was completed immediately because there is no charge. msg = Messages.ORDER_COMPLETED.format( order_number=response_data['order']['number']) log.debug(msg) response = DetailResponse(msg) else: msg = u'Unexpected response from basket endpoint.' log.error( msg + u' Could not enroll user %(username)s in course %(course_id)s.', { 'username': user.id, 'course_id': course_id }, ) raise InvalidResponseError(msg) except (exceptions.SlumberBaseException, exceptions.Timeout) as ex: log.exception(ex.message) return InternalRequestErrorResponse(ex.message) finally: audit_log('checkout_requested', course_id=course_id, mode=default_enrollment_mode.slug, processor_name=None, user_id=user.id) self._handle_marketing_opt_in(request, course_key, user) return response
def lms_enroll_user_in_course( username, course_id, mode, enterprise_uuid, 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 - 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, ) log.info('The user [%s] has been enrolled in course run [%s].', username, course_id) return response except CourseEnrollmentExistsError as error: # pylint: disable=unused-variable 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 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)