Exemplo n.º 1
0
def _set_regular_cert_status(user, course_key, enrollment_mode, course_grade):
    """
    Determine the regular (non-allowlist) certificate status for this user, in this course run.

    This is used when a downloadable cert cannot be generated, but we want to provide more info about why it cannot
    be generated.
    """
    if not _can_set_regular_cert_status(user, course_key, enrollment_mode):
        return None

    cert = GeneratedCertificate.certificate_for_student(user, course_key)
    status = _get_cert_status_common(user, course_key, enrollment_mode,
                                     course_grade, cert)
    if status is not None:
        return status

    if IDVerificationService.user_is_verified(
            user) and not _is_passing_grade(course_grade) and cert is not None:
        if cert.status != CertificateStatuses.notpassing:
            course_grade_val = _get_grade_value(course_grade)
            cert.mark_notpassing(mode=enrollment_mode,
                                 grade=course_grade_val,
                                 source='certificate_generation')
        return CertificateStatuses.notpassing

    return None
Exemplo n.º 2
0
def _get_cert_status_common(user, course_key, enrollment_mode, course_grade,
                            cert):
    """
    Determine the certificate status for this user, in this course run.

    This is used when a downloadable cert cannot be generated, but we want to provide more info about why it cannot
    be generated.
    """
    if CertificateInvalidation.has_certificate_invalidation(
            user, course_key) and cert is not None:
        if cert.status != CertificateStatuses.unavailable:
            cert.invalidate(mode=enrollment_mode,
                            source='certificate_generation')
        return CertificateStatuses.unavailable

    if not IDVerificationService.user_is_verified(
            user) and _has_passing_grade_or_is_allowlisted(
                user, course_key, course_grade):
        if cert is None:
            _generate_certificate_task(user=user,
                                       course_key=course_key,
                                       enrollment_mode=enrollment_mode,
                                       course_grade=course_grade,
                                       status=CertificateStatuses.unverified,
                                       generation_mode='batch')
        elif cert.status != CertificateStatuses.unverified:
            cert.mark_unverified(mode=enrollment_mode,
                                 source='certificate_generation')
        return CertificateStatuses.unverified

    return None
Exemplo n.º 3
0
def _id_verification_enforced_and_missing(user):
    """
    Return true if IDV is required for this course and the user does not have it
    """
    return settings.FEATURES.get(
        'ENABLE_CERTIFICATES_IDV_REQUIREMENT'
    ) and not IDVerificationService.user_is_verified(user)
Exemplo n.º 4
0
def _required_verification_missing(user):
    """
    Return true if IDV is required for this course and the user does not have it
    """
    return not settings.FEATURES.get(
        'ENABLE_INTEGRITY_SIGNATURE'
    ) and not IDVerificationService.user_is_verified(user)
Exemplo n.º 5
0
    def test_user_is_verified(self):
        """
        Test to make sure we correctly answer whether a user has been verified.
        """
        user = UserFactory.create()
        attempt = SoftwareSecurePhotoVerification(user=user)
        attempt.save()

        # If it's any of these, they're not verified...
        for status in ["created", "ready", "denied", "submitted", "must_retry"]:
            attempt.status = status
            attempt.save()
            assert_false(IDVerificationService.user_is_verified(user), status)

        attempt.status = "approved"
        attempt.save()
        assert_true(IDVerificationService.user_is_verified(user), attempt.status)
Exemplo n.º 6
0
    def test_user_is_verified(self):
        """
        Test to make sure we correctly answer whether a user has been verified.
        """
        user = UserFactory.create()
        attempt = SoftwareSecurePhotoVerification(user=user)
        attempt.save()

        # If it's any of these, they're not verified...
        for status in ["created", "ready", "denied", "submitted", "must_retry"]:
            attempt.status = status
            attempt.save()
            assert_false(IDVerificationService.user_is_verified(user), status)

        attempt.status = "approved"
        attempt.save()
        assert_true(IDVerificationService.user_is_verified(user), attempt.status)
Exemplo n.º 7
0
def _can_generate_allowlist_certificate(user, course_key):
    """
    Check if an allowlist certificate can be generated (created if it doesn't already exist, or updated if it does
    exist) for this user, in this course run.
    """
    if not is_using_certificate_allowlist(course_key):
        # This course run is not using the allowlist feature
        log.info(
            f'{course_key} is not using the certificate allowlist. Allowlist certificate cannot be generated'
            f'for {user.id}.')
        return False

    if not _is_on_certificate_allowlist(user, course_key):
        log.info(
            f'{user.id} : {course_key} is not on the certificate allowlist. Allowlist certificate cannot be '
            f'generated.')
        return False

    if not auto_certificate_generation_enabled():
        # Automatic certificate generation is globally disabled
        log.info(
            f'Automatic certificate generation is globally disabled. Allowlist certificate cannot be generated'
            f'for {user.id} : {course_key}.')
        return False

    if CertificateInvalidation.has_certificate_invalidation(user, course_key):
        # The invalidation list overrides the allowlist
        log.info(
            f'{user.id} : {course_key} is on the certificate invalidation list. Allowlist certificate cannot be '
            f'generated.')
        return False

    enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(
        user, course_key)
    if enrollment_mode is None:
        log.info(
            f'{user.id} : {course_key} does not have an enrollment. Allowlist certificate cannot be generated.'
        )
        return False

    if not modes_api.is_eligible_for_certificate(enrollment_mode):
        log.info(
            f'{user.id} : {course_key} has an enrollment mode of {enrollment_mode}, which is not eligible for an '
            f'allowlist certificate. Certificate cannot be generated.')
        return False

    if not IDVerificationService.user_is_verified(user):
        log.info(
            f'{user.id} does not have a verified id. Allowlist certificate cannot be generated for {course_key}.'
        )
        return False

    log.info(f'{user.id} : {course_key} is on the certificate allowlist')
    return _can_generate_allowlist_certificate_for_status(user, course_key)
Exemplo n.º 8
0
def can_generate_allowlist_certificate(user, course_key):
    """
    Check if an allowlist certificate can be generated (created if it doesn't already exist, or updated if it does
    exist) for this user, in this course run.
    """
    if not _is_using_certificate_allowlist(course_key):
        # This course run is not using the allowlist feature
        log.info(
            '{course} is not using the certificate allowlist. Certificate cannot be generated.'
            .format(course=course_key))
        return False

    if not auto_certificate_generation_enabled():
        # Automatic certificate generation is globally disabled
        log.info(
            'Automatic certificate generation is globally disabled. Certificate cannot be generated.'
        )
        return False

    if CertificateInvalidation.has_certificate_invalidation(user, course_key):
        # The invalidation list overrides the allowlist
        log.info(
            '{user} : {course} is on the certificate invalidation list. Certificate cannot be generated.'
            .format(user=user.id, course=course_key))
        return False

    enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(
        user, course_key)
    if enrollment_mode is None:
        log.info(
            '{user} : {course} does not have an enrollment. Certificate cannot be generated.'
            .format(user=user.id, course=course_key))
        return False

    if not IDVerificationService.user_is_verified(user):
        log.info(
            '{user} does not have a verified id. Certificate cannot be generated.'
            .format(user=user.id))
        return False

    if not CertificateWhitelist.objects.filter(
            user=user, course_id=course_key, whitelist=True).exists():
        log.info(
            '{user} : {course} is not on the certificate allowlist. Certificate cannot be generated.'
            .format(user=user.id, course=course_key))
        return False

    log.info('{user} : {course} is on the certificate allowlist'.format(
        user=user.id, course=course_key))
    cert = GeneratedCertificate.certificate_for_student(user, course_key)
    return _can_generate_allowlist_certificate_for_status(cert)
Exemplo n.º 9
0
def _can_generate_certificate_common(user, course_key):
    """
    Check if a course certificate can be generated (created if it doesn't already exist, or updated if it does
    exist) for this user, in this course run.

    This method contains checks that are common to both allowlist and V2 regular course certificates.
    """
    if CertificateInvalidation.has_certificate_invalidation(user, course_key):
        # The invalidation list prevents certificate generation
        log.info(
            f'{user.id} : {course_key} is on the certificate invalidation list. Certificate cannot be generated.'
        )
        return False

    enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(
        user, course_key)
    if enrollment_mode is None:
        log.info(
            f'{user.id} : {course_key} does not have an enrollment. Certificate cannot be generated.'
        )
        return False

    if not modes_api.is_eligible_for_certificate(enrollment_mode):
        log.info(
            f'{user.id} : {course_key} has an enrollment mode of {enrollment_mode}, which is not eligible for a '
            f'certificate. Certificate cannot be generated.')
        return False

    if not IDVerificationService.user_is_verified(user):
        log.info(
            f'{user.id} does not have a verified id. Certificate cannot be generated for {course_key}.'
        )
        return False

    if not _can_generate_certificate_for_status(user, course_key):
        return False

    course_overview = get_course_overview_or_none(course_key)
    if not course_overview:
        log.info(
            f'{course_key} does not a course overview. Certificate cannot be generated for {user.id}.'
        )
        return False

    if not has_html_certificates_enabled(course_overview):
        log.info(
            f'{course_key} does not have HTML certificates enabled. Certificate cannot be generated for '
            f'{user.id}.')
        return False

    return True
Exemplo n.º 10
0
def _get_cert_status_common(user, course_key, cert):
    """
    Determine the certificate status for this user, in this course run.

    This is used when a downloadable cert cannot be generated, but we want to provide more info about why it cannot
    be generated.
    """
    if CertificateInvalidation.has_certificate_invalidation(user, course_key):
        if cert is None:
            cert = GeneratedCertificate.objects.create(user=user, course_id=course_key)
        if cert.status != CertificateStatuses.unavailable:
            cert.invalidate()
        return CertificateStatuses.unavailable

    if not IDVerificationService.user_is_verified(user):
        if cert is None:
            cert = GeneratedCertificate.objects.create(user=user, course_id=course_key)
        if cert.status != CertificateStatuses.unverified:
            cert.mark_unverified()
        return CertificateStatuses.unverified

    return None
Exemplo n.º 11
0
def _set_v2_cert_status(user, course_key):
    """
    Determine the V2 certificate status for this user, in this course run.

    This is used when a downloadable cert cannot be generated, but we want to provide more info about why it cannot
    be generated.
    """
    if not _can_set_v2_cert_status(user, course_key):
        return None

    cert = GeneratedCertificate.certificate_for_student(user, course_key)
    status = _get_cert_status_common(user, course_key, cert)
    if status is not None:
        return status

    if IDVerificationService.user_is_verified(user) and not _has_passing_grade(
            user, course_key) and cert is not None:
        if cert.status != CertificateStatuses.notpassing:
            course_grade = _get_course_grade(user, course_key)
            cert.mark_notpassing(course_grade.percent,
                                 source='certificate_generation')
        return CertificateStatuses.notpassing

    return None
Exemplo n.º 12
0
def check_verify_status_by_course(user, course_enrollments):
    """
    Determine the per-course verification statuses for a given user.

    The possible statuses are:
        * VERIFY_STATUS_NEED_TO_VERIFY: The student has not yet submitted photos for verification.
        * VERIFY_STATUS_SUBMITTED: The student has submitted photos for verification,
          but has have not yet been approved.
        * VERIFY_STATUS_RESUBMITTED: The student has re-submitted photos for re-verification while
          they still have an active but expiring ID verification
        * VERIFY_STATUS_APPROVED: The student has been successfully verified.
        * VERIFY_STATUS_MISSED_DEADLINE: The student did not submit photos within the course's deadline.
        * VERIFY_STATUS_NEED_TO_REVERIFY: The student has an active verification, but it is
            set to expire before the verification deadline for the course.

    It is is also possible that a course does NOT have a verification status if:
        * The user is not enrolled in a verified mode, meaning that the user didn't pay.
        * The course does not offer a verified mode.
        * The user submitted photos but an error occurred while verifying them.
        * The user submitted photos but the verification was denied.

    In the last two cases, we rely on messages in the sidebar rather than displaying
    messages for each course.

    Arguments:
        user (User): The currently logged-in user.
        course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in.

    Returns:
        dict: Mapping of course keys verification status dictionaries.
            If no verification status is applicable to a course, it will not
            be included in the dictionary.
            The dictionaries have these keys:
                * status (str): One of the enumerated status codes.
                * days_until_deadline (int): Number of days until the verification deadline.
                * verification_good_until (str): Date string for the verification expiration date.

    """
    status_by_course = {}

    # Retrieve all verifications for the user, sorted in descending
    # order by submission datetime
    verifications = IDVerificationService.verifications_for_user(user)

    # Check whether the user has an active or pending verification attempt
    has_active_or_pending = IDVerificationService.user_has_valid_or_pending(user)

    # Retrieve expiration_datetime of most recent approved verification
    expiration_datetime = IDVerificationService.get_expiration_datetime(user, ['approved'])
    verification_expiring_soon = is_verification_expiring_soon(expiration_datetime)

    # Retrieve verification deadlines for the enrolled courses
    course_deadlines = VerificationDeadline.deadlines_for_enrollments(
        CourseEnrollment.enrollments_for_user(user)
    )
    recent_verification_datetime = None

    for enrollment in course_enrollments:

        # If the user hasn't enrolled as verified, then the course
        # won't display state related to its verification status.
        if enrollment.mode in CourseMode.VERIFIED_MODES:

            # Retrieve the verification deadline associated with the course.
            # This could be None if the course doesn't have a deadline.
            deadline = course_deadlines.get(enrollment.course_id)

            relevant_verification = verification_for_datetime(deadline, verifications)

            # Picking the max verification datetime on each iteration only with approved status
            if relevant_verification is not None and relevant_verification.status == "approved":
                recent_verification_datetime = max(
                    recent_verification_datetime if recent_verification_datetime is not None
                    else relevant_verification.expiration_datetime,
                    relevant_verification.expiration_datetime
                )

            # By default, don't show any status related to verification
            status = None
            should_display = True

            # Check whether the user was approved or is awaiting approval
            if relevant_verification is not None:
                should_display = relevant_verification.should_display_status_to_user()

                if relevant_verification.status == "approved":
                    if verification_expiring_soon:
                        status = VERIFY_STATUS_NEED_TO_REVERIFY
                    else:
                        status = VERIFY_STATUS_APPROVED
                elif relevant_verification.status == "submitted":
                    if verification_expiring_soon:
                        status = VERIFY_STATUS_RESUBMITTED
                    else:
                        status = VERIFY_STATUS_SUBMITTED

            # If the user didn't submit at all, then tell them they need to verify
            # If the deadline has already passed, then tell them they missed it.
            # If they submitted but something went wrong (error or denied),
            # then don't show any messaging next to the course, since we already
            # show messages related to this on the left sidebar.
            submitted = (
                relevant_verification is not None and
                relevant_verification.status not in ["created", "ready"]
            )
            if status is None and not submitted:
                if deadline is None or deadline > datetime.now(UTC):
                    if IDVerificationService.user_is_verified(user) and verification_expiring_soon:
                        # The user has an active verification, but the verification
                        # is set to expire within "EXPIRING_SOON_WINDOW" days (default is 4 weeks).
                        # Tell the student to reverify.
                        status = VERIFY_STATUS_NEED_TO_REVERIFY
                    elif not IDVerificationService.user_is_verified(user):
                        status = VERIFY_STATUS_NEED_TO_VERIFY
                else:
                    # If a user currently has an active or pending verification,
                    # then they may have submitted an additional attempt after
                    # the verification deadline passed.  This can occur,
                    # for example, when the support team asks a student
                    # to reverify after the deadline so they can receive
                    # a verified certificate.
                    # In this case, we still want to show them as "verified"
                    # on the dashboard.
                    if has_active_or_pending:
                        status = VERIFY_STATUS_APPROVED

                    # Otherwise, the student missed the deadline, so show
                    # them as "honor" (the kind of certificate they will receive).
                    else:
                        status = VERIFY_STATUS_MISSED_DEADLINE

            # Set the status for the course only if we're displaying some kind of message
            # Otherwise, leave the course out of the dictionary.
            if status is not None:
                days_until_deadline = None

                now = datetime.now(UTC)
                if deadline is not None and deadline > now:
                    days_until_deadline = (deadline - now).days

                status_by_course[enrollment.course_id] = {
                    'status': status,
                    'days_until_deadline': days_until_deadline,
                    'should_display': should_display,
                }

    if recent_verification_datetime:
        for key, value in iteritems(status_by_course):  # pylint: disable=unused-variable
            status_by_course[key]['verification_good_until'] = recent_verification_datetime.strftime("%m/%d/%Y")

    return status_by_course
Exemplo n.º 13
0
def _required_verification_missing(course_key, user):
    """
    Return true if IDV is required for this course and the user does not have it
    """
    return not is_integrity_signature_enabled(
        course_key) and not IDVerificationService.user_is_verified(user)
Exemplo n.º 14
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)
Exemplo n.º 15
0
    def add_cert(self, student, course_id, course=None, 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 has allow_certificate set to False in the
        userprofile table the status will change to 'restricted'

        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(
                (
                    u"Cannot create certificate generation task for user %s "
                    u"in the course '%s'; "
                    u"certificates are not allowed for CCX courses."
                ),
                student.id,
                unicode(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(
                (
                    u"Cannot create certificate generation task for user %s "
                    u"in the course '%s'; "
                    u"the certificate status '%s' is not one of %s."
                ),
                student.id,
                unicode(course_id),
                cert_status,
                unicode(valid_statuses)
            )
            return None

        # The caller can optionally pass a course in to avoid
        # re-fetching it from Mongo. If they have not provided one,
        # get it from the modulestore.
        if course is None:
            course = modulestore().get_course(course_id, depth=0)

        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)
        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 = is_whitelisted or CourseMode.is_eligible_for_certificate(enrollment_mode)
        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(
            (
                u"Certificate generated for student %s in the course: %s with template: %s. "
                u"given template: %s, "
                u"user is verified: %s, "
                u"mode is verified: %s,"
                u"generate_pdf is: %s"
            ),
            student.username,
            unicode(course_id),
            template_pdf,
            template_file,
            user_is_verified,
            mode_is_verified,
            generate_pdf
        )

        cert, created = 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
        try:
            grade_contents = lxml.html.fromstring(grade_contents).text_content()
            passing = True
        except (TypeError, XMLSyntaxError, ParserError) as exc:
            LOGGER.info(
                (
                    u"Could not retrieve grade for student %s "
                    u"in the course '%s' "
                    u"because an exception occurred while parsing the "
                    u"grade contents '%s' as HTML. "
                    u"The exception was: '%s'"
                ),
                student.id,
                unicode(course_id),
                grade_contents,
                unicode(exc)
            )

            # Log if the student is whitelisted
            if is_whitelisted:
                LOGGER.info(
                    u"Student %s is whitelisted in '%s'",
                    student.id,
                    unicode(course_id)
                )
                passing = True
            else:
                passing = False

        # 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(
                u"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(
                (
                    u"Student %s does not have a grade for '%s', "
                    u"so their certificate status has been set to '%s'. "
                    u"No certificate generation task was sent to the XQueue."
                ),
                student.id,
                unicode(course_id),
                cert.status
            )
            return cert

        # Check to see whether the student is on the the embargoed
        # country restricted list. If so, they should not receive a
        # certificate -- set their status to restricted and log it.
        if self.restricted.filter(user=student).exists():
            cert.status = status.restricted
            cert.save()

            LOGGER.info(
                (
                    u"Student %s is in the embargoed country restricted "
                    u"list, so their certificate status has been set to '%s' "
                    u"for the course '%s'. "
                    u"No certificate generation task was sent to the XQueue."
                ),
                student.id,
                cert.status,
                unicode(course_id)
            )
            return cert

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

        # Finally, generate the certificate and send it off.
        return self._generate_cert(cert, course, student, grade_contents, template_pdf, generate_pdf)
Exemplo n.º 16
0
def check_verify_status_by_course(user, course_enrollments):
    """
    Determine the per-course verification statuses for a given user.

    The possible statuses are:
        * VERIFY_STATUS_NEED_TO_VERIFY: The student has not yet submitted photos for verification.
        * VERIFY_STATUS_SUBMITTED: The student has submitted photos for verification,
          but has have not yet been approved.
        * VERIFY_STATUS_RESUBMITTED: The student has re-submitted photos for re-verification while
          they still have an active but expiring ID verification
        * VERIFY_STATUS_APPROVED: The student has been successfully verified.
        * VERIFY_STATUS_MISSED_DEADLINE: The student did not submit photos within the course's deadline.
        * VERIFY_STATUS_NEED_TO_REVERIFY: The student has an active verification, but it is
            set to expire before the verification deadline for the course.

    It is is also possible that a course does NOT have a verification status if:
        * The user is not enrolled in a verified mode, meaning that the user didn't pay.
        * The course does not offer a verified mode.
        * The user submitted photos but an error occurred while verifying them.
        * The user submitted photos but the verification was denied.

    In the last two cases, we rely on messages in the sidebar rather than displaying
    messages for each course.

    Arguments:
        user (User): The currently logged-in user.
        course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in.

    Returns:
        dict: Mapping of course keys verification status dictionaries.
            If no verification status is applicable to a course, it will not
            be included in the dictionary.
            The dictionaries have these keys:
                * status (str): One of the enumerated status codes.
                * days_until_deadline (int): Number of days until the verification deadline.
                * verification_good_until (str): Date string for the verification expiration date.

    """
    status_by_course = {}

    # Retrieve all verifications for the user, sorted in descending
    # order by submission datetime
    verifications = IDVerificationService.verifications_for_user(user)

    # Check whether the user has an active or pending verification attempt
    has_active_or_pending = IDVerificationService.user_has_valid_or_pending(user)

    # Retrieve expiration_datetime of most recent approved verification
    expiration_datetime = IDVerificationService.get_expiration_datetime(user, ['approved'])
    verification_expiring_soon = is_verification_expiring_soon(expiration_datetime)

    # Retrieve verification deadlines for the enrolled courses
    enrolled_course_keys = [enrollment.course_id for enrollment in course_enrollments]
    course_deadlines = VerificationDeadline.deadlines_for_courses(enrolled_course_keys)

    recent_verification_datetime = None

    for enrollment in course_enrollments:

        # If the user hasn't enrolled as verified, then the course
        # won't display state related to its verification status.
        if enrollment.mode in CourseMode.VERIFIED_MODES:

            # Retrieve the verification deadline associated with the course.
            # This could be None if the course doesn't have a deadline.
            deadline = course_deadlines.get(enrollment.course_id)

            relevant_verification = verification_for_datetime(deadline, verifications)

            # Picking the max verification datetime on each iteration only with approved status
            if relevant_verification is not None and relevant_verification.status == "approved":
                recent_verification_datetime = max(
                    recent_verification_datetime if recent_verification_datetime is not None
                    else relevant_verification.expiration_datetime,
                    relevant_verification.expiration_datetime
                )

            # By default, don't show any status related to verification
            status = None
            should_display = True

            # Check whether the user was approved or is awaiting approval
            if relevant_verification is not None:
                should_display = relevant_verification.should_display_status_to_user()

                if relevant_verification.status == "approved":
                    if verification_expiring_soon:
                        status = VERIFY_STATUS_NEED_TO_REVERIFY
                    else:
                        status = VERIFY_STATUS_APPROVED
                elif relevant_verification.status == "submitted":
                    if verification_expiring_soon:
                        status = VERIFY_STATUS_RESUBMITTED
                    else:
                        status = VERIFY_STATUS_SUBMITTED

            # If the user didn't submit at all, then tell them they need to verify
            # If the deadline has already passed, then tell them they missed it.
            # If they submitted but something went wrong (error or denied),
            # then don't show any messaging next to the course, since we already
            # show messages related to this on the left sidebar.
            submitted = (
                relevant_verification is not None and
                relevant_verification.status not in ["created", "ready"]
            )
            if status is None and not submitted:
                if deadline is None or deadline > datetime.now(UTC):
                    if IDVerificationService.user_is_verified(user) and verification_expiring_soon:
                        # The user has an active verification, but the verification
                        # is set to expire within "EXPIRING_SOON_WINDOW" days (default is 4 weeks).
                        # Tell the student to reverify.
                        status = VERIFY_STATUS_NEED_TO_REVERIFY
                    elif not IDVerificationService.user_is_verified(user):
                        status = VERIFY_STATUS_NEED_TO_VERIFY
                else:
                    # If a user currently has an active or pending verification,
                    # then they may have submitted an additional attempt after
                    # the verification deadline passed.  This can occur,
                    # for example, when the support team asks a student
                    # to reverify after the deadline so they can receive
                    # a verified certificate.
                    # In this case, we still want to show them as "verified"
                    # on the dashboard.
                    if has_active_or_pending:
                        status = VERIFY_STATUS_APPROVED

                    # Otherwise, the student missed the deadline, so show
                    # them as "honor" (the kind of certificate they will receive).
                    else:
                        status = VERIFY_STATUS_MISSED_DEADLINE

            # Set the status for the course only if we're displaying some kind of message
            # Otherwise, leave the course out of the dictionary.
            if status is not None:
                days_until_deadline = None

                now = datetime.now(UTC)
                if deadline is not None and deadline > now:
                    days_until_deadline = (deadline - now).days

                status_by_course[enrollment.course_id] = {
                    'status': status,
                    'days_until_deadline': days_until_deadline,
                    'should_display': should_display,
                }

    if recent_verification_datetime:
        for key, value in iteritems(status_by_course):  # pylint: disable=unused-variable
            status_by_course[key]['verification_good_until'] = recent_verification_datetime.strftime("%m/%d/%Y")

    return status_by_course
Exemplo n.º 17
0
def _can_generate_v2_certificate(user, course_key):
    """
    Check if a v2 course certificate can be generated (created if it doesn't already exist, or updated if it does
    exist) for this user, in this course run.
    """
    if not _is_using_v2_course_certificates(course_key):
        # This course run is not using the v2 course certificate feature
        log.info(
            f'{course_key} is not using v2 course certificates. Certificate cannot be generated.'
        )
        return False

    if not auto_certificate_generation_enabled():
        # Automatic certificate generation is globally disabled
        log.info(
            f'Automatic certificate generation is globally disabled. Certificate cannot be generated for '
            f'{user.id} : {course_key}.')
        return False

    if CertificateInvalidation.has_certificate_invalidation(user, course_key):
        # The invalidation list prevents certificate generation
        log.info(
            f'{user.id} : {course_key} is on the certificate invalidation list. Certificate cannot be generated.'
        )
        return False

    enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(
        user, course_key)
    if enrollment_mode is None:
        log.info(
            f'{user.id} : {course_key} does not have an enrollment. Certificate cannot be generated.'
        )
        return False

    if not modes_api.is_eligible_for_certificate(enrollment_mode):
        log.info(
            f'{user.id} : {course_key} has an enrollment mode of {enrollment_mode}, which is not eligible for an '
            f'allowlist certificate. Certificate cannot be generated.')
        return False

    if not IDVerificationService.user_is_verified(user):
        log.info(
            f'{user.id} does not have a verified id. Certificate cannot be generated for {course_key}.'
        )
        return False

    if _is_ccx_course(course_key):
        log.info(
            f'{course_key} is a CCX course. Certificate cannot be generated for {user.id}.'
        )
        return False

    course = modulestore().get_course(course_key, depth=0)
    if _is_beta_tester(user, course):
        log.info(
            f'{user.id} is a beta tester in {course_key}. Certificate cannot be generated.'
        )
        return False

    if not _has_passing_grade(user, course):
        log.info(
            f'{user.id} does not have a passing grade in {course_key}. Certificate cannot be generated.'
        )
        return False

    if not _can_generate_certificate_for_status(user, course_key):
        return False

    log.info(f'V2 certificate can be generated for {user.id} : {course_key}')
    return True