Merge pull request #7163 from edx/will/example-certificates
ECOM-1139: Example certificates.
This commit is contained in:
@@ -1,11 +1,20 @@
|
||||
"""
|
||||
Certificates API
|
||||
"""
|
||||
"""Certificates API
|
||||
|
||||
This is a Python API for generating certificates asynchronously.
|
||||
Other Django apps should use the API functions defined in this module
|
||||
rather than importing Django models directly.
|
||||
"""
|
||||
import logging
|
||||
from certificates.models import CertificateStatuses as cert_status, certificate_status_for_student
|
||||
from certificates.models import (
|
||||
CertificateStatuses as cert_status,
|
||||
certificate_status_for_student,
|
||||
CertificateGenerationCourseSetting,
|
||||
CertificateGenerationConfiguration,
|
||||
ExampleCertificateSet
|
||||
)
|
||||
from certificates.queue import XQueueCertInterface
|
||||
|
||||
|
||||
log = logging.getLogger("edx.certificate")
|
||||
|
||||
|
||||
@@ -63,3 +72,124 @@ def certificate_downloadable_status(student, course_key):
|
||||
response_data['download_url'] = current_status['download_url']
|
||||
|
||||
return response_data
|
||||
|
||||
|
||||
def set_cert_generation_enabled(course_key, is_enabled):
|
||||
"""Enable or disable self-generated certificates for a course.
|
||||
|
||||
There are two "switches" that control whether self-generated certificates
|
||||
are enabled for a course:
|
||||
|
||||
1) Whether the self-generated certificates feature is enabled.
|
||||
2) Whether self-generated certificates have been enabled for this particular course.
|
||||
|
||||
The second flag should be enabled *only* when someone has successfully
|
||||
generated example certificates for the course. This helps avoid
|
||||
configuration errors (for example, not having a template configured
|
||||
for the course installed on the workers). The UI for the instructor
|
||||
dashboard enforces this constraint.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The course identifier.
|
||||
|
||||
Keyword Arguments:
|
||||
is_enabled (boolean): If provided, enable/disable self-generated
|
||||
certificates for this course.
|
||||
|
||||
"""
|
||||
CertificateGenerationCourseSetting.set_enabled_for_course(course_key, is_enabled)
|
||||
|
||||
if is_enabled:
|
||||
log.info(u"Enabled self-generated certificates for course '%s'.", unicode(course_key))
|
||||
else:
|
||||
log.info(u"Disabled self-generated certificates for course '%s'.", unicode(course_key))
|
||||
|
||||
|
||||
def cert_generation_enabled(course_key):
|
||||
"""Check whether certificate generation is enabled for a course.
|
||||
|
||||
There are two "switches" that control whether self-generated certificates
|
||||
are enabled for a course:
|
||||
|
||||
1) Whether the self-generated certificates feature is enabled.
|
||||
2) Whether self-generated certificates have been enabled for this particular course.
|
||||
|
||||
Certificates are enabled for a course only when both switches
|
||||
are set to True.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The course identifier.
|
||||
|
||||
Returns:
|
||||
boolean: Whether self-generated certificates are enabled
|
||||
for the course.
|
||||
|
||||
"""
|
||||
return (
|
||||
CertificateGenerationConfiguration.current().enabled and
|
||||
CertificateGenerationCourseSetting.is_enabled_for_course(course_key)
|
||||
)
|
||||
|
||||
|
||||
def generate_example_certificates(course_key):
|
||||
"""Generate example certificates for a course.
|
||||
|
||||
Example certificates are used to validate that certificates
|
||||
are configured correctly for the course. Staff members can
|
||||
view the example certificates before enabling
|
||||
the self-generated certificates button for students.
|
||||
|
||||
Several example certificates may be generated for a course.
|
||||
For example, if a course offers both verified and honor certificates,
|
||||
examples of both types of certificate will be generated.
|
||||
|
||||
If an error occurs while starting the certificate generation
|
||||
job, the errors will be recorded in the database and
|
||||
can be retrieved using `example_certificate_status()`.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The course identifier.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
xqueue = XQueueCertInterface()
|
||||
for cert in ExampleCertificateSet.create_example_set(course_key):
|
||||
xqueue.add_example_cert(cert)
|
||||
|
||||
log.info(u"Started generated example certificates for course '%s'.", course_key)
|
||||
|
||||
|
||||
def example_certificates_status(course_key):
|
||||
"""Check the status of example certificates for a course.
|
||||
|
||||
This will check the *latest* example certificate task.
|
||||
This is generally what we care about in terms of enabling/disabling
|
||||
self-generated certificates for a course.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The course identifier.
|
||||
|
||||
Returns:
|
||||
list
|
||||
|
||||
Example Usage:
|
||||
|
||||
>>> from certificates import api as certs_api
|
||||
>>> certs_api.example_certificate_status(course_key)
|
||||
[
|
||||
{
|
||||
'description': 'honor',
|
||||
'status': 'success',
|
||||
'download_url': 'http://www.example.com/abcd/honor_cert.pdf'
|
||||
},
|
||||
{
|
||||
'description': 'verified',
|
||||
'status': 'error',
|
||||
'error_reason': 'No template found!'
|
||||
}
|
||||
]
|
||||
|
||||
"""
|
||||
return ExampleCertificateSet.latest_status(course_key)
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'CertificateGenerationCourseSetting'
|
||||
db.create_table('certificates_certificategenerationcoursesetting', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
|
||||
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
|
||||
('course_key', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
|
||||
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
))
|
||||
db.send_create_signal('certificates', ['CertificateGenerationCourseSetting'])
|
||||
|
||||
# Adding model 'ExampleCertificate'
|
||||
db.create_table('certificates_examplecertificate', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
|
||||
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
|
||||
('example_cert_set', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['certificates.ExampleCertificateSet'])),
|
||||
('description', self.gf('django.db.models.fields.CharField')(max_length=255)),
|
||||
('uuid', self.gf('django.db.models.fields.CharField')(default='2017ea093523484cb355fac9c3e7a22b', unique=True, max_length=255, db_index=True)),
|
||||
('access_key', self.gf('django.db.models.fields.CharField')(default='8dc842be46484291a65b9ea34c3a8af8', max_length=255, db_index=True)),
|
||||
('full_name', self.gf('django.db.models.fields.CharField')(default=u'John Do\xeb', max_length=255)),
|
||||
('template', self.gf('django.db.models.fields.CharField')(max_length=255)),
|
||||
('status', self.gf('django.db.models.fields.CharField')(default='started', max_length=255)),
|
||||
('error_reason', self.gf('django.db.models.fields.TextField')(default=None, null=True)),
|
||||
('download_url', self.gf('django.db.models.fields.CharField')(default=None, max_length=255, null=True)),
|
||||
))
|
||||
db.create_index('certificates_examplecertificate', ['uuid', 'access_key'])
|
||||
db.send_create_signal('certificates', ['ExampleCertificate'])
|
||||
|
||||
# Adding model 'ExampleCertificateSet'
|
||||
db.create_table('certificates_examplecertificateset', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
|
||||
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
|
||||
('course_key', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
|
||||
))
|
||||
db.send_create_signal('certificates', ['ExampleCertificateSet'])
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'CertificateGenerationCourseSetting'
|
||||
db.delete_table('certificates_certificategenerationcoursesetting')
|
||||
|
||||
# Deleting model 'ExampleCertificate'
|
||||
db.delete_table('certificates_examplecertificate')
|
||||
|
||||
# Deleting model 'ExampleCertificateSet'
|
||||
db.delete_table('certificates_examplecertificateset')
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'certificates.certificategenerationconfiguration': {
|
||||
'Meta': {'object_name': 'CertificateGenerationConfiguration'},
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'certificates.certificategenerationcoursesetting': {
|
||||
'Meta': {'object_name': 'CertificateGenerationCourseSetting'},
|
||||
'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'})
|
||||
},
|
||||
'certificates.certificatewhitelist': {
|
||||
'Meta': {'object_name': 'CertificateWhitelist'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'default': 'None', 'max_length': '255', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'whitelist': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
|
||||
},
|
||||
'certificates.examplecertificate': {
|
||||
'Meta': {'object_name': 'ExampleCertificate'},
|
||||
'access_key': ('django.db.models.fields.CharField', [], {'default': "'616e8368b9b2458b8ef8217713275322'", 'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'download_url': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}),
|
||||
'error_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}),
|
||||
'example_cert_set': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['certificates.ExampleCertificateSet']"}),
|
||||
'full_name': ('django.db.models.fields.CharField', [], {'default': "u'John Do\\xeb'", 'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'started'", 'max_length': '255'}),
|
||||
'template': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'uuid': ('django.db.models.fields.CharField', [], {'default': "'f38e7389280d4776907ebf96ac728bac'", 'unique': 'True', 'max_length': '255', 'db_index': 'True'})
|
||||
},
|
||||
'certificates.examplecertificateset': {
|
||||
'Meta': {'object_name': 'ExampleCertificateSet'},
|
||||
'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'})
|
||||
},
|
||||
'certificates.generatedcertificate': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'default': 'None', 'max_length': '255', 'blank': 'True'}),
|
||||
'created_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'download_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}),
|
||||
'download_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||
'error_reason': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
|
||||
'grade': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '32'}),
|
||||
'modified_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'unavailable'", 'max_length': '32'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'verify_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['certificates']
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Certificates are created for a student and an offering of a course.
|
||||
|
||||
@@ -44,17 +45,21 @@ Eligibility:
|
||||
then the student will be issued a certificate regardless of his grade,
|
||||
unless he has allow_certificate set to False.
|
||||
"""
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from datetime import datetime
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from model_utils import Choices
|
||||
from model_utils.models import TimeStampedModel
|
||||
from config_models.models import ConfigurationModel
|
||||
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
|
||||
from util.milestones_helpers import fulfill_course_milestone
|
||||
from course_modes.models import CourseMode
|
||||
|
||||
|
||||
class CertificateStatuses(object):
|
||||
@@ -179,6 +184,322 @@ def certificate_status_for_student(student, course_id):
|
||||
return {'status': CertificateStatuses.unavailable, 'mode': GeneratedCertificate.MODES.honor}
|
||||
|
||||
|
||||
class ExampleCertificateSet(TimeStampedModel):
|
||||
"""A set of example certificates.
|
||||
|
||||
Example certificates are used to verify that certificate
|
||||
generation is working for a particular course.
|
||||
|
||||
A particular course may have several kinds of certificates
|
||||
(e.g. honor and verified), in which case we generate
|
||||
multiple example certificates for the course.
|
||||
|
||||
"""
|
||||
course_key = CourseKeyField(max_length=255, db_index=True)
|
||||
|
||||
class Meta: # pylint: disable=missing-docstring, old-style-class
|
||||
get_latest_by = 'created'
|
||||
|
||||
@classmethod
|
||||
@transaction.commit_on_success
|
||||
def create_example_set(cls, course_key):
|
||||
"""Create a set of example certificates for a course.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey)
|
||||
|
||||
Returns:
|
||||
ExampleCertificateSet
|
||||
|
||||
"""
|
||||
cert_set = cls.objects.create(course_key=course_key)
|
||||
|
||||
ExampleCertificate.objects.bulk_create([
|
||||
ExampleCertificate(
|
||||
example_cert_set=cert_set,
|
||||
description=mode.slug,
|
||||
template=cls._template_for_mode(mode.slug, course_key)
|
||||
)
|
||||
for mode in CourseMode.modes_for_course(course_key)
|
||||
])
|
||||
|
||||
return cert_set
|
||||
|
||||
@classmethod
|
||||
def latest_status(cls, course_key):
|
||||
"""Summarize the latest status of example certificates for a course.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey)
|
||||
|
||||
Returns:
|
||||
list: List of status dictionaries. If no example certificates
|
||||
have been started yet, returns None.
|
||||
|
||||
"""
|
||||
try:
|
||||
latest = cls.objects.filter(course_key=course_key).latest()
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
queryset = ExampleCertificate.objects.filter(example_cert_set=latest).order_by('-created')
|
||||
return [cert.status_dict for cert in queryset]
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate through example certificates in the set.
|
||||
|
||||
Yields:
|
||||
ExampleCertificate
|
||||
|
||||
"""
|
||||
queryset = (ExampleCertificate.objects).select_related('example_cert_set').filter(example_cert_set=self)
|
||||
for cert in queryset:
|
||||
yield cert
|
||||
|
||||
@staticmethod
|
||||
def _template_for_mode(mode_slug, course_key):
|
||||
"""Calculate the template PDF based on the course mode. """
|
||||
return (
|
||||
u"certificate-template-{key.org}-{key.course}-verified.pdf".format(key=course_key)
|
||||
if mode_slug == 'verified'
|
||||
else u"certificate-template-{key.org}-{key.course}.pdf".format(key=course_key)
|
||||
)
|
||||
|
||||
|
||||
def _make_uuid():
|
||||
"""Return a 32-character UUID. """
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
class ExampleCertificate(TimeStampedModel):
|
||||
"""Example certificate.
|
||||
|
||||
Example certificates are used to verify that certificate
|
||||
generation is working for a particular course.
|
||||
|
||||
An example certificate is similar to an ordinary certificate,
|
||||
except that:
|
||||
|
||||
1) Example certificates are not associated with a particular user,
|
||||
and are never displayed to students.
|
||||
|
||||
2) We store the "inputs" for generating the example certificate
|
||||
to make it easier to debug when certificate generation fails.
|
||||
|
||||
3) We use dummy values.
|
||||
|
||||
"""
|
||||
# Statuses
|
||||
STATUS_STARTED = 'started'
|
||||
STATUS_SUCCESS = 'success'
|
||||
STATUS_ERROR = 'error'
|
||||
|
||||
# Dummy full name for the generated certificate
|
||||
EXAMPLE_FULL_NAME = u'John Doë'
|
||||
|
||||
example_cert_set = models.ForeignKey(ExampleCertificateSet)
|
||||
|
||||
description = models.CharField(
|
||||
max_length=255,
|
||||
help_text=ugettext_lazy(
|
||||
u"A human-readable description of the example certificate. "
|
||||
u"For example, 'verified' or 'honor' to differentiate between "
|
||||
u"two types of certificates."
|
||||
)
|
||||
)
|
||||
|
||||
# Inputs to certificate generation
|
||||
# We store this for auditing purposes if certificate
|
||||
# generation fails.
|
||||
uuid = models.CharField(
|
||||
max_length=255,
|
||||
default=_make_uuid,
|
||||
db_index=True,
|
||||
unique=True,
|
||||
help_text=ugettext_lazy(
|
||||
u"A unique identifier for the example certificate. "
|
||||
u"This is used when we receive a response from the queue "
|
||||
u"to determine which example certificate was processed."
|
||||
)
|
||||
)
|
||||
|
||||
access_key = models.CharField(
|
||||
max_length=255,
|
||||
default=_make_uuid,
|
||||
db_index=True,
|
||||
help_text=ugettext_lazy(
|
||||
u"An access key for the example certificate. "
|
||||
u"This is used when we receive a response from the queue "
|
||||
u"to validate that the sender is the same entity we asked "
|
||||
u"to generate the certificate."
|
||||
)
|
||||
)
|
||||
|
||||
full_name = models.CharField(
|
||||
max_length=255,
|
||||
default=EXAMPLE_FULL_NAME,
|
||||
help_text=ugettext_lazy(u"The full name that will appear on the certificate.")
|
||||
)
|
||||
|
||||
template = models.CharField(
|
||||
max_length=255,
|
||||
help_text=ugettext_lazy(u"The template file to use when generating the certificate.")
|
||||
)
|
||||
|
||||
# Outputs from certificate generation
|
||||
status = models.CharField(
|
||||
max_length=255,
|
||||
default=STATUS_STARTED,
|
||||
choices=(
|
||||
(STATUS_STARTED, 'Started'),
|
||||
(STATUS_SUCCESS, 'Success'),
|
||||
(STATUS_ERROR, 'Error')
|
||||
),
|
||||
help_text=ugettext_lazy(u"The status of the example certificate.")
|
||||
)
|
||||
|
||||
error_reason = models.TextField(
|
||||
null=True,
|
||||
default=None,
|
||||
help_text=ugettext_lazy(u"The reason an error occurred during certificate generation.")
|
||||
)
|
||||
|
||||
download_url = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
default=None,
|
||||
help_text=ugettext_lazy(u"The download URL for the generated certificate.")
|
||||
)
|
||||
|
||||
def update_status(self, status, error_reason=None, download_url=None):
|
||||
"""Update the status of the example certificate.
|
||||
|
||||
This will usually be called either:
|
||||
1) When an error occurs adding the certificate to the queue.
|
||||
2) When we receieve a response from the queue (either error or success).
|
||||
|
||||
If an error occurs, we store the error message;
|
||||
if certificate generation is successful, we store the URL
|
||||
for the generated certificate.
|
||||
|
||||
Arguments:
|
||||
status (str): Either `STATUS_SUCCESS` or `STATUS_ERROR`
|
||||
|
||||
Keyword Arguments:
|
||||
error_reason (unicode): A description of the error that occurred.
|
||||
download_url (unicode): The URL for the generated certificate.
|
||||
|
||||
Raises:
|
||||
ValueError: The status is not a valid value.
|
||||
|
||||
"""
|
||||
if status not in [self.STATUS_SUCCESS, self.STATUS_ERROR]:
|
||||
msg = u"Invalid status: must be either '{success}' or '{error}'.".format(
|
||||
success=self.STATUS_SUCCESS,
|
||||
error=self.STATUS_ERROR
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
self.status = status
|
||||
|
||||
if status == self.STATUS_ERROR and error_reason:
|
||||
self.error_reason = error_reason
|
||||
|
||||
if status == self.STATUS_SUCCESS and download_url:
|
||||
self.download_url = download_url
|
||||
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def status_dict(self):
|
||||
"""Summarize the status of the example certificate.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
result = {
|
||||
'description': self.description,
|
||||
'status': self.status,
|
||||
}
|
||||
|
||||
if self.error_reason:
|
||||
result['error_reason'] = self.error_reason
|
||||
|
||||
if self.download_url:
|
||||
result['download_url'] = self.download_url
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def course_key(self):
|
||||
"""The course key associated with the example certificate. """
|
||||
return self.example_cert_set.course_key
|
||||
|
||||
|
||||
class CertificateGenerationCourseSetting(TimeStampedModel):
|
||||
"""Enable or disable certificate generation for a particular course.
|
||||
|
||||
This controls whether students are allowed to "self-generate"
|
||||
certificates for a course. It does NOT prevent us from
|
||||
batch-generating certificates for a course using management
|
||||
commands.
|
||||
|
||||
In general, we should only enable self-generated certificates
|
||||
for a course once we successfully generate example certificates
|
||||
for the course. This is enforced in the UI layer, but
|
||||
not in the data layer.
|
||||
|
||||
"""
|
||||
course_key = CourseKeyField(max_length=255, db_index=True)
|
||||
enabled = models.BooleanField(default=False)
|
||||
|
||||
class Meta: # pylint: disable=missing-docstring, old-style-class
|
||||
get_latest_by = 'created'
|
||||
|
||||
@classmethod
|
||||
def is_enabled_for_course(cls, course_key):
|
||||
"""Check whether self-generated certificates are enabled for a course.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The identifier for the course.
|
||||
|
||||
Returns:
|
||||
boolean
|
||||
|
||||
"""
|
||||
try:
|
||||
latest = cls.objects.filter(course_key=course_key).latest()
|
||||
except cls.DoesNotExist:
|
||||
return False
|
||||
else:
|
||||
return latest.enabled
|
||||
|
||||
@classmethod
|
||||
def set_enabled_for_course(cls, course_key, is_enabled):
|
||||
"""Enable or disable self-generated certificates for a course.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The identifier for the course.
|
||||
is_enabled (boolean): Whether to enable or disable self-generated certificates.
|
||||
|
||||
"""
|
||||
CertificateGenerationCourseSetting.objects.create(
|
||||
course_key=course_key,
|
||||
enabled=is_enabled
|
||||
)
|
||||
|
||||
|
||||
class CertificateGenerationConfiguration(ConfigurationModel):
|
||||
"""Configure certificate generation."""
|
||||
"""Configure certificate generation.
|
||||
|
||||
Enable or disable the self-generated certificates feature.
|
||||
When this flag is disabled, the "generate certificate" button
|
||||
will be hidden on the progress page.
|
||||
|
||||
When the feature is enabled, the "generate certificate" button
|
||||
will appear for courses that have enabled self-generated
|
||||
certificates.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,27 +1,52 @@
|
||||
from certificates.models import GeneratedCertificate
|
||||
from certificates.models import certificate_status_for_student
|
||||
from certificates.models import CertificateStatuses as status
|
||||
from certificates.models import CertificateWhitelist
|
||||
|
||||
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
|
||||
from student.models import UserProfile, CourseEnrollment
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
"""Interface for adding certificate generation tasks to the XQueue. """
|
||||
import json
|
||||
import random
|
||||
import logging
|
||||
import lxml.html
|
||||
from lxml.etree import XMLSyntaxError, ParserError
|
||||
from lxml.etree import XMLSyntaxError, ParserError # pylint:disable=no-name-in-module
|
||||
|
||||
from django.test.client import RequestFactory
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from courseware import grades
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from capa.xqueue_interface import make_xheader, make_hashkey
|
||||
from student.models import UserProfile, CourseEnrollment
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
from certificates.models import (
|
||||
GeneratedCertificate,
|
||||
certificate_status_for_student,
|
||||
CertificateStatuses as status,
|
||||
CertificateWhitelist,
|
||||
ExampleCertificate
|
||||
)
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XQueueAddToQueueError(Exception):
|
||||
"""An error occurred when adding a certificate task to the queue. """
|
||||
|
||||
def __init__(self, error_code, error_msg):
|
||||
self.error_code = error_code
|
||||
self.error_msg = error_msg
|
||||
super(XQueueAddToQueueError, self).__init__(unicode(self))
|
||||
|
||||
def __unicode__(self):
|
||||
return (
|
||||
u"Could not add certificate to the XQueue. "
|
||||
u"The error code was '{code}' and the message was '{msg}'."
|
||||
).format(
|
||||
code=self.error_code,
|
||||
msg=self.error_msg
|
||||
)
|
||||
|
||||
|
||||
class XQueueCertInterface(object):
|
||||
"""
|
||||
XQueueCertificateInterface provides an
|
||||
@@ -205,7 +230,7 @@ class XQueueCertInterface(object):
|
||||
# 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)
|
||||
course = modulestore().get_course(course_id, depth=0)
|
||||
profile = UserProfile.objects.get(user=student)
|
||||
profile_name = profile.name
|
||||
|
||||
@@ -213,7 +238,7 @@ class XQueueCertInterface(object):
|
||||
self.request.user = student
|
||||
self.request.session = {}
|
||||
|
||||
course_name = course.display_name or course_id.to_deprecated_string()
|
||||
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)
|
||||
@@ -297,7 +322,7 @@ class XQueueCertInterface(object):
|
||||
contents = {
|
||||
'action': 'create',
|
||||
'username': student.username,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'course_id': unicode(course_id),
|
||||
'course_name': course_name,
|
||||
'name': profile_name,
|
||||
'grade': grade_contents,
|
||||
@@ -337,20 +362,106 @@ class XQueueCertInterface(object):
|
||||
|
||||
return new_status
|
||||
|
||||
def _send_to_xqueue(self, contents, key):
|
||||
"""Create a new task on the XQueue. """
|
||||
def add_example_cert(self, example_cert):
|
||||
"""Add a task to create an example certificate.
|
||||
|
||||
if self.use_https:
|
||||
proto = "https"
|
||||
else:
|
||||
proto = "http"
|
||||
Unlike other certificates, an example certificate is
|
||||
not associated with any particular user and is never
|
||||
shown to students.
|
||||
|
||||
xheader = make_xheader(
|
||||
'{0}://{1}/update_certificate?{2}'.format(
|
||||
proto, settings.SITE_NAME, key), key, settings.CERT_QUEUE)
|
||||
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:
|
||||
LOGGER.critical(u'Unable to add a request to the queue: %s %s', unicode(error), msg)
|
||||
raise Exception('Unable to send queue message')
|
||||
exc = XQueueAddToQueueError(error, msg)
|
||||
LOGGER.critical(unicode(exc))
|
||||
raise exc
|
||||
|
||||
257
lms/djangoapps/certificates/tests/test_api.py
Normal file
257
lms/djangoapps/certificates/tests/test_api.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""Tests for the certificates Python API. """
|
||||
from contextlib import contextmanager
|
||||
import ddt
|
||||
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch, Mock
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from config_models.models import cache
|
||||
|
||||
from certificates import api as certs_api
|
||||
from certificates.models import (
|
||||
CertificateStatuses,
|
||||
CertificateGenerationConfiguration,
|
||||
ExampleCertificate
|
||||
)
|
||||
from certificates.queue import XQueueCertInterface
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
|
||||
|
||||
class CertificateDownloadableStatusTests(ModuleStoreTestCase):
|
||||
"""Tests for the `certificate_downloadable_status` helper function. """
|
||||
|
||||
def setUp(self):
|
||||
super(CertificateDownloadableStatusTests, self).setUp()
|
||||
|
||||
self.student = UserFactory()
|
||||
self.student_no_cert = UserFactory()
|
||||
self.course = CourseFactory.create(
|
||||
org='edx',
|
||||
number='verified',
|
||||
display_name='Verified Course'
|
||||
)
|
||||
|
||||
self.request_factory = RequestFactory()
|
||||
|
||||
def test_user_cert_status_with_generating(self):
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.generating,
|
||||
mode='verified'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
certs_api.certificate_downloadable_status(self.student, self.course.id),
|
||||
{
|
||||
'is_downloadable': False,
|
||||
'is_generating': True,
|
||||
'download_url': None
|
||||
}
|
||||
)
|
||||
|
||||
def test_user_cert_status_with_error(self):
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.error,
|
||||
mode='verified'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
certs_api.certificate_downloadable_status(self.student, self.course.id),
|
||||
{
|
||||
'is_downloadable': False,
|
||||
'is_generating': True,
|
||||
'download_url': None
|
||||
}
|
||||
)
|
||||
|
||||
def test_user_with_out_cert(self):
|
||||
self.assertEqual(
|
||||
certs_api.certificate_downloadable_status(self.student_no_cert, self.course.id),
|
||||
{
|
||||
'is_downloadable': False,
|
||||
'is_generating': False,
|
||||
'download_url': None
|
||||
}
|
||||
)
|
||||
|
||||
def test_user_with_downloadable_cert(self):
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='verified',
|
||||
download_url='www.google.com'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
certs_api.certificate_downloadable_status(self.student, self.course.id),
|
||||
{
|
||||
'is_downloadable': True,
|
||||
'is_generating': False,
|
||||
'download_url': 'www.google.com'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class GenerateUserCertificatesTest(ModuleStoreTestCase):
|
||||
"""Tests for the `generate_user_certificates` helper function. """
|
||||
|
||||
def setUp(self):
|
||||
super(GenerateUserCertificatesTest, self).setUp()
|
||||
|
||||
self.student = UserFactory()
|
||||
self.student_no_cert = UserFactory()
|
||||
self.course = CourseFactory.create(
|
||||
org='edx',
|
||||
number='verified',
|
||||
display_name='Verified Course',
|
||||
grade_cutoffs={'cutoff': 0.75, 'Pass': 0.5}
|
||||
)
|
||||
self.enrollment = CourseEnrollment.enroll(self.student, self.course.id, mode='honor')
|
||||
self.request_factory = RequestFactory()
|
||||
|
||||
@override_settings(CERT_QUEUE='certificates')
|
||||
@patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75}))
|
||||
def test_new_cert_requests_into_xqueue_returns_generating(self):
|
||||
# Mock `grade.grade` and return a summary with passing score.
|
||||
# New requests save into xqueue and return the status
|
||||
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue:
|
||||
mock_send_to_queue.return_value = (0, "Successfully queued")
|
||||
result = certs_api.generate_user_certificates(self.student, self.course)
|
||||
self.assertEqual(result, 'generating')
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CertificateGenerationEnabledTest(TestCase):
|
||||
"""Test enabling/disabling self-generated certificates for a course. """
|
||||
|
||||
COURSE_KEY = CourseLocator(org='test', course='test', run='test')
|
||||
|
||||
def setUp(self):
|
||||
super(CertificateGenerationEnabledTest, self).setUp()
|
||||
|
||||
# Since model-based configuration is cached, we need
|
||||
# to clear the cache before each test.
|
||||
cache.clear()
|
||||
|
||||
@ddt.data(
|
||||
(None, None, False),
|
||||
(False, None, False),
|
||||
(False, True, False),
|
||||
(True, None, False),
|
||||
(True, False, False),
|
||||
(True, True, True)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_cert_generation_enabled(self, is_feature_enabled, is_course_enabled, expect_enabled):
|
||||
if is_feature_enabled is not None:
|
||||
CertificateGenerationConfiguration.objects.create(enabled=is_feature_enabled)
|
||||
|
||||
if is_course_enabled is not None:
|
||||
certs_api.set_cert_generation_enabled(self.COURSE_KEY, is_course_enabled)
|
||||
|
||||
self._assert_enabled_for_course(self.COURSE_KEY, expect_enabled)
|
||||
|
||||
def test_latest_setting_used(self):
|
||||
# Enable the feature
|
||||
CertificateGenerationConfiguration.objects.create(enabled=True)
|
||||
|
||||
# Enable for the course
|
||||
certs_api.set_cert_generation_enabled(self.COURSE_KEY, True)
|
||||
self._assert_enabled_for_course(self.COURSE_KEY, True)
|
||||
|
||||
# Disable for the course
|
||||
certs_api.set_cert_generation_enabled(self.COURSE_KEY, False)
|
||||
self._assert_enabled_for_course(self.COURSE_KEY, False)
|
||||
|
||||
def test_setting_is_course_specific(self):
|
||||
# Enable the feature
|
||||
CertificateGenerationConfiguration.objects.create(enabled=True)
|
||||
|
||||
# Enable for one course
|
||||
certs_api.set_cert_generation_enabled(self.COURSE_KEY, True)
|
||||
self._assert_enabled_for_course(self.COURSE_KEY, True)
|
||||
|
||||
# Should be disabled for another course
|
||||
other_course = CourseLocator(org='other', course='other', run='other')
|
||||
self._assert_enabled_for_course(other_course, False)
|
||||
|
||||
def _assert_enabled_for_course(self, course_key, expect_enabled):
|
||||
"""Check that self-generated certificates are enabled or disabled for the course. """
|
||||
actual_enabled = certs_api.cert_generation_enabled(course_key)
|
||||
self.assertEqual(expect_enabled, actual_enabled)
|
||||
|
||||
|
||||
class GenerateExampleCertificatesTest(TestCase):
|
||||
"""Test generation of example certificates. """
|
||||
|
||||
COURSE_KEY = CourseLocator(org='test', course='test', run='test')
|
||||
|
||||
def setUp(self):
|
||||
super(GenerateExampleCertificatesTest, self).setUp()
|
||||
|
||||
def test_generate_example_certs(self):
|
||||
# Generate certificates for the course
|
||||
with self._mock_xqueue() as mock_queue:
|
||||
certs_api.generate_example_certificates(self.COURSE_KEY)
|
||||
|
||||
# Verify that the appropriate certs were added to the queue
|
||||
self._assert_certs_in_queue(mock_queue, 1)
|
||||
|
||||
# Verify that the certificate status is "started"
|
||||
self._assert_cert_status({
|
||||
'description': 'honor',
|
||||
'status': 'started'
|
||||
})
|
||||
|
||||
def test_generate_example_certs_with_verified_mode(self):
|
||||
# Create verified and honor modes for the course
|
||||
CourseModeFactory(course_id=self.COURSE_KEY, mode_slug='honor')
|
||||
CourseModeFactory(course_id=self.COURSE_KEY, mode_slug='verified')
|
||||
|
||||
# Generate certificates for the course
|
||||
with self._mock_xqueue() as mock_queue:
|
||||
certs_api.generate_example_certificates(self.COURSE_KEY)
|
||||
|
||||
# Verify that the appropriate certs were added to the queue
|
||||
self._assert_certs_in_queue(mock_queue, 2)
|
||||
|
||||
# Verify that the certificate status is "started"
|
||||
self._assert_cert_status(
|
||||
{
|
||||
'description': 'verified',
|
||||
'status': 'started'
|
||||
},
|
||||
{
|
||||
'description': 'honor',
|
||||
'status': 'started'
|
||||
}
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def _mock_xqueue(self):
|
||||
"""Mock the XQueue method for adding a task to the queue. """
|
||||
with patch.object(XQueueCertInterface, 'add_example_cert') as mock_queue:
|
||||
yield mock_queue
|
||||
|
||||
def _assert_certs_in_queue(self, mock_queue, expected_num):
|
||||
"""Check that the certificate generation task was added to the queue. """
|
||||
certs_in_queue = [call_args[0] for (call_args, __) in mock_queue.call_args_list]
|
||||
self.assertEqual(len(certs_in_queue), expected_num)
|
||||
for cert in certs_in_queue:
|
||||
self.assertTrue(isinstance(cert, ExampleCertificate))
|
||||
|
||||
def _assert_cert_status(self, *expected_statuses):
|
||||
"""Check the example certificate status. """
|
||||
actual_status = certs_api.example_certificates_status(self.COURSE_KEY)
|
||||
self.assertEqual(list(expected_statuses), actual_status)
|
||||
73
lms/djangoapps/certificates/tests/test_models.py
Normal file
73
lms/djangoapps/certificates/tests/test_models.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Tests for certificate Django models. """
|
||||
from django.test import TestCase
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from certificates.models import (
|
||||
ExampleCertificate,
|
||||
ExampleCertificateSet
|
||||
)
|
||||
|
||||
|
||||
class ExampleCertificateTest(TestCase):
|
||||
"""Tests for the ExampleCertificate model. """
|
||||
|
||||
COURSE_KEY = CourseLocator(org='test', course='test', run='test')
|
||||
|
||||
DESCRIPTION = 'test'
|
||||
TEMPLATE = 'test.pdf'
|
||||
DOWNLOAD_URL = 'http://www.example.com'
|
||||
ERROR_REASON = 'Kaboom!'
|
||||
|
||||
def setUp(self):
|
||||
super(ExampleCertificateTest, self).setUp()
|
||||
self.cert_set = ExampleCertificateSet.objects.create(course_key=self.COURSE_KEY)
|
||||
self.cert = ExampleCertificate.objects.create(
|
||||
example_cert_set=self.cert_set,
|
||||
description=self.DESCRIPTION,
|
||||
template=self.TEMPLATE
|
||||
)
|
||||
|
||||
def test_update_status_success(self):
|
||||
self.cert.update_status(
|
||||
ExampleCertificate.STATUS_SUCCESS,
|
||||
download_url=self.DOWNLOAD_URL
|
||||
)
|
||||
self.assertEqual(
|
||||
self.cert.status_dict,
|
||||
{
|
||||
'description': self.DESCRIPTION,
|
||||
'status': ExampleCertificate.STATUS_SUCCESS,
|
||||
'download_url': self.DOWNLOAD_URL
|
||||
}
|
||||
)
|
||||
|
||||
def test_update_status_error(self):
|
||||
self.cert.update_status(
|
||||
ExampleCertificate.STATUS_ERROR,
|
||||
error_reason=self.ERROR_REASON
|
||||
)
|
||||
self.assertEqual(
|
||||
self.cert.status_dict,
|
||||
{
|
||||
'description': self.DESCRIPTION,
|
||||
'status': ExampleCertificate.STATUS_ERROR,
|
||||
'error_reason': self.ERROR_REASON
|
||||
}
|
||||
)
|
||||
|
||||
def test_update_status_invalid(self):
|
||||
with self.assertRaisesRegexp(ValueError, 'status'):
|
||||
self.cert.update_status('invalid')
|
||||
|
||||
def test_latest_status_unavailable(self):
|
||||
# Delete any existing statuses
|
||||
ExampleCertificateSet.objects.all().delete()
|
||||
|
||||
# Verify that the "latest" status is None
|
||||
result = ExampleCertificateSet.latest_status(self.COURSE_KEY)
|
||||
self.assertIs(result, None)
|
||||
|
||||
def test_latest_status_is_course_specific(self):
|
||||
other_course = CourseLocator(org='other', course='other', run='other')
|
||||
result = ExampleCertificateSet.latest_status(other_course)
|
||||
self.assertIs(result, None)
|
||||
97
lms/djangoapps/certificates/tests/test_queue.py
Normal file
97
lms/djangoapps/certificates/tests/test_queue.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for the XQueue certificates interface. """
|
||||
from contextlib import contextmanager
|
||||
import json
|
||||
from mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
|
||||
# It is really unfortunate that we are using the XQueue client
|
||||
# code from the capa library. In the future, we should move this
|
||||
# into a shared library. We import it here so we can mock it
|
||||
# and verify that items are being correctly added to the queue
|
||||
# in our `XQueueCertInterface` implementation.
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
|
||||
from certificates.queue import XQueueCertInterface
|
||||
from certificates.models import ExampleCertificateSet, ExampleCertificate
|
||||
|
||||
|
||||
@override_settings(CERT_QUEUE='certificates')
|
||||
class XQueueCertInterfaceTest(TestCase):
|
||||
"""Tests for the XQueue interface for certificate generation. """
|
||||
|
||||
COURSE_KEY = CourseLocator(org='test', course='test', run='test')
|
||||
|
||||
TEMPLATE = 'test.pdf'
|
||||
DESCRIPTION = 'test'
|
||||
ERROR_MSG = 'Kaboom!'
|
||||
|
||||
def setUp(self):
|
||||
super(XQueueCertInterfaceTest, self).setUp()
|
||||
self.xqueue = XQueueCertInterface()
|
||||
|
||||
def test_add_example_cert(self):
|
||||
cert = self._create_example_cert()
|
||||
with self._mock_xqueue() as mock_send:
|
||||
self.xqueue.add_example_cert(cert)
|
||||
|
||||
# Verify that the correct payload was sent to the XQueue
|
||||
self._assert_queue_task(mock_send, cert)
|
||||
|
||||
# Verify the certificate status
|
||||
self.assertEqual(cert.status, ExampleCertificate.STATUS_STARTED)
|
||||
|
||||
def test_add_example_cert_error(self):
|
||||
cert = self._create_example_cert()
|
||||
with self._mock_xqueue(success=False):
|
||||
self.xqueue.add_example_cert(cert)
|
||||
|
||||
# Verify the error status of the certificate
|
||||
self.assertEqual(cert.status, ExampleCertificate.STATUS_ERROR)
|
||||
self.assertIn(self.ERROR_MSG, cert.error_reason)
|
||||
|
||||
def _create_example_cert(self):
|
||||
"""Create an example certificate. """
|
||||
cert_set = ExampleCertificateSet.objects.create(course_key=self.COURSE_KEY)
|
||||
return ExampleCertificate.objects.create(
|
||||
example_cert_set=cert_set,
|
||||
description=self.DESCRIPTION,
|
||||
template=self.TEMPLATE
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def _mock_xqueue(self, success=True):
|
||||
"""Mock the XQueue method for sending a task to the queue. """
|
||||
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
|
||||
mock_send.return_value = (0, None) if success else (1, self.ERROR_MSG)
|
||||
yield mock_send
|
||||
|
||||
def _assert_queue_task(self, mock_send, cert):
|
||||
"""Check that the task was added to the queue. """
|
||||
expected_header = {
|
||||
'lms_key': cert.access_key,
|
||||
'lms_callback_url': 'https://edx.org/update_example_certificate?key={key}'.format(key=cert.uuid),
|
||||
'queue_name': 'certificates'
|
||||
}
|
||||
|
||||
expected_body = {
|
||||
'action': 'create',
|
||||
'username': cert.uuid,
|
||||
'name': u'John Doë',
|
||||
'course_id': unicode(self.COURSE_KEY),
|
||||
'template_pdf': 'test.pdf',
|
||||
'example_certificate': True
|
||||
}
|
||||
|
||||
self.assertTrue(mock_send.called)
|
||||
|
||||
__, kwargs = mock_send.call_args_list[0]
|
||||
actual_header = json.loads(kwargs['header'])
|
||||
actual_body = json.loads(kwargs['body'])
|
||||
|
||||
self.assertEqual(expected_header, actual_header)
|
||||
self.assertEqual(expected_body, actual_body)
|
||||
153
lms/djangoapps/certificates/tests/test_views.py
Normal file
153
lms/djangoapps/certificates/tests/test_views.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""Tests for certificates views. """
|
||||
|
||||
import json
|
||||
import ddt
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.cache import cache
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
|
||||
from certificates.models import ExampleCertificateSet, ExampleCertificate
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class UpdateExampleCertificateViewTest(TestCase):
|
||||
"""Tests for the XQueue callback that updates example certificates. """
|
||||
|
||||
COURSE_KEY = CourseLocator(org='test', course='test', run='test')
|
||||
|
||||
DESCRIPTION = 'test'
|
||||
TEMPLATE = 'test.pdf'
|
||||
DOWNLOAD_URL = 'http://www.example.com'
|
||||
ERROR_REASON = 'Kaboom!'
|
||||
|
||||
def setUp(self):
|
||||
super(UpdateExampleCertificateViewTest, self).setUp()
|
||||
self.cert_set = ExampleCertificateSet.objects.create(course_key=self.COURSE_KEY)
|
||||
self.cert = ExampleCertificate.objects.create(
|
||||
example_cert_set=self.cert_set,
|
||||
description=self.DESCRIPTION,
|
||||
template=self.TEMPLATE,
|
||||
)
|
||||
self.url = reverse('certificates.views.update_example_certificate')
|
||||
|
||||
# Since rate limit counts are cached, we need to clear
|
||||
# this before each test.
|
||||
cache.clear()
|
||||
|
||||
def test_update_example_certificate_success(self):
|
||||
response = self._post_to_view(self.cert, download_url=self.DOWNLOAD_URL)
|
||||
self._assert_response(response)
|
||||
|
||||
self.cert = ExampleCertificate.objects.get()
|
||||
self.assertEqual(self.cert.status, ExampleCertificate.STATUS_SUCCESS)
|
||||
self.assertEqual(self.cert.download_url, self.DOWNLOAD_URL)
|
||||
|
||||
def test_update_example_certificate_invalid_key(self):
|
||||
payload = {
|
||||
'xqueue_header': json.dumps({
|
||||
'lms_key': 'invalid'
|
||||
}),
|
||||
'xqueue_body': json.dumps({
|
||||
'username': self.cert.uuid,
|
||||
'url': self.DOWNLOAD_URL
|
||||
})
|
||||
}
|
||||
response = self.client.post(self.url, data=payload)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_update_example_certificate_error(self):
|
||||
response = self._post_to_view(self.cert, error_reason=self.ERROR_REASON)
|
||||
self._assert_response(response)
|
||||
|
||||
self.cert = ExampleCertificate.objects.get()
|
||||
self.assertEqual(self.cert.status, ExampleCertificate.STATUS_ERROR)
|
||||
self.assertEqual(self.cert.error_reason, self.ERROR_REASON)
|
||||
|
||||
@ddt.data('xqueue_header', 'xqueue_body')
|
||||
def test_update_example_certificate_invalid_params(self, missing_param):
|
||||
payload = {
|
||||
'xqueue_header': json.dumps({
|
||||
'lms_key': self.cert.access_key
|
||||
}),
|
||||
'xqueue_body': json.dumps({
|
||||
'username': self.cert.uuid,
|
||||
'url': self.DOWNLOAD_URL
|
||||
})
|
||||
}
|
||||
del payload[missing_param]
|
||||
|
||||
response = self.client.post(self.url, data=payload)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_update_example_certificate_missing_download_url(self):
|
||||
payload = {
|
||||
'xqueue_header': json.dumps({
|
||||
'lms_key': self.cert.access_key
|
||||
}),
|
||||
'xqueue_body': json.dumps({
|
||||
'username': self.cert.uuid
|
||||
})
|
||||
}
|
||||
response = self.client.post(self.url, data=payload)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_update_example_cetificate_non_json_param(self):
|
||||
payload = {
|
||||
'xqueue_header': '{/invalid',
|
||||
'xqueue_body': '{/invalid'
|
||||
}
|
||||
response = self.client.post(self.url, data=payload)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_unsupported_http_method(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
def test_bad_request_rate_limiting(self):
|
||||
payload = {
|
||||
'xqueue_header': json.dumps({
|
||||
'lms_key': 'invalid'
|
||||
}),
|
||||
'xqueue_body': json.dumps({
|
||||
'username': self.cert.uuid,
|
||||
'url': self.DOWNLOAD_URL
|
||||
})
|
||||
}
|
||||
|
||||
# Exceed the rate limit for invalid requests
|
||||
# (simulate a DDOS with invalid keys)
|
||||
for _ in range(100):
|
||||
response = self.client.post(self.url, data=payload)
|
||||
if response.status_code == 403:
|
||||
break
|
||||
|
||||
# The final status code should indicate that the rate
|
||||
# limit was exceeded.
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def _post_to_view(self, cert, download_url=None, error_reason=None):
|
||||
"""Simulate a callback from the XQueue to the example certificate end-point. """
|
||||
header = {'lms_key': cert.access_key}
|
||||
body = {'username': cert.uuid}
|
||||
|
||||
if download_url is not None:
|
||||
body['url'] = download_url
|
||||
|
||||
if error_reason is not None:
|
||||
body['error'] = 'error'
|
||||
body['error_reason'] = self.ERROR_REASON
|
||||
|
||||
payload = {
|
||||
'xqueue_header': json.dumps(header),
|
||||
'xqueue_body': json.dumps(body)
|
||||
}
|
||||
return self.client.post(self.url, data=payload)
|
||||
|
||||
def _assert_response(self, response):
|
||||
"""Check the response from the callback end-point. """
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(content['return_code'], 0)
|
||||
@@ -1,141 +0,0 @@
|
||||
"""
|
||||
Tests for the certificates api and helper function.
|
||||
"""
|
||||
from django.test import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch, Mock
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from certificates.api import certificate_downloadable_status, generate_user_certificates
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from certificates.models import CertificateStatuses
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
|
||||
|
||||
class CertificateDownloadableStatusTests(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the certificate_downloadable_status helper function
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(CertificateDownloadableStatusTests, self).setUp()
|
||||
|
||||
self.student = UserFactory()
|
||||
self.student_no_cert = UserFactory()
|
||||
self.course = CourseFactory.create(
|
||||
org='edx',
|
||||
number='verified',
|
||||
display_name='Verified Course'
|
||||
)
|
||||
|
||||
self.request_factory = RequestFactory()
|
||||
|
||||
def test_user_cert_status_with_generating(self):
|
||||
"""
|
||||
in case of certificate with error means means is_generating is True and is_downloadable is False
|
||||
"""
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.generating,
|
||||
mode='verified'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
certificate_downloadable_status(self.student, self.course.id),
|
||||
{
|
||||
'is_downloadable': False,
|
||||
'is_generating': True,
|
||||
'download_url': None
|
||||
}
|
||||
)
|
||||
|
||||
def test_user_cert_status_with_error(self):
|
||||
"""
|
||||
in case of certificate with error means means is_generating is True and is_downloadable is False
|
||||
"""
|
||||
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.error,
|
||||
mode='verified'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
certificate_downloadable_status(self.student, self.course.id),
|
||||
{
|
||||
'is_downloadable': False,
|
||||
'is_generating': True,
|
||||
'download_url': None
|
||||
}
|
||||
)
|
||||
|
||||
def test_user_with_out_cert(self):
|
||||
"""
|
||||
in case of no certificate means is_generating is False and is_downloadable is False
|
||||
"""
|
||||
self.assertEqual(
|
||||
certificate_downloadable_status(self.student_no_cert, self.course.id),
|
||||
{
|
||||
'is_downloadable': False,
|
||||
'is_generating': False,
|
||||
'download_url': None
|
||||
}
|
||||
)
|
||||
|
||||
def test_user_with_downloadable_cert(self):
|
||||
"""
|
||||
in case of downloadable certificate means is_generating is False and is_downloadable is True
|
||||
download_url has cert link
|
||||
"""
|
||||
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='verified',
|
||||
download_url='www.google.com'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
certificate_downloadable_status(self.student, self.course.id),
|
||||
{
|
||||
'is_downloadable': True,
|
||||
'is_generating': False,
|
||||
'download_url': 'www.google.com'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class GenerateUserCertificatesTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the generate_user_certificates helper function
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(GenerateUserCertificatesTest, self).setUp()
|
||||
|
||||
self.student = UserFactory()
|
||||
self.student_no_cert = UserFactory()
|
||||
self.course = CourseFactory.create(
|
||||
org='edx',
|
||||
number='verified',
|
||||
display_name='Verified Course',
|
||||
grade_cutoffs={'cutoff': 0.75, 'Pass': 0.5}
|
||||
)
|
||||
self.enrollment = CourseEnrollment.enroll(self.student, self.course.id, mode='honor')
|
||||
self.request_factory = RequestFactory()
|
||||
|
||||
@override_settings(CERT_QUEUE='certificates')
|
||||
@patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75}))
|
||||
def test_new_cert_requests_into_xqueue_returns_generating(self):
|
||||
"""
|
||||
mocking grade.grade and returns a summary with passing score.
|
||||
new requests saves into xqueue and returns the status
|
||||
"""
|
||||
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue:
|
||||
mock_send_to_queue.return_value = (0, "Successfully queued")
|
||||
self.assertEqual(generate_user_certificates(self.student, self.course), 'generating')
|
||||
@@ -4,14 +4,21 @@ import json
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, Http404, HttpResponseForbidden
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from capa.xqueue_interface import XQUEUE_METRIC_NAME
|
||||
from certificates.models import certificate_status_for_student, CertificateStatuses, GeneratedCertificate
|
||||
from certificates.models import (
|
||||
certificate_status_for_student,
|
||||
CertificateStatuses,
|
||||
GeneratedCertificate,
|
||||
ExampleCertificate
|
||||
)
|
||||
from certificates.queue import XQueueCertInterface
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from util.json_request import JsonResponse, JsonResponseBadRequest
|
||||
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -121,3 +128,99 @@ def update_certificate(request):
|
||||
cert.save()
|
||||
return HttpResponse(json.dumps({'return_code': 0}),
|
||||
mimetype='application/json')
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def update_example_certificate(request):
|
||||
"""Callback from the XQueue that updates example certificates.
|
||||
|
||||
Example certificates are used to verify that certificate
|
||||
generation is configured correctly for a course.
|
||||
|
||||
Unlike other certificates, example certificates
|
||||
are not associated with a particular user or displayed
|
||||
to students.
|
||||
|
||||
For this reason, we need a different end-point to update
|
||||
the status of generated example certificates.
|
||||
|
||||
Arguments:
|
||||
request (HttpRequest)
|
||||
|
||||
Returns:
|
||||
HttpResponse (200): Status was updated successfully.
|
||||
HttpResponse (400): Invalid parameters.
|
||||
HttpResponse (403): Rate limit exceeded for bad requests.
|
||||
HttpResponse (404): Invalid certificate identifier or access key.
|
||||
|
||||
"""
|
||||
logger.info(u"Received response for example certificate from XQueue.")
|
||||
|
||||
rate_limiter = BadRequestRateLimiter()
|
||||
|
||||
# Check the parameters and rate limits
|
||||
# If these are invalid, return an error response.
|
||||
if rate_limiter.is_rate_limit_exceeded(request):
|
||||
logger.info(u"Bad request rate limit exceeded for update example certificate end-point.")
|
||||
return HttpResponseForbidden("Rate limit exceeded")
|
||||
|
||||
if 'xqueue_body' not in request.POST:
|
||||
logger.info(u"Missing parameter 'xqueue_body' for update example certificate end-point")
|
||||
rate_limiter.tick_bad_request_counter(request)
|
||||
return JsonResponseBadRequest("Parameter 'xqueue_body' is required.")
|
||||
|
||||
if 'xqueue_header' not in request.POST:
|
||||
logger.info(u"Missing parameter 'xqueue_header' for update example certificate end-point")
|
||||
rate_limiter.tick_bad_request_counter(request)
|
||||
return JsonResponseBadRequest("Parameter 'xqueue_header' is required.")
|
||||
|
||||
try:
|
||||
xqueue_body = json.loads(request.POST['xqueue_body'])
|
||||
xqueue_header = json.loads(request.POST['xqueue_header'])
|
||||
except (ValueError, TypeError):
|
||||
logger.info(u"Could not decode params to example certificate end-point as JSON.")
|
||||
rate_limiter.tick_bad_request_counter(request)
|
||||
return JsonResponseBadRequest("Parameters must be JSON-serialized.")
|
||||
|
||||
# Attempt to retrieve the example certificate record
|
||||
# so we can update the status.
|
||||
try:
|
||||
uuid = xqueue_body.get('username')
|
||||
access_key = xqueue_header.get('lms_key')
|
||||
cert = ExampleCertificate.objects.get(uuid=uuid, access_key=access_key)
|
||||
except ExampleCertificate.DoesNotExist:
|
||||
# If we are unable to retrieve the record, it means the uuid or access key
|
||||
# were not valid. This most likely means that the request is NOT coming
|
||||
# from the XQueue. Return a 404 and increase the bad request counter
|
||||
# to protect against a DDOS attack.
|
||||
logger.info(u"Could not find example certificate with uuid '%s' and access key '%s'", uuid, access_key)
|
||||
rate_limiter.tick_bad_request_counter(request)
|
||||
raise Http404
|
||||
|
||||
if 'error' in xqueue_body:
|
||||
# If an error occurs, save the error message so we can fix the issue.
|
||||
error_reason = xqueue_body.get('error_reason')
|
||||
cert.update_status(ExampleCertificate.STATUS_ERROR, error_reason=error_reason)
|
||||
logger.warning(
|
||||
(
|
||||
u"Error occurred during example certificate generation for uuid '%s'. "
|
||||
u"The error response was '%s'."
|
||||
), uuid, error_reason
|
||||
)
|
||||
else:
|
||||
# If the certificate generated successfully, save the download URL
|
||||
# so we can display the example certificate.
|
||||
download_url = xqueue_body.get('url')
|
||||
if download_url is None:
|
||||
rate_limiter.tick_bad_request_counter(request)
|
||||
logger.warning(u"No download URL provided for example certificate with uuid '%s'.", uuid)
|
||||
return JsonResponseBadRequest(
|
||||
"Parameter 'download_url' is required for successfully generated certificates."
|
||||
)
|
||||
else:
|
||||
cert.update_status(ExampleCertificate.STATUS_SUCCESS, download_url=download_url)
|
||||
logger.info("Successfully updated example certificate with uuid '%s'.", uuid)
|
||||
|
||||
# Let the XQueue know that we handled the response
|
||||
return JsonResponse({'return_code': 0})
|
||||
|
||||
@@ -17,7 +17,9 @@ urlpatterns = (
|
||||
|
||||
# certificate view
|
||||
url(r'^update_certificate$', 'certificates.views.update_certificate'),
|
||||
url(r'^update_example_certificate$', 'certificates.views.update_example_certificate'),
|
||||
url(r'^request_certificate$', 'certificates.views.request_certificate'),
|
||||
|
||||
url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware
|
||||
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
|
||||
url(r'^login_ajax$', 'student.views.login_user', name="login"),
|
||||
|
||||
Reference in New Issue
Block a user