Merge branch 'master' into rails-5.1

Conflicts:
	Gemfile.lock
	app/datatables/repository_datatable.rb
	app/models/repository_cell.rb
This commit is contained in:
Luka Murn 2017-08-07 08:45:53 +02:00
commit ae5bccf709
36 changed files with 1332 additions and 1115 deletions

View file

@ -51,7 +51,7 @@ gem 'ajax-datatables-rails', '~> 0.3.1'
gem 'commit_param_routing' # Enables different submit actions in the same form to route to different actions in controller
gem 'kaminari'
gem "i18n-js", ">= 3.0.0.rc11" # Localization in javascript files
gem 'roo', '~> 2.1.0' # Spreadsheet parser
gem 'roo', '~> 2.7.1' # Spreadsheet parser
gem 'wicked_pdf'
gem 'silencer' # Silence certain Rails logs
gem 'wkhtmltopdf-heroku'
@ -64,6 +64,7 @@ gem 'sneaky-save', git: 'https://github.com/einzige/sneaky-save'
gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
gem 'delayed_paperclip'
gem 'rubyzip'
gem 'activerecord-import'
gem 'paperclip', '~> 5.1' # File attachment, image attachment library
gem 'aws-sdk', '~> 2'

View file

@ -7,7 +7,7 @@ GIT
GIT
remote: https://github.com/einzige/sneaky-save
revision: e7c77674abe74d598dfd58db7c680dd85936f207
revision: 7e7596720e76a3c243042be2f5f916525b143a54
specs:
sneaky-save (0.1.2)
activerecord (>= 3.2.0)
@ -56,6 +56,8 @@ GEM
activemodel (= 5.1.1)
activesupport (= 5.1.1)
arel (~> 8.0)
activerecord-import (0.19.1)
activerecord (>= 3.2)
activesupport (5.1.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
@ -70,26 +72,28 @@ GEM
ast (2.3.0)
auto_strip_attributes (2.1.0)
activerecord (>= 3.0)
autoprefixer-rails (7.1.1.2)
autoprefixer-rails (7.1.2.4)
execjs
autosize-rails (1.18.17)
rails (>= 3.1)
awesome_print (1.8.0)
aws-sdk (2.2.37)
aws-sdk-resources (= 2.2.37)
aws-sdk-core (2.2.37)
aws-sdk (2.10.21)
aws-sdk-resources (= 2.10.21)
aws-sdk-core (2.10.21)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.2.37)
aws-sdk-core (= 2.2.37)
aws-sdk-resources (2.10.21)
aws-sdk-core (= 2.10.21)
aws-sigv4 (1.0.1)
babel-source (5.8.35)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
base62 (1.0.0)
bcrypt (3.1.11)
better_errors (2.1.1)
better_errors (2.3.0)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
erubi (>= 1.0.0)
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
@ -102,9 +106,9 @@ GEM
bootstrap_form (2.7.0)
builder (3.2.3)
byebug (9.0.6)
capybara (2.14.4)
capybara (2.15.1)
addressable
mime-types (>= 1.16)
mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
@ -169,15 +173,14 @@ GEM
devise (>= 4.0.0)
diff-lcs (1.3)
docile (1.1.5)
erubi (1.6.0)
erubis (2.7.0)
erubi (1.6.1)
execjs (2.7.0)
factory_girl (4.8.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.8.0)
factory_girl (~> 4.8.0)
railties (>= 3.0.0)
faker (1.7.3)
faker (1.8.4)
i18n (~> 0.5)
ffi (1.9.18)
figaro (1.1.1)
@ -188,8 +191,8 @@ GEM
globalid (0.4.0)
activesupport (>= 4.2.0)
hammerjs-rails (2.0.4)
i18n (0.8.4)
i18n-js (3.0.0)
i18n (0.8.6)
i18n-js (3.0.1)
i18n (~> 0.6, >= 0.6.6)
introjs-rails (1.0.0)
sass-rails (>= 3.2)
@ -236,17 +239,18 @@ GEM
method_source (0.8.2)
mime-types (1.25.1)
mimemagic (0.3.2)
mini_mime (0.1.3)
mini_portile2 (2.2.0)
minitest (5.10.2)
minitest (5.10.3)
momentjs-rails (2.17.1)
railties (>= 3.1)
multi_json (1.12.1)
multi_test (0.1.2)
nested_form_fields (0.8)
nested_form_fields (0.8.1)
coffee-rails (>= 3.2.1)
jquery-rails
rails (>= 3.2.0)
newrelic_rpm (4.2.0.334)
newrelic_rpm (4.3.0.335)
nio4r (2.1.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
@ -260,7 +264,7 @@ GEM
cocaine (~> 0.5.5)
mime-types
mimemagic (~> 0.3.0)
parallel (1.11.2)
parallel (1.12.0)
parser (2.4.0.0)
ast (~> 2.2)
pg (0.21.0)
@ -318,7 +322,7 @@ GEM
rainbow (2.2.2)
rake
rake (12.0.0)
rb-fsevent (0.9.8)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rdoc (4.3.0)
@ -332,7 +336,7 @@ GEM
lazy_priority_queue (~> 0.1.0)
stream (~> 0.5.0)
rkelly-remix (0.0.7)
roo (2.1.1)
roo (2.7.1)
nokogiri (~> 1)
rubyzip (~> 1.1, < 2.0.0)
rspec-core (3.6.0)
@ -367,7 +371,7 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.4.24)
sass (3.4.25)
sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
@ -380,7 +384,7 @@ GEM
sdoc (0.4.2)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
shoulda-matchers (3.1.1)
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
silencer (1.0.1)
simple_token_authentication (1.15.1)
@ -413,8 +417,8 @@ GEM
stream (0.5)
thor (0.19.4)
thread_safe (0.3.6)
tilt (2.0.7)
tinymce-rails (4.6.4)
tilt (2.0.8)
tinymce-rails (4.6.5)
railties (>= 3.1.1)
turbolinks (5.0.1)
turbolinks-source (~> 5)
@ -446,6 +450,7 @@ PLATFORMS
ruby
DEPENDENCIES
activerecord-import
ajax-datatables-rails (~> 0.3.1)
aspector
auto_strip_attributes (~> 2.1)
@ -506,7 +511,7 @@ DEPENDENCIES
recaptcha
remotipart (~> 1.2)
rgl
roo (~> 2.1.0)
roo (~> 2.7.1)
rspec-rails
rubocop
ruby-graphviz (~> 1.2)
@ -536,4 +541,4 @@ RUBY VERSION
ruby 2.4.1p111
BUNDLED WITH
1.15.1
1.15.3

1
VERSION Normal file
View file

@ -0,0 +1 @@
1.12.3

View file

@ -18,7 +18,7 @@
$('#form-records-file').on('ajax:success', function(ev, data) {
$('#modal-import-records').modal('hide');
$(data.html).appendTo('body').promise().done(function() {
$('#parse-records_modal')
$('#parse-records-modal')
.modal('show')
.on('hidden.bs.modal', function() {
animateSpinner();
@ -26,6 +26,11 @@
});
repositoryRecordsImporter();
});
}).on('ajax:error', function(ev, data) {
$(this).find('.form-group').addClass('has-error');
$(this).find('.form-group').find('.help-block').remove();
$(this).find('.form-group').append("<span class='help-block'>" +
data.responseJSON.message + '</span>');
});
}
@ -41,8 +46,10 @@
success: function (data) {
var tabBody = $(pane.context.hash).find(".tab-content-body");
tabBody.html(data.html);
pane.tab('show').promise().done(function() {
pane.tab('show').promise().done(function(el) {
initImportRecordsModal();
RepositoryDatatable.destroy()
RepositoryDatatable.init(el.attr('data-repo-table'));
});
},
error: function (error) {

View file

@ -0,0 +1,10 @@
(function() {
'use strict';
// initialze repository datatable
$(document).ready(function() {
RepositoryDatatable.destroy()
RepositoryDatatable.init($('#content').attr('data-repo-id'));
onClickToggleAssignedRecords();
});
})();

View file

@ -1,5 +1,11 @@
//= require jquery-ui/widgets/sortable
var RepositoryDatatable = (function(global) {
'use strict';
var TABLE_ID = '';
var TABLE = {};
// Extend datatables API with searchable options
// (http://stackoverflow.com/questions/39912395/datatables-dynamically-set-columns-searchable)
$.fn.dataTable.Api.register('isColumnSearchable()', function(colSelector) {
@ -31,7 +37,6 @@ var selectedRecord;
var myData;
var loadFirstTime = true;
var table;
var originalHeader;
// Tells whether to filter only assigned repository records
@ -39,9 +44,9 @@ var viewAssigned;
function dataTableInit() {
// Make a copy of original repository table header
originalHeader = $('#repository-table thead').children().clone();
originalHeader = $(TABLE_ID + ' thead').children().clone();
viewAssigned = 'assigned';
table = $('#repository-table').DataTable({
TABLE = $(TABLE_ID).DataTable({
order: [[2, 'desc']],
dom: "R<'row'<'col-sm-9-custom toolbar'l><'col-sm-3-custom'f>>tpi",
stateSave: true,
@ -57,7 +62,7 @@ function dataTableInit() {
},
destroy: true,
ajax: {
url: $('#repository-table').data('source'),
url: $(TABLE_ID).data('source'),
data: function(d) {
d.assigned = viewAssigned;
},
@ -82,7 +87,6 @@ function dataTableInit() {
rowCallback: function(row, data) {
// Get row ID
var rowId = data.DT_RowId;
// If row ID is in the list of selected row IDs
if ($.inArray(rowId, rowsSelected) !== -1) {
$(row).find('input[type="checkbox"]').prop('checked', true);
@ -90,7 +94,7 @@ function dataTableInit() {
}
},
columns: (function() {
var numOfColumns = $('#repository-table').data('num-columns');
var numOfColumns = $(TABLE_ID).data('num-columns');
var columns = [];
for (var i = 0; i < numOfColumns; i++) {
var visible = (i <= 4);
@ -108,7 +112,7 @@ function dataTableInit() {
animateSpinner(this, false);
changeToViewMode();
updateButtons();
updateDataTableSelectAllCtrl(table);
updateDataTableSelectAllCtrl();
// Prevent row toggling when selecting user smart annotation link
SmartAnnotation.preventPropagation('.atwho-user-popover');
@ -127,7 +131,7 @@ function dataTableInit() {
// Send an Ajax request to the server to get the data. Note that
// this is a synchronous request since the data is expected back from the
// function
var repositoryId = $('#repository-table').data('repository-id');
var repositoryId = $(TABLE_ID).data('repository-id');
$.ajax({
url: '/repositories/' + repositoryId + '/state_load',
data: {},
@ -142,13 +146,12 @@ function dataTableInit() {
},
stateSaveCallback: function(settings, data) {
// Send an Ajax request to the server with the state object
var repositoryId = $('#repository-table').data('repository-id');
var repositoryId = $(TABLE_ID).data('repository-id');
// Save correct data
if (loadFirstTime === true) {
data = myData;
}
$.ajax({
async: false,
url: '/repositories/' + repositoryId + '/state_save',
data: { state: data },
dataType: 'json',
@ -160,9 +163,9 @@ function dataTableInit() {
fnInitComplete: function(oSettings) {
// Reload correct column order and visibility (if you refresh page)
// First two columns are fixed
table.column(0).visible(true);
table.column(1).visible(true);
for (var i = 2; i < table.columns()[0].length; i++) {
TABLE.column(0).visible(true);
TABLE.column(1).visible(true);
for (var i = 2; i < TABLE.columns()[0].length; i++) {
var visibility = false;
if (myData.columns[i]) {
visibility = myData.columns[i].visible;
@ -170,15 +173,16 @@ function dataTableInit() {
if (typeof (visibility) === 'string') {
visibility = (visibility === 'true');
}
table.column(i).visible(visibility);
table.setColumnSearchable(i, visibility);
TABLE.column(i).visible(visibility);
TABLE.setColumnSearchable(i, visibility);
}
oSettings._colReorder.fnOrder(myData.ColReorder);
table.on('mousedown', function() {
TABLE.on('mousedown', function() {
$('#repository-columns-dropdown').removeClass('open');
});
initHeaderTooltip();
initRowSelection();
bindExportActions();
}
});
@ -187,7 +191,7 @@ function dataTableInit() {
$('div.toolbarButtons').show();
// Handle click on table cells with checkboxes
$('#repository-table').on('click', 'tbody td', function(e) {
$(TABLE_ID).on('click', 'tbody td', function(e) {
if ($(e.target).is('.repository-row-selector')) {
// Skip if clicking on selector checkbox
return;
@ -195,7 +199,7 @@ function dataTableInit() {
$(this).parent().find('.repository-row-selector').trigger('click');
});
table.on('column-reorder', function() {
TABLE.on('column-reorder', function() {
initRowSelection();
});
@ -203,19 +207,18 @@ function dataTableInit() {
animateLoading();
});
return table;
}
table = dataTableInit();
// Timeout for table header scrolling
setTimeout(function() {
table.columns.adjust();
TABLE.columns.adjust();
}, 10);
return TABLE;
}
// Enables noSearchHidden plugin
$.fn.dataTable.defaults.noSearchHidden = true;
function bindExportActions() {
$('form#form-export').submit(function() {
var form = this;
@ -225,7 +228,7 @@ $('form#form-export').submit(function() {
$(form).find('input[name=header_ids\\[\\]]').remove();
// Append visible column information
$('.active table#repository-table thead tr th').each(function() {
$('.active table' + TABLE_ID + ' thead tr th').each(function() {
var th = $(this);
var val;
switch ($(th).attr('id')) {
@ -259,6 +262,8 @@ $('form#form-export').submit(function() {
});
}
});
}
function appendInput(form, val, name) {
$(form).append(
@ -277,7 +282,7 @@ function initRowSelection() {
}
// Get row ID
var $row = $(this).closest('tr');
var data = table.row($row).data();
var data = TABLE.row($row).data();
var rowId = data.DT_RowId;
// Determine whether row ID is in the list of selected row IDs
@ -297,7 +302,7 @@ function initRowSelection() {
$row.removeClass('selected');
}
updateDataTableSelectAllCtrl(table);
updateDataTableSelectAllCtrl();
e.stopPropagation();
updateButtons();
@ -318,9 +323,9 @@ function initRowSelection() {
}
// Updates "Select all" control in a data table
function updateDataTableSelectAllCtrl(table) {
var $table = table.table().node();
var $header = table.table().header();
function updateDataTableSelectAllCtrl() {
var $table = TABLE.table().node();
var $header = TABLE.table().header();
var $chkboxAll = $('.repository-row-selector', $table);
var $chkboxChecked = $('.repository-row-selector:checked', $table);
var chkboxSelectAll = $('input[name="select_all"]', $header).get(0);
@ -361,9 +366,9 @@ function initHeaderTooltip() {
var width = 200;
// set tooltip params in the table body
if ( $(this).parents('#repository-table').length ) {
offsetLeft = $('#repository-table').offset().left + 100;
width = $('#repository-table').width() - 200;
if ($(this).parents(TABLE_ID).length) {
offsetLeft = $(TABLE_ID).offset().left + 100;
width = $(TABLE_ID).width() - 200;
}
$('body').append($tooltip);
$tooltip.css('background-color', '#d2d2d2');
@ -386,16 +391,16 @@ function initHeaderTooltip() {
});
}
function onClickAddRecord() {
global.onClickAddRecord = function() {
changeToEditMode();
updateButtons();
saveAction = 'create';
var tr = document.createElement('tr');
if (table.column(1).visible() === false) {
table.column(1).visible(true);
if (TABLE.column(1).visible() === false) {
TABLE.column(1).visible(true);
}
$('table#repository-table thead tr').children('th').each(function() {
$('table' + TABLE_ID + ' thead tr').children('th').each(function() {
var th = $(this);
var td;
var input;
@ -418,7 +423,7 @@ function onClickAddRecord() {
tr.appendChild(createTdElement(''));
}
});
$('table#repository-table').prepend(tr);
$('table' + TABLE_ID).prepend(tr);
selectedRecord = tr;
// initialize smart annotation
@ -431,7 +436,7 @@ function onClickAddRecord() {
adjustTableHeader();
}
(function onClickToggleAssignedRecords() {
global.onClickToggleAssignedRecords = function() {
$('.repository-assign-group > .btn').click(function() {
$('.btn-group > .btn').removeClass('active btn-primary');
$('.btn-group > .btn').addClass('btn-default');
@ -440,19 +445,19 @@ function onClickAddRecord() {
$('#assigned-repo-records').on('click', function() {
viewAssigned = 'assigned';
table.ajax.reload(function() {
TABLE.ajax.reload(function() {
initRowSelection();
}, false);
});
$('#all-repo-records').on('click', function() {
viewAssigned = 'all';
table.ajax.reload(function() {
TABLE.ajax.reload(function() {
initRowSelection();
}, false);
});
})();
};
function onClickAssignRecords() {
global.onClickAssignRecords = function() {
animateSpinner();
$.ajax({
url: $('#assignRepositoryRecords').data('assign-url'),
@ -472,7 +477,7 @@ function onClickAssignRecords() {
});
}
function onClickUnassignRecords() {
global.onClickUnassignRecords = function() {
animateSpinner();
$.ajax({
url: $('#unassignRepositoryRecords').data('unassign-url'),
@ -492,10 +497,10 @@ function onClickUnassignRecords() {
});
}
function onClickDeleteRecord() {
global.onClickDeleteRecord = function() {
animateSpinner();
$.ajax({
url: $('table#repository-table').data('delete-record'),
url: $('table' + TABLE_ID).data('delete-record'),
type: 'POST',
dataType: 'json',
data: {selected_rows: rowsSelected},
@ -515,12 +520,12 @@ function onClickDeleteRecord() {
}
// Edit record
function onClickEdit() {
global.onClickEdit = function() {
if (rowsSelected.length !== 1) {
return;
}
var row = table.row('#' + rowsSelected[0]);
var row = TABLE.row('#' + rowsSelected[0]);
var node = row.node();
var rowData = row.data();
@ -537,8 +542,8 @@ function onClickEdit() {
type: 'GET',
dataType: 'json',
success: function(data) {
if (table.column(1).visible() === false) {
table.column(1).visible(true);
if (TABLE.column(1).visible() === false) {
TABLE.column(1).visible(true);
}
// Show save and cancel buttons in first two columns
$(node).children('td').eq(0).html($('#saveRecord').clone());
@ -556,8 +561,8 @@ function onClickEdit() {
var cells = data.repository_row.repository_cells;
$(node).children('td').each(function(i) {
var td = $(this);
var rawIndex = table.column.index('fromVisible', i);
var colHeader = table.column(rawIndex).header();
var rawIndex = TABLE.column.index('fromVisible', i);
var colHeader = TABLE.column(rawIndex).header();
if ($(colHeader).hasClass('repository-column')) {
// Check if cell on this record exists
var cell = cells[$(colHeader).attr('id')];
@ -573,7 +578,6 @@ function onClickEdit() {
});
// initialize smart annotation
SmartAnnotation.init($('[data-object="repository_cell"]'));
_.each($('[data-object="repository_cell"]'), function(el) {
if (_.isUndefined($(el).data('atwho'))) {
SmartAnnotation.init(el);
@ -596,11 +600,11 @@ function onClickEdit() {
}
// Save record
function onClickSave() {
global.onClickSave = function() {
var node;
var rowData;
if (saveAction === 'update') {
var row = table.row(selectedRecord);
var row = TABLE.row(selectedRecord);
node = row.node();
rowData = row.data();
} else if (saveAction === 'create') {
@ -608,7 +612,7 @@ function onClickSave() {
}
// First fetch all the data in input fields
var data = {
request_url: $('#repository-table').data('current-uri'),
request_url: $(TABLE_ID).data('current-uri'),
repository_row_id: $(selectedRecord).attr('id'),
repository_row: {},
repository_cells: {}
@ -633,7 +637,7 @@ function onClickSave() {
type = 'PUT';
} else {
type = 'POST';
url = $('table#repository-table').data('create-record');
url = $('table' + TABLE_ID).data('create-record');
}
$.ajax({
url: url,
@ -642,9 +646,11 @@ function onClickSave() {
data: data,
success: function(data) {
HelperModule.flashAlertMsg(data.flash, 'success');
SmartAnnotation.closePopup();
onClickCancel();
},
error: function(e) {
SmartAnnotation.closePopup();
var data = e.responseJSON;
clearAllErrors();
@ -785,15 +791,16 @@ function clearRowSelection() {
}
// Restore previous table
function onClickCancel() {
global.onClickCancel = function() {
if ($('#assigned').text().length === 0) {
table.column(1).visible(false);
TABLE.column(1).visible(false);
}
table.ajax.reload(function() {
TABLE.ajax.reload(function() {
initRowSelection();
}, false);
changeToViewMode();
updateButtons();
SmartAnnotation.closePopup();
animateSpinner(null, false);
}
@ -811,7 +818,7 @@ function getColumnIndex(id) {
if (id < 0) {
return false;
}
return table.column(id).index('visible');
return TABLE.column(id).index('visible');
}
// Takes object and surrounds it with input
@ -829,34 +836,31 @@ function createTdElement(content) {
// Adjust columns width in table header
function adjustTableHeader() {
table.columns.adjust();
TABLE.columns.adjust();
$('.dropdown-menu').parent()
.on('shown.bs.dropdown hidden.bs.dropdown', function() {
table.columns.adjust();
TABLE.columns.adjust();
});
}
function changeToViewMode() {
currentMode = 'viewMode';
// Table specific stuff
table.button(0).enable(true);
TABLE.button(0).enable(true);
}
function changeToEditMode() {
currentMode = 'editMode';
// Table specific stuff
table.button(0).enable(false);
TABLE.button(0).enable(false);
initHeaderTooltip();
}
/*
* Repository columns dropdown
*/
(function(table) {
'use strict';
var dropdown = $('#repository-columns-dropdown');
var dropdownList = $('#repository-columns-list');
var dropdown, dropdownList;
function createNewColumn() {
// Make an Ajax request to repository_columns_controller
@ -892,11 +896,11 @@ function changeToEditMode() {
$('div.toolbarButtons').hide();
// Destroy datatable
table.destroy();
TABLE.destroy();
// Add number of columns
$('#repository-table').data('num-columns',
$('#repository-table').data('num-columns') + 1);
$(TABLE_ID).data('num-columns',
$(TABLE_ID).data('num-columns') + 1);
// Add column to table (=table header)
originalHeader.append(
'<th class="repository-column" id="' + data.id + '" ' +
@ -908,13 +912,13 @@ function changeToEditMode() {
// Remove all event handlers as we re-initialize them later with
// new table
$('#repository-table').off();
$('#repository-table thead').empty();
$('#repository-table thead').append(originalHeader);
$(TABLE_ID).off();
$(TABLE_ID +' thead').empty();
$(TABLE_ID + ' thead').append(originalHeader);
// Re-initialize datatable
table = dataTableInit();
table.on('init.dt', function() {
TABLE = dataTableInit();
TABLE.on('init.dt', function() {
loadColumnsNames();
dropdownOverflow();
});
@ -960,10 +964,10 @@ function changeToEditMode() {
var scrollPosition = dropdownList.scrollTop();
// Clear the list
dropdownList.find('li[data-position]').remove();
_.each(table.columns().header(), function(el, index) {
_.each(TABLE.columns().header(), function(el, index) {
if (index > 1) {
var colIndex = $(el).attr('data-column-index');
var visible = table.column(colIndex).visible();
var visible = TABLE.column(colIndex).visible();
var editable = $(el).is('[data-editable]');
var deletable = $(el).is('[data-deletable]');
@ -993,19 +997,19 @@ function changeToEditMode() {
'form-control" style="display: none;" />' +
'<span class="pull-right controls">' +
'<span class="ok glyphicon glyphicon-ok" style="display: none;" ' +
'title="' + $('#repository-table').data('save-text') + '"></span>' +
'title="' + $(TABLE_ID).data('save-text') + '"></span>' +
'<span class="cancel glyphicon glyphicon-remove" ' +
'style="display: none;" ' +
'title="' + $('#repository-table').data('cancel-text') +
'title="' + $(TABLE_ID).data('cancel-text') +
'"></span>' +
'<span class="vis glyphicon ' + visClass + '" title="' +
$('#repository-table').data('columns-visibility-text') + '">' +
$(TABLE_ID).data('columns-visibility-text') + '">' +
'</span> ' +
'<span class="edit glyphicon glyphicon-pencil ' + editClass +
'" title="' + $('#repository-table').data('edit-text') +
'" title="' + $(TABLE_ID).data('edit-text') +
'"></span>' +
'<span class="del glyphicon glyphicon-trash ' + delClass +
'" title="' + $('#repository-table').data('columns-delete-text') +
'" title="' + $(TABLE_ID).data('columns-delete-text') +
'"></span>' +
'</span><br></span></li>';
dropdownList.append(html);
@ -1037,27 +1041,27 @@ function changeToEditMode() {
event.stopPropagation();
var self = $(this);
var li = self.closest('li');
var column = table.column(li.attr('data-position'));
var column = TABLE.column(li.attr('data-position'));
if (column.visible()) {
self.addClass('glyphicon-eye-close');
self.removeClass('glyphicon-eye-open');
li.addClass('col-invisible');
column.visible(false);
table.setColumnSearchable(column.index(), false);
TABLE.setColumnSearchable(column.index(), false);
} else {
self.addClass('glyphicon-eye-open');
self.removeClass('glyphicon-eye-close');
li.removeClass('col-invisible');
column.visible(true);
table.setColumnSearchable(column.index(), true);
TABLE.setColumnSearchable(column.index(), true);
initHeaderTooltip();
}
// Re-filter/search if neccesary
var searchText = $('div.dataTables_filter input').val();
if (!_.isEmpty(searchText)) {
table.search(searchText).draw();
TABLE.search(searchText).draw();
}
initRowSelection();
});
@ -1069,7 +1073,7 @@ function changeToEditMode() {
cancel: '.new-repository-column',
axis: 'y',
update: function() {
var reorderer = table.colReorder;
var reorderer = TABLE.colReorder;
var listIds = [];
// We skip first two columns
listIds.push(0, 1);
@ -1106,7 +1110,7 @@ function changeToEditMode() {
dropdownList.sortable('enable');
$(li).clearFormErrors();
text.html(generateColumnNameTooltip(newName));
$(table.columns().header()).filter('#' + id)
$(TABLE.columns().header()).filter('#' + id)
.html(generateColumnNameTooltip(newName));
originalHeader.find('#' + id).html(newName);
cancelEditMode();
@ -1290,28 +1294,28 @@ function changeToEditMode() {
$('div.toolbarButtons').hide();
// Destroy datatable
table.destroy();
TABLE.destroy();
// Subtract number of columns
$('#repository-table').data(
$(TABLE_ID).data(
'num-columns',
$('#repository-table').data('num-columns') - 1
$(TABLE_ID).data('num-columns') - 1
);
// Remove column from table (=table header) & rows
var th = originalHeader.find('#' + id);
var index = th.index();
th.remove();
$('#repository-table tbody td:nth-child(' + (index + 1) + ')').remove();
$(TABLE_ID + ' tbody td:nth-child(' + (index + 1) + ')').remove();
// Remove all event handlers as we re-initialize them later with
// new table
$('#repository-table').off();
$('#repository-table thead').empty();
$('#repository-table thead').append(originalHeader);
$(TABLE_ID).off();
$(TABLE_ID + ' thead').empty();
$(TABLE_ID + ' thead').append(originalHeader);
// Re-initialize datatable
table = dataTableInit();
TABLE = dataTableInit();
loadColumnsNames();
// Hide modal
@ -1343,7 +1347,7 @@ function changeToEditMode() {
}
function generateColumnNameTooltip(name) {
var maxLength = $('#repository-table').data('max-dropdown-length');
var maxLength = $(TABLE_ID).data('max-dropdown-length');
if ($.trim(name).length > maxLength) {
return '<div class="modal-tooltip">' +
truncateLongString(name, maxLength) +
@ -1354,7 +1358,9 @@ function changeToEditMode() {
// initialze dropdown after the table is loaded
function initDropdown() {
table.on('init.dt', function() {
TABLE.on('init.dt', function() {
dropdown = $('#repository-columns-dropdown');
dropdownList = $('#repository-columns-list');
initNewColumnForm();
initSorting();
toggleColumnVisibility();
@ -1371,5 +1377,19 @@ function changeToEditMode() {
});
}
function init(id) {
TABLE_ID = id;
TABLE = dataTableInit();
initDropdown();
})(table);
}
function destroy() {
TABLE = {};
TABLE_ID = '';
}
return Object.freeze({
init: init,
destroy: destroy
});
})(window);

View file

@ -0,0 +1,9 @@
(function() {
'use strict';
$(document).ready(function() {
$("[data-trigger='about-modal']").on('click', function() {
$('[data-role=about-modal]').modal('show');
});
});
})();

View file

@ -424,6 +424,11 @@ var SmartAnnotation = (function() {
init: init
};
}
// Closes the atwho popup * needed in repositories to close the popup
// if nothing is selected and the user leaves the form *
function closePopup() {
$('.atwho-header-res').find('.glyphicon-remove').click();
}
function initialize(field) {
var atWho = new setAtWho(field);
@ -432,7 +437,8 @@ var SmartAnnotation = (function() {
var publicApi = Object.freeze({
init: initialize,
preventPropagation: atwhoStopPropagation
preventPropagation: atwhoStopPropagation,
closePopup: closePopup
});
return publicApi;

View file

@ -184,18 +184,17 @@ class RepositoriesController < ApplicationController
def parse_sheet
repository = current_team.repositories.find_by_id(params[:id])
parsed_file = ImportRepository::ParseRepository.new(
file: params[:file],
repository: repository,
session: session
)
respond_to do |format|
unless params[:file]
repository_response(t('teams.parse_sheet.errors.no_file_selected'))
return
end
begin
parsed_file = ImportRepository::ParseRepository.new(
file: params[:file],
repository: repository,
session: session
)
if parsed_file.too_large?
repository_response(t('general.file.size_exceeded',
file_size: Constants::FILE_MAX_SIZE_MB))
@ -205,6 +204,7 @@ class RepositoriesController < ApplicationController
else
@import_data = parsed_file.data
if parsed_file.generated_temp_file?
respond_to do |format|
format.json do
render json: {
html: render_to_string(
@ -212,6 +212,7 @@ class RepositoriesController < ApplicationController
)
}
end
end
else
repository_response(t('teams.parse_sheet.errors.temp_file_failure'))
end
@ -223,7 +224,6 @@ class RepositoriesController < ApplicationController
repository_response(t('teams.parse_sheet.errors.invalid_extension'))
end
end
end
def import_records
respond_to do |format|
@ -238,8 +238,9 @@ class RepositoriesController < ApplicationController
number_of_rows: status[:nr_of_added])
render json: {}, status: :ok
else
flash[:alert] = t('repositories.import_records.error_flash',
message: status[:errors])
flash[:alert] =
t('repositories.import_records.partial_success_flash',
nr: status[:nr_of_added], total_nr: status[:total_nr])
render json: {}, status: :unprocessable_entity
end
else
@ -320,6 +321,7 @@ class RepositoriesController < ApplicationController
end
def repository_response(message)
respond_to do |format|
format.html do
flash[:alert] = message
redirect_to :back
@ -329,6 +331,7 @@ class RepositoriesController < ApplicationController
status: :unprocessable_entity
end
end
end
def generate_zip
# Fetch rows in the same order as in the currently viewed datatable

View file

@ -19,9 +19,7 @@ class RepositoryRowsController < ApplicationController
record.transaction do
record.name = record_params[:name] unless record_params[:name].blank?
unless record.save
errors[:default_fields] = record.errors.messages
end
errors[:default_fields] = record.errors.messages unless record.save
if params[:repository_cells]
params[:repository_cells].each do |key, value|
column = @repository.repository_columns.detect do |c|
@ -94,9 +92,7 @@ class RepositoryRowsController < ApplicationController
@record.transaction do
@record.name = record_params[:name].blank? ? nil : record_params[:name]
unless @record.save
errors[:default_fields] = @record.errors.messages
end
errors[:default_fields] = @record.errors.messages unless @record.save
if params[:repository_cells]
params[:repository_cells].each do |key, value|
existing = @record.repository_cells.detect do |c|
@ -119,7 +115,7 @@ class RepositoryRowsController < ApplicationController
column = @repository.repository_columns.detect do |c|
c.id == key.to_i
end
value = RepositoryTextValue.new(
cell_value = RepositoryTextValue.new(
data: value,
created_by: current_user,
last_modified_by: current_user,
@ -128,15 +124,15 @@ class RepositoryRowsController < ApplicationController
repository_column: column
}
)
if value.save
record_annotation_notification(@record, value.repository_cell)
if cell_value.save
record_annotation_notification(@record,
cell_value.repository_cell)
else
errors[:repository_cells] << {
"#{column.id}": value.errors.messages
"#{column.id}": cell_value.errors.messages
}
end
end
raise ActiveRecord::Rollback if errors[:repository_cells].any?
end
# Clean up empty cells, not present in updated record
@record.repository_cells.each do |cell|

View file

@ -36,8 +36,7 @@ class TeamsController < ApplicationController
# Get data (it will trigger any errors as well)
@header = sheet.row(1)
@rows = [];
@rows << Hash[[@header, sheet.row(2)].transpose]
@columns = sheet.row(2)
# Fill in fields for dropdown
@available_fields = @team.get_available_sample_fields

View file

@ -70,11 +70,11 @@ class WopiController < ActionController::Base
UserCanNotWriteRelative: true,
CloseUrl: @close_url,
DownloadUrl: url_for(controller: 'assets', action: 'download',
id: @asset.id),
id: @asset.id, host: ENV['WOPI_USER_HOST']),
HostEditUrl: url_for(controller: 'assets', action: 'edit',
id: @asset.id),
id: @asset.id, host: ENV['WOPI_USER_HOST']),
HostViewUrl: url_for(controller: 'assets', action: 'view',
id: @asset.id),
id: @asset.id, host: ENV['WOPI_USER_HOST']),
BreadcrumbBrandName: @breadcrumb_brand_name,
BreadcrumbBrandUrl: @breadcrumb_brand_url,
BreadcrumbFolderName: @breadcrumb_folder_name,
@ -82,7 +82,7 @@ class WopiController < ActionController::Base
}
response.headers['X-WOPI-HostEndpoint'] = ENV['WOPI_ENDPOINT_URL']
response.headers['X-WOPI-MachineName'] = ENV['WOPI_ENDPOINT_URL']
response.headers['X-WOPI-ServerVersion'] = Constants::APP_VERSION
response.headers['X-WOPI-ServerVersion'] = Scinote::Application::VERSION
render json: msg and return
end
@ -286,21 +286,21 @@ class WopiController < ActionController::Base
if @protocol.in_module?
@close_url = protocols_my_module_url(@protocol.my_module,
only_path: false,
host: ENV['WOPI_BREADCRUMBS_HOST'])
host: ENV['WOPI_USER_HOST'])
project = @protocol.my_module.experiment.project
@breadcrumb_brand_name = project.name
@breadcrumb_brand_url = project_url(project,
only_path: false,
host: ENV['WOPI_BREADCRUMBS_HOST'])
host: ENV['WOPI_USER_HOST'])
@breadcrumb_folder_name = @protocol.my_module.name
else
@close_url = protocols_url(only_path: false,
host: ENV['WOPI_BREADCRUMBS_HOST'])
host: ENV['WOPI_USER_HOST'])
@breadcrump_brand_name = 'Projects'
@breadcrumb_brand_url = root_url(only_path: false,
host: ENV['WOPI_BREADCRUMBS_HOST'])
host: ENV['WOPI_USER_HOST'])
@breadcrumb_folder_name = 'Protocol managament'
end
@breadcrumb_folder_url = @close_url
@ -310,12 +310,12 @@ class WopiController < ActionController::Base
@close_url = results_my_module_url(@my_module,
only_path: false,
host: ENV['WOPI_BREADCRUMBS_HOST'])
host: ENV['WOPI_USER_HOST'])
@breadcrumb_brand_name = @my_module.experiment.project.name
@breadcrumb_brand_url = project_url(@my_module.experiment.project,
only_path: false,
host: ENV['WOPI_BREADCRUMBS_HOST'])
host: ENV['WOPI_USER_HOST'])
@breadcrumb_folder_name = @my_module.name
@breadcrumb_folder_url = @close_url
end

View file

@ -152,7 +152,7 @@ class RepositoryDatatable < CustomDatatable
# Make mappings of custom columns, so we have same id for every column
i = 5
@columns_mappings = {}
@repository.repository_columns.each do |column|
@repository.repository_columns.order(:id).each do |column|
@columns_mappings[column.id] = i.to_s
i += 1
end
@ -230,13 +230,68 @@ class RepositoryDatatable < CustomDatatable
# Override default sort method if needed
def sort_records(records)
if sort_column(order_params) == ASSIGNED_SORT_COL
if params[:order].present? && params[:order].length == 1
if sort_column(params[:order].values[0]) == ASSIGNED_SORT_COL
# If "assigned" column is sorted when viewing assigned items
return records if @my_module && params[:assigned] == 'assigned'
# If "assigned" column is sorted
direction = sort_null_direction(order_params)
direction = sort_null_direction(params[:order].values[0])
if @my_module
# Depending on the sort, order nulls first or
# nulls last on repository_cells association
return records if dt_params[:assigned] == 'assigned'
records.joins(
"LEFT OUTER JOIN my_module_repository_rows ON
(repository_rows.id = my_module_repository_rows.repository_row_id
AND (my_module_repository_rows.my_module_id = #{@my_module.id} OR
my_module_repository_rows.id IS NULL))"
).order("my_module_repository_rows.id NULLS #{direction}")
else
sort_assigned_records(records, params[:order].values[0]['dir'])
end
elsif sorting_by_custom_column
# Check if have to filter records first
# if params[:search].present? && params[:search][:value].present?
# # Couldn't force ActiveRecord to yield the same query as below because
# # Rails apparently forgets to join stuff in subqueries -
# # #justrailsthings
# conditions = build_conditions_for(params[:search][:value])
#
# filter_query = %(SELECT "samples"."id" FROM "samples"
# LEFT OUTER JOIN "sample_custom_fields" ON
# "sample_custom_fields"."sample_id" = "samples"."id"
# LEFT OUTER JOIN "users" ON "users"."id" = "repository_row"."user_id"
# WHERE "samples"."team_id" = #{@team.id} AND #{conditions.to_sql})
#
# records = records.where("samples.id IN (#{filter_query})")
# end
ci = sortable_displayed_columns[
params[:order].values[0][:column].to_i - 1
]
column_id = @columns_mappings.key((ci.to_i + 1).to_s)
dir = sort_direction(params[:order].values[0])
# Because repository records can have multiple custom cells,
# we first group them by samples.id and inside that group we sort them by column_id. Because
# we sort them ASC, sorted columns will be on top. Distinct then only
# takes the first row and cuts the rest of every group and voila we have
# 1 row for every sample, which are not sorted yet ...
# records = records.select('DISTINCT ON (repository_rows.id) *')
# .order("repository_rows.id, CASE WHEN repository_cells.repository_column_id = #{column_id} THEN 1 ELSE 2 END ASC")
# ... this little gem (pun intended) then takes the records query, sorts it again
# and paginates it. sq.t0_* are determined empirically and are crucial -
# imagine A -> B -> C transitive relation but where A and C are the
# same. Useless right? But not when you acknowledge that find_by_sql
# method does some funky stuff when your query spans multiple queries -
# Sample object might have id from SampleType, name from
# User ... chaos ensues basically. If something changes in db this might
# change.
# formated_date = (I18n.t 'time.formats.datatables_date').gsub!(/^\"|\"?$/, '')
# Sample.find_by_sql("SELECT sq.t0_r0 as id, sq.t0_r1 as name, to_char( sq.t0_r4, '#{ formated_date }' ) as created_at, sq.t0_r5, s, sq.t0_r2 as user_id, sq.custom_field_id FROM (#{records.to_sql})
# as sq ORDER BY CASE WHEN sq.custom_field_id = #{column_id} THEN 1 ELSE 2 END #{dir}, sq.value #{dir}
# LIMIT #{per_page} OFFSET #{offset}")
records.joins(
"LEFT OUTER JOIN my_module_repository_rows ON
(repository_rows.id = my_module_repository_rows.repository_row_id
@ -307,4 +362,21 @@ class RepositoryDatatable < CustomDatatable
@sortable_displayed_columns = sort_order
end
def sort_assigned_records(records, direction)
assigned = records.joins(:my_module_repository_rows).distinct.pluck(:id)
unassigned = records.where.not(id: assigned).pluck(:id)
if direction == 'asc'
ids = assigned + unassigned
elsif direction == 'desc'
ids = unassigned + assigned
end
order_by_index = ActiveRecord::Base.send(
:sanitize_sql_array,
["position((',' || repository_rows.id || ',') in ?)",
ids.join(',') + ',']
)
records.order(order_by_index)
end
end

View file

@ -0,0 +1,8 @@
module AddonsHelper
def list_all_addons
Rails::Engine
.subclasses
.select { |c| c.name.start_with?('Scinote') }
.map(&:parent)
end
end

View file

@ -1075,11 +1075,13 @@ module PermissionHelper
end
def can_delete_column_in_repository(column)
is_normal_user_or_admin_of_team(column.repository.team)
column.created_by == current_user ||
is_admin_of_team(column.repository.team)
end
def can_edit_column_in_repository(column)
is_normal_user_or_admin_of_team(column.repository.team)
column.created_by == current_user ||
is_admin_of_team(column.repository.team)
end
def can_create_repository_records(repository)

View file

@ -384,10 +384,17 @@ class Asset < ApplicationRecord
action = get_action(file_ext, action)
if !action.nil?
action_url = action.urlsrc
if ENV['WOPI_BUSINESS_USERS'] && ENV['WOPI_BUSINESS_USERS']=='true'
action_url = action_url.gsub(/<IsLicensedUser=BUSINESS_USER&>/,
'IsLicensedUser=1&')
action_url = action_url.gsub(/<IsLicensedUser=BUSINESS_USER>/,
'IsLicensedUser=1')
else
action_url = action_url.gsub(/<IsLicensedUser=BUSINESS_USER&>/,
'IsLicensedUser=0&')
action_url = action_url.gsub(/<IsLicensedUser=BUSINESS_USER>/,
'IsLicensedUser=0')
end
action_url = action_url.gsub(/<.*?=.*?>/, '')
rest_url = Rails.application.routes.url_helpers.wopi_rest_endpoint_url(

View file

@ -111,57 +111,82 @@ class Repository < ApplicationRecord
# Imports records
def import_records(sheet, mappings, user)
errors = []
custom_fields = []
errors = false
columns = []
name_index = -1
total_nr = 0
nr_of_added = 0
mappings.each.with_index do |(_k, value), index|
if value == '-1'
# Fill blank space, so our indices stay the same
custom_fields << nil
columns << nil
name_index = index
else
cf = repository_columns.find_by_id(value)
custom_fields << cf
columns << repository_columns.find_by_id(value)
end
end
# Check for duplicate columns
col_compact = columns.compact
unless col_compact.map(&:id).uniq.length == col_compact.length
return { status: :error, nr_of_added: nr_of_added, total_nr: total_nr }
end
# Now we can iterate through record data and save stuff into db
transaction do
(2..sheet.last_row).each do |i|
error = []
total_nr += 1
record_row = RepositoryRow.new(name: sheet.row(i)[name_index],
repository: self,
created_by: user,
last_modified_by: user)
record_row.transaction(requires_new: true) do
unless record_row.save
errors = true
raise ActiveRecord::Rollback
end
row_cell_values = []
next unless record_row.valid?
sheet.row(i).each.with_index do |value, index|
if custom_fields[index] && value
rep_column = RepositoryTextValue.new(
if columns[index] && value
cell_value = RepositoryTextValue.new(
data: value,
created_by: user,
last_modified_by: user,
repository_cell_attributes: {
repository_row: record_row,
repository_column: custom_fields[index]
repository_column: columns[index]
}
)
error << rep_column.errors.messages unless rep_column.save
cell = RepositoryCell.new(repository_row: record_row,
repository_column: columns[index],
value: cell_value)
cell.skip_on_import = true
cell_value.repository_cell = cell
unless cell.valid? && cell_value.valid?
errors = true
raise ActiveRecord::Rollback
end
row_cell_values << cell_value
end
end
if error.any?
record_row.destroy
else
if RepositoryTextValue.import(row_cell_values,
recursive: true,
validate: false).failed_instances.any?
errors = true
raise ActiveRecord::Rollback
end
nr_of_added += 1
record_row.save
end
end
end
if errors.count > 0
return { status: :error, errors: errors, nr_of_added: nr_of_added }
if errors
return { status: :error, nr_of_added: nr_of_added, total_nr: total_nr }
end
{ status: :ok, nr_of_added: nr_of_added }
{ status: :ok, nr_of_added: nr_of_added, total_nr: total_nr }
end
private
@ -170,13 +195,11 @@ class Repository < ApplicationRecord
case File.extname(filename)
when '.csv'
Roo::CSV.new(file_path, extension: :csv)
when '.tdv'
Roo::CSV.new(file_path, nil, :ignore, csv_options: { col_sep: '\t' })
when '.tsv'
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
when '.txt'
# This assumption is based purely on biologist's habits
Roo::CSV.new(file_path, csv_options: { col_sep: '\t' })
when '.xls'
Roo::Excel.new(file_path)
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
when '.xlsx'
Roo::Excelx.new(file_path)
else

View file

@ -1,11 +1,15 @@
class RepositoryCell < ApplicationRecord
belongs_to :repository_row, optional: true
belongs_to :repository_column, optional: true
belongs_to :value, polymorphic: true, dependent: :destroy, optional: true
class RepositoryCell < ActiveRecord::Base
attr_accessor :skip_on_import
belongs_to :repository_row
belongs_to :repository_column
belongs_to :value, polymorphic: true, dependent: :destroy
validates :repository_column, presence: true
validate :repository_column_data_type
validates :repository_row, uniqueness: { scope: :repository_column }
validates :repository_row,
uniqueness: { scope: :repository_column },
unless: :skip_on_import
private

View file

@ -43,16 +43,14 @@ class Team < ApplicationRecord
end
case File.extname(filename)
when ".csv" then
when '.csv' then
Roo::CSV.new(file_path, extension: :csv)
when ".tdv" then
Roo::CSV.new(file_path, nil, :ignore, csv_options: {col_sep: "\t"})
when ".txt" then
when '.tsv' then
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
when '.txt' then
# This assumption is based purely on biologist's habits
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
when ".xls" then
Roo::Excel.new(file_path)
when ".xlsx" then
when '.xlsx' then
Roo::Excelx.new(file_path)
else
raise TypeError
@ -71,7 +69,7 @@ class Team < ApplicationRecord
# -3 == sample_group
# TODO: use constants
def import_samples(sheet, mappings, user)
errors = []
errors = false
nr_of_added = 0
total_nr = 0
@ -80,15 +78,15 @@ class Team < ApplicationRecord
sname_index = -1
stype_index = -1
sgroup_index = -1
mappings.each.with_index do |(k, v), i|
if v == "-1"
mappings.each.with_index do |(_, v), i|
if v == '-1'
# Fill blank space, so our indices stay the same
custom_fields << nil
sname_index = i
elsif v == "-2"
elsif v == '-2'
custom_fields << nil
stype_index = i
elsif v == "-3"
elsif v == '-3'
custom_fields << nil
sgroup_index = i
else
@ -99,87 +97,70 @@ class Team < ApplicationRecord
custom_fields << cf
end
end
# Now we can iterate through sample data and save stuff into db
(2..sheet.last_row).each do |i|
error = []
total_nr += 1
sample = Sample.new(name: sheet.row(i)[sname_index],
team: self,
user: user)
sample = Sample.new(
name: sheet.row(i)[sname_index],
team_id: id,
user: user
)
sample.transaction do
unless sample.valid?
errors = true
raise ActiveRecord::Rollback
end
if sample.save
sheet.row(i).each.with_index do |value, index|
# We need to have sample saved before messing with custom fields (they
# need sample id)
if index == stype_index
stype = SampleType.where(name: value, team_id: id).take
stype = SampleType.where(name: value, team: self).take
if stype
unless stype
stype = SampleType.new(name: value, team: self)
unless stype.save
errors = true
raise ActiveRecord::Rollback
end
end
sample.sample_type = stype
else
sample.create_sample_type(
name: value,
team_id: id
)
end
sample.save
elsif index == sgroup_index
sgroup = SampleGroup.where(name: value, team_id: id).take
sgroup = SampleGroup.where(name: value, team: self).take
if sgroup
sample.sample_group = sgroup
else
sample.create_sample_group(
name: value,
team_id: id
)
unless sgroup
sgroup = SampleGroup.new(name: value, team: self)
unless sgroup.save
errors = true
raise ActiveRecord::Rollback
end
sample.save
elsif value and mappings[index.to_s].strip.present? and index != sname_index
if custom_fields[index]
end
sample.sample_group = sgroup
elsif value && custom_fields[index]
# we're working with CustomField
scf = SampleCustomField.new(
sample_id: sample.id,
custom_field_id: custom_fields[index].id,
sample: sample,
custom_field: custom_fields[index],
value: value
)
if !scf.save
error << scf.errors.messages
unless scf.valid?
errors = true
raise ActiveRecord::Rollback
end
else
# This custom_field does not exist
error << {"#{mappings[index]}": "Does not exists"}
sample.sample_custom_fields << scf
end
end
if Sample.import([sample],
recursive: true,
validate: false).failed_instances.any?
errors = true
raise ActiveRecord::Rollback
end
else
error << sample.errors.messages
end
if error.present?
errors << { "#{i}": error}
else
nr_of_added += 1
end
end
if errors.count > 0 then
return {
status: :error,
errors: errors,
nr_of_added: nr_of_added,
total_nr: total_nr
}
if errors
return { status: :error, nr_of_added: nr_of_added, total_nr: total_nr }
else
return {
status: :ok,
nr_of_added: nr_of_added,
total_nr: total_nr
}
return { status: :ok, nr_of_added: nr_of_added, total_nr: total_nr }
end
end

View file

@ -11,15 +11,14 @@ module ImportRepository
def data
# Get data (it will trigger any errors as well)
header = @sheet.row(1)
rows = []
rows << Hash[[header, @sheet.row(2)].transpose]
columns = @sheet.row(2)
# Fill in fields for dropdown
@repository.available_repository_fields.transform_values! do |name|
truncate(name, length: Constants::NAME_TRUNCATION_LENGTH_DROPDOWN)
end
@temp_file = TempFile.create(session_id: @session.id, file: @file)
Data.new(header,
rows,
columns,
@repository.available_repository_fields,
@repository,
@temp_file)
@ -47,7 +46,7 @@ module ImportRepository
end
Data = Struct.new(
:header, :rows, :available_fields, :repository, :temp_file
:header, :columns, :available_fields, :repository, :temp_file
)
end
end

View file

@ -45,6 +45,9 @@
</div>
</div>
<!-- About us modal -->
<%= render "shared/about_modal" %>
<%= render "shared/navigation" %>
<div id="notifications">

View file

@ -31,7 +31,8 @@
<% end %>
</div>
<div id="content">
<div id="content"
data-repo-id="#repository-table-<%= @repository.id %>">
<%= render partial: "repositories/repository_table",
locals: {
repository: @repository,
@ -40,3 +41,7 @@
}
%>
</div>
<%= stylesheet_link_tag 'datatables' %>
<%= javascript_include_tag 'repositories/repository_datatable' %>
<%= javascript_include_tag 'repositories/my_module_repository' %>

View file

@ -1,5 +1,5 @@
<div class="modal fade"
id="parse-records_modal"
id="parse-records-modal"
aria-labelledby="parse-modal-title"
role="dialog">
<div class="modal-dialog modal-lg">
@ -30,6 +30,9 @@
include_blank: t('teams.parse_sheet.do_not_include_column'),
hide_label: true) %>
<br />
<% if th.nil? %>
<i><%= t('repositories.import_records.no_header_name') %></i>
<% else %>
<% if th.length > Constants::NAME_TRUNCATION_LENGTH_DROPDOWN %>
<div class="modal-tooltip">
<%= truncate(th, length: Constants::NAME_TRUNCATION_LENGTH_DROPDOWN) %>
@ -37,22 +40,21 @@
<% else %>
<%= th %>
<% end %>
<% end %>
</th>
<% end %>
</thead>
<tbody>
<% @import_data.rows.each do |row| %>
<tr>
<td>
<p><%= t('teams.parse_sheet.example_value') %></p>
</td>
<% row.each do |td| %>
<% @import_data.columns.each do |td| %>
<td>
<%= td[1] %>
<%= td %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>

View file

@ -1,5 +1,5 @@
<div class="repository-table">
<table id="repository-table" class="table"
<table id="repository-table-<%= repository.id %>" class="table"
data-current-uri="<%= request.original_url %>"
data-repository-id="<%= repository.id %>"
data-source="<%= repository_index_link %>"
@ -19,7 +19,7 @@
<th id="row-name"><%= t("repositories.table.row_name") %></th>
<th id="added-on"><%= t("repositories.table.added_on") %></th>
<th id="added-by"><%= t("repositories.table.added_by") %></th>
<% repository.repository_columns.each do |column| %>
<% repository.repository_columns.order(:id).each do |column| %>
<th class="repository-column" id="<%= column.id %>"
<%= 'data-editable' if can_edit_column_in_repository(column) %>
<%= 'data-deletable' if can_delete_column_in_repository(column) %>
@ -35,6 +35,3 @@
<tbody></tbody>
</table>
</div>
<%= stylesheet_link_tag 'datatables' %>
<%= javascript_include_tag('repositories/repository_datatable') %>

View file

@ -10,6 +10,7 @@
<li role="presentation">
<a href="#custom_repo_<%= repo.id %>"
data-toggle="tab"
data-repo-table="#repository-table-<%= repo.id %>"
aria-controls="custom_repo_<%= repo.id %>"
data-url="<%=team_repository_show_tab_path(current_team, repo)%>"
title="<%=repo.name%>"><%= truncate(repo.name, length: Constants::NAME_TRUNCATION_LENGTH) %></a>
@ -53,4 +54,6 @@
</div>
<% end %>
<%= stylesheet_link_tag 'datatables' %>
<%= javascript_include_tag 'repositories/repository_datatable' %>
<%= javascript_include_tag "repositories/index", "data-turbolinks-track" => true %>

View file

@ -25,6 +25,9 @@
include_blank: t('teams.parse_sheet.do_not_include_column'),
hide_label: true) %>
<br />
<% if th.nil? %>
<i><%= t('samples.modal_import.no_header_name') %></i>
<% else %>
<% if th.length > Constants::NAME_TRUNCATION_LENGTH_DROPDOWN %>
<div class="modal-tooltip">
<%= truncate(th, length: Constants::NAME_TRUNCATION_LENGTH_DROPDOWN) %>
@ -32,22 +35,21 @@
<% else %>
<%= th %>
<% end %>
<% end %>
</th>
<% end %>
</thead>
<tbody>
<% @rows.each do |row| %>
<tr>
<td>
<p><%= t('teams.parse_sheet.example_value') %></p>
</td>
<% row.each do |td| %>
<% @columns.each do |td| %>
<td>
<%= td[1] %>
<%= td %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>

View file

@ -0,0 +1,28 @@
<div class="modal" id="aboutModal" tabindex="-1" role="dialog" aria-labelledby="aboutModal" data-role="about-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><%= t('about.modal_title') %></h4>
</div>
<div class="modal-body">
<strong><%= t('about.core_version') %></strong>
<br />
<%= Scinote::Application::VERSION %>
<br />
<br />
<div data-hook="about-modal-addon-versions">
<strong><%= t('about.addon_versions') %></strong>
<br />
<% list_all_addons.each do |addon| %>
<%= "#{addon.name}:" %>
<br />
<%= addon::VERSION %>
<br />
<br />
<% end %>
</div>
</div>
</div>
</div>
</div>

View file

@ -19,7 +19,7 @@
<% if show_version %>
<%= image_tag('/images/logo.png', class: 'with-version', id: 'logo') %>
<span class="version">
<%= Constants::APP_VERSION %>
<%= Scinote::Application::VERSION %>
</span>
<% else %>
<%= image_tag('/images/logo.png', id: 'logo') %>
@ -234,6 +234,12 @@
<li><%= link_to t('nav.help.contact'),
Constants::CONTACT_URL,
target: "_blank" %></li>
<li role="separator" class="divider"></li>
<li>
<%= link_to '#', data: { trigger: 'about-modal' } do %>
<%= t('nav.help.about') %>
<% end %>
</li>
</ul>
</li>

View file

@ -203,7 +203,10 @@
<ul class="dropdown-menu repositories-dropdown-menu" aria-labelledby="repositoriesDropdownMenuLink">
<% @my_module.experiment.project.team.repositories.order(created_at: :asc).each do |repository| %>
<li>
<a class="dropdown-item" href="<%= repository_my_module_url(id: @my_module, repository_id: repository) %>" title="<%= repository.name %>">
<a class="dropdown-item"
href="<%= repository_my_module_url(id: @my_module, repository_id: repository) %>"
title="<%= repository.name %>"
data-no-turbolink="true">
<%= truncate(repository.name) %>
</a>
</li>

View file

@ -29,5 +29,8 @@ module Scinote
csv: 'text/plain',
wopitest: ['text/plain', 'inode/x-empty']
}
# sciNote Core Application version
VERSION = File.read(Rails.root.join('VERSION')).strip.freeze
end
end

View file

@ -80,6 +80,8 @@ Rails.application.config.assets.precompile += %w(repositories/index.js)
Rails.application.config.assets.precompile += %w(repositories/edit.js)
Rails.application.config.assets.precompile +=
%w(repositories/repository_datatable.js)
Rails.application.config.assets.precompile +=
%w(repositories/my_module_repository.js)
# Libraries needed for Handsontable formulas
Rails.application.config.assets.precompile += %w(lodash.js)

View file

@ -196,9 +196,6 @@ class Constants
# Other
#=============================================================================
# Application version
APP_VERSION = '1.12.1'.freeze
TEXT_EXTRACT_FILE_TYPES = [
'application/pdf',
'application/rtf',

View file

@ -105,7 +105,7 @@ Devise.setup do |config|
# The period the generated invitation token is valid, after
# this period, the invited resource won't be able to accept the invitation.
# When invite_for is 0 (the default), the invitation won't expire.
config.invite_for = 3.days
config.invite_for = 7.days
# Number of invitations users can send.
# - If invitation_limit is nil, there is no limit for invitations, users can
@ -163,7 +163,7 @@ Devise.setup do |config|
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
config.confirm_within = 3.days
config.confirm_within = 7.days
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email

View file

@ -81,7 +81,7 @@ module Paperclip
# Determine file content type from its name
def content_types_from_name
@content_types_from_name ||=
Paperclip.run('mimetype', '-b :file_name', file_name: @name).chomp
Paperclip.run('mimetype', '-b -- :file_name', file_name: @name).chomp
end
# Determine file media type from its name
@ -92,7 +92,7 @@ module Paperclip
# Determine file content type from mimetype command
def type_from_mimetype_command
@type_from_mimetype_command ||=
Paperclip.run('mimetype', '-b :file', file: @file.path).chomp
Paperclip.run('mimetype', '-b -- :file', file: @file.path).chomp
end
# Determine file media type from mimetype command
@ -105,7 +105,7 @@ module Paperclip
def type_from_file_command
unless defined? @type_from_file_command
@type_from_file_command =
Paperclip.run('file', '-b --mime :file', file: @file.path)
Paperclip.run('file', '-b --mime -- :file', file: @file.path)
.split(/[:;]\s+/).first
if allowed_spoof_exception?(@type_from_file_command,

View file

@ -72,6 +72,7 @@ en:
support: "Customer support"
premium: "Premium"
contact: "Contact us"
about: "About sciNote"
activities:
none: "No activities!"
label:
@ -87,6 +88,11 @@ en:
info: "Info"
account: "Account"
about:
modal_title: "About sciNote"
core_version: "sciNote core version"
addon_versions: "Addon versions"
sidebar:
title: "Navigation"
no_module_group: "No workflow"
@ -905,8 +911,9 @@ en:
add_new_record: "Add new item"
import_records:
import: 'Import'
no_header_name: 'No column name'
success_flash: "%{number_of_rows} new item(s) successfully imported."
error_flash: "Something went wrong: %{message}"
partial_success_flash: "%{nr} of %{total_nr} successfully imported. Other rows contained errors."
error_message:
temp_file_not_found: "This file could not be found. Your session might expire."
session_expired: "Your session expired. Please try again."
@ -944,7 +951,7 @@ en:
title: 'Import items'
modal_import:
title: 'Import items'
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.'
notice: 'You may upload .csv file (comma separated) or tab separated file (.txt or .tsv) or Excel file (.xlsx). First row should include header names, followed by rows with sample data.'
upload: 'Upload file'
js:
permission_error: "You don't have permission to edit this item."
@ -1002,7 +1009,8 @@ en:
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."
notice: "You may upload .csv file (comma separated) or tab separated file (.txt or .tsv) or Excel file (.xlsx). First row should include header names, followed by rows with sample data."
no_header_name: 'No column name'
upload: "Upload file"
modal_delete:
title: "Delete samples"

View file

@ -114,12 +114,16 @@ class AddonGenerator < Rails::Generators::NamedBase
gsub_file(file_path, '${ADDON_NAME}', @addon_name)
# lib/.../version.rb
dots = @modules.map { '/..' }.join
create_file(
"addons/#{@addon_name}/lib/" \
"#{@folders_path}/version.rb"
) do
embed_into_modules do
"VERSION = '0.0.1'.freeze\n"
"VERSION =\n" \
" File.read(\n" \
" \"\#{File.dirname(__FILE__)}#{dots}/../VERSION\"\n" \
" ).strip.freeze\n"
end
end
@ -182,6 +186,7 @@ class AddonGenerator < Rails::Generators::NamedBase
gsub_file(file_path, '${FULL_UNDERSCORE_NAME}', @full_underscore_name)
gsub_file(file_path, '${NAME}', name)
gsub_file(file_path, '${FOLDERS_PATH}', @folders_path)
create_file("addons/#{@addon_name}/VERSION") { '0.0.1' }
# Rakefile
file_path = "addons/#{@addon_name}/Rakefile"