Merge pull request #11080 from edx/saleem-latif/SOL-1529
SOL-1529: Allow PMs to Invalidate Certificates
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
// Backbone.js Application Collection: CertificateInvalidationCollection
|
||||
/*global define, RequireJS */
|
||||
|
||||
;(function(define) {
|
||||
'use strict';
|
||||
|
||||
define(
|
||||
['backbone', 'js/certificates/models/certificate_invalidation'],
|
||||
|
||||
function(Backbone, CertificateInvalidation) {
|
||||
return Backbone.Collection.extend({
|
||||
model: CertificateInvalidation
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,31 @@
|
||||
// Backbone.js Page Object Factory: Certificate Invalidation Factory
|
||||
/*global define, RequireJS */
|
||||
|
||||
;(function(define) {
|
||||
'use strict';
|
||||
define(
|
||||
[
|
||||
'js/certificates/views/certificate_invalidation_view',
|
||||
'js/certificates/collections/certificate_invalidation_collection'
|
||||
],
|
||||
function(CertificateInvalidationView, CertificateInvalidationCollection) {
|
||||
|
||||
return function(certificate_invalidation_collection_json, certificate_invalidation_url) {
|
||||
var certificate_invalidation_collection = new CertificateInvalidationCollection(
|
||||
JSON.parse(certificate_invalidation_collection_json), {
|
||||
parse: true,
|
||||
canBeEmpty: true,
|
||||
url: certificate_invalidation_url
|
||||
}
|
||||
);
|
||||
|
||||
var certificate_invalidation_view = new CertificateInvalidationView({
|
||||
collection: certificate_invalidation_collection
|
||||
});
|
||||
|
||||
certificate_invalidation_view.render();
|
||||
};
|
||||
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,35 @@
|
||||
// Backbone.js Application Model: CertificateInvalidation
|
||||
/*global define, RequireJS */
|
||||
|
||||
;(function(define) {
|
||||
'use strict';
|
||||
|
||||
define(
|
||||
['underscore', 'underscore.string', 'gettext', 'backbone'],
|
||||
|
||||
function(_, str, gettext, Backbone) {
|
||||
return Backbone.Model.extend({
|
||||
idAttribute: 'id',
|
||||
|
||||
defaults: {
|
||||
user: '',
|
||||
invalidated_by: '',
|
||||
created: '',
|
||||
notes: ''
|
||||
},
|
||||
|
||||
url: function() {
|
||||
return this.get('url');
|
||||
},
|
||||
|
||||
validate: function(attrs) {
|
||||
if (!_.str.trim(attrs.user)) {
|
||||
// A username or email must be provided for certificate invalidation
|
||||
return gettext('Student username/email field is required and can not be empty. ' +
|
||||
'Kindly fill in username/email and then press "Invalidate Certificate" button.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,121 @@
|
||||
// Backbone Application View: CertificateInvalidationView
|
||||
/*global define, RequireJS */
|
||||
|
||||
;(function(define) {
|
||||
'use strict';
|
||||
define(
|
||||
['jquery', 'underscore', 'gettext', 'backbone', 'js/certificates/models/certificate_invalidation'],
|
||||
|
||||
function($, _, gettext, Backbone, CertificateInvalidationModel) {
|
||||
return Backbone.View.extend({
|
||||
el: "#certificate-invalidation",
|
||||
messages: "div.message",
|
||||
events: {
|
||||
'click #invalidate-certificate': 'invalidateCertificate',
|
||||
'click .re-validate-certificate': 'reValidateCertificate'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.listenTo(this.collection, 'change add remove', this.render);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var template = this.loadTemplate('certificate-invalidation');
|
||||
this.$el.html(template({certificate_invalidations: this.collection.models}));
|
||||
},
|
||||
|
||||
loadTemplate: function(name) {
|
||||
var templateSelector = "#" + name + "-tpl",
|
||||
templateText = $(templateSelector).text();
|
||||
return _.template(templateText);
|
||||
},
|
||||
|
||||
invalidateCertificate: function() {
|
||||
var user = this.$("#certificate-invalidation-user").val();
|
||||
var notes = this.$("#certificate-invalidation-notes").val();
|
||||
|
||||
var certificate_invalidation = new CertificateInvalidationModel({
|
||||
url: this.collection.url,
|
||||
user: user,
|
||||
notes: notes
|
||||
});
|
||||
|
||||
if (this.collection.findWhere({user: user})) {
|
||||
this.showMessage(
|
||||
gettext("Certificate of ") + user +
|
||||
gettext(" has already been invalidated. Please check your spelling and retry."
|
||||
));
|
||||
}
|
||||
else if (certificate_invalidation.isValid()) {
|
||||
var self = this;
|
||||
certificate_invalidation.save(null, {
|
||||
wait: true,
|
||||
|
||||
success: function(model) {
|
||||
self.collection.add(model);
|
||||
self.showMessage(
|
||||
gettext('Certificate has been successfully invalidated for ') + user + '.'
|
||||
);
|
||||
},
|
||||
|
||||
error: function(model, response) {
|
||||
try {
|
||||
var response_data = JSON.parse(response.responseText);
|
||||
self.showMessage(response_data.message);
|
||||
}
|
||||
catch(exception) {
|
||||
self.showMessage(gettext("Server Error, Please refresh the page and try again."));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
else {
|
||||
this.showMessage(certificate_invalidation.validationError);
|
||||
}
|
||||
},
|
||||
|
||||
reValidateCertificate: function(event) {
|
||||
var certificate_invalidation = $(event.target).data();
|
||||
var model = this.collection.get(certificate_invalidation),
|
||||
self = this;
|
||||
|
||||
if (model) {
|
||||
model.destroy({
|
||||
success: function() {
|
||||
self.showMessage(gettext('The certificate for this learner has been re-validated and ' +
|
||||
'the system is re-running the grade for this learner.'));
|
||||
},
|
||||
error: function(model, response) {
|
||||
try {
|
||||
var response_data = JSON.parse(response.responseText);
|
||||
self.showMessage(response_data.message);
|
||||
}
|
||||
catch(exception) {
|
||||
self.showMessage(gettext("Server Error, Please refresh the page and try again."));
|
||||
}
|
||||
},
|
||||
wait: true,
|
||||
data: JSON.stringify(model.attributes)
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.showMessage(gettext('Could not find Certificate Invalidation in the list. ' +
|
||||
'Please refresh the page and try again'));
|
||||
}
|
||||
},
|
||||
|
||||
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) {
|
||||
$(this.messages + ">p" ).remove();
|
||||
this.$(this.messages).removeClass('hidden').append("<p>"+ gettext(message) + "</p>");
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,268 @@
|
||||
/*global define */
|
||||
define([
|
||||
'jquery',
|
||||
'common/js/spec_helpers/ajax_helpers',
|
||||
'js/certificates/models/certificate_invalidation',
|
||||
'js/certificates/views/certificate_invalidation_view',
|
||||
'js/certificates/collections/certificate_invalidation_collection'
|
||||
],
|
||||
function($, AjaxHelpers, CertificateInvalidationModel, CertificateInvalidationView,
|
||||
CertificateInvalidationCollection) {
|
||||
'use strict';
|
||||
describe("Field validation of invalidation model.", function() {
|
||||
var certificate_invalidation = null;
|
||||
var assertValid = function(fields, isValid, expectedErrors) {
|
||||
certificate_invalidation.set(fields);
|
||||
var errors = certificate_invalidation.validate(certificate_invalidation.attributes);
|
||||
|
||||
if (isValid) {
|
||||
expect(errors).toBe(undefined);
|
||||
} else {
|
||||
expect(errors).toEqual(expectedErrors);
|
||||
}
|
||||
};
|
||||
|
||||
var EXPECTED_ERRORS = {
|
||||
user_name_or_email_required: 'Student username/email field is required and can not be empty. ' +
|
||||
'Kindly fill in username/email and then press "Invalidate Certificate" button.'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
certificate_invalidation = new CertificateInvalidationModel({user: 'test_user'});
|
||||
certificate_invalidation.set({
|
||||
notes: "Test notes"
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts valid email addresses", function() {
|
||||
assertValid({user: "bob@example.com"}, true);
|
||||
assertValid({user: "bob+smith@example.com"}, true);
|
||||
assertValid({user: "bob+smith@example.com"}, true);
|
||||
assertValid({user: "bob+smith@example.com"}, true);
|
||||
assertValid({user: "bob@test.example.com"}, true);
|
||||
assertValid({user: "bob@test-example.com"}, true);
|
||||
});
|
||||
|
||||
it("displays username or email required error", function() {
|
||||
assertValid({user: ""}, false, EXPECTED_ERRORS.user_name_or_email_required);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Certificate invalidation collection initialization and updates.",
|
||||
function() {
|
||||
var certificate_invalidations = null,
|
||||
certificate_invalidation_url = 'test/url/';
|
||||
var certificate_invalidations_json = [
|
||||
{
|
||||
id: 1,
|
||||
user: "test1",
|
||||
invalidated_by: 2,
|
||||
created: "Thursday, October 29, 2015",
|
||||
notes: "test notes for test certificate invalidation"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: "test2",
|
||||
invalidated_by: 2,
|
||||
created: "Thursday, October 29, 2015",
|
||||
notes: "test notes for test certificate invalidation"
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(function() {
|
||||
certificate_invalidations = new CertificateInvalidationCollection(certificate_invalidations_json, {
|
||||
parse: true,
|
||||
canBeEmpty: true,
|
||||
url: certificate_invalidation_url
|
||||
});
|
||||
});
|
||||
|
||||
it("has 2 models in the collection after initialization", function() {
|
||||
expect(certificate_invalidations.models.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("model is removed from collection on destroy", function() {
|
||||
var model = certificate_invalidations.get({id: 2});
|
||||
model.destroy();
|
||||
expect(certificate_invalidations.models.length).toEqual(1);
|
||||
expect(certificate_invalidations.get({id: 2})).toBe(undefined);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describe("Certificate invalidation success/error messages on add/remove invalidations.", function() {
|
||||
var view = null,
|
||||
certificate_invalidation_url = 'test/url/',
|
||||
user_name_field = null,
|
||||
notes_field = null,
|
||||
invalidate_button=null,
|
||||
duplicate_user='test2',
|
||||
new_user='test4@test.com',
|
||||
requests=null;
|
||||
|
||||
var messages = {
|
||||
error: {
|
||||
empty_user_name_email: 'Student username/email field is required and can not be empty. ' +
|
||||
'Kindly fill in username/email and then press "Invalidate Certificate" button.',
|
||||
duplicate_user: "Certificate of " + (duplicate_user) + " has already been invalidated. " +
|
||||
"Please check your spelling and retry.",
|
||||
server_error: "Server Error, Please refresh the page and try again.",
|
||||
from_server: "Test Message from server"
|
||||
},
|
||||
success: {
|
||||
saved: "Certificate has been successfully invalidated for " + new_user + '.',
|
||||
re_validated: 'The certificate for this learner has been re-validated and ' +
|
||||
'the system is re-running the grade for this learner.'
|
||||
}
|
||||
};
|
||||
|
||||
var certificate_invalidations_json = [
|
||||
{
|
||||
id: 1,
|
||||
user: "test1",
|
||||
invalidated_by: 2,
|
||||
created: "Thursday, October 29, 2015",
|
||||
notes: "test notes for test certificate invalidation"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: "test2",
|
||||
invalidated_by: 2,
|
||||
created: "Thursday, October 29, 2015",
|
||||
notes: "test notes for test certificate invalidation"
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures();
|
||||
var fixture =readFixtures(
|
||||
"templates/instructor/instructor_dashboard_2/certificate-invalidation.underscore"
|
||||
);
|
||||
|
||||
setFixtures(
|
||||
"<div class='certificate-invalidation-container'>" +
|
||||
" <h2>Invalidate Certificates</h2> " +
|
||||
" <div id='certificate-invalidation'></div>" +
|
||||
"</div>" +
|
||||
"<script type='text/template' id='certificate-invalidation-tpl'>" + fixture + "</script>"
|
||||
);
|
||||
|
||||
var certificate_invalidations = new CertificateInvalidationCollection(certificate_invalidations_json, {
|
||||
parse: true,
|
||||
canBeEmpty: true,
|
||||
url: certificate_invalidation_url,
|
||||
generate_certificates_url: certificate_invalidation_url
|
||||
|
||||
});
|
||||
|
||||
view = new CertificateInvalidationView({collection: certificate_invalidations});
|
||||
view.render();
|
||||
|
||||
user_name_field = $("#certificate-invalidation-user");
|
||||
notes_field = $("#certificate-invalidation-notes");
|
||||
invalidate_button = $("#invalidate-certificate");
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
});
|
||||
|
||||
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/remove to collection", function() {
|
||||
var user = 'test3',
|
||||
notes = 'test3 notes',
|
||||
model = new CertificateInvalidationModel({user: user, notes: notes});
|
||||
|
||||
// Add another model in collection and verify it is rendered
|
||||
view.collection.add(model);
|
||||
expect(view.$el.find('table tbody tr').length).toBe(3);
|
||||
|
||||
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(user);
|
||||
|
||||
// Remove a model from collection
|
||||
var collection_model = view.collection.get({id: 2});
|
||||
collection_model.destroy();
|
||||
|
||||
// Verify view is updated
|
||||
expect(view.$el.find('table tbody tr').length).toBe(2);
|
||||
|
||||
|
||||
});
|
||||
|
||||
it("verifies view error message on duplicate certificate validation.", function() {
|
||||
$(user_name_field).val(duplicate_user);
|
||||
$(invalidate_button).click();
|
||||
|
||||
expect($("#certificate-invalidation div.message").text()).toEqual(messages.error.duplicate_user);
|
||||
});
|
||||
|
||||
it("verifies view error message on empty username/email field.", function() {
|
||||
$(user_name_field).val("");
|
||||
$(invalidate_button).click();
|
||||
|
||||
expect($("#certificate-invalidation div.message").text()).toEqual(messages.error.empty_user_name_email);
|
||||
});
|
||||
|
||||
it("verifies view success message on certificate invalidation.", function() {
|
||||
$(user_name_field).val(new_user);
|
||||
$(notes_field).val("test notes for user test4");
|
||||
$(invalidate_button).click();
|
||||
|
||||
AjaxHelpers.respondWithJson(
|
||||
requests,
|
||||
{
|
||||
id: 4,
|
||||
user: 'test4',
|
||||
validated_by: 5,
|
||||
created: "Thursday, December 29, 2015",
|
||||
notes: "test notes for user test4"
|
||||
}
|
||||
);
|
||||
expect($("#certificate-invalidation div.message").text()).toEqual(messages.success.saved);
|
||||
});
|
||||
|
||||
it("verifies view server error if server returns unknown response.", function() {
|
||||
$(user_name_field).val(new_user);
|
||||
$(notes_field).val("test notes for user test4");
|
||||
$(invalidate_button).click();
|
||||
|
||||
// Response with empty body
|
||||
AjaxHelpers.respondWithTextError(requests, 400, "");
|
||||
|
||||
expect($("#certificate-invalidation div.message").text()).toEqual(messages.error.server_error);
|
||||
});
|
||||
|
||||
it("verifies certificate re-validation request and success message.", function() {
|
||||
var user = 'test1',
|
||||
re_validate_certificate = "div.certificate-invalidation-container table tr:contains('" +
|
||||
user + "') td .re-validate-certificate";
|
||||
|
||||
$(re_validate_certificate).click();
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
|
||||
expect($("#certificate-invalidation div.message").text()).toEqual(messages.success.re_validated);
|
||||
});
|
||||
|
||||
it("verifies error message from server is displayed.", function() {
|
||||
var user = 'test1',
|
||||
re_validate_certificate = "div.certificate-invalidation-container table tr:contains('" +
|
||||
user + "') td .re-validate-certificate";
|
||||
|
||||
$(re_validate_certificate).click();
|
||||
AjaxHelpers.respondWithError(requests, 400, {
|
||||
success: false,
|
||||
message: messages.error.from_server
|
||||
});
|
||||
|
||||
expect($("#certificate-invalidation div.message").text()).toEqual(messages.error.from_server);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -655,6 +655,7 @@
|
||||
'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/instructor_dashboard/certificates_invalidation_spec.js',
|
||||
'lms/include/js/spec/instructor_dashboard/certificates_bulk_exception_spec.js',
|
||||
'lms/include/js/spec/instructor_dashboard/certificates_spec.js',
|
||||
'lms/include/js/spec/student_account/account_spec.js',
|
||||
|
||||
@@ -2159,16 +2159,18 @@ input[name="subject"] {
|
||||
}
|
||||
}
|
||||
|
||||
.student-username-or-email {
|
||||
width: 300px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.notes-field {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
|
||||
#certificate-white-list-editor {
|
||||
padding-top: 5px;
|
||||
.certificate-exception-inputs {
|
||||
.student-username-or-email {
|
||||
width: 300px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.notes-field {
|
||||
width: 400px;
|
||||
}
|
||||
p + p {
|
||||
margin-top: 5px;
|
||||
}
|
||||
@@ -2178,7 +2180,7 @@ input[name="subject"] {
|
||||
}
|
||||
}
|
||||
|
||||
.white-listed-students {
|
||||
.white-listed-students, .invalidation-history {
|
||||
margin-top: 10px;
|
||||
padding-top: 5px;
|
||||
table {
|
||||
|
||||
Reference in New Issue
Block a user