From bd9904adb3a642c0bdd5bbc5eb08547dab96517f Mon Sep 17 00:00:00 2001 From: Nejc Bernot Date: Tue, 4 Jul 2017 14:27:43 +0200 Subject: [PATCH 01/27] Adds support for business servers If the WOPI_BUSINESS_USERS environmental variable is set and true, indicates to Office online that all users are business users when trying to view/edit files --- app/models/asset.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/models/asset.rb b/app/models/asset.rb index ffd2b0432..78dc3bf5f 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -380,10 +380,17 @@ class Asset < ActiveRecord::Base action = get_action(file_ext, action) if !action.nil? action_url = action.urlsrc - action_url = action_url.gsub(//, + if (ENV['WOPI_BUSINESS_USERS']) { + action_url = action_url.gsub(//, + 'IsLicensedUser=1&') + action_url = action_url.gsub(//, + 'IsLicensedUser=1') + } else { + action_url = action_url.gsub(//, 'IsLicensedUser=0&') - action_url = action_url.gsub(//, + action_url = action_url.gsub(//, 'IsLicensedUser=0') + } action_url = action_url.gsub(/<.*?=.*?>/, '') rest_url = Rails.application.routes.url_helpers.wopi_rest_endpoint_url( From 91fe7fe4ad178729dbfb3b4ce30b265b2c7f5976 Mon Sep 17 00:00:00 2001 From: Nejc Bernot Date: Tue, 4 Jul 2017 17:13:59 +0200 Subject: [PATCH 02/27] Changes the completely NOT ruby code --- app/models/asset.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/models/asset.rb b/app/models/asset.rb index 78dc3bf5f..90a58a800 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -380,17 +380,17 @@ class Asset < ActiveRecord::Base action = get_action(file_ext, action) if !action.nil? action_url = action.urlsrc - if (ENV['WOPI_BUSINESS_USERS']) { + if (ENV['WOPI_BUSINESS_USERS']) action_url = action_url.gsub(//, - 'IsLicensedUser=1&') + 'IsLicensedUser=1&') action_url = action_url.gsub(//, - 'IsLicensedUser=1') - } else { + 'IsLicensedUser=1') + else action_url = action_url.gsub(//, - 'IsLicensedUser=0&') + 'IsLicensedUser=0&') action_url = action_url.gsub(//, - 'IsLicensedUser=0') - } + 'IsLicensedUser=0') + end action_url = action_url.gsub(/<.*?=.*?>/, '') rest_url = Rails.application.routes.url_helpers.wopi_rest_endpoint_url( From 803934e07d06ab1ad367fac450e7c7a66a87596e Mon Sep 17 00:00:00 2001 From: Nejc Bernot Date: Wed, 5 Jul 2017 10:23:10 +0200 Subject: [PATCH 03/27] Changes the host for the download and redirect paths --- app/controllers/wopi_controller.rb | 6 +++--- app/models/asset.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/wopi_controller.rb b/app/controllers/wopi_controller.rb index dd7b30192..7b954c71e 100644 --- a/app/controllers/wopi_controller.rb +++ b/app/controllers/wopi_controller.rb @@ -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_BREADCRUMBS_HOST']), HostEditUrl: url_for(controller: 'assets', action: 'edit', - id: @asset.id), + id: @asset.id, host: ENV['WOPI_BREADCRUMBS_HOST']), HostViewUrl: url_for(controller: 'assets', action: 'view', - id: @asset.id), + id: @asset.id, host: ENV['WOPI_BREADCRUMBS_HOST']), BreadcrumbBrandName: @breadcrumb_brand_name, BreadcrumbBrandUrl: @breadcrumb_brand_url, BreadcrumbFolderName: @breadcrumb_folder_name, diff --git a/app/models/asset.rb b/app/models/asset.rb index 90a58a800..9e674f059 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -380,7 +380,7 @@ class Asset < ActiveRecord::Base action = get_action(file_ext, action) if !action.nil? action_url = action.urlsrc - if (ENV['WOPI_BUSINESS_USERS']) + if (ENV['WOPI_BUSINESS_USERS'] && ENV['WOPI_BUSINESS_USERS']=='true' ) action_url = action_url.gsub(//, 'IsLicensedUser=1&') action_url = action_url.gsub(//, From d917cf3340a506a2f16ec82230c3dd6c9b93e968 Mon Sep 17 00:00:00 2001 From: Nejc Bernot Date: Wed, 5 Jul 2017 11:52:36 +0200 Subject: [PATCH 04/27] Renames the WOPI_BREADCRUMBS_HOST env variable to WOPI_USER_HOST --- app/controllers/wopi_controller.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/controllers/wopi_controller.rb b/app/controllers/wopi_controller.rb index 7b954c71e..7bd0490b3 100644 --- a/app/controllers/wopi_controller.rb +++ b/app/controllers/wopi_controller.rb @@ -70,11 +70,11 @@ class WopiController < ActionController::Base UserCanNotWriteRelative: true, CloseUrl: @close_url, DownloadUrl: url_for(controller: 'assets', action: 'download', - id: @asset.id, host: ENV['WOPI_BREADCRUMBS_HOST']), + id: @asset.id, host: ENV['WOPI_USER_HOST']), HostEditUrl: url_for(controller: 'assets', action: 'edit', - id: @asset.id, host: ENV['WOPI_BREADCRUMBS_HOST']), + id: @asset.id, host: ENV['WOPI_USER_HOST']), HostViewUrl: url_for(controller: 'assets', action: 'view', - id: @asset.id, host: ENV['WOPI_BREADCRUMBS_HOST']), + id: @asset.id, host: ENV['WOPI_USER_HOST']), BreadcrumbBrandName: @breadcrumb_brand_name, BreadcrumbBrandUrl: @breadcrumb_brand_url, BreadcrumbFolderName: @breadcrumb_folder_name, @@ -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 From 16e672b9d893a7d5b7ecc6b4901eb420c45a2f80 Mon Sep 17 00:00:00 2001 From: Luka Murn Date: Mon, 10 Jul 2017 09:26:35 +0200 Subject: [PATCH 05/27] Add VERSION file where the sciNote Core version is stored --- VERSION | 1 + app/controllers/wopi_controller.rb | 2 +- app/views/shared/_navigation.html.erb | 2 +- config/application.rb | 3 +++ config/initializers/constants.rb | 3 --- 5 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 000000000..6f165bc1b --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.12.1 \ No newline at end of file diff --git a/app/controllers/wopi_controller.rb b/app/controllers/wopi_controller.rb index dd7b30192..b548489c2 100644 --- a/app/controllers/wopi_controller.rb +++ b/app/controllers/wopi_controller.rb @@ -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 diff --git a/app/views/shared/_navigation.html.erb b/app/views/shared/_navigation.html.erb index ff7598c0f..1c9affa91 100644 --- a/app/views/shared/_navigation.html.erb +++ b/app/views/shared/_navigation.html.erb @@ -19,7 +19,7 @@ <% if show_version %> <%= image_tag('/images/logo.png', class: 'with-version', id: 'logo') %> - <%= Constants::APP_VERSION %> + <%= Scinote::Application::VERSION %> <% else %> <%= image_tag('/images/logo.png', id: 'logo') %> diff --git a/config/application.rb b/config/application.rb index 2260cbeeb..91f89022a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -37,5 +37,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 diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb index f705420b5..1bb0e2bc2 100644 --- a/config/initializers/constants.rb +++ b/config/initializers/constants.rb @@ -196,9 +196,6 @@ class Constants # Other #============================================================================= - # Application version - APP_VERSION = '1.12.1'.freeze - TEXT_EXTRACT_FILE_TYPES = [ 'application/pdf', 'application/rtf', From 3e80f761b8692637c9b75c4d263dfffb96cafe0f Mon Sep 17 00:00:00 2001 From: Luka Murn Date: Mon, 10 Jul 2017 15:27:50 +0200 Subject: [PATCH 06/27] Add "about" modal, prepare hooks, VERSION system for addons Also the addon generator is fixed so it properly creates VERSION file for new addons. --- .../javascripts/sitewide/about_modal.js | 9 ++++++ app/helpers/addons_helper.rb | 8 ++++++ app/views/layouts/application.html.erb | 3 ++ app/views/shared/_about_modal.html.erb | 28 +++++++++++++++++++ app/views/shared/_navigation.html.erb | 6 ++++ config/locales/en.yml | 6 ++++ lib/generators/addon/addon_generator.rb | 7 ++++- 7 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/sitewide/about_modal.js create mode 100644 app/helpers/addons_helper.rb create mode 100644 app/views/shared/_about_modal.html.erb diff --git a/app/assets/javascripts/sitewide/about_modal.js b/app/assets/javascripts/sitewide/about_modal.js new file mode 100644 index 000000000..ca558059a --- /dev/null +++ b/app/assets/javascripts/sitewide/about_modal.js @@ -0,0 +1,9 @@ +(function() { + 'use strict'; + + $(document).ready(function() { + $("[data-trigger='about-modal']").on('click', function() { + $('[data-role=about-modal]').modal('show'); + }); + }); +})(); diff --git a/app/helpers/addons_helper.rb b/app/helpers/addons_helper.rb new file mode 100644 index 000000000..150af4976 --- /dev/null +++ b/app/helpers/addons_helper.rb @@ -0,0 +1,8 @@ +module AddonsHelper + def list_all_addons + Rails::Engine + .subclasses + .select { |c| c.name.start_with?('Scinote') } + .map(&:parent) + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 4b7027acf..a508c5f48 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -45,6 +45,9 @@ + + <%= render "shared/about_modal" %> + <%= render "shared/navigation" %>
diff --git a/app/views/shared/_about_modal.html.erb b/app/views/shared/_about_modal.html.erb new file mode 100644 index 000000000..3d2deeb92 --- /dev/null +++ b/app/views/shared/_about_modal.html.erb @@ -0,0 +1,28 @@ + \ No newline at end of file diff --git a/app/views/shared/_navigation.html.erb b/app/views/shared/_navigation.html.erb index 1c9affa91..17f3a5281 100644 --- a/app/views/shared/_navigation.html.erb +++ b/app/views/shared/_navigation.html.erb @@ -234,6 +234,12 @@
  • <%= link_to t('nav.help.contact'), Constants::CONTACT_URL, target: "_blank" %>
  • + +
  • + <%= link_to '#', data: { trigger: 'about-modal' } do %> + <%= t('nav.help.about') %> + <% end %> +
  • diff --git a/config/locales/en.yml b/config/locales/en.yml index 6dce7f8ec..fa31517ed 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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" diff --git a/lib/generators/addon/addon_generator.rb b/lib/generators/addon/addon_generator.rb index 5ef5a0892..8b4c51376 100644 --- a/lib/generators/addon/addon_generator.rb +++ b/lib/generators/addon/addon_generator.rb @@ -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" From a6b80589f2466f481c14996a3f619465c5b3fa65 Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Mon, 10 Jul 2017 15:32:18 +0200 Subject: [PATCH 07/27] Fix default order of the columns in repositories [SCI-1445] --- app/assets/javascripts/repositories/repository_datatable.js | 1 - app/views/repositories/_repository_table.html.erb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/repositories/repository_datatable.js b/app/assets/javascripts/repositories/repository_datatable.js index 3ceeab6ff..f60e0e0d9 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js +++ b/app/assets/javascripts/repositories/repository_datatable.js @@ -148,7 +148,6 @@ function dataTableInit() { data = myData; } $.ajax({ - async: false, url: '/repositories/' + repositoryId + '/state_save', data: {state: data}, dataType: 'json', diff --git a/app/views/repositories/_repository_table.html.erb b/app/views/repositories/_repository_table.html.erb index 3147dae51..d53c963f1 100644 --- a/app/views/repositories/_repository_table.html.erb +++ b/app/views/repositories/_repository_table.html.erb @@ -19,7 +19,7 @@ <%= t("repositories.table.row_name") %> <%= t("repositories.table.added_on") %> <%= t("repositories.table.added_by") %> - <% repository.repository_columns.each do |column| %> + <% repository.repository_columns.order(:id).each do |column| %> <%= 'data-deletable' if can_delete_column_in_repository(column) %> From baaec67399f72b9c975927bb600190bd6eb8fef2 Mon Sep 17 00:00:00 2001 From: zmagod Date: Mon, 10 Jul 2017 16:22:32 +0200 Subject: [PATCH 08/27] fixes issue with smart annotations in repositories [fixes SCI-1452] --- .../javascripts/repositories/repository_datatable.js | 4 +++- app/assets/javascripts/sitewide/atwho_res.js.erb | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/repositories/repository_datatable.js b/app/assets/javascripts/repositories/repository_datatable.js index 3ceeab6ff..165006d31 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js +++ b/app/assets/javascripts/repositories/repository_datatable.js @@ -573,7 +573,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); @@ -642,9 +641,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(); @@ -794,6 +795,7 @@ function onClickCancel() { }, false); changeToViewMode(); updateButtons(); + SmartAnnotation.closePopup(); animateSpinner(null, false); } diff --git a/app/assets/javascripts/sitewide/atwho_res.js.erb b/app/assets/javascripts/sitewide/atwho_res.js.erb index 377918eb3..24889894f 100644 --- a/app/assets/javascripts/sitewide/atwho_res.js.erb +++ b/app/assets/javascripts/sitewide/atwho_res.js.erb @@ -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; From 19a8dfe1160692f93c91279f3bf9698d556215b1 Mon Sep 17 00:00:00 2001 From: Luka Murn Date: Tue, 11 Jul 2017 08:32:52 +0200 Subject: [PATCH 09/27] Extend confirmation period of emails to 7 days Closes SCI-1432. --- config/initializers/devise.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c9372900d..28e701cf6 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -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 From d746ae254903c0b214535c753c1de236ee153104 Mon Sep 17 00:00:00 2001 From: Nbernot Date: Tue, 11 Jul 2017 13:33:47 +0200 Subject: [PATCH 10/27] Removes parentheses around if conditions --- app/models/asset.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/asset.rb b/app/models/asset.rb index 9e674f059..4ef35b06b 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -380,7 +380,7 @@ class Asset < ActiveRecord::Base action = get_action(file_ext, action) if !action.nil? action_url = action.urlsrc - if (ENV['WOPI_BUSINESS_USERS'] && ENV['WOPI_BUSINESS_USERS']=='true' ) + if ENV['WOPI_BUSINESS_USERS'] && ENV['WOPI_BUSINESS_USERS']=='true' action_url = action_url.gsub(//, 'IsLicensedUser=1&') action_url = action_url.gsub(//, From ec76873e950651fc4909628d92ecc801b05fcc17 Mon Sep 17 00:00:00 2001 From: Mojca Lorber Date: Thu, 13 Jul 2017 11:41:19 +0200 Subject: [PATCH 11/27] fix sorting by assigned causes ajax error --- app/datatables/repository_datatable.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/datatables/repository_datatable.rb b/app/datatables/repository_datatable.rb index bdc46480f..710ba08cf 100644 --- a/app/datatables/repository_datatable.rb +++ b/app/datatables/repository_datatable.rb @@ -234,6 +234,8 @@ class RepositoryDatatable < AjaxDatatablesRails::Base def sort_records(records) 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 From 739f8b5b4e895dcae6ca67db7a252a3a81fcea37 Mon Sep 17 00:00:00 2001 From: Mojca Lorber Date: Thu, 13 Jul 2017 13:49:26 +0200 Subject: [PATCH 12/27] remove redundant rollback call --- app/controllers/repository_rows_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index f971ae73f..03d3637fd 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -136,7 +136,6 @@ class RepositoryRowsController < ApplicationController } 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| From 506e47a595930ba77f96146e8035b20918e64b7d Mon Sep 17 00:00:00 2001 From: zmagod Date: Fri, 14 Jul 2017 09:45:36 +0200 Subject: [PATCH 13/27] fixes sorting of assigned column [fixes SCI-1483] --- app/datatables/repository_datatable.rb | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/datatables/repository_datatable.rb b/app/datatables/repository_datatable.rb index 710ba08cf..93d27554d 100644 --- a/app/datatables/repository_datatable.rb +++ b/app/datatables/repository_datatable.rb @@ -248,10 +248,7 @@ class RepositoryDatatable < AjaxDatatablesRails::Base my_module_repository_rows.id IS NULL))" ).order("my_module_repository_rows.id NULLS #{direction}") else - records.joins( - 'LEFT OUTER JOIN my_module_repository_rows ON - (repository_rows.id = my_module_repository_rows.repository_row_id)' - ).order("my_module_repository_rows.id NULLS #{direction}") + sort_assigned_records(records, params[:order].values[0]['dir']) end elsif sorting_by_custom_column # Check if have to filter records first @@ -391,4 +388,21 @@ class RepositoryDatatable < AjaxDatatablesRails::Base @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 From 036ee390ed5d570a8133e7860aee56f20e4126d1 Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Fri, 14 Jul 2017 16:19:03 +0200 Subject: [PATCH 14/27] Fix error messages in repository import and uploading files starting with hyphen[SCI-1484] --- app/assets/javascripts/repositories/index.js | 5 ++ app/controllers/repositories_controller.rb | 70 ++++++++++---------- config/initializers/paperclip.rb | 6 +- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/repositories/index.js b/app/assets/javascripts/repositories/index.js index d6150415e..3ff5474cf 100644 --- a/app/assets/javascripts/repositories/index.js +++ b/app/assets/javascripts/repositories/index.js @@ -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("" + + data.responseJSON.message + ''); }); } diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index ca41b2ed4..656f2385b 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -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 @@ -320,13 +320,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 diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 2cdeb718e..4f91e6ce7 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -80,7 +80,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 @@ -91,7 +91,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 @@ -104,7 +104,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, From a2b7bcda4c9da9a5853651bb4e3dfc329ae429df Mon Sep 17 00:00:00 2001 From: zmagod Date: Mon, 17 Jul 2017 15:05:28 +0200 Subject: [PATCH 15/27] refactor repositories js files [fixes SCI-1485] --- app/assets/javascripts/repositories/index.js | 4 +- .../repositories/my_module_repository.js | 10 + .../repositories/repository_datatable.js | 1689 +++++++++-------- app/views/my_modules/repository.html.erb | 7 +- .../repositories/_repository_table.html.erb | 5 +- app/views/repositories/index.html.erb | 3 + .../shared/_secondary_navigation.html.erb | 5 +- config/initializers/assets.rb | 2 + 8 files changed, 883 insertions(+), 842 deletions(-) create mode 100644 app/assets/javascripts/repositories/my_module_repository.js diff --git a/app/assets/javascripts/repositories/index.js b/app/assets/javascripts/repositories/index.js index d6150415e..1fa15f9f4 100644 --- a/app/assets/javascripts/repositories/index.js +++ b/app/assets/javascripts/repositories/index.js @@ -41,8 +41,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) { diff --git a/app/assets/javascripts/repositories/my_module_repository.js b/app/assets/javascripts/repositories/my_module_repository.js new file mode 100644 index 000000000..cc22b599c --- /dev/null +++ b/app/assets/javascripts/repositories/my_module_repository.js @@ -0,0 +1,10 @@ +(function() { + 'use strict'; + + // initialze repository datatable + $(document).ready(function() { + RepositoryDatatable.destroy() + RepositoryDatatable.init($('#content').attr('data-repo-id')); + onClickToggleAssignedRecords(); + }); +})(); diff --git a/app/assets/javascripts/repositories/repository_datatable.js b/app/assets/javascripts/repositories/repository_datatable.js index 47c5dbe06..be818201a 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js +++ b/app/assets/javascripts/repositories/repository_datatable.js @@ -1,863 +1,866 @@ //= require jquery-ui/sortable -// Extend datatables API with searchable options -// (http://stackoverflow.com/questions/39912395/datatables-dynamically-set-columns-searchable) -$.fn.dataTable.Api.register('isColumnSearchable()', function(colSelector) { - var idx = this.column(colSelector).index(); - return this.settings()[0].aoColumns[idx].bSearchable; -}); -$.fn.dataTable.Api - .register('setColumnSearchable()', function(colSelector, value) { - if (value !== this.isColumnSearchable(colSelector)) { - var idx = this.column(colSelector).index(); - this.settings()[0].aoColumns[idx].bSearchable = value; - if (value === true) { - this.rows().invalidate(); - } - } - return value; +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) { + var idx = this.column(colSelector).index(); + return this.settings()[0].aoColumns[idx].bSearchable; }); - -var rowsSelected = []; - -// Tells whether we're currently viewing or editing table -var currentMode = 'viewMode'; - -// Tells what action will execute by pressing on save button (update/create) -var saveAction = 'update'; -var selectedRecord; - -// Helps saving correct table state -var myData; -var loadFirstTime = true; - -var table; -var originalHeader; - -// Tells whether to filter only assigned repository records -var viewAssigned; - -function dataTableInit() { - // Make a copy of original repository table header - originalHeader = $('#repository-table thead').children().clone(); - viewAssigned = 'assigned'; - table = $('#repository-table').DataTable({ - order: [[2, 'desc']], - dom: "R<'row'<'col-sm-9-custom toolbar'l><'col-sm-3-custom'f>>tpi", - stateSave: true, - processing: true, - serverSide: true, - sScrollX: '100%', - sScrollXInner: '100%', - scrollY: '64vh', - scrollCollapse: true, - colReorder: { - fixedColumnsLeft: 2, - realtime: false - }, - destroy: true, - ajax: { - url: $('#repository-table').data('source'), - data: function(d) { - d.assigned = viewAssigned; - }, - global: false, - type: 'POST' - }, - columnDefs: [{ - targets: 0, - searchable: false, - orderable: false, - className: 'dt-body-center', - sWidth: '1%', - render: function() { - return ""; + $.fn.dataTable.Api + .register('setColumnSearchable()', function(colSelector, value) { + if (value !== this.isColumnSearchable(colSelector)) { + var idx = this.column(colSelector).index(); + this.settings()[0].aoColumns[idx].bSearchable = value; + if (value === true) { + this.rows().invalidate(); + } + } + return value; + }); + + var rowsSelected = []; + + // Tells whether we're currently viewing or editing table + var currentMode = 'viewMode'; + + // Tells what action will execute by pressing on save button (update/create) + var saveAction = 'update'; + var selectedRecord; + + // Helps saving correct table state + var myData; + var loadFirstTime = true; + + var originalHeader; + + // Tells whether to filter only assigned repository records + var viewAssigned; + + function dataTableInit() { + // Make a copy of original repository table header + originalHeader = $(TABLE_ID + ' thead').children().clone(); + viewAssigned = 'assigned'; + TABLE = $(TABLE_ID).DataTable({ + order: [[2, 'desc']], + dom: "R<'row'<'col-sm-9-custom toolbar'l><'col-sm-3-custom'f>>tpi", + stateSave: true, + processing: true, + serverSide: true, + sScrollX: '100%', + sScrollXInner: '100%', + scrollY: '64vh', + scrollCollapse: true, + colReorder: { + fixedColumnsLeft: 2, + realtime: false + }, + destroy: true, + ajax: { + url: $(TABLE_ID).data('source'), + data: function(d) { + d.assigned = viewAssigned; + }, + global: false, + type: 'POST' + }, + columnDefs: [{ + targets: 0, + searchable: false, + orderable: false, + className: 'dt-body-center', + sWidth: '1%', + render: function() { + return ""; + } + }, { + targets: 1, + searchable: false, + orderable: true, + sWidth: '1%' + }], + 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); + $(row).addClass('selected'); + } + }, + columns: (function() { + var numOfColumns = $(TABLE_ID).data('num-columns'); + var columns = []; + for (var i = 0; i < numOfColumns; i++) { + var visible = (i <= 4); + var searchable = (i > 0 && i <= 4); + columns.push({ + data: String(i), + defaultContent: '', + visible: visible, + searchable: searchable + }); + } + return columns; + })(), + fnDrawCallback: function() { + animateSpinner(this, false); + changeToViewMode(); + updateButtons(); + updateDataTableSelectAllCtrl(); + // Prevent row toggling when selecting user smart annotation link + SmartAnnotation.preventPropagation('.atwho-user-popover'); + + // Show number of selected rows near pages info + $('#repository-table_info').append(''); + $('#selected_info').html(' (' + + rowsSelected.length + + ' entries selected)'); + initRowSelection(); + initHeaderTooltip(); + }, + preDrawCallback: function() { + animateSpinner(this); + }, + stateLoadCallback: function() { + // 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 = $(TABLE_ID).data('repository-id'); + $.ajax({ + url: '/repositories/' + repositoryId + '/state_load', + data: {}, + async: false, + dataType: 'json', + type: 'POST', + success: function(json) { + myData = json.state; + } + }); + return myData; + }, + stateSaveCallback: function(settings, data) { + // Send an Ajax request to the server with the state object + var repositoryId = $(TABLE_ID).data('repository-id'); + // Save correct data + if (loadFirstTime === true) { + data = myData; + } + $.ajax({ + url: '/repositories/' + repositoryId + '/state_save', + data: { state: data }, + dataType: 'json', + type: 'POST' + }); + loadFirstTime = false; + initHeaderTooltip(); + }, + 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++) { + var visibility = false; + if (myData.columns[i]) { + visibility = myData.columns[i].visible; + } + if (typeof (visibility) === 'string') { + visibility = (visibility === 'true'); + } + TABLE.column(i).visible(visibility); + TABLE.setColumnSearchable(i, visibility); + } + oSettings._colReorder.fnOrder(myData.ColReorder); + TABLE.on('mousedown', function() { + $('#repository-columns-dropdown').removeClass('open'); + }); + initHeaderTooltip(); + initRowSelection(); + bindExportActions(); + } + }); + + // Append button to inner toolbar in table + $('div.toolbarButtons').appendTo('div.toolbar'); + $('div.toolbarButtons').show(); + + // Handle click on table cells with checkboxes + $(TABLE_ID).on('click', 'tbody td', function(e) { + if ($(e.target).is('.repository-row-selector')) { + // Skip if clicking on selector checkbox + return; + } + $(this).parent().find('.repository-row-selector').trigger('click'); + }); + + TABLE.on('column-reorder', function() { + initRowSelection(); + }); + + $('#assignRepositories, #unassignRepositories').click(function() { + animateLoading(); + }); + + // Timeout for table header scrolling + setTimeout(function() { + TABLE.columns.adjust(); + }, 10); + + return TABLE; + } + + // Enables noSearchHidden plugin + $.fn.dataTable.defaults.noSearchHidden = true; + + function bindExportActions() { + $('form#form-export').submit(function() { + var form = this; + + if (currentMode === 'viewMode') { + // Remove all hidden fields + $(form).find('input[name=row_ids\\[\\]]').remove(); + $(form).find('input[name=header_ids\\[\\]]').remove(); + + // Append visible column information + $('.active table' + TABLE_ID + ' thead tr th').each(function() { + var th = $(this); + var val; + switch ($(th).attr('id')) { + case 'checkbox': + val = -1; + break; + case 'assigned': + val = -2; + break; + case 'row-name': + val = -3; + break; + case 'added-by': + val = -4; + break; + case 'added-on': + val = -5; + break; + default: + val = th.attr('id'); + } + + if (val) { + appendInput(form, val, 'header_ids[]'); + } + }); + + // Append records + $.each(rowsSelected, function(index, rowId) { + appendInput(form, rowId, 'row_ids[]'); + }); + } + }); + } + + + function appendInput(form, val, name) { + $(form).append( + $('') + .attr('type', 'hidden') + .attr('name', name) + .val(val) + ); + } + + function initRowSelection() { + // Handle clicks on checkbox + $('.dt-body-center .repository-row-selector').change(function(e) { + if (currentMode !== 'viewMode') { + return false; } - }, { - targets: 1, - searchable: false, - orderable: true, - sWidth: '1%' - }], - rowCallback: function(row, data) { // Get row ID + var $row = $(this).closest('tr'); + var data = TABLE.row($row).data(); 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); - $(row).addClass('selected'); + // Determine whether row ID is in the list of selected row IDs + var index = $.inArray(rowId, rowsSelected); + + // If checkbox is checked and row ID is not in list of selected row IDs + if (this.checked && index === -1) { + rowsSelected.push(rowId); + // Otherwise, if checkbox is not checked and row ID is in list of selected row IDs + } else if (!this.checked && index !== -1) { + rowsSelected.splice(index, 1); } - }, - columns: (function() { - var numOfColumns = $('#repository-table').data('num-columns'); - var columns = []; - for (var i = 0; i < numOfColumns; i++) { - var visible = (i <= 4); - var searchable = (i > 0 && i <= 4); - columns.push({ - data: String(i), - defaultContent: '', - visible: visible, - searchable: searchable - }); + + if (this.checked) { + $row.addClass('selected'); + } else { + $row.removeClass('selected'); } - return columns; - })(), - fnDrawCallback: function() { - animateSpinner(this, false); - changeToViewMode(); + + updateDataTableSelectAllCtrl(); + + e.stopPropagation(); updateButtons(); - updateDataTableSelectAllCtrl(table); - // Prevent row toggling when selecting user smart annotation link - SmartAnnotation.preventPropagation('.atwho-user-popover'); - - // Show number of selected rows near pages info - $('#repository-table_info').append(''); - $('#selected_info').html(' (' + - rowsSelected.length + - ' entries selected)'); - initRowSelection(); - initHeaderTooltip(); - }, - preDrawCallback: function() { - animateSpinner(this); - }, - stateLoadCallback: function() { - // 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'); - $.ajax({ - url: '/repositories/' + repositoryId + '/state_load', - data: {}, - async: false, - dataType: 'json', - type: 'POST', - success: function(json) { - myData = json.state; - } - }); - return myData; - }, - stateSaveCallback: function(settings, data) { - // Send an Ajax request to the server with the state object - var repositoryId = $('#repository-table').data('repository-id'); - // Save correct data - if (loadFirstTime === true) { - data = myData; - } - $.ajax({ - url: '/repositories/' + repositoryId + '/state_save', - data: {state: data}, - dataType: 'json', - type: 'POST' - }); - loadFirstTime = false; - initHeaderTooltip(); - }, - 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++) { - var visibility = false; - if (myData.columns[i]) { - visibility = myData.columns[i].visible; - } - if (typeof (visibility) === 'string') { - visibility = (visibility === 'true'); - } - table.column(i).visible(visibility); - table.setColumnSearchable(i, visibility); - } - oSettings._colReorder.fnOrder(myData.ColReorder); - table.on('mousedown', function() { - $('#repository-columns-dropdown').removeClass('open'); - }); - initHeaderTooltip(); - initRowSelection(); - } - }); - - // Append button to inner toolbar in table - $('div.toolbarButtons').appendTo('div.toolbar'); - $('div.toolbarButtons').show(); - - // Handle click on table cells with checkboxes - $('#repository-table').on('click', 'tbody td', function(e) { - if ($(e.target).is('.repository-row-selector')) { - // Skip if clicking on selector checkbox - return; - } - $(this).parent().find('.repository-row-selector').trigger('click'); - }); - - table.on('column-reorder', function() { - initRowSelection(); - }); - - $('#assignRepositories, #unassignRepositories').click(function() { - animateLoading(); - }); - - return table; -} - -table = dataTableInit(); - -// Timeout for table header scrolling -setTimeout(function() { - table.columns.adjust(); -}, 10); - -// Enables noSearchHidden plugin -$.fn.dataTable.defaults.noSearchHidden = true; - -$('form#form-export').submit(function() { - var form = this; - - if (currentMode === 'viewMode') { - // Remove all hidden fields - $(form).find('input[name=row_ids\\[\\]]').remove(); - $(form).find('input[name=header_ids\\[\\]]').remove(); - - // Append visible column information - $('.active table#repository-table thead tr th').each(function() { - var th = $(this); - var val; - switch ($(th).attr('id')) { - case 'checkbox': - val = -1; - break; - case 'assigned': - val = -2; - break; - case 'row-name': - val = -3; - break; - case 'added-by': - val = -4; - break; - case 'added-on': - val = -5; - break; - default: - val = th.attr('id'); - } - - if (val) { - appendInput(form, val, 'header_ids[]'); - } + // Update number of selected records info + $('#selected_info').html(' (' + rowsSelected.length + ' entries selected)'); }); - // Append records - $.each(rowsSelected, function(index, rowId) { - appendInput(form, rowId, 'row_ids[]'); + // Handle click on "Select all" control + $('.dataTables_scrollHead input[name="select_all"]').change(function(e) { + if (this.checked) { + $('.repository-row-selector:not(:checked)').trigger('click'); + } else { + $('.repository-row-selector:checked').trigger('click'); + } + // Prevent click event from propagating to parent + e.stopPropagation(); }); } -}); -function appendInput(form, val, name) { - $(form).append( - $('') - .attr('type', 'hidden') - .attr('name', name) - .val(val) - ); -} + // Updates "Select all" control in a data table + 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); -function initRowSelection() { - // Handle clicks on checkbox - $('.dt-body-center .repository-row-selector').change(function(e) { - if (currentMode !== 'viewMode') { - return false; - } - // Get row ID - var $row = $(this).closest('tr'); - var data = table.row($row).data(); - var rowId = data.DT_RowId; + // If none of the checkboxes are checked + if ($chkboxChecked.length === 0) { + chkboxSelectAll.checked = false; + if ('indeterminate' in chkboxSelectAll) { + chkboxSelectAll.indeterminate = false; + } - // Determine whether row ID is in the list of selected row IDs - var index = $.inArray(rowId, rowsSelected); + // If all of the checkboxes are checked + } else if ($chkboxChecked.length === $chkboxAll.length) { + chkboxSelectAll.checked = true; + if ('indeterminate' in chkboxSelectAll) { + chkboxSelectAll.indeterminate = false; + } - // If checkbox is checked and row ID is not in list of selected row IDs - if (this.checked && index === -1) { - rowsSelected.push(rowId); - // Otherwise, if checkbox is not checked and row ID is in list of selected row IDs - } else if (!this.checked && index !== -1) { - rowsSelected.splice(index, 1); - } - - if (this.checked) { - $row.addClass('selected'); + // If some of the checkboxes are checked } else { - $row.removeClass('selected'); + chkboxSelectAll.checked = true; + if ('indeterminate' in chkboxSelectAll) { + chkboxSelectAll.indeterminate = true; + } } + } - updateDataTableSelectAllCtrl(table); + function initHeaderTooltip() { + // Fix compatibility of fixed table header and column names modal-tooltip + $('.modal-tooltip').off(); + $('.modal-tooltip').hover(function() { + var $tooltip = $(this).find('.modal-tooltiptext'); + var offsetLeft = $tooltip.offset().left; + if ((offsetLeft + 200) > $(window).width()) { + offsetLeft -= 150; + } + var offsetTop = $tooltip.offset().top; + var width = 200; - e.stopPropagation(); + // set tooltip params in the table body + 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'); + $tooltip.css('border-radius', '6px'); + $tooltip.css('color', '#333'); + $tooltip.css('display', 'block'); + $tooltip.css('left', offsetLeft + 'px'); + $tooltip.css('padding', '5px'); + $tooltip.css('position', 'absolute'); + $tooltip.css('text-align', 'center'); + $tooltip.css('top', offsetTop + 'px'); + $tooltip.css('visibility', 'visible'); + $tooltip.css('width', width + 'px'); + $tooltip.css('word-wrap', 'break-word'); + $tooltip.css('z-index', '4'); + $(this).data('dropdown-tooltip', $tooltip); + }, function() { + $(this).append($(this).data('dropdown-tooltip')); + $(this).data('dropdown-tooltip').removeAttr('style'); + }); + } + + global.onClickAddRecord = function() { + changeToEditMode(); updateButtons(); - // Update number of selected records info - $('#selected_info').html(' (' + rowsSelected.length + ' entries selected)'); - }); - // Handle click on "Select all" control - $('.dataTables_scrollHead input[name="select_all"]').change(function(e) { - if (this.checked) { - $('.repository-row-selector:not(:checked)').trigger('click'); - } else { - $('.repository-row-selector:checked').trigger('click'); + saveAction = 'create'; + var tr = document.createElement('tr'); + if (TABLE.column(1).visible() === false) { + TABLE.column(1).visible(true); } - // Prevent click event from propagating to parent - e.stopPropagation(); - }); -} - -// Updates "Select all" control in a data table -function updateDataTableSelectAllCtrl(table) { - 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); - - // If none of the checkboxes are checked - if ($chkboxChecked.length === 0) { - chkboxSelectAll.checked = false; - if ('indeterminate' in chkboxSelectAll) { - chkboxSelectAll.indeterminate = false; - } - - // If all of the checkboxes are checked - } else if ($chkboxChecked.length === $chkboxAll.length) { - chkboxSelectAll.checked = true; - if ('indeterminate' in chkboxSelectAll) { - chkboxSelectAll.indeterminate = false; - } - - // If some of the checkboxes are checked - } else { - chkboxSelectAll.checked = true; - if ('indeterminate' in chkboxSelectAll) { - chkboxSelectAll.indeterminate = true; - } - } -} - -function initHeaderTooltip() { - // Fix compatibility of fixed table header and column names modal-tooltip - $('.modal-tooltip').off(); - $('.modal-tooltip').hover(function() { - var $tooltip = $(this).find('.modal-tooltiptext'); - var offsetLeft = $tooltip.offset().left; - if ((offsetLeft + 200) > $(window).width()) { - offsetLeft -= 150; - } - var offsetTop = $tooltip.offset().top; - 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; - } - $('body').append($tooltip); - $tooltip.css('background-color', '#d2d2d2'); - $tooltip.css('border-radius', '6px'); - $tooltip.css('color', '#333'); - $tooltip.css('display', 'block'); - $tooltip.css('left', offsetLeft + 'px'); - $tooltip.css('padding', '5px'); - $tooltip.css('position', 'absolute'); - $tooltip.css('text-align', 'center'); - $tooltip.css('top', offsetTop + 'px'); - $tooltip.css('visibility', 'visible'); - $tooltip.css('width', width + 'px'); - $tooltip.css('word-wrap', 'break-word'); - $tooltip.css('z-index', '4'); - $(this).data('dropdown-tooltip', $tooltip); - }, function() { - $(this).append($(this).data('dropdown-tooltip')); - $(this).data('dropdown-tooltip').removeAttr('style'); - }); -} - -function onClickAddRecord() { - changeToEditMode(); - updateButtons(); - - saveAction = 'create'; - var tr = document.createElement('tr'); - if (table.column(1).visible() === false) { - table.column(1).visible(true); - } - $('table#repository-table thead tr').children('th').each(function() { - var th = $(this); - var td; - var input; - if ($(th).attr('id') === 'checkbox') { - td = createTdElement(''); - $(td).html($('#saveRecord').clone()); - tr.appendChild(td); - } else if ($(th).attr('id') === 'assigned') { - td = createTdElement(''); - $(td).html($('#cancelSave').clone()); - tr.appendChild(td); - } else if ($(th).attr('id') === 'row-name') { - input = changeToInputField('repository_row', 'name', ''); - tr.appendChild(createTdElement(input)); - } else if ($(th).hasClass('repository-column')) { - input = changeToInputField('repository_cell', th.attr('id'), ''); - tr.appendChild(createTdElement(input)); - } else { - // Column we don't care for, just add empty td - tr.appendChild(createTdElement('')); - } - }); - $('table#repository-table').prepend(tr); - selectedRecord = tr; - - // initialize smart annotation - _.each($('[data-object="repository_cell"]'), function(el) { - if (_.isUndefined($(el).data('atwho'))) { - SmartAnnotation.init(el); - } - }); - // Adjust columns width in table header - adjustTableHeader(); -} - -(function onClickToggleAssignedRecords() { - $('.repository-assign-group > .btn').click(function() { - $('.btn-group > .btn').removeClass('active btn-primary'); - $('.btn-group > .btn').addClass('btn-default'); - $(this).addClass('active btn-primary'); - }); - - $('#assigned-repo-records').on('click', function() { - viewAssigned = 'assigned'; - table.ajax.reload(function() { - initRowSelection(); - }, false); - }); - $('#all-repo-records').on('click', function() { - viewAssigned = 'all'; - table.ajax.reload(function() { - initRowSelection(); - }, false); - }); -})(); - -function onClickAssignRecords() { - animateSpinner(); - $.ajax({ - url: $('#assignRepositoryRecords').data('assign-url'), - type: 'POST', - dataType: 'json', - data: {selected_rows: rowsSelected}, - success: function(data) { - HelperModule.flashAlertMsg(data.flash, 'success'); - onClickCancel(); - clearRowSelection(); - }, - error: function(data) { - HelperModule.flashAlertMsg(data.responseJSON.flash, 'danger'); - onClickCancel(); - clearRowSelection(); - } - }); -} - -function onClickUnassignRecords() { - animateSpinner(); - $.ajax({ - url: $('#unassignRepositoryRecords').data('unassign-url'), - type: 'POST', - dataType: 'json', - data: {selected_rows: rowsSelected}, - success: function(data) { - HelperModule.flashAlertMsg(data.flash, 'success'); - onClickCancel(); - clearRowSelection(); - }, - error: function(data) { - HelperModule.flashAlertMsg(data.responseJSON.flash, 'danger'); - onClickCancel(); - clearRowSelection(); - } - }); -} - -function onClickDeleteRecord() { - animateSpinner(); - $.ajax({ - url: $('table#repository-table').data('delete-record'), - type: 'POST', - dataType: 'json', - data: {selected_rows: rowsSelected}, - success: function(data) { - HelperModule.flashAlertMsg(data.flash, data.color); - rowsSelected = []; - onClickCancel(); - }, - error: function(e) { - if (e.status === 403) { - HelperModule.flashAlertMsg( - I18n.t('repositories.js.permission_error'), e.responseJSON.style - ); + $('table' + TABLE_ID + ' thead tr').children('th').each(function() { + var th = $(this); + var td; + var input; + if ($(th).attr('id') === 'checkbox') { + td = createTdElement(''); + $(td).html($('#saveRecord').clone()); + tr.appendChild(td); + } else if ($(th).attr('id') === 'assigned') { + td = createTdElement(''); + $(td).html($('#cancelSave').clone()); + tr.appendChild(td); + } else if ($(th).attr('id') === 'row-name') { + input = changeToInputField('repository_row', 'name', ''); + tr.appendChild(createTdElement(input)); + } else if ($(th).hasClass('repository-column')) { + input = changeToInputField('repository_cell', th.attr('id'), ''); + tr.appendChild(createTdElement(input)); + } else { + // Column we don't care for, just add empty td + tr.appendChild(createTdElement('')); } - } - }); -} + }); + $('table' + TABLE_ID).prepend(tr); + selectedRecord = tr; -// Edit record -function onClickEdit() { - if (rowsSelected.length !== 1) { - return; + // initialize smart annotation + _.each($('[data-object="repository_cell"]'), function(el) { + if (_.isUndefined($(el).data('atwho'))) { + SmartAnnotation.init(el); + } + }); + // Adjust columns width in table header + adjustTableHeader(); } - var row = table.row('#' + rowsSelected[0]); - var node = row.node(); - var rowData = row.data(); + global.onClickToggleAssignedRecords = function() { + $('.repository-assign-group > .btn').click(function() { + $('.btn-group > .btn').removeClass('active btn-primary'); + $('.btn-group > .btn').addClass('btn-default'); + $(this).addClass('active btn-primary'); + }); - $(node).find('td input').trigger('click'); - selectedRecord = node; - - clearAllErrors(); - changeToEditMode(); - updateButtons(); - saveAction = 'update'; - - $.ajax({ - url: rowData.recordEditUrl, - type: 'GET', - dataType: 'json', - success: function(data) { - 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()); - $(node).children('td').eq(1).html($('#cancelSave').clone()); - - // Record name column - var colIndex = getColumnIndex('#row-name'); - if (colIndex) { - $(node).children('td').eq(colIndex) - .html(changeToInputField('repository_row', 'name', - data.repository_row.name)); - } - - // Take care of custom cells - 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(); - if ($(colHeader).hasClass('repository-column')) { - // Check if cell on this record exists - var cell = cells[$(colHeader).attr('id')]; - if (cell) { - td.html(changeToInputField('repository_cell', - $(colHeader).attr('id'), - cell.value)); - } else { - td.html(changeToInputField('repository_cell', - $(colHeader).attr('id'), '')); - } - } - }); - - // initialize smart annotation - _.each($('[data-object="repository_cell"]'), function(el) { - if (_.isUndefined($(el).data('atwho'))) { - SmartAnnotation.init(el); - } - }); - // Adjust columns width in table header - adjustTableHeader(); - updateButtons(); - }, - error: function(e) { - if (e.status === 403) { - HelperModule.flashAlertMsg( - I18n.t('repositories.js.permission_error'), e.responseJSON.style - ); - changeToViewMode(); - updateButtons(); - } - } - }); -} - -// Save record -function onClickSave() { - var node; - var rowData; - if (saveAction === 'update') { - var row = table.row(selectedRecord); - node = row.node(); - rowData = row.data(); - } else if (saveAction === 'create') { - node = selectedRecord; - } - // First fetch all the data in input fields - var data = { - request_url: $('#repository-table').data('current-uri'), - repository_row_id: $(selectedRecord).attr('id'), - repository_row: {}, - repository_cells: {} + $('#assigned-repo-records').on('click', function() { + viewAssigned = 'assigned'; + TABLE.ajax.reload(function() { + initRowSelection(); + }, false); + }); + $('#all-repo-records').on('click', function() { + viewAssigned = 'all'; + TABLE.ajax.reload(function() { + initRowSelection(); + }, false); + }); }; - // Direct record attributes - // Record name - data.repository_row.name = $('td input[data-object = repository_row]').val(); - - // Custom cells - $(node).find('td input[data-object = repository_cell]').each(function() { - // Send data only and only if cell is not empty - if ($(this).val().trim()) { - data.repository_cells[$(this).attr('name')] = $(this).val(); - } - }); - - var url; - var type; - if (saveAction === 'update') { - url = rowData.recordUpdateUrl; - type = 'PUT'; - } else { - type = 'POST'; - url = $('table#repository-table').data('create-record'); + global.onClickAssignRecords = function() { + animateSpinner(); + $.ajax({ + url: $('#assignRepositoryRecords').data('assign-url'), + type: 'POST', + dataType: 'json', + data: { selected_rows: rowsSelected }, + success: function(data) { + HelperModule.flashAlertMsg(data.flash, 'success'); + onClickCancel(); + clearRowSelection(); + }, + error: function(data) { + HelperModule.flashAlertMsg(data.responseJSON.flash, 'danger'); + onClickCancel(); + clearRowSelection(); + } + }); } - $.ajax({ - url: url, - type: type, - dataType: 'json', - data: data, - success: function(data) { - HelperModule.flashAlertMsg(data.flash, 'success'); - SmartAnnotation.closePopup(); - onClickCancel(); - }, - error: function(e) { - SmartAnnotation.closePopup(); - var data = e.responseJSON; - clearAllErrors(); - if (e.status === 404) { - HelperModule.flashAlertMsg( - I18n.t('repositories.js.not_found_error'), 'danger' - ); - changeToViewMode(); - updateButtons(); - } else if (e.status === 403) { - HelperModule.flashAlertMsg( - I18n.t('repositories.js.permission_error'), 'danger' - ); - changeToViewMode(); - updateButtons(); - } else if (e.status === 400) { - if (data.default_fields) { - var defaultFields = data.default_fields; + global.onClickUnassignRecords = function() { + animateSpinner(); + $.ajax({ + url: $('#unassignRepositoryRecords').data('unassign-url'), + type: 'POST', + dataType: 'json', + data: {selected_rows: rowsSelected}, + success: function(data) { + HelperModule.flashAlertMsg(data.flash, 'success'); + onClickCancel(); + clearRowSelection(); + }, + error: function(data) { + HelperModule.flashAlertMsg(data.responseJSON.flash, 'danger'); + onClickCancel(); + clearRowSelection(); + } + }); + } - // Validate record name - if (defaultFields.name) { - var input = $(selectedRecord).find('input[name = name]'); + global.onClickDeleteRecord = function() { + animateSpinner(); + $.ajax({ + url: $('table' + TABLE_ID).data('delete-record'), + type: 'POST', + dataType: 'json', + data: {selected_rows: rowsSelected}, + success: function(data) { + HelperModule.flashAlertMsg(data.flash, data.color); + rowsSelected = []; + onClickCancel(); + }, + error: function(e) { + if (e.status === 403) { + HelperModule.flashAlertMsg( + I18n.t('repositories.js.permission_error'), e.responseJSON.style + ); + } + } + }); + } - if (input) { - input.closest('.form-group').addClass('has-error'); - input.parent().append("" + - defaultFields.name + '
    '); - } - } + // Edit record + global.onClickEdit = function() { + if (rowsSelected.length !== 1) { + return; + } + + var row = TABLE.row('#' + rowsSelected[0]); + var node = row.node(); + var rowData = row.data(); + + $(node).find('td input').trigger('click'); + selectedRecord = node; + + clearAllErrors(); + changeToEditMode(); + updateButtons(); + saveAction = 'update'; + + $.ajax({ + url: rowData.recordEditUrl, + type: 'GET', + dataType: 'json', + success: function(data) { + 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()); + $(node).children('td').eq(1).html($('#cancelSave').clone()); + + // Record name column + var colIndex = getColumnIndex('#row-name'); + if (colIndex) { + $(node).children('td').eq(colIndex) + .html(changeToInputField('repository_row', 'name', + data.repository_row.name)); } - // Validate custom cells - $.each(data.repository_cells || [], function(key, val) { - $.each(val, function(key, val) { - var input = $(selectedRecord).find('input[name=' + key + ']'); - if (input) { - input.closest('.form-group').addClass('has-error'); - input.parent().append("" + - val.data[0] + '
    '); + // Take care of custom cells + 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(); + if ($(colHeader).hasClass('repository-column')) { + // Check if cell on this record exists + var cell = cells[$(colHeader).attr('id')]; + if (cell) { + td.html(changeToInputField('repository_cell', + $(colHeader).attr('id'), + cell.value)); + } else { + td.html(changeToInputField('repository_cell', + $(colHeader).attr('id'), '')); } - }); + } }); - } - } - }); -} -// Enable/disable edit button -function updateButtons() { - if (currentMode === 'viewMode') { - $('#addRepositoryRecord').removeClass('disabled'); - $('#addRepositoryRecord').prop('disabled', false); - $('#addNewColumn').removeClass('disabled'); - $('#addNewColumn').prop('disabled', false); - $('#repository-columns-dropdown') - .find('.dropdown-toggle') - .prop('disabled', false); - $('th').removeClass('disable-click'); - $('.repository-row-selector').removeClass('disabled'); - $('.repository-row-selector').prop('disabled', false); - if (rowsSelected.length === 0) { - $('#editRepositoryRecord').prop('disabled', true); + // initialize smart annotation + _.each($('[data-object="repository_cell"]'), function(el) { + if (_.isUndefined($(el).data('atwho'))) { + SmartAnnotation.init(el); + } + }); + // Adjust columns width in table header + adjustTableHeader(); + updateButtons(); + }, + error: function(e) { + if (e.status === 403) { + HelperModule.flashAlertMsg( + I18n.t('repositories.js.permission_error'), e.responseJSON.style + ); + changeToViewMode(); + updateButtons(); + } + } + }); + } + + // Save record + global.onClickSave = function() { + var node; + var rowData; + if (saveAction === 'update') { + var row = TABLE.row(selectedRecord); + node = row.node(); + rowData = row.data(); + } else if (saveAction === 'create') { + node = selectedRecord; + } + // First fetch all the data in input fields + var data = { + request_url: $(TABLE_ID).data('current-uri'), + repository_row_id: $(selectedRecord).attr('id'), + repository_row: {}, + repository_cells: {} + }; + + // Direct record attributes + // Record name + data.repository_row.name = $('td input[data-object = repository_row]').val(); + + // Custom cells + $(node).find('td input[data-object = repository_cell]').each(function() { + // Send data only and only if cell is not empty + if ($(this).val().trim()) { + data.repository_cells[$(this).attr('name')] = $(this).val(); + } + }); + + var url; + var type; + if (saveAction === 'update') { + url = rowData.recordUpdateUrl; + type = 'PUT'; + } else { + type = 'POST'; + url = $('table' + TABLE_ID).data('create-record'); + } + $.ajax({ + url: url, + type: type, + dataType: 'json', + data: data, + success: function(data) { + HelperModule.flashAlertMsg(data.flash, 'success'); + SmartAnnotation.closePopup(); + onClickCancel(); + }, + error: function(e) { + SmartAnnotation.closePopup(); + var data = e.responseJSON; + clearAllErrors(); + + if (e.status === 404) { + HelperModule.flashAlertMsg( + I18n.t('repositories.js.not_found_error'), 'danger' + ); + changeToViewMode(); + updateButtons(); + } else if (e.status === 403) { + HelperModule.flashAlertMsg( + I18n.t('repositories.js.permission_error'), 'danger' + ); + changeToViewMode(); + updateButtons(); + } else if (e.status === 400) { + if (data.default_fields) { + var defaultFields = data.default_fields; + + // Validate record name + if (defaultFields.name) { + var input = $(selectedRecord).find('input[name = name]'); + + if (input) { + input.closest('.form-group').addClass('has-error'); + input.parent().append("" + + defaultFields.name + '
    '); + } + } + } + + // Validate custom cells + $.each(data.repository_cells || [], function(key, val) { + $.each(val, function(key, val) { + var input = $(selectedRecord).find('input[name=' + key + ']'); + if (input) { + input.closest('.form-group').addClass('has-error'); + input.parent().append("" + + val.data[0] + '
    '); + } + }); + }); + } + } + }); + } + + // Enable/disable edit button + function updateButtons() { + if (currentMode === 'viewMode') { + $('#addRepositoryRecord').removeClass('disabled'); + $('#addRepositoryRecord').prop('disabled', false); + $('#addNewColumn').removeClass('disabled'); + $('#addNewColumn').prop('disabled', false); + $('#repository-columns-dropdown') + .find('.dropdown-toggle') + .prop('disabled', false); + $('th').removeClass('disable-click'); + $('.repository-row-selector').removeClass('disabled'); + $('.repository-row-selector').prop('disabled', false); + if (rowsSelected.length === 0) { + $('#editRepositoryRecord').prop('disabled', true); + $('#editRepositoryRecord').addClass('disabled'); + $('#deleteRepositoryRecordsButton').prop('disabled', true); + $('#deleteRepositoryRecordsButton').addClass('disabled'); + $('#exportRepositoriesButton').addClass('disabled'); + $('#exportRepositoriesButton').prop('disabled', true); + $('#exportRepositoriesButton').off('click'); + $('#export-repositories').off('click'); + $('#assignRepositoryRecords').addClass('disabled'); + $('#assignRepositoryRecords').prop('disabled', true); + $('#unassignRepositoryRecords').addClass('disabled'); + $('#unassignRepositoryRecords').prop('disabled', true); + } else { + if (rowsSelected.length === 1) { + $('#editRepositoryRecord').prop('disabled', false); + $('#editRepositoryRecord').removeClass('disabled'); + } else { + $('#editRepositoryRecord').prop('disabled', true); + $('#editRepositoryRecord').addClass('disabled'); + } + $('#deleteRepositoryRecordsButton').prop('disabled', false); + $('#deleteRepositoryRecordsButton').removeClass('disabled'); + $('#exportRepositoriesButton').removeClass('disabled'); + $('#exportRepositoriesButton').prop('disabled', false); + $('#exportRepositoriesButton').on('click', function() { + $('#exportRepositoryModal').modal('show'); + }); + $('#export-repositories').on('click', function() { + animateSpinner(null, true); + $('#form-export').submit(); + }); + $('#assignRepositoryRecords').removeClass('disabled'); + $('#assignRepositoryRecords').prop('disabled', false); + $('#unassignRepositoryRecords').removeClass('disabled'); + $('#unassignRepositoryRecords').prop('disabled', false); + } + } else if (currentMode === 'editMode') { + $('#addRepositoryRecord').addClass('disabled'); + $('#addRepositoryRecord').prop('disabled', true); $('#editRepositoryRecord').addClass('disabled'); - $('#deleteRepositoryRecordsButton').prop('disabled', true); + $('#editRepositoryRecord').prop('disabled', true); + $('#addNewColumn').addClass('disabled'); + $('#addNewColumn').prop('disabled', true); $('#deleteRepositoryRecordsButton').addClass('disabled'); + $('#deleteRepositoryRecordsButton').prop('disabled', true); $('#exportRepositoriesButton').addClass('disabled'); - $('#exportRepositoriesButton').prop('disabled', true); $('#exportRepositoriesButton').off('click'); $('#export-repositories').off('click'); $('#assignRepositoryRecords').addClass('disabled'); $('#assignRepositoryRecords').prop('disabled', true); $('#unassignRepositoryRecords').addClass('disabled'); $('#unassignRepositoryRecords').prop('disabled', true); - } else { - if (rowsSelected.length === 1) { - $('#editRepositoryRecord').prop('disabled', false); - $('#editRepositoryRecord').removeClass('disabled'); - } else { - $('#editRepositoryRecord').prop('disabled', true); - $('#editRepositoryRecord').addClass('disabled'); - } - $('#deleteRepositoryRecordsButton').prop('disabled', false); - $('#deleteRepositoryRecordsButton').removeClass('disabled'); - $('#exportRepositoriesButton').removeClass('disabled'); - $('#exportRepositoriesButton').prop('disabled', false); - $('#exportRepositoriesButton').on('click', function() { - $('#exportRepositoryModal').modal('show'); - }); - $('#export-repositories').on('click', function() { - animateSpinner(null, true); - $('#form-export').submit(); - }); - $('#assignRepositoryRecords').removeClass('disabled'); - $('#assignRepositoryRecords').prop('disabled', false); - $('#unassignRepositoryRecords').removeClass('disabled'); - $('#unassignRepositoryRecords').prop('disabled', false); + $('#repository-columns-dropdown').find('.dropdown-toggle') + .prop('disabled', true); + $('th').addClass('disable-click'); + $('.repository-row-selector').addClass('disabled'); + $('.repository-row-selector').prop('disabled', true); } - } else if (currentMode === 'editMode') { - $('#addRepositoryRecord').addClass('disabled'); - $('#addRepositoryRecord').prop('disabled', true); - $('#editRepositoryRecord').addClass('disabled'); - $('#editRepositoryRecord').prop('disabled', true); - $('#addNewColumn').addClass('disabled'); - $('#addNewColumn').prop('disabled', true); - $('#deleteRepositoryRecordsButton').addClass('disabled'); - $('#deleteRepositoryRecordsButton').prop('disabled', true); - $('#exportRepositoriesButton').addClass('disabled'); - $('#exportRepositoriesButton').off('click'); - $('#export-repositories').off('click'); - $('#assignRepositoryRecords').addClass('disabled'); - $('#assignRepositoryRecords').prop('disabled', true); - $('#unassignRepositoryRecords').addClass('disabled'); - $('#unassignRepositoryRecords').prop('disabled', true); - $('#repository-columns-dropdown').find('.dropdown-toggle') - .prop('disabled', true); - $('th').addClass('disable-click'); - $('.repository-row-selector').addClass('disabled'); - $('.repository-row-selector').prop('disabled', true); } -} -// Clear all has-error tags -function clearAllErrors() { - // Remove any validation errors - $(selectedRecord).find('.has-error').each(function() { - $(this).removeClass('has-error'); - $(this).find('span').remove(); - }); - // Remove any alerts - $('#alert-container').find('div').remove(); -} - -function clearRowSelection() { - $('.dt-body-center .repository-row-selector').prop('checked', false); - $('.dt-body-center .repository-row-selector').closest('tr') - .removeClass('selected'); - rowsSelected = []; -} - -// Restore previous table -function onClickCancel() { - if ($('#assigned').text().length === 0) { - table.column(1).visible(false); - } - table.ajax.reload(function() { - initRowSelection(); - }, false); - changeToViewMode(); - updateButtons(); - SmartAnnotation.closePopup(); - animateSpinner(null, false); -} - -// Handle enter key -$(document).off('keypress').keypress(function(event) { - var keycode = (event.keyCode ? event.keyCode : event.which); - if (currentMode === 'editMode' && keycode === '13') { - $('#saveRecord').click(); - return false; - } -}); - -// Helper functions -function getColumnIndex(id) { - if (id < 0) { - return false; - } - return table.column(id).index('visible'); -} - -// Takes object and surrounds it with input -function changeToInputField(object, name, value) { - return "
    "; -} - -// Return td element with content -function createTdElement(content) { - var td = document.createElement('td'); - td.innerHTML = content; - return td; -} - -// Adjust columns width in table header -function adjustTableHeader() { - table.columns.adjust(); - $('.dropdown-menu').parent() - .on('shown.bs.dropdown hidden.bs.dropdown', function() { - table.columns.adjust(); + // Clear all has-error tags + function clearAllErrors() { + // Remove any validation errors + $(selectedRecord).find('.has-error').each(function() { + $(this).removeClass('has-error'); + $(this).find('span').remove(); }); -} + // Remove any alerts + $('#alert-container').find('div').remove(); + } -function changeToViewMode() { - currentMode = 'viewMode'; - // Table specific stuff - table.button(0).enable(true); -} + function clearRowSelection() { + $('.dt-body-center .repository-row-selector').prop('checked', false); + $('.dt-body-center .repository-row-selector').closest('tr') + .removeClass('selected'); + rowsSelected = []; + } -function changeToEditMode() { - currentMode = 'editMode'; - // Table specific stuff - table.button(0).enable(false); - initHeaderTooltip(); -} + // Restore previous table + global.onClickCancel = function() { + if ($('#assigned').text().length === 0) { + TABLE.column(1).visible(false); + } + TABLE.ajax.reload(function() { + initRowSelection(); + }, false); + changeToViewMode(); + updateButtons(); + SmartAnnotation.closePopup(); + animateSpinner(null, false); + } -/* - * Repository columns dropdown - */ -(function(table) { - 'use strict'; + // Handle enter key + $(document).off('keypress').keypress(function(event) { + var keycode = (event.keyCode ? event.keyCode : event.which); + if (currentMode === 'editMode' && keycode === '13') { + $('#saveRecord').click(); + return false; + } + }); - var dropdown = $('#repository-columns-dropdown'); - var dropdownList = $('#repository-columns-list'); + // Helper functions + function getColumnIndex(id) { + if (id < 0) { + return false; + } + return TABLE.column(id).index('visible'); + } + + // Takes object and surrounds it with input + function changeToInputField(object, name, value) { + return "
    "; + } + + // Return td element with content + function createTdElement(content) { + var td = document.createElement('td'); + td.innerHTML = content; + return td; + } + + // Adjust columns width in table header + function adjustTableHeader() { + TABLE.columns.adjust(); + $('.dropdown-menu').parent() + .on('shown.bs.dropdown hidden.bs.dropdown', function() { + TABLE.columns.adjust(); + }); + } + + function changeToViewMode() { + currentMode = 'viewMode'; + // Table specific stuff + TABLE.button(0).enable(true); + } + + function changeToEditMode() { + currentMode = 'editMode'; + // Table specific stuff + TABLE.button(0).enable(false); + initHeaderTooltip(); + } + + /* + * Repository columns dropdown + */ + + var dropdown, dropdownList; function createNewColumn() { // Make an Ajax request to repository_columns_controller @@ -893,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( ' 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]'); @@ -994,19 +997,19 @@ function changeToEditMode() { 'form-control" style="display: none;" />' + '' + '' + + 'title="' + $(TABLE_ID).data('save-text') + '">' + '' + '' + + $(TABLE_ID).data('columns-visibility-text') + '">' + ' ' + '' + '' + '
    '; dropdownList.append(html); @@ -1038,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(); }); @@ -1070,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); @@ -1107,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(); @@ -1291,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 @@ -1344,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 ' -
    +
    <%= render partial: "repositories/repository_table", locals: { repository: @repository, @@ -40,3 +41,7 @@ } %>
    + +<%= stylesheet_link_tag 'datatables' %> +<%= javascript_include_tag 'repositories/repository_datatable' %> +<%= javascript_include_tag 'repositories/my_module_repository' %> diff --git a/app/views/repositories/_repository_table.html.erb b/app/views/repositories/_repository_table.html.erb index d53c963f1..25592760a 100644 --- a/app/views/repositories/_repository_table.html.erb +++ b/app/views/repositories/_repository_table.html.erb @@ -1,5 +1,5 @@
    -
    - -<%= stylesheet_link_tag 'datatables' %> -<%= javascript_include_tag('repositories/repository_datatable') %> diff --git a/app/views/repositories/index.html.erb b/app/views/repositories/index.html.erb index 83fdb4bb8..6bf302f5d 100644 --- a/app/views/repositories/index.html.erb +++ b/app/views/repositories/index.html.erb @@ -10,6 +10,7 @@
  • <%= truncate(repo.name, length: Constants::NAME_TRUNCATION_LENGTH) %> @@ -53,4 +54,6 @@
  • <% end %> +<%= stylesheet_link_tag 'datatables' %> +<%= javascript_include_tag 'repositories/repository_datatable' %> <%= javascript_include_tag "repositories/index", "data-turbolinks-track" => true %> diff --git a/app/views/shared/_secondary_navigation.html.erb b/app/views/shared/_secondary_navigation.html.erb index fc5db56e2..aac6a0d50 100644 --- a/app/views/shared/_secondary_navigation.html.erb +++ b/app/views/shared/_secondary_navigation.html.erb @@ -203,7 +203,10 @@
    diff --git a/app/views/samples/_parse_samples_modal.html.erb b/app/views/samples/_parse_samples_modal.html.erb index 31a946b88..613c3d444 100644 --- a/app/views/samples/_parse_samples_modal.html.erb +++ b/app/views/samples/_parse_samples_modal.html.erb @@ -25,29 +25,31 @@ include_blank: t('teams.parse_sheet.do_not_include_column'), hide_label: true) %>
    - <% if th.length > Constants::NAME_TRUNCATION_LENGTH_DROPDOWN %> - + <% if th.nil? %> + <%= t('samples.modal_import.no_header_name') %> <% else %> - <%= th %> + <% if th.length > Constants::NAME_TRUNCATION_LENGTH_DROPDOWN %> + + <% else %> + <%= th %> + <% end %> <% end %> <% end %> - <% @rows.each do |row| %> - + + +

    <%= t('teams.parse_sheet.example_value') %>

    + + <% @columns.each do |td| %> -

    <%= t('teams.parse_sheet.example_value') %>

    + <%= td %> - <% row.each do |td| %> - - <%= td[1] %> - - <% end %> - - <% end %> + <% end %> + diff --git a/config/locales/en.yml b/config/locales/en.yml index fa31517ed..af6c491fe 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -911,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." @@ -1009,6 +1010,7 @@ en: 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." + no_header_name: 'No column name' upload: "Upload file" modal_delete: title: "Delete samples" From 6c33b46f6ae23eb7599d07208eb42dd42e52be3e Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Thu, 20 Jul 2017 11:44:10 +0200 Subject: [PATCH 17/27] Improve import of repository records plus tsv file import fixes [SCI-1487] --- Gemfile | 1 + Gemfile.lock | 3 + .../repositories/import/records_importer.js | 1 + app/assets/javascripts/repositories/index.js | 2 +- app/models/repository.rb | 101 ++++++++++-------- app/models/repository_cell.rb | 7 +- app/models/team.rb | 14 ++- .../_parse_records_modal.html.erb | 2 +- config/locales/en.yml | 4 +- 9 files changed, 79 insertions(+), 56 deletions(-) diff --git a/Gemfile b/Gemfile index 75fadeeee..0a074d9f9 100644 --- a/Gemfile +++ b/Gemfile @@ -59,6 +59,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', '~> 4.3' # File attachment, image attachment library gem 'aws-sdk', '~> 2.2.8' diff --git a/Gemfile.lock b/Gemfile.lock index eb70da5f8..aeeb3b107 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,6 +37,8 @@ GEM activemodel (= 4.2.5) activesupport (= 4.2.5) arel (~> 6.0) + activerecord-import (0.19.0) + activerecord (>= 3.2) activesupport (4.2.5) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) @@ -338,6 +340,7 @@ PLATFORMS ruby DEPENDENCIES + activerecord-import ajax-datatables-rails (~> 0.3.1) aspector auto_strip_attributes (~> 2.1) diff --git a/app/assets/javascripts/repositories/import/records_importer.js b/app/assets/javascripts/repositories/import/records_importer.js index 8ccbab01e..9f4ad2a77 100644 --- a/app/assets/javascripts/repositories/import/records_importer.js +++ b/app/assets/javascripts/repositories/import/records_importer.js @@ -31,6 +31,7 @@ disabledOptions = $("option[disabled='disabled']"); disabledOptions.removeAttr('disabled'); loadingRecords = true; + $('#parse-records-modal').modal('hide'); animateSpinner(); }).on('ajax:success', function(ev, data, status) { // Simply reload page to show flash and updated repository records list diff --git a/app/assets/javascripts/repositories/index.js b/app/assets/javascripts/repositories/index.js index 5b22e5486..fed0920ed 100644 --- a/app/assets/javascripts/repositories/index.js +++ b/app/assets/javascripts/repositories/index.js @@ -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(); diff --git a/app/models/repository.rb b/app/models/repository.rb index 0e6c193c3..138e6886f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -110,7 +110,7 @@ class Repository < ActiveRecord::Base # Imports records def import_records(sheet, mappings, user) errors = false - custom_fields = [] + columns = [] name_index = -1 total_nr = 0 nr_of_added = 0 @@ -118,54 +118,71 @@ class Repository < ActiveRecord::Base 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 - # Now we can iterate through record data and save stuff into db - (2..sheet.last_row).each do |i| - total_nr += 1 - cell_error = false - record_row = RepositoryRow.new(name: sheet.row(i)[name_index], - repository: self, - created_by: user, - last_modified_by: user) + # 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 - unless record_row.valid? - errors = true - next - end - 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] - } - ) - cell_error = true unless rep_column.save + # Now we can iterate through record data and save stuff into db + self.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 + + 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 cell_error - errors = true - record_row.destroy - else - nr_of_added += 1 - record_row.save - end end if errors - return { status: :error, - nr_of_added: nr_of_added, - total_nr: total_nr } + return { status: :error, nr_of_added: nr_of_added, total_nr: total_nr } end { status: :ok, nr_of_added: nr_of_added, total_nr: total_nr } end @@ -176,13 +193,11 @@ class Repository < ActiveRecord::Base 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 diff --git a/app/models/repository_cell.rb b/app/models/repository_cell.rb index e89887c94..403362622 100644 --- a/app/models/repository_cell.rb +++ b/app/models/repository_cell.rb @@ -1,11 +1,16 @@ 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 + validates :value, 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 diff --git a/app/models/team.rb b/app/models/team.rb index c3fae86c2..9238a0f03 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -37,16 +37,14 @@ class Team < ActiveRecord::Base 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 diff --git a/app/views/repositories/_parse_records_modal.html.erb b/app/views/repositories/_parse_records_modal.html.erb index c582b8bbe..ccab285b1 100644 --- a/app/views/repositories/_parse_records_modal.html.erb +++ b/app/views/repositories/_parse_records_modal.html.erb @@ -1,5 +1,5 @@