Module: DutyFree::Extensions

Defined in:
lib/duty_free/extensions.rb

Overview

rubocop:disable Style/CommentedKeyword

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

MAX_ID =
Arel.sql('MAX(id)')
IS_AMOEBA =
Gem.loaded_specs['amoeba']

Class Method Summary collapse

Class Method Details

._fk_from(assoc) ⇒ Object



819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
# File 'lib/duty_free/extensions.rb', line 819

def self._fk_from(assoc)
  # Try first to trust whatever they've marked as being the foreign_key, and then look
  # at the inverse's foreign key setting if available.  In all cases don't accept
  # anything that's not backed with a real column in the table.
  col_names = assoc.klass.column_names
  if (
       (fk_name = assoc.options[:foreign_key]) ||
       (fk_name = assoc.inverse_of&.options&.fetch(:foreign_key) { nil }) ||
       (assoc.respond_to?(:foreign_key) && (fk_name = assoc.foreign_key)) ||
       (fk_name = assoc.inverse_of&.foreign_key) ||
       (fk_name = assoc.inverse_of&.association_foreign_key)
     ) && col_names.include?(fk_name.to_s)
    return fk_name
  end

  # Don't let this fool you -- we really are in search of the foreign key name here,
  # and Rails 3.0 and older used some fairly interesting conventions, calling it instead
  # the "primary_key_name"!
  if assoc.respond_to?(:primary_key_name)
    if (fk_name = assoc.primary_key_name) && col_names.include?(fk_name.to_s)
      return fk_name
    end
    if (fk_name = assoc.inverse_of.primary_key_name) && col_names.include?(fk_name.to_s)
      return fk_name
    end
  end

  puts "* Wow, had no luck at all finding a foreign key for #{assoc.inspect}"
end

._recurse_def(klass, cols, import_template, build_tables = nil, order_by = nil, assocs = [], joins = [], pre_prefix = '', prefix = '') ⇒ Object

Recurse and return three arrays – one with all columns in sequence, and one a hierarchy of nested hashes to be used with ActiveRecord’s .joins() to facilitate export, and finally one that lists tables that need to be built along the way.



914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
# File 'lib/duty_free/extensions.rb', line 914

def self._recurse_def(klass, cols, import_template, build_tables = nil, order_by = nil, assocs = [], joins = [], pre_prefix = '', prefix = '')
  prefix = prefix[0..-2] if (is_build_table = prefix.end_with?('!'))
  prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])

  if prefix.present?
    # An indication to build this table and model if it doesn't exist?
    if is_build_table && build_tables
      namespaces = prefix.split('::')
      model_name = namespaces.map { |part| part.singularize.camelize }.join('::')
      prefix = namespaces.last
      # %%% If the model already exists, take belongs_to cues from it for building the table
      if (is_need_model = Object.const_defined?(model_name)) &&
         (is_need_table = ActiveRecord::ConnectionAdapters::SchemaStatements.table_exists?(
           (path_name = ::DutyFree::Util._prefix_join([prefixes, prefix.pluralize]))
         ))
        is_build_table = false
      else
        build_tables[path_name] = [namespaces, is_need_model, is_need_table] unless build_tables.include?(path_name)
      end
    end
    unless is_build_table && build_tables
      # Confirm we can actually navigate through this association
      prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
      if prefix_assoc
        assocs = assocs.dup << prefix_assoc
        if order_by && [:has_many, :has_and_belongs_to_many].include?(prefix_assoc.macro) &&
           (pk = prefix_assoc.active_record.primary_key)
          order_by << ["#{prefixes.tr('.', '_')}_", pk]
        end
      end
    end
  end
  cols = cols.inject([]) do |s, col|
    s + if col.is_a?(Hash)
          col.inject([]) do |s2, v|
            if order_by
              # Find what the type is for this guy
              next_klass = (assocs.last&.klass || klass).reflect_on_association(v.first)&.klass
              # Used to be:  { v.first.to_sym => (joins_array = []) }
              joins << { v.first.to_sym => (joins_array = [next_klass]) }
            end
            s2 + _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, build_tables, order_by, assocs, joins_array, prefixes, v.first.to_sym).first
          end
        elsif col.nil?
          if assocs.empty?
            []
          else
            # Bring in from another class
            # Used to be:  { prefix => (joins_array = []) }
            # %%% Probably need a next_klass thing like above
            joins << { prefix => (joins_array = [klass]) } if order_by
            # %%% Also bring in uniques and requireds
            _recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, build_tables, order_by, assocs, joins_array, prefixes).first
          end
        else
          [::DutyFree::Column.new(col, pre_prefix, prefix, assocs, klass, import_template[:as])]
        end
  end
  [cols, joins]
end

._save_pending(to_be_saved) ⇒ Object

Called before building any object linked through a has_one or has_many so that foreign key IDs can be added properly to those new objects. Finally at the end also called to save everything.



854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
# File 'lib/duty_free/extensions.rb', line 854

def self._save_pending(to_be_saved)
  while (tbs = to_be_saved.pop)
    # puts "Will be calling #{tbs[1].class.name} #{tbs[1]&.id} .#{tbs[2]} #{tbs[3].class.name} #{tbs[3]&.id}"
    # Wire this one up if it had come from a has_one
    if tbs[0] == tbs[1]
      tbs[1].class.has_one(tbs[2]) unless tbs[1].respond_to?(tbs[2])
      tbs[1].send(tbs[2], tbs[3])
    end

    ais = (tbs.first.class.respond_to?(:around_import_save) && tbs.first.class.method(:around_import_save)) ||
          (respond_to?(:around_import_save) && method(:around_import_save))
    if ais
      # Send them the sub_obj even if it might be invalid so they can choose
      # to make it valid if they wish.
      ais.call(tbs.first) do |modded_obj = nil|
        modded_obj = (modded_obj || tbs.first)
        modded_obj.save if modded_obj&.valid?
      end
    elsif tbs.first.valid?
      tbs.first.save
    else
      puts "* Unable to save #{tbs.first.inspect}"
    end

    next if tbs[1].nil? || # From a has_many?
            tbs[0] == tbs[1] || # From a has_one?
            tbs.first.new_record?

    if tbs[1] == :has_and_belongs_to_many # also used by has_many :through associations
      collection = tbs[3].send(tbs[2])
      being_shoveled_id = tbs[0].send(tbs[0].class.primary_key)
      if collection.empty? ||
         !collection.pluck("#{(klass = collection.first.class).table_name}.#{klass.primary_key}").include?(being_shoveled_id)
        collection << tbs[0]
        # puts collection.inspect
      end
    else # Traditional belongs_to
      tbs[1].send(tbs[2], tbs[3])
    end
  end
end

._template_columns(klass, import_template = nil) ⇒ Object

The snake-cased column alias names used in the query to export data



897
898
899
900
901
902
903
904
905
906
907
908
909
# File 'lib/duty_free/extensions.rb', line 897

def self._template_columns(klass, import_template = nil)
  template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
  if klass.instance_variable_get(:@template_import_columns) != import_template
    klass.instance_variable_set(:@template_import_columns, import_template)
    klass.instance_variable_set(:@template_detail_columns, (template_detail_columns = nil))
  end
  unless template_detail_columns
    # puts "* Redoing *"
    template_detail_columns = _recurse_def(klass, import_template[:all], import_template).first.map(&:to_sym)
    klass.instance_variable_set(:@template_detail_columns, template_detail_columns)
  end
  template_detail_columns
end

.import(obj_klass, data, import_template = nil, insert_only) ⇒ Object

With an array of incoming data, the first row having column names, perform the import



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
# File 'lib/duty_free/extensions.rb', line 421

def self.import(obj_klass, data, import_template = nil, insert_only)
  instance_variable_set(:@defined_uniques, nil)
  instance_variable_set(:@valid_uniques, nil)

  import_template ||= if obj_klass.constants.include?(:IMPORT_TEMPLATE)
                        obj_klass::IMPORT_TEMPLATE
                      else
                        obj_klass.suggest_template(0, false, false)
                      end
  # puts "Chose #{import_template}"
  inserts = []
  updates = []
  counts = Hash.new { |h, k| h[k] = [] }
  errors = []

  is_first = true
  uniques = nil
  cols = nil
  starred = []
  partials = []
  # See if we can find the model if given only a string
  if obj_klass.is_a?(String)
    obj_klass_camelized = obj_klass.camelize.singularize
    obj_klass = Object.const_get(obj_klass_camelized) if Object.const_defined?(obj_klass_camelized)
  end
  table_name = obj_klass.table_name unless obj_klass.is_a?(String)
  is_build_table = if import_template.include?(:all!)
                     # Search for presence of this table
                     table_name = obj_klass.underscore.pluralize if obj_klass.is_a?(String)
                     !ActiveRecord::ConnectionAdapters::SchemaStatements.table_exists?(table_name)
                   else
                     false
                   end
  # If we do need to build the table then we require an :all!, otherwise in the case that the table
  # does not need to be built or was already built then try :all, and fall back on :all!.
  all = import_template[is_build_table ? :all! : :all] || import_template[:all!]
  keepers = {}
  valid_unique = nil
  existing = {}
  devise_class = ''
  ret = nil

  # Multi-tenancy gem Apartment can be used if there are separate schemas per tenant
  reference_models = if Object.const_defined?('Apartment')
                       Apartment.excluded_models
                     else
                       []
                     end

  if Object.const_defined?('Devise')
    Object.const_get('Devise') # If this fails, devise_class will remain a blank string.
    devise_class = Devise.mappings.values.first.class_name
    reference_models -= [devise_class]
  else
    devise_class = ''
  end

  # Did they give us a filename?
  if data.is_a?(String)
    # Filenames with full paths can not be longer than 4096 characters, and can not
    # include newline characters
    data = if data.length <= 4096 && !data.index('\n')
             File.open(data)
           else
             # Any multi-line string is likely CSV data
             # %%% Test to see if TAB characters are present on the first line, instead of commas
             CSV.new(data)
           end
  end
  # Or perhaps us a file?
  if data.is_a?(File)
    # Use the "roo" gem if it's available
    # When we're ready to try parsing this thing on our own, shared strings and all, then use
    # the rubyzip gem also along with it:
    # https://github.com/rubyzip/rubyzip
    # require 'zip'
    data = if Object.const_defined?('Roo::Spreadsheet', { csv_options: { encoding: 'bom|utf-8' } })
             Roo::Spreadsheet.open(data)
           else
             # Otherwise generic CSV parsing
             require 'csv' unless Object.const_defined?('CSV')
             CSV.open(data)
           end
  end

  # Will show as just one transaction when using auditing solutions such as PaperTrail
  ActiveRecord::Base.transaction do
    # Check to see if they want to do anything before the whole import
    # First if defined in the import_template, then if there is a method in the class,
    # and finally (not yet implemented) a generic global before_import
    my_before_import = import_template[:before_import]
    my_before_import ||= respond_to?(:before_import) && method(:before_import)
    # my_before_import ||= some generic my_before_import
    if my_before_import
      last_arg_idx = my_before_import.parameters.length - 1
      arguments = [data, import_template][0..last_arg_idx]
      data = ret if (ret = my_before_import.call(*arguments)).is_a?(Enumerable)
    end
    build_tables = nil
    data.each_with_index do |row, row_num|
      row_errors = {}
      if is_first # Anticipate that first row has column names
        uniques = import_template[:uniques]

        # Look for UTF-8 BOM in very first cell
        row[0] = row[0][3..-1] if row[0].start_with?([239, 187, 191].pack('U*'))
        # How about a first character of FEFF or FFFE to support UTF-16 BOMs?
        #   FE FF big-endian (standard)
        #   FF FE little-endian
        row[0] = row[0][1..-1] if [65_279, 65_534].include?(row[0][0].ord)
        cols = row.map { |col| (col || '').strip }

        # Unique column combinations can be called out explicitly in models using uniques: {...}, or to just
        # define one column at a time simply mark with an asterisk.
        # Track and clean up stars
        starred = cols.select do |col|
          if col[0] == '*'
            col.slice!(0)
            col.strip!
          end
        end
        partials = cols.select do |col|
          if col[0] == '~'
            col.slice!(0)
            col.strip!
          end
        end
        cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) }
        obj_klass.send(:_defined_uniques, uniques, cols, cols.join('|'), starred)
        # Main object asking for table to be built?
        build_tables = {}
        build_tables[path_name] = [namespaces, is_need_model, is_need_table] if is_build_table
        # Make sure that at least half of them match what we know as being good column names
        template_column_objects = ::DutyFree::Extensions._recurse_def(obj_klass, import_template[:all], import_template, build_tables).first
        cols.each_with_index do |col, idx|
          # prefixes = col_detail.pre_prefix + (col_detail.prefix.blank? ? [] : [col_detail.prefix])
          # %%% Would be great here if when one comes back nil, try to find the closest match
          # and indicate to the user a "did you mean?" about it.
          keepers[idx] = template_column_objects.find { |col_obj| col_obj.titleize == col }
          # puts "Could not find a match for column #{idx + 1}, #{col}" if keepers[idx].nil?
        end
        raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns') if keepers.length < (cols.length / 2) - 1

        # Returns just the first valid unique lookup set if there are multiple
        valid_unique, bt_criteria = obj_klass.send(:_find_existing, uniques, cols, starred, import_template, keepers, false, insert_only)
        # Make a lookup from unique values to specific IDs
        existing = obj_klass.pluck(*([:id] + valid_unique.keys)).each_with_object(existing) do |v, s|
          s[v[1..-1].map(&:to_s)] = v.first
          s
        end
        is_first = false
      else # Normal row of data
        is_insert = false
        existing_unique = valid_unique.inject([]) do |s, v|
          s << if v.last.is_a?(Array)
                 v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
               else
                 row[v.last].to_s
               end
        end
        to_be_saved = []
        # Check to see if they want to preprocess anything
        existing_unique = @before_process.call(valid_unique, existing_unique) if @before_process ||= import_template[:before_process]
        obj = if (criteria = existing[existing_unique])
                obj_klass.find(criteria)
              else
                is_insert = true
                # unless build_tables.empty? # include?()
                #   binding.pry
                #   x = 5
                # end
                obj_klass.new
              end
        to_be_saved << [obj] unless criteria # || this one has any belongs_to that will be modified here
        sub_obj = nil
        polymorphics = []
        sub_objects = {}
        this_path = nil
        keepers.each do |key, v|
          next if v.nil?

          sub_obj = obj
          this_path = +''
          # puts "p: #{v.path}"
          v.path.each_with_index do |path_part, idx|
            this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
            unless (sub_next = sub_objects[this_path])
              # Check if we're hitting reference data / a lookup thing
              assoc = v.prefix_assocs[idx]
              # belongs_to some lookup (reference) data
              if assoc && reference_models.include?(assoc.class_name)
                lookup_match = assoc.klass.find_by(v.name => row[key])
                # Do a partial match if this column allows for it
                # and we only find one matching result.
                if lookup_match.nil? && partials.include?(v.titleize)
                  lookup_match ||= assoc.klass.where("#{v.name} LIKE '#{row[key]}%'")
                  lookup_match = (lookup_match.length == 1 ? lookup_match.first : nil)
                end
                sub_obj.send("#{path_part}=", lookup_match) unless lookup_match.nil?
                # Reference data from the public level means we stop here
                sub_obj = nil
                break
              end
              # Get existing related object, or create a new one
              # This first part works for belongs_to.  has_many and has_one get sorted below.
              # start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
              start = 0
              trim_prefix = v.titleize[start..-(v.name.length + 2)]
              trim_prefix << ' ' unless trim_prefix.blank?
              if assoc.belongs_to?
                klass = Object.const_get(assoc&.class_name)
                # Try to find a unique item if one is referenced
                sub_next = nil
                begin
                  sub_next, criteria, bt_criteria = klass.send(:_find_existing, uniques, cols, starred, import_template, keepers, nil,
                                                               false, # insert_only
                                                               row, klass, all, trim_prefix, assoc)
                rescue ::DutyFree::NoUniqueColumnError
                end
                bt_name = "#{path_part}="
                # Not yet wired up to the right one, or going to the parent of a self-referencing model?
                # puts "#{v.path} #{criteria.inspect}"
                unless sub_next || (klass == sub_obj.class && (all_criteria = criteria.merge(bt_criteria)).empty?)
                  sub_next = klass.new(all_criteria || {})
                  to_be_saved << [sub_next, sub_obj, bt_name, sub_next]
                end
                # This wires it up in memory, but doesn't yet put the proper foreign key ID in
                # place when the primary object is a new one (and thus not having yet been saved
                # doesn't yet have an ID).
                # binding.pry if !sub_next.new_record? && sub_next.name == 'Squidget Widget' # !sub_obj.changed?
                # # %%% Question is -- does the above set changed? when a foreign key is not yet set
                # # and only the in-memory object has changed?
                is_yet_changed = sub_obj.changed?
                sub_obj.send(bt_name, sub_next)

                # We need this in the case of updating the primary object across a belongs_to when
                # the foreign one already exists and is not otherwise changing, such as when it is
                # not a new one, so wouldn't otherwise be getting saved.
                to_be_saved << [sub_obj] if !sub_obj.new_record? && !is_yet_changed && sub_obj.changed?
              # From a has_many or has_one?
              # Rails 4.0 and later can do:  sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
              elsif [:has_many, :has_one, :has_and_belongs_to_many].include?(assoc.macro) # && !assoc.options[:through]
                ::DutyFree::Extensions._save_pending(to_be_saved)
                sub_next = sub_obj.send(path_part)
                # Try to find a unique item if one is referenced
                # %%% There is possibility that when bringing in related classes using a nil
                # in IMPORT_TEMPLATE[:all] that this will break.  Need to test deeply nested things.

                # assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
                sub_hm, criteria, bt_criteria = assoc.klass.send(:_find_existing, uniques, cols, starred, import_template, keepers, assoc.inverse_of,
                                                                 false, # insert_only
                                                                 row, sub_next, all, trim_prefix, assoc,
                                                                 # Just in case we're running Rails < 4.0 and this is a has_*
                                                                 sub_obj)
                # If still not found then create a new related object using this has_many collection
                # (criteria.empty? ? nil : sub_next.new(criteria))
                if sub_hm
                  sub_next = sub_hm
                elsif assoc.macro == :has_one
                  # assoc.active_record.name.underscore is only there to support older Rails
                  # that doesn't do automatic inverse_of
                  ho_name = "#{assoc.inverse_of&.name || assoc.active_record.name.underscore}="
                  sub_next = assoc.klass.new(criteria)
                  to_be_saved << [sub_next, sub_next, ho_name, sub_obj]
                elsif assoc.macro == :has_and_belongs_to_many ||
                      (assoc.macro == :has_many && assoc.options[:through])
                  # sub_next = sub_next.new(criteria)
                  # Search for one to wire up if it might already exist, otherwise create one
                  sub_next = assoc.klass.find_by(criteria) || assoc.klass.new(criteria)
                  to_be_saved << [sub_next, :has_and_belongs_to_many, assoc.name, sub_obj]
                else
                  # Two other methods that are possible to check for here are :conditions and
                  # :sanitized_conditions, which do not exist in Rails 4.0 and later.
                  sub_next = if assoc.respond_to?(:require_association)
                               # With Rails < 4.0, sub_next could come in as an Array or a broken CollectionProxy
                               assoc.klass.new({ ::DutyFree::Extensions._fk_from(assoc) => sub_obj.send(sub_obj.class.primary_key) }.merge(criteria))
                             else
                               sub_next.proxy_association.reflection.instance_variable_set(:@foreign_key, ::DutyFree::Extensions._fk_from(assoc))
                               sub_next.new(criteria)
                             end
                  to_be_saved << [sub_next]
                end
                # else
                # belongs_to for a found object, or a HMT
              end
              # # Incompatible with Rails < 4.2
              # # Look for possible missing polymorphic detail
              # # Maybe can test for this via assoc.through_reflection
              # if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
              #    (delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
              #    delegate.options[:polymorphic]
              #   polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
              # end

              # rubocop:disable Style/SoleNestedConditional
              unless sub_next.nil?
                # if sub_next.class.name == devise_class && # only for Devise users
                #     sub_next.email =~ Devise.email_regexp
                #   if existing.include?([sub_next.email])
                #     User already exists
                #   else
                #     sub_next.invite!
                #   end
                # end
                sub_objects[this_path] = sub_next if this_path.present?
              end
              # rubocop:enable Style/SoleNestedConditional
            end
            sub_obj = sub_next
          end
          next if sub_obj.nil?

          next unless sub_obj.respond_to?(sym = "#{v.name}=".to_sym)

          col_type = sub_obj.class.columns_hash[v.name.to_s]&.type
          if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
             (virtual_columns = virtual_columns[this_path] || virtual_columns)
            col_type = virtual_columns[v.name]
          end
          if col_type == :boolean
            if row[key].nil?
              # Do nothing when it's nil
            elsif %w[true t yes y].include?(row[key]&.strip&.downcase) # Used to cover 'on'
              row[key] = true
            elsif %w[false f no n].include?(row[key]&.strip&.downcase) # Used to cover 'off'
              row[key] = false
            else
              row_errors[v.name] ||= []
              row_errors[v.name] << "Boolean value \"#{row[key]}\" in column #{key + 1} not recognized"
            end
          end

          is_yet_changed = sub_obj.changed?
          sub_obj.send(sym, row[key])
          # If this one is transitioning from having not changed to now having been changed,
          # and is not a new one anyway that would already be lined up to get saved, then we
          # mark it to now be saved.
          to_be_saved << [sub_obj] if !sub_obj.new_record? && !is_yet_changed && sub_obj.changed?
          # else
          #   puts "  #{sub_obj.class.name} doesn't respond to #{sym}"
        end
        # Try to save final sub-object(s) if any exist
        ::DutyFree::Extensions._save_pending(to_be_saved)

        # Reinstate any missing polymorphic _type and _id values
        polymorphics.each do |poly|
          if !poly[:parent].new_record? || poly[:parent].save
            poly[:child].send("#{poly[:type_col]}=".to_sym, poly[:parent].class.name)
            poly[:child].send("#{poly[:id_col]}=".to_sym, poly[:parent].id)
          end
        end

        # Give a window of opportunity to tweak user objects controlled by Devise
        obj_class = obj.class
        is_do_save = if obj_class.respond_to?(:before_devise_save) && obj_class.name == devise_class
                       obj_class.before_devise_save(obj, existing)
                     else
                       true
                     end

        if obj.valid?
          obj.save if is_do_save
          # Snag back any changes to the unique columns.  (For instance, Devise downcases email addresses.)
          existing_unique = valid_unique.keys.inject([]) { |s, v| s << obj.send(v).to_s }
          # Update the duplicate counts and inserted / updated results
          counts[existing_unique] << row_num
          (is_insert ? inserts : updates) << { row_num => existing_unique } if is_do_save
          # Track this new object so we can properly sense any duplicates later
          existing[existing_unique] = obj.id
        else
          row_errors.merge! obj.errors.messages
        end
        errors << { row_num => row_errors } unless row_errors.empty?
      end
    end
    duplicates = counts.each_with_object([]) do |v, s|
      s += v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
      s
    end
    ret = { inserted: inserts, updated: updates, duplicates: duplicates, errors: errors }

    # Check to see if they want to do anything after the import
    # First if defined in the import_template, then if there is a method in the class,
    # and finally (not yet implemented) a generic global after_import
    my_after_import = import_template[:after_import]
    my_after_import ||= respond_to?(:after_import) && method(:after_import)
    # my_after_import ||= some generic my_after_import
    if my_after_import
      last_arg_idx = my_after_import.parameters.length - 1
      arguments = [ret][0..last_arg_idx]
      # rubocop:disable Lint/UselessAssignment
      ret = ret2 if (ret2 = my_after_import.call(*arguments)).is_a?(Hash)
      # rubocop:enable Lint/UselessAssignment
    end
  end
  ret
end

.included(base) ⇒ Object



14
15
16
17
# File 'lib/duty_free/extensions.rb', line 14

def self.included(base)
  base.send :extend, ClassMethods
  base.send :extend, ::DutyFree::SuggestTemplate::ClassMethods
end