Merge branch 'master' of https://github.com/biosistemika/scinote-web into zd_SCI_1248

This commit is contained in:
zmagod 2017-05-15 14:49:37 +02:00
commit 9bec7dd96e
37 changed files with 898 additions and 173 deletions

View file

@ -14,6 +14,7 @@ gem 'bootstrap_form'
gem 'yomu'
gem 'font-awesome-rails', '~> 4.6'
gem 'recaptcha', require: 'recaptcha/rails'
gem 'sanitize', '~> 4.4'
# JS datetime library, requirement of datetime picker
gem 'momentjs-rails', '>= 2.9.0'

View file

@ -103,6 +103,7 @@ GEM
colorize (0.8.1)
commit_param_routing (0.0.1)
concurrent-ruby (1.0.0)
crass (1.0.2)
debug_inspector (0.0.2)
deface (1.0.2)
colorize (>= 0.5.8)
@ -192,6 +193,8 @@ GEM
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
nokogumbo (1.4.10)
nokogiri
oj (2.17.4)
orm_adapter (0.5.0)
paperclip (4.3.2)
@ -266,6 +269,10 @@ GEM
ruby-graphviz (1.2.2)
ruby-progressbar (1.8.1)
rubyzip (1.1.7)
sanitize (4.4.0)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.4.23)
sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0)
@ -387,6 +394,7 @@ DEPENDENCIES
rubocop
ruby-graphviz (~> 1.2)
rubyzip
sanitize (~> 4.4)
sass-rails (~> 5.0)
scss_lint
sdoc (~> 0.4.0)

View file

@ -544,14 +544,12 @@ $("[data-action='new-step']").on("ajax:success", function(e, data) {
toggleButtons(false);
initializeCheckboxSorting();
$("#step_name").focus();
$("#new-step-main-tab a").on("shown.bs.tab", function() {
$("#step_name").focus();
});
TinyMCE.refresh();
});
// Needed because server-side validation failure clears locations of

View file

@ -25,7 +25,8 @@
Results.toggleResultEditButtons(false);
$('#result_name').focus();
},
error: function() {
error: function(xhr, status, e) {
$(this).renderFormErrors('result', xhr.responseJSON, true, e);
animateSpinner(null, false);
initNewResultAsset();
}
@ -74,15 +75,8 @@
initPreviewModal();
Comments.initialize();
initNewResultAsset();
}).on('ajax:error', function(e, data) {
// This check is here only because of remotipart bug, which returns
// HTML instead of JSON, go figure
var errors = '';
if (data.errors)
errors = data.errors;
else
errors = data.responseJSON.errors;
$form.renderFormErrors('result', errors, true, e);
}).on('ajax:error', function(xhr, status, e) {
$form.renderFormErrors('result', xhr.responseJSON, true, e);
animateSpinner(null, false);
});
}

View file

@ -154,7 +154,10 @@ function dataTableInit() {
fnInitComplete: function(oSettings, json) {
// Reload correct column order and visibility (if you refresh page)
for (var i = 0; i < table.columns()[0].length; i++) {
var visibility = myData.columns[i].visible;
var visibility = false;
if (myData.columns[i]) {
visibility = myData.columns[i].visible;
}
if (typeof (visibility) === 'string') {
visibility = (visibility === 'true');
}
@ -976,7 +979,6 @@ function changeToEditMode() {
// Add number of columns
$('#samples').data('num-columns',
$('#samples').data('num-columns') + 1);
// Add column to table (=table header)
originalHeader.append(
'<th class="custom-field" id="' + data.id + '" ' +
@ -1181,7 +1183,7 @@ function changeToEditMode() {
text.html(generateColumnNameTooltip(newName));
$(table.columns().header()).filter('#' + id)
.html(generateColumnNameTooltip(newName));
originalHeader.find('#' + id).html(newName);
cancelEditMode();
initHeaderTooltip();
},

View file

@ -2,18 +2,85 @@
* Define AJAX methods for handling errors on forms.
*/
/*
* Render errors specified in array of strings format (or string if
* just one error) for a single form element.
*
* Show error message/s and mark error input (if errMsgs is defined)
* and, if present, mark and show the tab where the error occured and
* focus/scroll to the error input, if it is the first one to be
* specified or if errMsgs is undefined.
*
* @param {string} errAttributes Span element (error) attributes
* @param {boolean} clearErr Set clearErr to true if this is the only
* error that can happen/show.
*/
var renderFormError = function(ev, input, errMsgs, clearErr, errAttributes) {
clearErr = _.isUndefined(clearErr) ? false : clearErr;
errAttributes = _.isUndefined(errAttributes) ? '' : ' ' + errAttributes;
var $form = $(input).closest('form');
if (!_.isUndefined(errMsgs)) {
if (clearErr) {
$form.clearFormErrors();
}
// Mark error form group
var $formGroup = $(input).closest('.form-group');
if (!$formGroup.hasClass('has-error')) {
$formGroup.addClass('has-error');
}
// Add error message/s
var errorText = ($.makeArray(errMsgs).map(function(m) {
return m.strToErrorFormat();
})).join('<br />');
var $errSpan = "<span class='help-block'" +
errAttributes + '>' + errorText + '</span>';
$formGroup.append($errSpan);
}
var $parent;
var $tab = $(input).closest('.tab-pane');
if ($tab.length) {
// Mark error tab
tabsPropagateErrorClass($form);
$parent = $tab;
} else {
$parent = $form;
}
// Focus and scroll to the error if it is the first (most upper) one
if ($parent.find('.form-group.has-error').length === 1 ||
_.isUndefined(errMsgs)) {
goToFormElement(input);
}
if (!_.isUndefined(ev)) {
// Don't submit form
ev.preventDefault();
ev.stopPropagation();
}
};
/*
* Render errors specified in JSON format for many form elements.
*/
$.fn.renderFormErrors = function (modelName, errors, clear, ev) {
clear = (typeof clear !== 'undefined') ? clear : true;
$.fn.renderFormErrors = function(modelName, errors, clear, ev) {
clear = ((typeof clear) === 'undefined') ? true : clear;
if (clear || _.isUndefined(clear)) {
this.clearFormErrors();
}
var form = $(this);
$.each(errors, function (field, messages) {
$input = $(_.filter(form.find('input, select, textarea'), function (el) {
$.each(errors, function(field, messages) {
// Special exception for file uploads in steps and results
if (field === 'assets.file') {
field = 'assets_attributes';
} else if (field === 'asset.file') {
field = 'asset_attribute';
}
var types = 'input, file, select, textarea';
var $input = $(_.filter(form.find(types), function(el) {
var name = $(el).attr('name');
if (name) {
return name.match(new RegExp(modelName + '\\[' + field + '\\(?'));
@ -25,80 +92,22 @@ $.fn.renderFormErrors = function (modelName, errors, clear, ev) {
});
};
/*
* Render errors specified in array of strings format (or string if
* just one error) for a single form element.
*
* Show error message/s and mark error input (if errMsgs is defined)
* and, if present, mark and show the tab where the error occured and
* focus/scroll to the error input, if it is the first one to be
* specified or if errMsgs is undefined.
*
* @param {string} errAttributes Span element (error) attributes
* @param {boolean} clearErr Set clearErr to true if this is the only
* error that can happen/show.
*/
var renderFormError = function (ev, input, errMsgs, clearErr, errAttributes) {
clearErr = _.isUndefined(clearErr) ? false : clearErr;
errAttributes = _.isUndefined(errAttributes) ? "" : " " + errAttributes;
$form = $(input).closest("form");
if (!_.isUndefined(errMsgs)) {
if (clearErr) {
$form.clearFormErrors();
}
// Mark error form group
$formGroup = $(input).closest(".form-group");
if (!$formGroup.hasClass("has-error")) {
$formGroup.addClass("has-error");
}
// Add error message/s
error_text = ($.makeArray(errMsgs).map(function (m) {
return m.strToErrorFormat();
})).join("<br />");
$errSpan = "<span class='help-block'" + errAttributes + ">" + error_text + "</span>";
$formGroup.append($errSpan);
}
$tab = $(input).closest(".tab-pane");
if ($tab.length) {
// Mark error tab
tabsPropagateErrorClass($form);
$parent = $tab;
} else {
$parent = $form;
}
// Focus and scroll to the error if it is the first (most upper) one
if ($parent.find(".form-group.has-error").length === 1 || _.isUndefined(errMsgs)) {
goToFormElement(input);
}
if(!_.isUndefined(ev)) {
// Don't submit form
ev.preventDefault();
ev.stopPropagation();
}
}
/*
* If any of form tabs (if exist) has errors, mark it and
* and show the first erroneous tab.
*/
function tabsPropagateErrorClass($form) {
var $contents = $form.find("div.tab-pane");
_.each($contents, function (tab) {
var $contents = $form.find('div.tab-pane');
_.each($contents, function(tab) {
var $tab = $(tab);
var $errorFields = $tab.find(".has-error");
var $errorFields = $tab.find('.has-error');
if ($errorFields.length) {
var id = $tab.attr("id");
var id = $tab.attr('id');
var navLink = $form.find("a[href='#" + id + "'][data-toggle='tab']");
if (navLink.parent().length) {
navLink.parent().addClass("has-error");
navLink.parent().addClass('has-error');
}
}
});
$form.find(".nav-tabs .has-error:first > a", $form).tab("show");
$form.find('.nav-tabs .has-error:first > a', $form).tab('show');
}

View file

@ -200,7 +200,7 @@ path, ._jsPlumb_endpoint {
cursor: pointer;
display: block;
position: absolute;
width: 294px;
width: 300px;
z-index: 5;
.panel-body .due-date-link {

View file

@ -282,7 +282,9 @@ a {
.badge-indicator,
.btn .badge-indicator {
font-size: 9px;
margin-left: -8px;
padding: 3px 5px;
top: 3px;
}

View file

@ -52,7 +52,7 @@ module ReportActions
def generate_experiment_contents_json(experiment, selected_modules)
res = []
experiment.my_modules.each do |my_module|
experiment.my_modules.order(:workflow_order).each do |my_module|
next unless selected_modules.include?(my_module.id)
res << generate_new_el(false)
@ -72,9 +72,9 @@ module ReportActions
ReportExtends::MODULE_CONTENTS.each do |contents|
protocol = contents.element == :step ? my_module.protocol.present? : true
next unless in_params?("module_#{contents.element}".to_sym) && protocol
res << generate_new_el(false)
if contents.children
contents.collection(my_module).each do |report_el|
res << generate_new_el(false)
el = generate_el(
"reports/elements/my_module_#{contents
.element
@ -82,9 +82,9 @@ module ReportActions
.singularize}_element.html.erb",
contents.parse_locals([report_el])
)
if contents.element == :step
if contents.locals.first == :step
el[:children] = generate_step_contents_json(report_el)
elsif contents.element == :result
elsif contents.locals.first == :result
el[:children] = generate_result_contents_json(report_el)
end
res << el
@ -92,6 +92,7 @@ module ReportActions
else
file_name = contents.file_name
file_name = contents.element if contents.element == :samples
res << generate_new_el(false)
res << generate_el(
"reports/elements/my_module_#{file_name}_element.html.erb",
contents.parse_locals([my_module, :asc])
@ -105,7 +106,7 @@ module ReportActions
def generate_step_contents_json(step)
res = []
if in_params? :step_checklists
step.checklists.each do |checklist|
step.checklists.asc.each do |checklist|
res << generate_new_el(false)
res << generate_el(
'reports/elements/step_checklist_element.html.erb',

View file

@ -185,6 +185,8 @@ class ExperimentsController < ApplicationController
cloned_experiment = @experiment.deep_clone_to_project(current_user,
project)
success = cloned_experiment.valid?
# Create workflow image
cloned_experiment.delay.generate_workflow_img if success
else
success = false
end

View file

@ -72,11 +72,7 @@ class ResultAssetsController < ApplicationController
}, status: :ok
end
else
# This response is sent as 200 OK due to IE security error when
# accessing iframe content.
format.json do
render json: { status: 'error', errors: @result.errors }
end
format.json { render json: @result.errors, status: :bad_request }
end
end
end

View file

@ -97,11 +97,6 @@ class StepsController < ApplicationController
status: :ok
end
else
# On error, delete the newly added files from S3, as they were
# uploaded on client-side (in case of client-side hacking of
# asset's signature response)
Asset.destroy_all(new_assets)
format.json {
render json: {
html: render_to_string(partial: 'new.html.erb')

View file

@ -53,6 +53,7 @@ class TeamsController < ApplicationController
)
if @temp_file.save
@temp_file.destroy_obsolete
# format.html
format.json {
render :json => {

View file

@ -1,5 +1,6 @@
module Users
class InvitationsController < Devise::InvitationsController
include InputSanitizeHelper
include UsersGenerator
prepend_before_action :check_captcha, only: [:update]
@ -175,8 +176,8 @@ module Users
message = "#{I18n.t('search.index.team')} #{team.name}"
notification = Notification.create(
type_of: :assignment,
title: ActionController::Base.helpers.sanitize(title),
message: ActionController::Base.helpers.sanitize(message)
title: sanitize_input(title),
message: sanitize_input(message)
)
if target_user.assignments_notification

View file

@ -92,10 +92,8 @@ module ApplicationHelper
def generate_annotation_notification(target_user, title, message)
notification = Notification.create(
type_of: :assignment,
title:
ActionController::Base.helpers.sanitize(title),
message:
ActionController::Base.helpers.sanitize(message)
title: sanitize_input(title),
message: sanitize_input(message)
)
if target_user.assignments_notification
UserNotification.create(notification: notification, user: target_user)
@ -122,11 +120,13 @@ module ApplicationHelper
project = Project.find_by_id(match[3].base62_decode)
next unless project
if project.archived?
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
"<span class='sa-type'>" \
"#{sanitize_input(match[2])}</span> " \
"#{link_to project.name,
projects_archive_path} #{I18n.t('atwho.res.archived')}"
else
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
"<span class='sa-type'>" \
"#{sanitize_input(match[2])}</span> " \
"#{link_to project.name,
project_path(project)}"
end
@ -134,12 +134,14 @@ module ApplicationHelper
experiment = Experiment.find_by_id(match[3].base62_decode)
next unless experiment
if experiment.archived?
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
"<span class='sa-type'>" \
"#{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(match[2])}</span> " \
"<span class='sa-type'>"\
"#{sanitize_input(match[2])}</span> " \
"#{link_to experiment.name,
canvas_experiment_path(experiment)}"
end
@ -147,12 +149,14 @@ module ApplicationHelper
my_module = MyModule.find_by_id(match[3].base62_decode)
next unless my_module
if my_module.archived?
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
"<span class='sa-type'>" \
"#{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(match[2])}</span> " \
"<span class='sa-type'>" \
"#{sanitize_input(match[2])}</span> " \
"#{link_to my_module.name,
protocols_my_module_path(my_module)}"
end
@ -213,19 +217,20 @@ module ApplicationHelper
user_description += %(<p></p></div></div></div>)
end
raw(image_tag(user_avatar_absolute_url(user, :icon_small),
class: 'atwho-user-img-popover')) +
raw('<a onClick="$(this).popover(\'show\')" ' \
'class="atwho-user-popover" data-container="body" ' \
'data-html="true" tabindex="0" data-trigger="focus" ' \
'data-placement="top" data-toggle="popover" data-content="') +
raw(user_description) + raw('" >') + user.full_name + raw('</a>')
raw("<img src='#{user_avatar_absolute_url(user, :icon_small)}'" \
"alt='avatar' class='atwho-user-img-popover'>") +
raw('<a onClick="$(this).popover(\'show\')" ' \
'class="atwho-user-popover" data-container="body" ' \
'data-html="true" tabindex="0" data-trigger="focus" ' \
'data-placement="top" data-toggle="popover" data-content="') +
raw(user_description) + raw('" >') + user.full_name + raw('</a>')
end
def user_avatar_absolute_url(user, style)
unless user.avatar(style) == '/images/icon_small/missing.png'
return user.avatar(style)
end
URI.join(root_url, "/images/#{style}/missing.png").to_s
URI.join(Rails.application.routes.url_helpers.root_url,
"/images/#{style}/missing.png").to_s
end
end

View file

@ -1,14 +1,17 @@
require 'sanitize'
module InputSanitizeHelper
def sanitize_input(
text,
tags = [],
attributes = []
)
ActionController::Base.helpers.sanitize(
text,
tags: Constants::WHITELISTED_TAGS + tags,
attributes: Constants::WHITELISTED_ATTRIBUTES + attributes
)
# Rails default ActionController::Base.helpers.sanitize method call
# the ActiveRecord connecton method on the caller object which in
# our cases throws an error when called from not ActiveRecord objects
# such as SamplesDatatables
def sanitize_input(html, tags = [], attributes = [])
Sanitize.fragment(
html,
elements: Constants::WHITELISTED_TAGS + tags,
attributes: { all: Constants::WHITELISTED_ATTRIBUTES + attributes },
css: Constants::WHITELISTED_CSS_ATTRIBUTES
).html_safe
end
def escape_input(text)
@ -16,15 +19,13 @@ module InputSanitizeHelper
end
def custom_auto_link(text, options = {})
simple_format = options.fetch(:simple_format) { true }
team = options.fetch(:team) { nil },
simple_f = options.fetch(:simple_format) { true }
team = options.fetch(:team) { nil }
wrapper_tag = options.fetch(:wrapper_tag) { {} }
tags = options.fetch(:tags) { [] }
text = if simple_format
simple_format(sanitize_input(text), {}, wrapper_tag)
else
sanitize_input(text, tags)
end
format_opt = wrapper_tag.merge(sanitize: false)
text = sanitize_input(text, tags)
text = simple_format(sanitize_input(text), {}, format_opt) if simple_f
auto_link(
smart_annotation_parser(text, team),
link: :urls,

View file

@ -40,10 +40,8 @@ module NotificationsHelper
notification = Notification.create(
type_of: :assignment,
title:
ActionController::Base.helpers.sanitize(title),
message:
ActionController::Base.helpers.sanitize(message)
title: sanitize_input(title),
message: sanitize_input(message)
)
if target_user.assignments_notification

View file

@ -11,10 +11,11 @@ module SearchHelper
if search_team != current_team
link_to text,
path,
data: { confirm: t('users.settings.changed_team_in_search',
data: { no_turbolink: true,
confirm: t('users.settings.changed_team_in_search',
team: search_team.name) }
else
link_to text, path
link_to text, path, data: { no_turbolink: true }
end
end

View file

@ -23,7 +23,9 @@ module TinyMceHelper
match = el.match(regex)
img = TinyMceAsset.find_by_id(match[1])
next unless img
image_tag img.url, data: { token: Base62.encode(img.id) }
image_tag img.url,
class: 'img-responsive',
data: { token: Base62.encode(img.id) }
end
end

View file

@ -1,4 +1,6 @@
class Activity < ActiveRecord::Base
include InputSanitizeHelper
after_create :generate_notification
enum type_of: [
@ -104,15 +106,12 @@ class Activity < ActiveRecord::Base
notification = Notification.create(
type_of: notification_type,
title:
ActionController::Base.helpers.sanitize(message, tags: %w(strong a)),
message:
ActionController::Base
.helpers.sanitize(
"#{I18n.t('search.index.project')}
#{project_m} #{experiment_m} #{task_m}",
tags: %w(strong a)
),
title: sanitize_input(message, %w(strong a)),
message: sanitize_input(
"#{I18n.t('search.index.project')}
#{project_m} #{experiment_m} #{task_m}",
%w(strong a)
),
generator_user_id: user.id
)

View file

@ -22,6 +22,8 @@ class Checklist < ActiveRecord::Base
reject_if: :all_blank,
allow_destroy: true
scope :asc, -> { order('checklists.created_at ASC') }
def self.search(user,
include_archived,
query = nil,

View file

@ -1,5 +1,6 @@
class Experiment < ActiveRecord::Base
include ArchivableModel, SearchableModel
include ArchivableModel
include SearchableModel
belongs_to :project, inverse_of: :experiments
belongs_to :created_by, foreign_key: :created_by_id, class_name: 'User'
@ -354,7 +355,7 @@ class Experiment < ActiveRecord::Base
i += 1 while experiment_names.include?(format(format, i, name))
clone = Experiment.new(
name: format(format, i, name),
name: format(format, i, name).truncate(Constants::NAME_MAX_LENGTH),
description: description,
created_by: current_user,
last_modified_by: current_user,
@ -371,10 +372,6 @@ class Experiment < ActiveRecord::Base
m.deep_clone_to_experiment(current_user, clone)
end
clone.save
# Create workflow image
clone.delay.generate_workflow_img
clone
end

View file

@ -189,4 +189,12 @@ class Project < ActiveRecord::Base
.distinct
end
end
def notifications_count(user)
res = 0
assigned_modules(user).find_each do |t|
res += 1 if t.is_overdue? || t.is_one_day_prior?
end
res
end
end

View file

@ -268,7 +268,7 @@ class Protocol < ActiveRecord::Base
step2.save
# Copy checklists
step.checklists.each do |checklist|
step.checklists.asc.each do |checklist|
checklist2 = Checklist.new(
name: checklist.name,
step: step2
@ -372,7 +372,7 @@ class Protocol < ActiveRecord::Base
end
def completed_steps
steps.select(&:completed)
steps.where(completed: true)
end
def space_taken

View file

@ -3,4 +3,11 @@ class TempFile < ActiveRecord::Base
has_attached_file :file
do_not_validate_attachment_file_type :file
def destroy_obsolete
destroy! if self
end
handle_asynchronously :destroy_obsolete,
run_at: proc { 7.days.from_now }
end

View file

@ -25,7 +25,7 @@ class TinyMceAsset < ActiveRecord::Base
# When using S3 file upload, we can limit file accessibility with url signing
def presigned_url(style = :large,
download: false,
timeout: Constants::URL_SHORT_EXPIRE_TIME)
timeout: Constants::URL_LONG_EXPIRE_TIME)
if stored_on_s3?
if download
download_arg = 'attachment; filename=' + URI.escape(image_file_name)
@ -46,7 +46,7 @@ class TinyMceAsset < ActiveRecord::Base
image.options[:storage].to_sym == :s3
end
def url(style = :large, timeout: Constants::URL_SHORT_EXPIRE_TIME)
def url(style = :large, timeout: Constants::URL_LONG_EXPIRE_TIME)
if image.is_stored_on_s3?
presigned_url(style, timeout: timeout)
else

View file

@ -44,14 +44,14 @@
<ul class="nav nav-tabs nav-tabs-less" role="tablist">
<% if can_view_module_info(my_module) %>
<li role="presentation">
<a class="btn btn-link" href="<%= my_module_url(id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_info" role="tab" data-remote="true">
<a class="btn btn-link task-card-view-info" href="<%= my_module_url(id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_info" role="tab" data-remote="true">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</a>
</li>
<% end %>
<% if can_view_module_users(my_module) %>
<li role="presentation">
<a class="btn btn-link" href="<%= my_module_user_my_modules_url(my_module_id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_users" role="tab" data-remote="true">
<a class="btn btn-link task-card-view-users" href="<%= my_module_user_my_modules_url(my_module_id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_users" role="tab" data-remote="true">
<span class="glyphicon glyphicon-user" aria-hidden="true"></span>
<span class="badge badge-indicator users-badge-indicator <%= 'hidden' unless my_module.users.count.positive? %>"
data-linked-id="<%= my_module.id %>">
@ -62,14 +62,14 @@
<% end %>
<% if can_view_module_activities(my_module) %>
<li role="presentation">
<a class="btn btn-link" href="<%= activities_tab_my_module_url(id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_activities" role="tab" data-remote="true">
<a class="btn btn-link task-card-view-activities" href="<%= activities_tab_my_module_url(id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_activities" role="tab" data-remote="true">
<span class="glyphicon glyphicon-equalizer" aria-hidden="true"></span>
</a>
</li>
<% end %>
<% if can_view_module_comments(my_module) %>
<li role="presentation">
<a class="btn btn-link" href="<%= my_module_my_module_comments_url(my_module_id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_comments" role="tab" data-remote="true">
<a class="btn btn-link task-card-view-comments" href="<%= my_module_my_module_comments_url(my_module_id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_comments" role="tab" data-remote="true">
<span class="glyphicon glyphicon-comment" aria-hidden="true"></span>
<span class="badge badge-indicator comments-badge-indicator <%= 'hidden' unless my_module.task_comments.count.positive? %>"
data-linked-id="<%= my_module.id %>">
@ -80,7 +80,7 @@
<% end %>
<% if can_view_module_samples(my_module) %>
<li role="presentation">
<a class="btn btn-link" href="<%= my_module_sample_my_modules_url(my_module_id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_samples" role="tab" data-remote="true">
<a class="btn btn-link task-card-view-samples" href="<%= my_module_sample_my_modules_url(my_module_id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_samples" role="tab" data-remote="true">
<span class="glyphicon glyphicon-tint" aria-hidden="true"></span>
<% if my_module.samples.count.positive? %>
<span class="badge badge-indicator"><%= my_module.samples.count %></span>

View file

@ -75,6 +75,9 @@ data-project-users-tab-url="<%= url_for project_user_projects_path(project_id: p
<li role="presentation">
<a class="btn btn-link" href="<%= url_for notifications_project_path(id: project.id, format: :json) %>" aria-controls="notifications-<%= project.id %>" role="tab" data-remote="true">
<span class="glyphicon glyphicon-bell"></span>
<% if project.notifications_count(current_user).positive? %>
<span class="badge badge-indicator"><%= project.notifications_count(current_user) %></span>
<% end %>
</a>
</li>
<% end %>

View file

@ -160,7 +160,7 @@
<% unless step.checklists.blank? then %>
<div class="col-xs-12">
<% step.checklists.each do |checklist| %>
<% step.checklists.asc.each do |checklist| %>
<strong><%= checklist.name %></strong>
<% if checklist.checklist_items.empty? %>
</br>

View file

@ -7,6 +7,44 @@
</div>
<div class="modal-body">
<p>
<span>
<%= t "samples.modal_info.sample_type" %>
<% if @sample.sample_type.present? %>
<%= @sample.sample_type.name %>
<% else %>
<em><%= t "samples.modal_info.no_type" %></em>
<% end %>
</span>
<br>
<span>
<%= t "samples.modal_info.sample_group" %>
<span class="glyphicon glyphicon-asterisk" style="<%= "color: #{@sample.sample_group.color}" if @sample.sample_group.present? %>"></span>
<% if @sample.sample_group.present? %>
<%= @sample.sample_group.name %>
<% else %>
<em><%= t "samples.modal_info.no_group" %></em>
<% end %>
</span>
<br>
<span>
<%= t "samples.modal_info.added_on" %>
<%= l @sample.created_at, format: :full %>
</span>
<br>
<span>
<%= t "samples.modal_info.added_by" %>
<%= @sample.user.full_name %>
</span>
<% @sample.sample_custom_fields.each do |sample_custom_field| %>
<br>
<span>
<%= t "samples.modal_info.custom_field", cf: sample_custom_field.custom_field.name %>
<%= sample_custom_field.value %>
</span>
<% end %>
</p>
<% if @sample.my_modules.count > 0 %>
<div>
<%= t("samples.modal_info.title", nr: @sample.my_modules.count) %>

View file

@ -134,7 +134,7 @@
<% unless step.checklists.blank? then %>
<div class="col-xs-12">
<% step.checklists.each do |checklist| %>
<% step.checklists.asc.each do |checklist| %>
<strong><%= custom_auto_link(checklist.name) %></strong>
<% if checklist.checklist_items.empty? %>
</br>

View file

@ -13,6 +13,10 @@ Rails.application.configure do
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
Rails.application.routes.default_url_options = {
host: Rails.application.secrets.mail_server_url
}
# Don't care if the mailer can't send.
config.action_mailer.default_url_options = {
host: Rails.application.secrets.mail_server_url

View file

@ -14,6 +14,10 @@ Rails.application.configure do
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
Rails.application.routes.default_url_options = {
host: Rails.application.secrets.mail_server_url
}
# Don't care if the mailer can't send.
config.action_mailer.default_url_options = {
host: Rails.application.secrets.mail_server_url

View file

@ -36,6 +36,10 @@ Rails.application.configure do
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
Rails.application.routes.default_url_options = {
host: Rails.application.secrets.mail_server_url
}
# Don't care if the mailer can't send.
config.action_mailer.default_url_options = { host: Rails.application.secrets.mail_server_url }
config.action_mailer.default_options = { from: Rails.application.secrets.mail_from }

View file

@ -222,8 +222,643 @@ class Constants
WHITELISTED_ATTRIBUTES = %w(
href src width height alt cite datetime title class name xml:lang abbr style
target data-*
).freeze
WHITELISTED_CSS_ATTRIBUTES = {
allow_comments: false,
allow_hacks: false,
at_rules_with_properties: %w[
bottom-center bottom-left bottom-left-corner bottom-right
bottom-right-corner font-face left-bottom left-middle left-top page
right-bottom right-middle right-top top-center top-left
top-left-corner top-right top-right-corner
],
at_rules_with_styles: %w[
-moz-keyframes -o-keyframes -webkit-keyframes document
keyframes media supports
],
protocols: ['http', 'https', :relative],
properties: %w[
-moz-appearance
-moz-background-inline-policy
-moz-box-sizing
-moz-column-count
-moz-column-fill
-moz-column-gap
-moz-column-rule
-moz-column-rule-color
-moz-column-rule-style
-moz-column-rule-width
-moz-column-width
-moz-font-feature-settings
-moz-font-language-override
-moz-hyphens
-moz-text-align-last
-moz-text-decoration-color
-moz-text-decoration-line
-moz-text-decoration-style
-moz-text-size-adjust
-ms-background-position-x
-ms-background-position-y
-ms-block-progression
-ms-content-zoom-chaining
-ms-content-zoom-limit
-ms-content-zoom-limit-max
-ms-content-zoom-limit-min
-ms-content-zoom-snap
-ms-content-zoom-snap-points
-ms-content-zoom-snap-type
-ms-content-zooming
-ms-filter
-ms-flex
-ms-flex-align
-ms-flex-direction
-ms-flex-order
-ms-flex-pack
-ms-flex-wrap
-ms-flow-from
-ms-flow-into
-ms-grid-column
-ms-grid-column-align
-ms-grid-column-span
-ms-grid-columns
-ms-grid-row
-ms-grid-row-align
-ms-grid-row-span
-ms-grid-rows
-ms-high-contrast-adjust
-ms-hyphenate-limit-chars
-ms-hyphenate-limit-lines
-ms-hyphenate-limit-zone
-ms-hyphens
-ms-ime-mode
-ms-interpolation-mode
-ms-layout-flow
-ms-layout-grid
-ms-layout-grid-char
-ms-layout-grid-line
-ms-layout-grid-mode
-ms-layout-grid-type
-ms-overflow-style
-ms-overflow-x
-ms-overflow-y
-ms-progress-appearance
-ms-scroll-chaining
-ms-scroll-limit
-ms-scroll-limit-x-max
-ms-scroll-limit-x-min
-ms-scroll-limit-y-max
-ms-scroll-limit-y-min
-ms-scroll-rails
-ms-scroll-snap-points-x
-ms-scroll-snap-points-y
-ms-scroll-snap-type
-ms-scroll-snap-x
-ms-scroll-snap-y
-ms-scroll-translation
-ms-scrollbar-arrow-color
-ms-scrollbar-base-color
-ms-scrollbar-darkshadow-color
-ms-scrollbar-face-color
-ms-scrollbar-highlight-color
-ms-scrollbar-shadow-color
-ms-scrollbar-track-color
-ms-text-align-last
-ms-text-autospace
-ms-text-justify
-ms-text-kashida-space
-ms-text-overflow
-ms-text-size-adjust
-ms-text-underline-position
-ms-touch-action
-ms-user-select
-ms-word-break
-ms-word-wrap
-ms-wrap-flow
-ms-wrap-margin
-ms-wrap-through
-ms-writing-mode
-ms-zoom
-webkit-align-content
-webkit-align-items
-webkit-align-self
-webkit-animation
-webkit-animation-delay
-webkit-animation-direction
-webkit-animation-duration
-webkit-animation-fill-mode
-webkit-animation-iteration-count
-webkit-animation-name
-webkit-animation-play-state
-webkit-animation-timing-function
-webkit-appearance
-webkit-backface-visibility
-webkit-background-blend-mode
-webkit-background-clip
-webkit-background-composite
-webkit-background-origin
-webkit-background-size
-webkit-blend-mode
-webkit-border-after
-webkit-border-after-color
-webkit-border-after-style
-webkit-border-after-width
-webkit-border-before
-webkit-border-before-color
-webkit-border-before-style
-webkit-border-before-width
-webkit-border-bottom-left-radius
-webkit-border-bottom-right-radius
-webkit-border-end
-webkit-border-end-color
-webkit-border-end-style
-webkit-border-end-width
-webkit-border-fit
-webkit-border-image
-webkit-border-radius
-webkit-border-start
-webkit-border-start-color
-webkit-border-start-style
-webkit-border-start-width
-webkit-border-top-left-radius
-webkit-border-top-right-radius
-webkit-box-align
-webkit-box-decoration-break
-webkit-box-flex
-webkit-box-flex-group
-webkit-box-lines
-webkit-box-ordinal-group
-webkit-box-orient
-webkit-box-pack
-webkit-box-reflect
-webkit-box-shadow
-webkit-box-sizing
-webkit-clip-path
-webkit-column-axis
-webkit-column-break-after
-webkit-column-break-before
-webkit-column-break-inside
-webkit-column-count
-webkit-column-gap
-webkit-column-progression
-webkit-column-rule
-webkit-column-rule-color
-webkit-column-rule-style
-webkit-column-rule-width
-webkit-column-span
-webkit-column-width
-webkit-columns
-webkit-filter
-webkit-flex
-webkit-flex-basis
-webkit-flex-direction
-webkit-flex-flow
-webkit-flex-grow
-webkit-flex-shrink
-webkit-flex-wrap
-webkit-flow-from
-webkit-flow-into
-webkit-font-size-delta
-webkit-font-smoothing
-webkit-grid-area
-webkit-grid-auto-columns
-webkit-grid-auto-flow
-webkit-grid-auto-rows
-webkit-grid-column
-webkit-grid-column-end
-webkit-grid-column-start
-webkit-grid-definition-columns
-webkit-grid-definition-rows
-webkit-grid-row
-webkit-grid-row-end
-webkit-grid-row-start
-webkit-justify-content
-webkit-line-clamp
-webkit-logical-height
-webkit-logical-width
-webkit-margin-after
-webkit-margin-after-collapse
-webkit-margin-before
-webkit-margin-before-collapse
-webkit-margin-bottom-collapse
-webkit-margin-collapse
-webkit-margin-end
-webkit-margin-start
-webkit-margin-top-collapse
-webkit-marquee
-webkit-marquee-direction
-webkit-marquee-increment
-webkit-marquee-repetition
-webkit-marquee-speed
-webkit-marquee-style
-webkit-mask
-webkit-mask-box-image
-webkit-mask-box-image-outset
-webkit-mask-box-image-repeat
-webkit-mask-box-image-slice
-webkit-mask-box-image-source
-webkit-mask-box-image-width
-webkit-mask-clip
-webkit-mask-composite
-webkit-mask-image
-webkit-mask-origin
-webkit-mask-position
-webkit-mask-position-x
-webkit-mask-position-y
-webkit-mask-repeat
-webkit-mask-repeat-x
-webkit-mask-repeat-y
-webkit-mask-size
-webkit-mask-source-type
-webkit-max-logical-height
-webkit-max-logical-width
-webkit-min-logical-height
-webkit-min-logical-width
-webkit-opacity
-webkit-order
-webkit-padding-after
-webkit-padding-before
-webkit-padding-end
-webkit-padding-start
-webkit-perspective
-webkit-perspective-origin
-webkit-perspective-origin-x
-webkit-perspective-origin-y
-webkit-region-break-after
-webkit-region-break-before
-webkit-region-break-inside
-webkit-region-fragment
-webkit-shape-inside
-webkit-shape-margin
-webkit-shape-outside
-webkit-shape-padding
-webkit-svg-shadow
-webkit-tap-highlight-color
-webkit-text-decoration
-webkit-text-decoration-color
-webkit-text-decoration-line
-webkit-text-decoration-style
-webkit-text-size-adjust
-webkit-touch-callout
-webkit-transform
-webkit-transform-origin
-webkit-transform-origin-x
-webkit-transform-origin-y
-webkit-transform-origin-z
-webkit-transform-style
-webkit-transition
-webkit-transition-delay
-webkit-transition-duration
-webkit-transition-property
-webkit-transition-timing-function
-webkit-user-drag
-webkit-wrap-flow
-webkit-wrap-through
align-content
align-items
align-self
alignment-adjust
alignment-baseline
all
anchor-point
animation
animation-delay
animation-direction
animation-duration
animation-fill-mode
animation-iteration-count
animation-name
animation-play-state
animation-timing-function
azimuth
backface-visibility
background
background-attachment
background-clip
background-color
background-image
background-origin
background-position
background-repeat
background-size
baseline-shift
binding
bleed
bookmark-label
bookmark-level
bookmark-state
border
border-bottom
border-bottom-color
border-bottom-left-radius
border-bottom-right-radius
border-bottom-style
border-bottom-width
border-collapse
border-color
border-image
border-image-outset
border-image-repeat
border-image-slice
border-image-source
border-image-width
border-left
border-left-color
border-left-style
border-left-width
border-radius
border-right
border-right-color
border-right-style
border-right-width
border-spacing
border-style
border-top
border-top-color
border-top-left-radius
border-top-right-radius
border-top-style
border-top-width
border-width
bottom
box-decoration-break
box-shadow
box-sizing
box-snap
box-suppress
break-after
break-before
break-inside
caption-side
chains
clear
clip
clip-path
clip-rule
color
color-interpolation-filters
column-count
column-fill
column-gap
column-rule
column-rule-color
column-rule-style
column-rule-width
column-span
column-width
columns
contain
content
counter-increment
counter-reset
counter-set
crop
cue
cue-after
cue-before
cursor
direction
display
display-inside
display-list
display-outside
dominant-baseline
elevation
empty-cells
filter
flex
flex-basis
flex-direction
flex-flow
flex-grow
flex-shrink
flex-wrap
float
float-offset
flood-color
flood-opacity
flow-from
flow-into
font
font-family
font-feature-settings
font-kerning
font-language-override
font-size
font-size-adjust
font-stretch
font-style
font-synthesis
font-variant
font-variant-alternates
font-variant-caps
font-variant-east-asian
font-variant-ligatures
font-variant-numeric
font-variant-position
font-weight
grid
grid-area
grid-auto-columns
grid-auto-flow
grid-auto-rows
grid-column
grid-column-end
grid-column-start
grid-row
grid-row-end
grid-row-start
grid-template
grid-template-areas
grid-template-columns
grid-template-rows
hanging-punctuation
height
hyphens
icon
image-orientation
image-rendering
image-resolution
ime-mode
initial-letters
inline-box-align
justify-content
justify-items
justify-self
left
letter-spacing
lighting-color
line-box-contain
line-break
line-grid
line-height
line-snap
line-stacking
line-stacking-ruby
line-stacking-shift
line-stacking-strategy
list-style
list-style-image
list-style-position
list-style-type
margin
margin-bottom
margin-left
margin-right
margin-top
marker-offset
marker-side
marks
mask
mask-box
mask-box-outset
mask-box-repeat
mask-box-slice
mask-box-source
mask-box-width
mask-clip
mask-image
mask-origin
mask-position
mask-repeat
mask-size
mask-source-type
mask-type
max-height
max-lines
max-width
min-height
min-width
move-to
nav-down
nav-index
nav-left
nav-right
nav-up
object-fit
object-position
opacity
order
orphans
outline
outline-color
outline-offset
outline-style
outline-width
overflow
overflow-wrap
overflow-x
overflow-y
padding
padding-bottom
padding-left
padding-right
padding-top
page
page-break-after
page-break-before
page-break-inside
page-policy
pause
pause-after
pause-before
perspective
perspective-origin
pitch
pitch-range
play-during
position
presentation-level
quotes
region-fragment
resize
rest
rest-after
rest-before
richness
right
rotation
rotation-point
ruby-align
ruby-merge
ruby-position
shape-image-threshold
shape-margin
shape-outside
size
speak
speak-as
speak-header
speak-numeral
speak-punctuation
speech-rate
stress
string-set
tab-size
table-layout
text-align
text-align-last
text-combine-horizontal
text-combine-upright
text-decoration
text-decoration-color
text-decoration-line
text-decoration-skip
text-decoration-style
text-emphasis
text-emphasis-color
text-emphasis-position
text-emphasis-style
text-height
text-indent
text-justify
text-orientation
text-overflow
text-rendering
text-shadow
text-size-adjust
text-space-collapse
text-transform
text-underline-position
text-wrap
top
touch-action
transform
transform-origin
transform-style
transition
transition-delay
transition-duration
transition-property
transition-timing-function
unicode-bidi
unicode-range
vertical-align
visibility
voice-balance
voice-duration
voice-family
voice-pitch
voice-range
voice-rate
voice-stress
voice-volume
volume
white-space
widows
width
will-change
word-break
word-spacing
word-wrap
wrap-flow
wrap-through
writing-mode
z-index
]
}.freeze
EXPORTABLE_ZIP_EXPIRATION_DAYS = 7
# Very basic regex to check for validity of emails

View file

@ -52,7 +52,7 @@ module ReportExtends
true,
[:step],
proc do |my_module|
my_module.protocol.completed_steps
my_module.protocol.completed_steps.order(:position)
end),
ModuleElement.new(:result_assets,
true,

View file

@ -850,9 +850,16 @@ en:
add_new_sample_group: "Add sample group"
add_new_column: "Add column"
modal_info:
added_on: "Added on"
added_by: "Added by"
custom_field: "%{cf}: "
head_title: "Information for sample '%{sample}'"
title: "This sample is assigned to %{nr} tasks."
no_tasks: "This sample in not assigned to any task."
no_group: "No sample group"
no_type: "No sample type"
sample_group: "Sample group:"
sample_type: "Sample type:"
modal_import:
title: "Import samples"
notice: "You may upload .csv file (comma separated) or tab separated file (.txt or .tdv) or Excel file (.xls, .xlsx). First row should include header names, followed by rows with sample data."
@ -1013,8 +1020,8 @@ en:
add_comment_to_step: "<i>%{user}</i> commented on Step %{step} <strong>%{step_name}</strong>."
complete_step: "<i>%{user}</i> completed Step %{step} <strong>%{step_name}</strong> (%{completed}/%{all} completed)."
uncomplete_step: "<i>%{user}</i> uncompleted Step %{step} <strong>%{step_name}</strong> (%{completed}/%{all} completed)."
check_step_checklist_item: "<i>%{user}</i> completed task <strong>%{checkbox}</strong> (%{completed}/%{all} completed) in Step %{step} <strong>%{step_name}</strong>."
uncheck_step_checklist_item: "<i>%{user}</i> uncompleted task <strong>%{checkbox}</strong> (%{completed}/%{all} completed) in Step %{step} <strong>%{step_name}</strong>."
check_step_checklist_item: "<i>%{user}</i> completed checklist item <strong>%{checkbox}</strong> (%{completed}/%{all} completed) in Step %{step} <strong>%{step_name}</strong>."
uncheck_step_checklist_item: "<i>%{user}</i> uncompleted checklist item <strong>%{checkbox}</strong> (%{completed}/%{all} completed) in Step %{step} <strong>%{step_name}</strong>."
edit_step: "<i>%{user}</i> edited Step %{step} <strong>%{step_name}</strong>."
add_asset_result: "<i>%{user}</i> added file result <strong>%{result}</strong>."
add_text_result: "<i>%{user}</i> added text result <strong>%{result}</strong>."