def test_check_for_existing_entitlement_and_enroll(self, mock_get_course_uuid): course = CourseFactory() CourseModeFactory( course_id=course.id, mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1) ) entitlement = CourseEntitlementFactory.create( mode=CourseMode.VERIFIED, user=self.user, ) mock_get_course_uuid.return_value = entitlement.course_uuid assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) CourseEntitlement.check_for_existing_entitlement_and_enroll( user=self.user, course_run_key=course.id, ) assert CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) entitlement.refresh_from_db() assert entitlement.enrollment_course_run
def test_check_for_no_entitlement_and_do_not_enroll( self, mock_get_course_uuid): course = CourseFactory() CourseModeFactory( course_id=course.id, mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1)) entitlement = CourseEntitlementFactory.create( mode=CourseMode.VERIFIED, user=self.user, ) mock_get_course_uuid.return_value = None assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) CourseEntitlement.check_for_existing_entitlement_and_enroll( user=self.user, course_run_key=course.id, ) assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) entitlement.refresh_from_db() assert entitlement.enrollment_course_run is None new_course = CourseFactory() CourseModeFactory( course_id=new_course.id, mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1)) # Return invalid uuid so that no entitlement returned for this new course mock_get_course_uuid.return_value = uuid4().hex try: CourseEntitlement.check_for_existing_entitlement_and_enroll( user=self.user, course_run_key=new_course.id, ) assert not CourseEnrollment.is_enrolled(user=self.user, course_key=new_course.id) except AttributeError as error: self.fail(error.message)
def test_check_for_no_entitlement_and_do_not_enroll(self, mock_get_course_uuid): course = CourseFactory() CourseModeFactory( course_id=course.id, mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1) ) entitlement = CourseEntitlementFactory.create( mode=CourseMode.VERIFIED, user=self.user, ) mock_get_course_uuid.return_value = None assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) CourseEntitlement.check_for_existing_entitlement_and_enroll( user=self.user, course_run_key=course.id, ) assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) entitlement.refresh_from_db() assert entitlement.enrollment_course_run is None new_course = CourseFactory() CourseModeFactory( course_id=new_course.id, mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1) ) # Return invalid uuid so that no entitlement returned for this new course mock_get_course_uuid.return_value = uuid4().hex try: CourseEntitlement.check_for_existing_entitlement_and_enroll( user=self.user, course_run_key=new_course.id, ) assert not CourseEnrollment.is_enrolled(user=self.user, course_key=new_course.id) except AttributeError as error: self.fail(error.message)
def change_enrollment(request, check_access=True): """ Modify the enrollment status for the logged-in user. TODO: This is lms specific and does not belong in common code. The request parameter must be a POST request (other methods return 405) that specifies course_id and enrollment_action parameters. If course_id or enrollment_action is not specified, if course_id is not valid, if enrollment_action is something other than "enroll" or "unenroll", if enrollment_action is "enroll" and enrollment is closed for the course, or if enrollment_action is "unenroll" and the user is not enrolled in the course, a 400 error will be returned. If the user is not logged in, 403 will be returned; it is important that only this case return 403 so the front end can redirect the user to a registration or login page when this happens. This function should only be called from an AJAX request, so the error messages in the responses should never actually be user-visible. Args: request (`Request`): The Django request object Keyword Args: check_access (boolean): If True, we check that an accessible course actually exists for the given course_key before we enroll the student. The default is set to False to avoid breaking legacy code or code with non-standard flows (ex. beta tester invitations), but for any standard enrollment flow you probably want this to be True. Returns: Response """ # Get the user user = request.user # Ensure the user is authenticated if not user.is_authenticated: return HttpResponseForbidden() # Ensure we received a course_id action = request.POST.get("enrollment_action") if 'course_id' not in request.POST: return HttpResponseBadRequest(_("Course id not specified")) try: course_id = CourseKey.from_string(request.POST.get("course_id")) except InvalidKeyError: log.warning( u"User %s tried to %s with invalid course id: %s", user.username, action, request.POST.get("course_id"), ) return HttpResponseBadRequest(_("Invalid course id")) # Allow us to monitor performance of this transaction on a per-course basis since we often roll-out features # on a per-course basis. monitoring_utils.set_custom_metric('course_id', text_type(course_id)) if action == "enroll": # Make sure the course exists # We don't do this check on unenroll, or a bad course id can't be unenrolled from if not modulestore().has_course(course_id): log.warning(u"User %s tried to enroll in non-existent course %s", user.username, course_id) return HttpResponseBadRequest(_("Course id is invalid")) # Record the user's email opt-in preference if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'): _update_email_opt_in(request, course_id.org) available_modes = CourseMode.modes_for_course_dict(course_id) # Check whether the user is blocked from enrolling in this course # This can occur if the user's IP is on a global blacklist # or if the user is enrolling in a country in which the course # is not available. redirect_url = embargo_api.redirect_if_blocked( course_id, user=user, ip_address=get_ip(request), url=request.path) if redirect_url: return HttpResponse(redirect_url) if CourseEntitlement.check_for_existing_entitlement_and_enroll( user=user, course_run_key=course_id): return HttpResponse( reverse('courseware', args=[six.text_type(course_id)])) # Check that auto enrollment is allowed for this course # (= the course is NOT behind a paywall) if CourseMode.can_auto_enroll(course_id): # Enroll the user using the default mode (audit) # We're assuming that users of the course enrollment table # will NOT try to look up the course enrollment model # by its slug. If they do, it's possible (based on the state of the database) # for no such model to exist, even though we've set the enrollment type # to "audit". try: enroll_mode = CourseMode.auto_enroll_mode( course_id, available_modes) if enroll_mode: CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode) except Exception: # pylint: disable=broad-except return HttpResponseBadRequest(_("Could not enroll")) # If we have more than one course mode or professional ed is enabled, # then send the user to the choose your track page. # (In the case of no-id-professional/professional ed, this will redirect to a page that # funnels users directly into the verification / payment flow) if CourseMode.has_verified_mode( available_modes) or CourseMode.has_professional_mode( available_modes): return HttpResponse( reverse("course_modes_choose", kwargs={'course_id': text_type(course_id)})) # Otherwise, there is only one mode available (the default) return HttpResponse() elif action == "unenroll": enrollment = CourseEnrollment.get_enrollment(user, course_id) if not enrollment: return HttpResponseBadRequest( _("You are not enrolled in this course")) certificate_info = cert_info(user, enrollment.course_overview) if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES: return HttpResponseBadRequest( _("Your certificate prevents you from unenrolling from this course" )) CourseEnrollment.unenroll(user, course_id) REFUND_ORDER.send(sender=None, course_enrollment=enrollment) return HttpResponse() else: return HttpResponseBadRequest(_("Enrollment action is invalid"))
def post(self, request, *args, **kwargs): """ Attempt to 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) # Check to see if the User has an entitlement and enroll them if they have one for this course if CourseEntitlement.check_for_existing_entitlement_and_enroll( user=user, course_run_key=course_key): return JsonResponse( { 'redirect_destination': reverse('courseware', args=[unicode(course_id)]), }, ) # 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 default_enrollment_mode: msg = Messages.ENROLL_DIRECTLY.format(username=user.username, course_id=course_id) if not default_enrollment_mode.sku: # If there are no course modes with SKUs, return a different message. 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) mode = CourseMode.AUDIT if audit_mode else CourseMode.HONOR SAILTHRU_AUDIT_PURCHASE.send(sender=None, user=user, mode=mode, course_id=course_id) self._handle_marketing_opt_in(request, course_key, user) return DetailResponse(msg) else: msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format( course_id=course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
def post(self, request, *args, **kwargs): """ Attempt to 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) # Check to see if the User has an entitlement and enroll them if they have one for this course if CourseEntitlement.check_for_existing_entitlement_and_enroll(user=user, course_run_key=course_key): return JsonResponse( { 'redirect_destination': reverse('courseware', args=[unicode(course_id)]), }, ) # 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 default_enrollment_mode: msg = Messages.ENROLL_DIRECTLY.format( username=user.username, course_id=course_id ) if not default_enrollment_mode.sku: # If there are no course modes with SKUs, return a different message. 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) mode = CourseMode.AUDIT if audit_mode else CourseMode.HONOR SAILTHRU_AUDIT_PURCHASE.send( sender=None, user=user, mode=mode, course_id=course_id ) self._handle_marketing_opt_in(request, course_key, user) return DetailResponse(msg) else: msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format(course_id=course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
def change_enrollment(request, check_access=True): """ Modify the enrollment status for the logged-in user. TODO: This is lms specific and does not belong in common code. The request parameter must be a POST request (other methods return 405) that specifies course_id and enrollment_action parameters. If course_id or enrollment_action is not specified, if course_id is not valid, if enrollment_action is something other than "enroll" or "unenroll", if enrollment_action is "enroll" and enrollment is closed for the course, or if enrollment_action is "unenroll" and the user is not enrolled in the course, a 400 error will be returned. If the user is not logged in, 403 will be returned; it is important that only this case return 403 so the front end can redirect the user to a registration or login page when this happens. This function should only be called from an AJAX request, so the error messages in the responses should never actually be user-visible. Args: request (`Request`): The Django request object Keyword Args: check_access (boolean): If True, we check that an accessible course actually exists for the given course_key before we enroll the student. The default is set to False to avoid breaking legacy code or code with non-standard flows (ex. beta tester invitations), but for any standard enrollment flow you probably want this to be True. Returns: Response """ # Get the user user = request.user # Ensure the user is authenticated if not user.is_authenticated: return HttpResponseForbidden() # Ensure we received a course_id action = request.POST.get("enrollment_action") if 'course_id' not in request.POST: return HttpResponseBadRequest(_("Course id not specified")) try: course_id = CourseKey.from_string(request.POST.get("course_id")) except InvalidKeyError: log.warning( u"User %s tried to %s with invalid course id: %s", user.username, action, request.POST.get("course_id"), ) return HttpResponseBadRequest(_("Invalid course id")) # Allow us to monitor performance of this transaction on a per-course basis since we often roll-out features # on a per-course basis. monitoring_utils.set_custom_metric('course_id', text_type(course_id)) if action == "enroll": # Make sure the course exists # We don't do this check on unenroll, or a bad course id can't be unenrolled from if not modulestore().has_course(course_id): log.warning( u"User %s tried to enroll in non-existent course %s", user.username, course_id ) return HttpResponseBadRequest(_("Course id is invalid")) # Record the user's email opt-in preference if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'): _update_email_opt_in(request, course_id.org) available_modes = CourseMode.modes_for_course_dict(course_id) # Check whether the user is blocked from enrolling in this course # This can occur if the user's IP is on a global blacklist # or if the user is enrolling in a country in which the course # is not available. redirect_url = embargo_api.redirect_if_blocked( course_id, user=user, ip_address=get_ip(request), url=request.path ) if redirect_url: return HttpResponse(redirect_url) if CourseEntitlement.check_for_existing_entitlement_and_enroll(user=user, course_run_key=course_id): return HttpResponse(reverse('courseware', args=[unicode(course_id)])) # Check that auto enrollment is allowed for this course # (= the course is NOT behind a paywall) if CourseMode.can_auto_enroll(course_id): # Enroll the user using the default mode (audit) # We're assuming that users of the course enrollment table # will NOT try to look up the course enrollment model # by its slug. If they do, it's possible (based on the state of the database) # for no such model to exist, even though we've set the enrollment type # to "audit". try: enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes) if enroll_mode: CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode) except Exception: # pylint: disable=broad-except return HttpResponseBadRequest(_("Could not enroll")) # If we have more than one course mode or professional ed is enabled, # then send the user to the choose your track page. # (In the case of no-id-professional/professional ed, this will redirect to a page that # funnels users directly into the verification / payment flow) if CourseMode.has_verified_mode(available_modes) or CourseMode.has_professional_mode(available_modes): return HttpResponse( reverse("course_modes_choose", kwargs={'course_id': text_type(course_id)}) ) # Otherwise, there is only one mode available (the default) return HttpResponse() elif action == "unenroll": enrollment = CourseEnrollment.get_enrollment(user, course_id) if not enrollment: return HttpResponseBadRequest(_("You are not enrolled in this course")) certificate_info = cert_info(user, enrollment.course_overview) if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES: return HttpResponseBadRequest(_("Your certificate prevents you from unenrolling from this course")) CourseEnrollment.unenroll(user, course_id) REFUND_ORDER.send(sender=None, course_enrollment=enrollment) return HttpResponse() else: return HttpResponseBadRequest(_("Enrollment action is invalid"))