From 8fe2908a6388a7ae911867116aed25c8d8cf7795 Mon Sep 17 00:00:00 2001
From: Alexis Reigel <mail@koffeinfrei.org>
Date: Thu, 31 May 2018 16:39:35 +0200
Subject: [PATCH] add model validation error reporting

---
 lib/excelsior/error.rb             |   3 +++
 lib/excelsior/import.rb            |  14 ++++++++++++--
 test/excelsior_test.rb             |   7 +++++++
 test/files/missing-first-name.xlsx | Bin 0 -> 5489 bytes
 4 files changed, 22 insertions(+), 2 deletions(-)
 create mode 100644 lib/excelsior/error.rb
 create mode 100644 test/files/missing-first-name.xlsx

diff --git a/lib/excelsior/error.rb b/lib/excelsior/error.rb
new file mode 100644
index 0000000..8493619
--- /dev/null
+++ b/lib/excelsior/error.rb
@@ -0,0 +1,3 @@
+module Excelsior
+  Error = Struct.new(:row, :errors)
+end
diff --git a/lib/excelsior/import.rb b/lib/excelsior/import.rb
index 455ee54..fd17e42 100644
--- a/lib/excelsior/import.rb
+++ b/lib/excelsior/import.rb
@@ -3,6 +3,7 @@ require 'rails'
 require 'active_record'
 require 'excelsior/source'
 require 'excelsior/mapping'
+require 'excelsior/error'
 
 module Excelsior
   class Import
@@ -26,12 +27,13 @@ module Excelsior
     end
 
     def run # takes an optional block
-      @rows.map do |row|
+      @rows.map.with_index do |row, i|
         attributes = map_row_values(row, @columns)
         if block_given?
           yield attributes
         else
-          model_class.create!(attributes)
+          record = model_class.create(attributes)
+          add_model_errors(record, i)
         end
       end
     end
@@ -52,5 +54,13 @@ module Excelsior
     def model_class
       self.class.name.gsub("Import", "").constantize
     end
+
+    def add_model_errors(record, index)
+      return if record.errors.empty?
+
+      @errors[:model] ||= []
+
+      @errors[:model] << Error.new(index + 1, record.errors.full_messages)
+    end
   end
 end
diff --git a/test/excelsior_test.rb b/test/excelsior_test.rb
index adafe5f..40b3c3f 100644
--- a/test/excelsior_test.rb
+++ b/test/excelsior_test.rb
@@ -15,6 +15,7 @@ end
 
 class ExcelsiorTest < Minitest::Test
   def setup
+    User.delete_all
     @import = UserImport.new
   end
 
@@ -74,4 +75,10 @@ class ExcelsiorTest < Minitest::Test
     import = UserImport.new("test/files/missing-column.xlsx")
     assert import.errors[:missing_column].any?
   end
+
+  def test_model_validations
+    import = UserImport.new("test/files/missing-first-name.xlsx").tap(&:run)
+    assert import.errors[:model].any?
+    assert_equal import.errors[:model], [Excelsior::Error.new(3, ["First name can't be blank"])]
+  end
 end
diff --git a/test/files/missing-first-name.xlsx b/test/files/missing-first-name.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..56f6349dbf85db3ad7986cca64c9c5f9c02d4d22
GIT binary patch
literal 5489
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
Y=IsD)RyL3#E(R`!2}}$Or-eW~0GhNb>i_@%

literal 0
HcmV?d00001

-- 
GitLab