mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-12-27 02:04:33 +08:00
A lot of file uploading edge cases considered. File uploading is now actually redirected to S3 server, as before was not. Error functions changed and error output format specified, which should be used consistently throughout the application. Some other refactoring.
This commit is contained in:
parent
b70f49f4e0
commit
951cf67b3d
32 changed files with 441 additions and 409 deletions
|
@ -162,4 +162,4 @@ $(document).ready(function(){
|
|||
$('.tree-link a').each( function(){
|
||||
truncateLongString( $(this), 30);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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("<span class='help-block'>" + errors["asset.file"] + "</span>");
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
|
21
app/assets/javascripts/sitewide/draw_components.js
Normal file
21
app/assets/javascripts/sitewide/draw_components.js
Normal file
|
@ -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);
|
||||
}
|
|
@ -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 = '<span class="help-block">';
|
||||
error_text += (_.map(messages, function(m) {
|
||||
return m.charAt(0).toUpperCase() + m.slice(1);
|
||||
})).join('<br />');
|
||||
error_text += '</span>';
|
||||
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("<span class='help-block'" + errAttributes + ">" + errMsg + "</span>");
|
||||
|
||||
// Add error message/s
|
||||
errAttributes = _.isUndefined(errAttributes) ? "" : " " + errAttributes;
|
||||
error_text = ($.makeArray(errMsgs).map(function(m) {
|
||||
return m.strToErrorFormat();
|
||||
})).join("<br />");
|
||||
$errSpan = "<span class='help-block'" + errAttributes + ">" + error_text + "</span>";
|
||||
$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");
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
12
app/assets/javascripts/sitewide/utils.js
Normal file
12
app/assets/javascripts/sitewide/utils.js
Normal file
|
@ -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;
|
||||
}
|
|
@ -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 () {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,7 +11,7 @@ module AssetsHelper
|
|||
data-download-url='#{download_asset_path(asset)}'
|
||||
>
|
||||
<span class='asset-loading-spinner' id='asset-loading-spinner-#{asset.id}'></span>
|
||||
#{t("general.file_loading", fileName: asset.file_file_name)}
|
||||
#{t("general.file.loading", fileName: asset.file_file_name)}
|
||||
</span>
|
||||
<script type='text/javascript'>
|
||||
$('#asset-loading-spinner-#{asset.id}').spin(
|
||||
|
|
|
@ -42,6 +42,10 @@ class Asset < ActiveRecord::Base
|
|||
has_many :report_elements, inverse_of: :asset, dependent: :destroy
|
||||
has_one :asset_text_datum, inverse_of: :asset, dependent: :destroy
|
||||
|
||||
# Specific file errors propagate to "fire" error hash key,
|
||||
# so use just these errors
|
||||
after_validation :filter_paperclip_errors
|
||||
|
||||
attr_accessor :file_content, :file_info, :preview_cached
|
||||
|
||||
def file_empty(name, size)
|
||||
|
@ -263,6 +267,14 @@ class Asset < ActiveRecord::Base
|
|||
|
||||
private
|
||||
|
||||
def filter_paperclip_errors
|
||||
if errors.size > 1
|
||||
temp_errors = errors[:file]
|
||||
errors.clear()
|
||||
errors.set(:file, temp_errors)
|
||||
end
|
||||
end
|
||||
|
||||
def file_changed?
|
||||
previous_changes.present? and
|
||||
(
|
||||
|
|
|
@ -80,9 +80,10 @@ class User < ActiveRecord::Base
|
|||
has_many :archived_protocols, class_name: 'Protocol', foreign_key: 'archived_by_id', inverse_of: :archived_by
|
||||
has_many :restored_protocols, class_name: 'Protocol', foreign_key: 'restored_by_id', inverse_of: :restored_by
|
||||
|
||||
# Prevents repetition of errors after validation (e.g. if file_size
|
||||
# exceeds limits, 2 same errors will be shown)
|
||||
after_validation :clean_paperclip_errors
|
||||
# If other errors besides parameter "avatar" exist,
|
||||
# they will propagate to "avatar" also, so remove them
|
||||
# and put all other (more specific ones) in it
|
||||
after_validation :filter_paperclip_errors
|
||||
|
||||
def name
|
||||
full_name
|
||||
|
@ -160,8 +161,18 @@ class User < ActiveRecord::Base
|
|||
self.avatar_file_size = size.to_i
|
||||
end
|
||||
|
||||
def clean_paperclip_errors
|
||||
errors.delete(:avatar)
|
||||
def filter_paperclip_errors
|
||||
if errors.key? :avatar
|
||||
errors.delete(:avatar)
|
||||
messages = []
|
||||
errors.each do |attribute, error|
|
||||
errors.full_messages_for(attribute).each do |message|
|
||||
messages << message.split(' ').drop(1).join(' ')
|
||||
end
|
||||
end
|
||||
errors.clear()
|
||||
errors.set(:avatar, messages)
|
||||
end
|
||||
end
|
||||
|
||||
# Whether user is active (= confirmed) or not
|
||||
|
|
|
@ -7,11 +7,7 @@
|
|||
<%= ff.file_field :file %>
|
||||
<% end %>
|
||||
<hr>
|
||||
<% if direct_upload %>
|
||||
<%= f.submit t("result_assets.edit.update"), class: 'btn btn-primary', onclick: 'startFileUpload(event, this);' %>
|
||||
<% else %>
|
||||
<%= f.submit t("result_assets.edit.update"), class: 'btn btn-primary', onclick: 'animateSpinner();' %>
|
||||
<% end %>
|
||||
<%= f.submit t("result_assets.edit.update"), class: 'btn btn-primary', onclick: "processResult(event, ResultTypeEnum.FILE, #{direct_upload});" %>
|
||||
<button type="button" class="btn btn-default cancel-edit">
|
||||
<%= t("general.cancel")%>
|
||||
</button>
|
||||
|
|
|
@ -4,11 +4,7 @@
|
|||
<%= f.fields_for :asset do |ff| %>
|
||||
<%= ff.file_field :file %>
|
||||
<% end %>
|
||||
<% if direct_upload %>
|
||||
<%= f.submit t("result_assets.new.create"), class: 'btn btn-primary', onclick: 'startFileUpload(event, this);' %>
|
||||
<% else %>
|
||||
<%= f.submit t("result_assets.new.create"), class: 'btn btn-primary', onclick: 'animateSpinner();' %>
|
||||
<% end %>
|
||||
<%= f.submit t("result_assets.new.create"), class: 'btn btn-primary', onclick: "processResult(event, ResultTypeEnum.FILE, #{direct_upload});" %>
|
||||
<button type="button" class="btn btn-default cancel-new">
|
||||
<%= t("general.cancel")%>
|
||||
</button>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<li>
|
||||
<hr>
|
||||
<%= bootstrap_form_for :comment, url: { format: :json }, method: :post, remote: true do |f| %>
|
||||
<%= f.text_field :message, hide_label: true, placeholder: t("general.comment_placeholder"), append: f.submit("+"), help: '.' %>
|
||||
<%= f.text_field :message, hide_label: true, placeholder: t("general.comment_placeholder"), append: f.submit("+", onclick: "processResult(event, ResultTypeEnum.COMMENT);"), help: '.' %>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<hr>
|
||||
<%= f.submit t("result_tables.edit.update"), class: 'btn btn-primary' %>
|
||||
<%= f.submit t("result_tables.edit.update"), class: 'btn btn-primary', onclick: "processResult(event, ResultTypeEnum.TABLE);" %>
|
||||
<button type="button" class="btn btn-default cancel-edit">
|
||||
<%= t("general.cancel")%>
|
||||
</button>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= f.submit t("result_tables.new.create"), class: 'btn btn-primary' %>
|
||||
<%= f.submit t("result_tables.new.create"), class: 'btn btn-primary', onclick: "processResult(event, ResultTypeEnum.TABLE);" %>
|
||||
<button type="button" class="btn btn-default cancel-new">
|
||||
<%= t("general.cancel")%>
|
||||
</button>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<%= ff.text_area :text, style: "margin-top: 10px;" %><br />
|
||||
<% end %>
|
||||
<hr>
|
||||
<%= f.submit t("result_texts.edit.update"), class: 'btn btn-primary' %>
|
||||
<%= f.submit t("result_texts.edit.update"), class: 'btn btn-primary', onclick: "processResult(event, ResultTypeEnum.TEXT);" %>
|
||||
<button type="button" class="btn btn-default cancel-edit">
|
||||
<%= t("general.cancel")%>
|
||||
</button>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<%= f.fields_for :result_text do |ff| %>
|
||||
<%= ff.text_area :text, style: "margin-top: 10px;" %><br />
|
||||
<% end %>
|
||||
<%= f.submit t("result_texts.new.create"), class: 'btn btn-primary' %>
|
||||
<%= f.submit t("result_texts.new.create"), class: 'btn btn-primary', onclick: "processResult(event, ResultTypeEnum.TEXT);" %>
|
||||
<button type="button" class="btn btn-default cancel-new">
|
||||
<%= t("general.cancel")%>
|
||||
</button>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<hr>
|
||||
<%= render partial: "empty_step.html.erb", locals: {step: @step, f: f} %>
|
||||
<hr>
|
||||
<%= f.submit t("protocols.steps.edit.edit_step"), class: 'btn btn-primary', onclick: "stepValidator(event, true, #{direct_upload});" %>
|
||||
<%= f.submit t("protocols.steps.edit.edit_step"), class: 'btn btn-primary', onclick: "processStep(event, true, #{direct_upload});" %>
|
||||
<a type="button" data-action="cancel-edit" class="btn btn-default" href="<%= step_path(id: @step, format: :json) %>" data-remote="true">
|
||||
<%= t("general.cancel")%>
|
||||
</a>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<hr>
|
||||
<%= render partial: "empty_step.html.erb", locals: {step: @step, f: f} %>
|
||||
<hr>
|
||||
<%= f.submit t("protocols.steps.new.add_step"), class: 'btn btn-primary', onclick: "stepValidator(event, false, #{direct_upload});" %>
|
||||
<%= f.submit t("protocols.steps.new.add_step"), class: 'btn btn-primary', onclick: "processStep(event, false, #{direct_upload});" %>
|
||||
<button type="button" data-action="cancel-new" class="btn btn-default">
|
||||
<%= t("general.cancel")%>
|
||||
</button>
|
||||
|
|
|
@ -28,11 +28,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<a href="#" class="btn btn-default" data-action="cancel"><%=t "general.cancel" %></a>
|
||||
<% if @direct_upload %>
|
||||
<%= f.submit t("users.registrations.edit.avatar_submit"), class: "btn btn-primary", onclick: 'startFileUpload(event, this);' %>
|
||||
<% else %>
|
||||
<%= f.submit t("users.registrations.edit.avatar_submit"), class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
<%= f.submit t("users.registrations.edit.avatar_submit"), class: 'btn btn-primary', onclick: "processFile(event, #{@direct_upload});" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,6 +24,9 @@ TAG_COLORS = [
|
|||
# Maximum uploaded file size in MB
|
||||
FILE_SIZE_LIMIT = 50
|
||||
|
||||
# Maximum uploaded avatar size in MB
|
||||
AVATAR_SIZE_LIMIT = 0.2
|
||||
|
||||
SEARCH_LIMIT = 20
|
||||
|
||||
SHOW_ALL_RESULTS = -1
|
||||
|
|
|
@ -18,9 +18,6 @@ en:
|
|||
head_title: "Forgot password"
|
||||
title: "Forgot your password?"
|
||||
submit: "Send me reset password instructions"
|
||||
names:
|
||||
not_blank: "name can't be blank"
|
||||
length_too_long: "name is too long (maximum is %{max_length} characters)"
|
||||
registrations:
|
||||
password_changed: "Password successfully updated."
|
||||
sessions:
|
||||
|
@ -1415,9 +1412,13 @@ en:
|
|||
public: "public"
|
||||
private: "private"
|
||||
search: "Search"
|
||||
file_size_exceeded: "File size must be less than %{file_size} MB."
|
||||
file_blank: "Please select a file."
|
||||
file_loading: "%{fileName} is loading..."
|
||||
file:
|
||||
loading: "%{fileName} is loading..."
|
||||
size_exceeded: "File size must be less than %{file_size} MB."
|
||||
blank: "You didn't select any file"
|
||||
text:
|
||||
not_blank: "can't be blank"
|
||||
length_too_long: "is too long (maximum is %{max_length} characters)"
|
||||
|
||||
mailer:
|
||||
invitation_to_organization:
|
||||
|
|
Loading…
Reference in a new issue