diff --git a/app/assets/javascripts/repositories/repository_datatable.js.erb b/app/assets/javascripts/repositories/repository_datatable.js.erb index 080224c7e..875f825b2 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js.erb +++ b/app/assets/javascripts/repositories/repository_datatable.js.erb @@ -570,6 +570,29 @@ var RepositoryDatatable = (function(global) { }); } + + global.onClickCopyRepositoryRecords = function() { + animateSpinner(); + $.ajax({ + url: $('table' + TABLE_ID).data('copy-records'), + type: 'POST', + dataType: 'json', + data: { selected_rows: rowsSelected }, + success: function(data) { + HelperModule.flashAlertMsg(data.flash, 'success'); + rowsSelected = []; + onClickCancel(); + }, + error: function(e) { + if (e.status === 403) { + HelperModule.flashAlertMsg( + I18n.t('repositories.js.permission_error'), e.responseJSON.style + ); + } + } + }); + } + // Edit record global.onClickEdit = function() { if (rowsSelected.length !== 1) { @@ -775,6 +798,8 @@ var RepositoryDatatable = (function(global) { $('.repository-row-selector').removeClass('disabled'); $('.repository-row-selector').prop('disabled', false); if (rowsSelected.length === 0) { + $('#copyRepositoryRecords').prop('disabled', true); + $('#copyRepositoryRecords').addClass('disabled'); $('#editRepositoryRecord').prop('disabled', true); $('#editRepositoryRecord').addClass('disabled'); $('#deleteRepositoryRecordsButton').prop('disabled', true); @@ -812,6 +837,8 @@ var RepositoryDatatable = (function(global) { } $('#deleteRepositoryRecordsButton').prop('disabled', false); $('#deleteRepositoryRecordsButton').removeClass('disabled'); + $('#copyRepositoryRecords').prop('disabled', false); + $('#copyRepositoryRecords').removeClass('disabled'); $('#assignRepositoryRecords').removeClass('disabled'); $('#assignRepositoryRecords').prop('disabled', false); $('#unassignRepositoryRecords').removeClass('disabled'); diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index cf2099807..ab1ca0551 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -5,9 +5,11 @@ class RepositoryRowsController < ApplicationController before_action :load_info_modal_vars, only: :show before_action :load_vars, only: %i(edit update) - before_action :load_repository, only: %i(create delete_records index) + before_action :load_repository, + only: %i(create delete_records index copy_records) before_action :check_create_permissions, only: :create - before_action :check_manage_permissions, only: %i(edit update delete_records) + before_action :check_manage_permissions, + only: %i(edit update delete_records copy_records) def index @draw = params[:draw].to_i @@ -283,6 +285,17 @@ class RepositoryRowsController < ApplicationController end end + def copy_records + duplicate_service = RepositoryActions::DuplicateRows.new( + current_user, @repository, params[:selected_rows] + ) + duplicate_service.call + render json: { + flash: t('repositories.copy_records_report', + number: duplicate_service.number_of_duplicated_items) + }, status: :ok + end + private def load_info_modal_vars diff --git a/app/services/repository_actions/duplicate_cell.rb b/app/services/repository_actions/duplicate_cell.rb new file mode 100644 index 000000000..c70a5d0ad --- /dev/null +++ b/app/services/repository_actions/duplicate_cell.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module RepositoryActions + class DuplicateCell + def initialize(cell, new_row, user, team) + @cell = cell + @new_row = new_row + @user = user + @team = team + end + + def call + self.send("duplicate_#{@cell.value_type.underscore}") + end + + private + + def duplicate_repository_list_value + old_value = @cell.value + RepositoryListValue.create( + old_value.attributes.merge( + id: nil, created_by: @user, last_modified_by: @user, + repository_cell_attributes: { + repository_row: @new_row, + repository_column: @cell.repository_column + } + ) + ) + end + + def duplicate_repository_text_value + old_value = @cell.value + RepositoryTextValue.create( + old_value.attributes.merge( + id: nil, created_by: @user, last_modified_by: @user, + repository_cell_attributes: { + repository_row: @new_row, + repository_column: @cell.repository_column + } + ) + ) + end + + def duplicate_repository_asset_value + old_value = @cell.value + new_asset = create_new_asset(old_value.asset) + RepositoryAssetValue.create( + old_value.attributes.merge( + id: nil, asset: new_asset, created_by: @user, last_modified_by: @user, + repository_cell_attributes: { + repository_row: @new_row, + repository_column: @cell.repository_column + } + ) + ) + end + + def duplicate_repository_date_value + old_value = @cell.value + RepositoryDateValue.create( + old_value.attributes.merge( + id: nil, created_by: @user, last_modified_by: @user, + repository_cell_attributes: { + repository_row: @new_row, + repository_column: @cell.repository_column + } + ) + ) + end + + # reuses the same code we have in copy protocols action + def create_new_asset(old_asset) + new_asset = Asset.new_empty( + old_asset.file_file_name, + old_asset.file_file_size + ) + new_asset.created_by = old_asset.created_by + new_asset.team = @team + new_asset.last_modified_by = @user + new_asset.file_processing = true if old_asset.is_image? + new_asset.file = old_asset.file + new_asset.save + + return unless new_asset.valid? + + if new_asset.is_image? + new_asset.file.reprocess!(:large) + new_asset.file.reprocess!(:medium) + end + + # Clone extracted text data if it exists + if old_asset.asset_text_datum + AssetTextDatum.create(data: new_asset.data, asset: new_asset) + end + + # Update estimated size of cloned asset + # (& file_present flag) + new_asset.update( + estimated_size: old_asset.estimated_size, + file_present: true + ) + + # Update team's space taken + @team.reload + @team.take_space(new_asset.estimated_size) + @team.save! + new_asset + end + end +end diff --git a/app/services/repository_actions/duplicate_rows.rb b/app/services/repository_actions/duplicate_rows.rb new file mode 100644 index 000000000..91a412ed3 --- /dev/null +++ b/app/services/repository_actions/duplicate_rows.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'repository_actions/duplicate_cell' + +module RepositoryActions + class DuplicateRows + attr_reader :number_of_duplicated_items + def initialize(user, repository, rows_ids = []) + @user = user + @repository = repository + @rows_to_duplicate = sanitize_rows_to_duplicate(rows_ids) + @number_of_duplicated_items = 0 + end + + def call + @rows_to_duplicate.each do |row_id| + duplicate_row(row_id) + end + end + + private + + def sanitize_rows_to_duplicate(rows_ids) + process_ids = rows_ids.map(&:to_i).uniq + @repository.repository_rows.where(id: process_ids).pluck(:id) + end + + def duplicate_row(id) + row = RepositoryRow.find_by_id(id) + new_row = RepositoryRow.new( + row.attributes.merge(new_row_attributes(row.name)) + ) + + if new_row.save + @number_of_duplicated_items += 1 + row.repository_cells.each do |cell| + duplicate_repository_cell(cell, new_row) + end + end + end + + def new_row_attributes(name) + timestamp = DateTime.now + { id: nil, + name: "#{name} (1)", + created_at: timestamp, + updated_at: timestamp } + end + + def duplicate_repository_cell(cell, new_row) + RepositoryActions::DuplicateCell.new( + cell, new_row, @user, @repository.team + ).call + end + end +end diff --git a/app/views/repositories/_repository_table.html.erb b/app/views/repositories/_repository_table.html.erb index aad7bcc3c..3505fdde6 100644 --- a/app/views/repositories/_repository_table.html.erb +++ b/app/views/repositories/_repository_table.html.erb @@ -6,6 +6,7 @@ data-num-columns="<%= 6 + repository.repository_columns.count %>" data-create-record="<%= repository_repository_rows_path(repository) %>" data-delete-record="<%= repository_delete_records_path(repository) %>" + data-copy-records="<%= repository_copy_records_path(repository) %>" data-max-dropdown-length="<%= Constants::NAME_TRUNCATION_LENGTH_DROPDOWN %>" data-save-text="<%= I18n.t('general.save') %>" data-edit-text="<%= I18n.t('general.edit') %>" diff --git a/app/views/repositories/show.html.erb b/app/views/repositories/show.html.erb index 3d224cff4..be529d437 100644 --- a/app/views/repositories/show.html.erb +++ b/app/views/repositories/show.html.erb @@ -106,12 +106,11 @@ diff --git a/config/locales/en.yml b/config/locales/en.yml index 6b3856742..24c2d796e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -938,6 +938,7 @@ en: errors_list_title: "Items were not imported because one or more errors were found:" no_repository_name: "Item name is required!" edit_record: "Edit" + copy_record: "Copy" delete_record: "Delete" save_record: "Save" cancel_save: "Cancel" @@ -989,6 +990,7 @@ en: no_records_assigned_flash: "No items were assigned to task" no_records_unassigned_flash: "No items were unassigned from task" default_column: 'Name' + copy_records_report: "%{number} item(s) successfully copied." libraries: manange_modal_column: diff --git a/config/routes.rb b/config/routes.rb index d5d486ef3..314b37022 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -461,6 +461,9 @@ Rails.application.routes.draw do to: 'repository_rows#delete_records', as: 'delete_records', defaults: { format: 'json' } + post 'copy_records', + to: 'repository_rows#copy_records', + defaults: { format: 'json' } get 'repository_columns/:id/destroy_html', to: 'repository_columns#destroy_html', as: 'columns_destroy_html' diff --git a/spec/controllers/repository_rows_controller_spec.rb b/spec/controllers/repository_rows_controller_spec.rb index feb487cb0..72dc1d42d 100644 --- a/spec/controllers/repository_rows_controller_spec.rb +++ b/spec/controllers/repository_rows_controller_spec.rb @@ -115,4 +115,12 @@ describe RepositoryRowsController, type: :controller do end end end + + describe 'POST #copy_records' do + it 'returns a success response' do + post :copy_records, params: { repository_id: repository.id, + selected_rows: [repository_row.id] } + expect(response).to have_http_status(:success) + end + end end diff --git a/spec/services/repository_actions/duplicate_rows_spec.rb b/spec/services/repository_actions/duplicate_rows_spec.rb new file mode 100644 index 000000000..2dc3b2cd0 --- /dev/null +++ b/spec/services/repository_actions/duplicate_rows_spec.rb @@ -0,0 +1,83 @@ +require 'rails_helper' + +describe RepositoryActions::DuplicateRows do + let!(:user) { create :user } + let!(:repository) { create :repository } + let!(:list_column) do + create(:repository_column, name: 'list', + repository: repository, + created_by: user, + data_type: 'RepositoryListValue') + end + let!(:text_column) do + create(:repository_column, name: 'text', + repository: repository, + created_by: user, + data_type: 'RepositoryTextValue') + end + + describe '#call' do + before do + @rows_ids = [] + + 3.times do |index| + row = create :repository_row, name: "row (#{index})", + repository: repository + create :repository_text_value, data: "text (#{index})", + repository_cell_attributes: { + repository_row: row, + repository_column: text_column + } + create :repository_list_value, + repository_list_item: create(:repository_list_item, + repository: repository, + repository_column: list_column, + data: "list item (#{index})"), + repository_cell_attributes: { + repository_row: row, + repository_column: list_column + } + @rows_ids << row.id.to_s + end + end + + it 'generates a duplicate of selected items' do + expect(repository.repository_rows.reload.size).to eq 3 + described_class.new(user, repository, @rows_ids).call + expect(repository.repository_rows.reload.size).to eq 6 + end + + it 'generates an exact duplicate of the row with custom column values' do + described_class.new(user, repository, [@rows_ids.first]).call + duplicated_row = repository.repository_rows.order('created_at ASC').last + expect(duplicated_row.name).to eq 'row (0) (1)' + duplicated_row.repository_cells.each do |cell| + if cell.value_type == 'RepositoryListValue' + expect(cell.value.data).to eq 'list item (0)' + else + expect(cell.value.data).to eq 'text (0)' + end + end + end + + it 'prevents to duplicate items that do not already belong to repository' do + new_repository = create :repository, name: 'new repo' + new_row = create :repository_row, name: 'other row', + repository: new_repository + described_class.new(user, repository, [new_row.id]).call + expect(repository.repository_rows.reload.size).to eq 3 + end + + it 'returns the number of duplicated items' do + service_obj = described_class.new(user, repository, @rows_ids) + service_obj.call + expect(service_obj.number_of_duplicated_items).to eq 3 + end + + it 'returns the number of duplicated items' do + service_obj = described_class.new(user, repository, []) + service_obj.call + expect(service_obj.number_of_duplicated_items).to eq 0 + end + end +end