mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 14:45:56 +08:00
Merge branch 'decoupling-settings-page' of https://github.com/biosistemika/scinote-web into zd_SCI_1669
This commit is contained in:
commit
5538805faa
2
Gemfile
2
Gemfile
|
@ -49,6 +49,7 @@ gem 'commit_param_routing' # Enables different submit actions in the same form t
|
|||
gem 'kaminari'
|
||||
gem "i18n-js", ">= 3.0.0.rc11" # Localization in javascript files
|
||||
gem 'roo', '~> 2.7.1' # Spreadsheet parser
|
||||
gem 'creek'
|
||||
gem 'wicked_pdf'
|
||||
gem 'silencer' # Silence certain Rails logs
|
||||
gem 'wkhtmltopdf-heroku'
|
||||
|
@ -68,6 +69,7 @@ gem 'activerecord-import'
|
|||
|
||||
gem 'paperclip', '~> 5.1' # File attachment, image attachment library
|
||||
gem 'aws-sdk', '~> 2'
|
||||
gem 'aws-sdk-v1'
|
||||
|
||||
gem 'delayed_job_active_record'
|
||||
gem 'devise-async',
|
||||
|
|
156
Gemfile.lock
156
Gemfile.lock
|
@ -73,7 +73,7 @@ GEM
|
|||
activemodel (= 5.1.1)
|
||||
activesupport (= 5.1.1)
|
||||
arel (~> 8.0)
|
||||
activerecord-import (0.19.1)
|
||||
activerecord-import (0.20.2)
|
||||
activerecord (>= 3.2)
|
||||
activesupport (5.1.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
|
@ -87,32 +87,36 @@ GEM
|
|||
arel (8.0.0)
|
||||
aspector (0.14.0)
|
||||
ast (2.3.0)
|
||||
auto_strip_attributes (2.1.0)
|
||||
auto_strip_attributes (2.2.0)
|
||||
activerecord (>= 3.0)
|
||||
autoprefixer-rails (7.1.2.4)
|
||||
autoprefixer-rails (7.1.6)
|
||||
execjs
|
||||
autosize-rails (1.18.17)
|
||||
rails (>= 3.1)
|
||||
awesome_print (1.8.0)
|
||||
aws-sdk (2.10.21)
|
||||
aws-sdk-resources (= 2.10.21)
|
||||
aws-sdk-core (2.10.21)
|
||||
aws-sdk (2.10.69)
|
||||
aws-sdk-resources (= 2.10.69)
|
||||
aws-sdk-core (2.10.69)
|
||||
aws-sigv4 (~> 1.0)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-resources (2.10.21)
|
||||
aws-sdk-core (= 2.10.21)
|
||||
aws-sigv4 (1.0.1)
|
||||
aws-sdk-resources (2.10.69)
|
||||
aws-sdk-core (= 2.10.69)
|
||||
aws-sdk-v1 (1.67.0)
|
||||
json (~> 1.4)
|
||||
nokogiri (~> 1)
|
||||
aws-sigv4 (1.0.2)
|
||||
babel-source (5.8.35)
|
||||
babel-transpiler (0.7.0)
|
||||
babel-source (>= 4.0, < 6)
|
||||
execjs (~> 2.0)
|
||||
backports (3.10.3)
|
||||
base62 (1.0.0)
|
||||
bcrypt (3.1.11)
|
||||
better_errors (2.3.0)
|
||||
better_errors (2.4.0)
|
||||
coderay (>= 1.0.0)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
binding_of_caller (0.7.2)
|
||||
binding_of_caller (0.7.3)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootstrap-sass (3.3.7)
|
||||
autoprefixer-rails (>= 5.2.1)
|
||||
|
@ -125,7 +129,7 @@ GEM
|
|||
bullet (5.6.1)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.10.0)
|
||||
byebug (9.0.6)
|
||||
byebug (9.1.0)
|
||||
capybara (2.13.0)
|
||||
addressable
|
||||
mime-types (>= 1.16)
|
||||
|
@ -141,7 +145,7 @@ GEM
|
|||
climate_control (0.2.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
coderay (1.1.1)
|
||||
coderay (1.1.2)
|
||||
coffee-rails (4.2.2)
|
||||
coffee-script (>= 2.2.0)
|
||||
railties (>= 4.0.0)
|
||||
|
@ -152,22 +156,31 @@ GEM
|
|||
commit_param_routing (0.0.1)
|
||||
concurrent-ruby (1.0.5)
|
||||
crass (1.0.2)
|
||||
cucumber (2.4.0)
|
||||
creek (2.0)
|
||||
httparty (~> 0.15.5)
|
||||
nokogiri (~> 1.7.0)
|
||||
rubyzip (>= 1.0.0)
|
||||
cucumber (3.0.1)
|
||||
builder (>= 2.1.2)
|
||||
cucumber-core (~> 1.5.0)
|
||||
cucumber-core (~> 3.0.0)
|
||||
cucumber-expressions (~> 4.0.3)
|
||||
cucumber-wire (~> 0.0.1)
|
||||
diff-lcs (>= 1.1.3)
|
||||
diff-lcs (~> 1.3)
|
||||
gherkin (~> 4.0)
|
||||
multi_json (>= 1.7.5, < 2.0)
|
||||
multi_test (>= 0.1.2)
|
||||
cucumber-core (1.5.0)
|
||||
gherkin (~> 4.0)
|
||||
cucumber-core (3.0.0)
|
||||
backports (>= 3.8.0)
|
||||
cucumber-tag_expressions (>= 1.0.1)
|
||||
gherkin (>= 4.1.3)
|
||||
cucumber-expressions (4.0.4)
|
||||
cucumber-rails (1.5.0)
|
||||
capybara (>= 1.1.2, < 3)
|
||||
cucumber (>= 1.3.8, < 4)
|
||||
mime-types (>= 1.17, < 4)
|
||||
nokogiri (~> 1.5)
|
||||
railties (>= 4, < 5.2)
|
||||
cucumber-tag_expressions (1.0.1)
|
||||
cucumber-wire (0.0.1)
|
||||
database_cleaner (1.6.1)
|
||||
debug_inspector (0.0.3)
|
||||
|
@ -192,9 +205,9 @@ GEM
|
|||
devise (>= 4.0.0)
|
||||
diff-lcs (1.3)
|
||||
docile (1.1.5)
|
||||
erubi (1.6.1)
|
||||
erubi (1.7.0)
|
||||
execjs (2.7.0)
|
||||
factory_girl (4.8.0)
|
||||
factory_girl (4.8.1)
|
||||
activesupport (>= 3.0.0)
|
||||
factory_girl_rails (4.8.0)
|
||||
factory_girl (~> 4.8.0)
|
||||
|
@ -209,9 +222,12 @@ GEM
|
|||
gherkin (4.1.3)
|
||||
globalid (0.4.0)
|
||||
activesupport (>= 4.2.0)
|
||||
hammerjs-rails (2.0.4)
|
||||
hammerjs-rails (2.0.8)
|
||||
headless (2.3.1)
|
||||
i18n (0.8.6)
|
||||
httparty (0.15.6)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (0.9.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.0.1)
|
||||
i18n (~> 0.6, >= 0.6.6)
|
||||
introjs-rails (1.0.0)
|
||||
|
@ -233,18 +249,18 @@ GEM
|
|||
js_cookie_rails (2.1.4)
|
||||
railties (>= 3.1)
|
||||
json (1.8.6)
|
||||
kaminari (1.0.1)
|
||||
kaminari (1.1.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.0.1)
|
||||
kaminari-activerecord (= 1.0.1)
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-actionview (1.0.1)
|
||||
kaminari-actionview (= 1.1.1)
|
||||
kaminari-activerecord (= 1.1.1)
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-actionview (1.1.1)
|
||||
actionview
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-activerecord (1.0.1)
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-activerecord (1.1.1)
|
||||
activerecord
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-core (1.0.1)
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-core (1.1.1)
|
||||
lazy_priority_queue (0.1.1)
|
||||
listen (3.1.5)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
|
@ -254,27 +270,29 @@ GEM
|
|||
logging (2.0.0)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.10)
|
||||
loofah (2.0.3)
|
||||
loofah (2.1.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
method_source (0.8.2)
|
||||
method_source (0.9.0)
|
||||
mime-types (1.25.1)
|
||||
mimemagic (0.3.2)
|
||||
mini_portile2 (2.3.0)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.10.3)
|
||||
momentjs-rails (2.17.1)
|
||||
railties (>= 3.1)
|
||||
multi_json (1.12.1)
|
||||
multi_json (1.12.2)
|
||||
multi_test (0.1.2)
|
||||
nested_form_fields (0.8.1)
|
||||
multi_xml (0.6.0)
|
||||
nested_form_fields (0.8.2)
|
||||
coffee-rails (>= 3.2.1)
|
||||
jquery-rails
|
||||
rails (>= 3.2.0)
|
||||
newrelic_rpm (4.3.0.335)
|
||||
newrelic_rpm (4.5.0.337)
|
||||
nio4r (2.1.0)
|
||||
nokogiri (1.8.1)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
nokogiri (1.7.2)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
nokogumbo (1.4.13)
|
||||
nokogiri
|
||||
oj (2.18.5)
|
||||
|
@ -291,17 +309,16 @@ GEM
|
|||
pg (0.21.0)
|
||||
polyglot (0.3.5)
|
||||
powerpack (0.1.1)
|
||||
pry (0.10.4)
|
||||
pry (0.11.2)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
slop (~> 3.4)
|
||||
pry-byebug (3.4.2)
|
||||
byebug (~> 9.0)
|
||||
method_source (~> 0.9.0)
|
||||
pry-byebug (3.5.0)
|
||||
byebug (~> 9.1)
|
||||
pry (~> 0.10)
|
||||
pry-rails (0.3.6)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (3.0.0)
|
||||
puma (3.9.1)
|
||||
puma (3.10.0)
|
||||
rack (2.0.3)
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
|
@ -341,12 +358,12 @@ GEM
|
|||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.2.2)
|
||||
rake
|
||||
rake (12.0.0)
|
||||
rake (12.1.0)
|
||||
rb-fsevent (0.10.2)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
rdoc (4.3.0)
|
||||
recaptcha (4.3.1)
|
||||
recaptcha (4.6.2)
|
||||
json
|
||||
remotipart (1.3.1)
|
||||
responders (2.4.0)
|
||||
|
@ -359,32 +376,32 @@ GEM
|
|||
roo (2.7.1)
|
||||
nokogiri (~> 1)
|
||||
rubyzip (~> 1.1, < 2.0.0)
|
||||
rspec-core (3.6.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-expectations (3.6.0)
|
||||
rspec-core (3.7.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-expectations (3.7.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-mocks (3.6.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-mocks (3.7.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-rails (3.6.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-rails (3.7.1)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
rspec-core (~> 3.6.0)
|
||||
rspec-expectations (~> 3.6.0)
|
||||
rspec-mocks (~> 3.6.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-support (3.6.0)
|
||||
rubocop (0.49.1)
|
||||
rspec-core (~> 3.7.0)
|
||||
rspec-expectations (~> 3.7.0)
|
||||
rspec-mocks (~> 3.7.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-support (3.7.0)
|
||||
rubocop (0.51.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.3.3.1, < 3.0)
|
||||
powerpack (~> 0.1)
|
||||
rainbow (>= 1.99.1, < 3.0)
|
||||
rainbow (>= 2.2.2, < 3.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||
ruby-graphviz (1.2.3)
|
||||
ruby-progressbar (1.8.1)
|
||||
ruby-progressbar (1.9.0)
|
||||
ruby_dep (1.5.0)
|
||||
rubyzip (1.2.1)
|
||||
sanitize (4.5.0)
|
||||
|
@ -398,7 +415,7 @@ GEM
|
|||
sprockets (>= 2.8, < 4.0)
|
||||
sprockets-rails (>= 2.0, < 4.0)
|
||||
tilt (>= 1.1, < 3)
|
||||
scss_lint (0.54.0)
|
||||
scss_lint (0.55.0)
|
||||
rake (>= 0.9, < 13)
|
||||
sass (~> 3.4.20)
|
||||
sdoc (0.4.2)
|
||||
|
@ -414,19 +431,18 @@ GEM
|
|||
actionmailer (>= 3.2.6, < 6)
|
||||
actionpack (>= 3.2.6, < 6)
|
||||
devise (>= 3.2, < 6)
|
||||
simplecov (0.14.1)
|
||||
simplecov (0.15.1)
|
||||
docile (~> 1.1.0)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.1)
|
||||
slop (3.6.0)
|
||||
simplecov-html (0.10.2)
|
||||
sourcemap (0.1.1)
|
||||
spinjs-rails (1.4)
|
||||
rails (>= 3.1)
|
||||
sprockets (3.7.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.0)
|
||||
sprockets-rails (3.2.1)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
|
@ -438,10 +454,10 @@ GEM
|
|||
ruby-progressbar (~> 1.8)
|
||||
sourcemap (~> 0.1)
|
||||
stream (0.5)
|
||||
thor (0.19.4)
|
||||
thor (0.20.0)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.8)
|
||||
tinymce-rails (4.6.5)
|
||||
tinymce-rails (4.6.7)
|
||||
railties (>= 3.1.1)
|
||||
turbolinks (5.0.1)
|
||||
turbolinks-source (~> 5)
|
||||
|
@ -481,6 +497,7 @@ DEPENDENCIES
|
|||
autosize-rails
|
||||
awesome_print
|
||||
aws-sdk (~> 2)
|
||||
aws-sdk-v1
|
||||
base62
|
||||
bcrypt (~> 3.1.10)
|
||||
better_errors
|
||||
|
@ -494,6 +511,7 @@ DEPENDENCIES
|
|||
capybara
|
||||
capybara-webkit (~> 1.14)
|
||||
commit_param_routing
|
||||
creek
|
||||
cucumber-rails (~> 1.5)
|
||||
database_cleaner
|
||||
deface (~> 1.0)
|
||||
|
|
|
@ -166,8 +166,9 @@
|
|||
var $nameInput = $form.find('#result_name');
|
||||
var nameValid = textValidator(ev, $nameInput, 0,
|
||||
<%= Constants::NAME_MAX_LENGTH %>);
|
||||
var $textInput = TinyMCE.getContent();
|
||||
textValidator(ev, $textInput, 1, <%= Constants::TEXT_MAX_LENGTH %>, false, true);
|
||||
var $descrTextarea = $form.find("#result_result_text_attributes_text");
|
||||
var $tinyMCEInput = TinyMCE.getContent();
|
||||
textValidator(ev, $descrTextarea, 1, <%= Constants::TEXT_MAX_LENGTH %>, false, $tinyMCEInput);
|
||||
break;
|
||||
case ResultTypeEnum.COMMENT:
|
||||
var $commentInput = $form.find('#comment_message');
|
||||
|
|
|
@ -163,7 +163,6 @@ function initializeEdit() {
|
|||
|
||||
// Read permissions from the data attributes of the form
|
||||
var canEditModules = _.isEqual($("#update-canvas").data("can-edit-modules"), "yes");
|
||||
var canEditModuleGroups = _.isEqual($("#update-canvas").data("can-edit-module-groups"), "yes");
|
||||
var canCreateModules = _.isEqual($("#update-canvas").data("can-create-modules"), "yes");
|
||||
var canCloneModules = _.isEqual($("#update-canvas").data("can-clone-modules"), "yes");
|
||||
var canMoveModules = _.isEqual($("#update-canvas").data("can-move-modules"), "yes");
|
||||
|
@ -211,11 +210,6 @@ function initializeEdit() {
|
|||
$(".edit-module").on("click touchstart", editModuleHandler);
|
||||
}
|
||||
|
||||
if (canEditModuleGroups) {
|
||||
initEditModuleGroups();
|
||||
$(".edit-module-group").on("click touchstart", editModuleGroupHandler);
|
||||
}
|
||||
|
||||
if (canCloneModules) {
|
||||
bindCloneModuleAction(
|
||||
$(".module-options a.clone-module"),
|
||||
|
@ -1220,22 +1214,6 @@ function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
|
|||
// Add click handler for the edit module
|
||||
$(editModuleLink).on("click touchstart", editModuleHandler);
|
||||
}
|
||||
if (_.isEqual($("#update-canvas").data("can-edit-module-groups"), "yes")) {
|
||||
var editModuleGroupItem = document.createElement("li");
|
||||
$(editModuleGroupItem).appendTo(dropdownMenu);
|
||||
$(editModuleGroupItem).hide();
|
||||
|
||||
var editModuleGroupLink = document.createElement("a");
|
||||
$(editModuleGroupLink)
|
||||
.attr("href", "")
|
||||
.attr("data-module-id", id)
|
||||
.addClass("edit-module-group")
|
||||
.html($("#edit-group-link-placeholder").text().trim())
|
||||
.appendTo(editModuleGroupItem);
|
||||
|
||||
// Add click handler for the edit module group
|
||||
$(editModuleGroupLink).on("click touchstart", editModuleGroupHandler);
|
||||
}
|
||||
|
||||
// Add clone links if neccesary
|
||||
if (_.isEqual($("#update-canvas").data("can-clone-modules"), "yes")) {
|
||||
|
@ -1636,95 +1614,11 @@ editModuleHandler = function(ev) {
|
|||
/**
|
||||
* Initialize editing of module groups.
|
||||
*/
|
||||
function initEditModuleGroups() {
|
||||
|
||||
function handleRenameConfirm(modal, ev) {
|
||||
var input = modal.find("#edit-module-group-name-input");
|
||||
// Validate module group name
|
||||
var moduleNameValid = textValidator(ev, input, 1,
|
||||
<%= Constants::NAME_MAX_LENGTH %>, true);
|
||||
if (moduleNameValid) {
|
||||
var newModuleGroupName = input.val();
|
||||
var moduleId = modal.attr("data-module-id");
|
||||
var moduleEl = $("#" + moduleId);
|
||||
// Update the module group name for all modules
|
||||
// currently in the module group
|
||||
var ids = connectedComponents(graph, moduleEl.attr("data-module-id"));
|
||||
_.each(ids, function(id) {
|
||||
$("#" + id).attr("data-module-group-name", newModuleGroupName);
|
||||
});
|
||||
|
||||
// Hide modal
|
||||
modal.modal("hide");
|
||||
}
|
||||
}
|
||||
|
||||
$("#modal-edit-module-group")
|
||||
.on("show.bs.modal", function (event) {
|
||||
var modal = $(this);
|
||||
var moduleId = modal.attr("data-module-id");
|
||||
var moduleEl = $("#" + moduleId);
|
||||
var input = modal.find("#edit-module-group-name-input");
|
||||
|
||||
// Set the input to the current module's name
|
||||
input
|
||||
.attr("value", moduleEl.attr("data-module-group-name"));
|
||||
input.val(moduleEl.attr("data-module-group-name"));
|
||||
|
||||
// Bind on enter button
|
||||
input.keydown(function(ev) {
|
||||
if (ev.keyCode == 13) {
|
||||
// "Submit" modal
|
||||
handleRenameConfirm(modal, ev);
|
||||
|
||||
// In any case, prevent form submission
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})
|
||||
.on("shown.bs.modal", function (event) {
|
||||
$(this).find("#edit-module-group-name-input").focus();
|
||||
})
|
||||
.on("hide.bs.modal", function (event) {
|
||||
// Remove potential error classes
|
||||
$(this).find("#edit-module-group-name-input").parent().removeClass("has-error");
|
||||
$(this).find("span.help-block").remove();
|
||||
|
||||
$(this).find("#edit-module-group-name-input").off("keydown");
|
||||
|
||||
// When hiding modal, re-enable events
|
||||
toggleCanvasEvents(true);
|
||||
});
|
||||
|
||||
// Bind the confirm button on modal
|
||||
$("#modal-edit-module-group").find("button[data-action='confirm']").on("click", function(ev) {
|
||||
var modal = $(this).closest(".modal");
|
||||
handleRenameConfirm(modal, ev);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler when editing a module group.
|
||||
*/
|
||||
editModuleGroupHandler = function(ev) {
|
||||
var modal = $("#modal-edit-module-group");
|
||||
var moduleEl = $(this).closest(".module");
|
||||
|
||||
// Set modal's module id
|
||||
modal.attr("data-module-id", moduleEl.attr("data-module-id"));
|
||||
|
||||
// Disable dragging & zooming events on canvas temporarily
|
||||
toggleCanvasEvents(false);
|
||||
|
||||
// Show modal
|
||||
modal.modal("show");
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
function initMoveModules() {
|
||||
function handleMoveConfirm(modal) {
|
||||
|
@ -1978,7 +1872,6 @@ function deleteModule(id, linkConnections) {
|
|||
_.each (ins, function(inEdge) {
|
||||
if (graph.degree(inEdge[0]) === 0) {
|
||||
tempModuleEl = $("#" + inEdge[0]);
|
||||
tempModuleEl.find(".edit-module-group").parents("li").hide();
|
||||
tempModuleEl.find(".clone-module-group").parents("li").hide();
|
||||
tempModuleEl.find(".move-module-group").parents("li").hide();
|
||||
tempModuleEl.find(".delete-module-group").parents("li").hide();
|
||||
|
@ -1989,7 +1882,6 @@ function deleteModule(id, linkConnections) {
|
|||
_.each (outs, function(outEdge) {
|
||||
if (graph.degree(outEdge[1]) === 0) {
|
||||
tempModuleEl = $("#" + outEdge[1]);
|
||||
tempModuleEl.find(".edit-module-group").parents("li").hide();
|
||||
tempModuleEl.find(".clone-module-group").parents("li").hide();
|
||||
tempModuleEl.find(".move-module-group").parents("li").hide();
|
||||
tempModuleEl.find(".delete-module-group").parents("li").hide();
|
||||
|
@ -2361,7 +2253,6 @@ cloneModuleGroupHandler = function(moduleId, modulesSel, gridDistX, gridDistY) {
|
|||
var nm = cloneModule(m, gridDistX, gridDistY, elLeft(m), elTop(m) + height + offset - gridDistY);
|
||||
|
||||
//Show module group options
|
||||
nm.find(".edit-module-group").parents("li").show();
|
||||
nm.find(".clone-module-group").parents("li").show();
|
||||
nm.find(".move-module-group").parents("li").show();
|
||||
nm.find(".delete-module-group").parents("li").show();
|
||||
|
@ -2963,12 +2854,10 @@ function initJsPlumb(containerSel, containerChildSel, modulesSel, params) {
|
|||
//Modules should belong to module group now
|
||||
//Show module group options for target and source
|
||||
|
||||
srcModuleEl.find(".edit-module-group").parents("li").show();
|
||||
srcModuleEl.find(".clone-module-group").parents("li").show();
|
||||
srcModuleEl.find(".move-module-group").parents("li").show();
|
||||
srcModuleEl.find(".delete-module-group").parents("li").show();
|
||||
|
||||
targetModuleEl.find(".edit-module-group").parents("li").show();
|
||||
targetModuleEl.find(".clone-module-group").parents("li").show();
|
||||
targetModuleEl.find(".move-module-group").parents("li").show();
|
||||
targetModuleEl.find(".delete-module-group").parents("li").show();
|
||||
|
@ -3001,13 +2890,11 @@ function initJsPlumb(containerSel, containerChildSel, modulesSel, params) {
|
|||
var targetModuleEl = $("#" + c.targetId);
|
||||
//First source
|
||||
if (graph.degree(c.sourceId) === 0) {
|
||||
srcModuleEl.find(".edit-module-group").parents("li").hide();
|
||||
srcModuleEl.find(".clone-module-group").parents("li").hide();
|
||||
srcModuleEl.find(".move-module-group").parents("li").hide();
|
||||
srcModuleEl.find(".delete-module-group").parents("li").hide();
|
||||
}
|
||||
if (graph.degree(c.targetId) === 0) {
|
||||
targetModuleEl.find(".edit-module-group").parents("li").hide();
|
||||
targetModuleEl.find(".clone-module-group").parents("li").hide();
|
||||
targetModuleEl.find(".move-module-group").parents("li").hide();
|
||||
targetModuleEl.find(".delete-module-group").parents("li").hide();
|
||||
|
|
|
@ -540,8 +540,9 @@
|
|||
var nameValid = textValidator(ev, $nameInput, 1,
|
||||
<%= Constants::NAME_MAX_LENGTH %>);
|
||||
var $descrTextarea = $form.find("#step_description");
|
||||
var $tinyMCEInput = TinyMCE.getContent();
|
||||
var descriptionValid = textValidator(ev, $descrTextarea, 0,
|
||||
<%= Constants::TEXT_MAX_LENGTH %>);
|
||||
<%= Constants::TEXT_MAX_LENGTH %>, false, $tinyMCEInput);
|
||||
|
||||
if (DragNDropSteps.filesStatus() &&
|
||||
checklistsValid &&
|
||||
|
|
|
@ -21,12 +21,13 @@
|
|||
|
||||
// loops through a list of files and display each file in a separate panel
|
||||
function listItems() {
|
||||
droppedFiles = droppedFiles.filter(Boolean);
|
||||
totalSize = 0;
|
||||
_enableSubmitButton();
|
||||
$('.panel-step-attachment-new').remove();
|
||||
_dragNdropAssetsOff();
|
||||
for(var i = 0; i < droppedFiles.length; i++) {
|
||||
$('#new-step-assets')
|
||||
.append(_uploadedAseetPreview(droppedFiles[i], i))
|
||||
.append(_uploadedAssetPreview(droppedFiles[i], i))
|
||||
.promise()
|
||||
.done(function() {
|
||||
_removeItemHandler(i);
|
||||
|
@ -42,7 +43,6 @@
|
|||
var prevEls = $('input').filter(function() {
|
||||
return this.name.match(regex);
|
||||
});
|
||||
droppedFiles = droppedFiles.filter(Boolean);
|
||||
var fd = new FormData($(ev.target).closest('form').get(0));
|
||||
for(var i = 0; i < droppedFiles.length; i++) {
|
||||
var index = i + prevEls.length;
|
||||
|
@ -56,30 +56,55 @@
|
|||
return fd;
|
||||
}
|
||||
|
||||
function _validateFilesSize(file) {
|
||||
var maxSize = file.size/1048576;
|
||||
if(maxSize > <%= Constants::FILE_MAX_SIZE_MB %> && filesValid) {
|
||||
return "<p><%= I18n.t 'general.file.size_exceeded', file_size: Constants::FILE_MAX_SIZE_MB %></p>";
|
||||
function _disableSubmitButton() {
|
||||
$('.step-save').prop('disabled', true);
|
||||
}
|
||||
|
||||
function _enableSubmitButton() {
|
||||
$('.step-save').prop('disabled', false);
|
||||
}
|
||||
|
||||
function _filerAndCheckFiles() {
|
||||
for(var i = 0; i < droppedFiles.length; i++) {
|
||||
if(droppedFiles[i].isValid == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return (droppedFiles.length > 0);
|
||||
}
|
||||
|
||||
function _validateFilesSize(file) {
|
||||
var fileSize = file.size;
|
||||
totalSize += parseInt(fileSize);
|
||||
if(fileSize > <%= Constants::FILE_MAX_SIZE_MB.megabyte %>) {
|
||||
file.isValid = false;
|
||||
_disableSubmitButton();
|
||||
return "<p class='dnd-error'><%= I18n.t 'general.file.size_exceeded', file_size: Constants::FILE_MAX_SIZE_MB %></p>";
|
||||
}
|
||||
totalSize += parseInt(maxSize);
|
||||
return '';
|
||||
}
|
||||
|
||||
function _validateTotalSize() {
|
||||
if(totalSize > <%= Constants::FILE_MAX_SIZE_MB %>) {
|
||||
if(totalSize > <%= Constants::FILE_MAX_SIZE_MB.megabyte %>) {
|
||||
filesValid = false;
|
||||
_disableSubmitButton();
|
||||
$.each($('.panel-step-attachment-new'), function() {
|
||||
$(this)
|
||||
.find('.panel-body')
|
||||
.append("<p class='dnd-error'><%= I18n.t('general.file.total_size', size: Constants::FILE_MAX_SIZE_MB) %></p>");
|
||||
if(!$(this).find('p').hasClass('dnd-total-error')) {
|
||||
$(this)
|
||||
.find('.panel-body')
|
||||
.append("<p class='dnd-total-error'><%= I18n.t('general.file.total_size', size: Constants::FILE_MAX_SIZE_MB) %></p>");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$('.dnd-error').remove();
|
||||
filesValid = true;
|
||||
$('.dnd-total-error').remove();
|
||||
if(_filerAndCheckFiles()) {
|
||||
filesValid = true;
|
||||
_enableSubmitButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _uploadedAseetPreview(asset, i) {
|
||||
function _uploadedAssetPreview(asset, i) {
|
||||
var html = '<div class="panel panel-default panel-step-attachment-new">';
|
||||
html += '<div class="panel-heading">';
|
||||
html += '<span class="glyphicon glyphicon-file"></span>';
|
||||
|
@ -104,10 +129,9 @@
|
|||
e.stopPropagation();
|
||||
var $el = $(this);
|
||||
var index = $el.data('item-id');
|
||||
totalSize -= parseInt(droppedFiles[index]/1048576);
|
||||
droppedFiles[index] = null;
|
||||
$el.closest('.panel-step-attachment-new').remove();
|
||||
_validateTotalSize();
|
||||
totalSize -= parseInt(droppedFiles[index].size);
|
||||
droppedFiles.splice(index, 1);
|
||||
listItems();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -131,11 +155,10 @@
|
|||
var totalSize = 0;
|
||||
|
||||
function init(files) {
|
||||
var filesPresent = droppedFiles.length;
|
||||
for(var i = 0; i < files.length; i++) {
|
||||
droppedFiles.push(files[i]);
|
||||
}
|
||||
listItems(filesPresent);
|
||||
listItems();
|
||||
}
|
||||
|
||||
// return the status of files if they are ready to submit
|
||||
|
@ -144,18 +167,24 @@
|
|||
}
|
||||
|
||||
// loops through a list of files and display each file in a separate panel
|
||||
function listItems(index) {
|
||||
_dragNdropAssetsOff();
|
||||
for(var i = index; i < droppedFiles.length; i++) {
|
||||
$('#new-result-assets-select')
|
||||
.after(_uploadedAseetPreview(droppedFiles[i], i))
|
||||
.promise()
|
||||
.done(function() {
|
||||
_removeItemHandler(i);
|
||||
});
|
||||
function listItems() {
|
||||
totalSize = 0;
|
||||
$('.panel-result-attachment-new').remove();
|
||||
if(droppedFiles.length < 1) {
|
||||
_disableSubmitButton();
|
||||
} else {
|
||||
_dragNdropAssetsOff();
|
||||
for(var i = 0; i < droppedFiles.length; i++) {
|
||||
$('#new-result-assets-select')
|
||||
.after(_uploadedAssetPreview(droppedFiles[i], i))
|
||||
.promise()
|
||||
.done(function() {
|
||||
_removeItemHandler(i);
|
||||
});
|
||||
}
|
||||
_validateTotalSize();
|
||||
dragNdropAssetsInit('results');
|
||||
}
|
||||
_validateTotalSize();
|
||||
dragNdropAssetsInit('results');
|
||||
}
|
||||
|
||||
// appent the files to the form before submit
|
||||
|
@ -181,31 +210,51 @@
|
|||
return fd;
|
||||
}
|
||||
|
||||
function _disableSubmitButton() {
|
||||
$('.save-result').prop('disabled', true);
|
||||
}
|
||||
|
||||
function _enableSubmitButton() {
|
||||
$('.save-result').prop('disabled', false);
|
||||
}
|
||||
|
||||
function _filerAndCheckFiles() {
|
||||
droppedFiles = droppedFiles.filter(Boolean);
|
||||
for(var i = 0; i < droppedFiles.length; i++) {
|
||||
if(droppedFiles[i].isValid == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return (droppedFiles.length > 0);
|
||||
}
|
||||
|
||||
function _validateFilesSize(file) {
|
||||
var maxSize = file.size/1048576;
|
||||
if(maxSize > <%= Constants::FILE_MAX_SIZE_MB %> && isValid) {
|
||||
return "<p><%= I18n.t 'general.file.size_exceeded', file_size: Constants::FILE_MAX_SIZE_MB %></p>";
|
||||
var fileSize = file.size;
|
||||
totalSize += parseInt(fileSize);
|
||||
if(fileSize > <%= Constants::FILE_MAX_SIZE_MB.megabyte %>) {
|
||||
file.isValid = false;
|
||||
_disableSubmitButton();
|
||||
return "<p class='dnd-error'><%= I18n.t 'general.file.size_exceeded', file_size: Constants::FILE_MAX_SIZE_MB %></p>";
|
||||
}
|
||||
totalSize += parseInt(maxSize);
|
||||
return '';
|
||||
}
|
||||
|
||||
function _validateTotalSize() {
|
||||
if(totalSize > <%= Constants::FILE_MAX_SIZE_MB %>) {
|
||||
if(totalSize > <%= Constants::FILE_MAX_SIZE_MB.megabyte %>) {
|
||||
isValid = false;
|
||||
_disableSubmitButton();
|
||||
$.each($('.panel-result-attachment-new'), function() {
|
||||
$(this)
|
||||
.find('.panel-body')
|
||||
.append("<p class='dnd-error'><%= I18n.t('general.file.total_size', size: Constants::FILE_MAX_SIZE_MB) %></p>");
|
||||
if(!$(this).find('p').hasClass('dnd-total-error')) {
|
||||
$(this)
|
||||
.find('.panel-body')
|
||||
.append("<p class='dnd-total-error'><%= I18n.t('general.file.total_size', size: Constants::FILE_MAX_SIZE_MB) %></p>");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$('.dnd-error').remove();
|
||||
isValid = true;
|
||||
$('.dnd-total-error').remove();
|
||||
if(_filerAndCheckFiles()) {
|
||||
isValid = true;
|
||||
_enableSubmitButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,7 +269,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
function _uploadedAseetPreview(asset, i) {
|
||||
function _uploadedAssetPreview(asset, i) {
|
||||
var html = '<div class="panel panel-default panel-result-attachment-new">';
|
||||
html += '<div class="panel-heading">';
|
||||
html += '<span class="glyphicon glyphicon-file"></span>';
|
||||
|
@ -249,10 +298,9 @@
|
|||
e.stopPropagation();
|
||||
var $el = $(this);
|
||||
var index = $el.data('item-id');
|
||||
totalSize -= parseInt(droppedFiles[index]/1048576);
|
||||
droppedFiles[index] = null;
|
||||
$el.closest('.panel-result-attachment-new').remove();
|
||||
_validateTotalSize();
|
||||
totalSize -= parseInt(droppedFiles[index].size);
|
||||
droppedFiles.splice(index, 1);
|
||||
listItems();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -22,11 +22,11 @@ $.fn.onSubmitValidator = function(validatorCb) {
|
|||
* @param {boolean} clearErr Set clearErr to true if this is the only
|
||||
* error that can happen/show.
|
||||
*/
|
||||
function textValidator(ev, textInput, textLimitMin, textLimitMax, clearErr, tinyMCE) {
|
||||
function textValidator(ev, textInput, textLimitMin, textLimitMax, clearErr, tinyMCEInput) {
|
||||
clearErr = _.isUndefined(clearErr) ? false : clearErr;
|
||||
|
||||
if(tinyMCE){
|
||||
var text = textInput.length;
|
||||
if(tinyMCEInput){
|
||||
var text = tinyMCEInput;
|
||||
} else {
|
||||
var text = $(textInput).val().trim();
|
||||
$(textInput).val(text);
|
||||
|
|
|
@ -1288,7 +1288,8 @@ ul.content-module-activities {
|
|||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.dnd-error {
|
||||
.dnd-error,
|
||||
.dnd-total-error {
|
||||
color: $color-milano-red;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,58 +29,41 @@ class CanvasController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
error = false
|
||||
|
||||
# Make sure that remove parameter is valid
|
||||
to_archive = []
|
||||
if can_archive_modules(@experiment) and
|
||||
update_params[:remove].present? then
|
||||
to_archive = update_params[:remove].split(",")
|
||||
unless to_archive.all? { |id| is_int? id }
|
||||
error = true
|
||||
if can_archive_modules(@experiment) && update_params[:remove].present?
|
||||
to_archive = update_params[:remove].split(',')
|
||||
if to_archive.all? { |id| is_int? id }
|
||||
to_archive.collect!(&:to_i)
|
||||
else
|
||||
to_archive.collect! { |id| id.to_i }
|
||||
return render_403
|
||||
end
|
||||
end
|
||||
|
||||
if error then
|
||||
render_403 and return
|
||||
end
|
||||
|
||||
# Make sure connections parameter is valid
|
||||
connections = []
|
||||
if can_edit_connections(@experiment) and
|
||||
update_params[:connections].present? then
|
||||
conns = update_params[:connections].split(",")
|
||||
unless conns.length % 2 == 0 and
|
||||
conns.all? { |c| c.is_a? String } then
|
||||
error = true
|
||||
else
|
||||
if can_edit_connections(@experiment) && update_params[:connections].present?
|
||||
conns = update_params[:connections].split(',')
|
||||
if conns.length.even? && conns.all? { |c| c.is_a? String }
|
||||
conns.each_slice(2).each do |c|
|
||||
connections << [c[0], c[1]]
|
||||
end
|
||||
else
|
||||
return render_403
|
||||
end
|
||||
end
|
||||
|
||||
if error then
|
||||
render_403 and return
|
||||
end
|
||||
|
||||
# Make sure positions parameter is valid
|
||||
positions = Hash.new
|
||||
if can_reposition_modules(@experiment) and
|
||||
update_params[:positions].present? then
|
||||
poss = update_params[:positions].split(";")
|
||||
center = ""
|
||||
(poss.collect { |pos| pos.split(",") }).each_with_index do |pos, index|
|
||||
unless pos.length == 3 &&
|
||||
pos[0].is_a?(String) &&
|
||||
float?(pos[1]) &&
|
||||
float?(pos[2])
|
||||
error = true
|
||||
break
|
||||
positions = {}
|
||||
if can_reposition_modules(@experiment) && update_params[:positions].present?
|
||||
poss = update_params[:positions].split(';')
|
||||
center = ''
|
||||
(poss.collect { |pos| pos.split(',') }).each_with_index do |pos, index|
|
||||
unless pos.length == 3 && pos[0].is_a?(String) &&
|
||||
float?(pos[1]) && float?(pos[2])
|
||||
return render_403
|
||||
end
|
||||
if index == 0
|
||||
if index.zero?
|
||||
center = pos
|
||||
x = 0
|
||||
y = 0
|
||||
|
@ -89,86 +72,59 @@ class CanvasController < ApplicationController
|
|||
y = pos[2].to_i - center[2].to_i
|
||||
end
|
||||
# Multiple modules cannot have same position
|
||||
if positions.any? { |k,v| v[:x] == x and v[:y] == y} then
|
||||
error = true
|
||||
break
|
||||
end
|
||||
return render_403 if positions.any? { |_, v| v[:x] == x && v[:y] == y }
|
||||
positions[pos[0]] = { x: x, y: y }
|
||||
end
|
||||
end
|
||||
|
||||
if error then
|
||||
render_403 and return
|
||||
end
|
||||
|
||||
# Make sure that to_add is an array of strings,
|
||||
# as well as that positions for newly added modules exist
|
||||
to_add = []
|
||||
if can_create_modules(@experiment) and
|
||||
update_params[:add].present? and
|
||||
update_params["add-names"].present? then
|
||||
ids = update_params[:add].split(",")
|
||||
names = update_params["add-names"].split("|")
|
||||
unless ids.length == names.length and
|
||||
ids.all? { |id| id.is_a? String and positions.include? id } and
|
||||
names.all? { |name| name.is_a? String }
|
||||
error = true
|
||||
else
|
||||
if can_create_modules(@experiment) && update_params[:add].present? &&
|
||||
update_params['add-names'].present?
|
||||
ids = update_params[:add].split(',')
|
||||
names = update_params['add-names'].split('|')
|
||||
if ids.length == names.length &&
|
||||
ids.all? { |id| id.is_a?(String) && positions.include?(id) } &&
|
||||
names.all? { |name| name.is_a? String }
|
||||
ids.each_with_index do |id, i|
|
||||
to_add << {
|
||||
id: id,
|
||||
name: names[i],
|
||||
x: positions[id][:x],
|
||||
y: positions[id][:y]
|
||||
}
|
||||
to_add << { id: id, name: names[i],
|
||||
x: positions[id][:x], y: positions[id][:y] }
|
||||
end
|
||||
else
|
||||
return render_403
|
||||
end
|
||||
end
|
||||
|
||||
if error then
|
||||
render_403 and return
|
||||
end
|
||||
|
||||
# Make sure rename parameter is valid
|
||||
to_rename = Hash.new
|
||||
if can_edit_modules(@experiment) and
|
||||
update_params[:rename].present? then
|
||||
to_rename = {}
|
||||
if can_edit_modules(@experiment) && update_params[:rename].present?
|
||||
begin
|
||||
to_rename = JSON.parse(update_params[:rename])
|
||||
|
||||
# Okay, JSON parsed!
|
||||
unless (
|
||||
to_rename.is_a? Hash and
|
||||
to_rename.keys.all? { |k| k.is_a? String } and
|
||||
to_rename.values.all? { |k| k.is_a? String }
|
||||
)
|
||||
error = true
|
||||
unless to_rename.is_a?(Hash) &&
|
||||
to_rename.keys.all? { |k| k.is_a? String } &&
|
||||
to_rename.values.all? { |k| k.is_a? String }
|
||||
return render_403
|
||||
end
|
||||
rescue
|
||||
error = true
|
||||
return render_403
|
||||
end
|
||||
end
|
||||
|
||||
if error then
|
||||
render_403 and return
|
||||
end
|
||||
|
||||
# Make sure move parameter is valid
|
||||
to_move = {}
|
||||
if can_move_modules(@experiment) && update_params[:move].present?
|
||||
begin
|
||||
to_move = JSON.parse(update_params[:move])
|
||||
|
||||
# Okay, JSON parsed!
|
||||
unless (
|
||||
to_move.is_a? Hash and
|
||||
to_move.keys.all? { |k| k.is_a? String } &&
|
||||
to_move.values.all? { |k| k.is_a? String }
|
||||
)
|
||||
error = true
|
||||
unless to_move.is_a?(Hash) &&
|
||||
to_move.keys.all? { |k| k.is_a? String } &&
|
||||
to_move.values.all? { |k| k.is_a? String }
|
||||
return render_403
|
||||
end
|
||||
rescue
|
||||
error = true
|
||||
return render_403
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -181,54 +137,21 @@ class CanvasController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
render_403 and return if error
|
||||
|
||||
# Make sure that to_clone is an array of pairs,
|
||||
# as well as that all IDs exist
|
||||
to_clone = Hash.new
|
||||
if can_clone_modules(@experiment) and
|
||||
update_params[:cloned].present? then
|
||||
clones = update_params[:cloned].split(";")
|
||||
(clones.collect { |v| v.split(",") }).each do |val|
|
||||
unless (val.length == 2 and
|
||||
is_int? val[0] and
|
||||
val[1].is_a? String and
|
||||
to_add.any? { |m| m[:id] == val[1] })
|
||||
error = true
|
||||
break
|
||||
else
|
||||
to_clone = {}
|
||||
if can_clone_modules(@experiment) && update_params[:cloned].present?
|
||||
clones = update_params[:cloned].split(';')
|
||||
(clones.collect { |v| v.split(',') }).each do |val|
|
||||
if val.length == 2 && is_int?(val[0]) && val[1].is_a?(String) &&
|
||||
to_add.any? { |m| m[:id] == val[1] }
|
||||
to_clone[val[1]] = val[0]
|
||||
else
|
||||
return render_403
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if error then
|
||||
render_403 and return
|
||||
end
|
||||
|
||||
module_groups = Hash.new
|
||||
if can_edit_module_groups(@experiment) and
|
||||
update_params["module-groups"].present? then
|
||||
begin
|
||||
module_groups = JSON.parse(update_params["module-groups"])
|
||||
|
||||
# Okay, JSON parsed!
|
||||
unless (
|
||||
module_groups.is_a? Hash and
|
||||
module_groups.keys.all? { |k| k.is_a? String } and
|
||||
module_groups.values.all? { |k| k.is_a? String }
|
||||
)
|
||||
error = true
|
||||
end
|
||||
rescue
|
||||
error = true
|
||||
end
|
||||
end
|
||||
|
||||
if error then
|
||||
render_403 and return
|
||||
end
|
||||
|
||||
# Call the "master" function to do all the updating for us
|
||||
unless @experiment.update_canvas(
|
||||
to_archive,
|
||||
|
@ -239,36 +162,29 @@ class CanvasController < ApplicationController
|
|||
to_clone,
|
||||
connections,
|
||||
positions,
|
||||
current_user,
|
||||
module_groups
|
||||
current_user
|
||||
)
|
||||
render_403 and return
|
||||
return render_403
|
||||
end
|
||||
|
||||
#Save activities that modules were archived
|
||||
# Save activities that modules were archived
|
||||
to_archive.each do |module_id|
|
||||
my_module = MyModule.find_by_id(module_id)
|
||||
unless my_module.blank?
|
||||
Activity.create(
|
||||
type_of: :archive_module,
|
||||
project: my_module.experiment.project,
|
||||
experiment: my_module.experiment,
|
||||
my_module: my_module,
|
||||
user: current_user,
|
||||
message: t(
|
||||
'activities.archive_module',
|
||||
user: current_user.full_name,
|
||||
module: my_module.name
|
||||
)
|
||||
)
|
||||
end
|
||||
next if my_module.blank?
|
||||
Activity.create(type_of: :archive_module,
|
||||
project: my_module.experiment.project,
|
||||
experiment: my_module.experiment,
|
||||
my_module: my_module,
|
||||
user: current_user,
|
||||
message: t('activities.archive_module',
|
||||
user: current_user.full_name,
|
||||
module: my_module.name))
|
||||
end
|
||||
|
||||
# Create workflow image
|
||||
@experiment.delay.generate_workflow_img
|
||||
|
||||
flash[:success] = t(
|
||||
"experiments.canvas.update.success_flash")
|
||||
flash[:success] = t('experiments.canvas.update.success_flash')
|
||||
redirect_to canvas_experiment_path(@experiment)
|
||||
end
|
||||
|
||||
|
|
|
@ -57,19 +57,21 @@ module ClientApi
|
|||
end
|
||||
|
||||
def update
|
||||
user_service = ClientApi::UserService.new(
|
||||
service = ClientApi::Users::UpdateService.new(
|
||||
current_user: current_user,
|
||||
params: user_params
|
||||
)
|
||||
if user_service.update_user!
|
||||
result = service.execute
|
||||
|
||||
if result[:status] == :success
|
||||
bypass_sign_in(current_user)
|
||||
success_response
|
||||
else
|
||||
unsuccess_response(current_user.errors.full_messages,
|
||||
:unprocessable_entity)
|
||||
error_response(
|
||||
message: result[:message],
|
||||
details: service.user.errors
|
||||
)
|
||||
end
|
||||
rescue CustomUserError => error
|
||||
unsuccess_response(error.to_s)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -84,22 +86,35 @@ module ClientApi
|
|||
:system_message_email_notification)
|
||||
end
|
||||
|
||||
def success_response(template = nil, locals = nil)
|
||||
def success_response(args = {})
|
||||
template = args.fetch(:template) { nil }
|
||||
locals = args.fetch(:locals) { {} }
|
||||
details = args.fetch(:details) { {} }
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
if template && locals
|
||||
render template: template, status: :ok, locals: locals
|
||||
if template
|
||||
render template: template,
|
||||
status: :ok,
|
||||
locals: locals
|
||||
else
|
||||
render json: {}, status: :ok
|
||||
render json: { details: details }, status: :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unsuccess_response(message, status = :unprocessable_entity)
|
||||
def error_response(args = {})
|
||||
message = args.fetch(:message) { t('client_api.generic_error_message') }
|
||||
details = args.fetch(:details) { {} }
|
||||
status = args.fetch(:status) { :unprocessable_entity }
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: { message: message },
|
||||
render json: {
|
||||
message: message,
|
||||
details: details
|
||||
},
|
||||
status: status
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,6 +77,8 @@ class ExperimentsController < ApplicationController
|
|||
|
||||
def canvas
|
||||
@project = @experiment.project
|
||||
@active_modules = @experiment.active_modules
|
||||
.includes(:tags, :inputs, :outputs)
|
||||
current_team_switch(@project.team)
|
||||
end
|
||||
|
||||
|
|
|
@ -291,7 +291,7 @@ class MyModulesController < ApplicationController
|
|||
|
||||
task_names = []
|
||||
new_samples = []
|
||||
@my_module.get_downstream_modules.each do |my_module|
|
||||
@my_module.downstream_modules.each do |my_module|
|
||||
new_samples = samples.select { |el| my_module.samples.exclude?(el) }
|
||||
my_module.samples.push(*new_samples)
|
||||
task_names << my_module.name
|
||||
|
@ -330,7 +330,7 @@ class MyModulesController < ApplicationController
|
|||
end
|
||||
|
||||
task_names = []
|
||||
@my_module.get_downstream_modules.each do |my_module|
|
||||
@my_module.downstream_modules.each do |my_module|
|
||||
task_names << my_module.name
|
||||
my_module.samples.destroy(samples & my_module.samples)
|
||||
end
|
||||
|
|
|
@ -182,7 +182,8 @@ class ReportsController < ApplicationController
|
|||
@html = '<h1>No content</h1>' if @html.blank?
|
||||
render pdf: 'report',
|
||||
header: { right: '[page] of [topage]' },
|
||||
template: 'reports/report.pdf.erb'
|
||||
template: 'reports/report.pdf.erb',
|
||||
disable_javascript: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -198,12 +198,14 @@ class RepositoriesController < ApplicationController
|
|||
if parsed_file.too_large?
|
||||
repository_response(t('general.file.size_exceeded',
|
||||
file_size: Constants::FILE_MAX_SIZE_MB))
|
||||
elsif parsed_file.empty?
|
||||
flash[:notice] = t('teams.parse_sheet.errors.empty_file')
|
||||
redirect_to back and return
|
||||
else
|
||||
@import_data = parsed_file.data
|
||||
if parsed_file.generated_temp_file?
|
||||
|
||||
if @import_data.header.empty? || @import_data.columns.empty?
|
||||
return repository_response(t('teams.parse_sheet.errors.empty_file'))
|
||||
end
|
||||
|
||||
if (@temp_file = parsed_file.generate_temp_file)
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
|
|
|
@ -76,23 +76,23 @@ class ResultAssetsController < ApplicationController
|
|||
|
||||
@result.last_modified_by = current_user
|
||||
@result.assign_attributes(update_params)
|
||||
success_flash = t("result_assets.update.success_flash",
|
||||
module: @my_module.name)
|
||||
success_flash = t('result_assets.update.success_flash',
|
||||
module: @my_module.name)
|
||||
|
||||
if @result.archived_changed?(from: false, to: true)
|
||||
if previous_asset.locked?
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
format.html do
|
||||
flash[:error] = t('result_assets.archive.error_flash')
|
||||
redirect_to results_my_module_path(@my_module)
|
||||
return
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
saved = @result.archive(current_user)
|
||||
success_flash = t("result_assets.archive.success_flash",
|
||||
module: @my_module.name)
|
||||
success_flash = t('result_assets.archive.success_flash',
|
||||
module: @my_module.name)
|
||||
if saved
|
||||
Activity.create(
|
||||
type_of: :archive_result,
|
||||
|
@ -126,7 +126,7 @@ class ResultAssetsController < ApplicationController
|
|||
# Asset (file) and/or name has been changed
|
||||
saved = @result.save
|
||||
|
||||
if saved then
|
||||
if saved
|
||||
# Release team's space taken due to
|
||||
# previous asset being removed
|
||||
team = @result.my_module.experiment.project.team
|
||||
|
@ -134,9 +134,7 @@ class ResultAssetsController < ApplicationController
|
|||
team.save
|
||||
|
||||
# Post process new file if neccesary
|
||||
if @result.asset.present?
|
||||
@result.asset.post_process_file(team)
|
||||
end
|
||||
@result.asset.post_process_file(team) if @result.asset.present?
|
||||
|
||||
Activity.create(
|
||||
type_of: :edit_result,
|
||||
|
@ -144,32 +142,33 @@ class ResultAssetsController < ApplicationController
|
|||
project: @my_module.experiment.project,
|
||||
experiment: @my_module.experiment,
|
||||
my_module: @my_module,
|
||||
message: t(
|
||||
"activities.edit_asset_result",
|
||||
user: current_user.full_name,
|
||||
result: @result.name
|
||||
)
|
||||
message: t('activities.edit_asset_result',
|
||||
user: current_user.full_name,
|
||||
result: @result.name)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
if saved
|
||||
format.html {
|
||||
format.html do
|
||||
flash[:success] = success_flash
|
||||
redirect_to results_my_module_path(@my_module)
|
||||
}
|
||||
format.json {
|
||||
end
|
||||
format.json do
|
||||
render json: {
|
||||
html: render_to_string({
|
||||
partial: "my_modules/result.html.erb", locals: {result: @result}
|
||||
})
|
||||
html: render_to_string(
|
||||
partial: 'my_modules/result.html.erb', locals: { result: @result }
|
||||
)
|
||||
}, status: :ok
|
||||
}
|
||||
end
|
||||
else
|
||||
format.json {
|
||||
render json: @result.errors, status: :bad_request
|
||||
}
|
||||
format.json do
|
||||
render json: {
|
||||
status: :error,
|
||||
errors: @result.errors
|
||||
}, status: :bad_request
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -189,22 +188,15 @@ class ResultAssetsController < ApplicationController
|
|||
|
||||
def load_vars_nested
|
||||
@my_module = MyModule.find_by_id(params[:my_module_id])
|
||||
|
||||
unless @my_module
|
||||
render_404
|
||||
end
|
||||
render_404 unless @my_module
|
||||
end
|
||||
|
||||
def check_create_permissions
|
||||
unless can_create_result_asset_in_module(@my_module)
|
||||
render_403
|
||||
end
|
||||
render_403 unless can_create_result_asset_in_module(@my_module)
|
||||
end
|
||||
|
||||
def check_edit_permissions
|
||||
unless can_edit_result_asset_in_module(@my_module)
|
||||
render_403
|
||||
end
|
||||
render_403 unless can_edit_result_asset_in_module(@my_module)
|
||||
end
|
||||
|
||||
def check_archive_permissions
|
||||
|
|
|
@ -8,7 +8,6 @@ class SearchController < ApplicationController
|
|||
|
||||
search_projects if @search_category == :projects
|
||||
search_experiments if @search_category == :experiments
|
||||
search_workflows if @search_category == :workflows
|
||||
search_modules if @search_category == :modules
|
||||
search_results if @search_category == :results
|
||||
search_tags if @search_category == :tags
|
||||
|
@ -145,7 +144,6 @@ class SearchController < ApplicationController
|
|||
def count_search_results
|
||||
@project_search_count = count_by_name Project
|
||||
@experiment_search_count = count_by_name Experiment
|
||||
@workflow_search_count = count_by_name MyModuleGroup
|
||||
@module_search_count = count_by_name MyModule
|
||||
@result_search_count = count_by_name Result
|
||||
@tag_search_count = count_by_name Tag
|
||||
|
@ -161,7 +159,6 @@ class SearchController < ApplicationController
|
|||
|
||||
@search_results_count = @project_search_count
|
||||
@search_results_count += @experiment_search_count
|
||||
@search_results_count += @workflow_search_count
|
||||
@search_results_count += @module_search_count
|
||||
@search_results_count += @result_search_count
|
||||
@search_results_count += @tag_search_count
|
||||
|
@ -190,14 +187,6 @@ class SearchController < ApplicationController
|
|||
@search_count = @experiment_search_count
|
||||
end
|
||||
|
||||
def search_workflows
|
||||
@workflow_results = []
|
||||
if @workflow_search_count > 0
|
||||
@workflow_results = search_by_name(MyModuleGroup)
|
||||
end
|
||||
@search_count = @workflow_search_count
|
||||
end
|
||||
|
||||
def search_modules
|
||||
@module_results = []
|
||||
@module_results = search_by_name(MyModule) if @module_search_count > 0
|
||||
|
|
|
@ -290,10 +290,12 @@ class StepsController < ApplicationController
|
|||
"activities.uncheck_step_checklist_item"
|
||||
completed_items = chkItem.checklist.checklist_items.where(checked: true).count
|
||||
all_items = chkItem.checklist.checklist_items.count
|
||||
text_activity = smart_annotation_parser(chkItem.text)
|
||||
.gsub(/\s+/, ' ')
|
||||
message = t(
|
||||
str,
|
||||
user: current_user.full_name,
|
||||
checkbox: smart_annotation_parser(simple_format(chkItem.text)),
|
||||
checkbox: text_activity,
|
||||
step: chkItem.checklist.step.position + 1,
|
||||
step_name: chkItem.checklist.step.name,
|
||||
completed: completed_items,
|
||||
|
|
|
@ -7,106 +7,57 @@ class TeamsController < ApplicationController
|
|||
def parse_sheet
|
||||
session[:return_to] ||= request.referer
|
||||
|
||||
respond_to do |format|
|
||||
if params[:file]
|
||||
begin
|
||||
unless params[:file]
|
||||
return parse_sheet_error(t('teams.parse_sheet.errors.no_file_selected'))
|
||||
end
|
||||
if params[:file].size > Constants::FILE_MAX_SIZE_MB.megabytes
|
||||
error = t('general.file.size_exceeded',
|
||||
file_size: Constants::FILE_MAX_SIZE_MB)
|
||||
return parse_sheet_error(error)
|
||||
end
|
||||
|
||||
if params[:file].size > Constants::FILE_MAX_SIZE_MB.megabytes
|
||||
error = t 'general.file.size_exceeded',
|
||||
file_size: Constants::FILE_MAX_SIZE_MB
|
||||
begin
|
||||
sheet = SpreadsheetParser.open_spreadsheet(params[:file])
|
||||
@header, @columns = SpreadsheetParser.first_two_rows(sheet)
|
||||
|
||||
format.html {
|
||||
flash[:alert] = error
|
||||
redirect_to session.delete(:return_to)
|
||||
if @header.empty? || @columns.empty?
|
||||
return parse_sheet_error(t('teams.parse_sheet.errors.empty_file'))
|
||||
end
|
||||
|
||||
# Fill in fields for dropdown
|
||||
@available_fields = @team.get_available_sample_fields
|
||||
# Truncate long fields
|
||||
@available_fields.update(@available_fields) do |_k, v|
|
||||
v.truncate(Constants::NAME_TRUNCATION_LENGTH_DROPDOWN)
|
||||
end
|
||||
|
||||
# Save file for next step (importing)
|
||||
@temp_file = TempFile.new(
|
||||
session_id: session.id,
|
||||
file: params[:file]
|
||||
)
|
||||
|
||||
if @temp_file.save
|
||||
@temp_file.destroy_obsolete
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
html: render_to_string(
|
||||
partial: 'samples/parse_samples_modal.html.erb'
|
||||
)
|
||||
}
|
||||
format.json {
|
||||
render json: {message: error},
|
||||
status: :unprocessable_entity
|
||||
}
|
||||
|
||||
else
|
||||
sheet = Team.open_spreadsheet(params[:file])
|
||||
|
||||
# Check if we actually have any rows (last_row > 1)
|
||||
if sheet.last_row.between?(0, 1)
|
||||
flash[:notice] = t(
|
||||
"teams.parse_sheet.errors.empty_file")
|
||||
redirect_to session.delete(:return_to) and return
|
||||
end
|
||||
|
||||
# Get data (it will trigger any errors as well)
|
||||
@header = sheet.row(1)
|
||||
@columns = sheet.row(2)
|
||||
|
||||
# Fill in fields for dropdown
|
||||
@available_fields = @team.get_available_sample_fields
|
||||
# Truncate long fields
|
||||
@available_fields.update(@available_fields) do |_k, v|
|
||||
v.truncate(Constants::NAME_TRUNCATION_LENGTH_DROPDOWN)
|
||||
end
|
||||
|
||||
# Save file for next step (importing)
|
||||
@temp_file = TempFile.new(
|
||||
session_id: session.id,
|
||||
file: params[:file]
|
||||
)
|
||||
|
||||
if @temp_file.save
|
||||
@temp_file.destroy_obsolete
|
||||
# format.html
|
||||
format.json {
|
||||
render :json => {
|
||||
:html => render_to_string({
|
||||
:partial => "samples/parse_samples_modal.html.erb"
|
||||
})
|
||||
}
|
||||
}
|
||||
else
|
||||
error = t("teams.parse_sheet.errors.temp_file_failure")
|
||||
format.html {
|
||||
flash[:alert] = error
|
||||
redirect_to session.delete(:return_to)
|
||||
}
|
||||
format.json {
|
||||
render json: {message: error},
|
||||
status: :unprocessable_entity
|
||||
}
|
||||
end
|
||||
end
|
||||
rescue ArgumentError, CSV::MalformedCSVError
|
||||
error = t('teams.parse_sheet.errors.invalid_file',
|
||||
encoding: ''.encoding)
|
||||
format.html {
|
||||
flash[:alert] = error
|
||||
redirect_to session.delete(:return_to)
|
||||
}
|
||||
format.json {
|
||||
render json: {message: error},
|
||||
status: :unprocessable_entity
|
||||
}
|
||||
rescue TypeError
|
||||
error = t("teams.parse_sheet.errors.invalid_extension")
|
||||
format.html {
|
||||
flash[:alert] = error
|
||||
redirect_to session.delete(:return_to)
|
||||
}
|
||||
format.json {
|
||||
render json: {message: error},
|
||||
status: :unprocessable_entity
|
||||
}
|
||||
end
|
||||
else
|
||||
error = t("teams.parse_sheet.errors.no_file_selected")
|
||||
format.html {
|
||||
flash[:alert] = error
|
||||
session[:return_to] ||= request.referer
|
||||
redirect_to session.delete(:return_to)
|
||||
}
|
||||
format.json {
|
||||
render json: {message: error},
|
||||
status: :unprocessable_entity
|
||||
}
|
||||
return parse_sheet_error(
|
||||
t('teams.parse_sheet.errors.temp_file_failure')
|
||||
)
|
||||
end
|
||||
rescue ArgumentError, CSV::MalformedCSVError
|
||||
return parse_sheet_error(t('teams.parse_sheet.errors.invalid_file',
|
||||
encoding: ''.encoding))
|
||||
rescue TypeError
|
||||
return parse_sheet_error(t('teams.parse_sheet.errors.invalid_extension'))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -122,7 +73,7 @@ class TeamsController < ApplicationController
|
|||
if @temp_file.session_id == session.id
|
||||
# Check if mappings exists or else we don't have anything to parse
|
||||
if params[:mappings]
|
||||
@sheet = Team.open_spreadsheet(@temp_file.file)
|
||||
@sheet = SpreadsheetParser.open_spreadsheet(@temp_file.file)
|
||||
|
||||
# Check for duplicated values
|
||||
h1 = params[:mappings].clone.delete_if { |k, v| v.empty? }
|
||||
|
@ -275,6 +226,20 @@ class TeamsController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def parse_sheet_error(error)
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:alert] = error
|
||||
session[:return_to] ||= request.referer
|
||||
redirect_to session.delete(:return_to)
|
||||
end
|
||||
format.json do
|
||||
render json: { message: error },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_vars
|
||||
@team = Team.find_by_id(params[:id])
|
||||
|
||||
|
|
|
@ -1,17 +1,52 @@
|
|||
module ActivityHelper
|
||||
# constants for correct truncation length
|
||||
TAGS_LENGTH = 4
|
||||
TRUNCATE_OFFSET = 3
|
||||
def activity_truncate(message, len = Constants::NAME_TRUNCATION_LENGTH)
|
||||
activity_titles = message.scan(/<strong>(.*?)<\/strong>/)
|
||||
activity_titles.each do |activity_title|
|
||||
activity_title = activity_title[0]
|
||||
if activity_title.length > Constants::NAME_TRUNCATION_LENGTH
|
||||
# find first closing tag of smart annotation
|
||||
closing_tag_sa = activity_title.index('</a>')
|
||||
unless closing_tag_sa.nil?
|
||||
opening_tag_sa = activity_title.index('<img')
|
||||
opening_temp = activity_title.index('<span')
|
||||
# depending on user/experiment set the first opening tag
|
||||
opening_tag_sa = opening_temp if opening_tag_sa.nil? ||
|
||||
opening_tag_sa > opening_temp
|
||||
end
|
||||
len_temp = len
|
||||
# check until we run out of smart annotations in message
|
||||
while !opening_tag_sa.nil? && !closing_tag_sa.nil? &&
|
||||
opening_tag_sa < len_temp
|
||||
stripped = strip_tags(activity_title[opening_tag_sa...closing_tag_sa])
|
||||
.length
|
||||
len_temp += (activity_title[opening_tag_sa...closing_tag_sa +
|
||||
TAGS_LENGTH]).length - stripped
|
||||
len = len_temp + TRUNCATE_OFFSET + TAGS_LENGTH if len <= closing_tag_sa
|
||||
closing_temp = closing_tag_sa + 1
|
||||
closing_tag_sa = activity_title.index('</a>', closing_temp)
|
||||
unless closing_tag_sa.nil?
|
||||
# find next smart annotation
|
||||
opening_tag_sa = activity_title.index('<img', closing_temp)
|
||||
opening_temp = activity_title.index('<span', closing_temp)
|
||||
# depending on user/experiment set the next opening tag
|
||||
opening_tag_sa = opening_temp if opening_tag_sa.nil? ||
|
||||
opening_tag_sa > opening_temp
|
||||
end
|
||||
end
|
||||
# adjust truncation length according to smart annotations length
|
||||
len = activity_title.length if len > activity_title.length &&
|
||||
len != Constants::NAME_TRUNCATION_LENGTH
|
||||
if activity_title.length > len
|
||||
title = "<div class='modal-tooltip'>
|
||||
#{truncate(activity_title, length: len)}
|
||||
#{truncate(activity_title, length: len, escape: false)}
|
||||
<span class='modal-tooltiptext'>
|
||||
#{activity_title}
|
||||
</span>
|
||||
</div>"
|
||||
else
|
||||
title = truncate(activity_title, length: len)
|
||||
title = truncate(activity_title, length: len, escape: false)
|
||||
end
|
||||
message = message.gsub(/#{Regexp.escape(activity_title)}/, title)
|
||||
end
|
||||
|
|
|
@ -126,12 +126,12 @@ module ApplicationHelper
|
|||
next unless project
|
||||
if project.archived?
|
||||
"<span class='sa-type'>" \
|
||||
"#{sanitize_input(match[2])}</span> " \
|
||||
"#{sanitize_input(match[2])}</span>" \
|
||||
"#{link_to project.name,
|
||||
projects_archive_path} #{I18n.t('atwho.res.archived')}"
|
||||
else
|
||||
"<span class='sa-type'>" \
|
||||
"#{sanitize_input(match[2])}</span> " \
|
||||
"#{sanitize_input(match[2])}</span>" \
|
||||
"#{link_to project.name,
|
||||
project_path(project)}"
|
||||
end
|
||||
|
@ -140,13 +140,13 @@ module ApplicationHelper
|
|||
next unless experiment
|
||||
if experiment.archived?
|
||||
"<span class='sa-type'>" \
|
||||
"#{sanitize_input(match[2])}</span> " \
|
||||
"#{sanitize_input(match[2])}</span>" \
|
||||
"#{link_to experiment.name,
|
||||
experiment_archive_project_path(experiment.project)} " \
|
||||
"#{I18n.t('atwho.res.archived')}"
|
||||
else
|
||||
"<span class='sa-type'>"\
|
||||
"#{sanitize_input(match[2])}</span> " \
|
||||
"#{sanitize_input(match[2])}</span>" \
|
||||
"#{link_to experiment.name,
|
||||
canvas_experiment_path(experiment)}"
|
||||
end
|
||||
|
@ -155,25 +155,25 @@ module ApplicationHelper
|
|||
next unless my_module
|
||||
if my_module.archived?
|
||||
"<span class='sa-type'>" \
|
||||
"#{sanitize_input(match[2])}</span> " \
|
||||
"#{sanitize_input(match[2])}</span>" \
|
||||
"#{link_to my_module.name,
|
||||
module_archive_experiment_path(my_module.experiment)} " \
|
||||
"#{I18n.t('atwho.res.archived')}"
|
||||
else
|
||||
"<span class='sa-type'>" \
|
||||
"#{sanitize_input(match[2])}</span> " \
|
||||
"#{sanitize_input(match[2])}</span>" \
|
||||
"#{link_to my_module.name,
|
||||
protocols_my_module_path(my_module)}"
|
||||
end
|
||||
when 'sam'
|
||||
sample = Sample.find_by_id(match[3].base62_decode)
|
||||
if sample
|
||||
"<span class='glyphicon glyphicon-tint'></span> " \
|
||||
"<span class='glyphicon glyphicon-tint'></span>" \
|
||||
"#{link_to(sample.name,
|
||||
sample_path(sample.id),
|
||||
class: 'sample-info-link')}"
|
||||
else
|
||||
"<span class='glyphicon glyphicon-tint'></span> " \
|
||||
"<span class='glyphicon glyphicon-tint'></span>" \
|
||||
"#{match[1]} #{I18n.t('atwho.res.deleted')}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -64,7 +64,6 @@ module PermissionHelper
|
|||
:can_edit_connections,
|
||||
:can_create_modules,
|
||||
:can_edit_modules,
|
||||
:can_edit_module_groups,
|
||||
:can_clone_modules,
|
||||
:can_archive_modules,
|
||||
:can_view_reports,
|
||||
|
@ -149,7 +148,6 @@ module PermissionHelper
|
|||
:can_edit_connections,
|
||||
:can_create_modules,
|
||||
:can_edit_modules,
|
||||
:can_edit_module_groups,
|
||||
:can_clone_modules,
|
||||
:can_archive_modules
|
||||
] do |proxy, *args, &block|
|
||||
|
@ -421,10 +419,6 @@ module PermissionHelper
|
|||
is_user_or_higher_of_project(experiment.project)
|
||||
end
|
||||
|
||||
def can_edit_module_groups(experiment)
|
||||
is_user_or_higher_of_project(experiment.project)
|
||||
end
|
||||
|
||||
def can_clone_modules(experiment)
|
||||
is_user_or_higher_of_project(experiment.project)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import React, { Component } from "react";
|
||||
import { HelpBlock } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import shortid from "shortid";
|
||||
|
||||
const MyHelpBlock = styled(HelpBlock)`
|
||||
& > span {
|
||||
margin-right: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
class ValidatedErrorHelpBlock extends Component {
|
||||
static renderErrorMessage(error) {
|
||||
const key = shortid.generate();
|
||||
if (error.intl) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
key={key}
|
||||
id={error.messageId}
|
||||
values={error.values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <span key={key}>{error.message}</span>;
|
||||
}
|
||||
|
||||
cleanProps() {
|
||||
// Remove additional props from the props
|
||||
const { tag, ...cleanProps } = this.props;
|
||||
return cleanProps;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Remove additional props from the props
|
||||
const { tag, ...cleanProps } = this.props;
|
||||
|
||||
const errors = this.context.errors(tag) || [];
|
||||
return (
|
||||
<MyHelpBlock {...cleanProps}>
|
||||
{errors.map((error) => ValidatedErrorHelpBlock.renderErrorMessage(error))}
|
||||
</MyHelpBlock>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ValidatedErrorHelpBlock.propTypes = {
|
||||
tag: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
ValidatedErrorHelpBlock.contextTypes = {
|
||||
errors: PropTypes.func
|
||||
}
|
||||
|
||||
export default ValidatedErrorHelpBlock;
|
|
@ -0,0 +1,119 @@
|
|||
import React, { Component } from "react";
|
||||
import update from "immutability-helper";
|
||||
import PropTypes from "prop-types";
|
||||
import _ from "lodash";
|
||||
|
||||
class ValidatedForm extends Component {
|
||||
static parseErrors(errors) {
|
||||
// This method is quite smart, in the sense that accepts either
|
||||
// errors in 3 shapes: localized error messages ({}),
|
||||
// unlocalized error messages ({}), or mere strings (unlocalized)
|
||||
const arr = _.isString(errors) ? [errors] : errors;
|
||||
return arr.map((el) => _.isString(el) ? { message: el } : el);
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {}
|
||||
|
||||
this.setErrors = this.setErrors.bind(this);
|
||||
this.setErrorsForTag = this.setErrorsForTag.bind(this);
|
||||
this.errors = this.errors.bind(this);
|
||||
this.hasAnyError = this.hasAnyError.bind(this);
|
||||
this.hasErrorForTag = this.hasErrorForTag.bind(this);
|
||||
this.addErrorsForTag = this.addErrorsForTag.bind(this);
|
||||
this.clearErrorsForTag = this.clearErrorsForTag.bind(this);
|
||||
this.clearErrors = this.clearErrors.bind(this);
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
// Pass functions downstream via context
|
||||
return {
|
||||
setErrors: this.setErrors,
|
||||
setErrorsForTag: this.setErrorsForTag,
|
||||
errors: this.errors,
|
||||
hasAnyError: this.hasAnyError,
|
||||
hasErrorForTag: this.hasErrorForTag,
|
||||
addErrorsForTag: this.addErrorsForTag,
|
||||
clearErrorsForTag: this.clearErrorsForTag,
|
||||
clearErrors: this.clearErrors
|
||||
};
|
||||
}
|
||||
|
||||
setErrors(errors) {
|
||||
const newState = {};
|
||||
_.entries(errors).forEach(([key, value]) => {
|
||||
newState[key] = ValidatedForm.parseErrors(value);
|
||||
});
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
setErrorsForTag(tag, errors) {
|
||||
const newState = update(this.state, {
|
||||
[tag]: { $set: ValidatedForm.parseErrors(errors) }
|
||||
});
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
errors(tag) {
|
||||
return this.state[tag];
|
||||
}
|
||||
|
||||
hasAnyError() {
|
||||
return _.values(this.state) &&
|
||||
_.flatten(_.values(this.state)).length > 0;
|
||||
}
|
||||
|
||||
hasErrorForTag(tag) {
|
||||
return _.has(this.state, tag) && this.state[tag].length > 0;
|
||||
}
|
||||
|
||||
addErrorsForTag(tag, errors) {
|
||||
let newState;
|
||||
if (_.has(this.state, tag)) {
|
||||
newState = update(this.state, { [tag]: { $push: errors } });
|
||||
} else {
|
||||
newState = update(this.state, { [tag]: { $set: errors } });
|
||||
}
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
clearErrorsForTag(tag) {
|
||||
const newState = update(this.state, { [tag]: { $set: [] } });
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
clearErrors() {
|
||||
this.setState({});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form {...this.props}>
|
||||
{this.props.children}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ValidatedForm.propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
ValidatedForm.defaultProps = {
|
||||
children: undefined
|
||||
}
|
||||
|
||||
ValidatedForm.childContextTypes = {
|
||||
setErrors: PropTypes.func,
|
||||
setErrorsForTag: PropTypes.func,
|
||||
errors: PropTypes.func,
|
||||
hasAnyError: PropTypes.func,
|
||||
hasErrorForTag: PropTypes.func,
|
||||
addErrorsForTag: PropTypes.func,
|
||||
clearErrorsForTag: PropTypes.func,
|
||||
clearErrors: PropTypes.func
|
||||
}
|
||||
|
||||
export default ValidatedForm;
|
|
@ -0,0 +1,70 @@
|
|||
import React, { Component } from "react";
|
||||
import { FormControl } from "react-bootstrap";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
class ValidatedFormControl extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.cleanProps = this.cleanProps.bind(this);
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
const tag = this.props.tag;
|
||||
const messageIds = this.props.messageIds;
|
||||
const target = e.target;
|
||||
|
||||
// Pass-through "original" onChange
|
||||
if (_.has(this.props, "onChange") && this.props.onChange !== undefined) {
|
||||
this.props.onChange(e);
|
||||
}
|
||||
|
||||
// Validate the field
|
||||
let errors = [];
|
||||
this.props.validatorsOnChange.forEach((validator) => {
|
||||
errors = errors.concat(validator(target, messageIds));
|
||||
});
|
||||
this.context.setErrorsForTag(tag, errors);
|
||||
}
|
||||
|
||||
cleanProps() {
|
||||
// Remove additional props from the props
|
||||
const {
|
||||
tag,
|
||||
messageIds,
|
||||
validatorsOnChange,
|
||||
onChange,
|
||||
...cleanProps
|
||||
} = this.props;
|
||||
return cleanProps;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FormControl
|
||||
onChange={this.handleChange}
|
||||
{...this.cleanProps()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ValidatedFormControl.propTypes = {
|
||||
tag: PropTypes.string.isRequired,
|
||||
messageIds: PropTypes.objectOf(PropTypes.string),
|
||||
validatorsOnChange: PropTypes.arrayOf(PropTypes.func),
|
||||
onChange: PropTypes.func
|
||||
}
|
||||
|
||||
ValidatedFormControl.defaultProps = {
|
||||
messageIds: {},
|
||||
validatorsOnChange: [],
|
||||
onChange: undefined
|
||||
}
|
||||
|
||||
ValidatedFormControl.contextTypes = {
|
||||
setErrorsForTag: PropTypes.func
|
||||
}
|
||||
|
||||
export default ValidatedFormControl;
|
|
@ -0,0 +1,30 @@
|
|||
import React from "react";
|
||||
import { FormGroup } from "react-bootstrap";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ValidatedFormGroup = (props, context) => {
|
||||
// Remove additional props from the props
|
||||
const { tag, ...cleanProps } = props;
|
||||
|
||||
const hasError = context.hasErrorForTag(tag);
|
||||
const formGroupClass = `form-group ${hasError ? " has-error" : ""}`;
|
||||
const validationState = hasError ? "error" : null;
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
className={formGroupClass}
|
||||
validationState={validationState}
|
||||
{...cleanProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ValidatedFormGroup.propTypes = {
|
||||
tag: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
ValidatedFormGroup.contextTypes = {
|
||||
hasErrorForTag: PropTypes.func
|
||||
}
|
||||
|
||||
export default ValidatedFormGroup;
|
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ValidatedSubmitButton = (props, context) =>
|
||||
<Button {...props} disabled={context.hasAnyError()}>
|
||||
{props.children}
|
||||
</Button>
|
||||
;
|
||||
|
||||
ValidatedSubmitButton.propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
ValidatedSubmitButton.defaultProps = {
|
||||
children: undefined
|
||||
}
|
||||
|
||||
ValidatedSubmitButton.contextTypes = {
|
||||
hasAnyError: PropTypes.func
|
||||
}
|
||||
|
||||
export default ValidatedSubmitButton;
|
5
app/javascript/src/components/validation/index.js
Normal file
5
app/javascript/src/components/validation/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export {default as ValidatedErrorHelpBlock} from "./components/ValidatedErrorHelpBlock";
|
||||
export {default as ValidatedForm} from "./components/ValidatedForm";
|
||||
export {default as ValidatedFormControl} from "./components/ValidatedFormControl";
|
||||
export {default as ValidatedFormGroup} from "./components/ValidatedFormGroup";
|
||||
export {default as ValidatedSubmitButton} from "./components/ValidatedSubmitButton";
|
45
app/javascript/src/components/validation/validators/file.js
Normal file
45
app/javascript/src/components/validation/validators/file.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import _ from "lodash";
|
||||
import { AVATAR_MAX_SIZE_MB } from "../../../config/constants/numeric";
|
||||
import { AVATAR_VALID_EXTENSIONS } from "../../../config/constants/strings";
|
||||
|
||||
export const avatarExtensionValidator = (target, messageIds = {}) => {
|
||||
const messageId = _.has(messageIds, "invalid_file_extension") ?
|
||||
messageIds.invalid_file_extension :
|
||||
"validators.file.invalid_file_extension";
|
||||
|
||||
const filePath = target.value;
|
||||
const ext = filePath
|
||||
.substring(filePath.lastIndexOf(".") + 1)
|
||||
.toLowerCase();
|
||||
const validExtsString = AVATAR_VALID_EXTENSIONS
|
||||
.map(val => `.${val}`)
|
||||
.join(", ");
|
||||
|
||||
if (!_.includes(AVATAR_VALID_EXTENSIONS, ext)) {
|
||||
return [{
|
||||
intl: true,
|
||||
messageId,
|
||||
values: { valid_extensions: validExtsString }
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export const avatarSizeValidator = (target, messageIds = {}) => {
|
||||
const messageId = _.has(messageIds, "file_too_large") ?
|
||||
messageIds.file_too_large :
|
||||
"validators.file.file_too_large";
|
||||
const maxSizeKb = AVATAR_MAX_SIZE_MB * 1024;
|
||||
|
||||
if (target.files && target.files[0]) {
|
||||
const fileSize = target.files[0].size / 1024; // size in KB
|
||||
if (fileSize > maxSizeKb) {
|
||||
return [{
|
||||
intl: true,
|
||||
messageId,
|
||||
values: { max_size: AVATAR_MAX_SIZE_MB }
|
||||
}];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
139
app/javascript/src/components/validation/validators/text.js
Normal file
139
app/javascript/src/components/validation/validators/text.js
Normal file
|
@ -0,0 +1,139 @@
|
|||
import _ from "lodash";
|
||||
import {
|
||||
NAME_MIN_LENGTH,
|
||||
NAME_MAX_LENGTH,
|
||||
TEXT_MAX_LENGTH,
|
||||
PASSWORD_MIN_LENGTH,
|
||||
PASSWORD_MAX_LENGTH,
|
||||
USER_INITIALS_MAX_LENGTH
|
||||
} from "../../../config/constants/numeric";
|
||||
import { EMAIL_REGEX } from "../../../config/constants/strings";
|
||||
|
||||
export const nameMinLengthValidator = (target, messageIds = {}) => {
|
||||
const messageId = _.has(messageIds, "text_too_short") ?
|
||||
messageIds.text_too_short :
|
||||
"validators.text.text_too_short";
|
||||
const value = target.value;
|
||||
|
||||
if (value.length < NAME_MIN_LENGTH) {
|
||||
return [{
|
||||
intl: true,
|
||||
messageId,
|
||||
values: { min_length: NAME_MIN_LENGTH }
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const nameMaxLengthValidator = (target, messageIds = {}) => {
|
||||
const messageId = _.has(messageIds, "text_too_long") ?
|
||||
messageIds.text_too_long :
|
||||
"validators.text.text_too_long";
|
||||
const value = target.value;
|
||||
|
||||
if (value.length > NAME_MAX_LENGTH) {
|
||||
return [{
|
||||
intl: true,
|
||||
messageId,
|
||||
values:{ max_length: NAME_MAX_LENGTH }
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const nameLengthValidator = (target, messageIds = {}) => {
|
||||
const res = nameMinLengthValidator(target, messageIds);
|
||||
if (res.length > 0) {
|
||||
return res;
|
||||
}
|
||||
return nameMaxLengthValidator(target, messageIds);
|
||||
};
|
||||
|
||||
export const textBlankValidator = (target, messageIds = {}) => {
|
||||
const messageId = _.has(messageIds, "text_blank") ?
|
||||
messageIds.text_blank :
|
||||
"validators.text.text_blank";
|
||||
const value = target.value;
|
||||
|
||||
if (value.length === 0) {
|
||||
return [{
|
||||
intl: true,
|
||||
messageId
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export const textMaxLengthValidator = (target, messageIds = {}) => {
|
||||
const messageId = _.has(messageIds, "text_too_long") ?
|
||||
messageIds.text_too_long :
|
||||
"validators.text.text_too_long";
|
||||
const value = target.value;
|
||||
|
||||
if (value.length > TEXT_MAX_LENGTH) {
|
||||
return [{
|
||||
intl: true,
|
||||
messageId,
|
||||
values: { max_length: TEXT_MAX_LENGTH }
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const passwordLengthValidator = (target, messageIds = {}) => {
|
||||
const messageIdTooShort = _.has(messageIds, "text_too_short") ?
|
||||
messageIds.text_too_short :
|
||||
"validators.text.text_too_short";
|
||||
const messageIdTooLong = _.has(messageIds, "text_too_long") ?
|
||||
messageIds.text_too_long :
|
||||
"validators.text.text_too_long";
|
||||
const value = target.value;
|
||||
|
||||
if (value.length < PASSWORD_MIN_LENGTH) {
|
||||
return [{
|
||||
intl: true,
|
||||
messageId: messageIdTooShort,
|
||||
values:{ min_length: PASSWORD_MIN_LENGTH }
|
||||
}];
|
||||
} else if (value.length > PASSWORD_MAX_LENGTH) {
|
||||
return [{
|
||||
intl: true,
|
||||
messageId: messageIdTooLong,
|
||||
values:{ max_length: PASSWORD_MAX_LENGTH }
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const userInitialsMaxLengthValidator = (target, messageIds = {}) => {
|
||||
const messageId = _.has(messageIds, "text_too_long") ?
|
||||
messageIds.text_too_long :
|
||||
"validators.text.text_too_long";
|
||||
const value = target.value;
|
||||
|
||||
if (value.length > USER_INITIALS_MAX_LENGTH) {
|
||||
return [{
|
||||
intl: true,
|
||||
messageId,
|
||||
values: { max_length: USER_INITIALS_MAX_LENGTH }
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const emailValidator = (target, messageIds = {}) => {
|
||||
const res = textBlankValidator(target, messageIds);
|
||||
if (res.length > 0) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const messageId = _.has(messageIds, "invalid_email") ?
|
||||
messageIds.invalid_email :
|
||||
"validators.text.invalid_email";
|
||||
const value = target.value;
|
||||
|
||||
if (!EMAIL_REGEX.test(value)) {
|
||||
return [{ intl: true, messageId }];
|
||||
}
|
||||
return [];
|
||||
};
|
|
@ -6,3 +6,4 @@ export const PASSWORD_MAX_LENGTH = 72;
|
|||
export const NAME_MAX_LENGTH = 255;
|
||||
export const TEXT_MAX_LENGTH = 10000;
|
||||
export const INVITE_USERS_LIMIT = 20;
|
||||
export const AVATAR_MAX_SIZE_MB = 0.2;
|
|
@ -2,3 +2,4 @@ export const ASSIGNMENT_NOTIFICATION = "ASSIGNMENT";
|
|||
export const RECENT_NOTIFICATION = "RECENT_NOTIFICATION";
|
||||
export const SYSTEM_NOTIFICATION = "SYSTEM_NOTIFICATION";
|
||||
export const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
export const AVATAR_VALID_EXTENSIONS = ["jpg", "jpeg", "png", "gif"];
|
|
@ -16,12 +16,17 @@ export default {
|
|||
all_teams_page: "SciNote | Settings | Teams",
|
||||
new_team_page: "SciNote | Settings | Teams | New"
|
||||
},
|
||||
error_messages: {
|
||||
text_too_short: "is too short (minimum is {min_length} characters)",
|
||||
text_too_long: "is too long (maximum is {max_length} characters)",
|
||||
cant_be_blank: "can't be blank",
|
||||
invalid_email: "invalid email",
|
||||
passwords_dont_match: "Passwords don't match"
|
||||
validators: {
|
||||
text: {
|
||||
text_too_short: "is too short (minimum is {min_length} characters)",
|
||||
text_too_long: "is too long (maximum is {max_length} characters)",
|
||||
text_blank: "can't be blank",
|
||||
invalid_email: "invalid email"
|
||||
},
|
||||
file: {
|
||||
invalid_file_extension: "invalid file extension (valid extensions are {valid_extensions})",
|
||||
file_too_large: "file too large (maximum size is {max_size} MB)"
|
||||
}
|
||||
},
|
||||
navbar: {
|
||||
page_title: "sciNote",
|
||||
|
|
|
@ -4,29 +4,39 @@ import { string, func } from "prop-types";
|
|||
import styled from "styled-components";
|
||||
import { FormattedMessage, FormattedHTMLMessage } from "react-intl";
|
||||
import {
|
||||
FormGroup,
|
||||
FormControl,
|
||||
ControlLabel,
|
||||
Button,
|
||||
ButtonToolbar,
|
||||
HelpBlock
|
||||
} from "react-bootstrap";
|
||||
import update from "immutability-helper";
|
||||
import { updateUser } from "../../../../../services/api/users_api";
|
||||
import { transformName } from "../../../../../services/helpers/string_helper";
|
||||
import { addAlert } from "../../../../../components/actions/AlertsActions";
|
||||
|
||||
import {
|
||||
BORDER_LIGHT_COLOR,
|
||||
COLOR_APPLE_BLOSSOM
|
||||
} from "../../../../../config/constants/colors";
|
||||
import {
|
||||
ENTER_KEY_CODE,
|
||||
USER_INITIALS_MAX_LENGTH,
|
||||
NAME_MAX_LENGTH,
|
||||
PASSWORD_MAX_LENGTH,
|
||||
PASSWORD_MIN_LENGTH
|
||||
} from "../../../../../config/constants/numeric";
|
||||
import { EMAIL_REGEX } from "../../../../../config/constants/strings";
|
||||
import {
|
||||
ValidatedForm,
|
||||
ValidatedFormGroup,
|
||||
ValidatedFormControl,
|
||||
ValidatedErrorHelpBlock,
|
||||
ValidatedSubmitButton
|
||||
} from "../../../../../components/validation";
|
||||
import {
|
||||
textBlankValidator,
|
||||
nameMaxLengthValidator,
|
||||
passwordLengthValidator,
|
||||
userInitialsMaxLengthValidator,
|
||||
emailValidator
|
||||
} from "../../../../../components/validation/validators/text";
|
||||
import {
|
||||
avatarExtensionValidator,
|
||||
avatarSizeValidator
|
||||
} from "../../../../../components/validation/validators/file";
|
||||
|
||||
const StyledInputEnabled = styled.div`
|
||||
border: 1px solid ${BORDER_LIGHT_COLOR};
|
||||
|
@ -38,10 +48,6 @@ const StyledInputEnabled = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const StyledHelpBlock = styled(HelpBlock)`
|
||||
color: ${COLOR_APPLE_BLOSSOM};
|
||||
`;
|
||||
|
||||
class InputEnabled extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -49,30 +55,16 @@ class InputEnabled extends Component {
|
|||
this.state = {
|
||||
value: this.props.inputValue === "********" ? "" : this.props.inputValue,
|
||||
current_password: "",
|
||||
password_confirmation: "",
|
||||
errorMessage: ""
|
||||
password_confirmation: ""
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handlePasswordConfirmation = this.handlePasswordConfirmation.bind(
|
||||
this
|
||||
);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
this.confirmationField = this.confirmationField.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.getValidationState = this.getValidationState.bind(this);
|
||||
this.handleFullNameValidation = this.handleFullNameValidation.bind(this);
|
||||
this.handleEmailValidation = this.handleEmailValidation.bind(this);
|
||||
this.handleInitialsValidation = this.handleInitialsValidation.bind(this);
|
||||
this.handlePasswordConfirmationValidation = this.handlePasswordConfirmationValidation.bind(
|
||||
this
|
||||
);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleCurrentPassword = this.handleCurrentPassword.bind(this);
|
||||
this.handleFileChange = this.handleFileChange.bind(this);
|
||||
}
|
||||
|
||||
getValidationState() {
|
||||
return this.state.errorMessage.length > 0 ? "error" : null;
|
||||
this.handlePasswordConfirmation = this.handlePasswordConfirmation.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.inputField = this.inputField.bind(this);
|
||||
this.confirmationField = this.confirmationField.bind(this);
|
||||
}
|
||||
|
||||
handleKeyPress(event) {
|
||||
|
@ -83,170 +75,30 @@ class InputEnabled extends Component {
|
|||
}
|
||||
|
||||
handleChange(event) {
|
||||
event.preventDefault();
|
||||
switch (this.props.dataField) {
|
||||
case "full_name":
|
||||
this.handleFullNameValidation(event);
|
||||
break;
|
||||
case "email":
|
||||
this.handleEmailValidation(event);
|
||||
break;
|
||||
case "initials":
|
||||
this.handleInitialsValidation(event);
|
||||
break;
|
||||
case "password":
|
||||
this.handlePasswordValidation(event);
|
||||
break;
|
||||
case "avatar":
|
||||
this.handleFileChange(event);
|
||||
break;
|
||||
default:
|
||||
this.setState({ value: event.target.value, errorMessage: "" });
|
||||
}
|
||||
}
|
||||
|
||||
handleFileChange(event) {
|
||||
this.setState({ value: event.currentTarget.files[0], errorMessage: "" });
|
||||
}
|
||||
|
||||
handlePasswordConfirmation(event) {
|
||||
const { value } = event.target;
|
||||
if (value.length === 0) {
|
||||
this.setState({
|
||||
password_confirmation: value,
|
||||
errorMessage: <FormattedMessage id="error_messages.cant_be_blank" />
|
||||
});
|
||||
}
|
||||
this.setState({ password_confirmation: value });
|
||||
}
|
||||
|
||||
handleFullNameValidation(event) {
|
||||
const { value } = event.target;
|
||||
if (value.length > NAME_MAX_LENGTH) {
|
||||
this.setState({
|
||||
value,
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: NAME_MAX_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else if (value.length === 0) {
|
||||
this.setState({
|
||||
value,
|
||||
errorMessage: <FormattedMessage id="error_messages.cant_be_blank" />
|
||||
});
|
||||
let newVal;
|
||||
if (this.props.dataField === "avatar") {
|
||||
newVal = event.currentTarget.files[0];
|
||||
} else {
|
||||
this.setState({ value, errorMessage: "" });
|
||||
}
|
||||
}
|
||||
|
||||
handleEmailValidation(event) {
|
||||
const { value } = event.target;
|
||||
if (!EMAIL_REGEX.test(value)) {
|
||||
this.setState({
|
||||
value,
|
||||
errorMessage: <FormattedMessage id="error_messages.invalid_email" />
|
||||
});
|
||||
} else if (value.length === 0) {
|
||||
this.setState({
|
||||
value,
|
||||
errorMessage: <FormattedMessage id="error_messages.cant_be_blank" />
|
||||
});
|
||||
} else {
|
||||
this.setState({ value, errorMessage: "" });
|
||||
}
|
||||
}
|
||||
|
||||
handleInitialsValidation(event) {
|
||||
const { value } = event.target;
|
||||
if (value.length > USER_INITIALS_MAX_LENGTH) {
|
||||
this.setState({
|
||||
value,
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: USER_INITIALS_MAX_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else if (value.length === 0) {
|
||||
this.setState({
|
||||
value,
|
||||
errorMessage: <FormattedMessage id="error_messages.cant_be_blank" />
|
||||
});
|
||||
} else {
|
||||
this.setState({ value, errorMessage: "" });
|
||||
}
|
||||
}
|
||||
|
||||
handlePasswordValidation(event) {
|
||||
const { value } = event.target;
|
||||
if (value.length > PASSWORD_MAX_LENGTH) {
|
||||
this.setState({
|
||||
value,
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: PASSWORD_MAX_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else if (value.length < PASSWORD_MIN_LENGTH) {
|
||||
this.setState({
|
||||
value,
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_short"
|
||||
values={{ min_length: PASSWORD_MIN_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.setState({ value, errorMessage: "" });
|
||||
}
|
||||
}
|
||||
|
||||
handlePasswordConfirmationValidation(event) {
|
||||
const { value } = event.target;
|
||||
if (value !== this.state.value) {
|
||||
this.setState({
|
||||
password_confirmation: value,
|
||||
errorMessage: (
|
||||
<FormattedMessage id="error_messages.passwords_dont_match" />
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.setState({ password_confirmation: value, errorMessage: "" });
|
||||
newVal = event.target.value;
|
||||
}
|
||||
const newState = update(this.state, {
|
||||
value: { $set: newVal }
|
||||
});
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
handleCurrentPassword(event) {
|
||||
const { value } = event.target;
|
||||
if (value.length > PASSWORD_MAX_LENGTH) {
|
||||
this.setState({
|
||||
current_password: value,
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: PASSWORD_MAX_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else if (value.length < PASSWORD_MIN_LENGTH) {
|
||||
this.setState({
|
||||
current_password: value,
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_short"
|
||||
values={{ min_length: PASSWORD_MIN_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.setState({ current_password: value, errorMessage: "" });
|
||||
}
|
||||
const newState = update(this.state, {
|
||||
current_password: { $set: event.target.value }
|
||||
});
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
handlePasswordConfirmation(event) {
|
||||
const newState = update(this.state, {
|
||||
password_confirmation: { $set: event.target.value }
|
||||
});
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
|
@ -292,7 +144,7 @@ class InputEnabled extends Component {
|
|||
}
|
||||
})
|
||||
.catch(({ response }) => {
|
||||
this.setState({ errorMessage: response.data.message.toString() });
|
||||
this.form.setErrors(response.data.details);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -301,108 +153,156 @@ class InputEnabled extends Component {
|
|||
|
||||
if (type === "email") {
|
||||
return (
|
||||
<div>
|
||||
<FormattedHTMLMessage id="settings_page.password_confirmation" />
|
||||
<FormControl
|
||||
<ValidatedFormGroup tag="current_password">
|
||||
<ControlLabel>
|
||||
<FormattedHTMLMessage id="settings_page.password_confirmation" />
|
||||
</ControlLabel>
|
||||
<ValidatedFormControl
|
||||
id="settings_page.current_password"
|
||||
tag="current_password"
|
||||
type="password"
|
||||
value={this.state.current_password}
|
||||
validatorsOnChange={[passwordLengthValidator]}
|
||||
onChange={this.handleCurrentPassword}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
/>
|
||||
</div>
|
||||
<ValidatedErrorHelpBlock tag="current_password" />
|
||||
</ValidatedFormGroup>
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
inputField() {
|
||||
const { inputType } = this.props;
|
||||
const { inputType, dataField } = this.props;
|
||||
|
||||
let validatorsOnChange = [];
|
||||
if (dataField === "full_name") {
|
||||
validatorsOnChange = [textBlankValidator, nameMaxLengthValidator];
|
||||
} else if (dataField === "initials") {
|
||||
validatorsOnChange = [textBlankValidator, userInitialsMaxLengthValidator];
|
||||
} else if (dataField === "email") {
|
||||
validatorsOnChange = [emailValidator];
|
||||
} else if (dataField === "avatar") {
|
||||
validatorsOnChange = [avatarExtensionValidator, avatarSizeValidator];
|
||||
}
|
||||
|
||||
if (inputType === "password") {
|
||||
return (
|
||||
<div>
|
||||
<FormattedHTMLMessage id="settings_page.password_confirmation" />
|
||||
<FormControl
|
||||
id="settings_page.current_password"
|
||||
type={inputType}
|
||||
value={this.state.current_password}
|
||||
onChange={this.handleCurrentPassword}
|
||||
autoFocus
|
||||
/>
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.new_password" />
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
id="settings_page.new_password"
|
||||
type={inputType}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
autoFocus
|
||||
/>
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.new_password_confirmation" />
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
id="settings_page.new_password_confirmation"
|
||||
type={inputType}
|
||||
value={this.state.password_confirmation}
|
||||
onChange={this.handlePasswordConfirmationValidation}
|
||||
/>
|
||||
<ValidatedFormGroup tag="current_password">
|
||||
<ControlLabel>
|
||||
<FormattedHTMLMessage id="settings_page.password_confirmation" />
|
||||
</ControlLabel>
|
||||
<ValidatedFormControl
|
||||
id="settings_page.current_password"
|
||||
type="password"
|
||||
value={this.state.current_password}
|
||||
tag="current_password"
|
||||
validatorsOnChange={[passwordLengthValidator]}
|
||||
onChange={this.handleCurrentPassword}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
autoFocus
|
||||
/>
|
||||
<ValidatedErrorHelpBlock tag="current_password" />
|
||||
</ValidatedFormGroup>
|
||||
<ValidatedFormGroup tag="new_password">
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.new_password" />
|
||||
</ControlLabel>
|
||||
<ValidatedFormControl
|
||||
id="settings_page.new_password"
|
||||
type="password"
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
tag="new_password"
|
||||
validatorsOnChange={[passwordLengthValidator]}
|
||||
autoFocus
|
||||
/>
|
||||
<ValidatedErrorHelpBlock tag="new_password" />
|
||||
</ValidatedFormGroup>
|
||||
<ValidatedFormGroup tag="new_password_confirmation">
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.new_password_confirmation" />
|
||||
</ControlLabel>
|
||||
<ValidatedFormControl
|
||||
id="settings_page.new_password_confirmation"
|
||||
type="password"
|
||||
value={this.state.password_confirmation}
|
||||
onChange={this.handlePasswordConfirmation}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
tag="new_password_confirmation"
|
||||
validatorsOnChange={[passwordLengthValidator]}
|
||||
/>
|
||||
<ValidatedErrorHelpBlock tag="new_password_confirmation" />
|
||||
</ValidatedFormGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (inputType === "file") {
|
||||
return (
|
||||
<FormControl
|
||||
id="user_avatar_input"
|
||||
type={this.props.inputType}
|
||||
onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
autoFocus
|
||||
/>
|
||||
<ValidatedFormGroup tag={dataField}>
|
||||
<ValidatedFormControl
|
||||
id="user_avatar_input"
|
||||
tag={dataField}
|
||||
type={this.props.inputType}
|
||||
onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
validatorsOnChange={validatorsOnChange}
|
||||
autoFocus
|
||||
/>
|
||||
<ValidatedErrorHelpBlock tag={dataField} />
|
||||
</ValidatedFormGroup>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormControl
|
||||
type={this.props.inputType}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
autoFocus
|
||||
/>
|
||||
<ValidatedFormGroup tag={dataField}>
|
||||
<ValidatedFormControl
|
||||
tag={dataField}
|
||||
type={this.props.inputType}
|
||||
onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
validatorsOnChange={validatorsOnChange}
|
||||
value={this.state.value}
|
||||
autoFocus
|
||||
/>
|
||||
<ValidatedErrorHelpBlock tag={dataField} />
|
||||
</ValidatedFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledInputEnabled id={transformName(this.props.labelTitle)}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<FormGroup validationState={this.getValidationState()}>
|
||||
<h4>
|
||||
<FormattedMessage id="settings_page.change" />
|
||||
<FormattedMessage id={this.props.labelTitle} />
|
||||
</h4>
|
||||
{this.props.labelValue !== "none" && (
|
||||
<ControlLabel>
|
||||
<FormattedMessage id={this.props.labelValue} />
|
||||
</ControlLabel>
|
||||
)}
|
||||
{this.inputField()}
|
||||
{this.confirmationField()}
|
||||
<StyledHelpBlock>{this.state.errorMessage}</StyledHelpBlock>
|
||||
<ButtonToolbar>
|
||||
<Button bsStyle="primary" type="submit">
|
||||
<FormattedMessage
|
||||
id={`general.${this.props.dataField === "avatar"
|
||||
? "upload"
|
||||
: "update"}`}
|
||||
/>
|
||||
</Button>
|
||||
<Button bsStyle="default" onClick={this.props.disableEdit}>
|
||||
<FormattedMessage id="general.cancel" />
|
||||
</Button>
|
||||
</ButtonToolbar>
|
||||
</FormGroup>
|
||||
</form>
|
||||
<StyledInputEnabled>
|
||||
<ValidatedForm
|
||||
onSubmit={this.handleSubmit}
|
||||
ref={(f) => { this.form = f; }}
|
||||
>
|
||||
<h4>
|
||||
<FormattedMessage id="settings_page.change" />
|
||||
<FormattedMessage id={this.props.labelTitle} />
|
||||
</h4>
|
||||
{this.props.labelValue !== "none" && (
|
||||
<ControlLabel>
|
||||
<FormattedMessage id={this.props.labelValue} />
|
||||
</ControlLabel>
|
||||
)}
|
||||
{this.inputField()}
|
||||
{this.confirmationField()}
|
||||
<ButtonToolbar>
|
||||
<ValidatedSubmitButton bsStyle="primary" type="submit">
|
||||
<FormattedMessage
|
||||
id={`general.${this.props.dataField === "avatar"
|
||||
? "upload"
|
||||
: "update"}`}
|
||||
/>
|
||||
</ValidatedSubmitButton>
|
||||
<Button bsStyle="default" onClick={this.props.disableEdit}>
|
||||
<FormattedMessage id="general.cancel" />
|
||||
</Button>
|
||||
</ButtonToolbar>
|
||||
</ValidatedForm>
|
||||
</StyledInputEnabled>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,17 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from "react";
|
||||
import type { Node } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
FormGroup,
|
||||
ControlLabel,
|
||||
FormControl,
|
||||
HelpBlock
|
||||
} from "react-bootstrap";
|
||||
import { Modal, Button, ControlLabel, FormControl } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
ValidatedForm,
|
||||
ValidatedFormGroup,
|
||||
ValidatedFormControl,
|
||||
ValidatedErrorHelpBlock,
|
||||
ValidatedSubmitButton
|
||||
} from "../../../../../components/validation";
|
||||
import { textMaxLengthValidator } from "../../../../../components/validation/validators/text";
|
||||
import { updateTeam } from "../../../../../services/api/teams_api";
|
||||
import { TEXT_MAX_LENGTH } from "../../../../../config/constants/numeric";
|
||||
import { COLOR_APPLE_BLOSSOM } from "../../../../../config/constants/colors";
|
||||
|
||||
const StyledHelpBlock = styled(HelpBlock)`
|
||||
color: ${COLOR_APPLE_BLOSSOM};
|
||||
`;
|
||||
|
||||
type Team = {
|
||||
id: number,
|
||||
|
@ -41,36 +33,19 @@ type State = {
|
|||
class UpdateTeamDescriptionModal extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
(this: any).state = { errorMessage: "", description: "" };
|
||||
(this: any).onCloseModal = this.onCloseModal.bind(this);
|
||||
(this: any).updateDescription = this.updateDescription.bind(this);
|
||||
(this: any).handleDescription = this.handleDescription.bind(this);
|
||||
(this: any).getValidationState = this.getValidationState.bind(this);
|
||||
this.state = { description: "" };
|
||||
this.onCloseModal = this.onCloseModal.bind(this);
|
||||
this.updateDescription = this.updateDescription.bind(this);
|
||||
this.handleDescription = this.handleDescription.bind(this);
|
||||
}
|
||||
|
||||
onCloseModal(): void {
|
||||
(this: any).setState({ errorMessage: "", description: "" });
|
||||
onCloseModal() {
|
||||
this.setState({ description: "" });
|
||||
this.props.hideModal();
|
||||
}
|
||||
|
||||
getValidationState(): string | null {
|
||||
return String(this.state.errorMessage).length > 0 ? "error" : null;
|
||||
}
|
||||
|
||||
handleDescription(el: SyntheticEvent<HTMLButtonElement>): void {
|
||||
const { value } = el.currentTarget;
|
||||
if (value.length > TEXT_MAX_LENGTH) {
|
||||
this.setState({
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: TEXT_MAX_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else {
|
||||
(this: any).setState({ errorMessage: "", description: value });
|
||||
}
|
||||
handleDescription(el) {
|
||||
this.setState({ description: el.target.value });
|
||||
}
|
||||
|
||||
updateDescription(): void {
|
||||
|
@ -85,40 +60,44 @@ class UpdateTeamDescriptionModal extends Component<Props, State> {
|
|||
render(): Node {
|
||||
return (
|
||||
<Modal show={this.props.showModal} onHide={this.onCloseModal}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<FormattedMessage id="settings_page.update_team_description_modal.title" />
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<FormGroup
|
||||
controlId="teamDescription"
|
||||
validationState={this.getValidationState()}
|
||||
>
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.update_team_description_modal.label" />
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
componentClass="textarea"
|
||||
defaultValue={this.props.team.description}
|
||||
onChange={this.handleDescription}
|
||||
/>
|
||||
<FormControl.Feedback />
|
||||
<StyledHelpBlock>{this.state.errorMessage}</StyledHelpBlock>
|
||||
</FormGroup>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
onClick={this.updateDescription}
|
||||
disabled={!_.isEmpty(this.state.errorMessage)}
|
||||
>
|
||||
<FormattedMessage id="general.update" />
|
||||
</Button>
|
||||
<Button onClick={this.onCloseModal}>
|
||||
<FormattedMessage id="general.close" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
<ValidatedForm
|
||||
ref={f => {
|
||||
this.form = f;
|
||||
}}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<FormattedMessage id="settings_page.update_team_description_modal.title" />
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<ValidatedFormGroup tag="description">
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.update_team_description_modal.label" />
|
||||
</ControlLabel>
|
||||
<ValidatedFormControl
|
||||
componentClass="textarea"
|
||||
tag="description"
|
||||
defaultValue={this.props.team.description}
|
||||
onChange={this.handleDescription}
|
||||
validatorsOnChange={[textMaxLengthValidator]}
|
||||
/>
|
||||
<FormControl.Feedback />
|
||||
<ValidatedErrorHelpBlock tag="description" />
|
||||
</ValidatedFormGroup>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<ValidatedSubmitButton
|
||||
bsStyle="primary"
|
||||
onClick={this.updateDescription}
|
||||
>
|
||||
<FormattedMessage id="general.update" />
|
||||
</ValidatedSubmitButton>
|
||||
<Button onClick={this.onCloseModal}>
|
||||
<FormattedMessage id="general.close" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</ValidatedForm>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,35 +1,26 @@
|
|||
// @flow
|
||||
import React, { Component } from "react";
|
||||
import type { Node } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
FormGroup,
|
||||
ControlLabel,
|
||||
FormControl,
|
||||
HelpBlock
|
||||
} from "react-bootstrap";
|
||||
import { Modal, Button, ControlLabel, FormControl } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
ValidatedForm,
|
||||
ValidatedFormGroup,
|
||||
ValidatedFormControl,
|
||||
ValidatedErrorHelpBlock,
|
||||
ValidatedSubmitButton
|
||||
} from "../../../../../components/validation";
|
||||
import { nameLengthValidator } from "../../../../../components/validation/validators/text";
|
||||
import { updateTeam } from "../../../../../services/api/teams_api";
|
||||
|
||||
import { NAME_MAX_LENGTH } from "../../../../../config/constants/numeric";
|
||||
import { COLOR_APPLE_BLOSSOM } from "../../../../../config/constants/colors";
|
||||
|
||||
const StyledHelpBlock = styled(HelpBlock)`
|
||||
color: ${COLOR_APPLE_BLOSSOM};
|
||||
`;
|
||||
|
||||
type Team = {
|
||||
id: number,
|
||||
name: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
errorMessage: Node,
|
||||
name: string
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
showModal: boolean,
|
||||
|
@ -41,36 +32,19 @@ type Props = {
|
|||
class UpdateTeamNameModal extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
(this: any).state = { errorMessage: "", name: props.team.name };
|
||||
this.state = { name: props.team.name };
|
||||
(this: any).onCloseModal = this.onCloseModal.bind(this);
|
||||
(this: any).updateName = this.updateName.bind(this);
|
||||
(this: any).handleName = this.handleName.bind(this);
|
||||
(this: any).getValidationState = this.getValidationState.bind(this);
|
||||
}
|
||||
|
||||
onCloseModal(): void {
|
||||
(this: any).setState({ errorMessage: "", name: "" });
|
||||
(this: any).setState({ name: "" });
|
||||
this.props.hideModal();
|
||||
}
|
||||
|
||||
getValidationState(): string | null {
|
||||
return String(this.state.errorMessage).length > 0 ? "error" : null;
|
||||
}
|
||||
|
||||
handleName(el: SyntheticEvent<HTMLButtonElement>): void {
|
||||
const { value } = el.currentTarget;
|
||||
if (value.length > NAME_MAX_LENGTH) {
|
||||
(this: any).setState({
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: NAME_MAX_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.setState({ errorMessage: "", name: value });
|
||||
}
|
||||
handleName(e: SyntheticEvent<HTMLButtonElement>): void {
|
||||
(this: any).setState({ name: e.target.value });
|
||||
}
|
||||
|
||||
updateName(): void {
|
||||
|
@ -79,46 +53,49 @@ class UpdateTeamNameModal extends Component<Props, State> {
|
|||
this.props.updateTeamCallback(response);
|
||||
this.onCloseModal();
|
||||
})
|
||||
.catch(error => this.setState({ errorMessage: error.message }));
|
||||
.catch(error => {
|
||||
this.form.setErrorsForTag("name", [error.message]);
|
||||
});
|
||||
}
|
||||
|
||||
render(): Node {
|
||||
return (
|
||||
<Modal show={this.props.showModal} onHide={this.onCloseModal}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<FormattedMessage id="settings_page.update_team_name_modal.title" />
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<FormGroup
|
||||
controlId="teamName"
|
||||
validationState={this.getValidationState()}
|
||||
>
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.update_team_name_modal.label" />
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
type="text"
|
||||
onChange={this.handleName}
|
||||
value={this.state.name}
|
||||
/>
|
||||
<FormControl.Feedback />
|
||||
<StyledHelpBlock>{this.state.errorMessage}</StyledHelpBlock>
|
||||
</FormGroup>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
onClick={this.updateName}
|
||||
disabled={!_.isEmpty(this.state.errorMessage)}
|
||||
>
|
||||
<FormattedMessage id="general.update" />
|
||||
</Button>
|
||||
<Button onClick={this.onCloseModal}>
|
||||
<FormattedMessage id="general.close" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
<ValidatedForm
|
||||
ref={f => {
|
||||
this.form = f;
|
||||
}}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<FormattedMessage id="settings_page.update_team_name_modal.title" />
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<ValidatedFormGroup tag="name">
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.update_team_name_modal.label" />
|
||||
</ControlLabel>
|
||||
<ValidatedFormControl
|
||||
type="text"
|
||||
tag="name"
|
||||
validatorsOnChange={[nameLengthValidator]}
|
||||
onChange={this.handleName}
|
||||
value={this.state.name}
|
||||
/>
|
||||
<FormControl.Feedback />
|
||||
<ValidatedErrorHelpBlock tag="name" />
|
||||
</ValidatedFormGroup>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<ValidatedSubmitButton onClick={this.updateName} bsStyle="primary">
|
||||
<FormattedMessage id="general.update" />
|
||||
</ValidatedSubmitButton>
|
||||
<Button onClick={this.onCloseModal}>
|
||||
<FormattedMessage id="general.close" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</ValidatedForm>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React from "react";
|
||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||
import { FormControl } from "react-bootstrap";
|
||||
import { ValidatedFormControl } from "../../../../../../components/validation";
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: "settings_page.new_team.name_placeholder" }
|
||||
});
|
||||
|
||||
const NameFormControl = ({ intl, ...props }) =>
|
||||
<FormControl
|
||||
<ValidatedFormControl
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
autoFocus={true}
|
||||
|
|
|
@ -3,10 +3,8 @@ import React, { Component } from "react";
|
|||
import { connect } from "react-redux";
|
||||
import {
|
||||
Breadcrumb,
|
||||
FormGroup,
|
||||
FormControl,
|
||||
ControlLabel,
|
||||
HelpBlock,
|
||||
Button,
|
||||
ButtonToolbar
|
||||
} from "react-bootstrap";
|
||||
|
@ -22,13 +20,19 @@ import {
|
|||
SETTINGS_TEAMS_ROUTE,
|
||||
SETTINGS_TEAM_ROUTE
|
||||
} from "../../../../../config/routes";
|
||||
|
||||
import {
|
||||
NAME_MIN_LENGTH,
|
||||
NAME_MAX_LENGTH,
|
||||
TEXT_MAX_LENGTH
|
||||
} from "../../../../../config/constants/numeric";
|
||||
import { getTeamsList } from "../../../../../components/actions/TeamsActions";
|
||||
import {
|
||||
ValidatedForm,
|
||||
ValidatedFormGroup,
|
||||
ValidatedFormControl,
|
||||
ValidatedErrorHelpBlock,
|
||||
ValidatedSubmitButton
|
||||
} from "../../../../../components/validation";
|
||||
import {
|
||||
nameLengthValidator,
|
||||
textMaxLengthValidator
|
||||
|
||||
} from "../../../../../components/validation/validators/text";
|
||||
|
||||
import { BORDER_LIGHT_COLOR } from "../../../../../config/constants/colors";
|
||||
|
||||
|
@ -53,14 +57,8 @@ type Props = {
|
|||
getTeamsList: Function
|
||||
};
|
||||
|
||||
type FormErrors = {
|
||||
name: string,
|
||||
description: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
team: Teams$NewTeam,
|
||||
formErrors: FormErrors,
|
||||
redirectTo: string
|
||||
};
|
||||
|
||||
|
@ -72,15 +70,10 @@ class SettingsNewTeam extends Component<Props, State> {
|
|||
name: "",
|
||||
description: ""
|
||||
},
|
||||
formErrors: {
|
||||
name: "",
|
||||
description: ""
|
||||
},
|
||||
redirectTo: ""
|
||||
};
|
||||
|
||||
(this: any).onSubmit = this.onSubmit.bind(this);
|
||||
(this: any).validateField = this.validateField.bind(this);
|
||||
(this: any).handleChange = this.handleChange.bind(this);
|
||||
(this: any).renderTeamNameFormGroup = this.renderTeamNameFormGroup.bind(
|
||||
this
|
||||
|
@ -100,138 +93,62 @@ class SettingsNewTeam extends Component<Props, State> {
|
|||
.then(response => {
|
||||
// Redirect to the new team page
|
||||
this.props.getTeamsList();
|
||||
(this: any).newState = { ...this.state };
|
||||
(this: any).newState = update((this: any).newState, {
|
||||
|
||||
const newState = update((this: any).state, {
|
||||
redirectTo: {
|
||||
$set: SETTINGS_TEAM_ROUTE.replace(":id", response.details.id)
|
||||
}
|
||||
});
|
||||
(this: any).setState((this: any).newState);
|
||||
(this: any).setState(newState);
|
||||
})
|
||||
.catch(er => {
|
||||
// Display errors
|
||||
(this: any).newState = { ...this.state };
|
||||
["name", "description"].forEach(el => {
|
||||
if (er.response.data.details[el]) {
|
||||
(this: any).newState = update((this: any).newState, {
|
||||
formErrors: {
|
||||
name: { $set: <span>{er.response.data.details[el]}</span> }
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
(this: any).setState((this: any).newState);
|
||||
(this: any).newTeamForm.setErrors(er.response.data.details);
|
||||
});
|
||||
}
|
||||
|
||||
validateField(key: string, value: string) {
|
||||
let errorMessage;
|
||||
if (key === "name") {
|
||||
errorMessage = "";
|
||||
|
||||
if (value.length < NAME_MIN_LENGTH) {
|
||||
errorMessage = (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_short"
|
||||
values={{ min_length: NAME_MIN_LENGTH }}
|
||||
/>
|
||||
);
|
||||
} else if (value.length > NAME_MAX_LENGTH) {
|
||||
errorMessage = (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: NAME_MAX_LENGTH }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
(this: any).newState = update((this: any).newState, {
|
||||
formErrors: { name: { $set: errorMessage } }
|
||||
});
|
||||
} else if (key === "description") {
|
||||
errorMessage = "";
|
||||
|
||||
if (value.length > TEXT_MAX_LENGTH) {
|
||||
errorMessage = (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: TEXT_MAX_LENGTH }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
(this: any).newState = update((this: any).newState, {
|
||||
formErrors: { description: { $set: errorMessage } }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e: SyntheticInputEvent<HTMLInputElement>): void {
|
||||
const key = e.target.name;
|
||||
handleChange(e: SyntheticInputEvent<HTMLInputElement>, tag: string): void {
|
||||
const value = e.target.value;
|
||||
|
||||
(this: any).newState = { ...this.state };
|
||||
|
||||
// Update value in the state
|
||||
(this: any).newState = update((this: any).newState, {
|
||||
team: { [key]: { $set: value } }
|
||||
const newState = update((this: any).state, {
|
||||
team: { [tag]: { $set: value } }
|
||||
});
|
||||
|
||||
// Validate the input
|
||||
(this: any).validateField(key, value);
|
||||
|
||||
// Refresh state
|
||||
(this: any).setState((this: any).newState);
|
||||
(this: any).setState(newState);
|
||||
}
|
||||
|
||||
renderTeamNameFormGroup() {
|
||||
const formGroupClass = this.state.formErrors.name
|
||||
? "form-group has-error"
|
||||
: "form-group";
|
||||
const validationState = this.state.formErrors.name ? "error" : null;
|
||||
return (
|
||||
<FormGroup
|
||||
controlId="formTeamName"
|
||||
className={formGroupClass}
|
||||
validationState={validationState}
|
||||
>
|
||||
<ValidatedFormGroup tag="name">
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.new_team.name_label" />
|
||||
</ControlLabel>
|
||||
<NameFormControl
|
||||
value={this.state.team.name}
|
||||
onChange={this.handleChange}
|
||||
name="name"
|
||||
tag="name"
|
||||
validatorsOnChange={[nameLengthValidator]}
|
||||
onChange={(e) => this.handleChange(e, "name")}
|
||||
/>
|
||||
<FormControl.Feedback />
|
||||
<HelpBlock>{this.state.formErrors.name}</HelpBlock>
|
||||
</FormGroup>
|
||||
<ValidatedErrorHelpBlock tag="name" />
|
||||
</ValidatedFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderTeamDescriptionFormGroup() {
|
||||
const formGroupClass = this.state.formErrors.description
|
||||
? "form-group has-error"
|
||||
: "form-group";
|
||||
const validationState = this.state.formErrors.description ? "error" : null;
|
||||
return (
|
||||
<FormGroup
|
||||
controlId="formTeamDescription"
|
||||
className={formGroupClass}
|
||||
validationState={validationState}
|
||||
>
|
||||
<ValidatedFormGroup tag="description">
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.new_team.description_label" />
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
<ValidatedFormControl
|
||||
componentClass="textarea"
|
||||
value={this.state.team.description}
|
||||
onChange={this.handleChange}
|
||||
name="description"
|
||||
tag="description"
|
||||
validatorsOnChange={[textMaxLengthValidator]}
|
||||
onChange={(e) => this.handleChange(e, "description")}
|
||||
/>
|
||||
<FormControl.Feedback />
|
||||
<HelpBlock>{this.state.formErrors.description}</HelpBlock>
|
||||
</FormGroup>
|
||||
<ValidatedErrorHelpBlock tag="description" />
|
||||
</ValidatedFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -241,10 +158,6 @@ class SettingsNewTeam extends Component<Props, State> {
|
|||
return <Redirect to={this.state.redirectTo} />;
|
||||
}
|
||||
|
||||
const btnDisabled =
|
||||
!_.isEmpty(this.state.formErrors.name) ||
|
||||
!_.isEmpty(this.state.formErrors.description);
|
||||
|
||||
return (
|
||||
<PageTitle localeID="page_title.new_team_page">
|
||||
<Wrapper>
|
||||
|
@ -259,7 +172,11 @@ class SettingsNewTeam extends Component<Props, State> {
|
|||
</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
|
||||
<form onSubmit={this.onSubmit} style={{ maxWidth: "500px" }}>
|
||||
<ValidatedForm
|
||||
onSubmit={this.onSubmit}
|
||||
ref={(f) => { (this: any).newTeamForm = f; }}
|
||||
style={{ maxWidth: "500px" }}
|
||||
>
|
||||
<MyFormGroupDiv>
|
||||
{this.renderTeamNameFormGroup()}
|
||||
<small>
|
||||
|
@ -274,20 +191,19 @@ class SettingsNewTeam extends Component<Props, State> {
|
|||
</small>
|
||||
</MyFormGroupDiv>
|
||||
<ButtonToolbar>
|
||||
<Button
|
||||
<ValidatedSubmitButton
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
disabled={btnDisabled}
|
||||
>
|
||||
<FormattedMessage id="settings_page.new_team.create" />
|
||||
</Button>
|
||||
</ValidatedSubmitButton>
|
||||
<LinkContainer to={SETTINGS_TEAMS_ROUTE}>
|
||||
<Button>
|
||||
<FormattedMessage id="general.cancel" />
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</ButtonToolbar>
|
||||
</form>
|
||||
</ValidatedForm>
|
||||
</Wrapper>
|
||||
</PageTitle>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { axiosInstance } from "./config";
|
||||
import axiosInstance from "./config";
|
||||
import { ACTIVITIES_PATH } from "./endpoints";
|
||||
|
||||
export function getActivities(
|
||||
|
|
|
@ -3,7 +3,10 @@ import store from "../../config/store";
|
|||
import { SIGN_IN_PATH } from "../../config/routes";
|
||||
import { destroyState } from "../../components/actions/UsersActions";
|
||||
|
||||
export const axiosInstance = axios.create({
|
||||
// estimate time react component needs to rerender after state refresh
|
||||
const TimeDelay = 500;
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content
|
||||
|
@ -13,8 +16,33 @@ export const axiosInstance = axios.create({
|
|||
setTimeout(() => {
|
||||
store.dispatch(destroyState)
|
||||
window.location = SIGN_IN_PATH;
|
||||
}, 500);
|
||||
}, TimeDelay);
|
||||
}
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
});
|
||||
|
||||
// Set global IN_REQUEST variable needed for wait_for_ajax function in cucumber tests
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
window.IN_REQUEST = true;
|
||||
return config;
|
||||
}, (error) => {
|
||||
setTimeout(() => {
|
||||
window.IN_REQUEST = false;
|
||||
}, TimeDelay)
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
setTimeout(() => {
|
||||
window.IN_REQUEST = false;
|
||||
}, TimeDelay)
|
||||
return response;
|
||||
}, (error) => {
|
||||
setTimeout(() => {
|
||||
window.IN_REQUEST = false;
|
||||
}, TimeDelay)
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
export default axiosInstance;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { axiosInstance } from "./config";
|
||||
import axiosInstance from "./config";
|
||||
import {
|
||||
RECENT_NOTIFICATIONS_PATH,
|
||||
UNREADED_NOTIFICATIONS_PATH
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { axiosInstance } from "./config";
|
||||
import axiosInstance from "./config";
|
||||
import {
|
||||
REMOVE_USER_FROM_TEAM_PATH,
|
||||
INVITE_USERS_PATH,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { axiosInstance } from "./config";
|
||||
import axiosInstance from "./config";
|
||||
import {
|
||||
USER_PROFILE_INFO,
|
||||
UPDATE_USER_PATH,
|
||||
|
|
|
@ -96,26 +96,27 @@ class Experiment < ApplicationRecord
|
|||
end
|
||||
|
||||
def modules_without_group
|
||||
MyModule.where(experiment_id: id).where(my_module_group: nil)
|
||||
.where(archived: false)
|
||||
MyModule.where(experiment_id: id)
|
||||
.where(my_module_group: nil)
|
||||
.where(archived: false)
|
||||
end
|
||||
|
||||
def active_module_groups
|
||||
self.my_module_groups.joins(:my_modules)
|
||||
.where('my_modules.archived = ?', false)
|
||||
.distinct
|
||||
my_module_groups.joins(:my_modules)
|
||||
.where('my_modules.archived = ?', false)
|
||||
.distinct
|
||||
end
|
||||
|
||||
def active_modules
|
||||
my_modules.where(:archived => false)
|
||||
my_modules.where(archived: false)
|
||||
end
|
||||
|
||||
def archived_modules
|
||||
my_modules.where(:archived => true)
|
||||
my_modules.where(archived: true)
|
||||
end
|
||||
|
||||
def assigned_samples
|
||||
Sample.joins(:my_modules).where(my_modules: {id: my_modules} )
|
||||
Sample.joins(:my_modules).where(my_modules: { id: my_modules })
|
||||
end
|
||||
|
||||
def unassigned_samples(assigned_samples)
|
||||
|
@ -131,15 +132,15 @@ class Experiment < ApplicationRecord
|
|||
to_clone,
|
||||
connections,
|
||||
positions,
|
||||
current_user,
|
||||
module_groups
|
||||
current_user
|
||||
)
|
||||
cloned_modules = []
|
||||
begin
|
||||
Experiment.transaction do
|
||||
with_lock do
|
||||
# First, add new modules
|
||||
new_ids, cloned_pairs, originals = add_modules(
|
||||
to_add, to_clone, current_user)
|
||||
to_add, to_clone, current_user
|
||||
)
|
||||
cloned_modules = cloned_pairs.collect { |mn, _| mn }
|
||||
|
||||
# Rename modules
|
||||
|
@ -147,39 +148,31 @@ class Experiment < ApplicationRecord
|
|||
|
||||
# Add activities that modules were created
|
||||
originals.each do |m|
|
||||
Activity.create(
|
||||
type_of: :create_module,
|
||||
Activity.create(type_of: :create_module,
|
||||
user: current_user,
|
||||
project: self.project,
|
||||
project: project,
|
||||
experiment: m.experiment,
|
||||
my_module: m,
|
||||
message: I18n.t(
|
||||
"activities.create_module",
|
||||
user: current_user.full_name,
|
||||
module: m.name
|
||||
)
|
||||
)
|
||||
message: I18n.t('activities.create_module',
|
||||
user: current_user.full_name,
|
||||
module: m.name))
|
||||
end
|
||||
|
||||
# Add activities that modules were cloned
|
||||
cloned_pairs.each do |mn, mo|
|
||||
Activity.create(
|
||||
type_of: :clone_module,
|
||||
Activity.create(type_of: :clone_module,
|
||||
project: mn.experiment.project,
|
||||
experiment: mn.experiment,
|
||||
my_module: mn,
|
||||
user: current_user,
|
||||
message: I18n.t(
|
||||
"activities.clone_module",
|
||||
user: current_user.full_name,
|
||||
module_new: mn.name,
|
||||
module_original: mo.name
|
||||
)
|
||||
)
|
||||
message: I18n.t('activities.clone_module',
|
||||
user: current_user.full_name,
|
||||
module_new: mn.name,
|
||||
module_original: mo.name))
|
||||
end
|
||||
|
||||
# Then, archive modules that need to be archived
|
||||
archive_modules(to_archive, current_user)
|
||||
archive_modules(to_archive, current_user) if to_archive.any?
|
||||
|
||||
# Update connections, positions & module group variables
|
||||
# with actual IDs retrieved from the new modules creation
|
||||
|
@ -196,17 +189,13 @@ class Experiment < ApplicationRecord
|
|||
updated_to_move_groups[mapped] = value
|
||||
end
|
||||
updated_connections = []
|
||||
connections.each do |a,b|
|
||||
connections.each do |a, b|
|
||||
updated_connections << [new_ids.fetch(a, a), new_ids.fetch(b, b)]
|
||||
end
|
||||
updated_positions = Hash.new
|
||||
updated_positions = {}
|
||||
positions.each do |id, pos|
|
||||
updated_positions[new_ids.fetch(id, id)] = pos
|
||||
end
|
||||
updated_module_groups = {}
|
||||
module_groups.each do |id, name|
|
||||
updated_module_groups[new_ids.fetch(id, id)] = name
|
||||
end
|
||||
|
||||
# Update connections
|
||||
update_module_connections(updated_connections)
|
||||
|
@ -218,7 +207,7 @@ class Experiment < ApplicationRecord
|
|||
normalize_module_positions
|
||||
|
||||
# Finally, update module groups
|
||||
update_module_groups(updated_module_groups, current_user)
|
||||
update_module_groups(current_user)
|
||||
|
||||
# Finally move any modules to another experiment
|
||||
move_modules(updated_to_move)
|
||||
|
@ -226,11 +215,13 @@ class Experiment < ApplicationRecord
|
|||
# Everyhing is set, now we can move any module groups
|
||||
move_module_groups(updated_to_move_groups)
|
||||
end
|
||||
rescue ActiveRecord::ActiveRecordError, ArgumentError, ActiveRecord::RecordNotSaved
|
||||
rescue ActiveRecord::ActiveRecordError,
|
||||
ArgumentError,
|
||||
ActiveRecord::RecordNotSaved => ex
|
||||
logger.error ex.message
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
true
|
||||
end
|
||||
|
||||
# This method generate the workflow image and saves it as
|
||||
|
@ -431,22 +422,15 @@ class Experiment < ApplicationRecord
|
|||
|
||||
# Archive all modules. Receives an array of module integer IDs.
|
||||
def archive_modules(module_ids)
|
||||
module_ids.each do |m_id|
|
||||
my_module = self.my_modules.find_by_id(m_id)
|
||||
unless my_module.blank?
|
||||
my_module.archive!
|
||||
end
|
||||
end
|
||||
modules.reload
|
||||
my_modules.where(id: module_ids).each(&:archive!)
|
||||
my_modules.reload
|
||||
end
|
||||
|
||||
# Archive all modules. Receives an array of module integer IDs and current user.
|
||||
# Archive all modules. Receives an array of module integer IDs
|
||||
# and current user.
|
||||
def archive_modules(module_ids, current_user)
|
||||
module_ids.each do |m_id|
|
||||
my_module = self.my_modules.find_by_id(m_id)
|
||||
unless my_module.blank?
|
||||
my_module.archive!(current_user)
|
||||
end
|
||||
my_modules.where(id: module_ids).each do |m|
|
||||
m.archive!(current_user)
|
||||
end
|
||||
my_modules.reload
|
||||
end
|
||||
|
@ -460,15 +444,14 @@ class Experiment < ApplicationRecord
|
|||
def add_modules(to_add, to_clone, current_user)
|
||||
originals = []
|
||||
cloned_pairs = {}
|
||||
ids_map = Hash.new
|
||||
ids_map = {}
|
||||
to_add.each do |m|
|
||||
original = MyModule.find_by_id(to_clone.fetch(m[:id], nil))
|
||||
if original.present? then
|
||||
if original.present?
|
||||
my_module = original.deep_clone(current_user)
|
||||
cloned_pairs[my_module] = original
|
||||
else
|
||||
my_module = MyModule.new(
|
||||
experiment: self)
|
||||
my_module = MyModule.new(experiment: self)
|
||||
originals << my_module
|
||||
end
|
||||
|
||||
|
@ -537,7 +520,7 @@ class Experiment < ApplicationRecord
|
|||
# to bottom left corner.
|
||||
def move_module_groups(to_move)
|
||||
to_move.each do |ids, experiment_id|
|
||||
modules = my_modules.where(id: ids)
|
||||
modules = my_modules.find(ids)
|
||||
groups = Set.new(modules.map(&:my_module_group))
|
||||
experiment = project.experiments.find_by_id(experiment_id)
|
||||
|
||||
|
@ -577,10 +560,8 @@ class Experiment < ApplicationRecord
|
|||
# Generates workflow img when the workflow or module is moved
|
||||
# to other experiment
|
||||
def generate_workflow_img_for_moved_modules(to_move)
|
||||
to_move.values.uniq.each do |id|
|
||||
experiment = Experiment.find_by_id(id)
|
||||
next unless experiment
|
||||
experiment.delay.generate_workflow_img
|
||||
Experiment.where(id: to_move.values.uniq).each do |exp|
|
||||
exp.delay.generate_workflow_img
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -592,42 +573,39 @@ class Experiment < ApplicationRecord
|
|||
require 'rgl/base'
|
||||
require 'rgl/adjacency'
|
||||
require 'rgl/topsort'
|
||||
|
||||
dg = RGL::DirectedAdjacencyGraph.new
|
||||
connections.each do |a,b|
|
||||
connections.each do |a, b|
|
||||
# Check if both vertices exist
|
||||
if (my_modules.find_all {|m| [a.to_i, b.to_i].include? m.id }).count == 2
|
||||
if (my_modules.find_all { |m| [a.to_i, b.to_i].include? m.id }).count == 2
|
||||
dg.add_edge(a, b)
|
||||
end
|
||||
end
|
||||
|
||||
# Check if cycles exist!
|
||||
topsort = dg.topsort_iterator.to_a
|
||||
if topsort.length == 0 and dg.edges.size > 1
|
||||
raise ArgumentError, "Cycles exist."
|
||||
if topsort.length.zero? && dg.edges.size > 1
|
||||
raise ArgumentError, 'Cycles exist.'
|
||||
end
|
||||
|
||||
# First, delete existing connections
|
||||
# but keep a copy of previous state
|
||||
previous_sources = {}
|
||||
previous_sources.default = []
|
||||
my_modules.each do |m|
|
||||
|
||||
my_modules.includes(inputs: { from: [:inputs, outputs: :to] }).each do |m|
|
||||
previous_sources[m.id] = []
|
||||
m.inputs.each do |c|
|
||||
previous_sources[m.id] << c.from
|
||||
end
|
||||
end
|
||||
self.my_modules.each do |m|
|
||||
unless m.outputs.destroy_all
|
||||
raise ActiveRecord::ActiveRecordError
|
||||
end
|
||||
end
|
||||
|
||||
# There are no callbacks in Connection, so delete_all should be safe
|
||||
Connection.delete_all(output_id: my_modules)
|
||||
|
||||
# Add new connections
|
||||
filtered_edges = dg.edges.collect { |e| [e.source, e.target] }
|
||||
filtered_edges.each do |a, b|
|
||||
Connection.create!(:input_id => b, :output_id => a)
|
||||
Connection.create!(input_id: b, output_id: a)
|
||||
end
|
||||
|
||||
# Unassign samples from former downstream modules
|
||||
|
@ -637,47 +615,49 @@ class Experiment < ApplicationRecord
|
|||
visited = []
|
||||
# Assign samples to all new downstream modules
|
||||
filtered_edges.each do |a, b|
|
||||
source = self.my_modules.find(a.to_i)
|
||||
target = self.my_modules.find(b.to_i)
|
||||
source = my_modules.includes({ inputs: :from }, :samples).find(a.to_i)
|
||||
target = my_modules.find(b.to_i)
|
||||
# Do this only for new edges
|
||||
if previous_sources[target.id].exclude?(source)
|
||||
# Go as high upstream as new edges take us
|
||||
# and then assign samples to all downsteam samples
|
||||
assign_samples_to_new_downstream_modules(previous_sources, visited, source)
|
||||
end
|
||||
next unless previous_sources[target.id].exclude?(source)
|
||||
# Go as high upstream as new edges take us
|
||||
# and then assign samples to all downsteam samples
|
||||
assign_samples_to_new_downstream_modules(previous_sources,
|
||||
visited,
|
||||
source)
|
||||
end
|
||||
|
||||
# Save topological order of modules (for modules without workflow,
|
||||
# leave them unordered)
|
||||
self.my_modules.each do |m|
|
||||
if topsort.include? m.id.to_s
|
||||
m.workflow_order = topsort.find_index(m.id.to_s)
|
||||
else
|
||||
m.workflow_order = -1
|
||||
end
|
||||
my_modules.includes(:my_module_group).each do |m|
|
||||
m.workflow_order =
|
||||
if topsort.include? m.id.to_s
|
||||
topsort.find_index(m.id.to_s)
|
||||
else
|
||||
-1
|
||||
end
|
||||
m.save!
|
||||
end
|
||||
|
||||
# Make sure to reload my modules, which now have updated connections and samples
|
||||
self.my_modules.reload
|
||||
# Make sure to reload my modules, which now have updated connections
|
||||
# and samples
|
||||
my_modules.reload
|
||||
true
|
||||
end
|
||||
|
||||
# When connections are deleted, unassign samples that
|
||||
# are not inherited anymore
|
||||
def unassign_samples_from_old_downstream_modules(sources)
|
||||
self.my_modules.each do |my_module|
|
||||
sources[my_module.id].each do |s|
|
||||
my_modules.each do |my_module|
|
||||
sources[my_module.id].each do |src|
|
||||
# Only do this for newly deleted connections
|
||||
if s.outputs.map{|i| i.to}.exclude? my_module
|
||||
my_module.get_downstream_modules.each do |dm|
|
||||
# Get unique samples for all upstream modules
|
||||
um = dm.get_upstream_modules
|
||||
um.shift # remove current module
|
||||
ums = um.map{|m| m.samples}.flatten.uniq
|
||||
s.samples.each do |sample|
|
||||
dm.samples.destroy(sample) if ums.exclude? sample
|
||||
end
|
||||
next unless src.outputs.map(&:to).exclude? my_module
|
||||
my_module.downstream_modules.each do |dm|
|
||||
# Get unique samples for all upstream modules
|
||||
um = dm.upstream_modules
|
||||
um.shift # remove current module
|
||||
ums = um.map(&:samples).flatten.uniq
|
||||
src.samples.find_each do |sample|
|
||||
dm.samples.destroy(sample) if ums.exclude? sample
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -687,24 +667,21 @@ class Experiment < ApplicationRecord
|
|||
# Assign samples to new connections recursively
|
||||
def assign_samples_to_new_downstream_modules(sources, visited, my_module)
|
||||
# If samples are already assigned for this module, stop going upstream
|
||||
if visited.include? (my_module)
|
||||
return
|
||||
end
|
||||
return if visited.include?(my_module)
|
||||
visited << my_module
|
||||
# Edge case, when module is source or it doesn't have any new input connections
|
||||
if my_module.inputs.blank? or (
|
||||
my_module.inputs.map{|c| c.from} -
|
||||
sources[my_module.id]
|
||||
).empty?
|
||||
my_module.get_downstream_modules.each do |dm|
|
||||
new_samples = my_module.samples.select { |el| dm.samples.exclude?(el) }
|
||||
dm.samples.push(*new_samples)
|
||||
# Edge case, when module is source or it doesn't have any new input
|
||||
# connections
|
||||
if my_module.inputs.blank? ||
|
||||
(my_module.inputs.map(&:from) - sources[my_module.id]).empty?
|
||||
my_module.downstream_modules.each do |dm|
|
||||
new_samples = my_module.samples.where.not(id: dm.samples)
|
||||
dm.samples << new_samples
|
||||
end
|
||||
else
|
||||
my_module.inputs.each do |input|
|
||||
# Go upstream for new in connections
|
||||
if sources[my_module.id].exclude?(input.from)
|
||||
assign_samples_to_new_downstream_modules(input.from)
|
||||
assign_samples_to_new_downstream_modules(sources, visited, input.from)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -714,102 +691,58 @@ class Experiment < ApplicationRecord
|
|||
# Input is a map where keys are module IDs, and values are
|
||||
# hashes like { x: <x>, y: <y> }.
|
||||
def update_module_positions(positions)
|
||||
positions.each do |id, pos|
|
||||
unless MyModule.update(id, x: pos[:x], y: pos[:y])
|
||||
raise ActiveRecord::ActiveRecordError
|
||||
end
|
||||
modules = my_modules.where(id: positions.keys)
|
||||
modules.each do |m|
|
||||
m.update_columns(x: positions[m.id.to_s][:x], y: positions[m.id.to_s][:y])
|
||||
end
|
||||
self.my_modules.reload
|
||||
my_modules.reload
|
||||
end
|
||||
|
||||
# Normalize module positions in this project.
|
||||
def normalize_module_positions
|
||||
# This method normalizes module positions so x-s and y-s
|
||||
# are all positive
|
||||
x_diff = (self.my_modules.collect { |m| m.x }).min
|
||||
y_diff = (self.my_modules.collect { |m| m.y }).min
|
||||
x_diff = my_modules.pluck(:x).min
|
||||
y_diff = my_modules.pluck(:y).min
|
||||
|
||||
self.my_modules.each do |m|
|
||||
unless
|
||||
m.update_attribute(:x, m.x - x_diff) and
|
||||
m.update_attribute(:y, m.y - y_diff)
|
||||
raise ActiveRecord::ActiveRecordError
|
||||
end
|
||||
my_modules.each do |m|
|
||||
m.update_columns(x: m.x - x_diff, y: m.y - y_diff)
|
||||
end
|
||||
end
|
||||
|
||||
# Recalculate module groups in this project. Input is
|
||||
# a hash of module ids and their corresponding module names.
|
||||
def update_module_groups(module_groups, current_user)
|
||||
def update_module_groups(current_user)
|
||||
require 'rgl/base'
|
||||
require 'rgl/adjacency'
|
||||
require 'rgl/connected_components'
|
||||
|
||||
dg = RGL::DirectedAdjacencyGraph[]
|
||||
group_ids = Set.new
|
||||
active_modules.each do |m|
|
||||
unless m.my_module_group.blank?
|
||||
group_ids << m.my_module_group.id
|
||||
end
|
||||
unless dg.has_vertex? m.id
|
||||
dg.add_vertex m.id
|
||||
end
|
||||
active_modules.includes(:my_module_group, outputs: :to).each do |m|
|
||||
group_ids << m.my_module_group.id unless m.my_module_group.blank?
|
||||
dg.add_vertex m.id unless dg.has_vertex? m.id
|
||||
m.outputs.each do |o|
|
||||
dg.add_edge m.id, o.to.id
|
||||
end
|
||||
end
|
||||
workflows = []
|
||||
dg.to_undirected.each_connected_component { |w| workflows << w }
|
||||
|
||||
# For each workflow, generate new names
|
||||
new_index = 1
|
||||
wf_names = []
|
||||
suffix = I18n.t("my_module_groups.new.suffix")
|
||||
cut_index = -(suffix.length + 1)
|
||||
workflows.each do |w|
|
||||
modules = MyModule.find(w)
|
||||
|
||||
# Get an array of module names
|
||||
names = []
|
||||
modules.each do |m|
|
||||
names << module_groups.fetch(m.id.to_s, "")
|
||||
end
|
||||
names = names.uniq
|
||||
name = (names.select { |v| v != "" }).join(", ")
|
||||
|
||||
if w.length <= 1
|
||||
name = nil
|
||||
elsif name.blank?
|
||||
name = I18n.t("my_module_groups.new.name", index: new_index)
|
||||
new_index += 1
|
||||
while MyModuleGroup.find_by(name: name).present?
|
||||
name = I18n.t("my_module_groups.new.name", index: new_index)
|
||||
new_index += 1
|
||||
end
|
||||
elsif name.length > Constants::NAME_MAX_LENGTH
|
||||
# If length is too long, shorten it
|
||||
name = name[0..(Constants::NAME_MAX_LENGTH + cut_index)] + suffix
|
||||
end
|
||||
|
||||
wf_names << name
|
||||
dg.to_undirected.each_connected_component do |w|
|
||||
workflows << my_modules.find(w)
|
||||
end
|
||||
|
||||
# Remove any existing module groups from modules
|
||||
unless MyModuleGroup.where(id: group_ids.to_a).destroy_all
|
||||
unless MyModuleGroup.destroy_all(id: group_ids.to_a)
|
||||
raise ActiveRecord::ActiveRecordError
|
||||
end
|
||||
|
||||
# Second, create new groups
|
||||
workflows.each_with_index do |w, i|
|
||||
workflows.each do |modules|
|
||||
# Single modules are not considered part of any workflow
|
||||
if w.length > 1
|
||||
group = MyModuleGroup.new(
|
||||
name: wf_names[i],
|
||||
experiment: self,
|
||||
my_modules: MyModule.find(w))
|
||||
group.created_by = current_user
|
||||
group.save!
|
||||
end
|
||||
next unless modules.length > 1
|
||||
MyModuleGroup.create!(experiment: self,
|
||||
my_modules: modules,
|
||||
created_by: current_user)
|
||||
end
|
||||
|
||||
my_module_groups.reload
|
||||
|
|
|
@ -278,28 +278,24 @@ class MyModule < ApplicationRecord
|
|||
end
|
||||
|
||||
# Treat this module as root, get all modules of that subtree
|
||||
def get_downstream_modules
|
||||
def downstream_modules
|
||||
final = []
|
||||
modules = [self]
|
||||
while !modules.empty?
|
||||
until modules.empty?
|
||||
my_module = modules.shift
|
||||
if !final.include?(my_module)
|
||||
final << my_module
|
||||
end
|
||||
final << my_module unless final.include?(my_module)
|
||||
modules.push(*my_module.my_modules)
|
||||
end
|
||||
final
|
||||
end
|
||||
|
||||
# Treat this module as inversed root, get all modules of that inversed subtree
|
||||
def get_upstream_modules
|
||||
def upstream_modules
|
||||
final = []
|
||||
modules = [self]
|
||||
while !modules.empty?
|
||||
until modules.empty?
|
||||
my_module = modules.shift
|
||||
if !final.include?(my_module)
|
||||
final << my_module
|
||||
end
|
||||
final << my_module unless final.include?(my_module)
|
||||
modules.push(*my_module.my_module_antecessors)
|
||||
end
|
||||
final
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
class MyModuleGroup < ApplicationRecord
|
||||
include SearchableModel
|
||||
|
||||
auto_strip_attributes :name, nullify: false
|
||||
validates :name,
|
||||
presence: true,
|
||||
length: { maximum: Constants::NAME_MAX_LENGTH }
|
||||
validates :experiment, presence: true
|
||||
|
||||
belongs_to :experiment, inverse_of: :my_module_groups, optional: true
|
||||
|
@ -14,39 +10,12 @@ class MyModuleGroup < ApplicationRecord
|
|||
optional: true
|
||||
has_many :my_modules, inverse_of: :my_module_group, dependent: :nullify
|
||||
|
||||
def self.search(user,
|
||||
include_archived,
|
||||
query = nil,
|
||||
page = 1,
|
||||
_current_team = nil,
|
||||
options = {})
|
||||
exp_ids =
|
||||
Experiment
|
||||
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
|
||||
.pluck(:id)
|
||||
|
||||
new_query = MyModuleGroup
|
||||
.distinct
|
||||
.where('my_module_groups.experiment_id IN (?)', exp_ids)
|
||||
.where_attributes_like('my_module_groups.name', query, options)
|
||||
|
||||
# Show all results if needed
|
||||
if page == Constants::SEARCH_NO_LIMIT
|
||||
new_query
|
||||
else
|
||||
new_query
|
||||
.limit(Constants::SEARCH_LIMIT)
|
||||
.offset((page - 1) * Constants::SEARCH_LIMIT)
|
||||
end
|
||||
end
|
||||
|
||||
def ordered_modules
|
||||
my_modules.order(workflow_order: :asc)
|
||||
end
|
||||
|
||||
def deep_clone_to_experiment(current_user, experiment)
|
||||
clone = MyModuleGroup.new(
|
||||
name: name,
|
||||
created_by: created_by,
|
||||
experiment: experiment
|
||||
)
|
||||
|
|
|
@ -60,17 +60,6 @@ class Repository < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def open_spreadsheet(file)
|
||||
filename = file.original_filename
|
||||
file_path = file.path
|
||||
|
||||
if file.class == Paperclip::Attachment && file.is_stored_on_s3?
|
||||
fa = file.fetch
|
||||
file_path = fa.path
|
||||
end
|
||||
generate_file(filename, file_path)
|
||||
end
|
||||
|
||||
def available_repository_fields
|
||||
fields = {}
|
||||
# First and foremost add record name
|
||||
|
@ -116,6 +105,7 @@ class Repository < ApplicationRecord
|
|||
name_index = -1
|
||||
total_nr = 0
|
||||
nr_of_added = 0
|
||||
header_skipped = false
|
||||
|
||||
mappings.each.with_index do |(_k, value), index|
|
||||
if value == '-1'
|
||||
|
@ -132,54 +122,63 @@ class Repository < ApplicationRecord
|
|||
unless col_compact.map(&:id).uniq.length == col_compact.length
|
||||
return { status: :error, nr_of_added: nr_of_added, total_nr: total_nr }
|
||||
end
|
||||
rows = SpreadsheetParser.spreadsheet_enumerator(sheet)
|
||||
|
||||
# Now we can iterate through record data and save stuff into db
|
||||
transaction do
|
||||
(2..sheet.last_row).each do |i|
|
||||
total_nr += 1
|
||||
record_row = RepositoryRow.new(name: sheet.row(i)[name_index],
|
||||
repository: self,
|
||||
created_by: user,
|
||||
last_modified_by: user)
|
||||
record_row.transaction(requires_new: true) do
|
||||
unless record_row.save
|
||||
errors = true
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
rows.each do |row|
|
||||
# Skip empty rows
|
||||
next if row.empty?
|
||||
unless header_skipped
|
||||
header_skipped = true
|
||||
next
|
||||
end
|
||||
total_nr += 1
|
||||
|
||||
row_cell_values = []
|
||||
# Creek XLSX parser returns Hash of the row, Roo - Array
|
||||
row = row.is_a?(Hash) ? row.values.map(&:to_s) : row.map(&:to_s)
|
||||
|
||||
sheet.row(i).each.with_index do |value, index|
|
||||
if columns[index] && value
|
||||
cell_value = RepositoryTextValue.new(
|
||||
data: value,
|
||||
created_by: user,
|
||||
last_modified_by: user,
|
||||
repository_cell_attributes: {
|
||||
repository_row: record_row,
|
||||
repository_column: columns[index]
|
||||
}
|
||||
)
|
||||
cell = RepositoryCell.new(repository_row: record_row,
|
||||
repository_column: columns[index],
|
||||
value: cell_value)
|
||||
cell.skip_on_import = true
|
||||
cell_value.repository_cell = cell
|
||||
unless cell.valid? && cell_value.valid?
|
||||
errors = true
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
row_cell_values << cell_value
|
||||
end
|
||||
end
|
||||
if RepositoryTextValue.import(row_cell_values,
|
||||
recursive: true,
|
||||
validate: false).failed_instances.any?
|
||||
errors = true
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
nr_of_added += 1
|
||||
record_row = RepositoryRow.new(name: row[name_index],
|
||||
repository: self,
|
||||
created_by: user,
|
||||
last_modified_by: user)
|
||||
record_row.transaction do
|
||||
unless record_row.save
|
||||
errors = true
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
||||
row_cell_values = []
|
||||
|
||||
row.each.with_index do |value, index|
|
||||
if columns[index] && value
|
||||
cell_value = RepositoryTextValue.new(
|
||||
data: value,
|
||||
created_by: user,
|
||||
last_modified_by: user,
|
||||
repository_cell_attributes: {
|
||||
repository_row: record_row,
|
||||
repository_column: columns[index]
|
||||
}
|
||||
)
|
||||
cell = RepositoryCell.new(repository_row: record_row,
|
||||
repository_column: columns[index],
|
||||
value: cell_value)
|
||||
cell.skip_on_import = true
|
||||
cell_value.repository_cell = cell
|
||||
unless cell.valid? && cell_value.valid?
|
||||
errors = true
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
row_cell_values << cell_value
|
||||
end
|
||||
end
|
||||
if RepositoryTextValue.import(row_cell_values,
|
||||
recursive: true,
|
||||
validate: false).failed_instances.any?
|
||||
errors = true
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
nr_of_added += 1
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -188,22 +187,4 @@ class Repository < ApplicationRecord
|
|||
end
|
||||
{ status: :ok, nr_of_added: nr_of_added, total_nr: total_nr }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_file(filename, file_path)
|
||||
case File.extname(filename)
|
||||
when '.csv'
|
||||
Roo::CSV.new(file_path, extension: :csv)
|
||||
when '.tsv'
|
||||
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
|
||||
when '.txt'
|
||||
# This assumption is based purely on biologist's habits
|
||||
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
|
||||
when '.xlsx'
|
||||
Roo::Excelx.new(file_path)
|
||||
else
|
||||
raise TypeError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,30 +32,6 @@ class Team < ApplicationRecord
|
|||
has_many :protocol_keywords, inverse_of: :team, dependent: :destroy
|
||||
has_many :tiny_mce_assets, inverse_of: :team, dependent: :destroy
|
||||
has_many :repositories, dependent: :destroy
|
||||
# Based on file's extension opens file (used for importing)
|
||||
def self.open_spreadsheet(file)
|
||||
filename = file.original_filename
|
||||
file_path = file.path
|
||||
|
||||
if file.class == Paperclip::Attachment and file.is_stored_on_s3?
|
||||
fa = file.fetch
|
||||
file_path = fa.path
|
||||
end
|
||||
|
||||
case File.extname(filename)
|
||||
when '.csv' then
|
||||
Roo::CSV.new(file_path, extension: :csv)
|
||||
when '.tsv' then
|
||||
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
|
||||
when '.txt' then
|
||||
# This assumption is based purely on biologist's habits
|
||||
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
|
||||
when '.xlsx' then
|
||||
Roo::Excelx.new(file_path)
|
||||
else
|
||||
raise TypeError
|
||||
end
|
||||
end
|
||||
|
||||
def search_users(query = nil)
|
||||
a_query = "%#{query}%"
|
||||
|
@ -72,6 +48,7 @@ class Team < ApplicationRecord
|
|||
errors = false
|
||||
nr_of_added = 0
|
||||
total_nr = 0
|
||||
header_skipped = false
|
||||
|
||||
# First let's query for all custom_fields we're refering to
|
||||
custom_fields = []
|
||||
|
@ -97,10 +74,22 @@ class Team < ApplicationRecord
|
|||
custom_fields << cf
|
||||
end
|
||||
end
|
||||
|
||||
rows = SpreadsheetParser.spreadsheet_enumerator(sheet)
|
||||
|
||||
# Now we can iterate through sample data and save stuff into db
|
||||
(2..sheet.last_row).each do |i|
|
||||
rows.each do |row|
|
||||
# Skip empty rows
|
||||
next if row.empty?
|
||||
unless header_skipped
|
||||
header_skipped = true
|
||||
next
|
||||
end
|
||||
total_nr += 1
|
||||
sample = Sample.new(name: sheet.row(i)[sname_index],
|
||||
# Creek XLSX parser returns Hash of the row, Roo - Array
|
||||
row = row.is_a?(Hash) ? row.values.map(&:to_s) : row.map(&:to_s)
|
||||
|
||||
sample = Sample.new(name: row[sname_index],
|
||||
team: self,
|
||||
user: user)
|
||||
|
||||
|
@ -110,12 +99,14 @@ class Team < ApplicationRecord
|
|||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
||||
sheet.row(i).each.with_index do |value, index|
|
||||
row.each.with_index do |value, index|
|
||||
if index == stype_index
|
||||
stype = SampleType.where(name: value.strip, team: self).take
|
||||
stype = SampleType.where(team: self)
|
||||
.where('name ILIKE ?', value.strip)
|
||||
.take
|
||||
|
||||
unless stype
|
||||
stype = SampleType.new(name: value, team: self)
|
||||
stype = SampleType.new(name: value.strip, team: self)
|
||||
unless stype.save
|
||||
errors = true
|
||||
raise ActiveRecord::Rollback
|
||||
|
@ -123,10 +114,12 @@ class Team < ApplicationRecord
|
|||
end
|
||||
sample.sample_type = stype
|
||||
elsif index == sgroup_index
|
||||
sgroup = SampleGroup.where(name: value.strip, team: self).take
|
||||
sgroup = SampleGroup.where(team: self)
|
||||
.where('name ILIKE ?', value.strip)
|
||||
.take
|
||||
|
||||
unless sgroup
|
||||
sgroup = SampleGroup.new(name: value, team: self)
|
||||
sgroup = SampleGroup.new(name: value.strip, team: self)
|
||||
unless sgroup.save
|
||||
errors = true
|
||||
raise ActiveRecord::Rollback
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
module ClientApi
|
||||
class UserService < BaseService
|
||||
def update_user!
|
||||
error = I18n.t('client_api.user.password_invalid')
|
||||
raise CustomUserError, error unless check_current_password
|
||||
@params.delete(:current_password) # removes unneeded element
|
||||
@current_user.update(@params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_current_password
|
||||
return true unless @params[:email] || @params[:password]
|
||||
pass_blank_err = I18n.t('client_api.user.blank_password_error')
|
||||
pass_match_err = I18n.t('client_api.user.passwords_dont_match')
|
||||
current_password = @params[:current_password]
|
||||
raise CustomUserError, pass_blank_err unless current_password
|
||||
raise CustomUserError, pass_match_err unless check_password_confirmation
|
||||
@current_user.valid_password? current_password
|
||||
end
|
||||
|
||||
def check_password_confirmation
|
||||
return true if @params[:email]
|
||||
@params[:password] == @params[:password_confirmation]
|
||||
end
|
||||
end
|
||||
CustomUserError = Class.new(StandardError)
|
||||
end
|
53
app/services/client_api/users/update_service.rb
Normal file
53
app/services/client_api/users/update_service.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
module ClientApi
|
||||
module Users
|
||||
class UpdateService < BaseService
|
||||
attr_accessor :user
|
||||
|
||||
def execute
|
||||
@user = @current_user
|
||||
|
||||
if current_password_valid? &&
|
||||
password_confirmation_valid? &&
|
||||
@user.update(@params.except(:current_password))
|
||||
success
|
||||
else
|
||||
error(@user.errors.full_messages.uniq.join('. '))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_password_valid?
|
||||
# Only check for current_password when updating
|
||||
# email or password
|
||||
return true unless @params[:email] || @params[:password]
|
||||
|
||||
if @user.valid_password?(@params[:current_password])
|
||||
return true
|
||||
else
|
||||
@user.errors.add(
|
||||
:current_password,
|
||||
I18n.t('client_api.user.current_password_invalid')
|
||||
)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def password_confirmation_valid?
|
||||
# Only check for password_confirmation when
|
||||
# updating password
|
||||
return true unless @params[:password]
|
||||
|
||||
if @params[:password] == @params[:password_confirmation]
|
||||
return true
|
||||
else
|
||||
@user.errors.add(
|
||||
:password_confirmation,
|
||||
I18n.t('client_api.user.password_confirmation_not_match')
|
||||
)
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,9 +17,11 @@ module ImportRepository
|
|||
private
|
||||
|
||||
def run_import_actions
|
||||
@repository.import_records(@repository.open_spreadsheet(@temp_file.file),
|
||||
@mappings,
|
||||
@user)
|
||||
@repository.import_records(
|
||||
SpreadsheetParser.open_spreadsheet(@temp_file.file),
|
||||
@mappings,
|
||||
@user
|
||||
)
|
||||
end
|
||||
|
||||
def run_checks
|
||||
|
|
|
@ -5,48 +5,40 @@ module ImportRepository
|
|||
@file = options.fetch(:file)
|
||||
@repository = options.fetch(:repository)
|
||||
@session = options.fetch(:session)
|
||||
@sheet = @repository.open_spreadsheet(@file)
|
||||
@sheet = SpreadsheetParser.open_spreadsheet(@file)
|
||||
end
|
||||
|
||||
def data
|
||||
# Get data (it will trigger any errors as well)
|
||||
header = @sheet.row(1)
|
||||
columns = @sheet.row(2)
|
||||
header, columns = SpreadsheetParser.first_two_rows(@sheet)
|
||||
# Fill in fields for dropdown
|
||||
@repository.available_repository_fields.transform_values! do |name|
|
||||
truncate(name, length: Constants::NAME_TRUNCATION_LENGTH_DROPDOWN)
|
||||
end
|
||||
@temp_file = TempFile.create(session_id: @session.id, file: @file)
|
||||
Data.new(header,
|
||||
columns,
|
||||
@repository.available_repository_fields,
|
||||
@repository,
|
||||
@temp_file)
|
||||
@repository)
|
||||
end
|
||||
|
||||
def too_large?
|
||||
@file.size > Constants::FILE_MAX_SIZE_MB.megabytes
|
||||
end
|
||||
|
||||
def empty?
|
||||
@sheet.last_row.between?(0, 1)
|
||||
end
|
||||
|
||||
def generated_temp_file?
|
||||
def generate_temp_file
|
||||
# Save file for next step (importing)
|
||||
@temp_file = TempFile.new(
|
||||
temp_file = TempFile.new(
|
||||
session_id: @session.id,
|
||||
file: @file
|
||||
)
|
||||
|
||||
if @temp_file.save
|
||||
@temp_file.destroy_obsolete
|
||||
return true
|
||||
if temp_file.save
|
||||
temp_file.destroy_obsolete
|
||||
return temp_file
|
||||
end
|
||||
end
|
||||
|
||||
Data = Struct.new(
|
||||
:header, :columns, :available_fields, :repository, :temp_file
|
||||
:header, :columns, :available_fields, :repository
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
55
app/services/spreadsheet_parser.rb
Normal file
55
app/services/spreadsheet_parser.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
class SpreadsheetParser
|
||||
# Based on file's extension opens file (used for importing)
|
||||
def self.open_spreadsheet(file)
|
||||
filename = file.original_filename
|
||||
file_path = file.path
|
||||
|
||||
if file.class == Paperclip::Attachment && file.is_stored_on_s3?
|
||||
fa = file.fetch
|
||||
file_path = fa.path
|
||||
end
|
||||
|
||||
case File.extname(filename)
|
||||
when '.csv'
|
||||
Roo::CSV.new(file_path, extension: :csv)
|
||||
when '.tsv'
|
||||
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
|
||||
when '.txt'
|
||||
# This assumption is based purely on biologist's habits
|
||||
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
|
||||
when '.xlsx'
|
||||
# Roo Excel parcel was replaced with Creek, but it can be enabled back,
|
||||
# just swap lines below. But only one can be enabled at the same time.
|
||||
# Roo::Excelx.new(file_path)
|
||||
Creek::Book.new(file_path).sheets[0]
|
||||
else
|
||||
raise TypeError
|
||||
end
|
||||
end
|
||||
|
||||
def self.spreadsheet_enumerator(sheet)
|
||||
if sheet.is_a?(Roo::CSV)
|
||||
sheet
|
||||
elsif sheet.is_a?(Roo::Excelx)
|
||||
sheet.each_row_streaming
|
||||
else
|
||||
sheet.rows
|
||||
end
|
||||
end
|
||||
|
||||
def self.first_two_rows(sheet)
|
||||
rows = spreadsheet_enumerator(sheet)
|
||||
header = []
|
||||
columns = []
|
||||
i = 1
|
||||
rows.each do |row|
|
||||
# Creek XLSX parser returns Hash of the row, Roo - Array
|
||||
row = row.is_a?(Hash) ? row.values.map(&:to_s) : row.map(&:to_s)
|
||||
header = row if i == 1 && row
|
||||
columns = row if i == 2 && row
|
||||
i += 1
|
||||
break if i > 2
|
||||
end
|
||||
return header, columns
|
||||
end
|
||||
end
|
|
@ -41,6 +41,7 @@ module DelayedUploaderTutorial
|
|||
my_module: my_module,
|
||||
user: current_user,
|
||||
created_at: temp_result.created_at,
|
||||
updated_at: temp_result.created_at,
|
||||
message: I18n.t(
|
||||
'activities.add_asset_result',
|
||||
user: current_user.full_name,
|
||||
|
|
|
@ -150,7 +150,6 @@ module FirstTimeDataGenerator
|
|||
|
||||
# Create a module group
|
||||
my_module_group = MyModuleGroup.create(
|
||||
name: 'Potato qPCR workflow',
|
||||
experiment: experiment
|
||||
)
|
||||
|
||||
|
@ -317,7 +316,7 @@ module FirstTimeDataGenerator
|
|||
samples_to_assign << sample
|
||||
end
|
||||
|
||||
my_modules[1].get_downstream_modules.each do |mm|
|
||||
my_modules[1].downstream_modules.each do |mm|
|
||||
samples_to_assign.each do |s|
|
||||
SampleMyModule.create(
|
||||
sample: s,
|
||||
|
@ -881,7 +880,7 @@ module FirstTimeDataGenerator
|
|||
).sneaky_save
|
||||
|
||||
# create thumbnail
|
||||
experiment.generate_workflow_img
|
||||
experiment.delay.generate_workflow_img
|
||||
|
||||
# Lastly, create cookie with according ids
|
||||
# so tutorial steps can be properly positioned
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<div id="update-canvas"
|
||||
data-can-create-modules="<%= can_create_modules(@experiment) ? "yes" : "no" %>"
|
||||
data-can-edit-modules="<%= can_edit_modules(@experiment) ? "yes" : "no" %>"
|
||||
data-can-edit-module-groups="<%= can_edit_module_groups(@experiment) ? "yes" : "no" %>"
|
||||
data-can-clone-modules="<%= can_clone_modules(@experiment) ? "yes" : "no" %>"
|
||||
data-can-move-modules="<%= can_move_modules(@experiment) ? "yes" : "no" %>"
|
||||
data-can-delete-modules="<%= can_archive_modules(@experiment) ? "yes" : "no" %>"
|
||||
|
@ -49,9 +48,6 @@
|
|||
<span style="display: none;" id="edit-link-placeholder">
|
||||
<%=t "experiments.canvas.edit.edit_module" %>
|
||||
</span>
|
||||
<span style="display: none;" id="edit-group-link-placeholder">
|
||||
<%=t "experiments.canvas.edit.edit_module_group" %>
|
||||
</span>
|
||||
<span style="display: none;" id="clone-link-placeholder">
|
||||
<%=t "experiments.canvas.edit.clone_module" %>
|
||||
</span>
|
||||
|
@ -87,9 +83,6 @@
|
|||
<% if can_edit_modules(@experiment) %>
|
||||
<%= render partial: "canvas/edit/modal/edit_module", locals: {experiment: @experiment } %>
|
||||
<% end %>
|
||||
<% if can_edit_module_groups(@experiment) %>
|
||||
<%= render partial: "canvas/edit/modal/edit_module_group", locals: {experiment: @experiment } %>
|
||||
<% end %>
|
||||
<% if can_move_modules(@experiment) %>
|
||||
<%= render partial: "canvas/edit/modal/move_module", locals: {experiment: @experiment } %>
|
||||
<%= render partial: "canvas/edit/modal/move_module_group", locals: {experiment: @experiment } %>
|
||||
|
|
|
@ -2,11 +2,6 @@
|
|||
id="<%= my_module.id %>"
|
||||
data-module-id="<%= my_module.id %>"
|
||||
data-module-name="<%= my_module.name %>"
|
||||
<% if my_module.my_module_group.present? %>
|
||||
data-module-group-name="<%= my_module.my_module_group.name %>"
|
||||
<% else %>
|
||||
data-module-group-name=""
|
||||
<% end %>
|
||||
data-module-x="<%= my_module.x %>"
|
||||
data-module-y="<%= my_module.y %>"
|
||||
data-module-conns="<%= construct_module_connections(my_module) %>">
|
||||
|
@ -26,11 +21,6 @@
|
|||
<a class="edit-module" href="" data-module-id="<%= my_module.id %>"><%=t "experiments.canvas.edit.edit_module" %></a>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if can_edit_module_groups(my_module.experiment) %>
|
||||
<li <%= 'style=display:none;' if my_module.my_module_group.blank? %>>
|
||||
<a class="edit-module-group" href="" data-module-id="<%= my_module.id %>"><%=t "experiments.canvas.edit.edit_module_group" %></a>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if can_clone_modules(my_module.experiment) %>
|
||||
<li>
|
||||
<a class ="clone-module" href="" data-module-id="<%= my_module.id %>"><%=t "experiments.canvas.edit.clone_module" %></a>
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
<div class="modal fade" id="modal-edit-module-group" tabindex="-1" role="dialog" aria-labelledby="modal-edit-module-group-label">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="modal-edit-module-group-label"><%=t "experiments.canvas.edit.modal_edit_module_group.title" %></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<%= bootstrap_form_tag do |f| %>
|
||||
<%= f.text_field t("experiments.canvas.edit.modal_edit_module_group.name"), id: "edit-module-group-name-input" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-action="confirm"><%=t "experiments.canvas.edit.modal_edit_module_group.confirm" %></button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal"><%=t "general.cancel" %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -58,7 +58,7 @@
|
|||
</div>
|
||||
|
||||
<div id="canvas-container" data-project-id="<%= @project.id %>">
|
||||
<%= render partial: 'canvas/full_zoom', locals: { experiment: @experiment, my_modules: @experiment.active_modules } %>
|
||||
<%= render partial: 'canvas/full_zoom', locals: { experiment: @experiment, my_modules: @active_modules } %>
|
||||
</div>
|
||||
|
||||
<!-- Manage tags modal -->
|
||||
|
|
|
@ -17,5 +17,10 @@
|
|||
</noscript>
|
||||
<div id="root"></div>
|
||||
<%= yield %>
|
||||
|
||||
<script>
|
||||
// GLOBALS
|
||||
window.IN_REQUEST = false;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -4,4 +4,5 @@
|
|||
<option value="<%= kw.name %>"><%= kw.name %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
<% end %>
|
||||
<i><%=t 'protocols.header.keywords_modal' %></i>
|
||||
<% end %>
|
||||
|
|
|
@ -10,9 +10,7 @@
|
|||
<body class="print-report-body">
|
||||
<div class="print-report">
|
||||
<% # Also whitelist <img> and <input type="checkbox"> tags %>
|
||||
<%= sanitize_input(fix_smart_annotation_image(@html),
|
||||
['img', 'input'],
|
||||
['type', 'disabled', 'checked']) %>
|
||||
<%= @html.html_safe %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<%= hidden_field_tag 'file_id', @import_data.temp_file.id %>
|
||||
<%= hidden_field_tag 'file_id', @temp_file.id %>
|
||||
|
||||
<div id="import-errors-container">
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<br />
|
||||
<button type="button"
|
||||
class="btn btn-primary save-result"
|
||||
disabled="true"
|
||||
data-href="<%= my_module_result_assets_path(format: :json) %>"
|
||||
onClick="DragNDropResults.processResult(this)"><%=t 'result_assets.new.create' %></button>
|
||||
<button type="button"
|
||||
|
@ -18,5 +19,4 @@
|
|||
onClick="DragNDropResults.destroyAll()">
|
||||
<%= t("general.cancel")%>
|
||||
</button>
|
||||
<%# end %>
|
||||
</div>
|
||||
|
|
|
@ -70,19 +70,6 @@
|
|||
<%= t'Experiments' %>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation"
|
||||
class="
|
||||
<%= "active" if @search_category.present? and @search_category == :workflows %>
|
||||
<%= "disabled" if @workflow_search_count == 0 %>"
|
||||
>
|
||||
<a href="?<%= {category: 'workflows', q: @search_query,
|
||||
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
|
||||
match_case: @search_case, utf8: '✓'}.to_query %>">
|
||||
<span class="badge pull-right"><%= @workflow_search_count %></span>
|
||||
<span class="glyphicon glyphicon-random"></span>
|
||||
<%= t'Workflows' %>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation"
|
||||
class="
|
||||
<%= "active" if @search_category.present? and @search_category == :modules %>
|
||||
|
@ -268,9 +255,6 @@
|
|||
<% if @search_category == :experiments and @experiment_search_count > 0 %>
|
||||
<%= render 'search/results/experiments', search_query: @search_query, results: @experiment_results %>
|
||||
<% end %>
|
||||
<% if @search_category == :workflows and @workflow_search_count > 0 %>
|
||||
<%= render 'search/results/workflows', search_query: @search_query, results: @workflow_results %>
|
||||
<% end %>
|
||||
<% if @search_category == :modules and @module_search_count > 0 %>
|
||||
<%= render 'search/results/modules', search_query: @search_query, results: @module_results %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
<% results.each do |workflow| %>
|
||||
<h5>
|
||||
<span class="glyphicon glyphicon-random"></span>
|
||||
<%= highlight workflow.name, search_query.strip.split(/\s+/) %>
|
||||
</h5>
|
||||
|
||||
<p>
|
||||
<span>
|
||||
<%=t 'search.index.created_at' %>
|
||||
<%=l workflow.created_at, format: :full %>
|
||||
</span>
|
||||
<br>
|
||||
<span>
|
||||
<%=t 'search.index.experiment' %>
|
||||
<%= render partial: 'search/results/partials/experiment_text.html.erb',
|
||||
locals: { experiment: workflow.experiment } %>
|
||||
</span>
|
||||
<br>
|
||||
<span>
|
||||
<%=t 'search.index.project' %>
|
||||
<%= render partial: 'search/results/partials/project_text.html.erb',
|
||||
locals: { project: workflow.experiment.project, link_to_page: :show } %>
|
||||
</span>
|
||||
<br>
|
||||
<span>
|
||||
<%=t 'search.index.team' %>
|
||||
<%= render partial: 'search/results/partials/team_text.html.erb',
|
||||
locals: { team: workflow.experiment.project.team } %>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
<% end %>
|
|
@ -23,12 +23,12 @@
|
|||
<%= f.password_field :password, autocomplete: 'off', class: 'form-control' %>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="password_confirmation_form">
|
||||
<%= f.label :password_confirmation %>
|
||||
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'form-control' %>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="team_name_form">
|
||||
<%= label :team, :name, t('users.registrations.new.team_name_label') %>
|
||||
<% if @team %>
|
||||
<%= text_field :team, :name, class: 'form-control', value: @team.name %>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Devise::Async.enabled = true
|
||||
Devise::Async.backend = :delayed_job
|
||||
Devise::Async.queue = :devise_email
|
||||
# Devise::Async.priority = 10
|
||||
# Devise::Async.priority = 10
|
|
@ -705,7 +705,6 @@ en:
|
|||
drag_connections: "Drag connection/s from here"
|
||||
options_header: "Options"
|
||||
edit_module: "Rename task"
|
||||
edit_module_group: "Rename workflow"
|
||||
clone_module: "Copy task as template (only Protocols steps copied)"
|
||||
clone_module_group: "Copy workflow as template (only Protocols steps copied)"
|
||||
move_module: "Move task to another experiment"
|
||||
|
@ -721,10 +720,6 @@ en:
|
|||
title: "Rename task"
|
||||
name: "Task name"
|
||||
confirm: "Rename task"
|
||||
modal_edit_module_group:
|
||||
title: "Rename workflow"
|
||||
name: "Workflow name"
|
||||
confirm: "Rename workflow"
|
||||
modal_move_module:
|
||||
title: "Move task to experiment"
|
||||
confirm: "Move task"
|
||||
|
@ -1421,6 +1416,7 @@ en:
|
|||
no_authors: "No authors"
|
||||
description: "Description"
|
||||
no_description: "No description"
|
||||
keywords_modal: "Input one or multiple keywords, confirm each keyword with ENTER key"
|
||||
edit_name_modal:
|
||||
title: "Edit name of protocol %{protocol}"
|
||||
label: "Name"
|
||||
|
@ -1826,9 +1822,7 @@ en:
|
|||
leave_team_error: "An error occured."
|
||||
leave_flash: "Successfuly left team %{team}."
|
||||
user:
|
||||
blank_password_error: "Password can't be blank!"
|
||||
passwords_dont_match: "Passwords don't match"
|
||||
password_invalid: "Password is invalid!"
|
||||
avatar_too_big: "Avatar file size must be less than 0.2 MB"
|
||||
current_password_invalid: "incorrect password"
|
||||
password_confirmation_not_match: "doesn't match"
|
||||
invite_users:
|
||||
permission_error: "You don't have permission to invite additional users to team. Contact its administrator/s."
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
class AddConnectionsAndSampleTasksIndexes < ActiveRecord::Migration[4.2]
|
||||
def change
|
||||
add_index :connections, :input_id
|
||||
add_index :connections, :output_id
|
||||
add_index :sample_my_modules, :my_module_id
|
||||
end
|
||||
end
|
5
db/migrate/20171005135350_update_my_module_groups.rb
Normal file
5
db/migrate/20171005135350_update_my_module_groups.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class UpdateMyModuleGroups < ActiveRecord::Migration[4.2]
|
||||
def change
|
||||
remove_column :my_module_groups, :name, :string
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@ Background:
|
|||
And "nonadmin@myorg.com" is in "BioSistemika Process" team as a "normal_user"
|
||||
And is signed in with "nonadmin@myorg.com", "mypassword1234"
|
||||
|
||||
@compile @javascript
|
||||
@javascript
|
||||
Scenario: Successful navigate to profile page
|
||||
Given I'm on the home page of "BioSistemika Process" team
|
||||
And I click on Avatar
|
||||
|
@ -24,7 +24,7 @@ Scenario: Unsuccessful avatar image upload, file is too big
|
|||
Then I click on image within ".avatar-container" element
|
||||
And I attach a "Moon.png" file to "user_avatar_input" field
|
||||
Then I click "Upload" button
|
||||
And I should see "Avatar file size must be less than 0.2 MB" error message under "user_avatar_input" field
|
||||
And I should see "file too large (maximum size is 0.2 MB)" error message under "user_avatar_input" field
|
||||
|
||||
@javascript
|
||||
Scenario: Unsuccessful avatar image upload, file is invalid
|
||||
|
@ -32,7 +32,7 @@ Scenario: Unsuccessful avatar image upload, file is invalid
|
|||
Then I click on image within ".avatar-container" element
|
||||
And I attach a "File.txt" file to "user_avatar_input" field
|
||||
Then I click "Upload" button
|
||||
And I should see "Avatar content type is invalid" error message under "user_avatar_input" field
|
||||
And I should see "invalid file extension" error message under "user_avatar_input" field
|
||||
|
||||
@javascript
|
||||
Scenario: Successful upload avatar image
|
||||
|
@ -93,7 +93,7 @@ Scenario: Unsuccessful Password Change, passwords does not match
|
|||
And I fill in "mypassword5678" in New password field
|
||||
And I fill in "mypassword56788" in New password confirmation field
|
||||
Then I click "Update" button
|
||||
And I should see "Passwords don't match"
|
||||
And I should see "doesn't match"
|
||||
|
||||
@javascript
|
||||
Scenario: Unsuccessful Password Change, current password is invalid
|
||||
|
@ -103,7 +103,7 @@ Scenario: Unsuccessful Password Change, current password is invalid
|
|||
And I fill in "mypassword5678" in New password field
|
||||
And I fill in "mypassword5678" in New password confirmation field
|
||||
Then I click "Update" button
|
||||
And I should see "Password is invalid!"
|
||||
And I should see "incorrect password"
|
||||
|
||||
@javascript
|
||||
Scenario: Successful Password Change
|
||||
|
|
|
@ -27,3 +27,21 @@ Feature: Sign up
|
|||
And I click "Sign up" button
|
||||
Then I should see "SpliceGirls"
|
||||
And I should be on homepage
|
||||
|
||||
@javascript
|
||||
Scenario: Unsuccessful sign up, password confirmation does not match
|
||||
Given I visit the sign up page
|
||||
Then I fill the sign up form with
|
||||
| Full name | Email | Password | Password confirmation | Team name |
|
||||
| Magnus | magnus@gmail.com | asdf1234 | asdf1234567 | SpliceGirls |
|
||||
And I click "Sign up" button
|
||||
Then I should see "doesn't match Password" error message under "password_confirmation_form" field
|
||||
|
||||
@javascript
|
||||
Scenario: Unsuccessful sign up, team name is missing
|
||||
Given I visit the sign up page
|
||||
Then I fill the sign up form with
|
||||
| Full name | Email | Password | Password confirmation |
|
||||
| Magnus | magnus@gmail.com | asdf1234 | asdf1234 |
|
||||
And I click "Sign up" button
|
||||
Then I should see "is too short (minimum is 2 characters)" error message under "team_name_form" field
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
# feature/sign up.feature
|
||||
Feature: Sign up
|
||||
As a new User
|
||||
I want to Sign up as a new User
|
||||
I want to create a new account
|
||||
|
||||
Background:
|
||||
Given the following users is registered:
|
||||
| email | password | team
|
||||
| nonadmin@myorg.com | mypassword1234 | BioSistemika Process
|
||||
|
||||
Scenario: Successful Sign up
|
||||
Given I am on Sign up page
|
||||
Then I fill the Sign up form with
|
||||
| Full name | Email | Password | Password confirmation | Team name |
|
||||
| Karli | nonuser@myorg.com | mypassword1234 | mypassword1234 | BioSistemika Process |
|
||||
And I click on reCAPTCHA.
|
||||
And I click on "Sign up" button
|
||||
Then I should see "BioSistemika Process"
|
||||
Then I should see "Hi, Karli" nex to the avatar
|
||||
And I should get a Gamification pop up message "Welcome to sciNote."
|
||||
|
||||
Scenario: Unsuccessful Sign up, Password confirmation does not match
|
||||
Given I am on Sign up page
|
||||
Then I fill the Sign up form with
|
||||
| Full name | Email | Password | Password confirmation | Team name |
|
||||
| Karli | nonuser@myorg.com | mypassword1234 | mypassword123344 | BioSistemika Process |
|
||||
And I click on reCAPTCHA.
|
||||
And I click on "Sign up" button
|
||||
Then I should see "doesn't match Password" error message under "Password confirmation" field
|
||||
|
||||
Scenario: Unsuccessful Sign up, Team name is missing
|
||||
Given I am on Sign up page
|
||||
Then I fill the Sign up form with
|
||||
| Full name | Email | Password | Password confirmation |
|
||||
| Karli | nonuser@myorg.com | mypassword1234 | mypassword123344 |
|
||||
And I click on reCAPTCHA.
|
||||
And I click on "Sign up" button
|
||||
Then I should see "is too short (minimum is 2 characters)" error message under "Team name" field
|
||||
|
||||
Scenario: Unsuccessful Sign up, reCAPTCHA is missing
|
||||
Given I am on Sign up page
|
||||
Then I fill the Sign up form with
|
||||
| Full name | Email | Password | Password confirmation | Team name |
|
||||
| Karli | nonuser@myorg.com | mypassword1234 | mypassword1234 | BioSistemika Process |
|
||||
And I click on "Sign up" button
|
||||
Then I should see "reCAPTCHA verification failed, please try again." error message under "recaptcha" field
|
||||
|
||||
Scenario: Unsuccessful Sign up, Email has already been taken
|
||||
Given I am on Sign up page
|
||||
Then I fill the Sign up form with
|
||||
| Full name | Email | Password | Password confirmation | Team name |
|
||||
| Karli | nonadmin@myorg.com | mypassword1234 | mypassword1234 | BioSistemika Process |
|
||||
And I click on "Sign up" button
|
||||
Then I should see "has already been taken" error message under Email field
|
|
@ -12,18 +12,18 @@ end
|
|||
|
||||
Then(/^I change "([^"]*)" with "([^"]*)" email$/) do |prev_email, new_email|
|
||||
wait_for_ajax
|
||||
find(:xpath, "//input[@value='#{prev_email}']").set(new_email)
|
||||
find(:css, "input[value='#{prev_email}']").set(new_email)
|
||||
end
|
||||
|
||||
Then(/^I fill in "([^"]*)" in Current password field$/) do |password|
|
||||
find(:xpath, '//input[@id="settings_page.current_password"]').set(password)
|
||||
find(:css, 'input[id="settings_page.current_password"]').set(password)
|
||||
end
|
||||
|
||||
Then(/^I fill in "([^"]*)" in New password field$/) do |password|
|
||||
find(:xpath, '//input[@id="settings_page.new_password"]').set(password)
|
||||
find(:css, 'input[id="settings_page.new_password"]').set(password)
|
||||
end
|
||||
|
||||
Then(/^I fill in "([^"]*)" in New password confirmation field$/) do |password|
|
||||
find(:xpath,
|
||||
'//input[@id="settings_page.new_password_confirmation"]').set(password)
|
||||
find(:css,
|
||||
'input[id="settings_page.new_password_confirmation"]').set(password)
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
When(/^I click "(.+)" button$/) do |button|
|
||||
click_on button
|
||||
click_on(button)
|
||||
end
|
||||
|
||||
Given(/^Show me the page$/) do
|
||||
|
@ -21,6 +21,7 @@ Given(/^I click "(.+)" link within "(.+)"$/) do |link, element|
|
|||
end
|
||||
|
||||
Then(/^I should see "(.+)"$/) do |text|
|
||||
wait_for_ajax
|
||||
expect(page).to have_content(text)
|
||||
end
|
||||
|
||||
|
@ -47,7 +48,10 @@ Given(/^"([^"]*)" is in "([^"]*)" team as a "([^"]*)"$/) do |user_email, team_na
|
|||
end
|
||||
|
||||
Then(/^I attach a "([^"]*)" file to "([^"]*)" field$/) do |file, field_id|
|
||||
wait_for_ajax
|
||||
attach_file(field_id, Rails.root.join('features', 'assets', file))
|
||||
# "expensive" operation needs some time :=)
|
||||
sleep(0.3)
|
||||
end
|
||||
|
||||
Then(/^I should see "([^"]*)" error message under "([^"]*)" field$/) do |message, field_id|
|
||||
|
@ -64,13 +68,16 @@ Then(/^I click on image within "([^"]*)" element$/) do |container|
|
|||
within(container) do
|
||||
find('img').click
|
||||
end
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
Then(/^I should see "([^"]*)" flash message$/) do |message|
|
||||
wait_for_ajax
|
||||
expect(find_by_id('alert-flash')).to have_content(message)
|
||||
end
|
||||
|
||||
Then(/^I click on Edit on "([^"]*)" input field$/) do |container_id|
|
||||
wait_for_ajax
|
||||
container = page.find_by_id(container_id)
|
||||
within(container) do
|
||||
find('button').click
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
Given(/^the following users are registered$/) do |table|
|
||||
table.hashes.each do |hash|
|
||||
FactoryGirl.create(:user, hash)
|
||||
User.find_by_email(hash.fetch('email')).confirm
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
# newer version of cucumber-rails. Consider adding your own code to a new file
|
||||
# instead of editing this one. Cucumber will automatically load all features/**/*.rb
|
||||
# files.
|
||||
|
||||
ENV['CUCUMBER'] = 'cucumber'
|
||||
|
||||
require 'cucumber/rails'
|
||||
|
@ -22,7 +21,7 @@ Capybara::Webkit.configure do |config|
|
|||
# Allow pages to make requests to any URL without issuing a warning.
|
||||
config.allow_unknown_urls
|
||||
|
||||
# Timeout if requests take longer than 5 seconds
|
||||
# Timeout if requests take longer than 30 seconds
|
||||
config.timeout = 30
|
||||
|
||||
# Don't raise errors when SSL certificates can't be validated
|
||||
|
@ -34,11 +33,18 @@ end
|
|||
|
||||
Capybara.javascript_driver = :webkit
|
||||
Capybara.default_max_wait_time = 30
|
||||
Capybara.asset_host = 'http://localhost:3000'
|
||||
Capybara.asset_host = 'http://localhost:3001'
|
||||
Capybara.server_port = 3001
|
||||
|
||||
# Precompile webpacker to avoid render bugs in capybara webkit
|
||||
# global hook throws an error :( https://github.com/cucumber/cucumber/wiki/Hooks
|
||||
Before('@compile') do
|
||||
system('NODE_ENV=production bundle exec rails webpacker:compile')
|
||||
|
||||
compiled = false
|
||||
Before do
|
||||
unless compiled
|
||||
system('NODE_ENV=production bundle exec rails webpacker:compile')
|
||||
compiled = true
|
||||
end
|
||||
end
|
||||
|
||||
# Capybara defaults to CSS3 selectors rather than XPath.
|
||||
|
@ -66,6 +72,8 @@ ActionController::Base.allow_rescue = false
|
|||
# Remove/comment out the lines below if your app doesn't have a database.
|
||||
# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead.
|
||||
begin
|
||||
require 'database_cleaner'
|
||||
require 'database_cleaner/cucumber'
|
||||
DatabaseCleaner.strategy = :truncation
|
||||
rescue NameError
|
||||
raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it."
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# @TODO make this funtion work!
|
||||
def wait_for_ajax
|
||||
counter = 0
|
||||
while page.evaluate_script('axios.interceptors.response.use(function(response) { return 1 })').to_i > 0
|
||||
while page.evaluate_script('window.IN_REQUEST')
|
||||
counter += 1
|
||||
sleep(0.1)
|
||||
if (0.1 * counter) >= Capybara.default_max_wait_time
|
||||
raise "AJAX request took longer than #{Capybara.default_max_wait_time} seconds."
|
||||
raise "AJAX request took longer than " \
|
||||
"#{Capybara.default_max_wait_time} seconds."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ClientApi::UserService do
|
||||
let(:user) do
|
||||
create :user,
|
||||
full_name: 'User One',
|
||||
initials: 'UO',
|
||||
email: 'user@happy.com',
|
||||
password: 'asdf1234',
|
||||
password_confirmation: 'asdf1234'
|
||||
end
|
||||
|
||||
describe '#update_user!' do
|
||||
it 'should update user email if the password is correct' do
|
||||
email = 'new_user@happy.com'
|
||||
params = { email: email, current_password: 'asdf1234' }
|
||||
user_service = ClientApi::UserService.new(current_user: user,
|
||||
params: params)
|
||||
user_service.update_user!
|
||||
expect(user.email).to eq(email)
|
||||
end
|
||||
|
||||
it 'should raise CustomUserError error if the password is not correct' do
|
||||
email = 'new_user@happy.com'
|
||||
params = { email: email, current_password: 'banana' }
|
||||
user_service = ClientApi::UserService.new(current_user: user,
|
||||
params: params)
|
||||
expect {
|
||||
user_service.update_user!
|
||||
}.to raise_error(ClientApi::CustomUserError)
|
||||
end
|
||||
|
||||
it 'should update initials and full name without password confirmation' do
|
||||
full_name = 'Happy User'
|
||||
initials = 'HU'
|
||||
user_service = ClientApi::UserService.new(
|
||||
current_user: user,
|
||||
params: { full_name: full_name, initials: initials }
|
||||
)
|
||||
user_service.update_user!
|
||||
expect(user.full_name).to eq(full_name)
|
||||
expect(user.initials).to eq(initials)
|
||||
end
|
||||
|
||||
it 'should raise an error if current password not present' do
|
||||
user_service = ClientApi::UserService.new(
|
||||
current_user: user,
|
||||
params: { password: 'hello1234', password_confirmation: 'hello1234' }
|
||||
)
|
||||
expect {
|
||||
user_service.update_user!
|
||||
}.to raise_error(ClientApi::CustomUserError)
|
||||
end
|
||||
|
||||
it 'should raise an error if password_confirmation don\'t match' do
|
||||
user_service = ClientApi::UserService.new(
|
||||
current_user: user,
|
||||
params: { password: 'hello1234',
|
||||
password_confirmation: 'hello1234567890',
|
||||
current_password: 'asdf1234' }
|
||||
)
|
||||
|
||||
expect {
|
||||
user_service.update_user!
|
||||
}.to raise_error(ClientApi::CustomUserError, 'Passwords don\'t match')
|
||||
end
|
||||
|
||||
it 'should update the password' do
|
||||
new_password = 'hello1234'
|
||||
user_service = ClientApi::UserService.new(
|
||||
current_user: user,
|
||||
params: { password: new_password,
|
||||
password_confirmation: new_password,
|
||||
current_password: 'asdf1234' }
|
||||
)
|
||||
user_service.update_user!
|
||||
expect(user.valid_password?(new_password)).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
79
spec/services/client_api/users/update_service_spec.rb
Normal file
79
spec/services/client_api/users/update_service_spec.rb
Normal file
|
@ -0,0 +1,79 @@
|
|||
require 'rails_helper'
|
||||
|
||||
include ClientApi::Users
|
||||
|
||||
describe ClientApi::Users::UpdateService do
|
||||
let(:user) do
|
||||
create :user,
|
||||
full_name: 'User One',
|
||||
initials: 'UO',
|
||||
email: 'user@happy.com',
|
||||
password: 'asdf1234',
|
||||
password_confirmation: 'asdf1234'
|
||||
end
|
||||
|
||||
it 'should update user email if the password is correct' do
|
||||
email = 'new_user@happy.com'
|
||||
params = { email: email, current_password: 'asdf1234' }
|
||||
service = UpdateService.new(current_user: user,
|
||||
params: params)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :success
|
||||
expect(user.email).to eq(email)
|
||||
end
|
||||
|
||||
it 'should raise CustomUserError error if the password is not correct' do
|
||||
email = 'new_user@happy.com'
|
||||
params = { email: email, current_password: 'banana' }
|
||||
service = UpdateService.new(current_user: user,
|
||||
params: params)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :error
|
||||
end
|
||||
|
||||
it 'should update initials and full name without password confirmation' do
|
||||
full_name = 'Happy User'
|
||||
initials = 'HU'
|
||||
service = UpdateService.new(
|
||||
current_user: user,
|
||||
params: { full_name: full_name, initials: initials }
|
||||
)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :success
|
||||
expect(user.full_name).to eq(full_name)
|
||||
expect(user.initials).to eq(initials)
|
||||
end
|
||||
|
||||
it 'should raise an error if current password not present' do
|
||||
service = UpdateService.new(
|
||||
current_user: user,
|
||||
params: { password: 'hello1234', password_confirmation: 'hello1234' }
|
||||
)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :error
|
||||
end
|
||||
|
||||
it 'should raise an error if password_confirmation don\'t match' do
|
||||
service = UpdateService.new(
|
||||
current_user: user,
|
||||
params: { password: 'hello1234',
|
||||
password_confirmation: 'hello1234567890',
|
||||
current_password: 'asdf1234' }
|
||||
)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :error
|
||||
end
|
||||
|
||||
it 'should update the password' do
|
||||
new_password = 'hello1234'
|
||||
service = UpdateService.new(
|
||||
current_user: user,
|
||||
params: { password: new_password,
|
||||
password_confirmation: new_password,
|
||||
current_password: 'asdf1234' }
|
||||
)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :success
|
||||
expect(user.valid_password?(new_password)).to be(true)
|
||||
end
|
||||
end
|
|
@ -8,15 +8,7 @@ class MyModuleGroupTest < ActiveSupport::TestCase
|
|||
@module_group = my_module_groups(:wf1)
|
||||
end
|
||||
|
||||
should validate_presence_of(:name)
|
||||
should validate_length_of(:name)
|
||||
.is_at_most(Constants::NAME_MAX_LENGTH)
|
||||
|
||||
test "should validate with valid data" do
|
||||
assert @module_group.valid?
|
||||
end
|
||||
|
||||
test "where_attributes_like should work" do
|
||||
attributes_like_test(MyModuleGroup, :name, "expression")
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue