queue.py 10.2 KB
Newer Older
1 2
from certificates.models import GeneratedCertificate
from certificates.models import certificate_status_for_student
3
from certificates.models import CertificateStatuses as status
4
from certificates.models import CertificateWhitelist
5

6 7 8 9 10 11
from courseware import grades, courses
from django.test.client import RequestFactory
from capa.xqueue_interface import XQueueInterface
from capa.xqueue_interface import make_xheader, make_hashkey
from django.conf import settings
from requests.auth import HTTPBasicAuth
12
from student.models import UserProfile, CourseEnrollment
13
from verify_student.models import SoftwareSecurePhotoVerification
14

15 16
import json
import random
17
import logging
18
import lxml.html
19
from lxml.etree import XMLSyntaxError, ParserError
20 21 22


logger = logging.getLogger(__name__)
23 24


John Jarvis committed
25
class XQueueCertInterface(object):
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
    """
    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.

    """
55

56
    def __init__(self, request=None):
57

John Jarvis committed
58 59 60
        # Get basic auth (username/password) for
        # xqueue connection if it's in the settings

61 62
        if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
            requests_auth = HTTPBasicAuth(
63
                *settings.XQUEUE_INTERFACE['basic_auth'])
64 65 66 67 68 69 70 71 72 73
        else:
            requests_auth = None

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

        self.xqueue_interface = XQueueInterface(
74 75 76 77
            settings.XQUEUE_INTERFACE['url'],
            settings.XQUEUE_INTERFACE['django_auth'],
            requests_auth,
        )
78 79
        self.whitelist = CertificateWhitelist.objects.all()
        self.restricted = UserProfile.objects.filter(allow_certificate=False)
80
        self.use_https = True
81

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

85
        Arguments:
86
          student   - User.object
87 88
          course_id - courseenrollment.course_id (string)

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

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

96
        Return the status object.
97
        """
98
        # TODO: when del_cert is implemented and plumbed through certificates
99 100 101 102 103 104 105 106 107 108
        #       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

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

111
    def del_cert(self, student, course_id):
112

113
        """
114 115 116 117 118 119 120
        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

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

121 122
        Certificate must be in the 'error' or 'downloadable' state
        otherwise it will return the current state
123 124 125

        """

126
        raise NotImplementedError
127

128
    def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, title='None'):
129
        """
130
        Request a new certificate for a student.
131 132

        Arguments:
133
          student   - User.object
134
          course_id - courseenrollment.course_id (CourseKey)
135 136 137
          forced_grade - a string indicating a grade parameter to pass with
                         the certificate request. If this is given, grading
                         will be skipped.
138

139
        Will change the certificate status to 'generating'.
140

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

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

        If a student has allow_certificate set to False in the
        userprofile table the status will change to 'restricted'
149 150 151 152 153

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

        Returns the student's status
154 155
        """

Calen Pennington committed
156
        VALID_STATUSES = [status.generating,
157 158
                          status.unavailable,
                          status.deleted,
159 160
                          status.error,
                          status.notpassing]
161

162
        cert_status = certificate_status_for_student(student, course_id)['status']
John Jarvis committed
163

164 165
        new_status = cert_status

166 167
        if cert_status in VALID_STATUSES:
            # grade the student
168

169 170 171 172 173
            # 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)
174
            profile_name = profile.name
175

176 177 178 179
            # Needed
            self.request.user = student
            self.request.session = {}

180
            course_name = course.display_name or course_id.to_deprecated_string()
181
            is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()
182
            grade = grades.grade(student, self.request, course)
183
            enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id)
184 185 186
            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)
187
            cert_mode = enrollment_mode
188
            if (mode_is_verified and user_is_verified and user_is_reverified):
189
                template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id)
190
            elif (mode_is_verified and not (user_is_verified and user_is_reverified)):
191
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
192
                cert_mode = GeneratedCertificate.MODES.honor
193 194
            else:
                # honor code and audit students
195
                template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
196 197
            if forced_grade:
                grade['grade'] = forced_grade
198

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

201
            cert.mode = cert_mode
202 203 204
            cert.user = student
            cert.grade = grade['percent']
            cert.course_id = course_id
205 206 207 208 209 210 211 212
            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
213

214
            if is_whitelisted or grade_contents is not None:
215

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

221
                if self.restricted.filter(user=student).exists():
222 223
                    new_status = status.restricted
                    cert.status = new_status
224
                    cert.save()
225
                else:
226 227
                    key = make_hashkey(random.random())
                    cert.key = key
228 229 230
                    contents = {
                        'action': 'create',
                        'username': student.username,
231
                        'course_id': course_id.to_deprecated_string(),
232
                        'course_name': course_name,
233 234
                        'name': profile_name,
                        'grade': grade_contents,
235
                        'template_pdf': template_pdf,
236
                    }
237 238
                    if template_file:
                        contents['template_pdf'] = template_file
239 240
                    new_status = status.generating
                    cert.status = new_status
241
                    cert.save()
242
                    self._send_to_xqueue(contents, key)
243
            else:
244 245
                cert_status = status.notpassing
                cert.status = cert_status
246
                cert.save()
247

248
        return new_status
249

250
    def _send_to_xqueue(self, contents, key):
251

252 253 254 255 256
        if self.use_https:
            proto = "https"
        else:
            proto = "http"

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

        (error, msg) = self.xqueue_interface.send_to_queue(
262
            header=xheader, body=json.dumps(contents))
263
        if error:
264
            logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg))
265
            raise Exception('Unable to send queue message')