spree_core 5.6.0 → 5.6.1

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/jobs/spree/imports/process_group_job.rb +45 -26
  3. data/app/jobs/spree/imports/process_rows_job.rb +10 -2
  4. data/app/models/concerns/spree/metafield_filterable.rb +162 -0
  5. data/app/models/concerns/spree/stores/setup.rb +6 -1
  6. data/app/models/spree/base.rb +35 -0
  7. data/app/models/spree/channel/gating.rb +4 -4
  8. data/app/models/spree/export.rb +14 -6
  9. data/app/models/spree/import.rb +92 -0
  10. data/app/models/spree/metafield.rb +11 -0
  11. data/app/models/spree/metafield_definition/search_capabilities.rb +104 -0
  12. data/app/models/spree/metafield_definition.rb +2 -1
  13. data/app/models/spree/metafields/long_text.rb +4 -0
  14. data/app/models/spree/metafields/number.rb +8 -0
  15. data/app/models/spree/metafields/short_text.rb +8 -0
  16. data/app/models/spree/product.rb +19 -1
  17. data/app/models/spree/report.rb +11 -1
  18. data/app/models/spree/search_provider/base.rb +5 -0
  19. data/app/models/spree/search_provider/database.rb +49 -3
  20. data/app/models/spree/search_provider/meilisearch.rb +63 -8
  21. data/app/presenters/spree/search_provider/product_presenter.rb +21 -1
  22. data/app/services/spree/imports/row_processors/product_variant.rb +6 -1
  23. data/app/services/spree/sample_data/helper.rb +55 -0
  24. data/app/services/spree/sample_data/import_builder.rb +32 -0
  25. data/app/services/spree/sample_data/import_runner.rb +23 -42
  26. data/app/services/spree/sample_data/loader.rb +6 -57
  27. data/app/services/spree/search_provider/metafield_schema.rb +144 -0
  28. data/app/services/spree/seeds/allowed_origins.rb +3 -4
  29. data/app/services/spree/seeds/states.rb +35 -16
  30. data/config/locales/en.yml +6 -0
  31. data/db/migrate/20260722000001_add_searchable_sortable_to_spree_metafield_definitions.rb +6 -0
  32. data/db/migrate/20260727000001_add_unique_country_abbr_index_to_spree_states.rb +30 -0
  33. data/lib/spree/core/preferences/preferable.rb +7 -1
  34. data/lib/spree/core/version.rb +1 -1
  35. data/lib/spree/permitted_attributes.rb +5 -1
  36. data/lib/spree/testing_support/factories/metafield_definition_factory.rb +10 -0
  37. data/lib/tasks/product_tag_tenants.rake +1 -3
  38. metadata +11 -4
@@ -2,6 +2,10 @@ module Spree
2
2
  module Metafields
3
3
  class LongText < Spree::Metafield
4
4
  normalizes :value, with: ->(value) { value.to_s.strip }
5
+
6
+ def self.searchable?
7
+ true
8
+ end
5
9
  end
6
10
  end
7
11
  end
@@ -3,6 +3,14 @@ module Spree
3
3
  class Number < Spree::Metafield
4
4
  validates :value, numericality: true
5
5
 
6
+ def self.searchable?
7
+ true
8
+ end
9
+
10
+ def self.sortable?
11
+ true
12
+ end
13
+
6
14
  def serialize_value
7
15
  value.to_d
8
16
  end
@@ -2,6 +2,14 @@ module Spree
2
2
  module Metafields
3
3
  class ShortText < Spree::Metafield
4
4
  normalizes :value, with: ->(value) { value.to_s.strip }
5
+
6
+ def self.searchable?
7
+ true
8
+ end
9
+
10
+ def self.sortable?
11
+ true
12
+ end
5
13
  end
6
14
  end
7
15
  end
@@ -31,7 +31,9 @@ module Spree
31
31
  include Spree::TranslatableResource
32
32
  include Spree::MemoizedData
33
33
  include Spree::Metafields
34
+ include Spree::MetafieldFilterable
34
35
  include Spree::Metadata
36
+ include Spree::Searchable
35
37
  include Spree::Product::Webhooks
36
38
  include Spree::Product::Slugs
37
39
  include Spree::Product::Channels
@@ -171,7 +173,23 @@ module Spree
171
173
  next none if query.blank?
172
174
 
173
175
  product_ids = Spree::Variant.search_by_product_name_or_sku(query).pluck(:product_id)
174
- where(id: product_ids.uniq.compact)
176
+
177
+ searchable_definitions = Spree::MetafieldDefinition.
178
+ for_resource_type('Spree::Product').
179
+ searchable
180
+
181
+ # Only pay for the metafield scan when some definition opts into search;
182
+ # the leading-wildcard LIKE over spree_metafields can't use an index.
183
+ metafield_ids = if searchable_definitions.exists?
184
+ joins(metafields: :metafield_definition).
185
+ merge(searchable_definitions).
186
+ where(search_condition(Spree::Metafield, :value, query)).
187
+ unscope(:order).distinct.pluck(:id)
188
+ else
189
+ []
190
+ end
191
+
192
+ where(id: (product_ids + metafield_ids).uniq.compact)
175
193
  }
176
194
 
177
195
  # Backward compatibility alias — remove in Spree 6.0
@@ -37,8 +37,18 @@ module Spree
37
37
  'Spree::Api::V3::ReportSerializer'.safe_constantize
38
38
  end
39
39
 
40
+ # Registered report subclasses, mirroring Spree::Export/Spree::Import so
41
+ # `api_type_for` can resolve an STI column value without constantizing it.
42
+ #
43
+ # @return [Array<Class>]
44
+ def self.available_types
45
+ Spree.reports
46
+ end
47
+
48
+ # @deprecated Use `.api_type` — identical derivation, consistent with the
49
+ # rest of the API's `type` fields.
40
50
  def self.report_type
41
- name.demodulize.underscore
51
+ api_type
42
52
  end
43
53
 
44
54
  # Returns a scope of records to be used for generating report lines
@@ -13,6 +13,11 @@ module Spree
13
13
  @store = store
14
14
  end
15
15
 
16
+ # @return [MetafieldSchema]
17
+ def metafield_schema
18
+ @metafield_schema ||= MetafieldSchema.new
19
+ end
20
+
16
21
  # Search and paginate products. Does NOT compute filter facets — use #filters for that.
17
22
  #
18
23
  # @param scope [ActiveRecord::Relation] base scope (store-scoped, visibility-filtered, authorized)
@@ -46,6 +46,7 @@ module Spree
46
46
  end
47
47
 
48
48
  filter_facets = build_facets(scope_with_options, category: category, option_value_ids: option_value_ids, scope_before_options: scope_before_options)
49
+ filter_facets[:sort_options] = filter_facets[:sort_options] + metafield_sort_options
49
50
 
50
51
  FiltersResult.new(
51
52
  filters: filter_facets[:filters],
@@ -60,6 +61,9 @@ module Spree
60
61
  def apply_search_and_filters(scope, query: nil, filters: {})
61
62
  scope = scope.search(query) if query.present?
62
63
 
64
+ filters = filters.to_unsafe_h if filters.respond_to?(:to_unsafe_h)
65
+ scope, filters = scope.with_metafield_filters(filters, schema: metafield_schema)
66
+
63
67
  ransack_filters = sanitize_filters(filters)
64
68
  if ransack_filters.present?
65
69
  search = scope.ransack(ransack_filters)
@@ -74,7 +78,7 @@ module Spree
74
78
  end
75
79
 
76
80
  def build_facets(scope, category: nil, option_value_ids: [], scope_before_options: nil)
77
- return { filters: [], sort_options: available_sort_options, default_sort: 'manual', total_count: scope.distinct.count } unless defined?(Spree::Api::V3::FiltersAggregator)
81
+ return { filters: [], sort_options: built_in_sort_options, default_sort: 'manual', total_count: scope.distinct.count } unless defined?(Spree::Api::V3::FiltersAggregator)
78
82
 
79
83
  Spree::Api::V3::FiltersAggregator.new(
80
84
  scope: scope,
@@ -98,6 +102,8 @@ module Spree
98
102
  scope_method = CUSTOM_SORT_SCOPES[sort]
99
103
  if scope_method
100
104
  scope.reorder(nil).send(scope_method)
105
+ elsif (parsed = metafield_schema.parse_sort(sort))
106
+ apply_metafield_sort(scope, parsed)
101
107
  else
102
108
  # Standard Ransack sort: 'name' → 'name asc', '-name' → 'name desc'
103
109
  ransack_sort = sort.split(',').map { |field|
@@ -112,8 +118,48 @@ module Spree
112
118
  end
113
119
  end
114
120
 
115
- def available_sort_options
116
- %w[price -price best_selling name -name -available_on available_on].map { |id| { id: id } }
121
+ # Project the ORDER BY expression into the SELECT list so PostgreSQL
122
+ # accepts it when DISTINCT is applied. Null-rank keeps products without
123
+ # the metafield last for both ASC and DESC.
124
+ def apply_metafield_sort(scope, parsed)
125
+ definition = metafield_schema.entry_for(parsed[:attribute])
126
+ return scope unless definition
127
+
128
+ products = Spree::Product.arel_table
129
+ # Aliased so this join can't collide with the `filter_cf` correlated
130
+ # subquery when a cf_* filter and a cf_* sort are combined.
131
+ metafields = Spree::Metafield.arel_table.alias('sort_cf')
132
+
133
+ sort_expr = Spree::Product.metafield_value_expression(metafields[:value], definition.field_type)
134
+ # NULL-rank so products missing the metafield sort last (Meilisearch parity).
135
+ # Projected as cf_sort_missing because PostgreSQL rejects (alias IS NULL) in ORDER BY.
136
+ null_rank = sort_expr.eq(nil)
137
+
138
+ direction = parsed[:direction] == 'desc' ? :desc : :asc
139
+
140
+ join = products.
141
+ outer_join(metafields).
142
+ on(
143
+ metafields[:resource_type].eq('Spree::Product').
144
+ and(metafields[:resource_id].eq(products[:id])).
145
+ and(metafields[:metafield_definition_id].eq(definition.id))
146
+ ).
147
+ join_sources
148
+
149
+ scope.
150
+ joins(join).
151
+ select(products[Arel.star]).
152
+ select(Arel::Nodes::As.new(sort_expr, Arel.sql('cf_sort_value'))).
153
+ select(Arel::Nodes::As.new(null_rank, Arel.sql('cf_sort_missing'))).
154
+ reorder(Arel.sql('cf_sort_missing').asc, sort_expr.public_send(direction))
155
+ end
156
+
157
+ def built_in_sort_options
158
+ %w[price -price best_selling name -name -available_on available_on].map { |id| { id: id, label: nil } }
159
+ end
160
+
161
+ def metafield_sort_options
162
+ metafield_schema.sort_options
117
163
  end
118
164
  end
119
165
  end
@@ -5,6 +5,8 @@ module Spree
5
5
  class Meilisearch < Base
6
6
  PREFIXED_ID_PATTERN = /\A[a-z]+_[A-Za-z0-9]+\z/
7
7
  ALLOWED_STATUSES = %w[active draft archived paused].freeze
8
+ BUILT_IN_FILTERABLE_ATTRIBUTES = %w[product_id status in_stock preorder store_ids channel_ids locale currency available_on discontinue_on price category_ids tags option_value_ids].freeze
9
+ METAFIELD_RANGE_OPERATORS = { 'gt' => '>', 'gteq' => '>=', 'lt' => '<', 'lteq' => '<=' }.freeze
8
10
 
9
11
  def self.indexing_required?
10
12
  true
@@ -51,7 +53,7 @@ module Spree
51
53
  unless ms_result
52
54
  return FiltersResult.new(
53
55
  filters: [],
54
- sort_options: available_sort_options.map { |id| { id: id } },
56
+ sort_options: built_in_and_metafield_sort_options,
55
57
  default_sort: 'manual',
56
58
  total_count: 0
57
59
  )
@@ -59,7 +61,7 @@ module Spree
59
61
 
60
62
  FiltersResult.new(
61
63
  filters: build_facet_response(facet_distribution),
62
- sort_options: available_sort_options.map { |id| { id: id } },
64
+ sort_options: built_in_and_metafield_sort_options,
63
65
  default_sort: 'manual',
64
66
  total_count: ms_result['estimatedTotalHits'] || 0
65
67
  )
@@ -91,6 +93,7 @@ module Spree
91
93
 
92
94
  def reindex(scope = nil)
93
95
  scope ||= store.products
96
+ @metafield_schema = MetafieldSchema.new
94
97
  ensure_index_settings!
95
98
 
96
99
  indexed = 0
@@ -180,7 +183,7 @@ module Spree
180
183
  # is referenced before the index settings were refreshed — otherwise
181
184
  # every search on an upgraded store returns empty until a reindex or
182
185
  # the next product write.
183
- if e.code == 'invalid_search_filter' && !settings_refreshed
186
+ if %w[invalid_search_filter invalid_search_sort].include?(e.code) && !settings_refreshed
184
187
  settings_refreshed = true
185
188
  ensure_index_settings!
186
189
  retry
@@ -240,23 +243,30 @@ module Spree
240
243
  end
241
244
 
242
245
  def searchable_attributes
243
- %w[name description sku option_values category_names tags]
246
+ %w[name description sku option_values category_names tags] + metafield_schema.searchable_attribute_keys
244
247
  end
245
248
 
246
249
  def filterable_attributes
247
- %w[product_id status in_stock preorder store_ids channel_ids locale currency available_on discontinue_on price category_ids tags option_value_ids]
250
+ BUILT_IN_FILTERABLE_ATTRIBUTES + metafield_schema.filterable_attribute_keys
248
251
  end
249
252
 
250
253
  def sortable_attributes
251
- %w[name price created_at available_on units_sold_count]
254
+ %w[name price created_at available_on units_sold_count] + metafield_schema.sortable_attribute_keys
252
255
  end
253
256
 
257
+ # Facets stay built-in only — cf_* distributions aren't consumed by
258
+ # +build_facet_response+, so requesting them would be wasted work.
254
259
  def facet_attributes
255
- filterable_attributes
260
+ BUILT_IN_FILTERABLE_ATTRIBUTES
256
261
  end
257
262
 
258
263
  def available_sort_options
259
- %w[price -price name -name -available_on available_on best_selling]
264
+ %w[price -price name -name -available_on available_on best_selling] + metafield_schema.sort_ids
265
+ end
266
+
267
+ def built_in_and_metafield_sort_options
268
+ %w[price -price name -name -available_on available_on best_selling].map { |id| { id: id, label: nil } } +
269
+ metafield_schema.sort_options
260
270
  end
261
271
 
262
272
  # Build Meilisearch filter conditions from API params.
@@ -331,9 +341,51 @@ module Spree
331
341
  when 'with_option_value_ids'
332
342
  # Handled by grouped option conditions in search_and_filter — skip here
333
343
  nil
344
+ else
345
+ metafield_filter_condition(key, value)
346
+ end
347
+ end
348
+
349
+ # Meilisearch has stable filter operators for equality, ranges, and
350
+ # EXISTS — but no string contains/starts/ends (CONTAINS is still
351
+ # experimental), so those predicates are ignored here. The Database
352
+ # provider supports the full predicate set.
353
+ def metafield_filter_condition(key, value)
354
+ parsed = metafield_schema.parse_filter(key)
355
+ return nil unless parsed
356
+
357
+ attribute = parsed[:definition].filter_key
358
+ numeric = parsed[:definition].field_type == 'number'
359
+
360
+ case parsed[:predicate]
361
+ when 'present'
362
+ "#{attribute} EXISTS"
363
+ when 'blank'
364
+ "#{attribute} NOT EXISTS"
365
+ when 'eq', 'not_eq'
366
+ operator = parsed[:predicate] == 'eq' ? '=' : '!='
367
+ formatted = format_metafield_filter_value(value, numeric)
368
+ formatted && "#{attribute} #{operator} #{formatted}"
369
+ when *METAFIELD_RANGE_OPERATORS.keys
370
+ return nil unless numeric
371
+
372
+ formatted = format_metafield_filter_value(value, numeric)
373
+ formatted && "#{attribute} #{METAFIELD_RANGE_OPERATORS[parsed[:predicate]]} #{formatted}"
334
374
  end
335
375
  end
336
376
 
377
+ def format_metafield_filter_value(value, numeric)
378
+ if numeric
379
+ Float(value.to_s, exception: false)&.to_s
380
+ else
381
+ "'#{escape_filter_string(value)}'"
382
+ end
383
+ end
384
+
385
+ def escape_filter_string(value)
386
+ value.to_s.gsub('\\') { '\\\\' }.gsub("'") { "\\'" }
387
+ end
388
+
337
389
  # Group prefixed option value IDs by option type (single DB query).
338
390
  # Returns { option_type_id => ['optval_abc', 'optval_def'], ... }
339
391
  def group_option_values_by_type(prefixed_ids)
@@ -380,6 +432,9 @@ module Spree
380
432
  when '-available_on' then ['available_on:desc']
381
433
  when 'available_on' then ['available_on:asc']
382
434
  when 'best_selling' then ['units_sold_count:desc']
435
+ else
436
+ parsed = metafield_schema.parse_sort(sort)
437
+ parsed && ["#{parsed[:attribute]}:#{parsed[:direction]}"]
383
438
  end
384
439
  end
385
440
 
@@ -75,7 +75,27 @@ module Spree
75
75
  available_on: product.available_on&.iso8601,
76
76
  created_at: product.created_at&.iso8601,
77
77
  updated_at: product.updated_at&.iso8601
78
- }
78
+ }.merge(metafield_document_attributes)
79
+ end
80
+
81
+ # Flat cf_* hash for searchable ∪ sortable metafields (Meilisearch docs).
82
+ def metafield_document_attributes
83
+ @metafield_document_attributes ||= product.metafields.filter_map do |metafield|
84
+ definition = metafield.metafield_definition
85
+ next unless definition&.searchable? || definition&.sortable?
86
+
87
+ [definition.filter_key, metafield_index_value(metafield, definition.field_type)]
88
+ end.to_h
89
+ end
90
+
91
+ def metafield_index_value(metafield, field_type)
92
+ serialized = metafield.serialize_value
93
+ case field_type
94
+ when 'number'
95
+ serialized.to_f
96
+ else
97
+ serialized.to_s
98
+ end
79
99
  end
80
100
 
81
101
  # Returns all market × locale pairs for this store
@@ -295,11 +295,16 @@ module Spree
295
295
  end
296
296
 
297
297
  def handle_tags(product)
298
+ return Spree::Imports::AssignTagsJob.perform_now(product.id, attributes['tags']) if import.preferred_inline
299
+
298
300
  Spree::Imports::AssignTagsJob.perform_later(product.id, attributes['tags'])
299
301
  end
300
302
 
301
303
  def handle_categories(product)
302
- Spree::Imports::CreateCategoriesJob.perform_later(product.id, store.id, prepare_taxon_pretty_names)
304
+ names = prepare_taxon_pretty_names
305
+ return Spree::Imports::CreateCategoriesJob.perform_now(product.id, store.id, names) if import.preferred_inline
306
+
307
+ Spree::Imports::CreateCategoriesJob.perform_later(product.id, store.id, names)
303
308
  end
304
309
 
305
310
  def prepare_taxon_pretty_names
@@ -0,0 +1,55 @@
1
+ module Spree
2
+ module SampleData
3
+ # Shared by the sample-data drivers: `Spree::SampleData::Loader` (the
4
+ # synchronous, rake-facing one) and any app that loads a subset itself —
5
+ # e.g. running the configuration files inline and handing the CSVs to the
6
+ # import pipeline. Keeping the file locations and catalog-publishing rules
7
+ # here means they don't have to be kept in sync by hand.
8
+ module Helper
9
+ private
10
+
11
+ def sample_data_path
12
+ @sample_data_path ||= Spree::Core::Engine.root.join('db', 'sample_data')
13
+ end
14
+
15
+ def load_ruby_file(name)
16
+ file = sample_data_path.join("#{name}.rb")
17
+ load file.to_s if file.exist?
18
+ end
19
+
20
+ # Publishes the catalog to every channel the store has. The import only
21
+ # adds a product to the default channel, and only when it has none, so
22
+ # anything else the store defines (wholesale, a locale-specific storefront,
23
+ # whatever an app has seeded) would otherwise be left empty. Catalog parity
24
+ # is the point — channels differentiate through their own gates and price
25
+ # lists, not through a narrower catalog. `add_products` upserts in bulk and
26
+ # skips rows that already exist.
27
+ def publish_sample_products(store)
28
+ product_ids = store.product_ids
29
+ return if product_ids.empty?
30
+
31
+ store.channels.each { |channel| channel.add_products(product_ids) }
32
+ end
33
+
34
+ # Cheap proxy for "the base seeds have run": the US states are seeded
35
+ # alongside everything else, and products/addresses depend on them.
36
+ def seeds_loaded?
37
+ Spree::State.joins(:country).where(spree_countries: { iso: 'US' }).exists?
38
+ end
39
+
40
+ def ensure_seeds_loaded
41
+ return if seeds_loaded?
42
+
43
+ Spree::Seeds::All.call
44
+ end
45
+
46
+ def without_geocoding
47
+ previous = Spree::Config[:geocode_addresses]
48
+ Spree::Config[:geocode_addresses] = false
49
+ yield
50
+ ensure
51
+ Spree::Config[:geocode_addresses] = previous
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,32 @@
1
+ module Spree
2
+ module SampleData
3
+ # Creates the `Spree::Import` record for a sample CSV, attaching the file
4
+ # but processing nothing — the two runners differ only in what they do next.
5
+ class ImportBuilder
6
+ # @return [Spree::Import] persisted, unprocessed
7
+ def self.call(csv_path:, import_class:, store: nil, user: nil, inline: false, skip_events: false)
8
+ store ||= Spree::Store.default
9
+ # An admin of the target store, not `admin_user_class.first` — in a
10
+ # multi-store or multi-tenant app that global first can belong to a
11
+ # different store entirely.
12
+ user ||= store.users.first
13
+
14
+ raise 'No admin user found. Please run seeds first.' unless user
15
+
16
+ import = import_class.new(owner: store, user: user)
17
+ # Persisted before save: the processing jobs reload the record, so these
18
+ # have to travel with it rather than live on the instance.
19
+ import.preferred_inline = inline
20
+ import.preferred_skip_events = skip_events
21
+ import.number = import.generate_permalink(import_class)
22
+ import.attachment.attach(
23
+ io: File.open(csv_path),
24
+ filename: File.basename(csv_path),
25
+ content_type: 'text/csv'
26
+ )
27
+ import.save!(validate: false)
28
+ import
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,53 +1,34 @@
1
- require 'csv'
2
-
3
1
  module Spree
4
2
  module SampleData
3
+ # Runs a sample CSV through the regular import pipeline: `start_mapping`
4
+ # auto-assigns the column mappings from the CSV headers (the sample files
5
+ # use the canonical schema names, so every column maps) and
6
+ # `complete_mapping` kicks off row creation and processing.
7
+ #
8
+ # Enqueues by default, so row creation, batching and the per-import
9
+ # concurrency cap behave exactly as they do for a merchant-uploaded CSV.
10
+ # Pass `inline: true` for rake tasks, seeds and the console — where the data
11
+ # has to exist when the call returns and there may be no worker attached.
12
+ # Never inline from a request: `products.csv` alone is 211 rows, each
13
+ # downloading a remote image.
5
14
  class ImportRunner
6
15
  prepend Spree::ServiceModule::Base
7
16
 
8
- def call(csv_path:, import_class:)
9
- store = Spree::Store.default
10
- admin = Spree.admin_user_class.first
11
-
12
- raise 'No admin user found. Please run seeds first.' unless admin
13
-
14
- import = import_class.new(
15
- owner: store,
16
- user: admin
17
- )
18
- import.number = import.generate_permalink(import_class)
19
- import.attachment.attach(
20
- io: File.open(csv_path),
21
- filename: File.basename(csv_path),
22
- content_type: 'text/csv'
17
+ # Pass `skip_events: true` when seeding demo data — subscribers would
18
+ # otherwise deliver webhooks and analytics for a catalog nobody created.
19
+ #
20
+ # @return [Spree::ServiceModule::Result] wrapping the import — completed
21
+ # when inline, queued otherwise
22
+ def call(csv_path:, import_class:, store: nil, user: nil, inline: false, skip_events: false)
23
+ import = Spree::SampleData::ImportBuilder.call(
24
+ csv_path: csv_path, import_class: import_class, store: store, user: user,
25
+ inline: inline, skip_events: skip_events
23
26
  )
24
- import.save!(validate: false)
25
- import.update_columns(status: 'processing')
26
- import.create_mappings
27
-
28
- row_number = 0
29
- failed = 0
30
-
31
- ::CSV.foreach(csv_path, headers: true) do |csv_row|
32
- row_number += 1
33
- import_row = import.rows.create!(
34
- row_number: row_number,
35
- data: csv_row.to_h.to_json,
36
- status: 'pending'
37
- )
38
-
39
- begin
40
- import_row.process!
41
- rescue StandardError => e
42
- failed += 1
43
- puts "\n Warning: Row #{row_number} failed: #{e.message}"
44
- end
45
27
 
46
- print '.' if (row_number % 10).zero?
47
- end
28
+ import.start_mapping
29
+ import.complete_mapping
48
30
 
49
- import.update!(status: 'completed')
50
- puts "\n Processed #{row_number} rows (#{failed} failed)"
31
+ success(import.reload)
51
32
  end
52
33
  end
53
34
  end
@@ -2,11 +2,13 @@ module Spree
2
2
  module SampleData
3
3
  class Loader
4
4
  prepend Spree::ServiceModule::Base
5
+ include Spree::SampleData::Helper
5
6
 
6
7
  def call
7
8
  Spree::Events.disable do
8
9
  without_geocoding do
9
10
  ActiveRecord::Base.no_touching do
11
+ puts 'Running seeds first...' unless seeds_loaded?
10
12
  ensure_seeds_loaded
11
13
 
12
14
  puts 'Loading sample configuration data...'
@@ -28,10 +30,7 @@ module Spree
28
30
  load_products
29
31
 
30
32
  puts 'Publishing sample products on the default channel...'
31
- publish_sample_products
32
-
33
- puts 'Loading sample categories...'
34
- load_categories
33
+ publish_sample_products(Spree::Store.default)
35
34
 
36
35
  puts 'Loading sample product translations...'
37
36
  load_product_translations
@@ -56,18 +55,6 @@ module Spree
56
55
 
57
56
  private
58
57
 
59
- def ensure_seeds_loaded
60
- us = Spree::Country.find_by(iso: 'US')
61
- return if us&.states&.any? && Spree::Store.default&.persisted?
62
-
63
- puts 'Running seeds first...'
64
- Spree::Seeds::All.call
65
- end
66
-
67
- def sample_data_path
68
- @sample_data_path ||= Spree::Core::Engine.root.join('db', 'sample_data')
69
- end
70
-
71
58
  def load_configuration_data
72
59
  load_ruby_file('shipping_methods')
73
60
  load_ruby_file('payment_methods')
@@ -76,57 +63,19 @@ module Spree
76
63
 
77
64
  def load_products
78
65
  csv_path = sample_data_path.join('products.csv')
79
- Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Products)
80
- end
81
-
82
- # Publishes the catalog to both surfaces: the public default channel and
83
- # the gated wholesale channel (catalog parity; wholesale differentiation
84
- # comes from the price list in +wholesale.rb+ and the channel's gate).
85
- def publish_sample_products
86
- store = Spree::Store.default
87
- store.default_channel.add_products(store.product_ids)
88
- store.channels.find_by(code: Spree::Seeds::Channels::WHOLESALE_CODE)&.add_products(store.product_ids)
89
- end
90
-
91
- def load_categories
92
- store = Spree::Store.default
93
- csv_path = sample_data_path.join('products.csv')
94
-
95
- require 'csv'
96
- ::CSV.foreach(csv_path, headers: true) do |row|
97
- product = store.products.find_by(slug: row['slug'])
98
- next unless product
99
-
100
- categories = [row['category1'], row['category2'], row['category3']].compact_blank
101
- next if categories.empty?
102
-
103
- Spree::Imports::CreateCategoriesJob.perform_now(product.id, store.id, categories)
104
- end
66
+ Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Products, inline: true)
105
67
  end
106
68
 
107
69
  def load_product_translations
108
70
  csv_path = sample_data_path.join('product_translations.csv')
109
71
  return unless csv_path.exist?
110
72
 
111
- Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::ProductTranslations)
73
+ Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::ProductTranslations, inline: true)
112
74
  end
113
75
 
114
76
  def load_customers
115
77
  csv_path = sample_data_path.join('customers.csv')
116
- Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Customers)
117
- end
118
-
119
- def load_ruby_file(name)
120
- file = sample_data_path.join("#{name}.rb")
121
- load file.to_s if file.exist?
122
- end
123
-
124
- def without_geocoding
125
- previous = Spree::Config[:geocode_addresses]
126
- Spree::Config[:geocode_addresses] = false
127
- yield
128
- ensure
129
- Spree::Config[:geocode_addresses] = previous
78
+ Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Customers, inline: true)
130
79
  end
131
80
  end
132
81
  end