From b7eb2160658229b87bef38f8f1d1bc2ba1549ff4 Mon Sep 17 00:00:00 2001
From: Alexis Reigel <mail@koffeinfrei.org>
Date: Fri, 1 Jun 2018 10:34:02 +0200
Subject: [PATCH] add transaction support

---
 lib/excelsior/import.rb      | 42 ++++++++++++-------
 lib/excelsior/transaction.rb | 15 +++++++
 test/excelsior_test.rb       | 78 ++++++++++++++++++++++++++++++++----
 3 files changed, 113 insertions(+), 22 deletions(-)
 create mode 100644 lib/excelsior/transaction.rb

diff --git a/lib/excelsior/import.rb b/lib/excelsior/import.rb
index 98087a5..d4e0527 100644
--- a/lib/excelsior/import.rb
+++ b/lib/excelsior/import.rb
@@ -5,11 +5,13 @@ require 'excelsior/source'
 require 'excelsior/mapping'
 require 'excelsior/error'
 require 'excelsior/report'
+require 'excelsior/transaction'
 
 module Excelsior
   class Import
     include Source
     include Mapping
+    include Transaction
 
     attr_accessor :source, :fields, :errors, :report
     attr_accessor :rows, :columns
@@ -29,21 +31,15 @@ module Excelsior
       valid?
     end
 
-    def run # takes an optional block
-      @rows.map.with_index do |row, i|
-        attributes = map_row_values(row, @columns)
-        if block_given?
-          begin
-            result = yield attributes
-            report_insert
-            result
-          rescue
-            report_failure
-          end
-        else
-          record = model_class.create(attributes)
-          add_model_errors(record, i)
+    def run(&block)
+      if self.class.use_transaction
+        model_class.transaction do
+          insert_rows(&block)
+
+          raise ActiveRecord::Rollback if @report.failed > 0
         end
+      else
+        insert_rows(&block)
       end
     end
 
@@ -84,5 +80,23 @@ module Excelsior
     def report_failure
       @report.failed += 1
     end
+
+    def insert_rows(&block)
+      @rows.map.with_index do |row, i|
+        attributes = map_row_values(row, @columns)
+        if block_given?
+          begin
+            result = block.call(attributes)
+            report_insert
+            result
+          rescue
+            report_failure
+          end
+        else
+          record = model_class.create(attributes)
+          add_model_errors(record, i)
+        end
+      end
+    end
   end
 end
diff --git a/lib/excelsior/transaction.rb b/lib/excelsior/transaction.rb
new file mode 100644
index 0000000..d3a2247
--- /dev/null
+++ b/lib/excelsior/transaction.rb
@@ -0,0 +1,15 @@
+module Excelsior
+  module Transaction
+    def self.included(host_class)
+      host_class.extend ClassMethods
+    end
+
+    module ClassMethods
+      attr_accessor :use_transaction
+
+      def transaction(use_transaction)
+        self.use_transaction = use_transaction
+      end
+    end
+  end
+end
diff --git a/test/excelsior_test.rb b/test/excelsior_test.rb
index 946db56..259dc2d 100644
--- a/test/excelsior_test.rb
+++ b/test/excelsior_test.rb
@@ -1,20 +1,22 @@
 require "test_helper"
 require "excelsior/import"
 
-class UserImport < Excelsior::Import
-  source "test/files/complete.xlsx"
-
-  map "Vorname", to: :first_name
-  map "Nachname", to: :last_name
-  map "E-Mail", to: :email
-end
-
 class User < ActiveRecord::Base
   validates :first_name, presence: true
 end
 
 describe Excelsior do
   before do
+    Object.send(:remove_const, :UserImport) if Object.constants.include?(:UserImport)
+
+    class UserImport < Excelsior::Import
+      source "test/files/complete.xlsx"
+
+      map "Vorname", to: :first_name
+      map "Nachname", to: :last_name
+      map "E-Mail", to: :email
+    end
+
     @import = UserImport.new
   end
 
@@ -34,6 +36,66 @@ describe Excelsior do
     end
   end
 
+  describe 'transaction' do
+    describe 'transaction is not set' do
+      it 'inserts only the valid rows' do
+        UserImport.new("test/files/missing-first-name.xlsx").run
+        assert_equal 2, User.count
+      end
+    end
+
+    describe 'transaction is set to true' do
+      before do
+        Object.send(:remove_const, :UserImport) if Object.constants.include?(:UserImport)
+
+        class UserImport < Excelsior::Import
+          source "test/files/complete.xlsx"
+          transaction true
+
+          map "Vorname", to: :first_name
+          map "Nachname", to: :last_name
+          map "E-Mail", to: :email
+        end
+      end
+
+      describe 'without a block' do
+        let(:import) { UserImport.new("test/files/missing-first-name.xlsx") }
+
+        before do
+          import.run
+        end
+
+        it 'rolls back if one record fails' do
+          assert_equal 0, User.count
+        end
+
+        it 'returns the report' do
+          assert_equal 2, import.report.inserted
+          assert_equal 1, import.report.failed
+          assert_equal 3, import.report.total
+        end
+      end
+
+      describe 'with a block' do
+        it 'rolls back if one record raises an error' do
+          UserImport.new("test/files/missing-first-name.xlsx").run do |v|
+            User.create!(v)
+          end
+
+          assert_equal 0, User.count
+        end
+
+        it 'does not roll back if the block does not raise an error' do
+          UserImport.new("test/files/missing-first-name.xlsx").run do |v|
+            User.create(v)
+          end
+
+          assert_equal 2, User.count
+        end
+      end
+    end
+  end
+
   describe '#rows' do
     it 'returns the correct number of rows' do
       assert_equal 2, @import.rows.length
-- 
GitLab