ECOM-1046 adding functionality for generating the certs.
ECOM-1046 minor change in code. rename file name.
This commit is contained in:
8
lms/djangoapps/certificates/admin.py
Normal file
8
lms/djangoapps/certificates/admin.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
django admin pages for certificates models
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from certificates.models import CertificateGenerationConfiguration
|
||||
|
||||
|
||||
admin.site.register(CertificateGenerationConfiguration)
|
||||
65
lms/djangoapps/certificates/api.py
Normal file
65
lms/djangoapps/certificates/api.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Certificates API
|
||||
"""
|
||||
|
||||
import logging
|
||||
from certificates.models import CertificateStatuses as cert_status, certificate_status_for_student
|
||||
from certificates.queue import XQueueCertInterface
|
||||
|
||||
log = logging.getLogger("edx.certificate")
|
||||
|
||||
|
||||
def generate_user_certificates(student, course):
|
||||
"""
|
||||
It will add the add-cert request into the xqueue.
|
||||
|
||||
Args:
|
||||
student (object): user
|
||||
course (object): course
|
||||
|
||||
Returns:
|
||||
returns status of generated certificate
|
||||
"""
|
||||
xqueue = XQueueCertInterface()
|
||||
ret = xqueue.add_cert(student, course.id, course=course)
|
||||
log.info(
|
||||
(
|
||||
u"Added a certificate generation task to the XQueue "
|
||||
u"for student %s in course '%s'. "
|
||||
u"The new certificate status is '%s'."
|
||||
),
|
||||
student.id,
|
||||
unicode(course.id),
|
||||
ret
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
def certificate_downloadable_status(student, course_key):
|
||||
"""
|
||||
Check the student existing certificates against a given course.
|
||||
if status is not generating and not downloadable or error then user can view the generate button.
|
||||
|
||||
Args:
|
||||
student (user object): logged-in user
|
||||
course_key (CourseKey): ID associated with the course
|
||||
|
||||
Returns:
|
||||
Dict containing student passed status also download url for cert if available
|
||||
"""
|
||||
current_status = certificate_status_for_student(student, course_key)
|
||||
|
||||
# If the certificate status is an error user should view that status is "generating".
|
||||
# On the back-end, need to monitor those errors and re-submit the task.
|
||||
|
||||
response_data = {
|
||||
'is_downloadable': False,
|
||||
'is_generating': True if current_status['status'] in [cert_status.generating, cert_status.error] else False,
|
||||
'download_url': None
|
||||
}
|
||||
|
||||
if current_status['status'] == cert_status.downloadable:
|
||||
response_data['is_downloadable'] = True
|
||||
response_data['download_url'] = current_status['download_url']
|
||||
|
||||
return response_data
|
||||
@@ -0,0 +1,90 @@
|
||||
# -*- 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):
|
||||
|
||||
# Changing field 'GeneratedCertificate.course_id'
|
||||
db.alter_column('certificates_generatedcertificate', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255))
|
||||
|
||||
# Changing field 'CertificateWhitelist.course_id'
|
||||
db.alter_column('certificates_certificatewhitelist', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255))
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Changing field 'GeneratedCertificate.course_id'
|
||||
db.alter_column('certificates_generatedcertificate', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=255))
|
||||
|
||||
# Changing field 'CertificateWhitelist.course_id'
|
||||
db.alter_column('certificates_certificatewhitelist', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=255))
|
||||
|
||||
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.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.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']
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- 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 'CertificateGenerationConfiguration'
|
||||
db.create_table('certificates_certificategenerationconfiguration', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)),
|
||||
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
))
|
||||
db.send_create_signal('certificates', ['CertificateGenerationConfiguration'])
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'CertificateGenerationConfiguration'
|
||||
db.delete_table('certificates_certificategenerationconfiguration')
|
||||
|
||||
|
||||
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.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.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']
|
||||
@@ -52,6 +52,7 @@ from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from datetime import datetime
|
||||
from model_utils import Choices
|
||||
from config_models.models import ConfigurationModel
|
||||
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
|
||||
from util.milestones_helpers import fulfill_course_milestone
|
||||
|
||||
@@ -176,3 +177,8 @@ def certificate_status_for_student(student, course_id):
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
pass
|
||||
return {'status': CertificateStatuses.unavailable, 'mode': GeneratedCertificate.MODES.honor}
|
||||
|
||||
|
||||
class CertificateGenerationConfiguration(ConfigurationModel):
|
||||
"""Configure certificate generation."""
|
||||
pass
|
||||
|
||||
141
lms/djangoapps/certificates/tests/tests_api.py
Normal file
141
lms/djangoapps/certificates/tests/tests_api.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
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')
|
||||
@@ -11,13 +11,15 @@ import ddt
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from django.http import Http404, HttpResponseBadRequest
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from certificates.models import CertificateStatuses, CertificateGenerationConfiguration
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
from edxmako.middleware import MakoMiddleware
|
||||
from edxmako.tests import mako_middleware_process_request
|
||||
from mock import MagicMock, patch, create_autospec
|
||||
from mock import MagicMock, patch, create_autospec, Mock
|
||||
from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey
|
||||
|
||||
import courseware.views as views
|
||||
@@ -671,6 +673,11 @@ class ProgressPageTests(ModuleStoreTestCase):
|
||||
resp = views.progress(self.request, course_id=self.course.id.to_deprecated_string())
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_resp_with_generate_cert_config_enabled(self):
|
||||
CertificateGenerationConfiguration(enabled=True).save()
|
||||
resp = views.progress(self.request, course_id=unicode(self.course.id))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
|
||||
class VerifyCourseKeyDecoratorTests(TestCase):
|
||||
"""
|
||||
@@ -695,3 +702,126 @@ class VerifyCourseKeyDecoratorTests(TestCase):
|
||||
view_function = ensure_valid_course_key(mocked_view)
|
||||
self.assertRaises(Http404, view_function, self.request, course_id=self.invalid_course_id)
|
||||
self.assertFalse(mocked_view.called)
|
||||
|
||||
|
||||
class IsCoursePassedTests(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the is_course_passed helper function
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(IsCoursePassedTests, self).setUp()
|
||||
|
||||
self.student = UserFactory()
|
||||
self.course = CourseFactory.create(
|
||||
org='edx',
|
||||
number='verified',
|
||||
display_name='Verified Course',
|
||||
grade_cutoffs={'cutoff': 0.75, 'Pass': 0.5}
|
||||
)
|
||||
self.request = RequestFactory()
|
||||
|
||||
def test_user_fails_if_not_clear_exam(self):
|
||||
# If user has not grade then false will return
|
||||
self.assertFalse(views.is_course_passed(self.course, None, self.student, self.request))
|
||||
|
||||
@patch('courseware.grades.grade', Mock(return_value={'percent': 0.9}))
|
||||
def test_user_pass_if_percent_appears_above_passing_point(self):
|
||||
# Mocking the grades.grade
|
||||
# If user has above passing marks then True will return
|
||||
self.assertTrue(views.is_course_passed(self.course, None, self.student, self.request))
|
||||
|
||||
@patch('courseware.grades.grade', Mock(return_value={'percent': 0.2}))
|
||||
def test_user_fail_if_percent_appears_below_passing_point(self):
|
||||
# Mocking the grades.grade
|
||||
# If user has below passing marks then False will return
|
||||
self.assertFalse(views.is_course_passed(self.course, None, self.student, self.request))
|
||||
|
||||
|
||||
class GenerateUserCertTests(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the view function Generated User Certs
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(GenerateUserCertTests, self).setUp()
|
||||
|
||||
self.student = UserFactory(username='dummy', password='123456', email='test@mit.edu')
|
||||
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 = RequestFactory()
|
||||
self.client.login(username=self.student, password='123456')
|
||||
self.url = reverse('generate_user_cert', kwargs={'course_id': unicode(self.course.id)})
|
||||
|
||||
def test_user_with_out_passing_grades(self):
|
||||
# If user has no grading then json will return failed message and badrequest code
|
||||
resp = self.client.post(self.url)
|
||||
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
|
||||
self.assertIn("Your certificate will be available when you pass the course.", resp.content)
|
||||
|
||||
@patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75}))
|
||||
@override_settings(CERT_QUEUE='certificates')
|
||||
def test_user_with_passing_grade(self):
|
||||
# If user has above passing grading then json will return cert generating message and
|
||||
# status valid code
|
||||
# mocking xqueue
|
||||
|
||||
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue:
|
||||
mock_send_to_queue.return_value = (0, "Successfully queued")
|
||||
resp = self.client.post(self.url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn("Creating certificate", resp.content)
|
||||
|
||||
@patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75}))
|
||||
def test_user_with_passing_existing_generating_cert(self):
|
||||
# If user has passing grade but also has existing generating cert
|
||||
# then json will return cert generating message with bad request code
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.generating,
|
||||
mode='verified'
|
||||
)
|
||||
resp = self.client.post(self.url)
|
||||
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
|
||||
self.assertIn("Creating certificate", resp.content)
|
||||
|
||||
@patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75}))
|
||||
def test_user_with_passing_existing_downloadable_cert(self):
|
||||
# If user has passing grade but also has existing downloadable cert
|
||||
# then json will return cert generating message with bad request code
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='verified'
|
||||
)
|
||||
resp = self.client.post(self.url)
|
||||
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
|
||||
self.assertIn("Creating certificate", resp.content)
|
||||
|
||||
def test_user_with_non_existing_course(self):
|
||||
# If try to access a course with valid key pattern then it will return
|
||||
# bad request code with course is not valid message
|
||||
resp = self.client.post('/courses/def/abc/in_valid/generate_user_cert')
|
||||
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
|
||||
self.assertIn("Course is not valid", resp.content)
|
||||
|
||||
def test_user_with_invalid_course_id(self):
|
||||
# If try to access a course with invalid key pattern then 404 will return
|
||||
resp = self.client.post('/courses/def/generate_user_cert')
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_user_without_login_return_error(self):
|
||||
# If user try to access without login should see a bad request status code with message
|
||||
self.client.logout()
|
||||
resp = self.client.post(self.url)
|
||||
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
|
||||
self.assertIn("You must be signed in to {platform_name} to create a certificate.".format(
|
||||
platform_name=settings.PLATFORM_NAME
|
||||
), resp.content)
|
||||
|
||||
@@ -20,9 +20,11 @@ from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.timezone import UTC
|
||||
from django.views.decorators.http import require_GET
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
from certificates.api import certificate_downloadable_status, generate_user_certificates
|
||||
from certificates.models import CertificateGenerationConfiguration
|
||||
from edxmako.shortcuts import render_to_response, render_to_string, marketing_link
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
@@ -60,6 +62,7 @@ from util.milestones_helpers import get_prerequisite_courses_display
|
||||
|
||||
from microsite_configuration import microsite
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from instructor.enrollment import uses_shib
|
||||
|
||||
from util.db import commit_on_success_with_read_committed
|
||||
@@ -1007,6 +1010,9 @@ def _progress(request, course_key, student_id):
|
||||
#This means the student didn't have access to the course (which the instructor requested)
|
||||
raise Http404
|
||||
|
||||
# checking certificate generation configuration
|
||||
show_generate_cert_btn = CertificateGenerationConfiguration.current().enabled
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'courseware_summary': courseware_summary,
|
||||
@@ -1014,9 +1020,14 @@ def _progress(request, course_key, student_id):
|
||||
'grade_summary': grade_summary,
|
||||
'staff_access': staff_access,
|
||||
'student': student,
|
||||
'reverifications': fetch_reverify_banner_info(request, course_key)
|
||||
'reverifications': fetch_reverify_banner_info(request, course_key),
|
||||
'passed': is_course_passed(course, grade_summary) if show_generate_cert_btn else False,
|
||||
'show_generate_cert_btn': show_generate_cert_btn
|
||||
}
|
||||
|
||||
if show_generate_cert_btn:
|
||||
context.update(certificate_downloadable_status(student, course_key))
|
||||
|
||||
with grades.manual_transaction():
|
||||
response = render_to_response('courseware/progress.html', context)
|
||||
|
||||
@@ -1231,3 +1242,72 @@ def course_survey(request, course_id):
|
||||
redirect_url=redirect_url,
|
||||
is_required=course.course_survey_required,
|
||||
)
|
||||
|
||||
|
||||
def is_course_passed(course, grade_summary=None, student=None, request=None):
|
||||
"""
|
||||
check user's course passing status. return True if passed
|
||||
|
||||
Arguments:
|
||||
course : course object
|
||||
grade_summary (dict) : contains student grade details.
|
||||
student : user object
|
||||
request (HttpRequest)
|
||||
|
||||
Returns:
|
||||
returns bool value
|
||||
"""
|
||||
nonzero_cutoffs = [cutoff for cutoff in course.grade_cutoffs.values() if cutoff > 0]
|
||||
success_cutoff = min(nonzero_cutoffs) if nonzero_cutoffs else None
|
||||
|
||||
if grade_summary is None:
|
||||
grade_summary = grades.grade(student, request, course)
|
||||
|
||||
return success_cutoff and grade_summary['percent'] > success_cutoff
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_POST
|
||||
def generate_user_cert(request, course_id):
|
||||
"""
|
||||
It will check all validation and on clearance will add the new-certificate request into the xqueue.
|
||||
|
||||
Args:
|
||||
request (django request object): the HTTP request object that triggered this view function
|
||||
course_id (unicode): id associated with the course
|
||||
|
||||
Returns:
|
||||
returns json response
|
||||
"""
|
||||
|
||||
if not request.user.is_authenticated():
|
||||
log.info(u"Anon user trying to generate certificate for %s", course_id)
|
||||
return HttpResponseBadRequest(
|
||||
_('You must be signed in to {platform_name} to create a certificate.').format(
|
||||
platform_name=settings.PLATFORM_NAME
|
||||
)
|
||||
)
|
||||
|
||||
student = request.user
|
||||
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
|
||||
course = modulestore().get_course(course_key, depth=2)
|
||||
if not course:
|
||||
return HttpResponseBadRequest(_("Course is not valid"))
|
||||
|
||||
if not is_course_passed(course, None, student, request):
|
||||
return HttpResponseBadRequest(_("Your certificate will be available when you pass the course."))
|
||||
|
||||
certificate_status = certificate_downloadable_status(student, course.id)
|
||||
|
||||
if not certificate_status["is_downloadable"] and not certificate_status["is_generating"]:
|
||||
generate_user_certificates(student, course)
|
||||
return HttpResponse(_("Creating certificate"))
|
||||
|
||||
# if certificate_status is not is_downloadable and is_generating or
|
||||
# if any error appears during certificate generation return the message cert is generating.
|
||||
# with badrequest
|
||||
# at backend debug the issue and re-submit the task.
|
||||
|
||||
return HttpResponseBadRequest(_("Creating certificate"))
|
||||
|
||||
@@ -299,10 +299,6 @@ FEATURES = {
|
||||
# Turn on Advanced Security by default
|
||||
'ADVANCED_SECURITY': True,
|
||||
|
||||
# Show a "Download your certificate" on the Progress page if the lowest
|
||||
# nonzero grade cutoff is met
|
||||
'SHOW_PROGRESS_SUCCESS_BUTTON': False,
|
||||
|
||||
# When a logged in user goes to the homepage ('/') should the user be
|
||||
# redirected to the dashboard - this is default Open edX behavior. Set to
|
||||
# False to not redirect the user
|
||||
@@ -1737,10 +1733,6 @@ GRADES_DOWNLOAD = {
|
||||
'ROOT_PATH': '/tmp/edx-s3/grades',
|
||||
}
|
||||
|
||||
######################## PROGRESS SUCCESS BUTTON ##############################
|
||||
# The following fields are available in the URL: {course_id} {student_id}
|
||||
PROGRESS_SUCCESS_BUTTON_URL = 'http://<domain>/<path>/{course_id}'
|
||||
PROGRESS_SUCCESS_BUTTON_TEXT_OVERRIDE = None
|
||||
|
||||
#### PASSWORD POLICY SETTINGS #####
|
||||
PASSWORD_MIN_LENGTH = 8
|
||||
|
||||
19
lms/static/js/courseware/certificates_api.js
Normal file
19
lms/static/js/courseware/certificates_api.js
Normal file
@@ -0,0 +1,19 @@
|
||||
$(document).ready(function() {
|
||||
$("#btn_generate_cert").click(function(e){
|
||||
e.preventDefault();
|
||||
var post_url = $("#btn_generate_cert").data("endpoint");
|
||||
$('#btn_generate_cert').prop("disabled", true);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: post_url,
|
||||
dataType: 'text',
|
||||
success: function () {
|
||||
location.reload();
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
$('#errors-info').html(jqXHR.responseText);
|
||||
$('#btn_generate_cert').prop("disabled", false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@
|
||||
<%static:css group='style-course'/>
|
||||
</%block>
|
||||
|
||||
|
||||
<%namespace name="progress_graph" file="/courseware/progress_graph.js"/>
|
||||
|
||||
<%block name="pagetitle">${_("{course_number} Progress").format(course_number=course.display_number_with_default) | h}</%block>
|
||||
@@ -27,6 +28,7 @@ from django.utils.http import urlquote_plus
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/courseware/certificates_api.js')}"></script>
|
||||
<script>
|
||||
${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", not course.no_grade, not course.no_grade)}
|
||||
</script>
|
||||
@@ -50,23 +52,27 @@ from django.utils.http import urlquote_plus
|
||||
<h1>${_("Course Progress for Student '{username}' ({email})").format(username=student.username, email=student.email)}</h1>
|
||||
</header>
|
||||
|
||||
%if settings.FEATURES.get("SHOW_PROGRESS_SUCCESS_BUTTON"):
|
||||
<%
|
||||
SUCCESS_BUTTON_URL = settings.PROGRESS_SUCCESS_BUTTON_URL.format(
|
||||
course_id=urlquote_plus(unicode(course.id)),
|
||||
student_id=urlquote_plus(student.id)
|
||||
)
|
||||
nonzero_cutoffs = [cutoff for cutoff in course.grade_cutoffs.values() if cutoff > 0]
|
||||
success_cutoff = min(nonzero_cutoffs) if nonzero_cutoffs else None
|
||||
%>
|
||||
%if success_cutoff and grade_summary['percent'] > success_cutoff:
|
||||
<div id="course-success">
|
||||
<a href="${SUCCESS_BUTTON_URL}">
|
||||
${settings.PROGRESS_SUCCESS_BUTTON_TEXT_OVERRIDE or _("Download your certificate")}
|
||||
</a>
|
||||
</div>
|
||||
%endif
|
||||
%endif
|
||||
%if show_generate_cert_btn:
|
||||
<div id="course-success">
|
||||
%if passed:
|
||||
% if is_downloadable and download_url:
|
||||
<a class="btn" href="${download_url}" target="_blank"
|
||||
title="${_('You can download your certificate as a PDF. You can then print your certificate or share it with others.')}">
|
||||
${_("Download Your Certificate")}
|
||||
</a>
|
||||
%elif is_generating:
|
||||
<button disabled="disabled">${_('Create Your Certificate')}</button>
|
||||
<p class="text-center">${_("Creating certificate")}</p>
|
||||
%else:
|
||||
<button data-endpoint="${reverse('generate_user_cert', args=[unicode(course.id)])}" id="btn_generate_cert">${_('Create Your Certificate')}</button>
|
||||
%endif
|
||||
%else:
|
||||
<button disabled="disabled">${_('Create Your Certificate')}</button>
|
||||
<p class="text-center">${_("Your certificate will be available when you pass the course.")}</p>
|
||||
%endif
|
||||
</div>
|
||||
<div id="errors-info" class="text-center"></div>
|
||||
%endif
|
||||
|
||||
%if not course.disable_progress_graph:
|
||||
<div id="grade-detail-graph" aria-hidden="true"></div>
|
||||
@@ -138,3 +144,4 @@ from django.utils.http import urlquote_plus
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -415,6 +415,11 @@ if settings.COURSEWARE_ENABLED:
|
||||
'courseware.masquerade.handle_ajax', name="masquerade_update"),
|
||||
)
|
||||
|
||||
urlpatterns += (
|
||||
url(r'^courses/{}/generate_user_cert'.format(settings.COURSE_ID_PATTERN),
|
||||
'courseware.views.generate_user_cert', name="generate_user_cert"),
|
||||
)
|
||||
|
||||
# discussion forums live within courseware, so courseware must be enabled first
|
||||
if settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
|
||||
urlpatterns += (
|
||||
|
||||
Reference in New Issue
Block a user