importer 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +70 -0
- data/Rakefile +60 -0
- data/VERSION +1 -0
- data/importer.gemspec +114 -0
- data/lib/importer.rb +102 -0
- data/lib/importer/import.rb +5 -0
- data/lib/importer/import/active_record.rb +37 -0
- data/lib/importer/import/simple.rb +57 -0
- data/lib/importer/imported_object.rb +7 -0
- data/lib/importer/imported_object/active_record.rb +41 -0
- data/lib/importer/imported_object/simple.rb +25 -0
- data/lib/importer/parser.rb +22 -0
- data/lib/importer/parser/base.rb +31 -0
- data/lib/importer/parser/csv.rb +24 -0
- data/lib/importer/parser/xml.rb +25 -0
- data/rails/init.rb +1 -0
- data/rails_generators/importer/importer_generator.rb +18 -0
- data/rails_generators/importer/templates/imported_objects_migration.rb +19 -0
- data/rails_generators/importer/templates/imports_migration.rb +17 -0
- data/test/database.yml +3 -0
- data/test/factories.rb +11 -0
- data/test/fixtures/empty.csv +0 -0
- data/test/fixtures/empty.xml +3 -0
- data/test/fixtures/product.csv +2 -0
- data/test/fixtures/product.xml +9 -0
- data/test/fixtures/products.csv +4 -0
- data/test/fixtures/products.xml +21 -0
- data/test/helper.rb +71 -0
- data/test/importer/import/active_record_test.rb +27 -0
- data/test/importer/import/simple_test.rb +27 -0
- data/test/importer/imported_object/active_record_test.rb +37 -0
- data/test/importer/imported_object/simple_test.rb +35 -0
- data/test/importer/imported_object_test.rb +13 -0
- data/test/importer/parser/csv_test.rb +59 -0
- data/test/importer/parser/xml_test.rb +58 -0
- data/test/importer/parser_test.rb +13 -0
- data/test/importer_test.rb +50 -0
- metadata +184 -0
| @@ -0,0 +1,57 @@ | |
| 1 | 
            +
            module Importer
         | 
| 2 | 
            +
              module Import
         | 
| 3 | 
            +
                # Simple import summary. It's not stored in database (as with +ActiveRecord+ import).
         | 
| 4 | 
            +
                #
         | 
| 5 | 
            +
                # Attributes:
         | 
| 6 | 
            +
                # * +new_objects_count+ - number of new objects created during the import
         | 
| 7 | 
            +
                # * +existing_objects_count+ - number of objects modified during the import
         | 
| 8 | 
            +
                # * +invalid_objects_count+ - number of objects that couldn't have been imported
         | 
| 9 | 
            +
                # * +workflow_state+ - import may be in one of three states: ready, started or
         | 
| 10 | 
            +
                #   finished. The state changes during the import process.
         | 
| 11 | 
            +
                class Simple
         | 
| 12 | 
            +
                  attr_reader :state, :new_objects_count, :existing_objects_count, :invalid_objects_count, :imported_objects
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  class << self
         | 
| 15 | 
            +
                    def create
         | 
| 16 | 
            +
                      import = new
         | 
| 17 | 
            +
                      import.save
         | 
| 18 | 
            +
                      import
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def initialize
         | 
| 23 | 
            +
                    @state                  = "ready"
         | 
| 24 | 
            +
                    @new_objects_count      = 0
         | 
| 25 | 
            +
                    @existing_objects_count = 0
         | 
| 26 | 
            +
                    @invalid_objects_count  = 0
         | 
| 27 | 
            +
                    @imported_objects       = []
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def start!
         | 
| 31 | 
            +
                    @state = "started"
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def finish!
         | 
| 35 | 
            +
                    @state = "finished"
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def add_object(imported_object)
         | 
| 39 | 
            +
                    case imported_object.state
         | 
| 40 | 
            +
                    when "new_object"
         | 
| 41 | 
            +
                      @new_objects_count += 1
         | 
| 42 | 
            +
                    when "existing_object"
         | 
| 43 | 
            +
                      @existing_objects_count += 1
         | 
| 44 | 
            +
                    when "invalid_object"
         | 
| 45 | 
            +
                      @invalid_objects_count += 1
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                    @imported_objects << imported_object
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def save
         | 
| 51 | 
            +
                    true
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  alias_method :save!, :save
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
            end
         | 
| @@ -0,0 +1,41 @@ | |
| 1 | 
            +
            require 'active_record'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Importer
         | 
| 4 | 
            +
              module ImportedObject
         | 
| 5 | 
            +
                # ActiveRecord model that stores detailed information of imported objects in
         | 
| 6 | 
            +
                # imported_objects database table.
         | 
| 7 | 
            +
                #
         | 
| 8 | 
            +
                # belongs_to :import - reference to import instance
         | 
| 9 | 
            +
                # belongs_to :object - reference to imported object
         | 
| 10 | 
            +
                #
         | 
| 11 | 
            +
                # Attributes:
         | 
| 12 | 
            +
                # * +data+ - object's detected attributes hash
         | 
| 13 | 
            +
                # * +validation_errors+ - object's validation errors hash.
         | 
| 14 | 
            +
                class ActiveRecord < ::ActiveRecord::Base
         | 
| 15 | 
            +
                  set_table_name "imported_objects"
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  named_scope :new_objects,       :conditions => { :state => "new_object" }
         | 
| 18 | 
            +
                  named_scope :existing_objects,  :conditions => { :state => "existing_object" }
         | 
| 19 | 
            +
                  named_scope :invalid_objects,   :conditions => { :state => "invalid_object" }
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  belongs_to  :import, :class_name => "Importer::Import::ActiveRecord"
         | 
| 22 | 
            +
                  belongs_to  :object, :polymorphic => true
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  after_save    :increment_counter
         | 
| 25 | 
            +
                  after_destroy :decrement_counter
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  serialize :data
         | 
| 28 | 
            +
                  serialize :validation_errors
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  protected
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def increment_counter
         | 
| 33 | 
            +
                    import.increment!("#{state}s_count") if import
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def decrement_counter
         | 
| 37 | 
            +
                    import.decrement!("#{state}s_count") if import
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            module Importer
         | 
| 2 | 
            +
              module ImportedObject
         | 
| 3 | 
            +
                # Simple imported object details. It's not stored in database (as with
         | 
| 4 | 
            +
                # +ActiveRecord+ imported object).
         | 
| 5 | 
            +
                #
         | 
| 6 | 
            +
                # Attributes:
         | 
| 7 | 
            +
                # * +data+ - object's detected attributes hash
         | 
| 8 | 
            +
                # * +validation_errors+ - object's validation errors hash
         | 
| 9 | 
            +
                class Simple
         | 
| 10 | 
            +
                  attr_reader   :import
         | 
| 11 | 
            +
                  attr_accessor :state, :object, :data, :validation_errors
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def initialize(attributes = {})
         | 
| 14 | 
            +
                    raise ArgumentError.new(":import attribute is required.") unless attributes[:import]
         | 
| 15 | 
            +
                    @import = attributes[:import]
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def save
         | 
| 19 | 
            +
                    import.add_object(self)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  alias_method :save!, :save
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            module Importer
         | 
| 2 | 
            +
              class ParserNotFoundError < ::Exception; end
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              # Determines the parser needed to parse given +file+ basing on +file+ extension.
         | 
| 5 | 
            +
              # Return Xml parser for .xml files, Csv parser for .csv file and so on.
         | 
| 6 | 
            +
              module Parser
         | 
| 7 | 
            +
                def self.get_klass(file)
         | 
| 8 | 
            +
                  extension = File.extname(file)[1..-1]
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  if extension
         | 
| 11 | 
            +
                    klass = extension.camelize
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    if Importer::Parser.const_defined?(klass.to_sym)
         | 
| 14 | 
            +
                      klass = "Importer::Parser::#{klass}".constantize
         | 
| 15 | 
            +
                      return klass
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  raise Importer::ParserNotFoundError.new("Can't find #{klass} parser.")
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            module Importer
         | 
| 2 | 
            +
              module Parser
         | 
| 3 | 
            +
                # Extend this class if you want to provide a custom parser.
         | 
| 4 | 
            +
                # You only need to implement +run+ instance method in subclasses.
         | 
| 5 | 
            +
                class Base
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  class << self
         | 
| 8 | 
            +
                    # Creates parser instance and processes the +file+
         | 
| 9 | 
            +
                    def run(file)
         | 
| 10 | 
            +
                      parser = new(file)
         | 
| 11 | 
            +
                      parser.run
         | 
| 12 | 
            +
                      parser.data
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  attr_reader :data
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def initialize(file)
         | 
| 19 | 
            +
                    @file = file
         | 
| 20 | 
            +
                    @data = []
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  # This method must be implemented in subclasses.
         | 
| 24 | 
            +
                  # It is meant to process input @file, and store the results in @data instance
         | 
| 25 | 
            +
                  # variable.
         | 
| 26 | 
            +
                  def run
         | 
| 27 | 
            +
                    raise Exception.new("This method must be implemented in subclasses.")
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            require 'fastercsv'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # CSV parser
         | 
| 4 | 
            +
            # It uses fastercsv lib to parse the files.
         | 
| 5 | 
            +
            module Importer
         | 
| 6 | 
            +
              module Parser
         | 
| 7 | 
            +
                class Csv < Base
         | 
| 8 | 
            +
                  def run
         | 
| 9 | 
            +
                    @data = []
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    data = FasterCSV.read(@file, :skip_blanks => true)
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    unless data.empty?
         | 
| 14 | 
            +
                      attributes = data.shift
         | 
| 15 | 
            +
                      @data = data.map do |values|
         | 
| 16 | 
            +
                        Hash[*attributes.zip(values).flatten]
         | 
| 17 | 
            +
                      end
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    @data
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            require 'crack/xml'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # XML parser
         | 
| 4 | 
            +
            # It uses crack/xml lib to parse the files.
         | 
| 5 | 
            +
            module Importer
         | 
| 6 | 
            +
              module Parser
         | 
| 7 | 
            +
                class Xml < Base
         | 
| 8 | 
            +
                  def run
         | 
| 9 | 
            +
                    @data = []
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    file = File.new(@file)
         | 
| 12 | 
            +
                    data = Crack::XML.parse(file)
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    root = data.shift[1]
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    if root
         | 
| 17 | 
            +
                      objects = root.shift[1]
         | 
| 18 | 
            +
                      @data = objects.is_a?(Hash) ? [objects] : objects
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    @data
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
    
        data/rails/init.rb
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            require File.join(File.dirname(__FILE__), '..', 'lib', 'importer')
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            module Rails
         | 
| 2 | 
            +
              module Generator
         | 
| 3 | 
            +
                class ImporterGenerator < NamedBase
         | 
| 4 | 
            +
                  def banner
         | 
| 5 | 
            +
                    "Usage: #{$0} importer import"
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def manifest
         | 
| 9 | 
            +
                    record do |m|
         | 
| 10 | 
            +
                      time = Time.now
         | 
| 11 | 
            +
                      m.template "imports_migration.rb", "db/migrate/#{time.strftime('%Y%m%d%H%M%S')}_create_imports.rb"
         | 
| 12 | 
            +
                      time += 1
         | 
| 13 | 
            +
                      m.template "imported_objects_migration.rb", "db/migrate/#{time.strftime('%Y%m%d%H%M%S')}_create_imported_objects.rb"
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            class CreateImportedObjects < ActiveRecord::Migration
         | 
| 2 | 
            +
              def self.up
         | 
| 3 | 
            +
                create_table :imported_objects do |t|
         | 
| 4 | 
            +
                  t.integer :import_id,         :null => false
         | 
| 5 | 
            +
                  t.string  :object_type
         | 
| 6 | 
            +
                  t.integer :object_id
         | 
| 7 | 
            +
                  t.text    :data
         | 
| 8 | 
            +
                  t.text    :validation_errors
         | 
| 9 | 
            +
                  t.string  :state,             :null => false, :limit => 20
         | 
| 10 | 
            +
                  t.timestamps
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
                add_index :imported_objects, :import_id
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              def self.down
         | 
| 16 | 
            +
                remove_index :imported_objects, :import_id
         | 
| 17 | 
            +
                drop_table :imported_objects
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            class CreateImports < ActiveRecord::Migration
         | 
| 2 | 
            +
              def self.up
         | 
| 3 | 
            +
                create_table :imports do |t|
         | 
| 4 | 
            +
                  t.integer :new_objects_count,      :null => false, :default => 0
         | 
| 5 | 
            +
                  t.integer :existing_objects_count, :null => false, :default => 0
         | 
| 6 | 
            +
                  t.integer :invalid_objects_count,  :null => false, :default => 0
         | 
| 7 | 
            +
                  t.string  :workflow_state,         :null => false, :default => "ready", :limit => 10
         | 
| 8 | 
            +
                  t.timestamps
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
                add_index :imports, :workflow_state
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              def self.down
         | 
| 14 | 
            +
                remove_index :imports, :workflow_state
         | 
| 15 | 
            +
                drop_table :imports
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
    
        data/test/database.yml
    ADDED
    
    
    
        data/test/factories.rb
    ADDED
    
    | @@ -0,0 +1,11 @@ | |
| 1 | 
            +
            Factory.define :active_record_import, :class => Importer::Import::ActiveRecord do |f|
         | 
| 2 | 
            +
            end
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            Factory.define :active_record_imported_object, :class => Importer::ImportedObject::ActiveRecord do |f|
         | 
| 5 | 
            +
            end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Factory.define :simple_import, :class => Importer::Import::Simple do |f|
         | 
| 8 | 
            +
            end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            Factory.define :product do |f|
         | 
| 11 | 
            +
            end
         | 
| 
            File without changes
         | 
| @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            <?xml version="1.0" encoding="utf-8"?>
         | 
| 2 | 
            +
            <products>
         | 
| 3 | 
            +
              <product>
         | 
| 4 | 
            +
                <customid>1</customid>
         | 
| 5 | 
            +
                <name>A black ball</name>
         | 
| 6 | 
            +
                <description>Round glass ball.</description>
         | 
| 7 | 
            +
                <price>86.00</price>
         | 
| 8 | 
            +
              </product>
         | 
| 9 | 
            +
              <product>
         | 
| 10 | 
            +
                <customid>2</customid>
         | 
| 11 | 
            +
                <name>A red hat</name>
         | 
| 12 | 
            +
                <description>Party hat.</description>
         | 
| 13 | 
            +
                <price>114.00</price>
         | 
| 14 | 
            +
              </product>
         | 
| 15 | 
            +
              <product>
         | 
| 16 | 
            +
                <customid>3</customid>
         | 
| 17 | 
            +
                <name>A white ribbon</name>
         | 
| 18 | 
            +
                <description>A really long one.</description>
         | 
| 19 | 
            +
                <price>oops</price>
         | 
| 20 | 
            +
              </product>
         | 
| 21 | 
            +
            </products>
         | 
    
        data/test/helper.rb
    ADDED
    
    | @@ -0,0 +1,71 @@ | |
| 1 | 
            +
            require 'rubygems'
         | 
| 2 | 
            +
            require 'test/unit'
         | 
| 3 | 
            +
            require 'shoulda'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
         | 
| 6 | 
            +
            $LOAD_PATH.unshift(File.dirname(__FILE__))
         | 
| 7 | 
            +
            require 'importer'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
         | 
| 10 | 
            +
            ActiveRecord::Base.establish_connection(config['test'])
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            require 'factory_girl'
         | 
| 13 | 
            +
            require 'factories'
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            class Test::Unit::TestCase
         | 
| 16 | 
            +
              def setup
         | 
| 17 | 
            +
                reset_tables
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              def reset_tables
         | 
| 21 | 
            +
                ActiveRecord::Base.connection.create_table :products, { :force => true } do |t|
         | 
| 22 | 
            +
                  t.string  :customid
         | 
| 23 | 
            +
                  t.string  :name
         | 
| 24 | 
            +
                  t.string  :description
         | 
| 25 | 
            +
                  t.decimal :price
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                ActiveRecord::Base.connection.create_table :imports, { :force => true } do |t|
         | 
| 29 | 
            +
                  t.integer :new_objects_count,       :default => 0
         | 
| 30 | 
            +
                  t.integer :existing_objects_count,  :default => 0
         | 
| 31 | 
            +
                  t.integer :invalid_objects_count,   :default => 0
         | 
| 32 | 
            +
                  t.string  :workflow_state,          :default => "ready"
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                ActiveRecord::Base.connection.create_table :imported_objects, { :force => true } do |t|
         | 
| 36 | 
            +
                  t.integer :import_id
         | 
| 37 | 
            +
                  t.string  :object_type
         | 
| 38 | 
            +
                  t.integer :object_id
         | 
| 39 | 
            +
                  t.string  :data
         | 
| 40 | 
            +
                  t.string  :validation_errors
         | 
| 41 | 
            +
                  t.string  :state
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
              # return path to a fixture file from test/fixtures dir
         | 
| 46 | 
            +
              def fixture_file(file)
         | 
| 47 | 
            +
                File.expand_path(File.dirname(__FILE__) + "/fixtures/#{file}")
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            class Product < ActiveRecord::Base
         | 
| 52 | 
            +
              include Importer
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              validates_numericality_of :price
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              def self.find_on_import(import, attributes)
         | 
| 57 | 
            +
                find_by_customid(attributes["customid"])
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
            end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            class InvalidProduct < Product
         | 
| 62 | 
            +
              set_table_name "products"
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              def self.find_on_import(import, attributes)
         | 
| 65 | 
            +
                if attributes["customid"] == "3"
         | 
| 66 | 
            +
                  raise ::Exception.new("An error occured.")
         | 
| 67 | 
            +
                else
         | 
| 68 | 
            +
                  super
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
            end
         |