Commit 726b7f2c by Awais

ECOM-1046 adding functionality for generating the certs.

ECOM-1046 minor change in code. rename file name.
parent 1d5b2e2b
"""
django admin pages for certificates models
"""
from django.contrib import admin
from certificates.models import CertificateGenerationConfiguration
admin.site.register(CertificateGenerationConfiguration)
"""
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
# -*- 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']
# -*- 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
"""
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
......
$(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 += (
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment