Merge branch 'smart-annotations'

Conflicts:
	app/assets/javascripts/comments.js.erb
	app/assets/javascripts/protocols/index.js
	app/assets/javascripts/protocols/steps.js.erb
	app/assets/javascripts/samples/sample_datatable.js.erb
	app/controllers/my_module_comments_controller.rb
	app/controllers/project_comments_controller.rb
	app/controllers/result_comments_controller.rb
	app/controllers/step_comments_controller.rb
	app/datatables/sample_datatable.rb
	app/views/my_module_comments/_comment.html.erb
	app/views/project_comments/_comment.html.erb
	app/views/projects/show/_experiment.html.erb
	app/views/reports/elements/_experiment_element.html.erb
	app/views/reports/elements/_result_comments_element.html.erb
	app/views/reports/elements/_result_text_element.html.erb
	app/views/reports/elements/_step_checklist_element.html.erb
	app/views/reports/elements/_step_comments_element.html.erb
	app/views/reports/elements/_step_element.html.erb
	app/views/result_comments/_comment.html.erb
	app/views/results/_result_text.html.erb
	app/views/step_comments/_comment.html.erb
	app/views/steps/_step.html.erb
	db/schema.rb
This commit is contained in:
Luka Murn 2017-01-24 14:33:23 +01:00
commit 53699193a2
76 changed files with 1775 additions and 288 deletions

View file

@ -64,9 +64,9 @@ gem 'aws-sdk-v1'
gem 'delayed_job_active_record'
gem 'devise-async'
gem 'ruby-graphviz', '~> 1.2' # Graphviz for rails
gem 'quill-rails', # Rich text editor
git: 'https://github.com/biosistemika/quill-rails.git',
ref: 'e765c04'
gem 'tinymce-rails' # Rich text editor
gem 'base62' # Used for smart annotations
group :development, :test do
gem 'byebug'

View file

@ -12,6 +12,8 @@
//= require jquery-ui/draggable
//= require jquery-ui/droppable
//= require jquery.ui.touch-punch.min
//= require jquery.caret.min
//= require jquery.atwho.min
//= require hammer
//= require introjs
//= require js.cookie
@ -25,14 +27,16 @@
//= require bootstrap-checkbox.min
//= require typeahead.bundle.min
//= require nested_form_fields
//= require highlight.pack
//= require tinymce-jquery
//= require_directory ./sitewide
//= require datatables
//= require dataTables.noSearchHidden
//= require bootstrap-select
//= require underscore
//= require i18n.js
//= require i18n/translations
//= require turbolinks
//= require quill
// Initialize links for submitting forms. This is useful for submitting
@ -227,3 +231,12 @@ var HelperModule = (function(){
return helpers;
})();
// initialize code markup in rich text fields
(function() {
$(document).ready(function() {
$('[class^=language]').each(function(i, block) {
hljs.highlightBlock(block);
});
});
})();

View file

@ -1,6 +1,5 @@
//= require protocols/import_export/import
//= require comments
//= require datatables
// Currently selected row in "load from protocol" modal
var selectedRow = null;

View file

@ -181,8 +181,8 @@ function processResult(ev, resultTypeEnum, editMode) {
var $nameInput = $form.find("#result_name");
var nameValid = textValidator(ev, $nameInput, 0,
<%= Constants::NAME_MAX_LENGTH %>);
var $textInput = $form.find("#result_result_text_attributes_text");
textValidator(ev, $textInput, 1, <%= Constants::TEXT_MAX_LENGTH %>);
var $textInput = TinyMCE.getContent();
textValidator(ev, $textInput, 1, <%= Constants::TEXT_MAX_LENGTH %>, false, true);
break;
case ResultTypeEnum.COMMENT:
var $commentInput = $form.find("#comment_message");

View file

@ -1,5 +1,4 @@
//= require protocols/import_export/import
//= require datatables
// Global variables
var rowsSelected = [];

View file

@ -8,7 +8,6 @@
function applyCheckboxCallBack() {
$("[data-action='check-item']").on('click', function(e){
var checkboxitem = $(this).find("input");
var checked = checkboxitem.is(":checked");
$.ajax({
url: checkboxitem.data("link-url"),
@ -79,8 +78,9 @@ function applyCancelCallBack() {
setTimeout(function() {
initStepsComments();
openLinksInNewTab();
initPreviewModal();
SmartAnnotation.preventPropagation('.atwho-user-popover');
TinyMCE.destroyAll();
}, 1000);
})
@ -107,6 +107,7 @@ function applyEditCallBack() {
animateSpinner(null, false);
initPreviewModal();
TinyMCE.init();
$("#new-step-checklists fieldset.nested_step_checklists ul").each(function () {
enableCheckboxSorting(this);
});
@ -114,7 +115,6 @@ function applyEditCallBack() {
$("#new-step-main-tab a").on("shown.bs.tab", function() {
$("#step_name").focus();
});
openLinksInNewTab();
});
}
@ -185,8 +185,12 @@ function formCallback($form) {
setTimeout(function() {
initStepsComments();
<<<<<<< HEAD
openLinksInNewTab();
animateSpinner(null, false);
=======
SmartAnnotation.preventPropagation('.atwho-user-popover');
>>>>>>> smart-annotations
}, 1000);
return true;
});
@ -210,6 +214,8 @@ function formEditAjax($form) {
toggleButtons(true);
initPreviewModal();
TinyMCE.destroyAll();
SmartAnnotation.preventPropagation('.atwho-user-popover');
// Show the edited step
$new_step.find(".panel-collapse:first").addClass("collapse in");
@ -226,6 +232,9 @@ function formEditAjax($form) {
initEditableHandsOnTable($form);
applyCancelCallBack();
TinyMCE.refresh();
SmartAnnotation.preventPropagation('.atwho-user-popover');
//Rerender tables
$form.find("[data-role='step-hot-table']").each(function() {
renderTable($(this));
@ -250,6 +259,10 @@ function formNewAjax($form) {
expandStep($new_step);
toggleButtons(true);
TinyMCE.init();
TinyMCE.highlight();
SmartAnnotation.preventPropagation('.atwho-user-popover');
//Rerender tables
$new_step.find("div.step-result-hot-table").each(function() {
$(this).handsontable("render");
@ -268,7 +281,12 @@ function formNewAjax($form) {
formCallback($form);
formNewAjax($form);
applyCancelOnNew();
<<<<<<< HEAD
animateSpinner(null, false);
=======
TinyMCE.destroyAll();
SmartAnnotation.preventPropagation('.atwho-user-popover');
>>>>>>> smart-annotations
});
}
@ -393,7 +411,7 @@ function initCallBacks() {
applyMoveStepCallBack();
applyCollapseLinkCallBack();
initDeleteStep();
initHighlightjs();
TinyMCE.highlight();
}
/*
@ -479,7 +497,6 @@ function initializeCheckboxSorting() {
// inserted into DOM.
setTimeout(function () {
var list = el.parent().find("fieldset.nested_step_checklists:last ul");
enableCheckboxSorting(list.get(0));
});
});
@ -497,6 +514,7 @@ $("[data-action='new-step']").on("ajax:success", function(e, data) {
applyCancelOnNew();
toggleButtons(false);
initializeCheckboxSorting();
TinyMCE.init();
$("#step_name").focus();
$("#new-step-main-tab a").on("shown.bs.tab", function() {
@ -560,14 +578,6 @@ function renderTable(table) {
}
}
function initHighlightjs() {
if(hljs){
$('.ql-editor pre').each(function(i, block) {
hljs.highlightBlock(block);
});
}
}
function initStepsComments() {
Comments.initialize();
Comments.initCommentOptions("ul.content-comments");
@ -582,8 +592,9 @@ $(document).ready(function() {
expandAllSteps();
setupAssetsLoading();
initStepsComments();
initHighlightjs();
initPreviewModal();
TinyMCE.highlight();
SmartAnnotation.preventPropagation('.atwho-user-popover');
$(function () {

View file

@ -11,6 +11,7 @@ $("#new-result-text").on("ajax:success", function(e, data) {
toggleResultEditButtons(true);
});
TinyMCE.init();
toggleResultEditButtons(false);
$("#result_name").focus();
@ -39,6 +40,7 @@ function applyEditResultTextCallback() {
toggleResultEditButtons(true);
});
TinyMCE.init();
toggleResultEditButtons(false);
$("#result_name").focus();
@ -61,12 +63,12 @@ function formAjaxResultText($form) {
applyCollapseLinkCallBack();
toggleResultEditButtons(true);
expandResult(newResult);
initHighlightjs();
TinyMCE.destroyAll();
});
$form.on("ajax:error", function(e, xhr, status, error) {
var data = xhr.responseJSON;
$form.renderFormErrors("result", data);
initHighlightjs();
TinyMCE.highlight();
if (data["result_text.text"]) {
var $el = $form.find("textarea[name=result\\[result_text_attributes\\]\\[text\\]]");
@ -76,15 +78,7 @@ function formAjaxResultText($form) {
});
}
function initHighlightjs() {
if(hljs) {
$('.ql-editor pre').each(function(i, block) {
hljs.highlightBlock(block);
});
}
}
$(document).ready(function() {
initHighlightjs();
TinyMCE.highlight();
});
applyEditResultTextCallback();

View file

@ -73,9 +73,8 @@ function dataTableInit() {
}, {
targets: 2,
render: function(data, type, row) {
return "<a href='#' data-href='" + row.sampleUpdateUrl + "'" +
"class='sample_info' data-toggle='modal'" +
"data-target='#modal-info-sample'>" + data + '</a>';
return "<a href='" + row.sampleInfoUrl + "'" +
"class='sample-info-link'>" + data + '</a>';
}
}],
rowCallback: function(row, data) {
@ -111,7 +110,7 @@ function dataTableInit() {
},
preDrawCallback: function() {
animateSpinner(this);
$('.sample_info').off('click');
$('.sample-info-link').off('click');
},
stateLoadCallback: function(settings) {
// Send an Ajax request to the server to get the data. Note that
@ -179,8 +178,11 @@ function dataTableInit() {
});
// Handle click on table cells with checkboxes
$('#samples').on('click', 'tbody td, thead th:first-child', function() {
$(this).parent().find('input[type="checkbox"]').trigger('click');
$('#samples').on('click', 'tbody td, thead th:first-child', function(e) {
if (!$(e.target).is('.sample-info-link')) {
// Skip if clicking on samples info link
$(this).parent().find('input[type="checkbox"]').trigger('click');
}
});
// Handle clicks on checkbox
@ -233,6 +235,9 @@ function dataTableInit() {
table.on('draw', function() {
updateDataTableSelectAllCtrl(table);
sampleInfoListener();
// Prevent sample row toggling when selecting user smart annotation link
SmartAnnotation.preventPropagation('.atwho-user-popover');
});
table.on('column-reorder', function() {
@ -411,7 +416,7 @@ function onClickEdit() {
saveAction = "update";
$.ajax({
url: rowData["sampleInfoUrl"],
url: rowData["sampleEditUrl"],
type: "GET",
dataType: "json",
success: function (data) {
@ -457,6 +462,12 @@ function onClickEdit() {
}
});
// initialize smart annotation
_.each($('[data-object="custom_fields"]'), function(el) {
if(_.isUndefined($(el).data('atwho'))) {
SmartAnnotation.init(el);
}
});
// Adjust columns width in table header
adjustTableHeader();
},
@ -742,6 +753,12 @@ function onClickAddSample() {
$("select[name=sample_group_id]").selectpicker();
$("select[name=sample_type_id]").selectpicker();
// initialize smart annotation
_.each($('[data-object="custom_fields"]'), function(el) {
if(_.isUndefined($(el).data('atwho'))) {
SmartAnnotation.init(el);
}
});
// Adjust columns width in table header
adjustTableHeader();
},

View file

@ -1,5 +1,3 @@
//= require datatables
// Create import samples ajax
$("#modal-import-samples").on("show.bs.modal", function(event) {
formGroup = $(this).find(".form-group");

View file

@ -0,0 +1,450 @@
var SmartAnnotation = (function() {
'use strict';
// utilities
var Util = (function() {
// helper method that binds show/hidden action
function showHideBinding() {
$.each(['show', 'hide'], function (i, ev) {
var el = $.fn[ev];
$.fn[ev] = function () {
this.trigger(ev);
return el.apply(this, arguments);
};
});
}
var publicApi = {
showHideBinding: showHideBinding
};
return publicApi;
})();
// stop the user annotation popover on click propagation
function atwhoStopPropagation(element) {
$(element).on('click', function(e) {
e.stopPropagation();
e.preventDefault();
});
}
function setAtWho(field) {
var FilterTypeEnum = Object.freeze({
USER: {tag: "users",
dataUrl: $(document.body).attr('data-atwho-users-url')},
TASK: {tag: "tsk",
dataUrl: $(document.body).attr('data-atwho-task-url')},
PROJECT: {tag: "prj",
dataUrl: $(document.body).attr('data-atwho-project-url')},
EXPERIMENT: {tag: "exp",
dataUrl: $(document.body).attr('data-atwho-experiment-url')},
SAMPLE: {tag: "sam",
dataUrl: $(document.body).attr('data-atwho-sample-url')},
MENU: {tag: "menu",
dataUrl: $(document.body).attr('data-atwho-menu-items')}
});
var prevAt,
// Default selected filter when using '#'
DEFAULT_SEARCH_FILTER = FilterTypeEnum.SAMPLE,
atWhoUpdating = false;
// helper methods for AtWho callback
function _templateEval(_tpl, map) {
var res;
try {
if (map.no_results) {
res = noResultsTemplate();
} else {
res = generateTemplate(map);
}
} catch (_error) {
res = '';
}
return res;
}
function _matchHighlighter(li, query, filterType) {
var $li, re;
function highlight(el, sel, re) {
var prevVal, newVal;
prevVal = el.find(sel).html();
newVal = prevVal.replace(re, '<strong>$&</strong>');
el.find(sel).html(newVal);
}
if (!query || $(li).data('no-results')) {
return li;
}
$li = $(li);
re = new RegExp(query, 'gi');
// search_filter is not passed for the user
if(filterType) {
highlight($li, '[data-val=name]', re);
} else {
highlight($li, '[data-val=full-name]', re);
highlight($li, '[data-val=email]', re);
}
return $li[0].outerHTML
}
function _generateInputTag(value, li) {
var res = '';
res += '[#' + li.attr('data-name');
res += '~' + li.attr('data-type');
res += '~' + li.attr('data-id') + ']';
return res;
}
// initialise dropdown dismiss button
function initDismissButton($currentAtWho) {
$currentAtWho.find('.dismiss').off('click')
.on('click', function() {
$(field).atwho('destroy');
init();
});
}
// Initialize or update dropdown header buttons
function updateHeaderButtons(query, filterTypeTag) {
var $currentAtWho = $('.atwho-view[style]');
initDismissButton($currentAtWho);
// Update the selected filter button when changing smart annotation type
$currentAtWho.find('[data-filter]')
.removeClass('btn-primary')
.addClass('btn-default');
$currentAtWho.find('[data-filter="' + filterTypeTag + '"]')
.removeClass('btn-default')
.addClass('btn-primary');
// Update the selected filter button when clicking on one of them
$currentAtWho.find('[data-filter]').off()
.on('click', function(e) {
var $selectedBtn = $(this);
var $prevBtn = $selectedBtn.closest('.title').children('.btn-primary');
$selectedBtn.removeClass('btn-default').addClass('btn-primary');
$prevBtn.removeClass('btn-primary').addClass('btn-default');
// Updates query and dropdown elements; focuses input
$(field).click().focus();
});
}
// Generates suggestion dropdown filter
function generateFilterMenu(active, res_data) {
var header = '<div class="atwho-header-res">' +
'<div class="title">' +
'<button data-filter="prj" class="btn btn-xs ' +
(active === 'prj' ? 'btn-primary' : 'btn-default') + '">project#</button>' +
'<button data-filter="exp" class="btn btn-xs ' +
(active === 'exp' ? 'btn-primary' : 'btn-default') + '">experiment#</button>' +
'<button data-filter="tsk" class="btn btn-xs ' +
(active === 'tsk' ? 'btn-primary' : 'btn-default') + '">task#</button>' +
'<button data-filter="sam" class="btn btn-xs ' +
(active === 'sam' ? 'btn-primary' : 'btn-default') + '">sample#</button>' +
'</div>' +
'<div class="help">' +
'<div>' +
'<strong><%= I18n.t("atwho.users.navigate_1") %></strong> ' +
'<%= I18n.t("atwho.users.navigate_2") %>' +
'</div>' +
'<div><strong><%= I18n.t("atwho.users.confirm_1") %></strong> ' +
'<%= I18n.t("atwho.users.confirm_2") %>' +
'</div>' +
'<div>' +
'<strong><%= I18n.t("atwho.users.dismiss_1") %></strong> ' +
'<%= I18n.t("atwho.users.dismiss_2") %>' +
'</div>' +
'<div class="dismiss">' +
'<span class="glyphicon glyphicon-remove"></span>' +
'</div>' +
'</div>' +
'</div>';
return header;
}
function noResultsTemplate() {
var res = '<div class="atwho-no-results" data-no-results="1">';
res += '<span><%= I18n.t("atwho.no_results") %></span>';
res += '</div>';
return res;
}
// Generates resources list items
function generateTemplate(map) {
var res = '';
res += '<li class="atwho-li atwho-li-res" data-name="' +
truncateLongString(map.name,
<%= Constants::NAME_TRUNCATION_LENGTH %>) +
'" data-id="' + map.id + '" data-type="' +
map.type + '">';
switch(map.type) {
case 'tsk':
res += '<span data-type class="res-type">' + map.type + '</span>';
break;
case 'prj':
res += '<span data-type class="res-type">' + map.type + '</span>';
break;
case 'exp':
res += '<span data-type class="res-type">' + map.type + '</span>';
break;
case 'sam':
res += '<span class="glyphicon glyphicon-tint"></span>';
break;
}
res += '&nbsp;';
res += '<span data-val="name" class="res-name">';
res += truncateLongString(map.name,
<%= Constants::NAME_TRUNCATION_LENGTH %>);
res += '</span>';
if(map.archived) {
res += '<%= I18n.t("atwho.res.archived") %></span>';
} else {
res += '</span>';
}
res += '&nbsp;';
switch (map.type) {
case 'tsk':
res += '<span class="res-description">&lt; ' + map.experimentName +
' &lt; ' + map.projectName + '</span>';
break;
case 'exp':
res += '<span class="res-description">&lt; ' + map.projectName + '</span>';
break;
case 'sam':
res += '<span class="res-description">' + map.description + '</span>';
break;
}
res += '</li>';
return res;
}
/**
* Hackish wrapper function to make AtWho work when switching between
* multiple AtWho instances (e.g. from # to task#).
*
* Prevents second execution of AtWho update callback, triggered when user
* switches to different AtWho instance (e.g. from # to task#), which causes
* both of them to be called. In such case, AtWhO modal needs to be
* rerendered.
*/
function atWhoSwitchHack(filterTypeTag, remoteFilterCb) {
if(atWhoUpdating || (!$(field).length && _.isUndefined(filterTypeTag))) {
setTimeout(function() {
$(field).click();
}, 100);
return;
}
atWhoUpdating = true;
setTimeout(function() {
remoteFilterCb();
atWhoUpdating = false;
}, 100);
}
function atWhoSettings(at, defaultFilterType) {
return {
at: at,
callbacks: {
remoteFilter: function(query, callback) {
var $currentAtWho = $('.atwho-view[style]');
var filterTypeTag = $currentAtWho
.find('.btn-primary')
.data('filter');
atWhoSwitchHack(filterTypeTag, function() {
var filterType;
if (_.isUndefined(filterTypeTag)) {
// Switched smart annotation type (i.e. changed input)
filterType = defaultFilterType;
} else {
// Switched filtering type (i.e. different filter button
// pressed; works also for specific annotation types, e.g.
// task#, and coverts to the correct annotation type on confirm)
$.each(FilterTypeEnum, function(k, v) {
if (v.tag == filterTypeTag) {
filterType = FilterTypeEnum[k];
return false;
}
});
}
if (prevAt != at) {
// Switching smart annotation type (i.e. chaned input)
prevAt = at;
filterType = defaultFilterType;
// Hide current AtWho
$currentAtWho.removeAttr("style");
}
$.getJSON(
filterType.dataUrl,
{query: query},
function(data) {
// Updates dropdown
if (data.res.length < 1) {
callback([{no_results: 1}]);
} else {
callback(data.res);
}
updateHeaderButtons(query, filterType.tag);
}
);
});
},
sorter: function(query, items, _searchKey) {
// Sorting is already done on server-side
return items;
},
tplEval: function(_tpl, map) {
return _templateEval(_tpl, map);
},
highlighter: function(li, query) {
return _matchHighlighter(li, query, true);
},
beforeInsert: function(value, li) {
return _generateInputTag(value, li);
}
},
headerTpl: generateFilterMenu(defaultFilterType),
limit: <%= Constants::ATWHO_SEARCH_LIMIT %>,
startWithSpace: true,
acceptSpaceBar: true,
displayTimeout: 120000
}
}
function init() {
$(field)
.atwho({
at: '@',
callbacks: {
remoteFilter: function(query, callback) {
$.getJSON(
FilterTypeEnum.USER.dataUrl,
{query: query},
function(data) {
if (data.users.length < 1) {
callback([{no_results: 1}]);
} else {
callback(data.users);
}
initDismissButton();
}
);
},
sorter: function(query, items, _searchKey) {
// Sorting is already done on server-side
return items;
},
tplEval: function(_tpl, map) {
var res;
try {
if (map.no_results) {
res = noResultsTemplate();
} else {
res = '';
res += '<li class="atwho-li atwho-li-user" ';
res += 'data-id="' + map.id + '" ';
res += 'data-full-name="' + map.full_name + '">';
res += '<img src="' + map.img_url + '" class="avatar" />';
res += '<span data-val="full-name">';
res += map.full_name;
res += '</span>';
res += '<small>';
res += '&nbsp;';
res += '&#183;';
res += '&nbsp;';
res += '<span data-val="email">';
res += map.email;
res += '</span>';
res += '</small>';
res += '</li>';
}
} catch (_error) {
res = '';
}
return res;
},
highlighter: function(li, query) {
return _matchHighlighter(li, query);
},
beforeInsert: function(value, li) {
var res = '';
res += '[@' + li.attr('data-full-name');
res += '~' + li.attr('data-id') + ']';
return res;
}
},
headerTpl:
'<div class="atwho-header-res">' +
'<div class="title title-user"><%= I18n.t("atwho.users.title") %></div>' +
'<div class="help">' +
'<div>' +
'<strong><%= I18n.t("atwho.users.navigate_1") %></strong> ' +
'<%= I18n.t("atwho.users.navigate_2") %>' +
'</div>' +
'<div>' +
'<strong><%= I18n.t("atwho.users.confirm_1") %></strong> ' +
'<%= I18n.t("atwho.users.confirm_2") %>' +
'</div>' +
'<div>' +
'<strong><%= I18n.t("atwho.users.dismiss_1") %></strong> ' +
'<%= I18n.t("atwho.users.dismiss_2") %>' +
'</div>' +
'<div class="dismiss">' +
'<span class="glyphicon glyphicon-remove"></span>' +
'</div>' +
'</div>' +
'</div>',
limit: <%= Constants::ATWHO_SEARCH_LIMIT %>,
startsWithSpace: true,
acceptSpaceBar: true,
displayTimeout: 120000
})
.atwho(atWhoSettings('#', DEFAULT_SEARCH_FILTER));
// .atwho(atWhoSettings('task#', FilterTypeEnum.TASK)) Waiting for better times
// .atwho(atWhoSettings('project#', FilterTypeEnum.PROJECT))
// .atwho(atWhoSettings('experiment#', FilterTypeEnum.EXPERIMENT))
// .atwho(atWhoSettings('sample#', FilterTypeEnum.SAMPLE));
}
return {
init: init
};
}
function initialize(field) {
var atWho = new setAtWho(field);
atWho.init();
}
var publicApi = Object.freeze({
init: initialize,
preventPropagation: atwhoStopPropagation
});
return publicApi;
})();
// initialize the smart annotations
(function initSmartAnnotation() {
$(document).on('focus', '[data-atwho-edit]', function() {
if(_.isUndefined($(this).data('atwho'))) {
SmartAnnotation.init(this);
}
});
})();

View file

@ -22,13 +22,17 @@ $.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) {
function textValidator(ev, textInput, textLimitMin, textLimitMax, clearErr, tinyMCE) {
clearErr = _.isUndefined(clearErr) ? false : clearErr;
var text = $(textInput).val().trim();
$(textInput).val(text);
var text_from_html = $("<div/>").html(text).text();
if (text_from_html.length < text.length) text = text_from_html;
if(tinyMCE){
var text = textInput.length;
} else {
var text = $(textInput).val().trim();
$(textInput).val(text);
var text_from_html = $("<div/>").html(text).text();
if (text_from_html.length < text.length) text = text_from_html;
}
var nameTooShort = text.length < textLimitMin;
var nameTooLong = text.length > textLimitMax;

View file

@ -1,23 +0,0 @@
//= require quill
// Globally overwrite links handling in Quill rich text editor
var Link = Quill.import('formats/link');
Link.sanitize = function(url) {
if (url.includes('http:') || url.includes('https:')) {
return url;
}
return 'http://' + url;
};
function openLinksInNewTab() {
_.each($('.ql-editor a'), function(el) {
if ($(el).attr('target') !== '_blank') {
$(el).attr('target', '_blank');
}
});
}
$(document).ready(function(){
openLinksInNewTab();
});

View file

@ -0,0 +1,44 @@
(function() {
'use strict';
$(document).on('click', '.sample-info-link', function(e) {
var that = $(this);
$.ajax({
method: 'GET',
url: that.attr('href'),
dataType: 'json'
}).done(function(xhr, settings, data) {
$('body').append($.parseHTML(data.responseJSON.html));
$('#modal-info-sample').modal('show', {
backdrop: true,
keyboard: false
}).on('hidden.bs.modal', function() {
$(this).find('.modal-body #sample-info-table').DataTable().destroy();
$(this).remove();
});
$('#sample-info-table').DataTable({
dom: 'RBltpi',
stateSave: false,
buttons: [],
processing: true,
colReorder: {
fixedColumnsLeft: 1000000 // Disable reordering
},
columnDefs: [{
targets: 0,
searchable: false,
orderable: false
}],
fnDrawCallback: function(settings, json) {
animateSpinner(this, false);
},
preDrawCallback: function(settings) {
animateSpinner(this);
}
});
});
e.preventDefault();
return false;
});
})();

View file

@ -0,0 +1,99 @@
var TinyMCE = (function() {
'use strict';
function initHighlightjs() {
$('[class*=language]').each(function(i, block) {
hljs.highlightBlock(block);
});
}
function initHighlightjsIframe(iframe) {
$('[class*=language]', iframe).each(function(i, block) {
hljs.highlightBlock(block);
});
}
// returns a public API for TinyMCE editor
return Object.freeze({
init : function() {
if (typeof tinyMCE != 'undefined') {
tinyMCE.init({
selector: "textarea.tinymce",
toolbar: ["undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | forecolor backcolor | codesample"],
plugins: "link,advlist,codesample,autolink,lists,charmap,hr,anchor,searchreplace,wordcount,visualblocks,visualchars,insertdatetime,nonbreaking,save,contextmenu,directionality,paste,textcolor,colorpicker,textpattern,imagetools,toc",
codesample_languages: [{"text":"R","value":"r"},{"text":"MATLAB","value":"matlab"},{"text":"Python","value":"python"},{"text":"JSON","value":"javascript"},{"text":"HTML/XML","value":"markup"},{"text":"JavaScript","value":"javascript"},{"text":"CSS","value":"css"},{"text":"PHP","value":"php"},{"text":"Ruby","value":"ruby"},{"text":"Java","value":"java"},{"text":"C","value":"c"},{"text":"C#","value":"csharp"},{"text":"C++","value":"cpp"}],
removed_menuitems: 'newdocument',
elementpath: false,
default_link_target: "_blank",
style_formats: [
{title: 'Headers', items: [
{title: 'Header 1', format: 'h1'},
{title: 'Header 2', format: 'h2'},
{title: 'Header 3', format: 'h3'},
{title: 'Header 4', format: 'h4'},
{title: 'Header 5', format: 'h5'},
{title: 'Header 6', format: 'h6'}
]},
{title: 'Inline', items: [
{title: 'Bold', icon: 'bold', format: 'bold'},
{title: 'Italic', icon: 'italic', format: 'italic'},
{title: 'Underline', icon: 'underline', format: 'underline'},
{title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough'},
{title: 'Superscript', icon: 'superscript', format: 'superscript'},
{title: 'Subscript', icon: 'subscript', format: 'subscript'},
{title: 'Code', icon: 'code', format: 'code'}
]},
{title: 'Blocks', items: [
{title: 'Paragraph', format: 'p'},
{title: 'Blockquote', format: 'blockquote'}
]},
{title: 'Alignment', items: [
{title: 'Left', icon: 'alignleft', format: 'alignleft'},
{title: 'Center', icon: 'aligncenter', format: 'aligncenter'},
{title: 'Right', icon: 'alignright', format: 'alignright'},
{title: 'Justify', icon: 'alignjustify', format: 'alignjustify'}
]}
],
init_instance_callback: function(editor) {
SmartAnnotation.init($(editor.contentDocument.activeElement));
initHighlightjsIframe($(this.iframeElement).contents());
},
setup: function(editor) {
editor.on('Change', function(e) {
if(e.keyCode == 13 && $(editor.contentDocument.activeElement).atwho('isSelecting')) {
return false;
}
});
editor.on('NodeChange', function(e) {
var node = e.element;
var editor = this;
setTimeout(function() {
if($(node).is('pre') && !editor.isHidden()){
initHighlightjsIframe($(editor.iframeElement).contents());
}
}, 200);
});
},
codesample_content_css: '<%= asset_path('highlightjs-github-theme') %>'
});
}
},
destroyAll: function() {
_.each(tinymce.editors, function(editor) {
editor.destroy();
initHighlightjs();
});
},
refresh: function() {
this.destroyAll();
this.init();
},
getContent: function() {
return tinymce.editors[0].getContent();
},
highlight: initHighlightjs
});
})();

View file

@ -1,5 +1,3 @@
//= require datatables
var usersDatatable = null;
// Initialize edit name modal window

View file

@ -1,4 +1,5 @@
/*
*= require highlightjs-github-theme
*= require_self
*= require_tree .
*= require jquery-ui/draggable
@ -7,7 +8,6 @@
*= require constants
*= require introjs
*= stub reports_pdf
*= require quill.snow
*/
@import "bootstrap-sprockets";
@import "bootstrap";

View file

@ -25,6 +25,7 @@ $color-emperor: #555;
$color-mine-shaft: #333;
$color-nero: #262626;
$color-black: #000;
$color-cloud: rgba(0, 0, 0, .1);
// Miscelaneous colors
$color-mystic: #eaeff2;

View file

@ -854,6 +854,14 @@ a[data-toggle="tooltip"] {
.dataTables_paginate {
float: right;
.pagination {
margin: 2px 0;
}
}
.dataTables_info {
padding-top: 8px;
}
}
@ -1719,3 +1727,212 @@ th.custom-field .modal-tooltiptext {
.disable-click {
pointer-events: none;
}
// AtWho (smart annotations)
// <Custom atwho style>
.atwho-view {
position: absolute;
top: 0;
left: 0;
display: none;
margin-top: 18px;
background: $color-white;
color: $color-black;
border: 1px solid $color-emperor;
border-radius: 3px;
box-shadow: 0 0 5px $color-cloud;
min-width: 520px;
max-height: 200px;
overflow: auto;
z-index: 11110 !important;
small {
font-size: smaller;
color: $color-emperor;
font-weight: normal;
}
strong {
color: $color-theme-primary;
}
.cur {
background: $color-theme-primary;
color: $color-white;
small {
color: $color-white;
}
strong {
color: $color-white;
font: bold;
}
}
ul {
list-style: none;
padding: 0;
margin: auto;
li {
display: block;
padding: 5px 10px;
border-bottom: 1px solid $color-emperor;
cursor: pointer;
}
}
}
// <End of overrides>
.atwho-header-res {
padding-top: 3px;
padding-bottom: 7px;
height: 34px;
background-color: $color-gallery;
border-bottom: 1px solid $color-emperor;
clear: both;
> div {
display: inline;
}
.title {
float: left;
margin-left: 15px;
.btn {
border-radius: 4px;
margin-right: 5px;
padding: 3px;
}
.btn:last-child {
margin-right: 20px;
}
.btn-default {
background-color: transparent;
}
}
.title-user {
padding-top: 4px;
}
.help {
float: right;
padding-top: 4px;
div {
display: inline;
margin-right: 15px;
font-size: smaller;
}
div strong {
color: $color-black;
}
.dismiss {
color: $color-emperor;
}
.dismiss:hover {
color: $color-black;
cursor: pointer;
}
}
}
.atwho-li-user {
.avatar {
border-radius: 50%;
margin-left: 5px;
margin-right: 10px;
height: 20px;
width: 20px;
}
}
.atwho-li-res {
.glyphicon-tint {
margin-left: 5px;
margin-right: 5px;
}
.res-type {
border: 1px solid $color-black;
border-radius: 4px;
font-weight: 600;
margin-left: 5px;
margin-right: 5px;
padding: 0 2px;
text-transform: capitalize;
}
.res-name {
font-weight: 600;
margin-right: 5px;
}
.res-description {
color: $color-emperor;
font-size: 10px;
}
}
.sa-type {
border: 1px solid $color-black;
border-radius: 4px;
color: $color-black;
font-weight: 600;
padding: 0 2px;
text-decoration: none;
text-transform: capitalize;
&:hover {
text-decoration: none;
}
}
.atwho-user-popover {
cursor: pointer;
padding-left: 5px;
}
.atwho-user-img-popover {
cursor: not-allowed;
}
.atwho-no-results {
padding: 5px 10px;
}
.popover {
border-radius: 3px;
min-width: 450px;
padding: 15px 10px;
z-index: 9999;
h5 {
font-weight: bold;
line-height: 0;
}
.silver {
color: $color-silver-chalice;
}
p {
max-width: 260px;
word-wrap: break-word;
}
}
.popover-remove {
cursor: pointer;
}

View file

@ -0,0 +1,107 @@
class AtWhoController < ApplicationController
before_action :load_vars
before_action :check_users_permissions
def users
# Search users
res = @organization
.search_users(@query)
.select(:id, :full_name, :email)
.limit(Constants::ATWHO_SEARCH_LIMIT)
.as_json
# Add avatars, Base62, convert to JSON
res.each do |user_obj|
user_obj['full_name'] =
user_obj['full_name']
.truncate(Constants::NAME_TRUNCATION_LENGTH_DROPDOWN)
user_obj['id'] = user_obj['id'].base62_encode
user_obj['img_url'] = avatar_path(user_obj['id'], :icon_small)
end
respond_to do |format|
format.json do
render json: {
users: res,
status: :ok
}
end
end
end
def menu_items
res = SmartAnnotation.new(current_user, current_organization, @query)
respond_to do |format|
format.json do
render json: {
prj: res.projects,
exp: res.experiments,
tsk: res.my_modules,
sam: res.samples,
status: :ok
}
end
end
end
def samples
res = SmartAnnotation.new(current_user, current_organization, @query)
respond_to do |format|
format.json do
render json: {
res: res.samples,
status: :ok
}
end
end
end
def projects
res = SmartAnnotation.new(current_user, current_organization, @query)
respond_to do |format|
format.json do
render json: {
res: res.projects,
status: :ok
}
end
end
end
def experiments
res = SmartAnnotation.new(current_user, current_organization, @query)
respond_to do |format|
format.json do
render json: {
res: res.experiments,
status: :ok
}
end
end
end
def my_modules
res = SmartAnnotation.new(current_user, current_organization, @query)
respond_to do |format|
format.json do
render json: {
res: res.my_modules,
status: :ok
}
end
end
end
private
def load_vars
@organization = Organization.find_by_id(params[:id])
@query = params[:query]
render_404 unless @organization
end
def check_users_permissions
render_403 unless can_view_organization_users(@organization)
end
end

View file

@ -1,6 +1,7 @@
class MyModuleCommentsController < ApplicationController
include ActionView::Helpers::TextHelper
include InputSanitizeHelper
include ApplicationHelper
before_action :load_vars
before_action :check_view_permissions, only: :index
@ -115,13 +116,15 @@ class MyModuleCommentsController < ApplicationController
module: @my_module.name
)
)
render json: {
comment: custom_auto_link(
simple_format(@comment.message),
link: :urls,
html: { target: '_blank' }
)
}, status: :ok
message = auto_link(
smart_annotation_parser(
simple_format(sanitize_input(@comment.message))
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
).html_safe
render json: { comment: message }, status: :ok
else
render json: { errors: @comment.errors.to_hash(true) },
status: :unprocessable_entity

View file

@ -1,6 +1,7 @@
class ProjectCommentsController < ApplicationController
include ActionView::Helpers::TextHelper
include InputSanitizeHelper
include ApplicationHelper
before_action :load_vars
before_action :check_view_permissions, only: :index
@ -112,13 +113,15 @@ class ProjectCommentsController < ApplicationController
project: @project.name
)
)
render json: {
comment: custom_auto_link(
simple_format(@comment.message),
link: :urls,
html: { target: '_blank' }
)
}, status: :ok
message = auto_link(
smart_annotation_parser(
simple_format(sanitize_input(@comment.message))
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
).html_safe
render json: { comment: message }, status: :ok
else
render json: { errors: @comment.errors.to_hash(true) },
status: :unprocessable_entity

View file

@ -1,6 +1,7 @@
class ResultCommentsController < ApplicationController
include ActionView::Helpers::TextHelper
include InputSanitizeHelper
include ApplicationHelper
before_action :load_vars
@ -113,13 +114,15 @@ class ResultCommentsController < ApplicationController
result: @result.name
)
)
render json: {
comment: custom_auto_link(
simple_format(@comment.message),
link: :urls,
html: { target: '_blank' }
)
}, status: :ok
message = auto_link(
smart_annotation_parser(
simple_format(sanitize_input(@comment.message))
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
)
render json: { comment: message }, status: :ok
else
render json: { errors: @comment.errors.to_hash(true) },
status: :unprocessable_entity

View file

@ -1,6 +1,7 @@
class StepCommentsController < ApplicationController
include ActionView::Helpers::TextHelper
include InputSanitizeHelper
include ApplicationHelper
before_action :load_vars
@ -118,13 +119,15 @@ class StepCommentsController < ApplicationController
)
)
end
render json: {
comment: custom_auto_link(
simple_format(@comment.message),
link: :urls,
html: { target: '_blank' }
)
}, status: :ok
message = auto_link(
smart_annotation_parser(
simple_format(sanitize_input(@comment.message))
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
).html_safe
render json: { comment: message }, status: :ok
else
render json: { errors: @comment.errors.to_hash(true) },
status: :unprocessable_entity

View file

@ -1,4 +1,7 @@
class StepsController < ApplicationController
include ActionView::Helpers::TextHelper
include ApplicationHelper
before_action :load_vars, only: [:edit, :update, :destroy, :show]
before_action :load_vars_nested, only: [:new, :create]
before_action :convert_table_contents_to_utf8, only: [:create, :update]
@ -241,7 +244,7 @@ class StepsController < ApplicationController
message = t(
str,
user: current_user.full_name,
checkbox: chkItem.text,
checkbox: smart_annotation_parser(simple_format(chkItem.text)),
step: chkItem.checklist.step.position + 1,
step_name: chkItem.checklist.step.name,
completed: completed_items,

View file

@ -4,6 +4,9 @@ class SampleDatatable < AjaxDatatablesRails::Base
include ActionView::Helpers::TextHelper
include SamplesHelper
include InputSanitizeHelper
include Rails.application.routes.url_helpers
include ActionView::Helpers::UrlHelper
include ApplicationHelper
ASSIGNED_SORT_COL = 'assigned'
@ -113,17 +116,12 @@ class SampleDatatable < AjaxDatatablesRails::Base
else
escape_input(record.sample_type.name)
end,
'4': if record.sample_group.nil?
"<span class='glyphicon glyphicon-asterisk'></span> " +
I18n.t('samples.table.no_group')
else
"<span class='glyphicon glyphicon-asterisk' "\
"style='color: #{escape_input(record.sample_group.color)}'>"\
"</span> " + escape_input(record.sample_group.name)
end,
'4': sample_group_cell(record),
'5': I18n.l(record.created_at, format: :full),
'6': escape_input(record.user.full_name),
'sampleInfoUrl':
Rails.application.routes.url_helpers.sample_path(record.id),
'sampleEditUrl':
Rails.application.routes.url_helpers.edit_sample_path(record.id),
'sampleUpdateUrl':
Rails.application.routes.url_helpers.sample_path(record.id)
@ -131,8 +129,15 @@ class SampleDatatable < AjaxDatatablesRails::Base
# Add custom attributes
record.sample_custom_fields.each do |scf|
sample[@cf_mappings[scf.custom_field_id]] =
custom_auto_link(scf.value, link: :urls, html: { target: '_blank' })
sample[@cf_mappings[scf.custom_field_id]] = auto_link(
smart_annotation_parser(
simple_format(sanitize_input(scf.value)),
@organization
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
).html_safe
end
sample
end
@ -144,6 +149,17 @@ class SampleDatatable < AjaxDatatablesRails::Base
"<span class='circle disabled'>&nbsp;</span>"
end
def sample_group_cell(record)
if record.sample_group.nil?
"<span class='glyphicon glyphicon-asterisk'></span> " \
"#{I18n.t('samples.table.no_group')}"
else
"<span class='glyphicon glyphicon-asterisk' " \
"style='color: #{escape_input(record.sample_group.color)}'></span> " \
"#{escape_input(record.sample_group.name)}"
end
end
# Query database for records (this will be later paginated and filtered)
# after that "data" function will return json
def get_raw_records

View file

@ -1,4 +1,7 @@
module ApplicationHelper
include ActionView::Helpers::AssetTagHelper
include ActionView::Helpers::UrlHelper
def module_page?
controller_name == 'my_modules'
end
@ -54,4 +57,113 @@ module ApplicationHelper
@my_module.nil? &&
!@experiment.nil?
end
def smart_annotation_parser(text, organization = nil)
new_text = smart_annotation_filter_resources(text)
new_text = smart_annotation_filter_users(new_text, organization)
new_text
end
# Check if text have smart annotations of resources
# and outputs a link to resource
def smart_annotation_filter_resources(text)
sa_reg = /\[\#(.*?)~(prj|exp|tsk|sam)~([0-9a-zA-Z]+)\]/
new_text = text.gsub(sa_reg) do |el|
match = el.match(sa_reg)
case match[2]
when 'prj'
project = Project.find_by_id(match[3].base62_decode)
next unless project
if project.archived?
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
"#{link_to project.name,
projects_archive_path} #{t'atwho.res.archived'}"
else
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
"#{link_to project.name,
project_path(project)}"
end
when 'exp'
experiment = Experiment.find_by_id(match[3].base62_decode)
next unless experiment
if experiment.archived?
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
"#{link_to experiment.name,
experiment_archive_project_path(experiment.project)} " \
"#{t'atwho.res.archived'}"
else
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
"#{link_to experiment.name,
canvas_experiment_path(experiment)}"
end
when 'tsk'
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> " \
"#{link_to my_module.name,
module_archive_experiment_path(my_module.experiment)} " \
"#{t'atwho.res.archived'}"
else
"<span class='sa-type'>#{sanitize(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> " \
"#{link_to(sample.name,
sample_path(sample.id),
class: 'sample-info-link')}"
else
"<span class='glyphicon glyphicon-tint'></span> " \
"#{match[1]} #{t'atwho.res.deleted'}"
end
end
end
new_text
end
# Check if text have smart annotations of users
# and outputs a popover with user information
def smart_annotation_filter_users(text, organization)
sa_user = /\[\@(.*?)~([0-9a-zA-Z]+)\]/
new_text = text.gsub(sa_user) do |el|
match = el.match(sa_user)
user = User.find_by_id(match[2].base62_decode)
organization ||= current_organization
if user &&
organization &&
UserOrganization.user_in_organization(user, organization).any?
user_org = user
.user_organizations
.where('user_organizations.organization_id = ?',
organization).first
user_description = %(<div class='col-xs-4'>
<img src='#{avatar_path(user, :thumb)}' alt='thumb'>
</div><div class='col-xs-8'>
<div class='row'><div class='col-xs-9 text-left'><h5>
#{user.full_name}</h5></div><div class='col-xs-3 text-right'>
<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>
</div></div><div class='row'><div class='col-xs-12'>
<p class='silver'>#{user.email}</p><p>
#{I18n.t('atwho.popover_html',
role: user_org.role.capitalize,
organization: user_org.organization.name,
time: user_org.created_at.strftime('%B %Y'))}
</p></div></div></div>)
raw(image_tag(avatar_path(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>')
end
end
new_text
end
end

View file

@ -260,5 +260,11 @@ module BootstrapFormHelper
end
text_area(name, opts)
end
# Returns <textarea> helper tag for tinyMCE editor
def tiny_mce_editor(name, options = {})
options.merge!(class: 'tinymce', cols: 120, rows: 15)
text_area(name, options)
end
end
end

View file

@ -13,6 +13,6 @@ module InputSanitizeHelper
def custom_auto_link(text, args)
args[:sanitize] = false
sanitize_input(auto_link(text, args))
auto_link(sanitize_input(text), args)
end
end

View file

@ -246,6 +246,11 @@ module PermissionHelper
# to "is project archived" or "is module archived" checks
# at the beginning of this file (via aspector).
# ---- ATWHO PERMISSIONS ----
def can_view_organization_users(organization)
is_member_of_organization(organization)
end
# ---- PROJECT PERMISSIONS ----
def can_view_projects(organization)

View file

@ -1,17 +0,0 @@
module QuillJsHelper
def sanitize_quill_js_input(input)
require "#{Rails.root}/app/utilities/scrubbers/quill_js_scrubber"
# We need to disable formatting to prevent unwanted \n
# symbols from creeping into sanitized HTML (which
# cause unwanted new lines when rendered in Quill.js)
disable_formatting =
Nokogiri::XML::Node::SaveOptions::DEFAULT_HTML ^
Nokogiri::XML::Node::SaveOptions::FORMAT
Loofah
.fragment(input)
.scrub!(QuillJsScrubber.new)
.to_html(save_with: disable_formatting)
end
end

View file

@ -33,33 +33,50 @@ class Experiment < ActiveRecord::Base
scope :is_archived, ->(is_archived) { where("archived = ?", is_archived) }
def self.search(user, include_archived, query = nil, page = 1)
def self.search(
user,
include_archived,
query = nil,
page = 1,
current_organization = nil
)
project_ids =
Project
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.select('id')
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
else
a_query = query
end
if include_archived
if current_organization
projects_ids =
Project
.search(user,
include_archived,
nil,
1,
current_organization)
.select('id')
new_query =
Experiment
.where(project: project_ids)
.where_attributes_like([:name, :description], a_query)
.where('experiments.project_id IN (?)', projects_ids)
.where_attributes_like([:name], a_query)
return include_archived ? new_query : new_query.is_archived(false)
elsif include_archived
new_query =
Experiment
.where(project: project_ids)
.where_attributes_like([:name, :description], a_query)
else
new_query =
Experiment
.is_archived(false)
.where(project: project_ids)
.where_attributes_like([:name, :description], a_query)
.is_archived(false)
.where(project: project_ids)
.where_attributes_like([:name, :description], a_query)
end
# Show all results if needed

View file

@ -41,33 +41,53 @@ class MyModule < ActiveRecord::Base
WIDTH = 30
HEIGHT = 14
def self.search(user, include_archived, query = nil, page = 1)
def self.search(
user,
include_archived,
query = nil,
page = 1,
current_organization = nil
)
exp_ids =
Experiment
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.select("id")
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
else
a_query = query
end
if include_archived
if current_organization
experiments_ids = Experiment
.search(user,
include_archived,
nil,
1,
current_organization)
.select('id')
new_query = MyModule
.distinct
.where("my_modules.experiment_id IN (?)", exp_ids)
.where_attributes_like([:name, :description], a_query)
.distinct
.where('my_modules.experiment_id IN (?)', experiments_ids)
.where_attributes_like([:name], a_query)
if include_archived
return new_query
else
return new_query.where('my_modules.archived = ?', false)
end
elsif include_archived
new_query = MyModule
.distinct
.where('my_modules.experiment_id IN (?)', exp_ids)
.where_attributes_like([:name, :description], a_query)
else
new_query = MyModule
.distinct
.where("my_modules.experiment_id IN (?)", exp_ids)
.where("my_modules.archived = ?", false)
.where_attributes_like([:name, :description], a_query)
.distinct
.where('my_modules.experiment_id IN (?)', exp_ids)
.where('my_modules.archived = ?', false)
.where_attributes_like([:name, :description], a_query)
end
# Show all results if needed

View file

@ -1,4 +1,6 @@
class Organization < ActiveRecord::Base
include SearchableModel
# Not really MVC-compliant, but we just use it for logger
# output in space_taken related functions
include ActionView::Helpers::NumberHelper
@ -51,6 +53,24 @@ class Organization < ActiveRecord::Base
end
end
def search_users(
query = nil,
attributes = [:full_name, :email]
)
if query
a_query = query
.strip
.gsub('_', '\\_')
.gsub('%', '\\%')
else
a_query = query
end
users
.where.not(confirmed_at: nil)
.where_attributes_like(attributes, a_query)
end
# Writes to user log
def log(message)
final = "[%s] %s" % [Time.current.to_s, message]

View file

@ -26,42 +26,66 @@ class Project < ActiveRecord::Base
has_many :report_elements, inverse_of: :project, dependent: :destroy
belongs_to :organization, inverse_of: :projects
def self.search(user, include_archived, query = nil, page = 1)
def self.search(
user,
include_archived,
query = nil,
page = 1,
current_organization = nil
)
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
else
a_query = query
end
org_ids =
Organization
.joins(:user_organizations)
.where("user_organizations.user_id = ?", user.id)
.select("id")
.distinct
if include_archived
if current_organization
new_query = Project
.distinct
.joins(:user_projects)
.where("projects.organization_id IN (?)", org_ids)
.where("projects.visibility = 1 OR user_projects.user_id = ?", user.id)
.where_attributes_like(:name, a_query)
.distinct
.joins(:user_projects)
.where('projects.organization_id = ?',
current_organization.id)
.where('projects.visibility = 1 OR user_projects.user_id = ?',
user.id)
.where_attributes_like(:name, a_query)
if include_archived
return new_query
else
return new_query.where('projects.archived = ?', false)
end
else
new_query = Project
org_ids =
Organization
.joins(:user_organizations)
.where('user_organizations.user_id = ?', user.id)
.select('id')
.distinct
.joins(:user_projects)
.where("projects.organization_id IN (?)", org_ids)
.where("projects.visibility = 1 OR user_projects.user_id = ?", user.id)
.where_attributes_like(:name, a_query)
.where("projects.archived = ?", false)
if include_archived
new_query = Project
.distinct
.joins(:user_projects)
.where('projects.organization_id IN (?)', org_ids)
.where(
'projects.visibility = 1 OR user_projects.user_id = ?',
user.id
)
.where_attributes_like(:name, a_query)
else
new_query = Project
.distinct
.joins(:user_projects)
.where('projects.organization_id IN (?)', org_ids)
.where(
'projects.visibility = 1 OR user_projects.user_id = ?',
user.id
)
.where_attributes_like(:name, a_query)
.where('projects.archived = ?', false)
end
end
# Show all results if needed

View file

@ -23,7 +23,8 @@ class Sample < ActiveRecord::Base
user,
include_archived,
query = nil,
page = 1
page = 1,
current_organization = nil
)
org_ids =
Organization
@ -33,33 +34,40 @@ class Sample < ActiveRecord::Base
.distinct
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
else
a_query = query
end
new_query = Sample
.distinct
.joins(:user)
.joins("LEFT OUTER JOIN sample_types ON samples.sample_type_id = sample_types.id")
.joins("LEFT OUTER JOIN sample_groups ON samples.sample_group_id = sample_groups.id")
.joins("LEFT OUTER JOIN sample_custom_fields ON samples.id = sample_custom_fields.sample_id")
.where("samples.organization_id IN (?)", org_ids)
.where_attributes_like(
[
"samples.name",
"sample_types.name",
"sample_groups.name",
"users.full_name",
"sample_custom_fields.value"
],
a_query
)
if current_organization
new_query = Sample
.distinct
.where('samples.organization_id = ?', current_organization.id)
.where_attributes_like(['samples.name'], a_query)
return new_query
else
new_query = Sample
.distinct
.joins(:user)
.joins('LEFT OUTER JOIN sample_types ON ' \
'samples.sample_type_id = sample_types.id')
.joins('LEFT OUTER JOIN sample_groups ON ' \
'samples.sample_group_id = sample_groups.id')
.joins('LEFT OUTER JOIN sample_custom_fields ON ' \
'samples.id = sample_custom_fields.sample_id')
.where('samples.organization_id IN (?)', org_ids)
.where_attributes_like(
[
'samples.name',
'sample_types.name',
'sample_groups.name',
'users.full_name',
'sample_custom_fields.value'
],
a_query
)
end
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query

View file

@ -30,6 +30,11 @@ class UserOrganization < ActiveRecord::Base
end
end
# returns user_organizations where the user is in org
def self.user_in_organization(user, organization)
where(user: user, organization: organization)
end
def destroy(new_owner)
# If any project of the organization has the sole owner and that
# owner is the user to be removed from the organization, then we must

View file

@ -1,5 +1,5 @@
module ProtocolsImporter
include RenamingUtil, QuillJsHelper
include RenamingUtil
def import_new_protocol(protocol_json, organization, type, user)
remove_empty_inputs(protocol_json)
@ -52,11 +52,8 @@ module ProtocolsImporter
if protocol_json['steps']
protocol_json['steps'].values.each do |step_json|
step = Step.create!(
name: step_json["name"],
description: # Sanitize description HTML
sanitize_quill_js_input(
step_json['description']
),
name: step_json['name'],
description: step_json['description'],
position: step_pos,
completed: false,
user: user,

View file

@ -1,11 +0,0 @@
class QuillJsScrubber < Rails::Html::PermitScrubber
def initialize
super
self.tags = %w(h1 span p br pre ul li strong em u sub sup s a blockquote ol)
self.attributes = %w(style class spellcheck href target)
end
def skip_node?(node)
node.text?
end
end

View file

@ -0,0 +1,100 @@
class SmartAnnotation
include ActionView::Helpers::SanitizeHelper
include ActionView::Helpers::TextHelper
attr_writer :current_user, :current_organization, :query
def initialize(current_user, current_organization, query)
@current_user = current_user
@current_organization = current_organization
@query = query
end
def my_modules
# Search tasks
res = MyModule
.search(@current_user, false, @query, 1, @current_organization)
.limit(Constants::ATWHO_SEARCH_LIMIT)
modules_list = []
res.each do |my_module_res|
my_mod = {}
my_mod['id'] = my_module_res.id.base62_encode
my_mod['name'] = sanitize(my_module_res.name)
my_mod['archived'] = my_module_res.archived
my_mod['experimentName'] = truncate(
sanitize(my_module_res.experiment.name,
length: Constants::NAME_TRUNCATION_LENGTH)
)
my_mod['projectName'] = truncate(
sanitize(my_module_res.experiment.project.name,
length: Constants::NAME_TRUNCATION_LENGTH)
)
my_mod['type'] = 'tsk'
modules_list << my_mod
end
modules_list
end
def projects
# Search projects
res = Project
.search(@current_user, false, @query, 1, @current_organization)
.limit(Constants::ATWHO_SEARCH_LIMIT)
projects_list = []
res.each do |project_res|
prj = {}
prj['id'] = project_res.id.base62_encode
prj['name'] = sanitize(project_res.name)
prj['type'] = 'prj'
projects_list << prj
end
projects_list
end
def experiments
# Search experiments
res = Experiment
.search(@current_user, false, @query, 1, @current_organization)
.limit(Constants::ATWHO_SEARCH_LIMIT)
experiments_list = []
res.each do |experiment_res|
exp = {}
exp['id'] = experiment_res.id.base62_encode
exp['name'] = sanitize(experiment_res.name)
exp['type'] = 'exp'
exp['projectName'] = truncate(
sanitize(experiment_res.project.name,
length: Constants::NAME_TRUNCATION_LENGTH)
)
experiments_list << exp
end
experiments_list
end
def samples
# Search samples
res = Sample
.search(@current_user, false, @query, 1, @current_organization)
.limit(Constants::ATWHO_SEARCH_LIMIT)
samples_list = []
res.each do |sample_res|
sam = {}
sam['id'] = sample_res.id.base62_encode
sam['name'] = sanitize(sample_res.name)
sam['description'] = "#{I18n.t('Added')} #{I18n.l(
sample_res.created_at, format: :full_date
)} #{I18n.t('by')} #{truncate(
sanitize(sample_res.user.full_name,
length: Constants::NAME_TRUNCATION_LENGTH)
)}"
sam['type'] = 'sam'
samples_list << sam
end
samples_list
end
end

View file

@ -1,7 +1,12 @@
<%= bootstrap_form_for(@comment, url: @update_url, remote: true, html: { method: :put, class: 'comment-form edit-comment-form' }, data: { role: 'edit-comment-message-form' }) do |f| %>
<div class="form-group">
<div class="input-group">
<%= f.smart_text_area :message, single_line: true, autofocus: true, hide_label: true, data: { role: 'message-input' }, value: @comment.message %>
<%= f.smart_text_area :message,
single_line: true,
autofocus: true,
hide_label: true,
data: { role: 'message-input', 'atwho-edit' => '' },
value: @comment.message %>
<span class="input-group-btn">
<a class="btn btn-default" data-action="save">
<span class="glyphicon glyphicon-ok"></span>
@ -11,4 +16,4 @@
<span class="help-block hide"></span>
<a data-action="cancel" href="#"><%= t('general.cancel') %></a>
</div>
<% end %>
<% end %>

View file

@ -15,7 +15,8 @@
<div class="form-group">
<%= form.smart_text_area :description,
label: t('experiments.new.description'),
id: 'experiment-description' %>
id: 'experiment-description',
data: { 'atwho-edit' => '' } %>
</div>
</div>
</div>

View file

@ -14,7 +14,17 @@
<%= csrf_meta_tags %>
</head>
<body class="<%= yield :body_class %>">
<body
class="<%= yield :body_class %>"
<% if user_signed_in? && current_organization.present? %>
data-atwho-users-url="<%= atwho_users_organization_path(current_organization) %>"
data-atwho-task-url="<%= atwho_my_modules_organization_path(current_organization) %>"
data-atwho-project-url="<%= atwho_projects_organization_path(current_organization) %>"
data-atwho-experiment-url="<%= atwho_experiments_organization_path(current_organization) %>"
data-atwho-sample-url="<%= atwho_samples_organization_path(current_organization) %>"
data-atwho-menu-items="<%= atwho_menu_items_organization_path(current_organization) %>"
<% end %>
>
<span style="display: none;" data-hook="body-js"></span>
<span style="display: none;" data-hook="application-body-html"></span>

View file

@ -39,8 +39,12 @@
</div>
<strong><%= comment.user.full_name %>:</strong>
<div data-role="comment-message-container">
<div data-role="comment-message">
<%= custom_auto_link(simple_format(comment.message),
link: :urls, html: { target: '_blank' }) %>
</div>
<div data-role="comment-message"><%= auto_link(
smart_annotation_parser(simple_format(
sanitize_input(comment.message)),
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
).html_safe %></div>
</div>

View file

@ -19,7 +19,13 @@
<li>
<hr>
<%= bootstrap_form_for :comment, url: { format: :json }, method: :post, remote: true, html: { class: 'comment-form' } do |f| %>
<%= f.smart_text_area :message, single_line: true, hide_label: true, placeholder: t("experiments.canvas.popups.comment_placeholder"), append: f.submit("+"), help: '.' %>
<%= f.smart_text_area :message,
single_line: true,
hide_label: true,
placeholder: t("experiments.canvas.popups.comment_placeholder"),
append: f.submit("+"),
help: '.',
data: { 'atwho-edit' => '' } %>
<% end %>
</li>
</ul>

View file

@ -1,5 +1,5 @@
<div class="panel panel-default panel-protocol-status">
<div class="panel-body">
<div class="panel-body">
<% if @protocol.linked? %>
<%= protocol_status_href(@protocol) %>
<% else %>

View file

@ -39,8 +39,12 @@
</div>
<strong><%= comment.user.full_name %>:</strong>
<div data-role="comment-message-container">
<div data-role="comment-message">
<%= custom_auto_link(simple_format(comment.message),
link: :urls, html: { target: '_blank' }) %>
</div>
<div data-role="comment-message"><%= auto_link(
smart_annotation_parser(simple_format(
sanitize_input(comment.message)),
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
).html_safe %></div>
</div>

View file

@ -18,7 +18,13 @@
<li>
<hr>
<%= bootstrap_form_for :comment, url: {format: :json}, method: :post, remote: true, html: { class: 'comment-form' } do |f| %>
<%= f.smart_text_area :message, single_line: true, hide_label: true, placeholder: t('projects.index.comment_placeholder'), append: f.submit('+'), help: '.' %>
<%= f.smart_text_area :message,
single_line: true,
hide_label: true,
placeholder: t('projects.index.comment_placeholder'),
append: f.submit('+'),
help: '.',
data: { 'atwho-edit' => '' } %>
<% end %>
</li>
</ul>

View file

@ -67,8 +67,14 @@
</div>
<div class="row">
<div class="col-xs-12">
<%= experiment.description %>
</span>
<%= auto_link(
smart_annotation_parser(
simple_format(experiment.description)
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
).html_safe %>
</div>
</div>
</div>

View file

@ -49,8 +49,16 @@
</span>
<% if experiment.description? %>
<div class='experiment-description'>
<%= custom_auto_link(simple_format(experiment.description),
link: :urls, html: { target: '_blank' }) %>
<%= auto_link(
smart_annotation_parser(
simple_format(
sanitize_input(experiment.description)
)
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
).html_safe %>
</div>
<% else %>
<span class='experiment-no-description'>

View file

@ -27,8 +27,10 @@
<div class="row">
<div class="col-xs-12">
<% if experiment.description.present? %>
<%= custom_auto_link(simple_format(experiment.description),
link: :urls, html: { target: '_blank' }) %>
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(experiment.description))),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %>
<% else %>
<em><%=t "projects.reports.elements.experiment.no_description" %></em>
<% end %>

View file

@ -31,9 +31,10 @@
</span>
<span class="comment-message">
&nbsp;
<%= custom_auto_link(simple_format(comment.message),
link: :urls,
html: { target: '_blank' }) %>
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(comment.message))),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %>
</span>
</li>
<% end %>

View file

@ -23,8 +23,10 @@
<div class="report-element-body">
<div class="row">
<div class="col-xs-12 text-container ql-editor">
<%= custom_auto_link(result_text.text,
link: :urls, html: { target: '_blank' }) %>
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(result_text.text))),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %>
</div>
</div>
</div>

View file

@ -8,7 +8,10 @@
<span class="glyphicon glyphicon-list"></span>
</div>
<div class="pull-left checklist-name">
<em><%=t 'projects.reports.elements.step_checklist.checklist_name', name: checklist.name %></em>
<%= auto_link(smart_annotation_parser(simple_format(t 'projects.reports.elements.step_checklist.checklist_name', name: checklist.name)),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %>
</div>
<div class="pull-left user-time">
<%=t 'projects.reports.elements.step_checklist.user_time', timestamp: l(timestamp, format: :full) %>
@ -24,9 +27,10 @@
<li>
<input type="checkbox" disabled="disabled" <%= "checked='checked'" if item.checked %>/>
<span class="<%= 'checked' if item.checked %>">
<%= custom_auto_link(simple_format(item.text),
link: :urls, html: { target: '_blank' }) %>
</span>
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(item.text))),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %></span>
</li>
<% end %>
</ul>

View file

@ -31,9 +31,10 @@
</span>
<span class="comment-message">
&nbsp;
<%= custom_auto_link(simple_format(comment.message),
link: :urls,
html: { target: '_blank' }) %>
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(comment.message))),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %>
</span>
</li>
<% end %>

View file

@ -27,8 +27,10 @@
<div class="row">
<div class="col-xs-12 ql-editor">
<% if strip_tags(step.description).present? %>
<%= custom_auto_link(step.description,
link: :urls, html: { target: '_blank' }) %>
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(step.description))),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %>
<% else %>
<em><%=t "projects.reports.elements.step.no_description" %></em>
<% end %>

View file

@ -1,4 +1,3 @@
<div>
<strong>
<%=t "my_modules.results.comment_title", user: comment.user.full_name, time: l(comment.created_at, format: :time) %>
</strong>
@ -40,8 +39,12 @@
<% end %>
</div>
<div data-role="comment-message-container">
<div data-role="comment-message">
<%= custom_auto_link(simple_format(comment.message),
link: :urls, html: { target: '_blank' }) %>
</div>
<div data-role="comment-message"><%= auto_link(
smart_annotation_parser(simple_format(
sanitize_input(comment.message)),
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
).html_safe %></div>
</div>

View file

@ -22,7 +22,13 @@
<li>
<hr>
<%= bootstrap_form_for :comment, url: { format: :json }, method: :post, remote: true, html: { class: 'comment-form' } do |f| %>
<%= f.smart_text_area :message, single_line: true, hide_label: true, placeholder: t("general.comment_placeholder"), append: f.submit("+", onclick: "processResult(event, ResultTypeEnum.COMMENT, false);"), help: '.' %>
<%= f.smart_text_area :message,
single_line: true,
hide_label: true,
placeholder: t("general.comment_placeholder"),
append: f.submit("+", onclick: "processResult(event, ResultTypeEnum.COMMENT, false);"),
help: '.',
data: { 'atwho-edit' => '' } %>
<% end %>
</li>
</ul>

View file

@ -3,7 +3,7 @@
<%= f.text_field :name, style: "margin-top: 10px;" %><br />
<%= f.fields_for :result_text do |ff| %>
<div class="form-group">
<%= quill_editor nil, { id: 'result_result_text_attributes_text', name: 'result[result_text_attributes][text]', value: @result.result_text.text } %>
<%= ff.tiny_mce_editor(:text, value: @result.result_text.text) %>
</div>
<% end %><br />
<%= f.submit t("result_texts.edit.update"), class: 'btn btn-primary save-result', onclick: "processResult(event, ResultTypeEnum.TEXT, true);" %>

View file

@ -3,7 +3,7 @@
<%= f.text_field :name, style: "margin-top: 10px;" %><br />
<%= f.fields_for :result_text do |ff| %>
<div class="form-group">
<%= quill_editor nil, { id: 'result_result_text_attributes_text', name: 'result[result_text_attributes][text]', value: @result.result_text.text } %>
<%= ff.tiny_mce_editor(:text) %>
</div>
<% end %><br />
<%= f.submit t("result_texts.new.create"), class: 'btn btn-primary save-result', onclick: "processResult(event, ResultTypeEnum.TEXT, false);" %>

View file

@ -1,4 +1,6 @@
<div class="ql-editor">
<%= custom_auto_link(result.result_text.text,
link: :urls, html: { target: '_blank' }) %>
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(result.result_text.text))),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %>
</div>

View file

@ -1,4 +1,3 @@
<div class="modal fade" id="modal-info-sample" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">

View file

@ -1,4 +1,4 @@
<div>
f<div>
<strong>
<%=t "protocols.steps.comment_title", user: comment.user.full_name, time: l(comment.created_at, format: :time) %>
</strong>
@ -40,8 +40,12 @@
<% end %>
</div>
<div data-role="comment-message-container">
<div data-role="comment-message">
<%= custom_auto_link(simple_format(comment.message),
link: :urls, html: { target: '_blank' }) %>
</div>
<div data-role="comment-message"><%= auto_link(
smart_annotation_parser(simple_format(
sanitize_input(comment.message)),
),
link: :urls,
sanitize: false,
html: { target: '_blank' }
).html_safe %></div>
</div>

View file

@ -22,7 +22,13 @@
<li>
<hr>
<%= bootstrap_form_for :comment, url: { format: :json }, html: { class: 'comment-form', id: "step-comment-#{@step.id}" }, method: :post, remote: true do |f| %>
<%= f.smart_text_area :message, single_line: true, hide_label: true, placeholder: t("general.comment_placeholder"), append: f.submit("+"), help: '.' %>
<%= f.smart_text_area :message,
single_line: true,
hide_label: true,
placeholder: t("general.comment_placeholder"),
append: f.submit("+"),
help: '.',
data: { 'atwho-edit' => '' } %>
<% end %>
</li>
</ul>

View file

@ -27,8 +27,7 @@
<div class="tab-pane active" role="tabpanel" id="new-step-main">
<%= f.text_field :name, label: t("protocols.steps.new.name"), placeholder: t("protocols.steps.new.name_placeholder") %>
<div class="form-group">
<label class="control-label" for="step_description"><%= t('protocols.steps.new.description') %></label>
<%= quill_editor nil, { id: 'step_description', name: 'step[description]', value: @step.description } %>
<%= f.tiny_mce_editor(:description) %>
</div>
</div>
<div class="tab-pane" role="tabpanel" id="new-step-checklists">

View file

@ -9,7 +9,12 @@
</div>
</div>
<div class="panel-body">
<%= ff.smart_text_area :name, label: t("protocols.steps.new.checklist_name"), class: "checklist_name", autofocus: true, placeholder: t("protocols.steps.new.checklist_name_placeholder") %>
<%= ff.smart_text_area :name,
label: t('protocols.steps.new.checklist_name'),
class: 'checklist_name',
autofocus: true,
placeholder: t('protocols.steps.new.checklist_name_placeholder'),
data: { 'atwho-edit' => '' } %>
<%= ff.label t("protocols.steps.new.checklist_items") %>
<ul>
<%= ff.nested_fields_for :checklist_items, ordered_checklist_items(ff.object) do |chkItems| %>
@ -18,7 +23,13 @@
<span class="glyphicon glyphicon-chevron-right handle-move pull-left"></span>
<div class="row">
<div class="col-xs-6">
<%= chkItems.smart_text_area :text, autofocus: true, placeholder: t("protocols.steps.new.checklist_item_placeholder"), hide_label: true, class: "checklist-item-text form-control", single_line: true %>
<%= chkItems.smart_text_area :text,
autofocus: true,
placeholder: t('protocols.steps.new.checklist_item_placeholder'),
hide_label: true,
class: 'checklist-item-text form-control',
single_line: true,
data: { 'atwho-edit' => '' } %>
<%= chkItems.hidden_field :position, class: "checklist-item-pos" %>
</div>
<div class="col-xs-1">

View file

@ -37,8 +37,10 @@
<em><%= t("protocols.steps.no_description") %></em>
<% else %>
<div class="ql-editor">
<%= custom_auto_link(step.description,
link: :urls, html: { target: '_blank' }) %>
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(step.description))),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %>
</div>
<% end %>
</div>
@ -120,11 +122,15 @@
<% unless step.checklists.blank? then %>
<div class="col-xs-12">
<% step.checklists.each do |checklist| %>
<strong>
<%= custom_auto_link(simple_format(checklist.name),
link: :urls,
html: { target: '_blank' }) %>
</strong>
<strong><%= auto_link(
smart_annotation_parser(
simple_format(
sanitize_input(checklist.name)
)
),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %></strong>
<% if checklist.checklist_items.empty? %>
</br>
<%= t("protocols.steps.empty_checklist") %>
@ -138,9 +144,15 @@
<% else %>
<input type="checkbox" value="" disabled="disabled" />
<% end %>
<%= custom_auto_link(simple_format(checklist_item.text),
link: :urls,
html: { target: '_blank' }) %>
<%= auto_link(
smart_annotation_parser(
simple_format(
sanitize_input(checklist_item.text)
)
),
link: :urls,
sanitize: false,
html: { target: '_blank' }).html_safe %>
</label>
</div>
<% end %>

View file

@ -60,6 +60,7 @@ Rails.application.config.assets.precompile += %w(projects/show.js)
Rails.application.config.assets.precompile += %w(notifications.js)
Rails.application.config.assets.precompile += %w(users/invite_users_modal.js)
Rails.application.config.assets.precompile += %w(samples/sample_types_groups.js)
Rails.application.config.assets.precompile += %w(highlightjs-github-theme.css)
# Libraries needed for Handsontable formulas
Rails.application.config.assets.precompile += %w(lodash.js)

View file

@ -48,6 +48,9 @@ class Constants
# Maximum number of users that can be invited in a single action
INVITE_USERS_LIMIT = 20
# Maximum nr. of search results for atwho (smart annotations)
ATWHO_SEARCH_LIMIT = 5
#=============================================================================
# File and data memory size
#=============================================================================

View file

@ -1455,6 +1455,20 @@ en:
assign_user_to_organization: "<i>%{assigned_user}</i> was added as %{role} to team <strong>%{organization}</strong> by <i>%{assigned_by_user}</i>."
unassign_user_from_organization: "<i>%{unassigned_user}</i> was removed from team <strong>%{organization}</strong> by <i>%{unassigned_by_user}</i>."
atwho:
no_results: "No results found"
users:
title: "People"
navigate_1: "up/down"
navigate_2: "to navigate"
confirm_1: "enter/tab"
confirm_2: "to confirm"
dismiss_1: "esc"
dismiss_2: "to dismiss"
res:
archived: "(archived)"
deleted: "(deleted)"
popover_html: "<span class='silver'>Team:</span>&nbsp;%{organization} <br> <span class='silver'>Role:</span>&nbsp;%{role} <br> <span class='silver'>Joined:</span>&nbsp;%{time}"
# This section contains general words that can be used in any parts of
# application.
@ -1519,3 +1533,5 @@ en:
Workflow: "Workflow"
Workflows: "Workflows"
More: "More"
Added: 'Added'
by: 'by'

View file

@ -86,6 +86,13 @@ Rails.application.routes.draw do
post 'parse_sheet'
post 'import_samples'
post 'export_samples'
# Used for atwho (smart annotations)
get 'atwho_users', to: 'at_who#users'
get 'atwho_samples', to: 'at_who#samples'
get 'atwho_projects', to: 'at_who#projects'
get 'atwho_experiments', to: 'at_who#experiments'
get 'atwho_my_modules', to: 'at_who#my_modules'
get 'atwho_menu_items', to: 'at_who#menu_items'
end
match '*path', :to => 'organizations#routing_error', via: [:get, :post, :put, :patch]
end

View file

@ -0,0 +1,5 @@
class AddIndexToUsersFullName < ActiveRecord::Migration
def change
add_index :users, :full_name
end
end

View file

@ -685,6 +685,7 @@ ActiveRecord::Schema.define(version: 20170116143350) do
add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["full_name"], name: "index_users_on_full_name", using: :btree
add_index "users", ["invitation_token"], name: "index_users_on_invitation_token", unique: true, using: :btree
add_index "users", ["invitations_count"], name: "index_users_on_invitations_count", using: :btree
add_index "users", ["invited_by_id"], name: "index_users_on_invited_by_id", using: :btree

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,99 @@
/*
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #998;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080;
}
.hljs-string,
.hljs-doctag {
color: #d14;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900;
font-weight: bold;
}
.hljs-subst {
font-weight: normal;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
color: #009926;
}
.hljs-symbol,
.hljs-bullet {
color: #990073;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-meta {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}