예제 #1
0
    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
예제 #2
0
def unenroll_entitlement(sender,
                         course_enrollment=None,
                         skip_refund=False,
                         **kwargs):  # pylint: disable=unused-argument
    """
    Un-enroll user from entitlement upon course run un-enrollment if exist.
    """
    CourseEntitlement.unenroll_entitlement(course_enrollment, skip_refund)
예제 #3
0
    def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False):
        self.site = site
        self.user = user
        self.mobile_only = mobile_only

        self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user))
        self.enrollments.sort(key=lambda e: e.created, reverse=True)

        self.enrolled_run_modes = {}
        self.course_run_ids = []
        for enrollment in self.enrollments:
            # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
            enrollment_id = str(enrollment.course_id)
            mode = enrollment.mode
            if mode == CourseMode.NO_ID_PROFESSIONAL_MODE:
                mode = CourseMode.PROFESSIONAL
            self.enrolled_run_modes[enrollment_id] = mode
            # We can't use dict.keys() for this because the course run ids need to be ordered
            self.course_run_ids.append(enrollment_id)

        self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user))
        self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements]

        if uuid:
            self.programs = [get_programs(uuid=uuid)]
        else:
            self.programs = attach_program_detail_url(get_programs(self.site), self.mobile_only)
예제 #4
0
 def post(self, request, username_or_email):
     """Allows support staff to alter a user's enrollment."""
     try:
         user = User.objects.get(
             Q(username=username_or_email) | Q(email=username_or_email))
         course_id = request.data['course_id']
         course_key = CourseKey.from_string(course_id)
         old_mode = request.data['old_mode']
         new_mode = request.data['new_mode']
         reason = request.data['reason']
         enrollment = CourseEnrollment.objects.get(user=user,
                                                   course_id=course_key)
         if enrollment.mode != old_mode:
             return HttpResponseBadRequest(
                 'User {username} is not enrolled with mode {old_mode}.'.
                 format(username=user.username, old_mode=old_mode))
     except KeyError as err:
         return HttpResponseBadRequest('The field {} is required.'.format(
             str(err)))
     except InvalidKeyError:
         return HttpResponseBadRequest('Could not parse course key.')
     except (CourseEnrollment.DoesNotExist, User.DoesNotExist):
         return HttpResponseBadRequest(
             'Could not find enrollment for user {username} in course {course}.'
             .format(username=username_or_email, course=str(course_key)))
     try:
         # Wrapped in a transaction so that we can be sure the
         # ManualEnrollmentAudit record is always created correctly.
         with transaction.atomic():
             update_enrollment(user.username,
                               course_id,
                               mode=new_mode,
                               include_expired=True)
             manual_enrollment = ManualEnrollmentAudit.create_manual_enrollment_audit(
                 request.user,
                 enrollment.user.email,
                 ENROLLED_TO_ENROLLED,
                 reason=reason,
                 enrollment=enrollment)
             if new_mode == CourseMode.CREDIT_MODE:
                 provider_ids = get_credit_provider_attribute_values(
                     course_key, 'id')
                 credit_provider_attr = {
                     'namespace': 'credit',
                     'name': 'provider_id',
                     'value': provider_ids[0],
                 }
                 CourseEnrollmentAttribute.add_enrollment_attr(
                     enrollment=enrollment,
                     data_list=[credit_provider_attr])
             entitlement = CourseEntitlement.get_fulfillable_entitlement_for_user_course_run(
                 user=user, course_run_key=course_id)
             if entitlement is not None and entitlement.mode == new_mode:
                 entitlement.set_enrollment(
                     CourseEnrollment.get_enrollment(user, course_id))
             return JsonResponse(
                 ManualEnrollmentSerializer(
                     instance=manual_enrollment).data)
     except CourseModeNotFoundError as err:
         return HttpResponseBadRequest(str(err))
예제 #5
0
    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,  # lint-amnesty, pylint: disable=no-member
            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)  # lint-amnesty, pylint: disable=no-member
예제 #6
0
def get_filtered_course_entitlements(user, org_whitelist, org_blacklist):
    """
    Given a user, return a filtered set of their course entitlements.

    Arguments:
        user (User): the user in question.
        org_whitelist (list[str]): If not None, ONLY entitlements of these orgs will be returned.
        org_blacklist (list[str]): CourseEntitlements of these orgs will be excluded.

    Returns:
        generator[CourseEntitlement]: a sequence of entitlements to be displayed
        on the user's dashboard.
    """
    course_entitlement_available_sessions = {}
    unfulfilled_entitlement_pseudo_sessions = {}
    course_entitlements = list(
        CourseEntitlement.get_active_entitlements_for_user(user))
    filtered_entitlements = []
    pseudo_session = None
    course_run_key = None

    for course_entitlement in course_entitlements:
        course_entitlement.update_expired_at()
        available_runs = get_visible_sessions_for_entitlement(
            course_entitlement)

        if not course_entitlement.enrollment_course_run:
            # Unfulfilled entitlements need a mock session for metadata
            pseudo_session = get_pseudo_session_for_entitlement(
                course_entitlement)
            unfulfilled_entitlement_pseudo_sessions[str(
                course_entitlement.uuid)] = pseudo_session

        # Check the org of the Course and filter out entitlements that are not available.
        if course_entitlement.enrollment_course_run:
            course_run_key = course_entitlement.enrollment_course_run.course_id
        elif available_runs:
            course_run_key = CourseKey.from_string(available_runs[0]['key'])
        elif pseudo_session:
            course_run_key = CourseKey.from_string(pseudo_session['key'])

        if course_run_key:
            # If there is no course_run_key at this point we will be unable to determine if it should be shown.
            # Therefore it should be excluded by default.
            if org_whitelist and course_run_key.org not in org_whitelist:
                continue
            elif org_blacklist and course_run_key.org in org_blacklist:
                continue

            course_entitlement_available_sessions[str(
                course_entitlement.uuid)] = available_runs
            filtered_entitlements.append(course_entitlement)

    return filtered_entitlements, course_entitlement_available_sessions, unfulfilled_entitlement_pseudo_sessions
예제 #7
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(
            "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_attribute('course_id', str(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(
                "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_client_ip(request)[0],
            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=[str(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': str(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"))
예제 #8
0
    def progress(self, programs=None, count_only=True):
        """Gauge a user's progress towards program completion.

        Keyword Arguments:
            programs (list): Specific list of programs to check the user's progress
                against. If left unspecified, self.engaged_programs will be used.

            count_only (bool): Whether or not to return counts of completed, in
                progress, and unstarted courses instead of serialized representations
                of the courses.

        Returns:
            list of dict, each containing information about a user's progress
                towards completing a program.
        """
        now = datetime.datetime.now(utc)

        progress = []
        programs = programs or self.engaged_programs
        for program in programs:
            program_copy = deepcopy(program)
            completed, in_progress, not_started = [], [], []

            for course in program_copy['courses']:
                active_entitlement = CourseEntitlement.get_entitlement_if_active(
                    user=self.user, course_uuid=course['uuid'])
                if self._is_course_complete(course):
                    completed.append(course)
                elif self._is_course_enrolled(course) or active_entitlement:
                    # Show all currently enrolled courses and active entitlements as in progress
                    if active_entitlement:
                        course[
                            'course_runs'] = get_fulfillable_course_runs_for_entitlement(
                                active_entitlement, course['course_runs'])
                        course[
                            'user_entitlement'] = active_entitlement.to_dict()
                        course['enroll_url'] = reverse(
                            'entitlements_api:v1:enrollments',
                            args=[str(active_entitlement.uuid)])
                        in_progress.append(course)
                    else:
                        course_in_progress = self._is_course_in_progress(
                            now, course)
                        if course_in_progress:
                            in_progress.append(course)
                        else:
                            course['expired'] = not course_in_progress
                            not_started.append(course)
                else:
                    not_started.append(course)

            progress.append({
                'uuid':
                program_copy['uuid'],
                'completed':
                len(completed) if count_only else completed,
                'in_progress':
                len(in_progress) if count_only else in_progress,
                'not_started':
                len(not_started) if count_only else not_started,
            })

        return progress
예제 #9
0
    def post(self, request, *args, **kwargs):  # lint-amnesty, pylint: disable=unused-argument
        """
        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 = str(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('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=[str(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
        course_name = None
        course_announcement = None
        if course is not None:
            course_name = course.display_name
            course_announcement = course.announcement
        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,
                    course_name=course_name,
                    username=user.username,
                    announcement=course_announcement)
            log.info(msg)
            self._enroll(course_key, user, default_enrollment_mode.slug)
            mode = CourseMode.AUDIT if audit_mode else CourseMode.HONOR  # lint-amnesty, pylint: disable=unused-variable
            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)