Class: Spree::Export

Inherits:
Object
  • Object
show all
Includes:
NumberIdentifier, SingleStoreResource, VendorConcern
Defined in:
app/models/spree/export.rb

Constant Summary collapse

SUPPORTED_FILE_FORMATS =
%i[csv].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.available_modelsObject



219
220
221
# File 'app/models/spree/export.rb', line 219

def available_models
  available_types.map(&:model_class)
end

.available_typesObject



215
216
217
# File 'app/models/spree/export.rb', line 215

def available_types
  Spree.export_types
end

.model_classObject

eg. Spree::Exports::Products => Spree::Product

Raises:

  • (NameError)


228
229
230
231
232
233
234
# File 'app/models/spree/export.rb', line 228

def model_class
  klass = "Spree::#{to_s.demodulize.singularize}".safe_constantize

  raise NameError, "Missing model class for #{self}" unless klass

  klass
end

.type_for_model(model) ⇒ Object



223
224
225
# File 'app/models/spree/export.rb', line 223

def type_for_model(model)
  available_types.find { |type| type.model_class.to_s == model.to_s }
end

Instance Method Details

#build_csv_line(_record) ⇒ Object

Raises:

  • (NotImplementedError)


114
115
116
# File 'app/models/spree/export.rb', line 114

def build_csv_line(_record)
  raise NotImplementedError, 'build_csv_line must be implemented'
end

#csv_headersObject

Raises:

  • (NotImplementedError)


103
104
105
# File 'app/models/spree/export.rb', line 103

def csv_headers
  raise NotImplementedError, 'csv_headers must be implemented'
end

#current_abilityObject



195
196
197
# File 'app/models/spree/export.rb', line 195

def current_ability
  @current_ability ||= Spree.ability_class.new(user, { store: store })
end

#decode_prefixed_id_filters(params) ⇒ Object

Replace any prefixed IDs in ‘search_params` with their raw DB IDs so Ransack can match them. Without this, an admin filtering an export by a foreign key (`promotion_id_eq: ’promo_xxx’‘, `vendor_id_in: […]`) would always get zero rows. We only touch values that look like prefixed IDs — anything else (numeric IDs, code strings, ranges, state names) passes through untouched.



146
147
148
# File 'app/models/spree/export.rb', line 146

def decode_prefixed_id_filters(params)
  params.transform_values { |value| decode_search_value(value) }
end

#decode_search_value(value) ⇒ Object



150
151
152
153
154
155
156
157
158
159
# File 'app/models/spree/export.rb', line 150

def decode_search_value(value)
  case value
  when String
    Spree::PrefixedId.prefixed_id?(value) ? (Spree::PrefixedId.decode_prefixed_id(value) || value) : value
  when Array
    value.map { |v| decode_search_value(v) }
  else
    value
  end
end

#done?Boolean

Returns:

  • (Boolean)


68
69
70
# File 'app/models/spree/export.rb', line 68

def done?
  attachment.present? && attachment.attached?
end

#event_serializer_classObject



64
65
66
# File 'app/models/spree/export.rb', line 64

def event_serializer_class
  'Spree::Api::V3::ExportSerializer'.safe_constantize
end

#export_file_nameObject

eg. Spree::Exports::Products => products-store-my-store-code-20241030133348.csv



200
201
202
# File 'app/models/spree/export.rb', line 200

def export_file_name
  "#{type.demodulize.underscore}-#{store.code}-#{created_at.strftime('%Y%m%d%H%M%S')}.#{format}"
end

#export_tmp_file_pathObject



204
205
206
# File 'app/models/spree/export.rb', line 204

def export_tmp_file_path
  Rails.root.join('tmp', export_file_name)
end

#generateObject



76
77
78
79
80
# File 'app/models/spree/export.rb', line 76

def generate
  send(:"generate_#{format}")
  handle_attachment
  send_export_done_email
end

#generate_asyncObject



72
73
74
# File 'app/models/spree/export.rb', line 72

def generate_async
  Spree::Exports::GenerateJob.perform_later(id)
end

#generate_csvObject



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'app/models/spree/export.rb', line 82

def generate_csv
  ::CSV.open(export_tmp_file_path, 'wb', encoding: 'UTF-8', col_sep: ',', row_sep: "\r\n") do |csv|
    csv << csv_headers
    records_to_export.includes(scope_includes).find_in_batches do |batch|
      batch.each do |record|
        if multi_line_csv?
          record.to_csv(store).each do |line|
            csv << Spree::CSV::FormulaSanitizer.row(line)
          end
        else
          csv << Spree::CSV::FormulaSanitizer.row(record.to_csv(store))
        end
      end
    end
  end
end

#handle_attachmentObject



118
119
120
121
122
# File 'app/models/spree/export.rb', line 118

def handle_attachment
  file = ::File.open(export_tmp_file_path)
  attachment.attach(io: file, filename: export_file_name)
  ::File.delete(export_tmp_file_path) if ::File.exist?(export_tmp_file_path)
end

#metafields_headersArray<String>

Returns an array of metafield headers for the model

Returns:

  • (Array<String>)


110
111
112
# File 'app/models/spree/export.rb', line 110

def metafields_headers
  @metafields_headers ||= Spree::MetafieldDefinition.for_resource_type(model_class.to_s).order(:namespace, :key).map(&:csv_header_name)
end

#model_classObject

eg. Spree::Exports::Products => Spree::Product



166
167
168
169
170
171
172
# File 'app/models/spree/export.rb', line 166

def model_class
  if type == 'Spree::Exports::Customers'
    Spree.user_class
  else
    "Spree::#{type.demodulize.singularize}".constantize
  end
end

#multi_line_csv?Boolean

Returns:

  • (Boolean)


99
100
101
# File 'app/models/spree/export.rb', line 99

def multi_line_csv?
  false
end

#normalize_search_paramsObject

Ensures search params maintain consistent format between UI and exports

  • Preserves valid JSON unchanged

  • Converts Ruby hashes to JSON strings

  • Handles malformed input gracefully



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'app/models/spree/export.rb', line 178

def normalize_search_params
  return if search_params.blank?

  if search_params.is_a?(Hash)
    self.search_params = search_params.to_json
    return
  end

  begin
    # It's a string, so we parse and re-dump to ensure consistency
    parsed = JSON.parse(search_params.to_s)
    self.search_params = parsed.to_json
  rescue JSON::ParserError
    # Leave as-is if not valid JSON string
  end
end

#records_to_exportObject



131
132
133
134
135
136
137
138
# File 'app/models/spree/export.rb', line 131

def records_to_export
  if search_params.present?
    params = search_params.is_a?(String) ? JSON.parse(search_params.to_s).to_h : search_params
    scope.ransack(decode_prefixed_id_filters(params))
  else
    scope.ransack
  end.result
end

#scopeObject



124
125
126
127
128
129
# File 'app/models/spree/export.rb', line 124

def scope
  scope = model_class
  scope = scope.for_store(store) if model_class.respond_to?(:for_store)
  scope = scope.for_vendor(vendor) if model_class.respond_to?(:for_vendor) && vendor.present?
  scope.accessible_by(current_ability)
end

#scope_includesObject



161
162
163
# File 'app/models/spree/export.rb', line 161

def scope_includes
  []
end

#send_export_done_emailObject



208
209
210
211
212
# File 'app/models/spree/export.rb', line 208

def send_export_done_email
  return if user.blank? # App-created exports (secret API key) have no user to email.

  Spree::ExportMailer.export_done(self).deliver_later
end