Files
edx-platform/cms/static/js/views/utils/view_utils.js
Matjaz Gregoric d8e076566d Improve JS course key validation to not allow special chars.
Course and Library keys cannot contiain !'()* special characters,
but the JS validation on the new course/library failed to detect
these characters.

`encodeURIComponent` is used to check the string for special characters,
but `encodeURIComponent` does not encode these characters: -_!~*'().
(see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent)

The -_~ characters are allowed in course keys, but !'()* are not,
so add an explicit check for these characters to make sure a field
containing these characters does not pass the validation.
2015-01-15 08:32:19 +01:00

249 lines
9.8 KiB
JavaScript

/**
* Provides useful utilities for views.
*/
define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt"],
function ($, _, gettext, NotificationView, PromptView) {
var toggleExpandCollapse, showLoadingIndicator, hideLoadingIndicator, confirmThenRunOperation,
runOperationShowingMessage, disableElementWhileRunning, getScrollOffset, setScrollOffset,
setScrollTop, redirect, reload, hasChangedAttributes, deleteNotificationHandler,
validateRequiredField, validateURLItemEncoding, validateTotalKeyLength, checkTotalKeyLengthViolations;
// see https://openedx.atlassian.net/browse/TNL-889 for what is it and why it's 65
var MAX_SUM_KEY_LENGTH = 65;
/**
* Toggles the expanded state of the current element.
*/
toggleExpandCollapse = function(target, collapsedClass) {
// Support the old 'collapsed' option until fully switched over to is-collapsed
if (!collapsedClass) {
collapsedClass = 'collapsed';
}
target.closest('.expand-collapse').toggleClass('expand collapse');
target.closest('.is-collapsible, .window').toggleClass(collapsedClass);
target.closest('.is-collapsible').children('article').slideToggle();
};
/**
* Show the page's loading indicator.
*/
showLoadingIndicator = function() {
$('.ui-loading').show();
};
/**
* Hide the page's loading indicator.
*/
hideLoadingIndicator = function() {
$('.ui-loading').hide();
};
/**
* Confirms with the user whether to run an operation or not, and then runs it if desired.
*/
confirmThenRunOperation = function(title, message, actionLabel, operation, onCancelCallback) {
return new PromptView.Warning({
title: title,
message: message,
actions: {
primary: {
text: actionLabel,
click: function(prompt) {
prompt.hide();
operation();
}
},
secondary: {
text: gettext('Cancel'),
click: function(prompt) {
if (onCancelCallback) {
onCancelCallback();
}
return prompt.hide();
}
}
}
}).show();
};
/**
* Shows a progress message for the duration of an asynchronous operation.
* Note: this does not remove the notification upon failure because an error
* will be shown that shouldn't be removed.
* @param message The message to show.
* @param operation A function that returns a promise representing the operation.
*/
runOperationShowingMessage = function(message, operation) {
var notificationView;
notificationView = new NotificationView.Mini({
title: gettext(message)
});
notificationView.show();
return operation().done(function() {
notificationView.hide();
});
};
/**
* Disables a given element when a given operation is running.
* @param {jQuery} element the element to be disabled.
* @param operation the operation during whose duration the
* element should be disabled. The operation should return
* a JQuery promise.
*/
disableElementWhileRunning = function(element, operation) {
element.addClass("is-disabled").attr('aria-disabled', true);
return operation().always(function() {
element.removeClass("is-disabled").attr('aria-disabled', false);
});
};
/**
* Returns a handler that removes a notification, both dismissing it and deleting it from the database.
* @param callback function to call when deletion succeeds
*/
deleteNotificationHandler = function(callback) {
return function (event) {
event.preventDefault();
$.ajax({
url: $(this).data('dismiss-link'),
type: 'DELETE',
success: callback
});
};
};
/**
* Performs an animated scroll so that the window has the specified scroll top.
* @param scrollTop The desired scroll top for the window.
*/
setScrollTop = function(scrollTop) {
$('html, body').animate({
scrollTop: scrollTop
}, 500);
};
/**
* Returns the relative position that the element is scrolled from the top of the view port.
* @param element The element in question.
*/
getScrollOffset = function(element) {
var elementTop = element.offset().top;
return elementTop - $(window).scrollTop();
};
/**
* Scrolls the window so that the element is scrolled down to the specified relative position
* from the top of the view port.
* @param element The element in question.
* @param offset The amount by which the element should be scrolled from the top of the view port.
*/
setScrollOffset = function(element, offset) {
var elementTop = element.offset().top,
newScrollTop = elementTop - offset;
setScrollTop(newScrollTop);
};
/**
* Redirects to the specified URL. This is broken out as its own function for unit testing.
*/
redirect = function(url) {
window.location = url;
};
/**
* Reloads the page. This is broken out as its own function for unit testing.
*/
reload = function() {
window.location.reload();
};
/**
* Returns true if a model has changes to at least one of the specified attributes.
* @param model The model in question.
* @param attributes The list of attributes to be compared.
* @returns {boolean} Returns true if attribute changes are found.
*/
hasChangedAttributes = function(model, attributes) {
var i, changedAttributes = model.changedAttributes();
if (!changedAttributes) {
return false;
}
for (i=0; i < attributes.length; i++) {
if (_.has(changedAttributes, attributes[i])) {
return true;
}
}
return false;
};
/**
* Helper method for course/library creation - verifies a required field is not blank.
*/
validateRequiredField = function (msg) {
return msg.length === 0 ? gettext('Required field.') : '';
};
/**
* Helper method for course/library creation.
* Check that a course (org, number, run) doesn't use any special characters
*/
validateURLItemEncoding = function (item, allowUnicode) {
var required = validateRequiredField(item);
if (required) {
return required;
}
if (allowUnicode) {
if (/\s/g.test(item)) {
return gettext('Please do not use any spaces in this field.');
}
}
else {
if (item !== encodeURIComponent(item) || item.match(/[!'()*]/)) {
return gettext('Please do not use any spaces or special characters in this field.');
}
}
return '';
};
// Ensure that sum length of key field values <= ${MAX_SUM_KEY_LENGTH} chars.
validateTotalKeyLength = function (key_field_selectors) {
var totalLength = _.reduce(
key_field_selectors,
function (sum, ele) { return sum + $(ele).val().length;},
0
);
return totalLength <= MAX_SUM_KEY_LENGTH;
};
checkTotalKeyLengthViolations = function(selectors, classes, key_field_selectors, message_tpl) {
if (!validateTotalKeyLength(key_field_selectors)) {
$(selectors.errorWrapper).addClass(classes.shown).removeClass(classes.hiding);
$(selectors.errorMessage).html('<p>' + _.template(message_tpl, {limit: MAX_SUM_KEY_LENGTH}) + '</p>');
$(selectors.save).addClass(classes.disabled);
} else {
$(selectors.errorWrapper).removeClass(classes.shown).addClass(classes.hiding);
}
};
return {
'toggleExpandCollapse': toggleExpandCollapse,
'showLoadingIndicator': showLoadingIndicator,
'hideLoadingIndicator': hideLoadingIndicator,
'confirmThenRunOperation': confirmThenRunOperation,
'runOperationShowingMessage': runOperationShowingMessage,
'disableElementWhileRunning': disableElementWhileRunning,
'deleteNotificationHandler': deleteNotificationHandler,
'setScrollTop': setScrollTop,
'getScrollOffset': getScrollOffset,
'setScrollOffset': setScrollOffset,
'redirect': redirect,
'reload': reload,
'hasChangedAttributes': hasChangedAttributes,
'validateRequiredField': validateRequiredField,
'validateURLItemEncoding': validateURLItemEncoding,
'validateTotalKeyLength': validateTotalKeyLength,
'checkTotalKeyLengthViolations': checkTotalKeyLengthViolations
};
});