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
|
||||
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):
|
||||
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]))
|
||||
|
||||
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
|
||||
def generate_certificates_button(self):
|
||||
"""
|
||||
@@ -1025,3 +1069,24 @@ class CertificatesPage(PageObject):
|
||||
Returns the "Pending Instructor Tasks" section.
|
||||
"""
|
||||
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.track_selection import TrackSelectionPage
|
||||
from ...pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
|
||||
from common.test.acceptance.tests.helpers import disable_animations
|
||||
|
||||
|
||||
class BaseInstructorDashboardTest(EventsTestMixin, UniqueCourseTest):
|
||||
@@ -588,9 +589,10 @@ class CertificatesTest(BaseInstructorDashboardTest):
|
||||
def setUp(self):
|
||||
super(CertificatesTest, self).setUp()
|
||||
self.course_fixture = CourseFixture(**self.course_info).install()
|
||||
self.log_in_as_instructor()
|
||||
instructor_dashboard_page = self.visit_instructor_dashboard()
|
||||
self.certificates_section = instructor_dashboard_page.select_certificates()
|
||||
self.user_name, self.user_id = self.log_in_as_instructor()
|
||||
self.instructor_dashboard_page = self.visit_instructor_dashboard()
|
||||
self.certificates_section = self.instructor_dashboard_page.select_certificates()
|
||||
disable_animations(self.certificates_section)
|
||||
|
||||
def test_generate_certificates_buttons_is_visible(self):
|
||||
"""
|
||||
@@ -621,3 +623,112 @@ class CertificatesTest(BaseInstructorDashboardTest):
|
||||
Then I see 'Pending Instructor Tasks' section
|
||||
"""
|
||||
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.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_extensions.db.fields import CreationDateTimeField
|
||||
from django_extensions.db.fields.json import JSONField
|
||||
from model_utils import Choices
|
||||
from model_utils.models import TimeStampedModel
|
||||
@@ -108,6 +109,40 @@ class CertificateWhitelist(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
course_id = CourseKeyField(max_length=255, blank=True, default=None)
|
||||
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):
|
||||
|
||||
@@ -30,6 +30,7 @@ class CertificateWhitelistFactory(DjangoModelFactory):
|
||||
|
||||
course_id = None
|
||||
whitelist = True
|
||||
notes = None
|
||||
|
||||
|
||||
class BadgeAssertionFactory(DjangoModelFactory):
|
||||
|
||||
@@ -204,8 +204,19 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
|
||||
super(CertificatesInstructorApiTest, self).setUp()
|
||||
self.global_staff = GlobalStaffFactory()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.user = UserFactory()
|
||||
|
||||
# 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()
|
||||
CertificateGenerationConfiguration.objects.create(enabled=True)
|
||||
|
||||
@@ -301,3 +312,138 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
|
||||
res_json = json.loads(response.content)
|
||||
self.assertIsNotNone(res_json['message'])
|
||||
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.mail.message import EmailMessage
|
||||
from django.db import IntegrityError
|
||||
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import validate_email
|
||||
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 certificates import api as certs_api
|
||||
from certificates.models import CertificateWhitelist
|
||||
|
||||
from bulk_email.models import CourseEmail
|
||||
from student.models import get_user_by_username_or_email
|
||||
|
||||
from .tools import (
|
||||
dump_student_extensions,
|
||||
@@ -2703,3 +2706,100 @@ def start_certificate_generation(request, course_id):
|
||||
'task_id': task.task_id
|
||||
}
|
||||
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',
|
||||
'instructor.views.api.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 course_modes.models import CourseMode, CourseModesArchive
|
||||
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 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)
|
||||
|
||||
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 = {
|
||||
'course': course,
|
||||
'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': unicode(course_key)}),
|
||||
'studio_url': get_studio_url(course, 'course'),
|
||||
'sections': sections,
|
||||
'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)
|
||||
|
||||
|
||||
@@ -491,3 +491,24 @@ def generate_certificates_for_all_students(request, course_key): # pylint: dis
|
||||
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(
|
||||
_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
|
||||
that are enrolled.
|
||||
For a given `course_id`, generate certificates for only students present in 'students' key in task_input
|
||||
json column, otherwise generate certificates for all enrolled students.
|
||||
"""
|
||||
start_time = time()
|
||||
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)
|
||||
|
||||
current_step = {'step': 'Calculating students already have certificates'}
|
||||
|
||||
@@ -9,6 +9,7 @@ Tests that CSV grade report generation works with unicode emails.
|
||||
import ddt
|
||||
from mock import Mock, patch
|
||||
import tempfile
|
||||
import json
|
||||
from openedx.core.djangoapps.course_groups import cohorts
|
||||
import unicodecsv
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -1611,12 +1612,18 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
|
||||
current_task = Mock()
|
||||
current_task.update_state = Mock()
|
||||
instructor_task = Mock()
|
||||
instructor_task.task_input = json.dumps({'students': None})
|
||||
with self.assertNumQueries(125):
|
||||
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
|
||||
mock_current_task.return_value = current_task
|
||||
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue:
|
||||
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(
|
||||
{
|
||||
'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/instructor_dashboard/ecommerce_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/access_spec.js',
|
||||
'lms/include/js/spec/student_account/logistration_factory_spec.js',
|
||||
|
||||
@@ -2123,3 +2123,84 @@ input[name="subject"] {
|
||||
@include left(2em);
|
||||
@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"/>
|
||||
<div class="certificates-wrapper">
|
||||
|
||||
@@ -82,4 +91,21 @@
|
||||
</div>
|
||||
%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>
|
||||
|
||||
@@ -66,7 +66,7 @@ from django.core.urlresolvers import reverse
|
||||
|
||||
## Include Underscore templates
|
||||
<%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">
|
||||
<%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" />
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user