def test_modes_for_course_multiple(self):
        """
        Finding the modes when there's multiple modes
        """
        mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
        mode2 = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None)
        set_modes = [mode1, mode2]
        for mode in set_modes:
            self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices)

        modes = CourseMode.modes_for_course(self.course_key)
        assert modes == set_modes
        assert mode1 == CourseMode.mode_for_course(self.course_key, u'honor')
        assert mode2 == CourseMode.mode_for_course(self.course_key, u'verified')
        assert CourseMode.mode_for_course(self.course_key, 'DNE') is None
    def move_users_for_course(self, course_key, from_mode, to_mode, commit):
        """
        Change the enrollment mode of users for a course.

        Arguments:
            course_key (CourseKey): to lookup the course.
            from_mode (str): the enrollment mode to change.
            to_mode (str): the enrollment mode to change to.
            commit (bool): required to make the change to the database. Otherwise
                                     just a count will be displayed.
        """
        unicode_course_key = str(course_key)
        if CourseMode.mode_for_course(course_key, to_mode) is None:
            logger.info(
                f'Mode ({to_mode}) does not exist for course ({unicode_course_key}).'
            )
            return

        course_enrollments = CourseEnrollment.objects.filter(
            course_id=course_key, mode=from_mode)
        logger.info('Moving %d users from %s to %s in course %s.',
                    course_enrollments.count(), from_mode, to_mode,
                    unicode_course_key)
        if commit:
            # call `change_mode` which will change the mode and also emit tracking event
            for enrollment in course_enrollments:
                with transaction.atomic():
                    enrollment.change_mode(mode=to_mode)

            logger.info('Finished moving users from %s to %s in course %s.',
                        from_mode, to_mode, unicode_course_key)
    def test_nodes_for_course_single(self):
        """
        Find the modes for a course with only one mode
        """

        self.create_mode('verified', 'Verified Certificate', 10)
        modes = CourseMode.modes_for_course(self.course_key)
        mode = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None)
        assert [mode] == modes

        modes_dict = CourseMode.modes_for_course_dict(self.course_key)
        assert modes_dict['verified'] == mode
        assert CourseMode.mode_for_course(self.course_key, 'verified') == mode
Beispiel #4
0
def correct_modes_for_fbe(course_key=None,
                          enrollment=None,
                          user=None,
                          course=None):
    """
    If CONTENT_TYPE_GATING is enabled use the following logic to determine whether
    enabled_for_enrollment should be false
    """
    if course_key is None and course is None:
        return True

    # Separate these two calls to help with cache hits (most modes_for_course callers pass in a positional course key)
    if course:
        modes = CourseMode.modes_for_course(course=course,
                                            include_expired=True,
                                            only_selectable=False)
    else:
        modes = CourseMode.modes_for_course(course_key,
                                            include_expired=True,
                                            only_selectable=False)

    modes_dict = {mode.slug: mode for mode in modes}
    course_key = course_key or course.id

    # If there is no audit mode or no verified mode, FBE will not be enabled
    if (CourseMode.AUDIT not in modes_dict) or (CourseMode.VERIFIED
                                                not in modes_dict):
        return False

    if enrollment and user:
        mode_slug = enrollment.mode
        if enrollment.is_active:
            course_mode = CourseMode.mode_for_course(
                course_key,
                mode_slug,
                modes=modes,
            )
            if course_mode is None:
                LOG.error(
                    "User %s is in an unknown CourseMode '%s'"
                    " for course %s. Granting full access to content for this user",
                    user.username,
                    mode_slug,
                    course_key,
                )
                return False

            if mode_slug != CourseMode.AUDIT:
                return False
    return True
Beispiel #5
0
    def test_nodes_for_course_single(self):
        """
        Find the modes for a course with only one mode
        """

        self.create_mode('verified', 'Verified Certificate', 10)
        modes = CourseMode.modes_for_course(self.course_key)
        mode = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None,
                    None, None, None)
        self.assertEqual([mode], modes)

        modes_dict = CourseMode.modes_for_course_dict(self.course_key)
        self.assertEqual(modes_dict['verified'], mode)
        self.assertEqual(
            CourseMode.mode_for_course(self.course_key, 'verified'), mode)
Beispiel #6
0
    def _attach_course_run_upgrade_url(self, run_mode):
        required_mode_slug = run_mode['type']
        enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_run_key)
        is_mode_mismatch = required_mode_slug != enrolled_mode_slug
        is_upgrade_required = is_mode_mismatch and CourseEnrollment.is_enrolled(self.user, self.course_run_key)

        if is_upgrade_required:
            # Requires that the ecommerce service be in use.
            required_mode = CourseMode.mode_for_course(self.course_run_key, required_mode_slug)
            ecommerce = EcommerceService()
            sku = getattr(required_mode, 'sku', None)
            if ecommerce.is_enabled(self.user) and sku:
                run_mode['upgrade_url'] = ecommerce.get_checkout_page_url(required_mode.sku)
            else:
                run_mode['upgrade_url'] = None
        else:
            run_mode['upgrade_url'] = None
Beispiel #7
0
    def get_group_for_user(cls, course_key, user, user_partition, **kwargs):  # pylint: disable=unused-argument
        """
        Returns the Group from the specified user partition to which the user
        is assigned, via enrollment mode. If a user is in a Credit mode, the Verified or
        Professional mode for the course is returned instead.

        If a course is using the Verified Track Cohorting pilot feature, this method
        returns None regardless of the user's enrollment mode.
        """
        if is_course_using_cohort_instead(course_key):
            return None

        # First, check if we have to deal with masquerading.
        # If the current user is masquerading as a specific student, use the
        # same logic as normal to return that student's group. If the current
        # user is masquerading as a generic student in a specific group, then
        # return that group.
        if get_course_masquerade(
                user, course_key) and not is_masquerading_as_specific_student(
                    user, course_key):
            return get_masquerading_user_group(course_key, user,
                                               user_partition)

        mode_slug, is_active = CourseEnrollment.enrollment_mode_for_user(
            user, course_key)
        if mode_slug and is_active:
            course_mode = CourseMode.mode_for_course(
                course_key,
                mode_slug,
                modes=CourseMode.modes_for_course(course_key,
                                                  include_expired=True,
                                                  only_selectable=False),
            )
            if course_mode and CourseMode.is_credit_mode(course_mode):
                # We want the verified track even if the upgrade deadline has passed, since we
                # are determining what content to show the user, not whether the user can enroll
                # in the verified track.
                course_mode = CourseMode.verified_mode_for_course(
                    course_key, include_expired=True)
            if not course_mode:
                course_mode = CourseMode.DEFAULT_MODE
            return Group(ENROLLMENT_GROUP_IDS[course_mode.slug]["id"],
                         str(course_mode.name))
        else:
            return None
Beispiel #8
0
    def add_cert(self,
                 student,
                 course_id,
                 forced_grade=None,
                 template_file=None,
                 generate_pdf=True):
        """
        Request a new certificate for a student.

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (CourseKey)
          forced_grade - a string indicating a grade parameter to pass with
                         the certificate request. If this is given, grading
                         will be skipped.
          generate_pdf - Boolean should a message be sent in queue to generate certificate PDF

        Will change the certificate status to 'generating' or
        `downloadable` in case of web view certificates.

        The course must not be a CCX.

        Certificate must be in the 'unavailable', 'error',
        'deleted' or 'generating' state.

        If a student has a passing grade or is in the whitelist
        table for the course a request will be made for a new cert.

        If a student does not have a passing grade the status
        will change to status.notpassing

        Returns the newly created certificate instance
        """

        if hasattr(course_id, 'ccx'):
            LOGGER.warning(
                ("Cannot create certificate generation task for user %s "
                 "in the course '%s'; "
                 "certificates are not allowed for CCX courses."), student.id,
                str(course_id))
            return None

        valid_statuses = [
            status.generating,
            status.unavailable,
            status.deleted,
            status.error,
            status.notpassing,
            status.downloadable,
            status.auditing,
            status.audit_passing,
            status.audit_notpassing,
            status.unverified,
        ]

        cert_status_dict = certificate_status_for_student(student, course_id)
        cert_status = cert_status_dict.get('status')
        download_url = cert_status_dict.get('download_url')
        cert = None
        if download_url:
            self._log_pdf_cert_generation_discontinued_warning(
                student.id, course_id, cert_status, download_url)
            return None

        if cert_status not in valid_statuses:
            LOGGER.warning(
                ("Cannot create certificate generation task for user %s "
                 "in the course '%s'; "
                 "the certificate status '%s' is not one of %s."), student.id,
                str(course_id), cert_status, str(valid_statuses))
            return None

        profile = UserProfile.objects.get(user=student)
        profile_name = profile.name

        # Needed for access control in grading.
        self.request.user = student
        self.request.session = {}

        is_whitelisted = self.whitelist.filter(user=student,
                                               course_id=course_id,
                                               whitelist=True).exists()
        course_grade = CourseGradeFactory().read(student, course_key=course_id)
        enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(
            student, course_id)
        mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES
        user_is_verified = IDVerificationService.user_is_verified(student)
        cert_mode = enrollment_mode

        is_eligible_for_certificate = modes_api.is_eligible_for_certificate(
            enrollment_mode, cert_status)
        if is_whitelisted and not is_eligible_for_certificate:
            # check if audit certificates are enabled for audit mode
            is_eligible_for_certificate = enrollment_mode != CourseMode.AUDIT or \
                not settings.FEATURES['DISABLE_AUDIT_CERTIFICATES']

        unverified = False
        # For credit mode generate verified certificate
        if cert_mode in (CourseMode.CREDIT_MODE, CourseMode.MASTERS):
            cert_mode = CourseMode.VERIFIED

        if template_file is not None:
            template_pdf = template_file
        elif mode_is_verified and user_is_verified:
            template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(
                id=course_id)
        elif mode_is_verified and not user_is_verified:
            template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(
                id=course_id)
            if CourseMode.mode_for_course(course_id, CourseMode.HONOR):
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                unverified = True
        else:
            # honor code and audit students
            template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(
                id=course_id)

        LOGGER.info((
            "Certificate generated for student %s in the course: %s with template: %s. "
            "given template: %s, "
            "user is verified: %s, "
            "mode is verified: %s,"
            "generate_pdf is: %s"), student.username, str(course_id),
                    template_pdf, template_file, user_is_verified,
                    mode_is_verified, generate_pdf)
        cert, __ = GeneratedCertificate.objects.get_or_create(
            user=student, course_id=course_id)

        cert.mode = cert_mode
        cert.user = student
        cert.grade = course_grade.percent
        cert.course_id = course_id
        cert.name = profile_name
        cert.download_url = ''

        # Strip HTML from grade range label
        grade_contents = forced_grade or course_grade.letter_grade
        passing = False
        try:
            grade_contents = lxml.html.fromstring(
                grade_contents).text_content()
            passing = True
        except (TypeError, XMLSyntaxError, ParserError) as exc:
            LOGGER.info(("Could not retrieve grade for student %s "
                         "in the course '%s' "
                         "because an exception occurred while parsing the "
                         "grade contents '%s' as HTML. "
                         "The exception was: '%s'"), student.id,
                        str(course_id), grade_contents, str(exc))

        # Check if the student is whitelisted
        if is_whitelisted:
            LOGGER.info("Student %s is whitelisted in '%s'", student.id,
                        str(course_id))
            passing = True

        # If this user's enrollment is not eligible to receive a
        # certificate, mark it as such for reporting and
        # analytics. Only do this if the certificate is new, or
        # already marked as ineligible -- we don't want to mark
        # existing audit certs as ineligible.
        cutoff = settings.AUDIT_CERT_CUTOFF_DATE
        if (cutoff and cert.created_date >= cutoff
            ) and not is_eligible_for_certificate:
            cert.status = status.audit_passing if passing else status.audit_notpassing
            cert.save()
            LOGGER.info(
                "Student %s with enrollment mode %s is not eligible for a certificate.",
                student.id, enrollment_mode)
            return cert
        # If they are not passing, short-circuit and don't generate cert
        elif not passing:
            cert.status = status.notpassing
            cert.save()

            LOGGER.info(
                ("Student %s does not have a grade for '%s', "
                 "so their certificate status has been set to '%s'. "
                 "No certificate generation task was sent to the XQueue."),
                student.id, str(course_id), cert.status)
            return cert

        if unverified:
            cert.status = status.unverified
            cert.save()
            LOGGER.info(
                ("User %s has a verified enrollment in course %s "
                 "but is missing ID verification. "
                 "Certificate status has been set to unverified"),
                student.id,
                str(course_id),
            )
            return cert

        # Finally, generate the certificate and send it off.
        return self._generate_cert(cert, student, grade_contents, template_pdf,
                                   generate_pdf)
Beispiel #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)
Beispiel #10
0
    def _terminate_enrollment(self, enrollment_api_client,
                              enterprise_enrollment, course_overview):
        """
        Helper method that switches the given enrollment to audit track, or, if
        no audit track exists for the given course, deletes the enrollment.
        Will do nothing if the user has already "completed" the course run.

        Args:
            enrollment_api_client (EnrollmentApiClient): The client with which we make requests to modify enrollments.
            enterprise_enrollment (EnterpriseCourseEnrollment): The enterprise enrollment which we attempt to revoke.
            course_overview (CourseOverview): The course overview object associated with the enrollment. Used
                to check for course completion.
        """
        course_run_id = course_overview.get('id')
        enterprise_customer_user = enterprise_enrollment.enterprise_customer_user
        audit_mode = CourseMode.AUDIT
        enterprise_id = enterprise_customer_user.enterprise_customer.uuid

        log_message_kwargs = {
            'user': enterprise_customer_user.username,
            'enterprise': enterprise_id,
            'course_id': course_run_id,
            'mode': audit_mode,
        }

        if self._has_user_completed_course_run(enterprise_enrollment,
                                               course_overview):
            LOGGER.info(
                'enrollment termination: not updating enrollment in {course_id} for User {user} '
                'in Enterprise {enterprise}, course is already complete.'.
                format(**log_message_kwargs))
            return self.EnrollmentTerminationStatus.COURSE_COMPLETED

        if CourseMode.mode_for_course(course_run_id, audit_mode):
            try:
                enrollment_api_client.update_course_enrollment_mode_for_user(
                    username=enterprise_customer_user.username,
                    course_id=course_run_id,
                    mode=audit_mode,
                )
                LOGGER.info(
                    'Enrollment termination: updated LMS enrollment for User {user} and Enterprise {enterprise} '
                    'in Course {course_id} to Course Mode {mode}.'.format(
                        **log_message_kwargs))
                return self.EnrollmentTerminationStatus.MOVED_TO_AUDIT
            except Exception as exc:  # pylint: disable=broad-except
                msg = (
                    'Enrollment termination: unable to update LMS enrollment for User {user} and '
                    'Enterprise {enterprise} in Course {course_id} to Course Mode {mode} because: {reason}'
                    .format(reason=str(exc), **log_message_kwargs))
                LOGGER.error('{msg}: {exc}'.format(msg=msg, exc=exc))
                raise EnrollmentModificationException(msg) from exc
        else:
            try:
                successfully_unenrolled = enrollment_api_client.unenroll_user_from_course(
                    username=enterprise_customer_user.username,
                    course_id=course_run_id,
                )
                if not successfully_unenrolled:
                    raise Exception(
                        self.EnrollmentTerminationStatus.UNENROLL_FAILED)

                LOGGER.info(
                    'Enrollment termination: successfully unenrolled User {user}, in Enterprise {enterprise} '
                    'from Course {course_id} that contains no audit mode.'.
                    format(**log_message_kwargs))
                return self.EnrollmentTerminationStatus.UNENROLLED
            except Exception as exc:  # pylint: disable=broad-except
                msg = (
                    'Enrollment termination: unable to unenroll User {user} in Enterprise {enterprise} '
                    'from Course {course_id}  because: {reason}'.format(
                        reason=str(exc), **log_message_kwargs))
                LOGGER.error('{msg}: {exc}'.format(msg=msg, exc=exc))
                raise EnrollmentModificationException(msg) from exc