diff --git a/app/controllers/importo/imports_controller.rb b/app/controllers/importo/imports_controller.rb index 1953a38..db7ce7e 100644 --- a/app/controllers/importo/imports_controller.rb +++ b/app/controllers/importo/imports_controller.rb @@ -8,6 +8,24 @@ def new @import = Import.new(kind: params[:kind], locale: I18n.locale) end + def preview + @import = Import.find(params[:id]) + if @import&.original&.attachment.present? + @original = Tempfile.new(['ActiveStorage', @import.original.filename.extension_with_delimiter]) + @original.binmode + @import.original.download { |block| @original.write(block) } + @original.flush + @original.rewind + sheet = Roo::Excelx.new(@original.path) + @sheet_data = sheet.parse(headers: true) + @check_header = @sheet_data.reject{ |h| h.keys == h.values }.map{|h| h.transform_values(&:to_s).compact_blank}.reduce({}, :merge).keys + end + if @sheet_data.nil? || @sheet_data.reject{ |h| h.keys == h.values }.blank? + Signum.error(Current.user, text: t('.no_file')) + redirect_to action: :new + end + end + def create unless import_params @import = Import.new(kind: params[:kind], locale: I18n.locale) @@ -17,14 +35,23 @@ def create end @import = Import.new(import_params.merge(locale: I18n.locale, importo_ownable: Importo.config.current_import_owner.call)) - if @import.valid? && @import.schedule! - redirect_to action: :index + if @import.valid? + @import.save! + redirect_to action: :preview, id: @import.id, kind: @import.kind else Signum.error(Current.user, text: t('.flash.error', error: @import.errors&.full_messages&.join('.'))) render :new end end + def cancel + @import = Import.find(params[:id]) + @import.original.purge if @import.concept? + Signum.error(Current.user, text: t('.flash.cancel', id: @import.id)) + # flash[:notice] = t('.flash.cancel', id: @import.id) + redirect_to action: :new, kind: @import.kind + end + def undo @import = Import.where(importo_ownable: Importo.config.current_import_owner.call).find(params[:id]) if @import.can_revert? && @import.revert @@ -34,6 +61,18 @@ def undo end end + def upload + @import = Import.find(params[:id]) + @import.checked_columns = params[:selected_items]&.reject { |element| element == "0" } + @import.confirm! if @import.can_confirm? + if @import.valid? && @import.schedule! + redirect_to action: :index + else + Signum.error(Current.user, text: t('.flash.error', id: @import.id)) + render :new + end + end + def destroy @import = Import.where(importo_ownable: Importo.config.current_import_owner.call).find(params[:id]) redirect_to(action: :index, alert: 'Not allowed') && return unless Importo.config.admin_can_destroy.call(@import) diff --git a/app/importers/concerns/importable.rb b/app/importers/concerns/importable.rb index 184da7a..21f126d 100644 --- a/app/importers/concerns/importable.rb +++ b/app/importers/concerns/importable.rb @@ -104,7 +104,7 @@ def after_save(_record, _row) # # Does the actual import # - def import! + def import!(checked_columns) raise ArgumentError, 'Invalid data structure' unless structure_valid? batch = Sidekiq::Batch.new @@ -116,7 +116,7 @@ def import! batch.jobs do column_with_delay = columns.select{|k,v| v.delay.present?} - loop_data_rows do |attributes, index| + loop_data_rows(checked_columns) do |attributes, index| if column_with_delay.present? delay = column_with_delay.map do |k, v| next unless attributes[k].present? diff --git a/app/importers/concerns/original.rb b/app/importers/concerns/original.rb index f8fb157..35513c3 100644 --- a/app/importers/concerns/original.rb +++ b/app/importers/concerns/original.rb @@ -111,7 +111,7 @@ def duplicate?(row_hash, id) duplicate(row_hash, id) end - def loop_data_rows + def loop_data_rows(checked_columns = nil) (data_start_row..spreadsheet.last_row).map do |index| row = cells_from_row(index, false) @@ -121,7 +121,10 @@ def loop_data_rows [column, value] end.to_h attributes.reject! { |k, _v| headers_added_by_import.include?(k) } - + if checked_columns.present? + selected_columns = checked_columns[:checked_columns].map{|i| col_for(i)&.first} + attributes.reject!{|k, _v| selected_columns.exclude?(k) } + end yield attributes, index end end diff --git a/app/importers/concerns/result_feedback.rb b/app/importers/concerns/result_feedback.rb index 887b59a..174e27c 100644 --- a/app/importers/concerns/result_feedback.rb +++ b/app/importers/concerns/result_feedback.rb @@ -33,7 +33,7 @@ def results_file end styles = [] attributes.map do |column, value| - export_format = columns[column]&.options.dig(:export, :format) + export_format = columns[column]&.options&.dig(:export, :format) format_code = if export_format == "number" || (export_format.nil? && value.is_a?(Numeric)) '#' elsif export_format == 'text' || (export_format.nil? && value.is_a?(String)) @@ -44,7 +44,7 @@ def results_file 'General' end config_style = {} - config_style.merge!(columns[column]&.options[:style]) unless columns[column]&.options[:style].nil? + config_style.merge!(columns[column]&.options[:style]) unless columns[column]&.options&.dig(:style).nil? config_style.merge!({ format_code: format_code, bg_color: bg_color }) styles << workbook.styles.add_style(config_style) end diff --git a/app/jobs/importo/purge_import_job.rb b/app/jobs/importo/purge_import_job.rb index 16c0605..fdd0a00 100644 --- a/app/jobs/importo/purge_import_job.rb +++ b/app/jobs/importo/purge_import_job.rb @@ -1,7 +1,9 @@ module Importo class PurgeImportJob < ApplicationJob - def perform(owner, months) + def perform(owner, months,state = nil) + imports = Import.where(importo_ownable: owner, created_at: ..months.months.ago.beginning_of_day) + imports = imports.where(state: state) if state imports.each do |import| import.original.purge diff --git a/app/models/importo/import.rb b/app/models/importo/import.rb index 6fbc887..cb4bdde 100644 --- a/app/models/importo/import.rb +++ b/app/models/importo/import.rb @@ -3,7 +3,7 @@ module Importo class Import < Importo::ApplicationRecord # include ActiveStorage::Downloading - + attr_accessor :checked_columns belongs_to :importo_ownable, polymorphic: true has_many :message_instances, as: :messagable @@ -12,7 +12,6 @@ class Import < Importo::ApplicationRecord validates :kind, presence: true validates :original, presence: true validate :content_validator - begin has_one_attached :original has_one_attached :result @@ -20,7 +19,8 @@ class Import < Importo::ApplicationRecord # Weird loading sequence error, is fixed by the lib/importo/helpers end - state_machine :state, initial: :new do + state_machine :state, initial: :concept do + state :confirmed state :importing state :scheduled state :completed @@ -35,11 +35,15 @@ class Import < Importo::ApplicationRecord after_transition any => :reverting, do: :schedule_revert event :schedule do - transition new: :scheduled + transition confirmed: :scheduled + end + + event :confirm do + transition concept: :confirmed end event :import do - transition new: :importing + transition confirmed: :importing transition scheduled: :importing transition failed: :importing end @@ -86,7 +90,7 @@ def importer private def schedule_import - ImportService.perform_later(import: self) + ImportService.perform_later(import: self, checked_columns: self.checked_columns) end def schedule_revert diff --git a/app/services/importo/import_context.rb b/app/services/importo/import_context.rb index dedb02c..98686ec 100644 --- a/app/services/importo/import_context.rb +++ b/app/services/importo/import_context.rb @@ -4,6 +4,7 @@ module Importo class ImportContext < ApplicationContext input do attribute :import, type: Import, typecaster: ->(value) { value.is_a?(Import) ? value : Import.find(value) } + attribute :checked_columns, type: Array, typecaster: ->(value){ value.is_a?(Array) ? value : [] } end end end diff --git a/app/services/importo/import_service.rb b/app/services/importo/import_service.rb index 76803f6..46e67e7 100644 --- a/app/services/importo/import_service.rb +++ b/app/services/importo/import_service.rb @@ -4,7 +4,7 @@ module Importo class ImportService < ApplicationService def perform context.import.import! - context.import.importer.import! + context.import.importer.import!(checked_columns: context.checked_columns ) rescue StandardError context.import.failure! context.fail! diff --git a/app/views/importo/imports/preview.html.slim b/app/views/importo/imports/preview.html.slim new file mode 100644 index 0000000..76dbaca --- /dev/null +++ b/app/views/importo/imports/preview.html.slim @@ -0,0 +1,36 @@ += sts.form_with(url: upload_import_path) do |f| + = sts.card :"importo_imports #{@import.kind}", title: t('.title', kind: @import&.kind.capitalize), icon: 'fad fa-file-spreadsheet' do |card| + - card.action + / = f.button 'Cancel', value: 'cancel' + = f.submit 'Upload', value: 'upload' + = link_to(t('.cancel'), cancel_import_path(@import), method: :post, class: 'button') + .px-4.sm:px-6.lg:px-8 + / .sm:flex.sm:items-center + / .sm:flex-auto + / h1.text-base.font-semibold.leading-6.text-gray-900 + / | Users + / p.mt-2.text-sm.text-gray-700 + / | A list of all the users in your account including their name, title, email and role. + / .mt-4.sm:ml-16.sm:mt-0.sm:flex-none + / button.block.rounded-md.bg-indigo-600.px-3.py-2.text-center.text-sm.font-semibold.text-white.shadow-sm.hover:bg-indigo-500.focus-visible:outline.focus-visible:outline-2.focus-visible:outline-offset-2.focus-visible:outline-indigo-600[type="button"] + / | Add user + .mt-8.flow-root + .-mx-4.-my-2.overflow-x-auto.sm:-mx-6.lg:-mx-8 + .inline-block.min-w-full.py-2.align-middle.bg-white.dark:bg-gray-900.dark:border-gray-700 + table.min-w-full.divide-y.divide-gray-20.border-2 + thead.bg-gray-50 + tr + - @sheet_data.first.keys.each_with_index do |header, index| + th.px-3.py-3.5.text-left.text-sm.font-semibold.border-2.text-gray-600[scope="col"] + = f.check_box :selected_items, { multiple: true, checked: @check_header.include?(header), disabled: @check_header.exclude?(header) }, ActionController::Base.helpers.strip_tags(header) + tr + - @sheet_data.first.keys.each_with_index do |header, index| + th.px-3.py-3.5.text-left.text-sm.font-semibold.border-2.text-gray-600[scope="col"] + / = f.check_box :active, checked: @check_header.include?(header) + = ActionController::Base.helpers.strip_tags(header) + tbody.divide-y.divide-gray-200.bg-white + - @sheet_data.reject! { |h| h.keys == h.values }&.each do |h| + tr + - h.each do |key, value| + td.whitespace-nowrap.px-3.py-4.text-sm.text-gray-500.border-2 + = value diff --git a/config/locales/en.yml b/config/locales/en.yml index 17552ed..1396623 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,7 +2,7 @@ en: helpers: submit: importo/import: - create: "Import" + create: "Preview" importo: sheet: results: @@ -14,6 +14,8 @@ en: explanation: Purpose example: Example imports: + cancel: + success: "Import canceled for id %{id}, Redirecting to new" index: title: Import results card: @@ -38,6 +40,15 @@ en: no_file: Import failed, please upload a file. error: Import failed, there were problems %{error}. success: "Import scheduled with id %{id}, you will get an email with the results." + preview: + no_file: Import failed, please upload valid file. + title: "%{kind} Import Preview" + cancel: Cancel + upload: + flash: + no_file: Import failed, please upload a file. + error: Import failed, there were problems. + success: "Import scheduled with id %{id}, you will get an email with the results." errors: structure_invalid: "The structure is invalid, these are the invalid headers: %{invalid_headers}" importers: diff --git a/config/routes.rb b/config/routes.rb index 331b3cf..eab1202 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,10 +4,13 @@ resources :imports, except: %i[new] do member do post :undo + post :upload + post :cancel end end get ':kind/new', to: 'imports#new', as: :new_import get ':kind/sample', to: 'imports#sample', as: :sample_import get ':kind/export', to: 'imports#export', as: :export + get ':kind/:id/preview', to: 'imports#preview', as: :preview root to: 'imports#index' end diff --git a/test/models/importo/import_test.rb b/test/models/importo/import_test.rb index 62af1ed..45406a1 100644 --- a/test/models/importo/import_test.rb +++ b/test/models/importo/import_test.rb @@ -58,6 +58,7 @@ class ImportTest < ActiveSupport::TestCase filename: 'simple_sheet.xlsx') assert import.save, import.errors.messages + import.confirm import.schedule assert_equal 'scheduled', import.state @@ -78,6 +79,7 @@ class ImportTest < ActiveSupport::TestCase 'atest-description']]), filename: 'simple_sheet.xlsx' ) assert import.save, import.errors.messages + import.confirm import.schedule assert_equal 'scheduled', import.state @@ -102,6 +104,7 @@ class ImportTest < ActiveSupport::TestCase import.original.attach(io: simple_sheet([%w[aid atest atest-description], %w[bid btest btest-description]]), filename: 'simple_sheet.xlsx') assert import.save + import.confirm import.schedule assert_equal 'scheduled', import.state @@ -177,6 +180,7 @@ class ImportTest < ActiveSupport::TestCase %w[aid atest atest-description]]), filename: 'simple_sheet.xlsx' ) assert import.save, import.errors.messages + import.confirm import.schedule assert_equal 'scheduled', import.state @@ -193,6 +197,7 @@ class ImportTest < ActiveSupport::TestCase %w[aid atest atest-description]]), filename: 'simple_sheet.xlsx' ) assert import.save, import.errors.messages + import.confirm import.schedule assert_equal 'scheduled', import.state @@ -209,6 +214,7 @@ class ImportTest < ActiveSupport::TestCase %w[aid atest atest-description]]), filename: 'simple_sheet.xlsx' ) assert import.save, import.errors.messages + import.confirm import.schedule assert_equal 'scheduled', import.state