| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
 | # coding: utf-8
class SimpleExporter < SimpleInterface
  def export opts={}
    configuration.validate!
    init_env opts
    @csv = nil
    fail_with_error "Unable to write in file: #{self.filepath}" do
      dir = Pathname.new(self.filepath).dirname
      FileUtils.mkdir_p dir
      @csv = CSV.open(self.filepath, 'w', self.configuration.csv_options)
    end
    self.configuration.before_actions(:parsing).each do |action| action.call self end
    @statuses = ""
    if ENV["NO_TRANSACTION"]
      process_collection
    else
      ActiveRecord::Base.transaction do
        process_collection
      end
    end
    self.status ||= :success
  rescue SimpleInterface::FailedOperation
    self.status = :failed
  ensure
    @csv&.close
    task_finished
  end
  def collection
    @collection ||= begin
      coll = configuration.collection
      coll = coll.call() if coll.is_a?(Proc)
      coll
    end
  end
  def encode_string s
    s.encode("utf-8").force_encoding("utf-8")
  end
  protected
  def init_env opts
    @number_of_lines = collection.size
    super opts
  end
  def process_collection
    self.configuration.before_actions(:all).each do |action| action.call self end
    log "Starting export ..."
    log "Export will be written in #{filepath}"
    @csv << self.configuration.columns.map(&:name)
    if collection.is_a?(ActiveRecord::Relation) && collection.model.column_names.include?("id")
      ids = collection.pluck :id
      ids.in_groups_of(configuration.batch_size).each do |batch_ids|
        collection.where(id: batch_ids).each do |item|
          handle_item item
        end
      end
    else
      collection.each{|item| handle_item item }
    end
    print_state
  end
  def map_item_to_rows item
    return [item] unless configuration.item_to_rows_mapping
    configuration.item_to_rows_mapping.call(item).map {|row| row.is_a?(ActiveRecord::Base) ? row : CustomRow.new(row) }
  end
  def resolve_value item, col
    scoped_item = col.scope.inject(item){|tmp, scope| tmp.send(scope)}
    val = col[:value]
    if val.nil? || val.is_a?(Proc)
      if val.is_a?(Proc)
        val = instance_exec(scoped_item, &val)
      else
        attributes = [col.attribute].flatten
        val = attributes.inject(scoped_item){|tmp, attr| tmp.send(attr) if tmp }
      end
    end
    if val.nil? && !col.omit_nil?
      push_in_journal({event: :attribute_not_found, message: "Value missing for: #{[col.scope, col.attribute].flatten.join('.')}", kind: :warning})
      self.status ||= :success_with_warnings
      @new_status ||= colorize("✓", :orange)
    end
    if val.nil? && col.required?
      @new_status = colorize("x", :red)
      raise "MISSING VALUE FOR COLUMN #{col.name}"
    end
    val = encode_string(val) if val.is_a?(String)
    val
  end
  def handle_item item
    number_of_lines = @number_of_lines
    @current_item = item
    map_item_to_rows(item).each_with_index do |item, i|
      @number_of_lines = number_of_lines + i
      @current_row = item.attributes
      @current_row = @current_row.slice(*configuration.logged_attributes) if configuration.logged_attributes.present?
      row = []
      @new_status = nil
      self.configuration.columns.each do |col|
        val = resolve_value item, col
        @new_status ||= colorize("✓", :green)
        row << val
      end
      push_in_journal({event: :success, kind: :log})
      @statuses += @new_status
      print_state if @current_line % 20 == 0 || i > 0
      @current_line += 1
      @csv << row
    end
  end
  class CustomRow < OpenStruct
    def initialize data
      super data
      @data = data
    end
    def attributes
      flatten_hash @data
    end
    protected
    def flatten_hash h
      h.each_with_object({}) do |(k, v), h|
        if v.is_a? Hash
          flatten_hash(v).map do |h_k, h_v|
            h["#{k}.#{h_k}".to_sym] = h_v
          end
        else
          h[k] = v
        end
      end
    end
  end
  class Configuration < SimpleInterface::Configuration
    attr_accessor :collection
    attr_accessor :batch_size
    attr_accessor :logged_attributes
    attr_accessor :item_to_rows_mapping
    def initialize import_name, opts={}
      super import_name, opts
      @collection = opts[:collection]
      @batch_size = opts[:batch_size] || 1000
      @logged_attributes = opts[:logged_attributes]
      @item_to_rows_mapping = opts[:item_to_rows_mapping]
    end
    def options
      super.update({
        collection: collection,
        batch_size: batch_size,
        logged_attributes: logged_attributes,
        item_to_rows_mapping: item_to_rows_mapping,
      })
    end
    def map_item_to_rows &block
      @item_to_rows_mapping = block
    end
    def add_column name, opts={}
      raise "Column already defined: #{name}" if @columns.any?{|c| c.name == name.to_s}
      super name, opts
    end
    def validate!
      raise "Incomplete configuration, missing collection for #{@import_name}" if collection.nil?
    end
  end
end
 |