Merge pull request #10750 from edx/asadiqbal08/SOL-1417-CSV
SOL-1417 Certs: Allow CSV upload for Cert Exceptions
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
@@ -765,3 +767,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
|
||||
|
||||
@@ -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
|
||||
@@ -2918,3 +2918,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)
|
||||
|
||||
@@ -157,4 +157,8 @@ urlpatterns = patterns(
|
||||
url(r'^generate_certificate_exceptions/(?P<generate_for>[^/]*)',
|
||||
'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'),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
193
lms/static/js/certificates/views/certificate_bulk_whitelist.js
Normal file
193
lms/static/js/certificates/views/certificate_bulk_whitelist.js
Normal file
@@ -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.
|
||||
$('<div/>', {
|
||||
class: 'message ' + div_class + ' ' + group
|
||||
}).appendTo('.results').prepend( "<b>" + heading + "</b>" );
|
||||
|
||||
for(var i = 0; i < display_data.length; i++){
|
||||
$('<div/>', {
|
||||
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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<h3><%= gettext("Bulk Exceptions") %></h3>
|
||||
<div class="bulk_white_list_csv">
|
||||
<p class="under-heading-text">
|
||||
<%= gettext("You can upload a CSV file of usernames or email addresses to be added to the certificate exceptions white list.") %>
|
||||
</p>
|
||||
<form id="bulk-white-list-exception-form" enctype="multipart/form-data">
|
||||
<div class="customBrowseBtn">
|
||||
<input disabled="disabled" class="browse-file" placeholder="<%= gettext("Choose File") %>" />
|
||||
<div class="file-browse btn btn-primary">
|
||||
<span class="browse"> <%= gettext("Browse") %> </span>
|
||||
<input class="file_field" id="browseBtn" name="students_list" type="file" accept=".csv"/>
|
||||
</div>
|
||||
</div>
|
||||
<button class="is-disabled upload-csv-button" type="submit"><%= gettext("Upload CSV") %></button>
|
||||
</form>
|
||||
<div class="results"></div>
|
||||
</div>
|
||||
@@ -1,9 +1,9 @@
|
||||
<h3><%= gettext("Individual Exceptions") %></h3>
|
||||
<p class="under-heading-text"> <%= gettext("You can add a username or email address to be added to the certificate exceptions white list.") %></p>
|
||||
<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>
|
||||
|
||||
@@ -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}");
|
||||
</%static:require_module>
|
||||
|
||||
<%page args="section_data"/>
|
||||
@@ -114,13 +114,15 @@ import json
|
||||
<input type="button" id="btn-start-regenerating-certificates" value="${_('Regenerate Certificates')}" data-endpoint="${section_data['urls']['start_certificate_regeneration']}"/>
|
||||
</form>
|
||||
<div class="message certificate-regeneration-status"></div>
|
||||
</div>
|
||||
|
||||
<div class="certificate_exception-container">
|
||||
<div class="certificate-exception-container">
|
||||
<hr>
|
||||
<h2> ${_("Certificate Exceptions")} </h2>
|
||||
<p class="under-heading-text">
|
||||
${_("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>
|
||||
<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>
|
||||
<div class="bulk-white-list-exception"></div>
|
||||
<br />
|
||||
<div id="certificate-white-list-editor"></div>
|
||||
<div class="white-listed-students" id="white-listed-students"></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","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"]:
|
||||
<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