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();
});
})();

File diff suppressed because it is too large Load diff

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,27 +184,27 @@ 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
if parsed_file.too_large?
repository_response(t('general.file.size_exceeded',
file_size: Constants::FILE_MAX_SIZE_MB))
elsif parsed_file.empty?
flash[:notice] = t('teams.parse_sheet.errors.empty_file')
redirect_to back and return
else
@import_data = parsed_file.data
if parsed_file.generated_temp_file?
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))
elsif parsed_file.empty?
flash[:notice] = t('teams.parse_sheet.errors.empty_file')
redirect_to back and return
else
@import_data = parsed_file.data
if parsed_file.generated_temp_file?
respond_to do |format|
format.json do
render json: {
html: render_to_string(
@ -212,16 +212,16 @@ class RepositoriesController < ApplicationController
)
}
end
else
repository_response(t('teams.parse_sheet.errors.temp_file_failure'))
end
else
repository_response(t('teams.parse_sheet.errors.temp_file_failure'))
end
rescue ArgumentError, CSV::MalformedCSVError
repository_response(t('teams.parse_sheet.errors.invalid_file',
encoding: ''.encoding))
rescue TypeError
repository_response(t('teams.parse_sheet.errors.invalid_extension'))
end
rescue ArgumentError, CSV::MalformedCSVError
repository_response(t('teams.parse_sheet.errors.invalid_file',
encoding: ''.encoding))
rescue TypeError
repository_response(t('teams.parse_sheet.errors.invalid_extension'))
end
end
@ -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,13 +321,15 @@ class RepositoriesController < ApplicationController
end
def repository_response(message)
format.html do
flash[:alert] = message
redirect_to :back
end
format.json do
render json: { message: message },
status: :unprocessable_entity
respond_to do |format|
format.html do
flash[:alert] = message
redirect_to :back
end
format.json do
render json: { message: message },
status: :unprocessable_entity
end
end
end

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 "assigned" column is sorted
direction = sort_null_direction(order_params)
if @my_module
# Depending on the sort, order nulls first or
# nulls last on repository_cells association
return records if dt_params[:assigned] == 'assigned'
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(params[:order].values[0])
if @my_module
# Depending on the sort, order nulls first or
# nulls last on repository_cells association
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
action_url = action_url.gsub(/<IsLicensedUser=BUSINESS_USER&>/,
'IsLicensedUser=0&')
action_url = action_url.gsub(/<IsLicensedUser=BUSINESS_USER>/,
'IsLicensedUser=0')
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
(2..sheet.last_row).each do |i|
error = []
record_row = RepositoryRow.new(name: sheet.row(i)[name_index],
repository: self,
created_by: user,
last_modified_by: user)
transaction do
(2..sheet.last_row).each do |i|
total_nr += 1
record_row = RepositoryRow.new(name: sheet.row(i)[name_index],
repository: self,
created_by: user,
last_modified_by: user)
record_row.transaction(requires_new: true) do
unless record_row.save
errors = true
raise ActiveRecord::Rollback
end
next unless record_row.valid?
sheet.row(i).each.with_index do |value, index|
if custom_fields[index] && value
rep_column = RepositoryTextValue.new(
data: value,
created_by: user,
last_modified_by: user,
repository_cell_attributes: {
repository_row: record_row,
repository_column: custom_fields[index]
}
)
error << rep_column.errors.messages unless rep_column.save
row_cell_values = []
sheet.row(i).each.with_index do |value, index|
if columns[index] && value
cell_value = RepositoryTextValue.new(
data: value,
created_by: user,
last_modified_by: user,
repository_cell_attributes: {
repository_row: record_row,
repository_column: columns[index]
}
)
cell = RepositoryCell.new(repository_row: record_row,
repository_column: columns[index],
value: cell_value)
cell.skip_on_import = true
cell_value.repository_cell = cell
unless cell.valid? && cell_value.valid?
errors = true
raise ActiveRecord::Rollback
end
row_cell_values << cell_value
end
end
if RepositoryTextValue.import(row_cell_values,
recursive: true,
validate: false).failed_instances.any?
errors = true
raise ActiveRecord::Rollback
end
nr_of_added += 1
end
end
if error.any?
record_row.destroy
else
nr_of_added += 1
record_row.save
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
Roo::CSV.new(file_path, csv_options: { col_sep: "\t" })
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,17 +78,17 @@ 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
sgroup_index = i
else
cf = CustomField.find_by_id(v)
@ -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
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
if sgroup
sample.sample_group = sgroup
else
sample.create_sample_group(
name: value,
team_id: id
)
end
sample.save
elsif value and mappings[index.to_s].strip.present? and index != sname_index
if custom_fields[index]
# we're working with CustomField
scf = SampleCustomField.new(
sample_id: sample.id,
custom_field_id: custom_fields[index].id,
value: value
)
if !scf.save
error << scf.errors.messages
unless stype
stype = SampleType.new(name: value, team: self)
unless stype.save
errors = true
raise ActiveRecord::Rollback
end
else
# This custom_field does not exist
error << {"#{mappings[index]}": "Does not exists"}
end
sample.sample_type = stype
elsif index == sgroup_index
sgroup = SampleGroup.where(name: value, team: self).take
unless sgroup
sgroup = SampleGroup.new(name: value, team: self)
unless sgroup.save
errors = true
raise ActiveRecord::Rollback
end
end
sample.sample_group = sgroup
elsif value && custom_fields[index]
# we're working with CustomField
scf = SampleCustomField.new(
sample: sample,
custom_field: custom_fields[index],
value: value
)
unless scf.valid?
errors = true
raise ActiveRecord::Rollback
end
sample.sample_custom_fields << scf
end
end
else
error << sample.errors.messages
end
if error.present?
errors << { "#{i}": error}
else
if Sample.import([sample],
recursive: true,
validate: false).failed_instances.any?
errors = true
raise ActiveRecord::Rollback
end
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,29 +30,31 @@
include_blank: t('teams.parse_sheet.do_not_include_column'),
hide_label: true) %>
<br />
<% if th.length > Constants::NAME_TRUNCATION_LENGTH_DROPDOWN %>
<div class="modal-tooltip">
<%= truncate(th, length: Constants::NAME_TRUNCATION_LENGTH_DROPDOWN) %>
</div>
<% if th.nil? %>
<i><%= t('repositories.import_records.no_header_name') %></i>
<% else %>
<%= th %>
<% if th.length > Constants::NAME_TRUNCATION_LENGTH_DROPDOWN %>
<div class="modal-tooltip">
<%= truncate(th, length: Constants::NAME_TRUNCATION_LENGTH_DROPDOWN) %>
</div>
<% else %>
<%= th %>
<% end %>
<% end %>
</th>
<% end %>
</thead>
<tbody>
<% @import_data.rows.each do |row| %>
<tr>
<tr>
<td>
<p><%= t('teams.parse_sheet.example_value') %></p>
</td>
<% @import_data.columns.each do |td| %>
<td>
<p><%= t('teams.parse_sheet.example_value') %></p>
<%= td %>
</td>
<% row.each do |td| %>
<td>
<%= td[1] %>
</td>
<% end %>
</tr>
<% end %>
<% end %>
</tr>
</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,29 +25,31 @@
include_blank: t('teams.parse_sheet.do_not_include_column'),
hide_label: true) %>
<br />
<% if th.length > Constants::NAME_TRUNCATION_LENGTH_DROPDOWN %>
<div class="modal-tooltip">
<%= truncate(th, length: Constants::NAME_TRUNCATION_LENGTH_DROPDOWN) %>
</div>
<% if th.nil? %>
<i><%= t('samples.modal_import.no_header_name') %></i>
<% else %>
<%= th %>
<% if th.length > Constants::NAME_TRUNCATION_LENGTH_DROPDOWN %>
<div class="modal-tooltip">
<%= truncate(th, length: Constants::NAME_TRUNCATION_LENGTH_DROPDOWN) %>
</div>
<% else %>
<%= th %>
<% end %>
<% end %>
</th>
<% end %>
</thead>
<tbody>
<% @rows.each do |row| %>
<tr>
<tr>
<td>
<p><%= t('teams.parse_sheet.example_value') %></p>
</td>
<% @columns.each do |td| %>
<td>
<p><%= t('teams.parse_sheet.example_value') %></p>
<%= td %>
</td>
<% row.each do |td| %>
<td>
<%= td[1] %>
</td>
<% end %>
</tr>
<% end %>
<% end %>
</tr>
</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"