Exemplo n.º 1
0
    def test_can_auto_enroll(self, modes_and_prices, can_auto_enroll):
        # Create the modes and min prices
        for mode_slug, min_price in modes_and_prices:
            self.create_mode(mode_slug, mode_slug.capitalize(), min_price=min_price)

        # Verify that we can or cannot auto enroll
        self.assertEqual(CourseMode.can_auto_enroll(self.course_key), can_auto_enroll)
Exemplo n.º 2
0
    def test_can_auto_enroll(self, modes_and_prices, can_auto_enroll):
        # Create the modes and min prices
        for mode_slug, min_price in modes_and_prices:
            self.create_mode(mode_slug, mode_slug.capitalize(), min_price=min_price)

        # Verify that we can or cannot auto enroll
        self.assertEqual(CourseMode.can_auto_enroll(self.course_key), can_auto_enroll)
Exemplo n.º 3
0
def refund_seat(course_enrollment, change_mode=False):
    """
    Attempt to initiate a refund for any orders associated with the seat being unenrolled,
    using the commerce service.

    Arguments:
        course_enrollment (CourseEnrollment): a student enrollment
        change_mode (Boolean): change the course mode to free mode or not

    Returns:
        A list of the external service's IDs for any refunds that were initiated
            (may be empty).

    Raises:
        exceptions.SlumberBaseException: for any unhandled HTTP error during communication with the E-Commerce Service.
        exceptions.Timeout: if the attempt to reach the commerce service timed out.
    """
    User = get_user_model()  # pylint:disable=invalid-name
    course_key_str = unicode(course_enrollment.course_id)
    enrollee = course_enrollment.user

    service_user = User.objects.get(
        username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    api_client = ecommerce_api_client(service_user)

    log.info('Attempting to create a refund for user [%s], course [%s]...',
             enrollee.id, course_key_str)

    refund_ids = api_client.refunds.post({
        'course_id': course_key_str,
        'username': enrollee.username
    })

    if refund_ids:
        log.info('Refund successfully opened for user [%s], course [%s]: %r',
                 enrollee.id, course_key_str, refund_ids)

        _process_refund(
            refund_ids=refund_ids,
            api_client=api_client,
            mode=course_enrollment.mode,
            user=enrollee,
        )
        if change_mode and CourseMode.can_auto_enroll(
                course_id=CourseKey.from_string(course_key_str)):
            course_enrollment.update_enrollment(
                mode=CourseMode.auto_enroll_mode(course_id=course_key_str),
                is_active=False,
                skip_refund=True)
            course_enrollment.save()
    else:
        log.info('No refund opened for user [%s], course [%s]', enrollee.id,
                 course_key_str)

    return refund_ids
Exemplo n.º 4
0
def change_enrollment(strategy, user=None, *args, **kwargs):
    """Enroll a user in a course.

    If a user entered the authentication flow when trying to enroll
    in a course, then attempt to enroll the user.
    We will try to do this if the pipeline was started with the
    querystring param `enroll_course_id`.

    In the following cases, we can't enroll the user:
        * The course does not have an honor mode.
        * The course has an honor mode with a minimum price.
        * The course is not yet open for enrollment.
        * The course does not exist.

    If we can't enroll the user now, then skip this step.
    For paid courses, users will be redirected to the payment flow
    upon completion of the authentication pipeline
    (configured using the ?next parameter to the third party auth login url).

    """
    enroll_course_id = strategy.session_get('enroll_course_id')
    if enroll_course_id:
        course_id = CourseKey.from_string(enroll_course_id)
        modes = CourseMode.modes_for_course_dict(course_id)
        # If the email opt in parameter is found, set the preference.
        email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
        if email_opt_in:
            opt_in = email_opt_in.lower() == 'true'
            profile.update_email_opt_in(user.username, course_id.org, opt_in)
        if CourseMode.can_auto_enroll(course_id, modes_dict=modes):
            try:
                CourseEnrollment.enroll(user, course_id, check_access=True)
            except CourseEnrollmentException:
                pass
            except Exception as ex:
                logger.exception(ex)

        # Handle white-label courses as a special case
        # If a course is white-label, we should add it to the shopping cart.
        elif CourseMode.is_white_label(course_id, modes_dict=modes):
            try:
                cart = Order.get_cart_for_user(user)
                PaidCourseRegistration.add_to_order(cart, course_id)
            except (
                CourseDoesNotExistException,
                ItemAlreadyInCartException,
                AlreadyEnrolledInCourseException
            ):
                pass
            # It's more important to complete login than to
            # ensure that the course was added to the shopping cart.
            # Log errors, but don't stop the authentication pipeline.
            except Exception as ex:
                logger.exception(ex)
Exemplo n.º 5
0
def enroll(user, course_id, request, check_access):
    # 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)
        raise InvalidCourseIdError()

    # 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 redirect_url

    # 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 (honor)
        # 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 "honor".

        # Exception is to be handled by the caller
        CourseEnrollment.enroll(user, course_id, check_access=check_access)

    # 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):
        reverse("course_modes_choose",
                kwargs={'course_id': unicode(course_id)})

    # Otherwise, there is only one mode available (the default)
    # Success
    return None
Exemplo n.º 6
0
def change_enrollment(strategy, user=None, *args, **kwargs):
    """Enroll a user in a course.

    If a user entered the authentication flow when trying to enroll
    in a course, then attempt to enroll the user.
    We will try to do this if the pipeline was started with the
    querystring param `enroll_course_id`.

    In the following cases, we can't enroll the user:
        * The course does not have an honor mode.
        * The course has an honor mode with a minimum price.
        * The course is not yet open for enrollment.
        * The course does not exist.

    If we can't enroll the user now, then skip this step.
    For paid courses, users will be redirected to the payment flow
    upon completion of the authentication pipeline
    (configured using the ?next parameter to the third party auth login url).

    """
    enroll_course_id = strategy.session_get('enroll_course_id')
    if enroll_course_id:
        course_id = CourseKey.from_string(enroll_course_id)
        modes = CourseMode.modes_for_course_dict(course_id)
        # If the email opt in parameter is found, set the preference.
        email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
        if email_opt_in:
            opt_in = email_opt_in.lower() == 'true'
            profile.update_email_opt_in(user.username, course_id.org, opt_in)
        if CourseMode.can_auto_enroll(course_id, modes_dict=modes):
            try:
                CourseEnrollment.enroll(user, course_id, check_access=True)
            except CourseEnrollmentException:
                pass
            except Exception as ex:
                logger.exception(ex)

        # Handle white-label courses as a special case
        # If a course is white-label, we should add it to the shopping cart.
        elif CourseMode.is_white_label(course_id, modes_dict=modes):
            try:
                cart = Order.get_cart_for_user(user)
                PaidCourseRegistration.add_to_order(cart, course_id)
            except (CourseDoesNotExistException, ItemAlreadyInCartException,
                    AlreadyEnrolledInCourseException):
                pass
            # It's more important to complete login than to
            # ensure that the course was added to the shopping cart.
            # Log errors, but don't stop the authentication pipeline.
            except Exception as ex:
                logger.exception(ex)
Exemplo n.º 7
0
def enroll(user, course_id, request, check_access):
    # 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
        )
        raise InvalidCourseIdError()

    # 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 redirect_url

    # 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 (honor)
        # 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 "honor".

        # Exception is to be handled by the caller
        CourseEnrollment.enroll(user, course_id, check_access=check_access)

    # 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):
        reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)})

    # Otherwise, there is only one mode available (the default)
    # Success
    return None
Exemplo n.º 8
0
def refund_seat(course_enrollment, change_mode=False):
    """
    Attempt to initiate a refund for any orders associated with the seat being unenrolled,
    using the commerce service.

    Arguments:
        course_enrollment (CourseEnrollment): a student enrollment
        change_mode (Boolean): change the course mode to free mode or not

    Returns:
        A list of the external service's IDs for any refunds that were initiated
            (may be empty).

    Raises:
        exceptions.SlumberBaseException: for any unhandled HTTP error during communication with the E-Commerce Service.
        exceptions.Timeout: if the attempt to reach the commerce service timed out.
    """
    User = get_user_model()  # pylint:disable=invalid-name
    course_key_str = unicode(course_enrollment.course_id)
    enrollee = course_enrollment.user

    service_user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    api_client = ecommerce_api_client(service_user)

    log.info('Attempting to create a refund for user [%s], course [%s]...', enrollee.id, course_key_str)

    refund_ids = api_client.refunds.post({'course_id': course_key_str, 'username': enrollee.username})

    if refund_ids:
        log.info('Refund successfully opened for user [%s], course [%s]: %r', enrollee.id, course_key_str, refund_ids)

        _process_refund(
            refund_ids=refund_ids,
            api_client=api_client,
            mode=course_enrollment.mode,
            user=enrollee,
        )
        if change_mode and CourseMode.can_auto_enroll(course_id=CourseKey.from_string(course_key_str)):
            course_enrollment.update_enrollment(mode=CourseMode.auto_enroll_mode(course_id=course_key_str),
                                                is_active=False, skip_refund=True)
            course_enrollment.save()
    else:
        log.info('No refund opened for user [%s], course [%s]', enrollee.id, course_key_str)

    return refund_ids
Exemplo n.º 9
0
 def enroll_user(self,
                 bot,
                 chat_id,
                 telegram_user,
                 course_id,
                 show_description=True):
     user = telegram_user.student
     if isinstance(course_id, CourseLocator):
         course_key = course_id
     else:
         course_key = CourseKey.from_string(course_id)
     if CourseMode.can_auto_enroll(course_key):
         available_modes = CourseMode.modes_for_course_dict(course_key)
         try:
             enroll_mode = CourseMode.auto_enroll_mode(
                 course_key, available_modes)
             if enroll_mode:
                 CourseEnrollment.enroll(user,
                                         course_key,
                                         check_access=True,
                                         mode=enroll_mode)
             if not BotFriendlyCourses.objects.filter(
                     course_key=course_key).exists():
                 bot.sendMessage(chat_id=chat_id,
                                 text="You've been enrolled")
                 if show_description:
                     self.get_course_description(bot,
                                                 chat_id,
                                                 course_name=None,
                                                 course_key=course_key)
         except AlreadyEnrolledError:
             bot.sendMessage(
                 chat_id=chat_id,
                 text=
                 "It seems like you've been already enrolled to that course"
             )
         except EnrollmentClosedError:
             bot.sendMessage(
                 chat_id=chat_id,
                 text=
                 "I'm sorry but enrollment to that course has been closes")
         except Exception as e:
             print e
             bot.sendMessage(chat_id=chat_id, text="Something goes wrong")
Exemplo n.º 10
0
def granted(request, course_id, student_id):
    user = request.user
    if user.is_superuser:
        try:
            student = User.objects.get(id=student_id)
        except Exception as e:
            return JsonResponse({"Error": e.message})
        try:
            course_key = SlashSeparatedCourseKey.from_deprecated_string(
                course_id)
        except Exception as e:
            return JsonResponse({"Error": e.message})

        if CourseMode.can_auto_enroll(course_key):
            # 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".
            available_modes = CourseMode.modes_for_course_dict(course_key)
            try:
                enroll_mode = CourseMode.auto_enroll_mode(
                    course_key, available_modes)
                if enroll_mode:
                    CourseEnrollment.enroll(student,
                                            course_key,
                                            check_access=True,
                                            mode=enroll_mode)
                    enroll_student = RequestEnroll.objects.get(student=student)
                    enroll_student.enrollment_status = 'granted'
                    enroll_student.save()
            except Exception as e:  # pylint: disable=broad-except
                return JsonResponse({'Error': e.message})

            return JsonResponse({'Student Enrolled': True})
    else:
        return JsonResponse(
            {'Error': 'User is not allowed to do this operation'})
Exemplo n.º 11
0
 def enroll_user(self, bot, chat_id, telegram_user, course_id, show_description=True):
     user = telegram_user.student
     if isinstance(course_id, CourseLocator):
         course_key = course_id
     else:
         course_key = CourseKey.from_string(course_id)
     if CourseMode.can_auto_enroll(course_key):
         available_modes = CourseMode.modes_for_course_dict(course_key)
         try:
             enroll_mode = CourseMode.auto_enroll_mode(course_key, available_modes)
             if enroll_mode:
                 CourseEnrollment.enroll(user, course_key, check_access=True, mode=enroll_mode)
             if not BotFriendlyCourses.objects.filter(course_key=course_key).exists():
                 bot.sendMessage(chat_id=chat_id, text="You've been enrolled")
                 if show_description:
                     self.get_course_description(bot, chat_id, course_name=None, course_key=course_key)
         except AlreadyEnrolledError:
             bot.sendMessage(chat_id=chat_id, text="It seems like you've been already enrolled to that course")
         except EnrollmentClosedError:
             bot.sendMessage(chat_id=chat_id, text="I'm sorry but enrollment to that course has been closes")
         except Exception as e:
             print e
             bot.sendMessage(chat_id=chat_id, text="Something goes wrong")
Exemplo n.º 12
0
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"))
Exemplo n.º 13
0
def change_enrollment(strategy, auth_entry=None, user=None, *args, **kwargs):
    """Enroll a user in a course.

    If a user entered the authentication flow when trying to enroll
    in a course, then attempt to enroll the user.
    We will try to do this if the pipeline was started with the
    querystring param `enroll_course_id`.

    In the following cases, we can't enroll the user:
        * The course does not have an honor mode.
        * The course has an honor mode with a minimum price.
        * The course is not yet open for enrollment.
        * The course does not exist.

    If we can't enroll the user now, then skip this step.
    For paid courses, users will be redirected to the payment flow
    upon completion of the authentication pipeline
    (configured using the ?next parameter to the third party auth login url).

    Keyword Arguments:
        auth_entry: The entry mode into the pipeline.
        user (User): The user being authenticated.
    """
    # We skip enrollment if the user entered the flow from the "link account"
    # button on the student dashboard.  At this point, either:
    #
    # 1) The user already had a linked account when they started the enrollment flow,
    # in which case they would have been enrolled during the normal authentication process.
    #
    # 2) The user did NOT have a linked account, in which case they would have
    # needed to go through the login/register page.  Since we preserve the querystring
    # args when sending users to this page, successfully authenticating through this page
    # would also enroll the student in the course.
    enroll_course_id = strategy.session_get('enroll_course_id')
    if enroll_course_id and auth_entry != AUTH_ENTRY_DASHBOARD:
        course_id = CourseKey.from_string(enroll_course_id)
        modes = CourseMode.modes_for_course_dict(course_id)

        # If the email opt in parameter is found, set the preference.
        email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
        if email_opt_in:
            opt_in = email_opt_in.lower() == 'true'
            update_email_opt_in(user, course_id.org, opt_in)

        # Check whether we're blocked from enrolling by a
        # country access rule.
        # Note: We skip checking the user's profile setting
        # for country here because the "redirect URL" pointing
        # to the blocked message page is set when the user
        # *enters* the pipeline, at which point they're
        # not authenticated.  If they end up being blocked
        # from the courseware, it's better to let them
        # enroll and then show the message when they
        # enter the course than to skip enrollment
        # altogether.
        is_blocked = not embargo_api.check_course_access(
            course_id,
            ip_address=get_ip(strategy.request),
            url=strategy.request.path)
        if is_blocked:
            # If we're blocked, skip enrollment.
            # A redirect URL should have been set so the user
            # ends up on the embargo page when enrollment completes.
            pass

        elif CourseMode.can_auto_enroll(course_id, modes_dict=modes):
            try:
                CourseEnrollment.enroll(user, course_id, check_access=True)
            except CourseEnrollmentException:
                pass
            except Exception as ex:
                logger.exception(ex)

        # Handle white-label courses as a special case
        # If a course is white-label, we should add it to the shopping cart.
        elif CourseMode.is_white_label(course_id, modes_dict=modes):
            try:
                cart = Order.get_cart_for_user(user)
                PaidCourseRegistration.add_to_order(cart, course_id)
            except (
                    CourseDoesNotExistException,
                    ItemAlreadyInCartException,
                    AlreadyEnrolledInCourseException,
            ):
                pass
            # It's more important to complete login than to
            # ensure that the course was added to the shopping cart.
            # Log errors, but don't stop the authentication pipeline.
            except Exception as ex:  # pylint: disable=broad-except
                logger.exception(ex)
Exemplo n.º 14
0
def change_enrollment(strategy, auth_entry=None, user=None, *args, **kwargs):
    """Enroll a user in a course.

    If a user entered the authentication flow when trying to enroll
    in a course, then attempt to enroll the user.
    We will try to do this if the pipeline was started with the
    querystring param `enroll_course_id`.

    In the following cases, we can't enroll the user:
        * The course does not have an honor mode.
        * The course has an honor mode with a minimum price.
        * The course is not yet open for enrollment.
        * The course does not exist.

    If we can't enroll the user now, then skip this step.
    For paid courses, users will be redirected to the payment flow
    upon completion of the authentication pipeline
    (configured using the ?next parameter to the third party auth login url).

    Keyword Arguments:
        auth_entry: The entry mode into the pipeline.
        user (User): The user being authenticated.
    """
    # We skip enrollment if the user entered the flow from the "link account"
    # button on the account settings page.  At this point, either:
    #
    # 1) The user already had a linked account when they started the enrollment flow,
    # in which case they would have been enrolled during the normal authentication process.
    #
    # 2) The user did NOT have a linked account, in which case they would have
    # needed to go through the login/register page.  Since we preserve the querystring
    # args when sending users to this page, successfully authenticating through this page
    # would also enroll the student in the course.
    enroll_course_id = strategy.session_get('enroll_course_id')
    if enroll_course_id and auth_entry != AUTH_ENTRY_ACCOUNT_SETTINGS:
        course_id = CourseKey.from_string(enroll_course_id)
        modes = CourseMode.modes_for_course_dict(course_id)

        # If the email opt in parameter is found, set the preference.
        email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
        if email_opt_in:
            opt_in = email_opt_in.lower() == 'true'
            update_email_opt_in(user, course_id.org, opt_in)

        # Check whether we're blocked from enrolling by a
        # country access rule.
        # Note: We skip checking the user's profile setting
        # for country here because the "redirect URL" pointing
        # to the blocked message page is set when the user
        # *enters* the pipeline, at which point they're
        # not authenticated.  If they end up being blocked
        # from the courseware, it's better to let them
        # enroll and then show the message when they
        # enter the course than to skip enrollment
        # altogether.
        is_blocked = not embargo_api.check_course_access(
            course_id, ip_address=get_ip(strategy.request),
            url=strategy.request.path
        )
        if is_blocked:
            # If we're blocked, skip enrollment.
            # A redirect URL should have been set so the user
            # ends up on the embargo page when enrollment completes.
            pass

        elif CourseMode.can_auto_enroll(course_id, modes_dict=modes):
            try:
                CourseEnrollment.enroll(user, course_id, check_access=True)
            except CourseEnrollmentException:
                pass
            except Exception as ex:
                logger.exception(ex)

        # Handle white-label courses as a special case
        # If a course is white-label, we should add it to the shopping cart.
        elif CourseMode.is_white_label(course_id, modes_dict=modes):
            try:
                cart = Order.get_cart_for_user(user)
                PaidCourseRegistration.add_to_order(cart, course_id)
            except (
                CourseDoesNotExistException,
                ItemAlreadyInCartException,
                AlreadyEnrolledInCourseException,
            ):
                pass
            # It's more important to complete login than to
            # ensure that the course was added to the shopping cart.
            # Log errors, but don't stop the authentication pipeline.
            except Exception as ex:  # pylint: disable=broad-except
                logger.exception(ex)
Exemplo n.º 15
0
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"))