diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 3a53d73bf..913a4182f 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -162,4 +162,4 @@ $(document).ready(function(){
$('.tree-link a').each( function(){
truncateLongString( $(this), 30);
});
-});
\ No newline at end of file
+});
diff --git a/app/assets/javascripts/direct-upload.js b/app/assets/javascripts/direct-upload.js
index aa5278567..86ece445d 100644
--- a/app/assets/javascripts/direct-upload.js
+++ b/app/assets/javascripts/direct-upload.js
@@ -1,14 +1,13 @@
(function (exports) {
+ // Edits (size, quality, parameters) image file for S3 server uploading
function generateThumbnail(origFile, type, max_width, max_height, cb) {
var img = new Image;
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
// todo allow for different x/y ratio
-
canvas.width = max_width;
canvas.height = max_height;
- img.src = URL.createObjectURL(origFile);
img.onload = function () {
var size;
@@ -18,77 +17,76 @@
if (this.width > this.height) {
size = this.height;
offsetX = (this.width - this.height) / 2;
-
} else {
size = this.width;
offsetY = (this.height - this.width) / 2;
}
-
if(type === "image/jpeg") {
type = "image/jpg";
}
- ctx.drawImage(this, offsetX, offsetY, size, size, 0, 0, canvas.width, canvas.height);
-
+ ctx.drawImage(this, offsetX, offsetY, size, size, 0, 0,
+ canvas.width, canvas.height);
canvas.toBlob(function (blob) {
cb(blob);
- }, type, 0.8)
+ }, type, 0.8);
};
+ img.src = URL.createObjectURL(origFile);
}
-
- function fetchUploadSignature(file, origId, signUrl, cb) {
+ // This server checks if files are OK (correct file type, presence,
+ // size and spoofing) and generates posts for S3 server file uploading
+ // (each post for different size of the same file)
+ // We do this synchronically, because we need to verify all files
+ // before uploading them
+ function fetchUploadSignature(file, signUrl, cb) {
var csrfParam = $("meta[name=csrf-param]").attr("content");
var csrfToken = $("meta[name=csrf-token]").attr("content");
- var xhr = new XMLHttpRequest;
- var data = [];
- data.push("file_name=" + file.name);
- data.push("file_size=" + file.size);
- data.push(csrfParam + "=" + encodeURIComponent(csrfToken));
+ var formData = new FormData();
+ formData.append("file", file);
- if (origId) {
- data.push("asset_id=" + origId);
- }
-
- xhr.open("POST", signUrl);
- xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
- xhr.send(data.join("&"));
-
- xhr.onload = function () {
- try {
- var data = JSON.parse(xhr.responseText);
- cb(data);
- } catch (e) {
- cb();
- }
- };
+ $.ajax({
+ url : signUrl,
+ type : 'POST',
+ data : formData,
+ async : false,
+ processData: false,
+ contentType: false,
+ complete : function(xhr) {
+ if (xhr.readyState === 4) { // complete
+ var data = JSON.parse(xhr.responseText);
+ cb(data);
+ } else if (xhr.readyState == 0) { // connection error
+ cb();
+ }
+ }
+ });
}
-
- function uploadData(data, cb) {
+ // Upload file to S3 server
+ function uploadData(postData, cb) {
var xhr = new XMLHttpRequest;
var fd = new FormData();
- var fields = data.fields;
- var url = data.url;
+ var fields = postData.fields;
+ var url = postData.url;
for (var k in fields) {
fd.append(k, fields[k]);
}
+ fd.append("file", postData.file, postData.fileName);
- fd.append("file", data.file, data.fileName);
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === 4) { // complete
+ cb();
+ } else if (xhr.readyState == 0) { // connection error
+ cb(I18n.t("errors.upload"));
+ }
+ }
xhr.open("POST", url);
xhr.send(fd);
-
- xhr.onload = function () {
- cb();
- };
- xhr.onerror = function (error) {
- cb(I18n.t("errors.upload"));
- };
}
-
var styleOptionRe = /(\d+)x(\d+)/i;
function parseStyleOption(option) {
@@ -100,71 +98,76 @@
};
}
+ // Validates files on this server and uploads them to S3 server
+ exports.directUpload = function (ev, fileInputs, signUrl, cb) {
+ var noErrors = true;
+ var inputPointer = 0;
+ animateSpinner();
- exports.directUpload = function (form, origId, signUrl, cb, cbErr, errKey) {
- var $fileInputs = $(form).find("input[type=file]");
- var file = $fileInputs.get(0).files[0];
+ function processFile () {
+ var fileInput = fileInputs.get(inputPointer);
+ if (!fileInput || !fileInput.files[0]) {
+ return;
+ }
+ var file = fileInput.files[0];
+ inputPointer++;
- var isValid = filesValidator($fileInputs);
+ fetchUploadSignature(file, signUrl, function (data) {
- if (!isValid) {
- cbErr();
- } else {
- fetchUploadSignature(file, origId, signUrl, function (data) {
+ function processError(errMsgs) {
+ renderFormError(ev, fileInput, errMsgs);
+ noErrors = false;
+ }
function processPost(error) {
- var postData = posts[postPosition];
-
+ // File post error handling
if (error) {
- var errObj = {};
- errKey = errKey|| "asset.file";
- errObj[errKey] = [error];
-
- cbErr(errObj);
- isValid = false;
- return;
+ processError(error);
}
+
+ var postData = posts[postPosition];
if (!postData) {
- cb(data.asset_id);
- isValid = false;
+ animateSpinner(null, false);
return;
}
-
postData.fileName = file.name;
postPosition += 1;
- var styleSize;
if (postData.style_option) {
- styleSize = parseStyleOption(postData.style_option);
-
+ var styleSize = parseStyleOption(postData.style_option);
generateThumbnail(file, postData.mime_type, styleSize.width,
styleSize.height, function (blob) {
postData.file = blob;
uploadData(postData, processPost);
});
-
- } else {
+ } else {
postData.file = file;
uploadData(postData, processPost);
}
}
- if (!data || data.status === 'error') {
- cbErr(data && data.errors);
- isValid = false;
- return;
+ // File signature error handling
+ if (_.isUndefined(data)) {
+ processError(I18n.t("errors.upload"));
+ }
+ if (data.status === "error") {
+ processError(jsonToValuesArray(data.errors));
}
- var posts = data.posts;
- var postPosition = 0;
+ processFile();
+ if(noErrors) {
+ // Use file input to pass file info on submit
+ cb(fileInput, data.asset_id);
- processPost();
+ var posts = data.posts;
+ var postPosition = 0;
+ processPost();
+ }
});
}
- return isValid;
+ processFile();
};
}(this));
-
diff --git a/app/assets/javascripts/my_modules/results.js b/app/assets/javascripts/my_modules/results.js
index 0382515d8..9a1551639 100644
--- a/app/assets/javascripts/my_modules/results.js
+++ b/app/assets/javascripts/my_modules/results.js
@@ -89,9 +89,9 @@ function initResultCommentsLink($el) {
var listItem = moreBtn.parents('li');
$(data.html).insertBefore(listItem);
if (data.results_number < data.per_page) {
- moreBtn.remove();
+ moreBtn.remove();
} else {
- moreBtn.attr("href", data.more_url);
+ moreBtn.attr("href", data.more_url);
}
}
});
@@ -302,39 +302,70 @@ function showTutorial() {
return tutorialModuleId == currentModuleId;
}
+var ResultTypeEnum = Object.freeze({
+ FILE: 0,
+ TABLE: 1,
+ TEXT: 2,
+ COMMENT: 3
+});
+
+function processResult(ev, resultTypeEnum, forS3) {
+ var $form = $(ev.target.form);
+ $form.clear_form_errors();
+
+ switch(resultTypeEnum) {
+ case ResultTypeEnum.FILE:
+ var $nameInput = $form.find("#result_name");
+ var nameValid = textValidator(ev, $nameInput, true);
+ var $fileInput = $form.find("#result_asset_attributes_file");
+ var filesValid = filesValidator(ev, $fileInput, FileTypeEnum.FILE);
+
+ if(nameValid && filesValid) {
+ if(forS3) {
+ // Redirects file uploading to S3
+ startFileUpload(ev, ev.target);
+ } else {
+ // Local file uploading
+ animateSpinner();
+ }
+ }
+ break;
+ case ResultTypeEnum.TABLE:
+ var $nameInput = $form.find("#result_name");
+ var nameValid = textValidator(ev, $nameInput, true);
+ break;
+ case ResultTypeEnum.TEXT:
+ var $nameInput = $form.find("#result_name");
+ var nameValid = textValidator(ev, $nameInput, true);
+ var $textInput = $form.find("#result_result_text_attributes_text");
+ textValidator(ev, $textInput, false, false);
+ break;
+ case ResultTypeEnum.COMMENT:
+ var $commentInput = $form.find("#comment_message");
+ var commentValid = textValidator(ev, $commentInput, false, false);
+ break;
+ }
+}
+
// S3 direct uploading
function startFileUpload(ev, btn) {
- var form = btn.form;
- var $form = $(form);
- var assetInput = $form.find("input[name='result[asset_attributes][id]']").get(0);
- var fileInput = $form.find("input[type=file]").get(0);
- var origAssetId = assetInput ? assetInput.value : null;
+ var $form = $(btn.form);
+ var $editFileInput = $form.find("input[name='result[asset_attributes][id]']").get(0);
+ var $fileInput = $form.find("input[type=file]");
var url = '/asset_signature.json';
- $form.clear_form_errors();
- animateSpinner();
-
- var noErrors = directUpload(form, origAssetId, url, function (assetId) {
- // edit mode - input field has to be removed
- if (assetInput) {
- assetInput.value = assetId;
+ directUpload(ev, $fileInput, url, function (fileInput, fileId) {
+ if ($editFileInput) {
+ // edit mode - input field has to be removed
+ $editFileInput.value = fileId;
$(fileInput).remove();
-
- // create mode
} else {
+ // create mode
fileInput.type = "hidden";
- fileInput.name = "result[asset_attributes][id]";
- fileInput.value = assetId;
+ fileInput.name = fileInput.name.replace("[file]", "[id]");
+ fileInput.value = fileId;
}
-
- btn.onclick = null;
- $(btn).click();
-
- }, function (errors) {
- showResultFormErrors($form, errors);
});
-
- return noErrors;
}
// This checks if the ctarget param exist in the
diff --git a/app/assets/javascripts/protocols/steps.js b/app/assets/javascripts/protocols/steps.js
index 5eb24c046..c6efbe7fe 100644
--- a/app/assets/javascripts/protocols/steps.js
+++ b/app/assets/javascripts/protocols/steps.js
@@ -201,7 +201,8 @@ function formEditAjax($form) {
renderTable($(this));
});
- animateSpinner(null, false);
+ var $stepImgs = $new_step.find("img");
+ reloadImagesHack($stepImgs);
})
.on("ajax:error", function(e, xhr, status, error) {
$(this).after(xhr.responseJSON.html);
@@ -220,8 +221,6 @@ function formEditAjax($form) {
$form.find("[data-role='step-hot-table']").each(function() {
renderTable($(this));
});
-
- animateSpinner(null, false);
});
}
@@ -247,7 +246,8 @@ function formNewAjax($form) {
$(this).handsontable("render");
});
- animateSpinner(null, false);
+ var $stepImgs = $new_step.find("img");
+ reloadImagesHack($stepImgs);
})
.on("ajax:error", function(e, xhr, status, error) {
$(this).after(xhr.responseJSON.html);
@@ -260,8 +260,6 @@ function formNewAjax($form) {
formCallback($form);
formNewAjax($form);
applyCancelOnNew();
-
- animateSpinner(null, false);
});
}
@@ -283,7 +281,6 @@ function toggleButtons(show) {
// Also hide "no steps" label if no steps are present
$("[data-role='no-steps-text']").hide();
-
}
}
@@ -580,7 +577,7 @@ $("[data-action='new-step']").on("ajax:success", function(e, data) {
// Needed because server-side validation failure clears locations of
// files to be uploaded and checklist's items etc. Also user
// experience is improved
-function stepValidator(ev, editMode, forS3) {
+function processStep(ev, editMode, forS3) {
var $form = $(ev.target.form);
$form.clear_form_errors();
@@ -589,20 +586,18 @@ function stepValidator(ev, editMode, forS3) {
removeBlankFileForms($form);
var $fileInputs = $form.find("input[type=file]");
- var filesValid = filesValidator(ev, $fileInputs);
+ var filesValid = filesValidator(ev, $fileInputs, FileTypeEnum.FILE);
var $checklists = $form.find(".nested_step_checklists");
var checklistsValid = checklistsValidator(ev, $checklists, editMode);
var $nameInput = $form.find("#step_name");
- var nameValid = nameValidator(ev, $nameInput);
+ var nameValid = textValidator(ev, $nameInput);
if(filesValid && checklistsValid && nameValid) {
if(forS3) {
- // Needed to redirect uploaded files to S3
+ // Redirects file uploading to S3
startFileUpload(ev, ev.target);
} else {
- // Files are saved locally
- // Validations passed, so animate spinner for possible file uploading
- // (startFileUpload already calls it)
+ // Local file uploading
animateSpinner();
}
}
@@ -659,54 +654,13 @@ function renderTable(table) {
// S3 direct uploading
function startFileUpload(ev, btn) {
- var form = btn.form;
- var $form = $(form);
- var fileInputs = $form.find("input[type=file]");
+ var $form = $(btn.form);
+ var $fileInputs = $form.find("input[type=file]");
var url = '/asset_signature.json';
- var inputPos = 0;
- var inputPointer = 0;
- animateSpinner();
-
- function processFile () {
- var fileInput = fileInputs.get(inputPos);
- inputPos += 1;
- inputPointer += 1;
-
- if (!fileInput) {
- btn.onclick = null;
- $(btn).click();
- return false;
- }
-
- return directUpload(form, null, url, function (assetId) {
- fileInput.type = "hidden";
- fileInput.name = fileInput.name.replace("[file]", "[id]");
- fileInput.value = assetId;
- inputPointer -= 1;
-
- processFile();
- }, function (errors) {
- var assetErrorMsg;
-
- for (var c in errors) {
- if (/^asset\./.test(c)) {
- assetErrorMsg = errors[c];
- break;
- }
- }
- if (assetErrorMsg) {
- var el = $form.find("input[type=file]").get(inputPointer - 1);
- var $el = $(el);
-
- $form.clear_form_errors();
- renderFormError(e, $el, assetErrorMsg);
- } else {
- tabsPropagateErrorClass($form);
- }
- });
- }
-
- var noErrors = processFile();
- return noErrors;
+ directUpload(ev, $fileInputs, url, function (fileInput, fileId) {
+ fileInput.type = "hidden";
+ fileInput.name = fileInput.name.replace("[file]", "[id]");
+ fileInput.value = fileId;
+ });
}
diff --git a/app/assets/javascripts/results/result_assets.js b/app/assets/javascripts/results/result_assets.js
index 518fb5db3..f4b1c7a64 100644
--- a/app/assets/javascripts/results/result_assets.js
+++ b/app/assets/javascripts/results/result_assets.js
@@ -3,7 +3,6 @@ $("#new-result-asset").on("ajax:success", function(e, data) {
var $form = $(data.html);
$("#results").prepend($form);
- $form.files_validator();
formAjaxResultAsset($form);
// Cancel button
@@ -18,7 +17,7 @@ $("#new-result-asset").on("ajax:success", function(e, data) {
});
$("#new-result-asset").on("ajax:error", function(e, xhr, status, error) {
- //TODO: Add error handling
+ // TODO
});
// Edit result asset button behaviour
@@ -30,7 +29,6 @@ function applyEditResultAssetCallback() {
$result.after($form);
$result.remove();
- $form.files_validator();
formAjaxResultAsset($form);
// Cancel button
@@ -47,46 +45,31 @@ function applyEditResultAssetCallback() {
});
$(".edit-result-asset").on("ajax:error", function(e, xhr, status, error) {
- //TODO: Add error handling
+ // TODO
});
}
-function showResultFormErrors($form, errors) {
- $form.render_form_errors("result", errors);
-
- if (errors["asset.file"]) {
- var $el = $form.find("input[type=file]");
-
- $el.closest(".form-group").addClass("has-error");
- $el.parent().append("" + errors["asset.file"] + "");
- }
-}
-
// Apply ajax callback to form
function formAjaxResultAsset($form) {
$form
.on("ajax:success", function(e, data) {
+ $form.after(data.html);
+ var newResult = $form.next();
+ initFormSubmitLinks(newResult);
+ $(this).remove();
+ applyEditResultAssetCallback();
+ applyCollapseLinkCallBack();
- if (data.status === "ok") {
- $form.after(data.html);
- var newResult = $form.next();
- initFormSubmitLinks(newResult);
- $(this).remove();
- applyEditResultAssetCallback();
- applyCollapseLinkCallBack();
- toggleResultEditButtons(true);
- initResultCommentTabAjax();
- expandResult(newResult);
+ toggleResultEditButtons(true);
+ initResultCommentTabAjax();
+ expandResult(newResult);
- } else if (data.status === 'error') {
- showResultFormErrors($form, data.errors);
- }
- animateSpinner(null, false);
+ var $resultImg = newResult.find("img");
+ reloadImagesHack($resultImg);
})
- .on("ajax:error", function() {
- animateSpinner(null, false);
+ .on("ajax:error", function(e, data) {
+ $form.render_form_errors("result", data.errors, true, e);
});
}
-
applyEditResultAssetCallback();
diff --git a/app/assets/javascripts/sitewide/draw_components.js b/app/assets/javascripts/sitewide/draw_components.js
new file mode 100644
index 000000000..5633ee4ce
--- /dev/null
+++ b/app/assets/javascripts/sitewide/draw_components.js
@@ -0,0 +1,21 @@
+// By adding unique attribute to image's src,
+// we force browser to reload/update cached image
+function reloadImage(img) {
+ var src = $(img).attr("src");
+ src = src.split("?", 1);
+ src += "?timestamp=" + new Date().getTime();
+ $(img).attr("src", src);
+}
+
+// Hack for image retrieval after upload (403 is
+// thrown, mostly Chrome issue, and hence image
+// isn't retrieved)
+function reloadImagesHack(imgs) {
+ setTimeout(function() {
+ if(imgs.length) {
+ imgs.each(function() {
+ reloadImage($(this));
+ });
+ }
+ }, 1000);
+}
diff --git a/app/assets/javascripts/sitewide/form_errors.js b/app/assets/javascripts/sitewide/form_errors.js
index 65ec92f71..9384725c4 100644
--- a/app/assets/javascripts/sitewide/form_errors.js
+++ b/app/assets/javascripts/sitewide/form_errors.js
@@ -1,61 +1,53 @@
// Define AJAX methods for handling errors on forms
-$.fn.render_form_errors = function(model_name, errors, clear) {
- if (clear || clear === undefined) {
+
+// Render errors specified in JSON format for many form elements
+$.fn.render_form_errors = function(modelName, errors, clear = true, ev) {
+ if (clear || _.isUndefined(clear)) {
this.clear_form_errors();
}
- $(this).render_form_errors_no_clear(model_name, errors, false);
-};
-$.fn.render_form_errors_input_group = function(model_name, errors) {
- this.clear_form_errors();
- $(this).render_form_errors_no_clear(model_name, errors, true);
-};
-
-$.fn.render_form_errors_no_clear = function(model_name, errors, input_group) {
var form = $(this);
-
$.each(errors, function(field, messages) {
- input = $(_.filter(form.find('input, select, textarea'), function(el) {
+ $input = $(_.filter(form.find('input, select, textarea'), function(el) {
var name = $(el).attr('name');
if (name) {
- return name.match(new RegExp(model_name + '\\[' + field + '\\(?'));
+ return name.match(new RegExp(modelName + '\\[' + field + '\\(?'));
}
return false;
}));
- input.closest('.form-group').addClass('has-error');
- var error_text = '';
- error_text += (_.map(messages, function(m) {
- return m.charAt(0).toUpperCase() + m.slice(1);
- })).join('
');
- error_text += '';
- if (input_group) {
- input.closest('.form-group').append(error_text);
- } else {
- input.parent().append(error_text);
- }
+
+ renderFormError(ev, $input, messages);
});
};
- // Show error message and mark error input (if errMsg is defined)
- // and, if present, mark and show the tab where the error occured,
- // and go to the input, if it is the most upper one or if errMsg is
- // undefined
- // NOTE: Similar to $.fn.render_form_errors, except here we process
- // one error at a time, which is not read from the form but is
- // specified manually.
-function renderFormError(ev, nameInput, errMsg, errAttributes) {
- if(!_.isUndefined(errMsg)) {
- var $errMsgSpan = $(nameInput).next(".help-block");
- errAttributes = _.isUndefined(errAttributes) ? "" : " " + errAttributes;
- if (!$errMsgSpan.length) {
- $(nameInput).closest(".form-group").addClass("has-error");
+ // Render errors specified in array of strings format (or string if
+ // just one error) for a single form element
+ //
+ // Show error message/s and mark error input (if errMsgs is defined)
+ // and, if present, mark and show the tab where the error occured and
+ // focus/scroll to the error input, if it is the first one to be
+ // specified or if errMsgs is undefined
+function renderFormError(ev, input, errMsgs, errAttributes) {
+ if (!_.isUndefined(errMsgs)) {
+ // Mark error form group
+ $formGroup = $(input).closest(".form-group");
+ if (!$formGroup.hasClass("has-error")) {
+ $formGroup.addClass("has-error");
}
- $(nameInput).after("" + errMsg + "");
+
+ // Add error message/s
+ errAttributes = _.isUndefined(errAttributes) ? "" : " " + errAttributes;
+ error_text = ($.makeArray(errMsgs).map(function(m) {
+ return m.strToErrorFormat();
+ })).join("
");
+ $errSpan = "" + error_text + "";
+ $formGroup.append($errSpan);
}
- $form = $(nameInput).closest("form");
- $tab = $(nameInput).closest(".tab-pane");
+ $form = $(input).closest("form");
+ $tab = $(input).closest(".tab-pane");
if ($tab.length) {
+ // Mark error tab
tabsPropagateErrorClass($form);
$parent = $tab;
} else {
@@ -63,19 +55,19 @@ function renderFormError(ev, nameInput, errMsg, errAttributes) {
}
// Focus and scroll to the error if it is the first (most upper) one
- if ($parent.find(".form-group.has-error").length === 1 || _.isUndefined(errMsg)) {
- goToFormElement(nameInput);
+ if ($parent.find(".form-group.has-error").length === 1 || _.isUndefined(errMsgs)) {
+ goToFormElement(input);
}
- // Don't submit form
- ev.preventDefault();
- ev.stopPropagation();
- // Remove spinner if present
- animateSpinner(null, false);
+ if(!_.isUndefined(ev)) {
+ // Don't submit form
+ ev.preventDefault();
+ ev.stopPropagation();
+ }
}
-// If any of tabs (if exist) has errors, mark parent tab
-// navigation link and show the tab (if not already)
+// If any of form tabs (if exist) has errors, mark it and
+// and show the first erroneous tab
function tabsPropagateErrorClass($form) {
var $contents = $form.find("div.tab-pane");
_.each($contents, function(tab) {
@@ -89,5 +81,5 @@ function tabsPropagateErrorClass($form) {
}
}
});
- $(".nav-tabs .has-error:first:not(.active) > a", $form).tab("show");
+ $form.find(".nav-tabs .has-error:first > a", $form).tab("show");
}
diff --git a/app/assets/javascripts/sitewide/form_validators.js.erb b/app/assets/javascripts/sitewide/form_validators.js.erb
index db86d5a71..04032badb 100644
--- a/app/assets/javascripts/sitewide/form_validators.js.erb
+++ b/app/assets/javascripts/sitewide/form_validators.js.erb
@@ -1,19 +1,19 @@
// Form validators. They'll find, render and focus error/s and
// prevent form submission.
-function nameValidator(ev, $nameInput) {
- var nameTooShort = $nameInput.val().length === 0;
- var nameTooLong = $nameInput.val().length > 50;
+function textValidator(ev, textInput, canBeBlank = false, limitLength = true) {
+ var nameTooShort = $(textInput).val().length === 0;
+ var nameTooLong = $(textInput).val().length > 50;
var errMsg;
- if (nameTooShort) {
- errMsg = I18n.t("devise.names.not_blank");
- } else if (nameTooLong) {
- errMsg = I18n.t("devise.names.length_too_long", { max_length: 50 });
+ if (!canBeBlank && nameTooShort) {
+ errMsg = I18n.t("general.text.not_blank");
+ } else if (limitLength && nameTooLong) {
+ errMsg = I18n.t("general.text.length_too_long", { max_length: 50 });
}
var hasErrors = !_.isUndefined(errMsg);
if (hasErrors) {
- renderFormError(ev, $nameInput, errMsg);
+ renderFormError(ev, $(textInput), errMsg);
}
return !hasErrors;
}
@@ -41,7 +41,7 @@ function checklistsValidator(ev, checklists, editMode) {
if (!$checklistNameInput.val()) {
if (anyChecklistItemFilled || editMode) {
// In edit mode, checklist's name can't be blank
- var errMsg = I18n.t("devise.names.not_blank");
+ var errMsg = I18n.t("general.text.not_blank");
renderFormError(ev, $checklistNameInput, errMsg);
noErrors = false;
} else {
@@ -55,62 +55,77 @@ function checklistsValidator(ev, checklists, editMode) {
return noErrors;
}
-// Add JavaScript client-side upload file checking
-// Callback function can be provided to be called
-// any time at least one file size is not valid
-$.fn.files_validator = function(callback) {
+$.fn.files_validator = function(fileTypeEnum) {
var $form = $(this);
if ($form.length) {
$form.submit(function (ev) {
$form.clear_form_errors();
var $fileInputs = $form.find("input[type=file]");
- filesValidator(ev, $fileInputs, callback);
+ filesValidator(ev, $fileInputs, fileTypeEnum);
});
}
};
-function filesValidator(ev, fileInputs, callback) {
- var filesSizeValid = true;
+function filesValidator(ev, fileInputs, fileTypeEnum) {
+ var filesValid = true;
if (fileInputs.length) {
- var filesSizeValid = filesSizeValidator(ev, fileInputs);
+ var filesPresentValid = filesPresentValidator(ev, fileInputs);
+ var filesSizeValid = filesSizeValidator(ev, fileInputs, fileTypeEnum);
// TODO File content check
-
- if (!filesSizeValid && callback) {
- callback();
- }
+ filesValid = filesPresentValid && filesSizeValid;
}
- return filesSizeValid;
+ return filesValid;
}
-function filesSizeValidator(ev, fileInputs) {
+function filesPresentValidator(ev, fileInputs) {
+ var filesPresentValid = true;
+ _.each(fileInputs, function(fileInput) {
+ if (!fileInput.files[0]) {
+ assetError = I18n.t("general.file.blank");
+ renderFormError(ev, fileInput, assetError, "data-error='file-missing'");
+ filesPresentValid = false;
+ }
+ });
+ return filesPresentValid;
+}
+
+var FileTypeEnum = Object.freeze({
+ FILE: 0,
+ AVATAR: 1
+});
+
+function filesSizeValidator(ev, fileInputs, fileTypeEnum) {
function getFileTooBigError(file) {
if (!file) {
return ;
}
var size = parseInt(file.size);
- <% sizeLimit = FILE_SIZE_LIMIT %>;
- if (size > <%= sizeLimit.megabytes %>) {
- return "<%= I18n.t 'general.file_size_exceeded', file_size: sizeLimit %>".strToFormFormat();
+ <% avatarSizerLimit = AVATAR_SIZE_LIMIT %>;
+ <% fileSizeLimit = FILE_SIZE_LIMIT %>;
+
+ if (fileTypeEnum == FileTypeEnum.FILE && size > <%= fileSizeLimit.megabytes %>) {
+ return "<%= I18n.t 'general.file.size_exceeded', file_size: fileSizeLimit %>".strToErrorFormat();
+ } else if (fileTypeEnum == FileTypeEnum.AVATAR && size > <%= avatarSizerLimit.megabytes %>) {
+ return "<%= I18n.t 'general.file.size_exceeded', file_size: avatarSizerLimit %>".strToErrorFormat();
}
};
// Check if any file exceeds allowed size limit
- var fileSizeValid = true;
+ var filesSizeValid = true;
_.each(fileInputs, function(fileInput) {
var file = fileInput.files[0];
var assetError = getFileTooBigError(file);
- var input = $(fileInput);
if (assetError) {
- renderFormError(ev, input, assetError, "data-error='file-size'");
- fileSizeValid = false;
+ renderFormError(ev, fileInput, assetError, "data-error='file-size'");
+ filesSizeValid = false;
}
});
- if(fileSizeValid) {
+ if(filesSizeValid) {
// Check if there is enough free space for the files
- fileSizeValid = enaughSpaceValidator(ev, fileInputs);
+ filesSizeValid = enaughSpaceValidator(ev, fileInputs);
}
- return fileSizeValid;
+ return filesSizeValid;
}
// Overriden in billing module for checking
diff --git a/app/assets/javascripts/sitewide/string_utils.js b/app/assets/javascripts/sitewide/string_utils.js
index 67156d05c..cab16e8d4 100644
--- a/app/assets/javascripts/sitewide/string_utils.js
+++ b/app/assets/javascripts/sitewide/string_utils.js
@@ -14,12 +14,12 @@ function truncateLongString( el, chars ) {
}
}
-// Usefull for converting locals messages to form
+// Usefull for converting locals messages to error
// format (i.e. lower cased capital and no dot)
-String.prototype.strToFormFormat = function() {
+String.prototype.strToErrorFormat = function() {
var length = this.length;
if (this[length - 1] === ".") {
length -= 1;
}
return this.charAt(0).toLowerCase() + this.slice(1, length);
-}
\ No newline at end of file
+}
diff --git a/app/assets/javascripts/sitewide/utils.js b/app/assets/javascripts/sitewide/utils.js
new file mode 100644
index 000000000..072cae556
--- /dev/null
+++ b/app/assets/javascripts/sitewide/utils.js
@@ -0,0 +1,12 @@
+// Converts JSON data received from the server
+// to flat array of values
+function jsonToValuesArray(jsonData) {
+ errMsgs =[];
+ for (var key in jsonData) {
+ var values = jsonData[key];
+ $.each(values, function(idx, val) {
+ errMsgs.push(val);
+ });
+ }
+ return errMsgs;
+}
diff --git a/app/assets/javascripts/users/registrations/edit.js b/app/assets/javascripts/users/registrations/edit.js
index c88c521f6..c90cd2de8 100644
--- a/app/assets/javascripts/users/registrations/edit.js
+++ b/app/assets/javascripts/users/registrations/edit.js
@@ -31,28 +31,29 @@ var forms = $("form[data-for]");
// Add "edit form" listeners
forms
.find("[data-action='edit']").click(function() {
- var form = $(this).closest("form");
+ var $form = $(this).closest("form");
// First, hide all form edits
_.each(forms, function(form) {
- toggleFormVisibility($(form), false);
+ toggleFormVisibility($form, false);
});
// Then, edit the current form
- toggleFormVisibility(form, true);
+ toggleFormVisibility($form, true);
});
// Add "cancel form" listeners
forms
.find("[data-action='cancel']").click(function() {
- var form = $(this).closest("form");
+ var $form = $(this).closest("form");
// Hide the edit portion of the form
- toggleFormVisibility(form, false);
+ toggleFormVisibility($form, false);
});
// Add form submit listeners
forms
+.not("[data-for='avatar']")
.on("ajax:success", function(ev, data, status) {
// Simply reload the page
location.reload();
@@ -62,52 +63,28 @@ forms
$(this).render_form_errors("user", data.responseJSON);
});
-// Add upload file size checking
-$("form[data-for='avatar']").files_validator();
+function processFile(ev, forS3) {
+ var $form = $(ev.target.form);
+ $form.clear_form_errors();
+
+ var $fileInput = $form.find("input[type=file]");
+ if(filesValidator(ev, $fileInput, FileTypeEnum.AVATAR)) {
+ if(forS3) {
+ // Redirects file uploading to S3
+ startFileUpload(ev, ev.target);
+ } else {
+ // Local file uploading
+ animateSpinner();
+ }
+ }
+}
// S3 direct uploading
function startFileUpload(ev, btn) {
- var form = btn.form;
- var $form = $(form);
- var fileInput = $form.find("input[type=file]").get(0);
+ var $form = $(btn.form);
+ var $fileInput = $form.find("input[type=file]");
var url = "/avatar_signature.json";
- $form.clear_form_errors();
- animateSpinner($form);
-
- var noErrors = directUpload(form, null, url, function (assetId) {
- var file = fileInput.files[0];
- fileInput.type = "hidden";
- fileInput.name = fileInput.name.replace("[avatar]", "[avatar_file_name]");
- fileInput.value = file.name;
-
- $("#user_change_avatar").remove();
-
- btn.onclick = null;
- $(btn).click();
- animateSpinner($form, false);
- }, function (errors) {
- $form.render_form_errors("user", errors);
-
- var avatarErrorMsg;
-
- animateSpinner($form, false);
- for (var c in errors) {
- if (/^avatar/.test(c)) {
- avatarErrorMsg = errors[c];
- break;
- }
- }
-
- if (avatarErrorMsg) {
- var $el = $form.find("input[type=file]");
-
- $form.clear_form_errors();
- renderFormError(ev, $el, avatarErrorMsg);
- }
- }, "avatar");
-
- return noErrors;
+ directUpload(ev, $fileInput, url, function () {
+ });
}
-
-
diff --git a/app/assets/javascripts/users/settings/organizations/add_user_modal.js b/app/assets/javascripts/users/settings/organizations/add_user_modal.js
index 9c36ec7d7..4a64091db 100644
--- a/app/assets/javascripts/users/settings/organizations/add_user_modal.js
+++ b/app/assets/javascripts/users/settings/organizations/add_user_modal.js
@@ -119,7 +119,7 @@ modal
})
.on("ajax:error", inviteExistingForm.selector, function(ev, data, status) {
// Display form errors
- inviteExistingForm.render_form_errors_input_group("", data.responseJSON);
+ inviteExistingForm.render_form_errors("", data.responseJSON);
});
// Update values & enable "invite" button
diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb
index b72ca1d2f..2f3d16e20 100644
--- a/app/controllers/assets_controller.rb
+++ b/app/controllers/assets_controller.rb
@@ -6,26 +6,28 @@ class AssetsController < ApplicationController
respond_to do |format|
format.json {
- if params[:asset_id]
- asset = Asset.find_by_id params[:asset_id]
+ validationAsset = nil
+ if asset_params[:asset_id]
+ asset = Asset.find_by_id asset_params[:asset_id]
asset.file.destroy
- asset.file_empty params[:file_name], params[:file_size]
+ asset.file_empty asset_params[:file].original_filename, asset_params[:file].size()
+ validationAsset = asset
else
- asset = Asset.new_empty params[:file_name], params[:file_size]
+ # We can't verify file content (spoofing) of an empty
+ # file, so we use dummy validationAsset instead
+ asset = Asset.new_empty asset_params[:file].original_filename, asset_params[:file].size()
+ validationAsset = Asset.new(asset_params)
end
- if not asset.valid?
- errors = Hash[asset.errors.map{|k,v| ["asset.#{k}",v]}]
-
+ if not validationAsset.valid?
render json: {
status: 'error',
- errors: errors
- }
+ errors: validationAsset.errors
+ } , status: :bad_request
else
asset.save!
posts = generate_upload_posts asset
-
render json: {
asset_id: asset.id,
posts: posts
@@ -149,4 +151,11 @@ class AssetsController < ApplicationController
posts
end
+ def asset_params
+ params.permit(
+ :asset_id,
+ :file
+ )
+ end
+
end
diff --git a/app/controllers/result_assets_controller.rb b/app/controllers/result_assets_controller.rb
index 73a0055f9..362559976 100644
--- a/app/controllers/result_assets_controller.rb
+++ b/app/controllers/result_assets_controller.rb
@@ -111,11 +111,18 @@ class ResultAssetsController < ApplicationController
update_params = result_params
previous_size = @result.space_taken
- @result.asset.last_modified_by = current_user
+ if update_params.key? :asset_attributes
+ asset = Asset.find_by_id(update_params[:asset_attributes][:id])
+ asset.created_by = current_user
+ asset.last_modified_by = current_user
+ @result.asset = asset
+ end
+
@result.last_modified_by = current_user
@result.assign_attributes(update_params)
success_flash = t("result_assets.update.success_flash",
module: @my_module.name)
+
if @result.archived_changed?(from: false, to: true)
saved = @result.archive(current_user)
success_flash = t("result_assets.archive.success_flash",
@@ -140,8 +147,6 @@ class ResultAssetsController < ApplicationController
saved = @result.save
if saved then
- @result.reload
-
# Release organization's space taken due to
# previous asset being removed
org = @result.my_module.experiment.project.organization
@@ -175,7 +180,6 @@ class ResultAssetsController < ApplicationController
}
format.json {
render json: {
- status: 'ok',
html: render_to_string({
partial: "my_modules/result.html.erb", locals: {result: @result}
})
diff --git a/app/controllers/result_comments_controller.rb b/app/controllers/result_comments_controller.rb
index 8285a0b60..767a7124e 100644
--- a/app/controllers/result_comments_controller.rb
+++ b/app/controllers/result_comments_controller.rb
@@ -84,7 +84,7 @@ class ResultCommentsController < ApplicationController
format.html { render :new }
format.json {
render json: {
- errors: @comment.errors.to_hash(true)
+ errors: @comment.errors
}
}
end
diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb
index c3a1395cb..1af1eb456 100644
--- a/app/controllers/steps_controller.rb
+++ b/app/controllers/steps_controller.rb
@@ -33,12 +33,21 @@ class StepsController < ApplicationController
step_data = step_params.except(:assets_attributes)
step_assets = step_params.slice(:assets_attributes)
@step = Step.new(step_data)
+
if step_assets.size > 0
step_assets[:assets_attributes].each do |i, data|
- asset = Asset.find_by_id(data[:id])
- asset.created_by = current_user
- asset.last_modified_by = current_user
- @step.assets << asset
+ # Ignore destroy requests on create
+ if data[:_destroy].nil?
+ if data[:id].present?
+ asset = Asset.find_by_id(data[:id])
+ else
+ # For validation purposses if no JS
+ asset = Asset.new(data)
+ end
+ asset.created_by = current_user
+ asset.last_modified_by = current_user
+ @step.assets << asset
+ end
end
end
else
@@ -151,10 +160,10 @@ class StepsController < ApplicationController
if step_assets.include? :assets_attributes
step_assets[:assets_attributes].each do |i, data|
- asset_id = data[:id]
- asset = Asset.find_by_id(asset_id)
+ asset = Asset.find_by_id(data[:id])
unless @step.assets.include? asset or not asset
+ asset.created_by = current_user
asset.last_modified_by = current_user
@step.assets << asset
end
@@ -203,18 +212,13 @@ class StepsController < ApplicationController
format.json {
render json: {
html: render_to_string({
- partial: "steps/step.html.erb", locals: {step: @step}
- })}, status: :ok
+ partial: "step.html.erb", locals: {step: @step}
+ })}
}
else
format.json {
- render json: {
- html: render_to_string({
- partial: "edit.html.erb",
- locals: {
- direct_upload: @direct_upload
- }
- })}, status: :bad_request
+ render json: @step.errors,
+ status: :bad_request
}
end
end
@@ -533,14 +537,13 @@ class StepsController < ApplicationController
attr_params = update_params[key]
for pos, attrs in params[key] do
+ assetExists = Asset.exists?(attrs[:id])
if attrs[:_destroy] == "1"
attr_params[pos] = {id: attrs[:id], _destroy: "1"}
- params[key].delete(pos)
- else
- if has_destroy_params(params[key][pos])
- attr_params[pos] = {id: attrs[:id]}
- extract_destroy_params(params[key][pos], attr_params[pos])
- end
+ params[key].delete(pos) if assetExists
+ elsif has_destroy_params(params[key][pos])
+ attr_params[pos] = {id: attrs[:id]} if assetExists
+ extract_destroy_params(params[key][pos], attr_params[pos])
end
end
end
diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb
index 3ad72659b..67a8a26d4 100644
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
@@ -1,5 +1,4 @@
class Users::RegistrationsController < Devise::RegistrationsController
-
before_action :load_paperclip_vars
def avatar
@@ -14,13 +13,18 @@ class Users::RegistrationsController < Devise::RegistrationsController
# Changed avatar values are only used for pre-generating S3 key
# and user object is not persisted with this values.
- current_user.empty_avatar params[:file_name], params[:file_size]
+ current_user.empty_avatar avatar_params[:file].original_filename, avatar_params[:file].size()
- unless current_user.valid?
+ validationAsset = Asset.new(avatar_params)
+ unless current_user.valid? and validationAsset.valid?
+ if validationAsset.errors[:file].any?
+ # Add file content error
+ current_user.errors[:avatar] << validationAsset.errors[:file].first
+ end
render json: {
status: 'error',
errors: current_user.errors
- }
+ }, status: :bad_request
else
render json: {
posts: generate_upload_posts
@@ -107,7 +111,7 @@ class Users::RegistrationsController < Devise::RegistrationsController
format.json {
flash.keep
sign_in resource_name, resource, bypass: true
- render json: { status: :ok }
+ render json: {}
}
else
clean_up_passwords resource
@@ -116,7 +120,7 @@ class Users::RegistrationsController < Devise::RegistrationsController
}
format.json {
render json: self.resource.errors,
- status: :unprocessable_entity
+ status: :bad_request
}
end
end
@@ -189,6 +193,14 @@ class Users::RegistrationsController < Devise::RegistrationsController
)
end
+ def avatar_params
+ params.permit(
+ :file
+ )
+ end
+
+ # Generates posts for uploading files (many sizes of same file)
+ # to S3 server
def generate_upload_posts
posts = []
file_size = current_user.avatar_file_size
@@ -237,4 +249,5 @@ class Users::RegistrationsController < Devise::RegistrationsController
def after_inactive_sign_up_path_for(resource)
new_user_session_path
end
+
end
diff --git a/app/helpers/assets_helper.rb b/app/helpers/assets_helper.rb
index 4ead88e69..8a9bdbace 100644
--- a/app/helpers/assets_helper.rb
+++ b/app/helpers/assets_helper.rb
@@ -11,7 +11,7 @@ module AssetsHelper
data-download-url='#{download_asset_path(asset)}'
>
- #{t("general.file_loading", fileName: asset.file_file_name)}
+ #{t("general.file.loading", fileName: asset.file_file_name)}