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



253
254
255
# File 'app/models/spree/export.rb', line 253

def available_models
  available_types.map(&:model_class)
end

.available_typesObject



234
235
236
# File 'app/models/spree/export.rb', line 234

def available_types
  Spree.export_types
end

.model_classObject

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

Raises:

  • (NameError)


262
263
264
265
266
267
268
# File 'app/models/spree/export.rb', line 262

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

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

  klass
end

.required_scopeSymbol?

Admin API scope family gating this export type — an export is a bulk read, so an API key needs read_<required_scope> to create, view, and download it. Derived from the class name (Spree::Exports::Customers => :customers); override in subclasses whose records are gated by a different scope (e.g. coupon codes => :promotions). Returns nil on the base class, so unmapped types are only accessible to read_all/write_all keys.

Returns:

  • (Symbol, nil)


247
248
249
250
251
# File 'app/models/spree/export.rb', line 247

def required_scope
  return nil if self == Spree::Export

  to_s.demodulize.underscore.to_sym
end

.type_for_model(model) ⇒ Object



257
258
259
# File 'app/models/spree/export.rb', line 257

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)


123
124
125
# File 'app/models/spree/export.rb', line 123

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

#csv_headersObject

Raises:

  • (NotImplementedError)


112
113
114
# File 'app/models/spree/export.rb', line 112

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

#current_abilityObject



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

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.



155
156
157
# File 'app/models/spree/export.rb', line 155

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

#decode_search_value(value) ⇒ Object



159
160
161
162
163
164
165
166
167
168
# File 'app/models/spree/export.rb', line 159

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)


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

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

#event_serializer_classObject



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

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



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

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

#export_tmp_file_pathObject



213
214
215
# File 'app/models/spree/export.rb', line 213

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

#generateObject



85
86
87
88
89
# File 'app/models/spree/export.rb', line 85

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

#generate_asyncObject



81
82
83
# File 'app/models/spree/export.rb', line 81

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

#generate_csvObject



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'app/models/spree/export.rb', line 91

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



127
128
129
130
131
# File 'app/models/spree/export.rb', line 127

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>)


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

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



175
176
177
178
179
180
181
# File 'app/models/spree/export.rb', line 175

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)


108
109
110
# File 'app/models/spree/export.rb', line 108

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


187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'app/models/spree/export.rb', line 187

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



140
141
142
143
144
145
146
147
# File 'app/models/spree/export.rb', line 140

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

#results_urlObject

Public API name for the results_url preference (read/write symmetry). String preferences round-trip nil as "" — normalize blank to nil.



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

def results_url
  preferred_results_url.presence
end

#results_url=(value) ⇒ Object



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

def results_url=(value)
  self.preferred_results_url = value
end

#scopeObject



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

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



170
171
172
# File 'app/models/spree/export.rb', line 170

def scope_includes
  []
end

#send_export_done_emailObject



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

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