Merge pull request #10363 from edx/saleem-latif/SOL-1315
SOL-1315: Add Cert Exception UI on Instructor Dash
This commit is contained in:
@@ -996,6 +996,23 @@ class CertificatesPage(PageObject):
|
|||||||
url = None
|
url = None
|
||||||
PAGE_SELECTOR = 'section#certificates'
|
PAGE_SELECTOR = 'section#certificates'
|
||||||
|
|
||||||
|
def wait_for_certificate_exceptions_section(self):
|
||||||
|
"""
|
||||||
|
Wait for Certificate Exceptions to be rendered on page
|
||||||
|
"""
|
||||||
|
self.wait_for_element_visibility(
|
||||||
|
'div.certificate_exception-container',
|
||||||
|
'Certificate Exception Section is visible'
|
||||||
|
)
|
||||||
|
self.wait_for_element_visibility('#add-exception', 'Add Exception button is visible')
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""
|
||||||
|
Refresh Certificates Page and wait for the page to load completely.
|
||||||
|
"""
|
||||||
|
self.browser.refresh()
|
||||||
|
self.wait_for_page()
|
||||||
|
|
||||||
def is_browser_on_page(self):
|
def is_browser_on_page(self):
|
||||||
return self.q(css='a[data-section=certificates].active-section').present
|
return self.q(css='a[data-section=certificates].active-section').present
|
||||||
|
|
||||||
@@ -1005,6 +1022,33 @@ class CertificatesPage(PageObject):
|
|||||||
"""
|
"""
|
||||||
return self.q(css=' '.join([self.PAGE_SELECTOR, css_selector]))
|
return self.q(css=' '.join([self.PAGE_SELECTOR, css_selector]))
|
||||||
|
|
||||||
|
def add_certificate_exception(self, student, free_text_note):
|
||||||
|
"""
|
||||||
|
Add Certificate Exception for 'student'.
|
||||||
|
"""
|
||||||
|
self.wait_for_element_visibility('#add-exception', 'Add Exception button is visible')
|
||||||
|
|
||||||
|
self.get_selector('#certificate-exception').fill(student)
|
||||||
|
self.get_selector('#notes').fill(free_text_note)
|
||||||
|
self.get_selector('#add-exception').click()
|
||||||
|
|
||||||
|
self.wait_for(
|
||||||
|
lambda: student in self.get_selector('div.white-listed-students table tr:last-child td').text,
|
||||||
|
description='Certificate Exception added to list'
|
||||||
|
)
|
||||||
|
|
||||||
|
def click_generate_certificate_exceptions_button(self): # pylint: disable=invalid-name
|
||||||
|
"""
|
||||||
|
Click 'Generate Exception Certificates' button in 'Certificates Exceptions' section
|
||||||
|
"""
|
||||||
|
self.get_selector('#generate-exception-certificates').click()
|
||||||
|
|
||||||
|
def click_add_exception_button(self):
|
||||||
|
"""
|
||||||
|
Click 'Add Exception' button in 'Certificates Exceptions' section
|
||||||
|
"""
|
||||||
|
self.get_selector('#add-exception').click()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def generate_certificates_button(self):
|
def generate_certificates_button(self):
|
||||||
"""
|
"""
|
||||||
@@ -1025,3 +1069,24 @@ class CertificatesPage(PageObject):
|
|||||||
Returns the "Pending Instructor Tasks" section.
|
Returns the "Pending Instructor Tasks" section.
|
||||||
"""
|
"""
|
||||||
return self.get_selector('div.running-tasks-container')
|
return self.get_selector('div.running-tasks-container')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def certificate_exceptions_section(self):
|
||||||
|
"""
|
||||||
|
Returns the "Certificate Exceptions" section.
|
||||||
|
"""
|
||||||
|
return self.get_selector('div.certificate_exception-container')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_certificate_exception(self):
|
||||||
|
"""
|
||||||
|
Returns the Last Certificate Exception in Certificate Exceptions list in "Certificate Exceptions" section.
|
||||||
|
"""
|
||||||
|
return self.get_selector('div.white-listed-students table tr:last-child td')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
"""
|
||||||
|
Returns the Message (error/success) in "Certificate Exceptions" section.
|
||||||
|
"""
|
||||||
|
return self.get_selector('div.message')
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from ...pages.lms.dashboard import DashboardPage
|
|||||||
from ...pages.lms.problem import ProblemPage
|
from ...pages.lms.problem import ProblemPage
|
||||||
from ...pages.lms.track_selection import TrackSelectionPage
|
from ...pages.lms.track_selection import TrackSelectionPage
|
||||||
from ...pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
|
from ...pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
|
||||||
|
from common.test.acceptance.tests.helpers import disable_animations
|
||||||
|
|
||||||
|
|
||||||
class BaseInstructorDashboardTest(EventsTestMixin, UniqueCourseTest):
|
class BaseInstructorDashboardTest(EventsTestMixin, UniqueCourseTest):
|
||||||
@@ -588,9 +589,10 @@ class CertificatesTest(BaseInstructorDashboardTest):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(CertificatesTest, self).setUp()
|
super(CertificatesTest, self).setUp()
|
||||||
self.course_fixture = CourseFixture(**self.course_info).install()
|
self.course_fixture = CourseFixture(**self.course_info).install()
|
||||||
self.log_in_as_instructor()
|
self.user_name, self.user_id = self.log_in_as_instructor()
|
||||||
instructor_dashboard_page = self.visit_instructor_dashboard()
|
self.instructor_dashboard_page = self.visit_instructor_dashboard()
|
||||||
self.certificates_section = instructor_dashboard_page.select_certificates()
|
self.certificates_section = self.instructor_dashboard_page.select_certificates()
|
||||||
|
disable_animations(self.certificates_section)
|
||||||
|
|
||||||
def test_generate_certificates_buttons_is_visible(self):
|
def test_generate_certificates_buttons_is_visible(self):
|
||||||
"""
|
"""
|
||||||
@@ -621,3 +623,112 @@ class CertificatesTest(BaseInstructorDashboardTest):
|
|||||||
Then I see 'Pending Instructor Tasks' section
|
Then I see 'Pending Instructor Tasks' section
|
||||||
"""
|
"""
|
||||||
self.assertTrue(self.certificates_section.pending_tasks_section.visible)
|
self.assertTrue(self.certificates_section.pending_tasks_section.visible)
|
||||||
|
|
||||||
|
def test_certificate_exceptions_section_is_visible(self):
|
||||||
|
"""
|
||||||
|
Scenario: On the Certificates tab of the Instructor Dashboard, Certificate Exceptions section is visible.
|
||||||
|
Given that I am on the Certificates tab on the Instructor Dashboard
|
||||||
|
Then I see 'CERTIFICATE EXCEPTIONS' section
|
||||||
|
"""
|
||||||
|
self.assertTrue(self.certificates_section.certificate_exceptions_section.visible)
|
||||||
|
|
||||||
|
def test_instructor_can_add_certificate_exception(self):
|
||||||
|
"""
|
||||||
|
Scenario: On the Certificates tab of the Instructor Dashboard, Instructor can added new certificate
|
||||||
|
exception to list
|
||||||
|
|
||||||
|
Given that I am on the Certificates tab on the Instructor Dashboard
|
||||||
|
When I fill in student username and click 'Add Exception' button
|
||||||
|
Then new certificate exception should be visible in certificate exceptions list
|
||||||
|
"""
|
||||||
|
# Add a student to Certificate exception list
|
||||||
|
self.certificates_section.add_certificate_exception(self.user_name, '')
|
||||||
|
self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text)
|
||||||
|
|
||||||
|
def test_error_on_duplicate_certificate_exception(self):
|
||||||
|
"""
|
||||||
|
Scenario: On the Certificates tab of the Instructor Dashboard,
|
||||||
|
Error message appears if student being added already exists in certificate exceptions list
|
||||||
|
|
||||||
|
Given that I am on the Certificates tab on the Instructor Dashboard
|
||||||
|
When I fill in student username that already is in the list and click 'Add Exception' button
|
||||||
|
Then Error Message should say 'username/email already in exception list'
|
||||||
|
"""
|
||||||
|
# Add a student to Certificate exception list
|
||||||
|
self.certificates_section.add_certificate_exception(self.user_name, '')
|
||||||
|
|
||||||
|
# Add duplicate student to Certificate exception list
|
||||||
|
self.certificates_section.add_certificate_exception(self.user_name, '')
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
'username/email already in exception list',
|
||||||
|
self.certificates_section.message.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_error_on_empty_user_name(self):
|
||||||
|
"""
|
||||||
|
Scenario: On the Certificates tab of the Instructor Dashboard,
|
||||||
|
Error message appears if no username/email is entered while clicking "Add Exception" button
|
||||||
|
|
||||||
|
Given that I am on the Certificates tab on the Instructor Dashboard
|
||||||
|
When I click on 'Add Exception' button
|
||||||
|
AND student username/email field is empty
|
||||||
|
Then Error Message should say 'Student username/email is required.'
|
||||||
|
"""
|
||||||
|
# Click 'Add Exception' button without filling username/email field
|
||||||
|
self.certificates_section.wait_for_certificate_exceptions_section()
|
||||||
|
self.certificates_section.click_add_exception_button()
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
'Student username/email is required.',
|
||||||
|
self.certificates_section.message.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_generate_certificate_exception(self):
|
||||||
|
"""
|
||||||
|
Scenario: On the Certificates tab of the Instructor Dashboard, when user clicks
|
||||||
|
'Generate Exception Certificates' newly added certificate exceptions should be synced on server
|
||||||
|
|
||||||
|
Given that I am on the Certificates tab on the Instructor Dashboard
|
||||||
|
When I click 'Generate Exception Certificates'
|
||||||
|
Then newly added certificate exceptions should be synced on server
|
||||||
|
"""
|
||||||
|
# Add a student to Certificate exception list
|
||||||
|
self.certificates_section.add_certificate_exception(self.user_name, '')
|
||||||
|
|
||||||
|
# Click 'Generate Exception Certificates' button
|
||||||
|
self.certificates_section.click_generate_certificate_exceptions_button()
|
||||||
|
self.certificates_section.wait_for_ajax()
|
||||||
|
|
||||||
|
# Revisit Page
|
||||||
|
self.certificates_section.refresh()
|
||||||
|
|
||||||
|
# wait for the certificate exception section to render
|
||||||
|
self.certificates_section.wait_for_certificate_exceptions_section()
|
||||||
|
|
||||||
|
# validate certificate exception synced with server is visible in certificate exceptions list
|
||||||
|
self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text)
|
||||||
|
|
||||||
|
def test_invalid_user_on_generate_certificate_exception(self):
|
||||||
|
"""
|
||||||
|
Scenario: On the Certificates tab of the Instructor Dashboard, when user clicks
|
||||||
|
'Generate Exception Certificates' error message should appear if user does not exist
|
||||||
|
|
||||||
|
Given that I am on the Certificates tab on the Instructor Dashboard
|
||||||
|
When I click 'Generate Exception Certificates'
|
||||||
|
AND the user specified by instructor does not exist
|
||||||
|
Then an error message "Student (username/email=test_user) does not exist" is displayed
|
||||||
|
"""
|
||||||
|
invalid_user = 'test_user_non_existent'
|
||||||
|
# Add a student to Certificate exception list
|
||||||
|
self.certificates_section.add_certificate_exception(invalid_user, '')
|
||||||
|
|
||||||
|
# Click 'Generate Exception Certificates' button
|
||||||
|
self.certificates_section.click_generate_certificate_exceptions_button()
|
||||||
|
self.certificates_section.wait_for_ajax()
|
||||||
|
|
||||||
|
# validate certificate exception synced with server is visible in certificate exceptions list
|
||||||
|
self.assertIn(
|
||||||
|
'Student (username/email={}) does not exist'.format(invalid_user),
|
||||||
|
self.certificates_section.message.text
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from south.utils import datetime_utils as datetime
|
||||||
|
from south.db import db
|
||||||
|
from south.v2 import SchemaMigration
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(SchemaMigration):
|
||||||
|
|
||||||
|
def forwards(self, orm):
|
||||||
|
# Adding field 'CertificateWhitelist.created'
|
||||||
|
db.add_column('certificates_certificatewhitelist', 'created',
|
||||||
|
self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, blank=True),
|
||||||
|
keep_default=False)
|
||||||
|
|
||||||
|
# Adding field 'CertificateWhitelist.notes'
|
||||||
|
db.add_column('certificates_certificatewhitelist', 'notes',
|
||||||
|
self.gf('django.db.models.fields.TextField')(default=None, null=True),
|
||||||
|
keep_default=False)
|
||||||
|
|
||||||
|
|
||||||
|
def backwards(self, orm):
|
||||||
|
# Deleting field 'CertificateWhitelist.created'
|
||||||
|
db.delete_column('certificates_certificatewhitelist', 'created')
|
||||||
|
|
||||||
|
# Deleting field 'CertificateWhitelist.notes'
|
||||||
|
db.delete_column('certificates_certificatewhitelist', 'notes')
|
||||||
|
|
||||||
|
|
||||||
|
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.badgeassertion': {
|
||||||
|
'Meta': {'unique_together': "(('course_id', 'user', 'mode'),)", 'object_name': 'BadgeAssertion'},
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'default': 'None', 'max_length': '255', 'blank': 'True'}),
|
||||||
|
'data': ('django.db.models.fields.TextField', [], {'default': "'{}'"}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'mode': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'certificates.badgeimageconfiguration': {
|
||||||
|
'Meta': {'object_name': 'BadgeImageConfiguration'},
|
||||||
|
'default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'mode': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '125'})
|
||||||
|
},
|
||||||
|
'certificates.certificategenerationconfiguration': {
|
||||||
|
'Meta': {'ordering': "('-change_date',)", '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.certificatehtmlviewconfiguration': {
|
||||||
|
'Meta': {'ordering': "('-change_date',)", 'object_name': 'CertificateHtmlViewConfiguration'},
|
||||||
|
'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'}),
|
||||||
|
'configuration': ('django.db.models.fields.TextField', [], {}),
|
||||||
|
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'certificates.certificatetemplate': {
|
||||||
|
'Meta': {'unique_together': "(('organization_id', 'course_key', 'mode'),)", 'object_name': 'CertificateTemplate'},
|
||||||
|
'course_key': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '125', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||||
|
'organization_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'template': ('django.db.models.fields.TextField', [], {})
|
||||||
|
},
|
||||||
|
'certificates.certificatetemplateasset': {
|
||||||
|
'Meta': {'object_name': 'CertificateTemplateAsset'},
|
||||||
|
'asset': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
|
||||||
|
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'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'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'notes': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': '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': "'7c97c3537f944135b53a6d44ad8774b8'", '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': "'82a6c6ad7a624746910b0dc584d950e0'", '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']
|
||||||
@@ -58,6 +58,7 @@ from django.db.models.signals import post_save
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django_extensions.db.fields import CreationDateTimeField
|
||||||
from django_extensions.db.fields.json import JSONField
|
from django_extensions.db.fields.json import JSONField
|
||||||
from model_utils import Choices
|
from model_utils import Choices
|
||||||
from model_utils.models import TimeStampedModel
|
from model_utils.models import TimeStampedModel
|
||||||
@@ -108,6 +109,40 @@ class CertificateWhitelist(models.Model):
|
|||||||
user = models.ForeignKey(User)
|
user = models.ForeignKey(User)
|
||||||
course_id = CourseKeyField(max_length=255, blank=True, default=None)
|
course_id = CourseKeyField(max_length=255, blank=True, default=None)
|
||||||
whitelist = models.BooleanField(default=0)
|
whitelist = models.BooleanField(default=0)
|
||||||
|
created = CreationDateTimeField(_('created'))
|
||||||
|
notes = models.TextField(default=None, null=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_certificate_white_list(cls, course_id):
|
||||||
|
"""
|
||||||
|
Return certificate white list for the given course as dict object,
|
||||||
|
returned dictionary will have the following key-value pairs
|
||||||
|
|
||||||
|
[{
|
||||||
|
id: 'id (pk) of CertificateWhitelist item'
|
||||||
|
user_id: 'User Id of the student'
|
||||||
|
user_name: 'name of the student'
|
||||||
|
user_email: 'email of the student'
|
||||||
|
course_id: 'Course key of the course to whom certificate exception belongs'
|
||||||
|
created: 'Creation date of the certificate exception'
|
||||||
|
notes: 'Additional notes for the certificate exception'
|
||||||
|
}, {...}, ...]
|
||||||
|
|
||||||
|
"""
|
||||||
|
white_list = cls.objects.filter(course_id=course_id, whitelist=True)
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for item in white_list:
|
||||||
|
result.append({
|
||||||
|
'id': item.id,
|
||||||
|
'user_id': item.user.id,
|
||||||
|
'user_name': unicode(item.user.username),
|
||||||
|
'user_email': unicode(item.user.email),
|
||||||
|
'course_id': unicode(item.course_id),
|
||||||
|
'created': item.created.strftime("%A, %B %d, %Y"),
|
||||||
|
'notes': unicode(item.notes or ''),
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class GeneratedCertificate(models.Model):
|
class GeneratedCertificate(models.Model):
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class CertificateWhitelistFactory(DjangoModelFactory):
|
|||||||
|
|
||||||
course_id = None
|
course_id = None
|
||||||
whitelist = True
|
whitelist = True
|
||||||
|
notes = None
|
||||||
|
|
||||||
|
|
||||||
class BadgeAssertionFactory(DjangoModelFactory):
|
class BadgeAssertionFactory(DjangoModelFactory):
|
||||||
|
|||||||
@@ -204,8 +204,19 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
|
|||||||
super(CertificatesInstructorApiTest, self).setUp()
|
super(CertificatesInstructorApiTest, self).setUp()
|
||||||
self.global_staff = GlobalStaffFactory()
|
self.global_staff = GlobalStaffFactory()
|
||||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||||
|
self.user = UserFactory()
|
||||||
|
|
||||||
# Enable certificate generation
|
# Enable certificate generation
|
||||||
|
self.certificate_exception_data = [
|
||||||
|
dict(
|
||||||
|
created="Wednesday, October 28, 2015",
|
||||||
|
notes="Test Notes for Test Certificate Exception",
|
||||||
|
user_email='',
|
||||||
|
user_id='',
|
||||||
|
user_name=unicode(self.user.username)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
cache.clear()
|
cache.clear()
|
||||||
CertificateGenerationConfiguration.objects.create(enabled=True)
|
CertificateGenerationConfiguration.objects.create(enabled=True)
|
||||||
|
|
||||||
@@ -301,3 +312,138 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
|
|||||||
res_json = json.loads(response.content)
|
res_json = json.loads(response.content)
|
||||||
self.assertIsNotNone(res_json['message'])
|
self.assertIsNotNone(res_json['message'])
|
||||||
self.assertIsNotNone(res_json['task_id'])
|
self.assertIsNotNone(res_json['task_id'])
|
||||||
|
|
||||||
|
def test_certificate_exception_added_successfully(self):
|
||||||
|
"""
|
||||||
|
Test certificates exception addition api endpoint returns success status and updated certificate exception data
|
||||||
|
when called with valid course key and certificate exception data
|
||||||
|
"""
|
||||||
|
self.client.login(username=self.global_staff.username, password='test')
|
||||||
|
url = reverse(
|
||||||
|
'create_certificate_exception',
|
||||||
|
kwargs={'course_id': unicode(self.course.id), 'white_list_student': ''}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data=json.dumps(self.certificate_exception_data),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert successful request processing
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
res_json = json.loads(response.content)
|
||||||
|
|
||||||
|
# Assert Request was successful
|
||||||
|
self.assertTrue(res_json['success'])
|
||||||
|
|
||||||
|
# Assert Success Message
|
||||||
|
self.assertEqual(res_json['message'], u'Students added to Certificate white list successfully')
|
||||||
|
|
||||||
|
# Assert Certificate Exception Updated data
|
||||||
|
certificate_exception = json.loads(res_json['data'])[0]
|
||||||
|
self.assertEqual(certificate_exception['user_email'], self.user.email)
|
||||||
|
self.assertEqual(certificate_exception['user_name'], self.user.username)
|
||||||
|
self.assertEqual(certificate_exception['user_id'], self.user.id) # pylint: disable=no-member
|
||||||
|
|
||||||
|
def test_certificate_exception_invalid_username_error(self):
|
||||||
|
"""
|
||||||
|
Test certificates exception addition api endpoint returns failure when called with
|
||||||
|
invalid username.
|
||||||
|
"""
|
||||||
|
invalid_user = 'test_invalid_user_name'
|
||||||
|
self.certificate_exception_data[0].update({'user_name': invalid_user})
|
||||||
|
|
||||||
|
self.client.login(username=self.global_staff.username, password='test')
|
||||||
|
url = reverse(
|
||||||
|
'create_certificate_exception',
|
||||||
|
kwargs={'course_id': unicode(self.course.id), 'white_list_student': ''}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data=json.dumps(self.certificate_exception_data),
|
||||||
|
content_type='application/json')
|
||||||
|
|
||||||
|
# Assert 400 status code in response
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
res_json = json.loads(response.content)
|
||||||
|
|
||||||
|
# Assert Request not successful
|
||||||
|
self.assertFalse(res_json['success'])
|
||||||
|
|
||||||
|
# Assert Error Message
|
||||||
|
self.assertEqual(
|
||||||
|
res_json['message'],
|
||||||
|
u'Student (username/email={user}) does not exist'.format(user=invalid_user)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_certificate_exception_missing_username_and_email_error(self):
|
||||||
|
"""
|
||||||
|
Test certificates exception addition api endpoint returns failure when called with
|
||||||
|
missing username/email.
|
||||||
|
"""
|
||||||
|
self.certificate_exception_data[0].update({'user_name': '', 'user_email': ''})
|
||||||
|
|
||||||
|
self.client.login(username=self.global_staff.username, password='test')
|
||||||
|
url = reverse(
|
||||||
|
'create_certificate_exception',
|
||||||
|
kwargs={'course_id': unicode(self.course.id), 'white_list_student': ''}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data=json.dumps(self.certificate_exception_data),
|
||||||
|
content_type='application/json')
|
||||||
|
|
||||||
|
# Assert 400 status code in response
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
res_json = json.loads(response.content)
|
||||||
|
|
||||||
|
# Assert Request not successful
|
||||||
|
self.assertFalse(res_json['success'])
|
||||||
|
|
||||||
|
# Assert Error Message
|
||||||
|
self.assertEqual(
|
||||||
|
res_json['message'],
|
||||||
|
u'Student username/email is required.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_certificate_exception_duplicate_user_error(self):
|
||||||
|
"""
|
||||||
|
Test certificates exception addition api endpoint returns failure when called with
|
||||||
|
username/email that already exists in 'CertificateWhitelist' table.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.client.login(username=self.global_staff.username, password='test')
|
||||||
|
url = reverse(
|
||||||
|
'create_certificate_exception',
|
||||||
|
kwargs={'course_id': unicode(self.course.id), 'white_list_student': ''}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
url,
|
||||||
|
data=json.dumps(self.certificate_exception_data),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make some request again to simulate duplicate user scenario
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data=json.dumps(self.certificate_exception_data),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert 400 status code in response
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
res_json = json.loads(response.content)
|
||||||
|
|
||||||
|
# Assert Request not successful
|
||||||
|
self.assertFalse(res_json['success'])
|
||||||
|
|
||||||
|
user = self.certificate_exception_data[0]['user_name']
|
||||||
|
# Assert Error Message
|
||||||
|
self.assertEqual(
|
||||||
|
res_json['message'],
|
||||||
|
u"Student (username/email={user_id} already in certificate exception list)".format(user_id=user)
|
||||||
|
)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from django.views.decorators.cache import cache_control
|
|||||||
from django.core.exceptions import ValidationError, PermissionDenied
|
from django.core.exceptions import ValidationError, PermissionDenied
|
||||||
from django.core.mail.message import EmailMessage
|
from django.core.mail.message import EmailMessage
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
@@ -91,8 +92,10 @@ from instructor.views import INVOICE_KEY
|
|||||||
from submissions import api as sub_api # installed from the edx-submissions repository
|
from submissions import api as sub_api # installed from the edx-submissions repository
|
||||||
|
|
||||||
from certificates import api as certs_api
|
from certificates import api as certs_api
|
||||||
|
from certificates.models import CertificateWhitelist
|
||||||
|
|
||||||
from bulk_email.models import CourseEmail
|
from bulk_email.models import CourseEmail
|
||||||
|
from student.models import get_user_by_username_or_email
|
||||||
|
|
||||||
from .tools import (
|
from .tools import (
|
||||||
dump_student_extensions,
|
dump_student_extensions,
|
||||||
@@ -2703,3 +2706,100 @@ def start_certificate_generation(request, course_id):
|
|||||||
'task_id': task.task_id
|
'task_id': task.task_id
|
||||||
}
|
}
|
||||||
return JsonResponse(response_payload)
|
return JsonResponse(response_payload)
|
||||||
|
|
||||||
|
|
||||||
|
@ensure_csrf_cookie
|
||||||
|
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||||
|
@require_global_staff
|
||||||
|
@require_POST
|
||||||
|
def create_certificate_exception(request, course_id, white_list_student=None):
|
||||||
|
"""
|
||||||
|
Add Students to certificate white list.
|
||||||
|
"""
|
||||||
|
course_key = CourseKey.from_string(course_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
certificate_white_list = json.loads(request.body)
|
||||||
|
except ValueError:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'message': _('Invalid Json data')
|
||||||
|
}, status=400)
|
||||||
|
try:
|
||||||
|
certificate_white_list, students = process_certificate_exceptions(certificate_white_list, course_key)
|
||||||
|
except ValueError as error:
|
||||||
|
return JsonResponse(
|
||||||
|
{'success': False, 'message': error.message, 'data': json.dumps(certificate_white_list)},
|
||||||
|
status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
if white_list_student == 'all':
|
||||||
|
# Generate Certificates for all white listed students
|
||||||
|
students = User.objects.filter(
|
||||||
|
certificatewhitelist__course_id=course_key,
|
||||||
|
certificatewhitelist__whitelist=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if students:
|
||||||
|
# generate certificates for students if 'students' list is not empty
|
||||||
|
instructor_task.api.generate_certificates_for_students(request, course_key, students=students)
|
||||||
|
|
||||||
|
response_payload = {
|
||||||
|
'success': True,
|
||||||
|
'message': _('Students added to Certificate white list successfully'),
|
||||||
|
'data': json.dumps(certificate_white_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponse(response_payload)
|
||||||
|
|
||||||
|
|
||||||
|
def process_certificate_exceptions(data_list, course_key):
|
||||||
|
"""
|
||||||
|
Validate user data for certificate exceptions, raise ValueError in case of invalid data and create
|
||||||
|
'CertificateWhitelist' record for students in data_list.
|
||||||
|
|
||||||
|
return updated data_list after creating 'CertificateWhitelist' records in db.
|
||||||
|
"""
|
||||||
|
students = []
|
||||||
|
users = [data.get('user_name', False) or data.get('user_email', False) for data in data_list]
|
||||||
|
|
||||||
|
if not all(users):
|
||||||
|
# Username and email can not both be empty
|
||||||
|
raise ValueError(_('Student username/email is required.'))
|
||||||
|
|
||||||
|
if len(users) != len(set(users)):
|
||||||
|
# Duplicate Student username/email is not allowed
|
||||||
|
raise ValueError(_('Duplicate Student Username/password.'))
|
||||||
|
|
||||||
|
for data in data_list:
|
||||||
|
user = data.get('user_name', '') or data.get('user_email', '')
|
||||||
|
try:
|
||||||
|
db_user = get_user_by_username_or_email(user)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise ValueError(_('Student (username/email={user}) does not exist').format(user=user))
|
||||||
|
except MultipleObjectsReturned:
|
||||||
|
raise ValueError(_('Multiple Students found with username/email={user}').format(user=user))
|
||||||
|
|
||||||
|
if CertificateWhitelist.objects.filter(user=db_user, whitelist=True).count() > 0:
|
||||||
|
raise ValueError(
|
||||||
|
_("Student (username/email={user_id} already in certificate exception list)").format(user_id=user)
|
||||||
|
)
|
||||||
|
|
||||||
|
certificate_white_list = CertificateWhitelist.objects.create(
|
||||||
|
user=db_user,
|
||||||
|
course_id=course_key,
|
||||||
|
whitelist=True,
|
||||||
|
notes=data.get('notes', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
data.update({
|
||||||
|
'id': certificate_white_list.id,
|
||||||
|
'user_email': db_user.email,
|
||||||
|
'user_name': db_user.username,
|
||||||
|
'user_id': db_user.id,
|
||||||
|
'created': certificate_white_list.created.strftime("%A, %B %d, %Y"),
|
||||||
|
})
|
||||||
|
|
||||||
|
students.append(db_user)
|
||||||
|
|
||||||
|
return data_list, students
|
||||||
|
|||||||
@@ -142,4 +142,8 @@ urlpatterns = patterns(
|
|||||||
url(r'^start_certificate_generation',
|
url(r'^start_certificate_generation',
|
||||||
'instructor.views.api.start_certificate_generation',
|
'instructor.views.api.start_certificate_generation',
|
||||||
name='start_certificate_generation'),
|
name='start_certificate_generation'),
|
||||||
|
|
||||||
|
url(r'^create_certificate_exception/(?P<white_list_student>[^/]*)',
|
||||||
|
'instructor.views.api.create_certificate_exception',
|
||||||
|
name='create_certificate_exception'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from student.models import CourseEnrollment
|
|||||||
from shoppingcart.models import Coupon, PaidCourseRegistration, CourseRegCodeItem
|
from shoppingcart.models import Coupon, PaidCourseRegistration, CourseRegCodeItem
|
||||||
from course_modes.models import CourseMode, CourseModesArchive
|
from course_modes.models import CourseMode, CourseModesArchive
|
||||||
from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole
|
from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole
|
||||||
from certificates.models import CertificateGenerationConfiguration
|
from certificates.models import CertificateGenerationConfiguration, CertificateWhitelist
|
||||||
from certificates import api as certs_api
|
from certificates import api as certs_api
|
||||||
from util.date_utils import get_default_time_display
|
from util.date_utils import get_default_time_display
|
||||||
|
|
||||||
@@ -164,13 +164,21 @@ def instructor_dashboard_2(request, course_id):
|
|||||||
|
|
||||||
disable_buttons = not _is_small_course(course_key)
|
disable_buttons = not _is_small_course(course_key)
|
||||||
|
|
||||||
|
certificate_white_list = CertificateWhitelist.get_certificate_white_list(course_key)
|
||||||
|
certificate_exception_url = reverse(
|
||||||
|
'create_certificate_exception',
|
||||||
|
kwargs={'course_id': unicode(course_key), 'white_list_student': ''}
|
||||||
|
)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'course': course,
|
'course': course,
|
||||||
'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': unicode(course_key)}),
|
'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': unicode(course_key)}),
|
||||||
'studio_url': get_studio_url(course, 'course'),
|
'studio_url': get_studio_url(course, 'course'),
|
||||||
'sections': sections,
|
'sections': sections,
|
||||||
'disable_buttons': disable_buttons,
|
'disable_buttons': disable_buttons,
|
||||||
'analytics_dashboard_message': analytics_dashboard_message
|
'analytics_dashboard_message': analytics_dashboard_message,
|
||||||
|
'certificate_white_list': certificate_white_list,
|
||||||
|
'certificate_exception_url': certificate_exception_url
|
||||||
}
|
}
|
||||||
return render_to_response('instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
|
return render_to_response('instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
|
||||||
|
|
||||||
|
|||||||
@@ -491,3 +491,24 @@ def generate_certificates_for_all_students(request, course_key): # pylint: dis
|
|||||||
task_key = ""
|
task_key = ""
|
||||||
|
|
||||||
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_certificates_for_students(request, course_key, students=None): # pylint: disable=invalid-name
|
||||||
|
"""
|
||||||
|
Submits a task to generate certificates for given students enrolled in the course or
|
||||||
|
all students if argument 'students' is None
|
||||||
|
|
||||||
|
Raises AlreadyRunningError if certificates are currently being generated.
|
||||||
|
"""
|
||||||
|
if students:
|
||||||
|
task_type = 'generate_certificates_certain_student'
|
||||||
|
students = [student.id for student in students]
|
||||||
|
task_input = {'students': students}
|
||||||
|
else:
|
||||||
|
task_type = 'generate_certificates_all_student'
|
||||||
|
task_input = {}
|
||||||
|
|
||||||
|
task_class = generate_certificates
|
||||||
|
task_key = ""
|
||||||
|
|
||||||
|
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
||||||
|
|||||||
@@ -1398,11 +1398,17 @@ def upload_proctored_exam_results_report(_xmodule_instance_args, _entry_id, cour
|
|||||||
def generate_students_certificates(
|
def generate_students_certificates(
|
||||||
_xmodule_instance_args, _entry_id, course_id, task_input, action_name): # pylint: disable=unused-argument
|
_xmodule_instance_args, _entry_id, course_id, task_input, action_name): # pylint: disable=unused-argument
|
||||||
"""
|
"""
|
||||||
For a given `course_id`, generate certificates for all students
|
For a given `course_id`, generate certificates for only students present in 'students' key in task_input
|
||||||
that are enrolled.
|
json column, otherwise generate certificates for all enrolled students.
|
||||||
"""
|
"""
|
||||||
start_time = time()
|
start_time = time()
|
||||||
enrolled_students = CourseEnrollment.objects.users_enrolled_in(course_id)
|
enrolled_students = CourseEnrollment.objects.users_enrolled_in(course_id)
|
||||||
|
|
||||||
|
students = task_input.get('students', None)
|
||||||
|
|
||||||
|
if students is not None:
|
||||||
|
enrolled_students = enrolled_students.filter(id__in=students)
|
||||||
|
|
||||||
task_progress = TaskProgress(action_name, enrolled_students.count(), start_time)
|
task_progress = TaskProgress(action_name, enrolled_students.count(), start_time)
|
||||||
|
|
||||||
current_step = {'step': 'Calculating students already have certificates'}
|
current_step = {'step': 'Calculating students already have certificates'}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Tests that CSV grade report generation works with unicode emails.
|
|||||||
import ddt
|
import ddt
|
||||||
from mock import Mock, patch
|
from mock import Mock, patch
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import json
|
||||||
from openedx.core.djangoapps.course_groups import cohorts
|
from openedx.core.djangoapps.course_groups import cohorts
|
||||||
import unicodecsv
|
import unicodecsv
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
@@ -1611,12 +1612,18 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
|||||||
|
|
||||||
current_task = Mock()
|
current_task = Mock()
|
||||||
current_task.update_state = Mock()
|
current_task.update_state = Mock()
|
||||||
|
instructor_task = Mock()
|
||||||
|
instructor_task.task_input = json.dumps({'students': None})
|
||||||
with self.assertNumQueries(125):
|
with self.assertNumQueries(125):
|
||||||
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
|
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
|
||||||
mock_current_task.return_value = current_task
|
mock_current_task.return_value = current_task
|
||||||
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue:
|
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue:
|
||||||
mock_queue.return_value = (0, "Successfully queued")
|
mock_queue.return_value = (0, "Successfully queued")
|
||||||
result = generate_students_certificates(None, None, self.course.id, None, 'certificates generated')
|
with patch('instructor_task.models.InstructorTask.objects.get') as instructor_task_object:
|
||||||
|
instructor_task_object.return_value = instructor_task
|
||||||
|
result = generate_students_certificates(
|
||||||
|
None, None, self.course.id, {}, 'certificates generated'
|
||||||
|
)
|
||||||
self.assertDictContainsSubset(
|
self.assertDictContainsSubset(
|
||||||
{
|
{
|
||||||
'action_name': 'certificates generated',
|
'action_name': 'certificates generated',
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// Backbone.js Application Collection: CertificateWhiteList
|
||||||
|
/*global define, RequireJS */
|
||||||
|
|
||||||
|
;(function(define){
|
||||||
|
'use strict';
|
||||||
|
define([
|
||||||
|
'backbone',
|
||||||
|
'gettext',
|
||||||
|
'js/certificates/models/certificate_exception'
|
||||||
|
],
|
||||||
|
|
||||||
|
function(Backbone, gettext, CertificateExceptionModel){
|
||||||
|
|
||||||
|
var CertificateWhiteList = Backbone.Collection.extend({
|
||||||
|
model: CertificateExceptionModel,
|
||||||
|
|
||||||
|
initialize: function(attrs, options){
|
||||||
|
this.url = options.url;
|
||||||
|
},
|
||||||
|
|
||||||
|
getModel: function(attrs){
|
||||||
|
var model = this.findWhere({user_name: attrs.user_name});
|
||||||
|
if(attrs.user_name && model){
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
model = this.findWhere({user_email: attrs.user_email});
|
||||||
|
if(attrs.user_email && model){
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
sync: function(options, appended_url){
|
||||||
|
var filtered = this.filter(function(model){
|
||||||
|
return model.isNew();
|
||||||
|
});
|
||||||
|
|
||||||
|
Backbone.sync(
|
||||||
|
'create',
|
||||||
|
new CertificateWhiteList(filtered, {url: this.url + appended_url}),
|
||||||
|
options
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function(data){
|
||||||
|
_.each(data, function(item){
|
||||||
|
var certificate_exception_model =
|
||||||
|
this.getModel({user_name: item.user_name, user_email: item.user_email});
|
||||||
|
certificate_exception_model.set(item);
|
||||||
|
}, this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return CertificateWhiteList;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).call(this, define || RequireJS.define);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Backbone.js Page Object Factory: Certificates
|
||||||
|
/*global define, RequireJS */
|
||||||
|
|
||||||
|
;(function(define){
|
||||||
|
'use strict';
|
||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'js/certificates/views/certificate_whitelist',
|
||||||
|
'js/certificates/models/certificate_exception',
|
||||||
|
'js/certificates/views/certificate_whitelist_editor',
|
||||||
|
'js/certificates/collections/certificate_whitelist'
|
||||||
|
],
|
||||||
|
function($, CertificateWhiteListListView, CertificateExceptionModel, CertificateWhiteListEditorView ,
|
||||||
|
CertificateWhiteListCollection){
|
||||||
|
return function(certificate_white_list_json, certificate_exception_url){
|
||||||
|
|
||||||
|
var certificateWhiteList = new CertificateWhiteListCollection(JSON.parse(certificate_white_list_json), {
|
||||||
|
parse: true,
|
||||||
|
canBeEmpty: true,
|
||||||
|
url: certificate_exception_url
|
||||||
|
});
|
||||||
|
|
||||||
|
new CertificateWhiteListListView({
|
||||||
|
collection: certificateWhiteList
|
||||||
|
}).render();
|
||||||
|
|
||||||
|
new CertificateWhiteListEditorView({
|
||||||
|
collection: certificateWhiteList
|
||||||
|
}).render();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).call(this, define || RequireJS.define);
|
||||||
36
lms/static/js/certificates/models/certificate_exception.js
Normal file
36
lms/static/js/certificates/models/certificate_exception.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Backbone.js Application Model: CertificateWhitelist
|
||||||
|
/*global define, RequireJS */
|
||||||
|
|
||||||
|
;(function(define){
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
define([
|
||||||
|
'underscore',
|
||||||
|
'underscore.string',
|
||||||
|
'backbone',
|
||||||
|
'gettext'
|
||||||
|
],
|
||||||
|
|
||||||
|
function(_, str, Backbone, gettext){
|
||||||
|
|
||||||
|
return Backbone.Model.extend({
|
||||||
|
idAttribute: 'id',
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
user_id: '',
|
||||||
|
user_name: '',
|
||||||
|
user_email: '',
|
||||||
|
created: '',
|
||||||
|
notes: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: function(attrs){
|
||||||
|
if (!_.str.trim(attrs.user_name) && !_.str.trim(attrs.user_email)) {
|
||||||
|
return gettext('Student username/email is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).call(this, define || RequireJS.define);
|
||||||
72
lms/static/js/certificates/views/certificate_whitelist.js
Normal file
72
lms/static/js/certificates/views/certificate_whitelist.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Backbone Application View: CertificateWhitelist View
|
||||||
|
/*global define, RequireJS */
|
||||||
|
|
||||||
|
;(function(define){
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'underscore',
|
||||||
|
'gettext',
|
||||||
|
'backbone'
|
||||||
|
],
|
||||||
|
|
||||||
|
function($, _, gettext, Backbone){
|
||||||
|
return Backbone.View.extend({
|
||||||
|
el: "#white-listed-students",
|
||||||
|
generate_exception_certificates_radio:
|
||||||
|
'input:radio[name=generate-exception-certificates-radio]:checked',
|
||||||
|
|
||||||
|
events: {
|
||||||
|
'click #generate-exception-certificates': 'generateExceptionCertificates'
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function(){
|
||||||
|
// Re-render the view when an item is added to the collection
|
||||||
|
this.listenTo(this.collection, 'change add', this.render);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(){
|
||||||
|
var template = this.loadTemplate('certificate-white-list');
|
||||||
|
this.$el.html(template({certificates: this.collection.models}));
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
loadTemplate: function(name) {
|
||||||
|
var templateSelector = "#" + name + "-tpl",
|
||||||
|
templateText = $(templateSelector).text();
|
||||||
|
return _.template(templateText);
|
||||||
|
},
|
||||||
|
|
||||||
|
generateExceptionCertificates: function(){
|
||||||
|
this.collection.sync(
|
||||||
|
{success: this.showSuccess(this), error: this.showError(this)},
|
||||||
|
$(this.generate_exception_certificates_radio).val()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
showSuccess: function(caller_object){
|
||||||
|
return function(xhr){
|
||||||
|
var response = xhr;
|
||||||
|
$(".message").text(response.message).removeClass('msg-error').addClass('msg-success').focus();
|
||||||
|
caller_object.collection.update(JSON.parse(response.data));
|
||||||
|
$('html, body').animate({
|
||||||
|
scrollTop: $("#certificate-exception").offset().top - 10
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
showError: function(caller_object){
|
||||||
|
return function(xhr){
|
||||||
|
var response = JSON.parse(xhr.responseText);
|
||||||
|
$(".message").text(response.message).removeClass('msg-success').addClass("msg-error").focus();
|
||||||
|
caller_object.collection.update(JSON.parse(response.data));
|
||||||
|
$('html, body').animate({
|
||||||
|
scrollTop: $("#certificate-exception").offset().top - 10
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).call(this, define || RequireJS.define);
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Backbone Application View: CertificateWhiteList Editor View
|
||||||
|
/*global define, RequireJS */
|
||||||
|
|
||||||
|
;(function(define){
|
||||||
|
'use strict';
|
||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'underscore',
|
||||||
|
'gettext',
|
||||||
|
'backbone',
|
||||||
|
'js/certificates/models/certificate_exception'
|
||||||
|
],
|
||||||
|
function($, _, gettext, Backbone, CertificateExceptionModel){
|
||||||
|
return Backbone.View.extend({
|
||||||
|
el: "#certificate-white-list-editor",
|
||||||
|
message_div: '.message',
|
||||||
|
|
||||||
|
events: {
|
||||||
|
'click #add-exception': 'addException'
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(){
|
||||||
|
var template = this.loadTemplate('certificate-white-list-editor');
|
||||||
|
this.$el.html(template());
|
||||||
|
},
|
||||||
|
|
||||||
|
loadTemplate: function(name) {
|
||||||
|
var templateSelector = "#" + name + "-tpl",
|
||||||
|
templateText = $(templateSelector).text();
|
||||||
|
return _.template(templateText);
|
||||||
|
},
|
||||||
|
|
||||||
|
addException: function(){
|
||||||
|
var value = this.$("#certificate-exception").val();
|
||||||
|
var notes = this.$("#notes").val();
|
||||||
|
var user_email = '', user_name='', model={};
|
||||||
|
|
||||||
|
if(this.isEmailAddress(value)){
|
||||||
|
user_email = value;
|
||||||
|
model = {user_email: user_email};
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
user_name = value;
|
||||||
|
model = {user_name: user_name};
|
||||||
|
}
|
||||||
|
|
||||||
|
var certificate_exception = new CertificateExceptionModel({
|
||||||
|
user_name: user_name,
|
||||||
|
user_email: user_email,
|
||||||
|
notes: notes
|
||||||
|
});
|
||||||
|
|
||||||
|
if(this.collection.findWhere(model)){
|
||||||
|
this.showMessage("username/email already in exception list", 'msg-error');
|
||||||
|
}
|
||||||
|
else if(certificate_exception.isValid()){
|
||||||
|
this.collection.add(certificate_exception, {validate: true});
|
||||||
|
this.showMessage("Student Added to exception list", 'msg-success');
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.showMessage(certificate_exception.validationError, 'msg-error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isEmailAddress: function validateEmail(email) {
|
||||||
|
var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
|
||||||
|
return re.test(email);
|
||||||
|
},
|
||||||
|
|
||||||
|
showMessage: function(message, messageClass){
|
||||||
|
this.$(this.message_div).text(message).
|
||||||
|
removeClass('msg-error msg-success').addClass(messageClass).focus();
|
||||||
|
$('html, body').animate({
|
||||||
|
scrollTop: this.$el.offset().top - 20
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).call(this, define || RequireJS.define);
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
/*global define, sinon */
|
||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'common/js/spec_helpers/ajax_helpers',
|
||||||
|
'js/certificates/models/certificate_exception',
|
||||||
|
'js/certificates/views/certificate_whitelist',
|
||||||
|
'js/certificates/views/certificate_whitelist_editor',
|
||||||
|
'js/certificates/collections/certificate_whitelist'
|
||||||
|
],
|
||||||
|
function($, AjaxHelpers, CertificateExceptionModel, CertificateWhiteListView, CertificateWhiteListEditorView,
|
||||||
|
CertificateWhiteListCollection) {
|
||||||
|
'use strict';
|
||||||
|
describe("edx.certificates.models.certificates_exception.CertificateExceptionModel", function() {
|
||||||
|
var certificate_exception = null;
|
||||||
|
var assertValid = function(fields, isValid, expectedErrors) {
|
||||||
|
certificate_exception.set(fields);
|
||||||
|
var errors = certificate_exception.validate(certificate_exception.attributes);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
expect(errors).toBe(undefined);
|
||||||
|
} else {
|
||||||
|
expect(errors).toEqual(expectedErrors);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var EXPECTED_ERRORS = {
|
||||||
|
user_name_or_email_required: "Student username/email is required."
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
|
||||||
|
certificate_exception = new CertificateExceptionModel({user_name: 'test_user'});
|
||||||
|
certificate_exception.set({
|
||||||
|
notes: "Test notes"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts valid email addresses", function() {
|
||||||
|
assertValid({user_email: "bob@example.com"}, true);
|
||||||
|
assertValid({user_email: "bob+smith@example.com"}, true);
|
||||||
|
assertValid({user_email: "bob+smith@example.com"}, true);
|
||||||
|
assertValid({user_email: "bob+smith@example.com"}, true);
|
||||||
|
assertValid({user_email: "bob@test.example.com"}, true);
|
||||||
|
assertValid({user_email: "bob@test-example.com"}, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays username or email required error", function() {
|
||||||
|
assertValid({user_name: ""}, false, EXPECTED_ERRORS.user_name_or_email_required);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("edx.certificates.collections.certificate_whitelist.CertificateWhiteList", function() {
|
||||||
|
var certificate_white_list = null,
|
||||||
|
certificate_exception_url = 'test/url/';
|
||||||
|
var certificates_exceptions_json = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
user_id: "1",
|
||||||
|
user_name: "test1",
|
||||||
|
user_email: "test1@test.com",
|
||||||
|
course_id: "edX/test/course",
|
||||||
|
created: "Thursday, October 29, 2015",
|
||||||
|
notes: "test notes for test certificate exception"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
user_id : "2",
|
||||||
|
user_name: "test2",
|
||||||
|
user_email : "test2@test.com",
|
||||||
|
course_id: "edX/test/course",
|
||||||
|
created: "Thursday, October 29, 2015",
|
||||||
|
notes: "test notes for test certificate exception"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
certificate_white_list = new CertificateWhiteListCollection(certificates_exceptions_json, {
|
||||||
|
parse: true,
|
||||||
|
canBeEmpty: true,
|
||||||
|
url: certificate_exception_url
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has 2 models in the collection after initialization", function() {
|
||||||
|
expect(certificate_white_list.models.length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct model on getModel call and 'undefined' if queried model is not present", function() {
|
||||||
|
expect(certificate_white_list.getModel({user_name: 'test1'})).not.toBe(undefined);
|
||||||
|
expect(certificate_white_list.getModel({user_name: 'test_invalid_user'})).toBe(undefined);
|
||||||
|
|
||||||
|
expect(certificate_white_list.getModel({user_email: 'test1@test.com'})).not.toBe(undefined);
|
||||||
|
expect(certificate_white_list.getModel({user_email: 'test_invalid_user@test.com'})).toBe(undefined);
|
||||||
|
|
||||||
|
expect(certificate_white_list.getModel({user_name: 'test1'}).attributes).toEqual(
|
||||||
|
{
|
||||||
|
id: '1', user_id: '1', user_name: 'test1', user_email: 'test1@test.com',
|
||||||
|
course_id: 'edX/test/course', created: "Thursday, October 29, 2015",
|
||||||
|
notes: 'test notes for test certificate exception'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(certificate_white_list.getModel({user_email: 'test2@test.com'}).attributes).toEqual(
|
||||||
|
{
|
||||||
|
id: '2', user_id: '2', user_name: 'test2', user_email: 'test2@test.com',
|
||||||
|
course_id: 'edX/test/course', created: "Thursday, October 29, 2015",
|
||||||
|
notes: 'test notes for test certificate exception'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends empty certificate exceptions list if no model is added', function(){
|
||||||
|
var successCallback = sinon.spy(),
|
||||||
|
errorCallback = sinon.spy(),
|
||||||
|
requests = AjaxHelpers.requests(this),
|
||||||
|
add_students = 'all';
|
||||||
|
var expected = {
|
||||||
|
url: certificate_exception_url + add_students,
|
||||||
|
postData : []
|
||||||
|
};
|
||||||
|
|
||||||
|
certificate_white_list.sync({success: successCallback, error: errorCallback}, add_students);
|
||||||
|
AjaxHelpers.expectJsonRequest(requests, 'POST', expected.url, expected.postData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs only newly added models with the server', function(){
|
||||||
|
var successCallback = sinon.spy(),
|
||||||
|
errorCallback = sinon.spy(),
|
||||||
|
requests = AjaxHelpers.requests(this),
|
||||||
|
add_students = 'new';
|
||||||
|
|
||||||
|
certificate_white_list.add({user_name: 'test3', notes: 'test3 notes'});
|
||||||
|
certificate_white_list.sync({success: successCallback, error: errorCallback}, add_students);
|
||||||
|
|
||||||
|
var expected = {
|
||||||
|
url: certificate_exception_url + add_students,
|
||||||
|
postData : [
|
||||||
|
{user_id: "",
|
||||||
|
user_name: "test3",
|
||||||
|
user_email: "",
|
||||||
|
created: "",
|
||||||
|
notes: "test3 notes"}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
AjaxHelpers.expectJsonRequest(requests, 'POST', expected.url, expected.postData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("edx.certificates.views.certificate_whitelist.CertificateWhiteListView", function() {
|
||||||
|
var view = null,
|
||||||
|
certificate_exception_url = 'test/url/';
|
||||||
|
|
||||||
|
var certificates_exceptions_json = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
user_id: "1",
|
||||||
|
user_name: "test1",
|
||||||
|
user_email: "test1@test.com",
|
||||||
|
course_id: "edX/test/course",
|
||||||
|
created: "Thursday, October 29, 2015",
|
||||||
|
notes: "test notes for test certificate exception"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
user_id : "2",
|
||||||
|
user_name: "test2",
|
||||||
|
user_email : "test2@test.com",
|
||||||
|
course_id: "edX/test/course",
|
||||||
|
created: "Thursday, October 29, 2015",
|
||||||
|
notes: "test notes for test certificate exception"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
setFixtures();
|
||||||
|
var fixture =
|
||||||
|
readFixtures("templates/instructor/instructor_dashboard_2/certificate-white-list.underscore");
|
||||||
|
setFixtures("<script type='text/template' id='certificate-white-list-tpl'>" + fixture + "</script>" +
|
||||||
|
"<div class='white-listed-students' id='white-listed-students'></div>");
|
||||||
|
|
||||||
|
var certificate_white_list = new CertificateWhiteListCollection(certificates_exceptions_json, {
|
||||||
|
parse: true,
|
||||||
|
canBeEmpty: true,
|
||||||
|
url: certificate_exception_url
|
||||||
|
});
|
||||||
|
|
||||||
|
view = new CertificateWhiteListView({collection: certificate_white_list});
|
||||||
|
view.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verifies view is initialized and rendered successfully", function() {
|
||||||
|
expect(view).not.toBe(undefined);
|
||||||
|
expect(view.$el.find('table tbody tr').length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verifies view is rendered on add/update to collection", function() {
|
||||||
|
var user = 'test1',
|
||||||
|
notes = 'test1 notes updates',
|
||||||
|
email='update_email@test.com';
|
||||||
|
|
||||||
|
// Add another model in collection and verify it is rendered
|
||||||
|
view.collection.add({user_name: 'test3', notes: 'test3 notes'});
|
||||||
|
expect(view.$el.find('table tbody tr').length).toBe(3);
|
||||||
|
|
||||||
|
// Update a model in collection and verify it is rendered
|
||||||
|
view.collection.update([
|
||||||
|
{user_name: user, notes: notes, user_email: email}
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(view.$el.find('table tbody tr td:contains("' + user + '")').parent().html()).
|
||||||
|
toMatch(notes);
|
||||||
|
expect(view.$el.find('table tbody tr td:contains("' + user + '")').parent().html()).
|
||||||
|
toMatch(email);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verifies collection sync is called when "Generate Exception Certificates" is clicked', function(){
|
||||||
|
var successCallback = sinon.spy(),
|
||||||
|
errorCallback = sinon.spy();
|
||||||
|
|
||||||
|
sinon.stub(view, "showSuccess").returns(successCallback);
|
||||||
|
sinon.stub(view, "showError").returns(errorCallback);
|
||||||
|
sinon.stub(view.collection, "sync");
|
||||||
|
|
||||||
|
view.$el.find("#generate-exception-certificates").click();
|
||||||
|
|
||||||
|
expect(view.collection.sync.called).toBe(true);
|
||||||
|
expect(view.collection.sync.calledWith({success: successCallback, error: errorCallback})).
|
||||||
|
toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verifies sync is called with "new/all" argument depending upon selected radio button', function(){
|
||||||
|
var successCallback = sinon.spy(),
|
||||||
|
errorCallback = sinon.spy();
|
||||||
|
|
||||||
|
sinon.stub(view, "showSuccess").returns(successCallback);
|
||||||
|
sinon.stub(view, "showError").returns(errorCallback);
|
||||||
|
sinon.stub(view.collection, "sync");
|
||||||
|
|
||||||
|
view.$el.find("#generate-exception-certificates").click();
|
||||||
|
|
||||||
|
// By default 'Generate a Certificate for all New additions to the Exception list ' is selected
|
||||||
|
expect(view.collection.sync.calledWith({success: successCallback, error: errorCallback}), 'new').
|
||||||
|
toBe(true);
|
||||||
|
|
||||||
|
// Select 'Generate a Certificate for all users on the Exception list ' option
|
||||||
|
view.$el.find("input:radio[name=generate-exception-certificates-radio][value=all]").click();
|
||||||
|
view.$el.find("#generate-exception-certificates").click();
|
||||||
|
expect(view.collection.sync.calledWith({success: successCallback, error: errorCallback}), 'all').
|
||||||
|
toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("edx.certificates.views.certificate_whitelist_editor.CertificateWhiteListEditorView", function() {
|
||||||
|
var view = null,
|
||||||
|
certificate_exception_url = 'test/url/';
|
||||||
|
var certificates_exceptions_json = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
user_id: "1",
|
||||||
|
user_name: "test1",
|
||||||
|
user_email: "test1@test.com",
|
||||||
|
course_id: "edX/test/course",
|
||||||
|
created: "Thursday, October 29, 2015",
|
||||||
|
notes: "test notes for test certificate exception"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
user_id : "2",
|
||||||
|
user_name: "test2",
|
||||||
|
user_email : "test2@test.com",
|
||||||
|
course_id: "edX/test/course",
|
||||||
|
created: "Thursday, October 29, 2015",
|
||||||
|
notes: "test notes for test certificate exception"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
setFixtures();
|
||||||
|
|
||||||
|
var fixture = readFixtures(
|
||||||
|
"templates/instructor/instructor_dashboard_2/certificate-white-list-editor.underscore"
|
||||||
|
);
|
||||||
|
|
||||||
|
setFixtures(
|
||||||
|
"<script type='text/template' id='certificate-white-list-editor-tpl'>" + fixture + "</script>" +
|
||||||
|
"<div id='certificate-white-list-editor'></div>"
|
||||||
|
);
|
||||||
|
|
||||||
|
var certificate_white_list = new CertificateWhiteListCollection(certificates_exceptions_json, {
|
||||||
|
parse: true,
|
||||||
|
canBeEmpty: true,
|
||||||
|
url: certificate_exception_url
|
||||||
|
});
|
||||||
|
|
||||||
|
view = new CertificateWhiteListEditorView({collection: certificate_white_list});
|
||||||
|
view.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verifies view is initialized and rendered successfully", function() {
|
||||||
|
expect(view).not.toBe(undefined);
|
||||||
|
expect(view.$el.find('#certificate-exception').length).toBe(1);
|
||||||
|
expect(view.$el.find('#notes').length).toBe(1);
|
||||||
|
expect(view.$el.find('#add-exception').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verifies success and error messages", function() {
|
||||||
|
var message_selector='.message',
|
||||||
|
error_class = 'msg-error',
|
||||||
|
success_class = 'msg-success',
|
||||||
|
success_message = 'Student Added to exception list';
|
||||||
|
|
||||||
|
var error_messages = {
|
||||||
|
empty_user_name_email: 'Student username/email is required.',
|
||||||
|
duplicate_user: 'username/email already in exception list'
|
||||||
|
};
|
||||||
|
|
||||||
|
// click 'Add Exception' button with empty username/email field
|
||||||
|
view.$el.find('#add-exception').click();
|
||||||
|
|
||||||
|
// Verify error message for missing username/email
|
||||||
|
expect(view.$el.find(message_selector)).toHaveClass(error_class);
|
||||||
|
expect(view.$el.find(message_selector).html()).toMatch(error_messages.empty_user_name_email);
|
||||||
|
|
||||||
|
// Add a new Exception to list
|
||||||
|
view.$el.find('#certificate-exception').val("test_user");
|
||||||
|
view.$el.find('#notes').val("test user notes");
|
||||||
|
view.$el.find('#add-exception').click();
|
||||||
|
|
||||||
|
// Verify success message
|
||||||
|
expect(view.$el.find(message_selector)).toHaveClass(success_class);
|
||||||
|
expect(view.$el.find(message_selector).html()).toMatch(success_message);
|
||||||
|
|
||||||
|
// Add a duplicate Certificate Exception
|
||||||
|
view.$el.find('#certificate-exception').val("test_user");
|
||||||
|
view.$el.find('#notes').val("test user notes");
|
||||||
|
view.$el.find('#add-exception').click();
|
||||||
|
|
||||||
|
// Verify success message
|
||||||
|
expect(view.$el.find(message_selector)).toHaveClass(error_class);
|
||||||
|
expect(view.$el.find(message_selector).html()).toMatch(error_messages.duplicate_user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -643,6 +643,7 @@
|
|||||||
'lms/include/js/spec/shoppingcart/shoppingcart_spec.js',
|
'lms/include/js/spec/shoppingcart/shoppingcart_spec.js',
|
||||||
'lms/include/js/spec/instructor_dashboard/ecommerce_spec.js',
|
'lms/include/js/spec/instructor_dashboard/ecommerce_spec.js',
|
||||||
'lms/include/js/spec/instructor_dashboard/student_admin_spec.js',
|
'lms/include/js/spec/instructor_dashboard/student_admin_spec.js',
|
||||||
|
'lms/include/js/spec/instructor_dashboard/certificates_exception_spec.js',
|
||||||
'lms/include/js/spec/student_account/account_spec.js',
|
'lms/include/js/spec/student_account/account_spec.js',
|
||||||
'lms/include/js/spec/student_account/access_spec.js',
|
'lms/include/js/spec/student_account/access_spec.js',
|
||||||
'lms/include/js/spec/student_account/logistration_factory_spec.js',
|
'lms/include/js/spec/student_account/logistration_factory_spec.js',
|
||||||
|
|||||||
@@ -2123,3 +2123,84 @@ input[name="subject"] {
|
|||||||
@include left(2em);
|
@include left(2em);
|
||||||
@include right(auto);
|
@include right(auto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#certificate-white-list-editor{
|
||||||
|
.msg-success{
|
||||||
|
border-top: 2px solid $confirm-color;
|
||||||
|
background: tint($confirm-color,95%);
|
||||||
|
color: $confirm-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-exception-inputs{
|
||||||
|
.student-username-or-email{
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
.notes-field{
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
p+p{
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.message{
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.white-listed-students {
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
th {
|
||||||
|
@extend %t-copy-sub2;
|
||||||
|
background-color: $gray-l5;
|
||||||
|
padding: ($baseline*0.75) ($baseline/2) ($baseline*0.75) ($baseline/2);
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: left;
|
||||||
|
color: $gray;
|
||||||
|
|
||||||
|
&.date-column{
|
||||||
|
width: 230px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: ($baseline/2);
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
box-shadow: 0 2px 2px $shadow-l1;
|
||||||
|
border: 1px solid $gray-l4;
|
||||||
|
background: $white;
|
||||||
|
|
||||||
|
tr {
|
||||||
|
@include transition(all $tmg-f2 ease-in-out 0s);
|
||||||
|
border-top: 1px solid $gray-l4;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
background-color: $gray-l6;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $gray-d1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $blue-l5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<div class='certificate-exception-inputs'>
|
||||||
|
<input class='student-username-or-email' id="certificate-exception" type="text" placeholder="Student email or username" aria-describedby='student-user-name-or-email-tip'>
|
||||||
|
<textarea class='notes-field' id="notes" rows="10" placeholder="Free text notes" aria-describedby='notes-field-tip'></textarea>
|
||||||
|
<input type="button" id="add-exception" value="Add Exception">
|
||||||
|
|
||||||
|
<p id='student-user-name-or-email-tip'><%- gettext("Specify either Student's username or email for whom to create certificate exception") %></p>
|
||||||
|
<p id='notes-field-tip'><%- gettext("Enter Notes associated with this certificate exception") %></p>
|
||||||
|
<div class='message'></div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<% if (certificates.length === 0) { %>
|
||||||
|
<p><%- gettext("No results") %></p>
|
||||||
|
<% } else { %>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th><%- gettext("Name") %></th>
|
||||||
|
<th><%- gettext("User ID") %></th>
|
||||||
|
<th><%- gettext("User Email") %></th>
|
||||||
|
<th class='date-column'><%- gettext("Date Exception Granted") %></th>
|
||||||
|
<th><%- gettext("Notes") %></th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% for (var i = 0; i < certificates.length; i++) {
|
||||||
|
var cert = certificates[i];
|
||||||
|
%>
|
||||||
|
<tr>
|
||||||
|
<td><%- cert.get("user_name") %></td>
|
||||||
|
<td><%- cert.get("user_id") %></td>
|
||||||
|
<td><%- cert.get("user_email") %></td>
|
||||||
|
<td><%- cert.get("created") %></td>
|
||||||
|
<td><%- cert.get("notes") %></td>
|
||||||
|
</tr>
|
||||||
|
<% } %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<label>
|
||||||
|
<input type='radio' name='generate-exception-certificates-radio' checked="checked" value='new' aria-describedby='generate-exception-certificates-radio-new-tip'>
|
||||||
|
<span id='generate-exception-certificates-radio-new-tip'><%- gettext('Generate a Certificate for all ') %><strong><%- gettext('New') %></strong> <%- gettext('additions to the Exception list') %></span>
|
||||||
|
</label>
|
||||||
|
<br/>
|
||||||
|
<label>
|
||||||
|
<input type='radio' name='generate-exception-certificates-radio' value='all' aria-describedby='generate-exception-certificates-radio-all-tip'>
|
||||||
|
<span id='generate-exception-certificates-radio-all-tip'><%- gettext('Generate a Certificate for all users on the Exception list') %></span>
|
||||||
|
</label>
|
||||||
|
<br/>
|
||||||
|
<input type="button" id="generate-exception-certificates" value="<%- gettext('Generate Exception Certificates') %>" />
|
||||||
@@ -1,4 +1,13 @@
|
|||||||
<%! from django.utils.translation import ugettext as _ %>
|
<%namespace name='static' file='../../static_content.html'/>
|
||||||
|
|
||||||
|
<%! from django.utils.translation import ugettext as _
|
||||||
|
import json
|
||||||
|
%>
|
||||||
|
|
||||||
|
<%static:require_module module_name="js/certificates/factories/certificate_whitelist_factory" class_name="CertificateWhitelistFactory">
|
||||||
|
CertificateWhitelistFactory('${json.dumps(certificate_white_list)}', "${certificate_exception_url}");
|
||||||
|
</%static:require_module>
|
||||||
|
|
||||||
<%page args="section_data"/>
|
<%page args="section_data"/>
|
||||||
<div class="certificates-wrapper">
|
<div class="certificates-wrapper">
|
||||||
|
|
||||||
@@ -82,4 +91,21 @@
|
|||||||
</div>
|
</div>
|
||||||
%endif
|
%endif
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
<div class="certificate_exception-container">
|
||||||
|
<hr>
|
||||||
|
<h2> ${_("Certificate Exceptions")} </h2>
|
||||||
|
<div class="certificate-exception-section">
|
||||||
|
<p>${_("Use this to generate certificates for users who did not pass the course but have been given an exception by the Course Team to earn a certificate.")} </p>
|
||||||
|
<br />
|
||||||
|
<div id="certificate-white-list-editor"></div>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<div class="white-listed-students" id="white-listed-students"></div>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
</div>
|
||||||
|
<div class="no-pending-tasks-message"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ from django.core.urlresolvers import reverse
|
|||||||
|
|
||||||
## Include Underscore templates
|
## Include Underscore templates
|
||||||
<%block name="header_extras">
|
<%block name="header_extras">
|
||||||
% for template_name in ["cohorts", "enrollment-code-lookup-links", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification", "cohort-state", "cohort-discussions-inline", "cohort-discussions-course-wide", "cohort-discussions-category","cohort-discussions-subcategory"]:
|
% for template_name in ["cohorts", "enrollment-code-lookup-links", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification", "cohort-state", "cohort-discussions-inline", "cohort-discussions-course-wide", "cohort-discussions-category","cohort-discussions-subcategory","certificate-white-list","certificate-white-list-editor"]:
|
||||||
<script type="text/template" id="${template_name}-tpl">
|
<script type="text/template" id="${template_name}-tpl">
|
||||||
<%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" />
|
<%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" />
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user