Example #1
0
    def __init__(self, request=None):
        # MakoMiddleware Note:
        # Line below has the side-effect of writing to a module level lookup
        # table that will allow problems to render themselves. If this is not
        # present, problems that a student hasn't seen will error when loading,
        # causing the grading system to under-count the possible score and
        # inflate their grade. This dependency is bad and was probably recently
        # introduced. This is the bandage until we can trace the root cause.
        m = MakoMiddleware()

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
            requests_auth = HTTPBasicAuth(
                *settings.XQUEUE_INTERFACE['basic_auth'])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get('/')
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE['url'],
            settings.XQUEUE_INTERFACE['django_auth'],
            requests_auth,
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
Example #2
0
    def __init__(self, request=None):

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
            requests_auth = HTTPBasicAuth(
                *settings.XQUEUE_INTERFACE['basic_auth'])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get('/')
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE['url'],
            settings.XQUEUE_INTERFACE['django_auth'],
            requests_auth,
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
        self.use_https = True
Example #3
0
    def __init__(self, request=None):
        # MakoMiddleware Note:
        # Line below has the side-effect of writing to a module level lookup
        # table that will allow problems to render themselves. If this is not
        # present, problems that a student hasn't seen will error when loading,
        # causing the grading system to under-count the possible score and
        # inflate their grade. This dependency is bad and was probably recently
        # introduced. This is the bandage until we can trace the root cause.
        m = MakoMiddleware()

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
            requests_auth = HTTPBasicAuth(
                    *settings.XQUEUE_INTERFACE['basic_auth'])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get('/')
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
                settings.XQUEUE_INTERFACE['url'],
                settings.XQUEUE_INTERFACE['django_auth'],
                requests_auth,
                )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
Example #4
0
class XQueueCertInterface(object):
    """
    XQueueCertificateInterface provides an
    interface to the xqueue server for
    managing student certificates.

    Instantiating an object will create a new
    connection to the queue server.

    See models.py for valid state transitions,
    summary of methods:

       add_cert:   Add a new certificate.  Puts a single
                   request on the queue for the student/course.
                   Once the certificate is generated a post
                   will be made to the update_certificate
                   view which will save the certificate
                   download URL.

       regen_cert: Regenerate an existing certificate.
                   For a user that already has a certificate
                   this will delete the existing one and
                   generate a new cert.


       del_cert:   Delete an existing certificate
                   For a user that already has a certificate
                   this will delete his cert.

    """

    def __init__(self, request=None):

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
            requests_auth = HTTPBasicAuth(
                *settings.XQUEUE_INTERFACE['basic_auth'])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get('/')
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE['url'],
            settings.XQUEUE_INTERFACE['django_auth'],
            requests_auth,
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)

    def regen_cert(self, student, course_id):
        """
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Removes certificate for a student, will change
        the certificate status to 'regenerating'.

        Certificate must be in the 'error' or 'downloadable' state

        If the student has a passing grade a certificate
        request will be put on the queue

        If the student is not passing his state will change
        to status.notpassing

        otherwise it will return the current state

        """

        raise NotImplementedError

    def del_cert(self, student, course_id):

        """
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Removes certificate for a student, will change
        the certificate status to 'deleting'.

        Certificate must be in the 'error' or 'downloadable' state
        otherwise it will return the current state

        """

        raise NotImplementedError

    def add_cert(self, student, course_id, course=None):
        """

        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Request a new certificate for a student.
        Will change the certificate status to 'generating'.

        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 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 student's status

        """

        VALID_STATUSES = [status.generating,
                          status.unavailable, status.deleted, status.error,
                          status.notpassing]

        cert_status = certificate_status_for_student(
            student, course_id)['status']

        if cert_status in VALID_STATUSES:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = courses.get_course_by_id(course_id)
            profile = UserProfile.objects.get(user=student)

            cert, created = GeneratedCertificate.objects.get_or_create(
                user=student, course_id=course_id)

            grade = grades.grade(student, self.request, course)
            is_whitelisted = self.whitelist.filter(
                user=student, course_id=course_id, whitelist=True).exists()

            if is_whitelisted or grade['grade'] is not None:

                key = make_hashkey(random.random())

                cert.grade = grade['percent']
                cert.user = student
                cert.course_id = course_id
                cert.key = key
                cert.name = profile.name

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue
                if self.restricted.filter(user=student).exists():
                    cert.status = status.restricted
                else:
                    contents = {
                        'action': 'create',
                        'username': student.username,
                        'course_id': course_id,
                        'name': profile.name,
                    }
                    cert.status = status.generating
                    self._send_to_xqueue(contents, key)
                cert.save()
            else:
                cert_status = status.notpassing
                cert.grade = grade['percent']
                cert.status = cert_status
                cert.user = student
                cert.course_id = course_id
                cert.name = profile.name
                cert.save()

        return cert_status

    def _send_to_xqueue(self, contents, key):

        xheader = make_xheader(
            'https://{0}/update_certificate?{1}'.format(
                settings.SITE_NAME, key), key, settings.CERT_QUEUE)

        (error, msg) = self.xqueue_interface.send_to_queue(
            header=xheader, body=json.dumps(contents))
        if error:
            logger.critical('Unable to add a request to the queue')
            raise Exception('Unable to send queue message')
Example #5
0
class XQueueCertInterface(object):
    """
    XQueueCertificateInterface provides an
    interface to the xqueue server for
    managing student certificates.

    Instantiating an object will create a new
    connection to the queue server.

    See models.py for valid state transitions,
    summary of methods:

       add_cert:   Add a new certificate.  Puts a single
                   request on the queue for the student/course.
                   Once the certificate is generated a post
                   will be made to the update_certificate
                   view which will save the certificate
                   download URL.

       regen_cert: Regenerate an existing certificate.
                   For a user that already has a certificate
                   this will delete the existing one and
                   generate a new cert.


       del_cert:   Delete an existing certificate
                   For a user that already has a certificate
                   this will delete his cert.

    """

    def __init__(self, request=None):

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
            requests_auth = HTTPBasicAuth(
                *settings.XQUEUE_INTERFACE['basic_auth'])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get('/')
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE['url'],
            settings.XQUEUE_INTERFACE['django_auth'],
            requests_auth,
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
        self.use_https = True

    def regen_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, generate_pdf=True):
        """(Re-)Make certificate for a particular student in a particular course

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (string)

        WARNING: this command will leave the old certificate, if one exists,
                 laying around in AWS taking up space. If this is a problem,
                 take pains to clean up storage before running this command.

        Change the certificate status to unavailable (if it exists) and request
        grading. Passing grades will put a certificate request on the queue.

        Return the certificate.
        """
        # TODO: when del_cert is implemented and plumbed through certificates
        #       repo also, do a deletion followed by a creation r/t a simple
        #       recreation. XXX: this leaves orphan cert files laying around in
        #       AWS. See note in the docstring too.
        try:
            certificate = GeneratedCertificate.eligible_certificates.get(user=student, course_id=course_id)

            LOGGER.info(
                (
                    u"Found an existing certificate entry for student %s "
                    u"in course '%s' "
                    u"with status '%s' while regenerating certificates. "
                ),
                student.id,
                unicode(course_id),
                certificate.status
            )

            certificate.status = status.unavailable
            certificate.save()

            LOGGER.info(
                (
                    u"The certificate status for student %s "
                    u"in course '%s' has been changed to '%s'."
                ),
                student.id,
                unicode(course_id),
                certificate.status
            )

        except GeneratedCertificate.DoesNotExist:
            pass

        return self.add_cert(
            student,
            course_id,
            course=course,
            forced_grade=forced_grade,
            template_file=template_file,
            generate_pdf=generate_pdf
        )

    def del_cert(self, student, course_id):

        """
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Removes certificate for a student, will change
        the certificate status to 'deleting'.

        Certificate must be in the 'error' or 'downloadable' state
        otherwise it will return the current state

        """

        raise NotImplementedError

    # pylint: disable=too-many-statements
    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,
        ]

        cert_status = certificate_status_for_student(student, course_id)['status']
        cert = 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().create(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 = SoftwareSecurePhotoVerification.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 == CourseMode.CREDIT_MODE:
            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"
            ),
            student.username,
            unicode(course_id),
            template_pdf,
            template_file,
            user_is_verified,
            mode_is_verified
        )

        cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id)  # pylint: disable=no-member

        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 = CertificateStatuses.audit_passing if passing else CertificateStatuses.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)

    def _generate_cert(self, cert, course, student, grade_contents, template_pdf, generate_pdf):
        """
        Generate a certificate for the student. If `generate_pdf` is True,
        sends a request to XQueue.
        """
        course_id = unicode(course.id)

        key = make_hashkey(random.random())
        cert.key = key
        contents = {
            'action': 'create',
            'username': student.username,
            'course_id': course_id,
            'course_name': course.display_name or course_id,
            'name': cert.name,
            'grade': grade_contents,
            'template_pdf': template_pdf,
        }
        if generate_pdf:
            cert.status = status.generating
        else:
            cert.status = status.downloadable
            cert.verify_uuid = uuid4().hex

        cert.save()

        if generate_pdf:
            try:
                self._send_to_xqueue(contents, key)
            except XQueueAddToQueueError as exc:
                cert.status = ExampleCertificate.STATUS_ERROR
                cert.error_reason = unicode(exc)
                cert.save()
                LOGGER.critical(
                    (
                        u"Could not add certificate task to XQueue.  "
                        u"The course was '%s' and the student was '%s'."
                        u"The certificate task status has been marked as 'error' "
                        u"and can be re-submitted with a management command."
                    ), course_id, student.id
                )
            else:
                LOGGER.info(
                    (
                        u"The certificate status has been set to '%s'.  "
                        u"Sent a certificate grading task to the XQueue "
                        u"with the key '%s'. "
                    ),
                    cert.status,
                    key
                )
        return cert

    def add_example_cert(self, example_cert):
        """Add a task to create an example certificate.

        Unlike other certificates, an example certificate is
        not associated with any particular user and is never
        shown to students.

        If an error occurs when adding the example certificate
        to the queue, the example certificate status
        will be set to "error".

        Arguments:
            example_cert (ExampleCertificate)

        """
        contents = {
            'action': 'create',
            'course_id': unicode(example_cert.course_key),
            'name': example_cert.full_name,
            'template_pdf': example_cert.template,

            # Example certificates are not associated with a particular user.
            # However, we still need to find the example certificate when
            # we receive a response from the queue.  For this reason,
            # we use the example certificate's unique identifier as a username.
            # Note that the username is *not* displayed on the certificate;
            # it is used only to identify the certificate task in the queue.
            'username': example_cert.uuid,

            # We send this extra parameter to differentiate
            # example certificates from other certificates.
            # This is not used by the certificates workers or XQueue.
            'example_certificate': True,
        }

        # The callback for example certificates is different than the callback
        # for other certificates.  Although both tasks use the same queue,
        # we can distinguish whether the certificate was an example cert based
        # on which end-point XQueue uses once the task completes.
        callback_url_path = reverse('certificates.views.update_example_certificate')

        try:
            self._send_to_xqueue(
                contents,
                example_cert.access_key,
                task_identifier=example_cert.uuid,
                callback_url_path=callback_url_path
            )
            LOGGER.info(u"Started generating example certificates for course '%s'.", example_cert.course_key)
        except XQueueAddToQueueError as exc:
            example_cert.update_status(
                ExampleCertificate.STATUS_ERROR,
                error_reason=unicode(exc)
            )
            LOGGER.critical(
                (
                    u"Could not add example certificate with uuid '%s' to XQueue.  "
                    u"The exception was %s.  "
                    u"The example certificate has been marked with status 'error'."
                ), example_cert.uuid, unicode(exc)
            )

    def _send_to_xqueue(self, contents, key, task_identifier=None, callback_url_path='/update_certificate'):
        """Create a new task on the XQueue.

        Arguments:
            contents (dict): The contents of the XQueue task.
            key (str): An access key for the task.  This will be sent
                to the callback end-point once the task completes,
                so that we can validate that the sender is the same
                entity that received the task.

        Keyword Arguments:
            callback_url_path (str): The path of the callback URL.
                If not provided, use the default end-point for student-generated
                certificates.

        """
        callback_url = u'{protocol}://{base_url}{path}'.format(
            protocol=("https" if self.use_https else "http"),
            base_url=settings.SITE_NAME,
            path=callback_url_path
        )

        # Append the key to the URL
        # This is necessary because XQueue assumes that only one
        # submission is active for a particular URL.
        # If it receives a second submission with the same callback URL,
        # it "retires" any other submission with the same URL.
        # This was a hack that depended on the URL containing the user ID
        # and courseware location; an assumption that does not apply
        # to certificate generation.
        # XQueue also truncates the callback URL to 128 characters,
        # but since our key lengths are shorter than that, this should
        # not affect us.
        callback_url += "?key={key}".format(
            key=(
                task_identifier
                if task_identifier is not None
                else key
            )
        )

        xheader = make_xheader(callback_url, key, settings.CERT_QUEUE)

        (error, msg) = self.xqueue_interface.send_to_queue(
            header=xheader, body=json.dumps(contents))
        if error:
            exc = XQueueAddToQueueError(error, msg)
            LOGGER.critical(unicode(exc))
            raise exc
Example #6
0
class XQueueCertInterface(object):
    """
    XQueueCertificateInterface provides an
    interface to the xqueue server for
    managing student certificates.

    Instantiating an object will create a new
    connection to the queue server.

    See models.py for valid state transitions,
    summary of methods:

       add_cert:   Add a new certificate.  Puts a single
                   request on the queue for the student/course.
                   Once the certificate is generated a post
                   will be made to the update_certificate
                   view which will save the certificate
                   download URL.

       regen_cert: Regenerate an existing certificate.
                   For a user that already has a certificate
                   this will delete the existing one and
                   generate a new cert.


       del_cert:   Delete an existing certificate
                   For a user that already has a certificate
                   this will delete his cert.

    """

    def __init__(self, request=None):

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
            requests_auth = HTTPBasicAuth(
                *settings.XQUEUE_INTERFACE['basic_auth'])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get('/')
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE['url'],
            settings.XQUEUE_INTERFACE['django_auth'],
            requests_auth,
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
        self.use_https = True

    def regen_cert(self, student, course_id, course=None):
        """(Re-)Make certificate for a particular student in a particular course

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (string)

        WARNING: this command will leave the old certificate, if one exists,
                 laying around in AWS taking up space. If this is a problem,
                 take pains to clean up storage before running this command.

        Change the certificate status to unavailable (if it exists) and request
        grading. Passing grades will put a certificate request on the queue.

        Return the status object.
        """
        # TODO: when del_cert is implemented and plumbed through certificates
        #       repo also, do a deletion followed by a creation r/t a simple
        #       recreation. XXX: this leaves orphan cert files laying around in
        #       AWS. See note in the docstring too.
        try:
            certificate = GeneratedCertificate.objects.get(user=student, course_id=course_id)
            certificate.status = status.unavailable
            certificate.save()
        except GeneratedCertificate.DoesNotExist:
            pass

        return self.add_cert(student, course_id, course)

    def del_cert(self, student, course_id):

        """
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Removes certificate for a student, will change
        the certificate status to 'deleting'.

        Certificate must be in the 'error' or 'downloadable' state
        otherwise it will return the current state

        """

        raise NotImplementedError

    def add_cert(self, student, course_id, course=None):
        """

        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Request a new certificate for a student.
        Will change the certificate status to 'generating'.

        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 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 student's status

        """

        VALID_STATUSES = [status.generating,
                          status.unavailable,
                          status.deleted,
                          status.error,
                          status.notpassing]

        cert_status = certificate_status_for_student(student, course_id)['status']

        new_status = cert_status

        if cert_status in VALID_STATUSES:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = courses.get_course_by_id(course_id)
            profile = UserProfile.objects.get(user=student)

            # Needed
            self.request.user = student
            self.request.session = {}

            grade = grades.grade(student, self.request, course)
            is_whitelisted = self.whitelist.filter(
                user=student, course_id=course_id, whitelist=True).exists()
            enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)
            mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified)
            user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
            user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student)
            org = course_id.split('/')[0]
            course_num = course_id.split('/')[1]
            cert_mode = enrollment_mode
            if (mode_is_verified and user_is_verified and user_is_reverified):
                template_pdf = "certificate-template-{0}-{1}-verified.pdf".format(
                    org, course_num)
            elif (mode_is_verified and not (user_is_verified and user_is_reverified)):
                template_pdf = "certificate-template-{0}-{1}.pdf".format(
                    org, course_num)
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                # honor code and audit students
                template_pdf = "certificate-template-{0}-{1}.pdf".format(
                    org, course_num)

            cert, created = GeneratedCertificate.objects.get_or_create(
                user=student, course_id=course_id)

            cert.mode = cert_mode
            cert.user = student
            cert.grade = grade['percent']
            cert.course_id = course_id
            cert.name = profile.name

            if is_whitelisted or grade['grade'] is not None:

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue

                if self.restricted.filter(user=student).exists():
                    new_status = status.restricted
                    cert.status = new_status
                    cert.save()
                else:
                    key = make_hashkey(random.random())
                    cert.key = key
                    contents = {
                        'action': 'create',
                        'username': student.username,
                        'course_id': course_id,
                        'name': profile.name,
                        'grade': grade['grade'],
                        'template_pdf': template_pdf,
                    }
                    new_status = status.generating
                    cert.status = new_status
                    cert.save()
                    self._send_to_xqueue(contents, key)
            else:
                new_status = status.notpassing
                cert.status = new_status
                cert.save()

        return new_status

    def _send_to_xqueue(self, contents, key):

        if self.use_https:
            proto = "https"
        else:
            proto = "http"

        xheader = make_xheader(
            '{0}://{1}/update_certificate?{2}'.format(
                proto, settings.SITE_NAME, key), key, settings.CERT_QUEUE)

        (error, msg) = self.xqueue_interface.send_to_queue(
            header=xheader, body=json.dumps(contents))
        if error:
            logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg))
            raise Exception('Unable to send queue message')
Example #7
0
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.x_module import XModuleDescriptor

from .field_overrides import OverrideFieldData

log = logging.getLogger(__name__)

if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
    REQUESTS_AUTH = HTTPBasicAuth(*settings.XQUEUE_INTERFACE['basic_auth'])
else:
    REQUESTS_AUTH = None

XQUEUE_INTERFACE = XQueueInterface(
    settings.XQUEUE_INTERFACE['url'],
    settings.XQUEUE_INTERFACE['django_auth'],
    REQUESTS_AUTH,
)

# TODO: course_id and course_key are used interchangeably in this file, which is wrong.
# Some brave person should make the variable names consistently someday, but the code's
# coupled enough that it's kind of tricky--you've been warned!


class LmsModuleRenderError(Exception):
    """
    An exception class for exceptions thrown by module_render that don't fit well elsewhere
    """
    pass

Example #8
0
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
from .model_data import LmsKeyValueStore, LmsUsage, ModelDataCache

from xmodule.modulestore.exceptions import ItemNotFoundError
from statsd import statsd

log = logging.getLogger(__name__)

if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
    requests_auth = HTTPBasicAuth(*settings.XQUEUE_INTERFACE['basic_auth'])
else:
    requests_auth = None

xqueue_interface = XQueueInterface(
    settings.XQUEUE_INTERFACE['url'],
    settings.XQUEUE_INTERFACE['django_auth'],
    requests_auth,
)


def make_track_function(request):
    '''
    Make a tracking function that logs what happened.
    For use in ModuleSystem.
    '''
    import track.views

    def f(event_type, event):
        return track.views.server_track(request,
                                        event_type,
                                        event,
Example #9
0
class XQueueCertInterface(object):
    """
    XQueueCertificateInterface provides an
    interface to the xqueue server for
    managing student certificates.

    Instantiating an object will create a new
    connection to the queue server.

    See models.py for valid state transitions,
    summary of methods:

       add_cert:   Add a new certificate.  Puts a single
                   request on the queue for the student/course.
                   Once the certificate is generated a post
                   will be made to the update_certificate
                   view which will save the certificate
                   download URL.

       regen_cert: Regenerate an existing certificate.
                   For a user that already has a certificate
                   this will delete the existing one and
                   generate a new cert.


       del_cert:   Delete an existing certificate
                   For a user that already has a certificate
                   this will delete his cert.

    """

    def __init__(self, request=None):

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
            requests_auth = HTTPBasicAuth(
                *settings.XQUEUE_INTERFACE['basic_auth'])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get('/')
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE['url'],
            settings.XQUEUE_INTERFACE['django_auth'],
            requests_auth,
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
        self.use_https = True

    def regen_cert(self, student, course_id, course=None, forced_grade=None, template_file=None):
        """(Re-)Make certificate for a particular student in a particular course

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (string)

        WARNING: this command will leave the old certificate, if one exists,
                 laying around in AWS taking up space. If this is a problem,
                 take pains to clean up storage before running this command.

        Change the certificate status to unavailable (if it exists) and request
        grading. Passing grades will put a certificate request on the queue.

        Return the status object.
        """
        # TODO: when del_cert is implemented and plumbed through certificates
        #       repo also, do a deletion followed by a creation r/t a simple
        #       recreation. XXX: this leaves orphan cert files laying around in
        #       AWS. See note in the docstring too.
        try:
            certificate = GeneratedCertificate.objects.get(user=student, course_id=course_id)

            LOGGER.info(
                (
                    u"Found an existing certificate entry for student %s "
                    u"in course '%s' "
                    u"with status '%s' while regenerating certificates. "
                ),
                student.id,
                unicode(course_id),
                certificate.status
            )

            certificate.status = status.unavailable
            certificate.save()

            LOGGER.info(
                (
                    u"The certificate status for student %s "
                    u"in course '%s' has been changed to '%s'."
                ),
                student.id,
                unicode(course_id),
                certificate.status
            )

        except GeneratedCertificate.DoesNotExist:
            pass

        return self.add_cert(student, course_id, course, forced_grade, template_file)

    def del_cert(self, student, course_id):

        """
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Removes certificate for a student, will change
        the certificate status to 'deleting'.

        Certificate must be in the 'error' or 'downloadable' state
        otherwise it will return the current state

        """

        raise NotImplementedError

    def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, title='None'):
        """
        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.

        Will change the certificate status to 'generating'.

        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 student's status
        """

        valid_statuses = [
            status.generating,
            status.unavailable,
            status.deleted,
            status.error,
            status.notpassing
        ]

        cert_status = certificate_status_for_student(student, course_id)['status']
        new_status = cert_status

        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)
            )
        else:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = courses.get_course_by_id(course_id)
            profile = UserProfile.objects.get(user=student)
            profile_name = profile.name

            # Needed
            self.request.user = student
            self.request.session = {}

            course_name = course.display_name or course_id.to_deprecated_string()
            is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()
            grade = grades.grade(student, self.request, course)
            enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id)
            mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified)
            user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
            user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student)
            cert_mode = enrollment_mode
            if (mode_is_verified and user_is_verified and user_is_reverified):
                template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id)
            elif (mode_is_verified and not (user_is_verified and user_is_reverified)):
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                # honor code and audit students
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
            if forced_grade:
                grade['grade'] = forced_grade

            cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id)

            cert.mode = cert_mode
            cert.user = student
            cert.grade = grade['percent']
            cert.course_id = course_id
            cert.name = profile_name
            # Strip HTML from grade range label
            grade_contents = grade.get('grade', None)
            try:
                grade_contents = lxml.html.fromstring(grade_contents).text_content()
            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)
                )

                #   Despite blowing up the xml parser, bad values here are fine
                grade_contents = None

            if is_whitelisted or grade_contents is not None:

                if is_whitelisted:
                    LOGGER.info(
                        u"Student %s is whitelisted in '%s'",
                        student.id,
                        unicode(course_id)
                    )

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue

                if self.restricted.filter(user=student).exists():
                    new_status = status.restricted
                    cert.status = new_status
                    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,
                        new_status,
                        unicode(course_id)
                    )
                else:
                    key = make_hashkey(random.random())
                    cert.key = key
                    contents = {
                        'action': 'create',
                        'username': student.username,
                        'course_id': course_id.to_deprecated_string(),
                        'course_name': course_name,
                        'name': profile_name,
                        'grade': grade_contents,
                        'template_pdf': template_pdf,
                    }
                    if template_file:
                        contents['template_pdf'] = template_file
                    new_status = status.generating
                    cert.status = new_status
                    cert.save()
                    self._send_to_xqueue(contents, key)

                    LOGGER.info(
                        (
                            u"The certificate status has been set to '%s'.  "
                            u"Sent a certificate grading task to the XQueue "
                            u"with the key '%s'. "
                        ),
                        key,
                        new_status
                    )
            else:
                cert_status = status.notpassing
                cert.status = cert_status
                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 new_status

    def _send_to_xqueue(self, contents, key):
        """Create a new task on the XQueue. """

        if self.use_https:
            proto = "https"
        else:
            proto = "http"

        xheader = make_xheader(
            '{0}://{1}/update_certificate?{2}'.format(
                proto, settings.SITE_NAME, key), key, settings.CERT_QUEUE)

        (error, msg) = self.xqueue_interface.send_to_queue(
            header=xheader, body=json.dumps(contents))
        if error:
            LOGGER.critical(u'Unable to add a request to the queue: %s %s', unicode(error), msg)
            raise Exception('Unable to send queue message')
Example #10
0
class XQueueCertInterface(object):
    """
    XQueueCertificateInterface provides an
    interface to the xqueue server for
    managing student certificates.

    Instantiating an object will create a new
    connection to the queue server.

    See models.py for valid state transitions,
    summary of methods:

       add_cert:   Add a new certificate.  Puts a single
                   request on the queue for the student/course.
                   Once the certificate is generated a post
                   will be made to the update_certificate
                   view which will save the certificate
                   download URL.

       regen_cert: Regenerate an existing certificate.
                   For a user that already has a certificate
                   this will delete the existing one and
                   generate a new cert.


       del_cert:   Delete an existing certificate
                   For a user that already has a certificate
                   this will delete his cert.

    """

    def __init__(self, request=None):

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get("basic_auth") is not None:
            requests_auth = HTTPBasicAuth(*settings.XQUEUE_INTERFACE["basic_auth"])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get("/")
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE["url"], settings.XQUEUE_INTERFACE["django_auth"], requests_auth
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
        self.use_https = True

    def regen_cert(self, student, course_id, course=None, forced_grade=None, template_file=None):
        """(Re-)Make certificate for a particular student in a particular course

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (string)

        WARNING: this command will leave the old certificate, if one exists,
                 laying around in AWS taking up space. If this is a problem,
                 take pains to clean up storage before running this command.

        Change the certificate status to unavailable (if it exists) and request
        grading. Passing grades will put a certificate request on the queue.

        Return the status object.
        """
        # TODO: when del_cert is implemented and plumbed through certificates
        #       repo also, do a deletion followed by a creation r/t a simple
        #       recreation. XXX: this leaves orphan cert files laying around in
        #       AWS. See note in the docstring too.
        try:
            certificate = GeneratedCertificate.objects.get(user=student, course_id=course_id)

            LOGGER.info(
                (
                    u"Found an existing certificate entry for student %s "
                    u"in course '%s' "
                    u"with status '%s' while regenerating certificates. "
                ),
                student.id,
                unicode(course_id),
                certificate.status,
            )

            certificate.status = status.unavailable
            certificate.save()

            LOGGER.info(
                (u"The certificate status for student %s " u"in course '%s' has been changed to '%s'."),
                student.id,
                unicode(course_id),
                certificate.status,
            )

        except GeneratedCertificate.DoesNotExist:
            pass

        return self.add_cert(student, course_id, course, forced_grade, template_file)

    def del_cert(self, student, course_id):

        """
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Removes certificate for a student, will change
        the certificate status to 'deleting'.

        Certificate must be in the 'error' or 'downloadable' state
        otherwise it will return the current state

        """

        raise NotImplementedError

    def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, title="None"):
        """
        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.

        Will change the certificate status to 'generating'.

        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 student's status
        """

        valid_statuses = [
            status.generating,
            status.unavailable,
            status.deleted,
            status.error,
            status.notpassing,
            status.downloadable,
        ]

        cert_status = certificate_status_for_student(student, course_id)["status"]
        new_status = cert_status

        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),
            )
        else:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = modulestore().get_course(course_id, depth=0)
            profile = UserProfile.objects.get(user=student)
            profile_name = profile.name

            # Needed
            self.request.user = student
            self.request.session = {}

            course_name = course.display_name or unicode(course_id)
            is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()
            grade = grades.grade(student, self.request, course)
            enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id)
            mode_is_verified = enrollment_mode == GeneratedCertificate.MODES.verified
            user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
            user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student)
            cert_mode = enrollment_mode
            if mode_is_verified and user_is_verified and user_is_reverified:
                template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id)
            elif mode_is_verified and not (user_is_verified and user_is_reverified):
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                # honor code and audit students
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
            if forced_grade:
                grade["grade"] = forced_grade

            cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id)

            if not created:
                LOGGER.info(
                    u"Regenerate certificate for user %s in course %s "
                    u"with status %s, download_uuid %s, "
                    u"and download_url %s",
                    cert.user.id,
                    unicode(cert.course_id),
                    cert.status,
                    cert.download_uuid,
                    cert.download_url,
                )

            cert.mode = cert_mode
            cert.user = student
            cert.grade = grade["percent"]
            cert.course_id = course_id
            cert.name = profile_name
            cert.download_url = ""
            # Strip HTML from grade range label
            grade_contents = grade.get("grade", None)
            try:
                grade_contents = lxml.html.fromstring(grade_contents).text_content()
            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),
                )

                #   Despite blowing up the xml parser, bad values here are fine
                grade_contents = None

            if is_whitelisted or grade_contents is not None:

                if is_whitelisted:
                    LOGGER.info(u"Student %s is whitelisted in '%s'", student.id, unicode(course_id))

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue

                if self.restricted.filter(user=student).exists():
                    new_status = status.restricted
                    cert.status = new_status
                    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,
                        new_status,
                        unicode(course_id),
                    )
                else:
                    key = make_hashkey(random.random())
                    cert.key = key
                    contents = {
                        "action": "create",
                        "username": student.username,
                        "course_id": unicode(course_id),
                        "course_name": course_name,
                        "name": profile_name,
                        "grade": grade_contents,
                        "template_pdf": template_pdf,
                    }
                    if template_file:
                        contents["template_pdf"] = template_file
                    new_status = status.generating
                    cert.status = new_status
                    cert.save()

                    try:
                        self._send_to_xqueue(contents, key)
                    except XQueueAddToQueueError as exc:
                        new_status = ExampleCertificate.STATUS_ERROR
                        cert.status = new_status
                        cert.error_reason = unicode(exc)
                        cert.save()
                        LOGGER.critical(
                            (
                                u"Could not add certificate task to XQueue.  "
                                u"The course was '%s' and the student was '%s'."
                                u"The certificate task status has been marked as 'error' "
                                u"and can be re-submitted with a management command."
                            ),
                            student.id,
                            course_id,
                        )
                    else:
                        LOGGER.info(
                            (
                                u"The certificate status has been set to '%s'.  "
                                u"Sent a certificate grading task to the XQueue "
                                u"with the key '%s'. "
                            ),
                            key,
                            new_status,
                        )
            else:
                new_status = status.notpassing
                cert.status = new_status
                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),
                    new_status,
                )

        return new_status

    def add_example_cert(self, example_cert):
        """Add a task to create an example certificate.

        Unlike other certificates, an example certificate is
        not associated with any particular user and is never
        shown to students.

        If an error occurs when adding the example certificate
        to the queue, the example certificate status
        will be set to "error".

        Arguments:
            example_cert (ExampleCertificate)

        """
        contents = {
            "action": "create",
            "course_id": unicode(example_cert.course_key),
            "name": example_cert.full_name,
            "template_pdf": example_cert.template,
            # Example certificates are not associated with a particular user.
            # However, we still need to find the example certificate when
            # we receive a response from the queue.  For this reason,
            # we use the example certificate's unique identifier as a username.
            # Note that the username is *not* displayed on the certificate;
            # it is used only to identify the certificate task in the queue.
            "username": example_cert.uuid,
            # We send this extra parameter to differentiate
            # example certificates from other certificates.
            # This is not used by the certificates workers or XQueue.
            "example_certificate": True,
        }

        # The callback for example certificates is different than the callback
        # for other certificates.  Although both tasks use the same queue,
        # we can distinguish whether the certificate was an example cert based
        # on which end-point XQueue uses once the task completes.
        callback_url_path = reverse("certificates.views.update_example_certificate")

        try:
            self._send_to_xqueue(
                contents,
                example_cert.access_key,
                task_identifier=example_cert.uuid,
                callback_url_path=callback_url_path,
            )
            LOGGER.info(u"Started generating example certificates for course '%s'.", example_cert.course_key)
        except XQueueAddToQueueError as exc:
            example_cert.update_status(ExampleCertificate.STATUS_ERROR, error_reason=unicode(exc))
            LOGGER.critical(
                (
                    u"Could not add example certificate with uuid '%s' to XQueue.  "
                    u"The exception was %s.  "
                    u"The example certificate has been marked with status 'error'."
                ),
                example_cert.uuid,
                unicode(exc),
            )

    def _send_to_xqueue(self, contents, key, task_identifier=None, callback_url_path="/update_certificate"):
        """Create a new task on the XQueue.

        Arguments:
            contents (dict): The contents of the XQueue task.
            key (str): An access key for the task.  This will be sent
                to the callback end-point once the task completes,
                so that we can validate that the sender is the same
                entity that received the task.

        Keyword Arguments:
            callback_url_path (str): The path of the callback URL.
                If not provided, use the default end-point for student-generated
                certificates.

        """
        callback_url = u"{protocol}://{base_url}{path}".format(
            protocol=("https" if self.use_https else "http"), base_url=settings.SITE_NAME, path=callback_url_path
        )

        # Append the key to the URL
        # This is necessary because XQueue assumes that only one
        # submission is active for a particular URL.
        # If it receives a second submission with the same callback URL,
        # it "retires" any other submission with the same URL.
        # This was a hack that depended on the URL containing the user ID
        # and courseware location; an assumption that does not apply
        # to certificate generation.
        # XQueue also truncates the callback URL to 128 characters,
        # but since our key lengths are shorter than that, this should
        # not affect us.
        callback_url += "?key={key}".format(key=(task_identifier if task_identifier is not None else key))

        xheader = make_xheader(callback_url, key, settings.CERT_QUEUE)

        (error, msg) = self.xqueue_interface.send_to_queue(header=xheader, body=json.dumps(contents))
        if error:
            exc = XQueueAddToQueueError(error, msg)
            LOGGER.critical(unicode(exc))
            raise exc
Example #11
0
class XQueueCertInterface(object):
    """
    XQueueCertificateInterface provides an
    interface to the xqueue server for
    managing student certificates.

    Instantiating an object will create a new
    connection to the queue server.

    See models.py for valid state transitions,
    summary of methods:

       add_cert:   Add a new certificate.  Puts a single
                   request on the queue for the student/course.
                   Once the certificate is generated a post
                   will be made to the update_certificate
                   view which will save the certificate
                   download URL.

       regen_cert: Regenerate an existing certificate.
                   For a user that already has a certificate
                   this will delete the existing one and
                   generate a new cert.


       del_cert:   Delete an existing certificate
                   For a user that already has a certificate
                   this will delete his cert.

    """

    def __init__(self, request=None):

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get("basic_auth") is not None:
            requests_auth = HTTPBasicAuth(*settings.XQUEUE_INTERFACE["basic_auth"])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get("/")
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE["url"], settings.XQUEUE_INTERFACE["django_auth"], requests_auth
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)

    def regen_cert(self, student, course_id, course=None):
        """(Re-)Make certificate for a particular student in a particular course

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (string)

        WARNING: this command will leave the old certificate, if one exists,
                 laying around in AWS taking up space. If this is a problem, 
                 take pains to clean up storage before running this command.

        Change the certificate status to unavailable (if it exists) and request
        grading. Passing grades will put a certificate request on the queue.

        Return the status object.
        """
        # TODO: when del_cert is implemented and plumbed through certificates
        #       repo also, do a deletion followed by a creation r/t a simple
        #       recreation. XXX: this leaves orphan cert files laying around in
        #       AWS. See note in the docstring too.
        try:
            certificate = GeneratedCertificate.objects.get(user=student, course_id=course_id)
            certificate.status = status.unavailable
            certificate.save()
        except GeneratedCertificate.DoesNotExist:
            pass

        return self.add_cert(student, course_id, course)

    def del_cert(self, student, course_id):

        """
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Removes certificate for a student, will change
        the certificate status to 'deleting'.

        Certificate must be in the 'error' or 'downloadable' state
        otherwise it will return the current state

        """

        raise NotImplementedError

    def add_cert(self, student, course_id, course=None):
        """

        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Request a new certificate for a student.
        Will change the certificate status to 'generating'.

        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 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 student's status

        """

        VALID_STATUSES = [status.generating, status.unavailable, status.deleted, status.error, status.notpassing]

        cert_status = certificate_status_for_student(student, course_id)["status"]

        if cert_status in VALID_STATUSES:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = courses.get_course_by_id(course_id)
            profile = UserProfile.objects.get(user=student)

            cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id)

            # Needed
            self.request.user = student
            self.request.session = {}

            grade = grades.grade(student, self.request, course)
            is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()

            if is_whitelisted or grade["grade"] is not None:

                key = make_hashkey(random.random())

                cert.grade = grade["percent"]
                cert.user = student
                cert.course_id = course_id
                cert.key = key
                cert.name = profile.name

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue
                if self.restricted.filter(user=student).exists():
                    cert.status = status.restricted
                    cert.save()
                else:
                    contents = {
                        "action": "create",
                        "username": student.username,
                        "course_id": course_id,
                        "name": profile.name,
                        "grade": grade["grade"],
                    }
                    cert.status = status.generating
                    cert.save()
                    self._send_to_xqueue(contents, key)
            else:
                cert_status = status.notpassing
                cert.grade = grade["percent"]
                cert.user = student
                cert.course_id = course_id
                cert.name = profile.name
                cert.status = cert_status
                cert.save()

        return cert_status

    def _send_to_xqueue(self, contents, key):

        xheader = make_xheader(
            "https://{0}/update_certificate?{1}".format(settings.SITE_NAME, key), key, settings.CERT_QUEUE
        )

        (error, msg) = self.xqueue_interface.send_to_queue(header=xheader, body=json.dumps(contents))
        if error:
            logger.critical("Unable to add a request to the queue: {} {}".format(error, msg))
            raise Exception("Unable to send queue message")
Example #12
0
class XQueueCertInterface(object):
    """
    XQueueCertificateInterface provides an
    interface to the xqueue server for
    managing student certificates.

    Instantiating an object will create a new
    connection to the queue server.

    See models.py for valid state transitions,
    summary of methods:

       add_cert:   Add a new certificate.  Puts a single
                   request on the queue for the student/course.
                   Once the certificate is generated a post
                   will be made to the update_certificate
                   view which will save the certificate
                   download URL.

       regen_cert: Regenerate an existing certificate.
                   For a user that already has a certificate
                   this will delete the existing one and
                   generate a new cert.


       del_cert:   Delete an existing certificate
                   For a user that already has a certificate
                   this will delete his cert.

    """
    def __init__(self, request=None):

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
            requests_auth = HTTPBasicAuth(
                *settings.XQUEUE_INTERFACE['basic_auth'])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get('/')
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE['url'],
            settings.XQUEUE_INTERFACE['django_auth'],
            requests_auth,
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
        self.use_https = True

    def regen_cert(self,
                   student,
                   course_id,
                   course=None,
                   forced_grade=None,
                   template_file=None):
        """(Re-)Make certificate for a particular student in a particular course

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (string)

        WARNING: this command will leave the old certificate, if one exists,
                 laying around in AWS taking up space. If this is a problem,
                 take pains to clean up storage before running this command.

        Change the certificate status to unavailable (if it exists) and request
        grading. Passing grades will put a certificate request on the queue.

        Return the status object.
        """
        # TODO: when del_cert is implemented and plumbed through certificates
        #       repo also, do a deletion followed by a creation r/t a simple
        #       recreation. XXX: this leaves orphan cert files laying around in
        #       AWS. See note in the docstring too.
        try:
            certificate = GeneratedCertificate.objects.get(user=student,
                                                           course_id=course_id)

            LOGGER.info(
                (u"Found an existing certificate entry for student %s "
                 u"in course '%s' "
                 u"with status '%s' while regenerating certificates. "),
                student.id, unicode(course_id), certificate.status)

            certificate.status = status.unavailable
            certificate.save()

            LOGGER.info((u"The certificate status for student %s "
                         u"in course '%s' has been changed to '%s'."),
                        student.id, unicode(course_id), certificate.status)

        except GeneratedCertificate.DoesNotExist:
            pass

        return self.add_cert(student, course_id, course, forced_grade,
                             template_file)

    def del_cert(self, student, course_id):
        """
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Removes certificate for a student, will change
        the certificate status to 'deleting'.

        Certificate must be in the 'error' or 'downloadable' state
        otherwise it will return the current state

        """

        raise NotImplementedError

    def add_cert(self,
                 student,
                 course_id,
                 course=None,
                 forced_grade=None,
                 template_file=None,
                 title='None'):
        """
        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.

        Will change the certificate status to 'generating'.

        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 student's status
        """

        valid_statuses = [
            status.generating, status.unavailable, status.deleted,
            status.error, status.notpassing
        ]

        cert_status = certificate_status_for_student(student,
                                                     course_id)['status']
        new_status = cert_status

        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))
        else:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = modulestore().get_course(course_id, depth=0)
            profile = UserProfile.objects.get(user=student)
            profile_name = profile.name

            # Needed
            self.request.user = student
            self.request.session = {}

            course_name = course.display_name or unicode(course_id)
            is_whitelisted = self.whitelist.filter(user=student,
                                                   course_id=course_id,
                                                   whitelist=True).exists()
            grade = grades.grade(student, self.request, course)
            enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(
                student, course_id)
            mode_is_verified = (
                enrollment_mode == GeneratedCertificate.MODES.verified)
            user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(
                student)
            user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(
                course_id, student)
            cert_mode = enrollment_mode
            if (mode_is_verified and user_is_verified and user_is_reverified):
                template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(
                    id=course_id)
            elif (mode_is_verified
                  and not (user_is_verified and user_is_reverified)):
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(
                    id=course_id)
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                # honor code and audit students
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(
                    id=course_id)
            if forced_grade:
                grade['grade'] = forced_grade

            cert, __ = GeneratedCertificate.objects.get_or_create(
                user=student, course_id=course_id)

            cert.mode = cert_mode
            cert.user = student
            cert.grade = grade['percent']
            cert.course_id = course_id
            cert.name = profile_name
            # Strip HTML from grade range label
            grade_contents = grade.get('grade', None)
            try:
                grade_contents = lxml.html.fromstring(
                    grade_contents).text_content()
            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))

                #   Despite blowing up the xml parser, bad values here are fine
                grade_contents = None

            if is_whitelisted or grade_contents is not None:

                if is_whitelisted:
                    LOGGER.info(u"Student %s is whitelisted in '%s'",
                                student.id, unicode(course_id))

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue

                if self.restricted.filter(user=student).exists():
                    new_status = status.restricted
                    cert.status = new_status
                    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, new_status, unicode(course_id))
                else:
                    key = make_hashkey(random.random())
                    cert.key = key
                    contents = {
                        'action': 'create',
                        'username': student.username,
                        'course_id': unicode(course_id),
                        'course_name': course_name,
                        'name': profile_name,
                        'grade': grade_contents,
                        'template_pdf': template_pdf,
                    }
                    if template_file:
                        contents['template_pdf'] = template_file
                    new_status = status.generating
                    cert.status = new_status
                    cert.save()
                    self._send_to_xqueue(contents, key)

                    LOGGER.info(
                        (u"The certificate status has been set to '%s'.  "
                         u"Sent a certificate grading task to the XQueue "
                         u"with the key '%s'. "), key, new_status)
            else:
                new_status = status.notpassing
                cert.status = new_status
                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), new_status)

        return new_status

    def add_example_cert(self, example_cert):
        """Add a task to create an example certificate.

        Unlike other certificates, an example certificate is
        not associated with any particular user and is never
        shown to students.

        If an error occurs when adding the example certificate
        to the queue, the example certificate status
        will be set to "error".

        Arguments:
            example_cert (ExampleCertificate)

        """
        contents = {
            'action': 'create',
            'course_id': unicode(example_cert.course_key),
            'name': example_cert.full_name,
            'template_pdf': example_cert.template,

            # Example certificates are not associated with a particular user.
            # However, we still need to find the example certificate when
            # we receive a response from the queue.  For this reason,
            # we use the example certificate's unique identifier as a username.
            # Note that the username is *not* displayed on the certificate;
            # it is used only to identify the certificate task in the queue.
            'username': example_cert.uuid,

            # We send this extra parameter to differentiate
            # example certificates from other certificates.
            # This is not used by the certificates workers or XQueue.
            'example_certificate': True,
        }

        # The callback for example certificates is different than the callback
        # for other certificates.  Although both tasks use the same queue,
        # we can distinguish whether the certificate was an example cert based
        # on which end-point XQueue uses once the task completes.
        callback_url_path = reverse(
            'certificates.views.update_example_certificate')

        try:
            self._send_to_xqueue(contents,
                                 example_cert.access_key,
                                 task_identifier=example_cert.uuid,
                                 callback_url_path=callback_url_path)
        except XQueueAddToQueueError as exc:
            example_cert.update_status(ExampleCertificate.STATUS_ERROR,
                                       error_reason=unicode(exc))

    def _send_to_xqueue(self,
                        contents,
                        key,
                        task_identifier=None,
                        callback_url_path='update_certificate'):
        """Create a new task on the XQueue.

        Arguments:
            contents (dict): The contents of the XQueue task.
            key (str): An access key for the task.  This will be sent
                to the callback end-point once the task completes,
                so that we can validate that the sender is the same
                entity that received the task.

        Keyword Arguments:
            callback_url_path (str): The path of the callback URL.
                If not provided, use the default end-point for student-generated
                certificates.

        """
        callback_url = u'{protocol}://{base_url}{path}'.format(
            protocol=("https" if self.use_https else "http"),
            base_url=settings.SITE_NAME,
            path=callback_url_path)

        # Append the key to the URL
        # This is necessary because XQueue assumes that only one
        # submission is active for a particular URL.
        # If it receives a second submission with the same callback URL,
        # it "retires" any other submission with the same URL.
        # This was a hack that depended on the URL containing the user ID
        # and courseware location; an assumption that does not apply
        # to certificate generation.
        # XQueue also truncates the callback URL to 128 characters,
        # but since our key lengths are shorter than that, this should
        # not affect us.
        callback_url += "?key={key}".format(
            key=(task_identifier if task_identifier is not None else key))

        xheader = make_xheader(callback_url, key, settings.CERT_QUEUE)

        (error,
         msg) = self.xqueue_interface.send_to_queue(header=xheader,
                                                    body=json.dumps(contents))
        if error:
            exc = XQueueAddToQueueError(error, msg)
            LOGGER.critical(unicode(exc))
            raise exc
Example #13
0
class XQueueCertInterface(object):
    """
    XQueueCertificateInterface provides an
    interface to the xqueue server for
    managing student certificates.

    Instantiating an object will create a new
    connection to the queue server.

    See models.py for valid state transitions,
    summary of methods:

       add_cert:   Add a new certificate.  Puts a single
                   request on the queue for the student/course.
                   Once the certificate is generated a post
                   will be made to the update_certificate
                   view which will save the certificate
                   download URL.

       regen_cert: Regenerate an existing certificate.
                   For a user that already has a certificate
                   this will delete the existing one and
                   generate a new cert.


       del_cert:   Delete an existing certificate
                   For a user that already has a certificate
                   this will delete his cert.

    """

    def __init__(self, request=None):

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get("basic_auth") is not None:
            requests_auth = HTTPBasicAuth(*settings.XQUEUE_INTERFACE["basic_auth"])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get("/")
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE["url"], settings.XQUEUE_INTERFACE["django_auth"], requests_auth
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
        self.use_https = True

    def regen_cert(self, student, course_id, course=None, forced_grade=None, template_file=None):
        """(Re-)Make certificate for a particular student in a particular course

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (string)

        WARNING: this command will leave the old certificate, if one exists,
                 laying around in AWS taking up space. If this is a problem,
                 take pains to clean up storage before running this command.

        Change the certificate status to unavailable (if it exists) and request
        grading. Passing grades will put a certificate request on the queue.

        Return the status object.
        """
        # TODO: when del_cert is implemented and plumbed through certificates
        #       repo also, do a deletion followed by a creation r/t a simple
        #       recreation. XXX: this leaves orphan cert files laying around in
        #       AWS. See note in the docstring too.
        try:
            certificate = GeneratedCertificate.objects.get(user=student, course_id=course_id)
            certificate.status = status.unavailable
            certificate.save()
        except GeneratedCertificate.DoesNotExist:
            pass

        return self.add_cert(student, course_id, course, forced_grade, template_file)

    def del_cert(self, student, course_id):

        """
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Removes certificate for a student, will change
        the certificate status to 'deleting'.

        Certificate must be in the 'error' or 'downloadable' state
        otherwise it will return the current state

        """

        raise NotImplementedError

    def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, title="None"):
        """
        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.

        Will change the certificate status to 'generating'.

        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 student's status
        """

        VALID_STATUSES = [status.generating, status.unavailable, status.deleted, status.error, status.notpassing]

        cert_status = certificate_status_for_student(student, course_id)["status"]

        new_status = cert_status

        if cert_status in VALID_STATUSES:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = courses.get_course_by_id(course_id)
            profile = UserProfile.objects.get(user=student)
            profile_name = profile.name

            # Needed
            self.request.user = student
            self.request.session = {}

            course_name = course.display_name or course_id.to_deprecated_string()
            is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()
            grade = grades.grade(student, self.request, course)
            enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id)
            mode_is_verified = enrollment_mode == GeneratedCertificate.MODES.verified
            user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
            user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student)
            cert_mode = enrollment_mode
            if mode_is_verified and user_is_verified and user_is_reverified:
                template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id)
            elif mode_is_verified and not (user_is_verified and user_is_reverified):
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                # honor code and audit students
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
            if forced_grade:
                grade["grade"] = forced_grade

            cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id)

            cert.mode = cert_mode
            cert.user = student
            cert.grade = grade["percent"]
            cert.course_id = course_id
            cert.name = profile_name
            # Strip HTML from grade range label
            grade_contents = grade.get("grade", None)
            try:
                grade_contents = lxml.html.fromstring(grade_contents).text_content()
            except (TypeError, XMLSyntaxError, ParserError) as e:
                #   Despite blowing up the xml parser, bad values here are fine
                grade_contents = None

            if is_whitelisted or grade_contents is not None:

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue

                if self.restricted.filter(user=student).exists():
                    new_status = status.restricted
                    cert.status = new_status
                    cert.save()
                else:
                    key = make_hashkey(random.random())
                    cert.key = key
                    contents = {
                        "action": "create",
                        "username": student.username,
                        "course_id": course_id.to_deprecated_string(),
                        "course_name": course_name,
                        "name": profile_name,
                        "grade": grade_contents,
                        "template_pdf": template_pdf,
                    }
                    if template_file:
                        contents["template_pdf"] = template_file
                    new_status = status.generating
                    cert.status = new_status
                    cert.save()
                    self._send_to_xqueue(contents, key)
            else:
                cert_status = status.notpassing
                cert.status = cert_status
                cert.save()

        return new_status

    def _send_to_xqueue(self, contents, key):

        if self.use_https:
            proto = "https"
        else:
            proto = "http"

        xheader = make_xheader(
            "{0}://{1}/update_certificate?{2}".format(proto, settings.SITE_NAME, key), key, settings.CERT_QUEUE
        )

        (error, msg) = self.xqueue_interface.send_to_queue(header=xheader, body=json.dumps(contents))
        if error:
            logger.critical("Unable to add a request to the queue: {} {}".format(error, msg))
            raise Exception("Unable to send queue message")
Example #14
0
class XQueueCertInterface(object):
    """
    XQueueCertificateInterface provides an
    interface to the xqueue server for
    managing student certificates.

    Instantiating an object will create a new
    connection to the queue server.

    See models.py for valid state transitions,
    summary of methods:

       add_cert:   Add a new certificate.  Puts a single
                   request on the queue for the student/course.
                   Once the certificate is generated a post
                   will be made to the update_certificate
                   view which will save the certificate
                   download URL.

       regen_cert: Regenerate an existing certificate.
                   For a user that already has a certificate
                   this will delete the existing one and
                   generate a new cert.


       del_cert:   Delete an existing certificate
                   For a user that already has a certificate
                   this will delete his cert.

    """
    def __init__(self, request=None):

        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

        if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
            requests_auth = HTTPBasicAuth(
                *settings.XQUEUE_INTERFACE['basic_auth'])
        else:
            requests_auth = None

        if request is None:
            factory = RequestFactory()
            self.request = factory.get('/')
        else:
            self.request = request

        self.xqueue_interface = XQueueInterface(
            settings.XQUEUE_INTERFACE['url'],
            settings.XQUEUE_INTERFACE['django_auth'],
            requests_auth,
        )
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
        self.use_https = True

    def regen_cert(self,
                   student,
                   course_id,
                   course=None,
                   forced_grade=None,
                   template_file=None):
        """(Re-)Make certificate for a particular student in a particular course

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (string)

        WARNING: this command will leave the old certificate, if one exists,
                 laying around in AWS taking up space. If this is a problem,
                 take pains to clean up storage before running this command.

        Change the certificate status to unavailable (if it exists) and request
        grading. Passing grades will put a certificate request on the queue.

        Return the status object.
        """
        # TODO: when del_cert is implemented and plumbed through certificates
        #       repo also, do a deletion followed by a creation r/t a simple
        #       recreation. XXX: this leaves orphan cert files laying around in
        #       AWS. See note in the docstring too.
        try:
            certificate = GeneratedCertificate.objects.get(user=student,
                                                           course_id=course_id)

            LOGGER.info(
                (u"Found an existing certificate entry for student %s "
                 u"in course '%s' "
                 u"with status '%s' while regenerating certificates. "),
                student.id, unicode(course_id), certificate.status)

            certificate.status = status.unavailable
            certificate.save()

            LOGGER.info((u"The certificate status for student %s "
                         u"in course '%s' has been changed to '%s'."),
                        student.id, unicode(course_id), certificate.status)

        except GeneratedCertificate.DoesNotExist:
            pass

        return self.add_cert(student, course_id, course, forced_grade,
                             template_file)

    def del_cert(self, student, course_id):
        """
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Removes certificate for a student, will change
        the certificate status to 'deleting'.

        Certificate must be in the 'error' or 'downloadable' state
        otherwise it will return the current state

        """

        raise NotImplementedError

    def add_cert(self,
                 student,
                 course_id,
                 course=None,
                 forced_grade=None,
                 template_file=None,
                 title='None'):
        """
        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.

        Will change the certificate status to 'generating'.

        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 student's status
        """

        valid_statuses = [
            status.generating, status.unavailable, status.deleted,
            status.error, status.notpassing
        ]

        cert_status = certificate_status_for_student(student,
                                                     course_id)['status']
        new_status = cert_status

        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))
        else:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = courses.get_course_by_id(course_id)
            profile = UserProfile.objects.get(user=student)
            profile_name = profile.name

            # Needed
            self.request.user = student
            self.request.session = {}

            course_name = course.display_name or course_id.to_deprecated_string(
            )
            is_whitelisted = self.whitelist.filter(user=student,
                                                   course_id=course_id,
                                                   whitelist=True).exists()
            grade = grades.grade(student, self.request, course)
            enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(
                student, course_id)
            mode_is_verified = (
                enrollment_mode == GeneratedCertificate.MODES.verified)
            user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(
                student)
            user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(
                course_id, student)
            cert_mode = enrollment_mode
            if (mode_is_verified and user_is_verified and user_is_reverified):
                template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(
                    id=course_id)
            elif (mode_is_verified
                  and not (user_is_verified and user_is_reverified)):
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(
                    id=course_id)
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                # honor code and audit students
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(
                    id=course_id)
            if forced_grade:
                grade['grade'] = forced_grade

            cert, __ = GeneratedCertificate.objects.get_or_create(
                user=student, course_id=course_id)

            cert.mode = cert_mode
            cert.user = student
            cert.grade = grade['percent']
            cert.course_id = course_id
            cert.name = profile_name
            # Strip HTML from grade range label
            grade_contents = grade.get('grade', None)
            try:
                grade_contents = lxml.html.fromstring(
                    grade_contents).text_content()
            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))

                #   Despite blowing up the xml parser, bad values here are fine
                grade_contents = None

            if is_whitelisted or grade_contents is not None:

                if is_whitelisted:
                    LOGGER.info(u"Student %s is whitelisted in '%s'",
                                student.id, unicode(course_id))

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue

                if self.restricted.filter(user=student).exists():
                    new_status = status.restricted
                    cert.status = new_status
                    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, new_status, unicode(course_id))
                else:
                    key = make_hashkey(random.random())
                    cert.key = key
                    contents = {
                        'action': 'create',
                        'username': student.username,
                        'course_id': course_id.to_deprecated_string(),
                        'course_name': course_name,
                        'name': profile_name,
                        'grade': grade_contents,
                        'template_pdf': template_pdf,
                    }
                    if template_file:
                        contents['template_pdf'] = template_file
                    new_status = status.generating
                    cert.status = new_status
                    cert.save()
                    self._send_to_xqueue(contents, key)

                    LOGGER.info(
                        (u"The certificate status has been set to '%s'.  "
                         u"Sent a certificate grading task to the XQueue "
                         u"with the key '%s'. "), key, new_status)
            else:
                cert_status = status.notpassing
                cert.status = cert_status
                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 new_status

    def _send_to_xqueue(self, contents, key):
        """Create a new task on the XQueue. """

        if self.use_https:
            proto = "https"
        else:
            proto = "http"

        xheader = make_xheader(
            '{0}://{1}/update_certificate?{2}'.format(proto,
                                                      settings.SITE_NAME, key),
            key, settings.CERT_QUEUE)

        (error,
         msg) = self.xqueue_interface.send_to_queue(header=xheader,
                                                    body=json.dumps(contents))
        if error:
            LOGGER.critical(u'Unable to add a request to the queue: %s %s',
                            unicode(error), msg)
            raise Exception('Unable to send queue message')