From 7c68d80c670b4b87bbd61127aa3559e40f5bbd3b Mon Sep 17 00:00:00 2001 From: asadiqbal Date: Thu, 26 Nov 2015 15:43:10 +0500 Subject: [PATCH] SOL-1417 --- .../pages/lms/instructor_dashboard.py | 4 +- .../instructor/tests/test_certificates.py | 160 +++++++++++++++ lms/djangoapps/instructor/views/api.py | 96 ++++++++- lms/djangoapps/instructor/views/api_urls.py | 4 + .../instructor/views/instructor_dashboard.py | 5 + .../certificate_whitelist_factory.js | 11 +- .../views/certificate_bulk_whitelist.js | 193 ++++++++++++++++++ .../sass/course/instructor/_instructor_2.scss | 83 ++++---- .../certificate-bulk-white-list.underscore | 17 ++ .../certificate-white-list-editor.underscore | 4 +- .../instructor_dashboard_2/certificates.html | 10 +- .../instructor_dashboard_2.html | 2 +- 12 files changed, 539 insertions(+), 50 deletions(-) create mode 100644 lms/static/js/certificates/views/certificate_bulk_whitelist.js create mode 100644 lms/templates/instructor/instructor_dashboard_2/certificate-bulk-white-list.underscore diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py index f89910a80e..7423761898 100644 --- a/common/test/acceptance/pages/lms/instructor_dashboard.py +++ b/common/test/acceptance/pages/lms/instructor_dashboard.py @@ -1001,7 +1001,7 @@ class CertificatesPage(PageObject): Wait for Certificate Exceptions to be rendered on page """ self.wait_for_element_visibility( - 'div.certificate_exception-container', + 'div.certificate-exception-container', 'Certificate Exception Section is visible' ) self.wait_for_element_visibility('#add-exception', 'Add Exception button is visible') @@ -1097,7 +1097,7 @@ class CertificatesPage(PageObject): """ Returns the "Certificate Exceptions" section. """ - return self.get_selector('div.certificate_exception-container') + return self.get_selector('div.certificate-exception-container') @property def last_certificate_exception(self): diff --git a/lms/djangoapps/instructor/tests/test_certificates.py b/lms/djangoapps/instructor/tests/test_certificates.py index c3622536bc..a6779be67c 100644 --- a/lms/djangoapps/instructor/tests/test_certificates.py +++ b/lms/djangoapps/instructor/tests/test_certificates.py @@ -18,6 +18,8 @@ from certificates.models import CertificateGenerationConfiguration, CertificateS GeneratedCertificate from certificates import api as certs_api from student.models import CourseEnrollment +from django.core.files.uploadedfile import SimpleUploadedFile +import io @attr('shard_1') @@ -734,3 +736,161 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase): res_json['message'], u"Invalid data, user_id must be present for all certificate exceptions." ) + + +@attr('shard_1') +@ddt.ddt +class TestCertificatesInstructorApiBulkWhiteListExceptions(SharedModuleStoreTestCase): + """ + Test Bulk certificates white list exceptions from csv file + """ + @classmethod + def setUpClass(cls): + super(TestCertificatesInstructorApiBulkWhiteListExceptions, cls).setUpClass() + cls.course = CourseFactory.create() + cls.url = reverse('generate_bulk_certificate_exceptions', + kwargs={'course_id': cls.course.id}) + + def setUp(self): + super(TestCertificatesInstructorApiBulkWhiteListExceptions, self).setUp() + self.global_staff = GlobalStaffFactory() + self.enrolled_user_1 = UserFactory( + username='TestStudent1', + email='test_student1@example.com', + first_name='Enrolled', + last_name='Student' + ) + self.enrolled_user_2 = UserFactory( + username='TestStudent2', + email='test_student2@example.com', + first_name='Enrolled', + last_name='Student' + ) + + self.not_enrolled_student = UserFactory( + username='NotEnrolledStudent', + email='nonenrolled@test.com', + first_name='NotEnrolled', + last_name='Student' + ) + CourseEnrollment.enroll(self.enrolled_user_1, self.course.id) + CourseEnrollment.enroll(self.enrolled_user_2, self.course.id) + + # Global staff can see the certificates section + self.client.login(username=self.global_staff.username, password="test") + + def test_create_white_list_exception_record(self): + """ + Happy path test to create a single new white listed record + """ + csv_content = "test_student1@example.com,dummy_notes\n" \ + "test_student2@example.com,dummy_notes" + data = self.upload_file(csv_content=csv_content) + self.assertEquals(len(data['general_errors']), 0) + self.assertEquals(len(data['row_errors']['data_format_error']), 0) + self.assertEquals(len(data['row_errors']['user_not_exist']), 0) + self.assertEquals(len(data['row_errors']['user_already_white_listed']), 0) + self.assertEquals(len(data['row_errors']['user_not_enrolled']), 0) + self.assertEquals(len(data['success']), 2) + self.assertEquals(len(CertificateWhitelist.objects.all()), 2) + + def test_invalid_data_format_in_csv(self): + """ + Try uploading a CSV file with invalid data formats and verify the errors. + """ + csv_content = "test_student1@example.com,test,1,USA\n" \ + "test_student2@example.com,test,1" + + data = self.upload_file(csv_content=csv_content) + self.assertEquals(len(data['row_errors']['data_format_error']), 2) + self.assertEquals(len(data['general_errors']), 0) + self.assertEquals(len(data['success']), 0) + self.assertEquals(len(CertificateWhitelist.objects.all()), 0) + + def test_file_upload_type_not_csv(self): + """ + Try uploading some non-CSV file e.g. .JPG file and verify that it is rejected + """ + uploaded_file = SimpleUploadedFile("temp.jpg", io.BytesIO(b"some initial binary data: \x00\x01").read()) + response = self.client.post(self.url, {'students_list': uploaded_file}) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertNotEquals(len(data['general_errors']), 0) + self.assertEquals(data['general_errors'][0], 'Make sure that the file you upload is in CSV format with ' + 'no extraneous characters or rows.') + + def test_bad_file_upload_type(self): + """ + Try uploading CSV file with invalid binary data and verify that it is rejected + """ + uploaded_file = SimpleUploadedFile("temp.csv", io.BytesIO(b"some initial binary data: \x00\x01").read()) + response = self.client.post(self.url, {'students_list': uploaded_file}) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertNotEquals(len(data['general_errors']), 0) + self.assertEquals(data['general_errors'][0], 'Could not read uploaded file.') + + def test_invalid_email_in_csv(self): + """ + Test failure case of a poorly formatted email field + """ + csv_content = "test_student.example.com,dummy_notes" + + data = self.upload_file(csv_content=csv_content) + self.assertEquals(len(data['row_errors']['user_not_exist']), 1) + self.assertEquals(len(data['success']), 0) + self.assertEquals(len(CertificateWhitelist.objects.all()), 0) + + def test_csv_user_not_enrolled(self): + """ + If the user is not enrolled in the course then there should be a user_not_enrolled error. + """ + csv_content = "nonenrolled@test.com,dummy_notes" + + data = self.upload_file(csv_content=csv_content) + self.assertEquals(len(data['row_errors']['user_not_enrolled']), 1) + self.assertEquals(len(data['general_errors']), 0) + self.assertEquals(len(data['success']), 0) + + def test_certificate_exception_already_exist(self): + """ + Test error if existing user is already in certificates exception list. + """ + CertificateWhitelist.objects.create( + user=self.enrolled_user_1, + course_id=self.course.id, + whitelist=True, + notes='' + ) + csv_content = "test_student1@example.com,dummy_notes" + data = self.upload_file(csv_content=csv_content) + self.assertEquals(len(data['row_errors']['user_already_white_listed']), 1) + self.assertEquals(len(data['general_errors']), 0) + self.assertEquals(len(data['success']), 0) + self.assertEquals(len(CertificateWhitelist.objects.all()), 1) + + def test_csv_file_not_attached(self): + """ + Test when the user does not attach a file + """ + csv_content = "test_student1@example.com,dummy_notes\n" \ + "test_student2@example.com,dummy_notes" + + uploaded_file = SimpleUploadedFile("temp.csv", csv_content) + + response = self.client.post(self.url, {'file_not_found': uploaded_file}) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertEquals(len(data['general_errors']), 1) + self.assertEquals(len(data['success']), 0) + + def upload_file(self, csv_content): + """ + Upload a csv file. + :return json data + """ + uploaded_file = SimpleUploadedFile("temp.csv", csv_content) + response = self.client.post(self.url, {'students_list': uploaded_file}) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + return data diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 2d42076863..fb38a75a81 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -12,7 +12,7 @@ import re import time import requests from django.conf import settings -from django.views.decorators.csrf import ensure_csrf_cookie +from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt from django.views.decorators.http import require_POST, require_http_methods from django.views.decorators.cache import cache_control from django.core.exceptions import ValidationError, PermissionDenied @@ -2910,3 +2910,97 @@ def generate_certificate_exceptions(request, course_id, generate_for=None): } return JsonResponse(response_payload) + + +@csrf_exempt +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +@require_global_staff +@require_POST +def generate_bulk_certificate_exceptions(request, course_id): # pylint: disable=invalid-name + """ + Add Students to certificate white list from the uploaded csv file. + :return response in dict format. + { + general_errors: [errors related to csv file e.g. csv uploading, csv attachment, content reading etc. ], + row_errors: { + data_format_error: [users/data in csv file that are not well formatted], + user_not_exist: [csv with none exiting users in LMS system], + user_already_white_listed: [users that are already white listed], + user_not_enrolled: [rows with not enrolled users in the given course] + }, + success: [list of successfully added users to the certificate white list model] + } + """ + user_index = 0 + notes_index = 1 + row_errors_key = ['data_format_error', 'user_not_exist', 'user_already_white_listed', 'user_not_enrolled'] + course_key = CourseKey.from_string(course_id) + students, general_errors, success = [], [], [] + row_errors = {key: [] for key in row_errors_key} + + def build_row_errors(key, _user, row_count): + """ + inner method to build dict of csv data as row errors. + """ + row_errors[key].append(_('user "{user}" in row# {row}').format(user=_user, row=row_count)) + + if 'students_list' in request.FILES: + try: + upload_file = request.FILES.get('students_list') + if upload_file.name.endswith('.csv'): + students = [row for row in csv.reader(upload_file.read().splitlines())] + else: + general_errors.append(_('Make sure that the file you upload is in CSV format with no ' + 'extraneous characters or rows.')) + + except Exception: # pylint: disable=broad-except + general_errors.append(_('Could not read uploaded file.')) + finally: + upload_file.close() + + row_num = 0 + for student in students: + row_num += 1 + # verify that we have exactly two column in every row either email or username and notes but allow for + # blank lines + if len(student) != 2: + if len(student) > 0: + build_row_errors('data_format_error', student[user_index], row_num) + log.info(u'invalid data/format in csv row# %s', row_num) + continue + + user = student[user_index] + try: + user = get_user_by_username_or_email(user) + except ObjectDoesNotExist: + build_row_errors('user_not_exist', user, row_num) + log.info(u'student %s does not exist', user) + else: + if len(CertificateWhitelist.get_certificate_white_list(course_key, user)) > 0: + build_row_errors('user_already_white_listed', user, row_num) + log.warning(u'student %s already exist.', user.username) + + # make sure user is enrolled in course + elif not CourseEnrollment.is_enrolled(user, course_key): + build_row_errors('user_not_enrolled', user, row_num) + log.warning(u'student %s is not enrolled in course.', user.username) + + else: + CertificateWhitelist.objects.create( + user=user, + course_id=course_key, + whitelist=True, + notes=student[notes_index] + ) + success.append(_('user "{username}" in row# {row}').format(username=user.username, row=row_num)) + + else: + general_errors.append(_('File is not attached.')) + + results = { + 'general_errors': general_errors, + 'row_errors': row_errors, + 'success': success + } + + return JsonResponse(results) diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 7b7d6535ca..da49d295da 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -157,4 +157,8 @@ urlpatterns = patterns( url(r'^generate_certificate_exceptions/(?P[^/]*)', 'instructor.views.api.generate_certificate_exceptions', name='generate_certificate_exceptions'), + + url(r'^generate_bulk_certificate_exceptions', + 'instructor.views.api.generate_bulk_certificate_exceptions', + name='generate_bulk_certificate_exceptions'), ) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 3582e5f7c3..9aa1c9a684 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -169,6 +169,10 @@ def instructor_dashboard_2(request, course_id): 'generate_certificate_exceptions', kwargs={'course_id': unicode(course_key), 'generate_for': ''} ) + generate_bulk_certificate_exceptions_url = reverse( # pylint: disable=invalid-name + 'generate_bulk_certificate_exceptions', + kwargs={'course_id': unicode(course_key)} + ) certificate_exception_view_url = reverse( 'certificate_exception_view', kwargs={'course_id': unicode(course_key)} @@ -183,6 +187,7 @@ def instructor_dashboard_2(request, course_id): 'analytics_dashboard_message': analytics_dashboard_message, 'certificate_white_list': certificate_white_list, 'generate_certificate_exceptions_url': generate_certificate_exceptions_url, + 'generate_bulk_certificate_exceptions_url': generate_bulk_certificate_exceptions_url, 'certificate_exception_view_url': certificate_exception_view_url } return render_to_response('instructor/instructor_dashboard_2/instructor_dashboard_2.html', context) diff --git a/lms/static/js/certificates/factories/certificate_whitelist_factory.js b/lms/static/js/certificates/factories/certificate_whitelist_factory.js index 38e37c89f6..267c63a9f2 100644 --- a/lms/static/js/certificates/factories/certificate_whitelist_factory.js +++ b/lms/static/js/certificates/factories/certificate_whitelist_factory.js @@ -8,12 +8,13 @@ 'js/certificates/views/certificate_whitelist', 'js/certificates/models/certificate_exception', 'js/certificates/views/certificate_whitelist_editor', - 'js/certificates/collections/certificate_whitelist' + 'js/certificates/collections/certificate_whitelist', + 'js/certificates/views/certificate_bulk_whitelist' ], function($, CertificateWhiteListListView, CertificateExceptionModel, CertificateWhiteListEditorView , - CertificateWhiteListCollection){ + CertificateWhiteListCollection, CertificateBulkWhiteList){ return function(certificate_white_list_json, generate_certificate_exceptions_url, - certificate_exception_view_url){ + certificate_exception_view_url, generate_bulk_certificate_exceptions_url){ var certificateWhiteList = new CertificateWhiteListCollection(JSON.parse(certificate_white_list_json), { parse: true, @@ -32,6 +33,10 @@ certificateWhiteListEditorView: certificateWhiteListEditorView }).render(); + new CertificateBulkWhiteList({ + bulk_exception_url: generate_bulk_certificate_exceptions_url + }).render(); + }; } ); diff --git a/lms/static/js/certificates/views/certificate_bulk_whitelist.js b/lms/static/js/certificates/views/certificate_bulk_whitelist.js new file mode 100644 index 0000000000..4d340f36f4 --- /dev/null +++ b/lms/static/js/certificates/views/certificate_bulk_whitelist.js @@ -0,0 +1,193 @@ +// Backbone Application View: CertificateBulkWhitelist View +/*global define, RequireJS */ + +;(function(define){ + 'use strict'; + + define([ + 'jquery', + 'underscore', + 'gettext', + 'backbone' + ], + + function($, _, gettext, Backbone){ + var DOM_SELECTORS = { + bulk_exception: ".bulk-white-list-exception", + upload_csv_button: ".upload-csv-button", + browse_file: ".browse-file", + bulk_white_list_exception_form: "form#bulk-white-list-exception-form" + }; + + var MESSAGE_GROUP = { + successfully_added: 'successfully-added', + general_errors: 'general-errors', + data_format_error: 'data-format-error', + user_not_exist: 'user-not-exist', + user_already_white_listed: 'user-already-white-listed', + user_not_enrolled: 'user-not-enrolled' + }; + + return Backbone.View.extend({ + el: DOM_SELECTORS.bulk_exception, + events: { + 'change #browseBtn': 'chooseFile', + 'click .upload-csv-button': 'uploadCSV' + }, + + initialize: function(options){ + // Re-render the view when an item is added to the collection + this.bulk_exception_url = options.bulk_exception_url; + }, + + render: function(){ + var template = this.loadTemplate('certificate-bulk-white-list'); + this.$el.html(template()); + }, + + loadTemplate: function(name) { + var templateSelector = "#" + name + "-tpl", + templateText = $(templateSelector).text(); + return _.template(templateText); + }, + + uploadCSV: function() { + var form = this.$el.find(DOM_SELECTORS.bulk_white_list_exception_form); + var self = this; + form.unbind('submit').submit(function(e) { + var data = new FormData(e.currentTarget); + $.ajax({ + dataType: 'json', + type: 'POST', + url: self.bulk_exception_url, + data: data, + processData: false, + contentType: false, + success: function(data_from_server) { + self.display_response(data_from_server); + } + }); + e.preventDefault(); // avoid to execute the actual submit of the form. + }); + }, + + display_response: function(data_from_server) { + $(".results").empty(); + + // Display general error messages + if (data_from_server.general_errors.length) { + var errors = data_from_server.general_errors; + generate_div('msg-error', MESSAGE_GROUP.general_errors, gettext('Errors!'), errors); + } + + // Display success message + if (data_from_server.success.length) { + var success_data = data_from_server.success; + generate_div( + 'msg-success', + MESSAGE_GROUP.successfully_added, + get_text(success_data.length, MESSAGE_GROUP.successfully_added), + success_data + ); + } + + // Display data row error messages + if (Object.keys(data_from_server.row_errors).length) { + var row_errors = data_from_server.row_errors; + + if (row_errors.data_format_error.length) { + var format_errors = row_errors.data_format_error; + generate_div( + 'msg-error', + MESSAGE_GROUP.data_format_error, + get_text(format_errors.length, MESSAGE_GROUP.data_format_error), + format_errors + ); + } + if (row_errors.user_not_exist.length) { + var user_not_exist = row_errors.user_not_exist; + generate_div( + 'msg-error', + MESSAGE_GROUP.user_not_exist, + get_text(user_not_exist.length, MESSAGE_GROUP.user_not_exist), + user_not_exist + ); + } + if (row_errors.user_already_white_listed.length) { + var user_already_white_listed = row_errors.user_already_white_listed; + generate_div( + 'msg-error', + MESSAGE_GROUP.user_already_white_listed, + get_text(user_already_white_listed.length, MESSAGE_GROUP.user_already_white_listed), + user_already_white_listed + ); + } + if (row_errors.user_not_enrolled.length) { + var user_not_enrolled = row_errors.user_not_enrolled; + generate_div( + 'msg-error', + MESSAGE_GROUP.user_not_enrolled, + get_text(user_not_enrolled.length, MESSAGE_GROUP.user_not_enrolled), + user_not_enrolled + ); + } + } + + function generate_div(div_class, group, heading, display_data) { + // inner function generate div and display response messages. + $('
', { + class: 'message ' + div_class + ' ' + group + }).appendTo('.results').prepend( "" + heading + "" ); + + for(var i = 0; i < display_data.length; i++){ + $('
', { + text: display_data[i] + }).appendTo('.results > .' + div_class + '.' + group); + } + } + + function get_text(qty, group) { + // inner function to display appropriate heading text + var text; + switch(group) { + case MESSAGE_GROUP.successfully_added: + text = qty > 1 ? gettext(qty + ' learners are successfully added to exception list'): + gettext(qty + ' learner is successfully added to the exception list'); + break; + + case MESSAGE_GROUP.data_format_error: + text = qty > 1 ? gettext(qty + ' records are not in correct format'): + gettext(qty + ' record is not in correct format'); + break; + + case MESSAGE_GROUP.user_not_exist: + text = qty > 1 ? gettext(qty + ' learners do not exist in LMS'): + gettext(qty + ' learner does not exist in LMS'); + break; + + case MESSAGE_GROUP.user_already_white_listed: + text = qty > 1 ? gettext(qty + ' learners are already white listed'): + gettext(qty + ' learner is already white listed'); + break; + + case MESSAGE_GROUP.user_not_enrolled: + text = qty > 1 ? gettext(qty + ' learners are not enrolled in course'): + gettext(qty + ' learner is not enrolled in course'); + break; + } + return text; + } + }, + + chooseFile: function(event) { + if (event && event.preventDefault) { event.preventDefault(); } + if (event.currentTarget.files.length === 1) { + this.$el.find(DOM_SELECTORS.upload_csv_button).removeClass('is-disabled'); + this.$el.find(DOM_SELECTORS.browse_file).val( + event.currentTarget.value.substring(event.currentTarget.value.lastIndexOf("\\") + 1)); + } + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 2966bf02dc..904e3b1982 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -471,43 +471,6 @@ .enrollment_signup_button { @include margin-right($baseline/4); } - // Custom File upload - .customBrowseBtn { - margin: ($baseline/2) 0; - display: inline-block; - .file-browse { - position:relative; - overflow:hidden; - display: inline; - @include margin-left(-5px); - span.browse{ - @include button(simple, $blue); - @include margin-right($baseline); - padding: 6px ($baseline/2); - font-size: 12px; - border-radius: 0 3px 3px 0; - } - input.file_field { - position:absolute; - @include right(0); - top:0; - margin:0; - padding:0; - cursor:pointer; - opacity:0; - filter:alpha(opacity=0); - } - } - & > span, & input[disabled]{ - vertical-align: middle; - } - input[disabled] { - @include border-radius(4px 0 0 4px); - @include padding(6px 6px 5px); - border: 1px solid $lightGrey1; - cursor: not-allowed; - } - } } .enroll-option { @@ -1839,6 +1802,15 @@ input[name="subject"] { width: 75%; } + .certificate-exception-container { + h3 { + border-bottom: 1px groove black; + display: inline-block; + } + p.under-heading-text { + margin: 12px 0 12px 0; + } + } } input[name="subject"] { @@ -2225,3 +2197,40 @@ input[name="subject"] { } } } +// Custom File upload +.customBrowseBtn { + margin: ($baseline/2) 0; + display: inline-block; + .file-browse { + position:relative; + overflow:hidden; + display: inline; + @include margin-left(-5px); + span.browse{ + @include button(simple, $blue); + @include margin-right($baseline); + padding: 6px ($baseline/2); + font-size: 12px; + border-radius: 0 3px 3px 0; + } + input.file_field { + position:absolute; + @include right(0); + top:0; + margin:0; + padding:0; + cursor:pointer; + opacity:0; + filter:alpha(opacity=0); + } + } + & > span, & input[disabled]{ + vertical-align: middle; + } + input[disabled] { + @include border-radius(4px 0 0 4px); + @include padding(6px 6px 5px); + border: 1px solid $lightGrey1; + cursor: not-allowed; + } +} diff --git a/lms/templates/instructor/instructor_dashboard_2/certificate-bulk-white-list.underscore b/lms/templates/instructor/instructor_dashboard_2/certificate-bulk-white-list.underscore new file mode 100644 index 0000000000..419e9b112e --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/certificate-bulk-white-list.underscore @@ -0,0 +1,17 @@ +

<%= gettext("Bulk Exceptions") %>

+
+

+ <%= gettext("You can upload a CSV file of usernames or email addresses to be added to the certificate exceptions white list.") %> +

+
+
+ " /> +
+ <%= gettext("Browse") %> + +
+
+ +
+
+
\ No newline at end of file diff --git a/lms/templates/instructor/instructor_dashboard_2/certificate-white-list-editor.underscore b/lms/templates/instructor/instructor_dashboard_2/certificate-white-list-editor.underscore index 4d282a200d..ac165cbcca 100644 --- a/lms/templates/instructor/instructor_dashboard_2/certificate-white-list-editor.underscore +++ b/lms/templates/instructor/instructor_dashboard_2/certificate-white-list-editor.underscore @@ -1,9 +1,9 @@ +

<%= gettext("Individual Exceptions") %>

+

<%= gettext("You can add a username or email address to be added to the certificate exceptions white list.") %>

-

<%- gettext("Specify either Student's username or email for whom to create certificate exception") %>

-

<%- gettext("Enter Notes associated with this certificate exception") %>

diff --git a/lms/templates/instructor/instructor_dashboard_2/certificates.html b/lms/templates/instructor/instructor_dashboard_2/certificates.html index 68ac9ad3c5..5e78fa8ff2 100644 --- a/lms/templates/instructor/instructor_dashboard_2/certificates.html +++ b/lms/templates/instructor/instructor_dashboard_2/certificates.html @@ -5,7 +5,7 @@ import json %> <%static:require_module module_name="js/certificates/factories/certificate_whitelist_factory" class_name="CertificateWhitelistFactory"> - CertificateWhitelistFactory('${json.dumps(certificate_white_list)}', "${generate_certificate_exceptions_url}", "${certificate_exception_view_url}"); + CertificateWhitelistFactory('${json.dumps(certificate_white_list)}', "${generate_certificate_exceptions_url}", "${certificate_exception_view_url}", "${generate_bulk_certificate_exceptions_url}"); <%page args="section_data"/> @@ -114,13 +114,15 @@ import json
-
-
+

${_("Certificate Exceptions")}

+

+ ${_("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.")} +

-

${_("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.")}

+

diff --git a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html index 6f0ccc7422..e657dcb847 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -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","certificate-white-list","certificate-white-list-editor"]: +% 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","certificate-bulk-white-list"]: