spree_legacy_product_properties 1.0.0

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +20 -0
  3. data/README.md +59 -0
  4. data/Rakefile +23 -0
  5. data/app/controllers/spree/admin/products_controller_decorator.rb +26 -0
  6. data/app/controllers/spree/admin/properties_controller.rb +26 -0
  7. data/app/finders/spree/product_properties/find_available.rb +20 -0
  8. data/app/finders/spree/products/find_decorator.rb +47 -0
  9. data/app/helpers/spree/admin/properties_helper.rb +9 -0
  10. data/app/models/concerns/spree/filter_param.rb +21 -0
  11. data/app/models/spree/permission_sets/property_management.rb +10 -0
  12. data/app/models/spree/product_decorator.rb +107 -0
  13. data/app/models/spree/product_property.rb +51 -0
  14. data/app/models/spree/products/duplicator_decorator.rb +29 -0
  15. data/app/models/spree/property.rb +86 -0
  16. data/app/models/spree/property_prototype.rb +9 -0
  17. data/app/models/spree/prototype_decorator.rb +10 -0
  18. data/app/presenters/spree/filters/properties_presenter.rb +23 -0
  19. data/app/presenters/spree/filters/property_presenter.rb +42 -0
  20. data/app/services/spree/products/prepare_nested_attributes_decorator.rb +22 -0
  21. data/app/views/spree/admin/products/form/_properties.html.erb +48 -0
  22. data/app/views/spree/admin/properties/_filters.html.erb +5 -0
  23. data/app/views/spree/admin/properties/_form.html.erb +8 -0
  24. data/app/views/spree/admin/properties/_table_header.html.erb +9 -0
  25. data/app/views/spree/admin/properties/_table_row.html.erb +25 -0
  26. data/app/views/spree/admin/properties/edit.html.erb +2 -0
  27. data/app/views/spree/admin/properties/index.html.erb +22 -0
  28. data/app/views/spree/admin/properties/new.html.erb +1 -0
  29. data/app/views/spree/admin/properties/update.turbo_stream.erb +1 -0
  30. data/config/initializers/spree.rb +62 -0
  31. data/config/routes.rb +5 -0
  32. data/db/migrate/20221229000000_create_spree_properties_and_product_properties.rb +113 -0
  33. data/lib/generators/spree_legacy_product_properties/install/install_generator.rb +20 -0
  34. data/lib/spree_legacy_product_properties/configuration.rb +13 -0
  35. data/lib/spree_legacy_product_properties/engine.rb +23 -0
  36. data/lib/spree_legacy_product_properties/factories.rb +34 -0
  37. data/lib/spree_legacy_product_properties/version.rb +7 -0
  38. data/lib/spree_legacy_product_properties.rb +12 -0
  39. metadata +120 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 52e57ea0de8690904e22e45a92e2fdc608723cc4583f47d3e5f2bdb004e9d139
4
+ data.tar.gz: 6cb6cb7f5d729ced258a185ca106fa0b878c1e1c108908ef40caf1202fcd8d54
5
+ SHA512:
6
+ metadata.gz: 84b05d1d995acbb3ab9d78b71ffb49130490798f162788b04bfd91c95ea9b94ec71c0477e3d43b8aa0bf9c0e8815b90ffed06fa27c58987ac2d9491b10c777de
7
+ data.tar.gz: 9822db1173331febc6152cf567b61513fb0eadfb08a7a05240a31efc8ea9fed423fd8f5bfb37d8e2a9b1cef58e3a5abb66dc6418572bbdcca875818992581082
data/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2015-2025 Vendo Connect Inc., Vendo Sp. z o.o.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Spree Legacy product properties
2
+
3
+ This is a Legacy product properties extension for [Spree Commerce](https://spreecommerce.org), an open source e-commerce platform built with Ruby on Rails.
4
+
5
+ ## Installation
6
+
7
+ 1. Add this extension to your Gemfile with this line:
8
+
9
+ ```ruby
10
+ bundle add spree_legacy_product_properties
11
+ ```
12
+
13
+ 2. Run the install generator
14
+
15
+ ```ruby
16
+ bundle exec rails g spree_legacy_product_properties:install
17
+ ```
18
+
19
+ 3. Restart your server
20
+
21
+ If your server was running, restart it so that it can find the assets properly.
22
+
23
+ ## Developing
24
+
25
+ 1. Create a dummy app
26
+
27
+ ```bash
28
+ bundle update
29
+ bundle exec rake test_app
30
+ ```
31
+
32
+ 2. Add your new code
33
+ 3. Run tests
34
+
35
+ ```bash
36
+ bundle exec rspec
37
+ ```
38
+
39
+ When testing your applications integration with this extension you may use it's factories.
40
+ Simply add this require statement to your spec_helper:
41
+
42
+ ```ruby
43
+ require 'spree_legacy_product_properties/factories'
44
+ ```
45
+
46
+ ## Releasing a new version
47
+
48
+ ```shell
49
+ bundle exec gem bump -p -t
50
+ bundle exec gem release
51
+ ```
52
+
53
+ For more options please see [gem-release README](https://github.com/svenfuchs/gem-release)
54
+
55
+ ## Contributing
56
+
57
+ If you'd like to contribute, please take a look at the
58
+ [instructions](CONTRIBUTING.md) for installing dependencies and crafting a good
59
+ pull request.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ require 'spree/testing_support/extension_rake'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default do
10
+ if Dir['spec/dummy'].empty?
11
+ Rake::Task[:test_app].invoke
12
+ Dir.chdir('../../')
13
+ end
14
+ Rake::Task[:spec].invoke
15
+ end
16
+
17
+ desc 'Generates a dummy app for testing'
18
+ task :test_app do
19
+ ENV['LIB_NAME'] = 'spree_legacy_product_properties'
20
+ Rake::Task['extension:test_app'].execute(
21
+ install_admin: true
22
+ )
23
+ end
@@ -0,0 +1,26 @@
1
+ module Spree
2
+ module Admin
3
+ module ProductsControllerDecorator
4
+ def self.prepended(base)
5
+ base.new_action.before :build_product_properties
6
+ base.edit_action.before :build_product_properties
7
+ end
8
+
9
+ private
10
+
11
+ def build_product_properties
12
+ return unless Spree::Config[:product_properties_enabled]
13
+
14
+ Spree::Property.all.each do |property|
15
+ @product.product_properties.build(property: property) unless @product.product_properties.find do |product_property|
16
+ product_property.property_id == property.id
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ if defined?(Spree::Admin::ProductsController)
25
+ Spree::Admin::ProductsController.prepend(Spree::Admin::ProductsControllerDecorator)
26
+ end
@@ -0,0 +1,26 @@
1
+ module Spree
2
+ module Admin
3
+ class PropertiesController < ResourceController
4
+ include ProductsBreadcrumbConcern
5
+ add_breadcrumb Spree.t(:properties), :admin_properties_path
6
+
7
+ before_action :add_breadcrumbs
8
+
9
+ protected
10
+
11
+ def update_turbo_stream_enabled?
12
+ true
13
+ end
14
+
15
+ def add_breadcrumbs
16
+ if @property.present? && @property.persisted?
17
+ add_breadcrumb @property.presentation, spree.edit_admin_property_path(@property)
18
+ end
19
+ end
20
+
21
+ def permitted_resource_params
22
+ params.require(:property).permit(:name, :presentation, :position, :kind, :display_on)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ module Spree
2
+ module ProductProperties
3
+ class FindAvailable
4
+ include ProductFilterable
5
+
6
+ def initialize(scope: ProductProperty.spree_base_scopes, products_scope: Product.spree_base_scopes)
7
+ @scope = scope
8
+ @products_scope = products_scope
9
+ end
10
+
11
+ def execute
12
+ find_available(scope, products_scope).includes(:translations)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :scope, :products_scope
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,47 @@
1
+ module Spree
2
+ module Products
3
+ module FindDecorator
4
+ def initialize(scope:, params:)
5
+ @properties = params.dig(:filter, :properties)
6
+ super
7
+ end
8
+
9
+ def execute
10
+ products = super
11
+ # Apply property filtering after all other filters
12
+ by_properties(products)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :properties
18
+
19
+ def properties?
20
+ properties.present? && properties.values.reject(&:empty?).present?
21
+ end
22
+
23
+ def by_properties(products)
24
+ return products unless properties?
25
+
26
+ product_ids = []
27
+ index = 0
28
+
29
+ properties.to_unsafe_hash.each do |property_filter_param, product_properties_values|
30
+ next if property_filter_param.blank? || product_properties_values.empty?
31
+
32
+ values = product_properties_values.split(',').reject(&:empty?).uniq.map(&:parameterize)
33
+
34
+ next if values.empty?
35
+
36
+ ids = scope.unscope(:order, :includes).with_property_values(property_filter_param, values).ids
37
+ product_ids = index == 0 ? ids : product_ids & ids
38
+ index += 1
39
+ end
40
+
41
+ products.where(id: product_ids)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ Spree::Products::Find.prepend(Spree::Products::FindDecorator)
@@ -0,0 +1,9 @@
1
+ module Spree
2
+ module Admin
3
+ module PropertiesHelper
4
+ def sorted_product_properties(product)
5
+ product.product_properties.sort_by_property_position
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module FilterParam
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_save :set_filter_param
7
+ end
8
+
9
+ protected
10
+
11
+ def set_filter_param
12
+ return if param_candidate.blank?
13
+
14
+ self.filter_param = param_candidate.parameterize
15
+ end
16
+
17
+ def param_candidate
18
+ name
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ module Spree
2
+ module PermissionSets
3
+ class PropertyManagement < PermissionSets::Base
4
+ def activate!
5
+ can :manage, Spree::Property
6
+ can :manage, Spree::ProductProperty
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,107 @@
1
+ module Spree
2
+ module ProductDecorator
3
+ def self.prepended(base)
4
+ base.has_many :product_properties, dependent: :destroy, inverse_of: :product
5
+ base.has_many :properties, through: :product_properties
6
+
7
+ base.accepts_nested_attributes_for :product_properties, allow_destroy: true, reject_if: lambda { |pp|
8
+ pp[:property_id].blank? || (pp[:id].blank? && pp[:value].blank?)
9
+ }
10
+
11
+ base.whitelisted_ransackable_associations |= %w[properties]
12
+
13
+ # Property scopes
14
+ base.add_search_scope :with_property do |property|
15
+ joins(:properties).where(Spree::Product.property_conditions(property))
16
+ end
17
+
18
+ base.add_search_scope :with_property_value do |property, value|
19
+ if Spree.use_translations?
20
+ joins(:properties).
21
+ join_translation_table(Spree::Property).
22
+ join_translation_table(Spree::ProductProperty).
23
+ where(Spree::ProductProperty.translation_table_alias => { value: value }).
24
+ where(Spree::Product.property_conditions(property))
25
+ else
26
+ joins(:properties).
27
+ where(Spree::ProductProperty.table_name => { value: value }).
28
+ where(Spree::Product.property_conditions(property))
29
+ end
30
+ end
31
+
32
+ base.add_search_scope :with_property_values do |property_filter_param, property_values|
33
+ joins(product_properties: :property).
34
+ where(Spree::Property.table_name => { filter_param: property_filter_param }).
35
+ where(Spree::ProductProperty.table_name => { filter_param: property_values.map(&:parameterize) })
36
+ end
37
+ end
38
+
39
+ def self.property_conditions(property)
40
+ properties_table = Spree::Property.table_name
41
+
42
+ case property
43
+ when Spree::Property then { "#{properties_table}.id" => property.id }
44
+ when Integer then { "#{properties_table}.id" => property }
45
+ else
46
+ if Spree::Property.column_for_attribute('id').type == :uuid
47
+ ["#{properties_table}.name = ? OR #{properties_table}.id = ?", property, property]
48
+ else
49
+ { "#{properties_table}.name" => property }
50
+ end
51
+ end
52
+ end
53
+
54
+ def property(property_name)
55
+ if product_properties.loaded?
56
+ product_properties.detect { |pp| pp.property.name == property_name }.try(:value)
57
+ else
58
+ product_properties.joins(:property).find_by(spree_properties: { name: property_name }).try(:value)
59
+ end
60
+ end
61
+
62
+ def set_property(property_name, property_value, property_presentation = property_name)
63
+ property_name = property_name.to_s.parameterize
64
+ ApplicationRecord.transaction do
65
+ prop = if Spree::Property.where(name: property_name).exists?
66
+ existing_property = Spree::Property.where(name: property_name).first
67
+ existing_property.presentation ||= property_presentation
68
+ existing_property.save
69
+ existing_property
70
+ else
71
+ Spree::Property.create(name: property_name, presentation: property_presentation)
72
+ end
73
+
74
+ product_property = if Spree::ProductProperty.where(product: self, property: prop).exists?
75
+ Spree::ProductProperty.where(product: self, property: prop).first
76
+ else
77
+ Spree::ProductProperty.new(product: self, property: prop)
78
+ end
79
+
80
+ product_property.value = property_value
81
+ product_property.save!
82
+ end
83
+ end
84
+
85
+ def remove_property(property_name)
86
+ product_properties.joins(:property).find_by(spree_properties: { name: property_name.parameterize })&.destroy
87
+ end
88
+
89
+ def storefront_description
90
+ property('short_description') || description
91
+ end
92
+
93
+ private
94
+
95
+ def add_associations_from_prototype
96
+ if prototype_id && (prototype = Spree::Prototype.find_by(id: prototype_id))
97
+ prototype.properties.each do |prop|
98
+ product_properties.create(property: prop, value: 'Placeholder')
99
+ end
100
+ self.option_types = prototype.option_types
101
+ self.taxons = prototype.taxons
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ Spree::Product.prepend(Spree::ProductDecorator)
@@ -0,0 +1,51 @@
1
+ module Spree
2
+ class ProductProperty < Spree.base_class
3
+ include Spree::FilterParam
4
+ include Spree::TranslatableResource
5
+
6
+ if Spree.always_use_translations?
7
+ TRANSLATABLE_FIELDS = %i[value filter_param].freeze
8
+ translates(*TRANSLATABLE_FIELDS)
9
+ else
10
+ TRANSLATABLE_FIELDS = %i[value].freeze
11
+ translates(*TRANSLATABLE_FIELDS, column_fallback: true)
12
+ end
13
+
14
+ self::Translation.class_eval do
15
+ normalizes :value, with: ->(value) { value&.to_s&.squish&.presence }
16
+ end
17
+
18
+ normalizes :value, with: ->(value) { value&.to_s&.squish&.presence }
19
+
20
+ acts_as_list scope: :product
21
+
22
+ with_options inverse_of: :product_properties do
23
+ belongs_to :product, touch: true, class_name: 'Spree::Product'
24
+ belongs_to :property, touch: true, class_name: 'Spree::Property'
25
+ end
26
+
27
+ validates :property, presence: true
28
+ validates :property_id, uniqueness: { scope: :product_id }
29
+ validates :value, presence: true
30
+
31
+ default_scope { order(:position) }
32
+
33
+ scope :filterable, -> { joins(:property).where(Property.table_name => { filterable: true }) }
34
+ scope :for_products, ->(products) { where(product_id: products) }
35
+ scope :sort_by_property_position, -> {
36
+ unscope(:order).joins(:property).order(Spree::Property.table_name => { position: :asc })
37
+ }
38
+
39
+ self.whitelisted_ransackable_attributes = ['value', 'filter_param']
40
+ self.whitelisted_ransackable_associations = ['property']
41
+
42
+ # virtual attributes for use with AJAX completion stuff
43
+ delegate :name, :presentation, to: :property, prefix: true, allow_nil: true
44
+
45
+ protected
46
+
47
+ def param_candidate
48
+ value
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ module Spree
2
+ module Products
3
+ module DuplicatorDecorator
4
+ def call(product:, include_images: true)
5
+ result = super
6
+ return result unless result.success?
7
+
8
+ new_product = result.value
9
+ new_product.product_properties = duplicate_properties(product.product_properties) if new_product.persisted?
10
+
11
+ result
12
+ end
13
+
14
+ protected
15
+
16
+ def duplicate_properties(product_properties)
17
+ product_properties.map do |prop|
18
+ new_prop = prop.dup
19
+ new_prop.product = nil
20
+ new_prop.created_at = nil
21
+ new_prop.updated_at = nil
22
+ new_prop
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ Spree::Products::Duplicator.prepend(Spree::Products::DuplicatorDecorator)
@@ -0,0 +1,86 @@
1
+ module Spree
2
+ class Property < Spree.base_class
3
+ include Spree::FilterParam
4
+ include Spree::Metadata
5
+ include Spree::ParameterizableName
6
+ include Spree::UniqueName
7
+ include Spree::DisplayOn
8
+ include Spree::TranslatableResource
9
+
10
+ TRANSLATABLE_FIELDS = %i[presentation].freeze
11
+ translates(*TRANSLATABLE_FIELDS, column_fallback: !Spree.always_use_translations?)
12
+
13
+ self::Translation.class_eval do
14
+ normalizes :presentation, with: ->(value) { value&.to_s&.squish&.presence }
15
+ end
16
+
17
+ acts_as_list
18
+
19
+ has_many :property_prototypes, class_name: 'Spree::PropertyPrototype'
20
+ has_many :prototypes, through: :property_prototypes, class_name: 'Spree::Prototype'
21
+
22
+ has_many :product_properties, dependent: :delete_all, inverse_of: :property
23
+ has_many :products, through: :product_properties
24
+
25
+ validates :name, :presentation, presence: true
26
+
27
+ default_scope { order(:position) }
28
+ scope :sorted, -> { order(:name) }
29
+ scope :filterable, -> { where(filterable: true) }
30
+
31
+ KIND_OPTIONS = { short_text: 0, long_text: 1, number: 2, rich_text: 3 }.freeze
32
+ enum :kind, KIND_OPTIONS
33
+
34
+ DEPENDENCY_UPDATE_FIELDS = [:presentation, :name, :kind, :filterable, :display_on, :position].freeze
35
+
36
+ after_touch :touch_all_products
37
+ after_update :touch_all_products, if: -> { DEPENDENCY_UPDATE_FIELDS.any? { |field| saved_changes.key?(field) } }
38
+ after_save :ensure_product_properties_have_filter_params
39
+
40
+ self.whitelisted_ransackable_attributes = ['presentation', 'filterable']
41
+
42
+ def uniq_values(product_properties_scope: nil)
43
+ with_uniq_values_cache_key(product_properties_scope) do
44
+ properties = product_properties
45
+ properties = properties.where(id: product_properties_scope) if product_properties_scope.present?
46
+ properties.where('value IS NOT NULL AND value != ?', '').pluck(:filter_param, :value).uniq
47
+ end
48
+ end
49
+
50
+ # Returns the metafield type for the property kind
51
+ # @return [String] eg. 'Spree::Metafields::ShortText'
52
+ def kind_to_metafield_type
53
+ case kind
54
+ when 'short_text'
55
+ 'Spree::Metafields::ShortText'
56
+ when 'long_text'
57
+ 'Spree::Metafields::LongText'
58
+ when 'number'
59
+ 'Spree::Metafields::Number'
60
+ when 'rich_text'
61
+ 'Spree::Metafields::RichText'
62
+ else
63
+ 'Spree::Metafields::ShortText'
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def touch_all_products
70
+ products.touch_all
71
+ end
72
+
73
+ def with_uniq_values_cache_key(product_properties_scope, &block)
74
+ return block.call if product_properties_scope.present?
75
+
76
+ uniq_values_cache_key = ['property-uniq-values', cache_key_with_version]
77
+ Rails.cache.fetch(uniq_values_cache_key) { block.call }
78
+ end
79
+
80
+ def ensure_product_properties_have_filter_params
81
+ return unless filterable?
82
+
83
+ product_properties.where(filter_param: [nil, '']).where('value IS NOT NULL AND value != ?', '').find_each(&:save)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,9 @@
1
+ module Spree
2
+ class PropertyPrototype < Spree.base_class
3
+ belongs_to :prototype, class_name: 'Spree::Prototype'
4
+ belongs_to :property, class_name: 'Spree::Property'
5
+
6
+ validates :prototype, :property, presence: true
7
+ validates :prototype_id, uniqueness: { scope: :property_id }
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module Spree
2
+ module PrototypeDecorator
3
+ def self.prepended(base)
4
+ base.has_many :property_prototypes, class_name: 'Spree::PropertyPrototype'
5
+ base.has_many :properties, through: :property_prototypes, class_name: 'Spree::Property'
6
+ end
7
+ end
8
+ end
9
+
10
+ Spree::Prototype.prepend(Spree::PrototypeDecorator)
@@ -0,0 +1,23 @@
1
+ module Spree
2
+ module Filters
3
+ class PropertiesPresenter
4
+ def initialize(product_properties_scope:)
5
+ @product_properties = product_properties_scope.includes(:property)
6
+ end
7
+
8
+ def to_a
9
+ grouped_options.map do |property, product_properties|
10
+ PropertyPresenter.new(property: property, product_properties: product_properties)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :product_properties
17
+
18
+ def grouped_options
19
+ product_properties.group_by(&:property)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ module Spree
2
+ module Filters
3
+ class PropertyPresenter
4
+ def initialize(property:, product_properties:)
5
+ @property = property
6
+ @product_properties = product_properties
7
+ end
8
+
9
+ attr_reader :product_properties
10
+
11
+ delegate_missing_to :property
12
+
13
+ def uniq_values
14
+ property.uniq_values(product_properties_scope: product_properties)
15
+ end
16
+
17
+ def to_h
18
+ {
19
+ id: property.id,
20
+ name: property.name,
21
+ presentation: property.presentation,
22
+ values: values_hash
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :property
29
+
30
+ def values_hash
31
+ value_hashes = uniq_values.map do |filter_param, value|
32
+ {
33
+ value: value,
34
+ filter_param: filter_param
35
+ }
36
+ end
37
+
38
+ value_hashes.sort_by { |e| e[:value] }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ module Spree
2
+ module Products
3
+ module PrepareNestedAttributesDecorator
4
+ def call
5
+ # Mark product properties for removal when value is left blank
6
+ if params[:product_properties_attributes].present?
7
+ params[:product_properties_attributes].each do |key, product_property_params|
8
+ next unless product_property_params[:id].present?
9
+ next if product_property_params[:value].present?
10
+
11
+ # https://api.rubyonrails.org/v7.1.3.4/classes/ActiveRecord/NestedAttributes/ClassMethods.html
12
+ params[:product_properties_attributes][key]['_destroy'] = '1'
13
+ end
14
+ end
15
+
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ Spree::Products::PrepareNestedAttributes.prepend(Spree::Products::PrepareNestedAttributesDecorator)
@@ -0,0 +1,48 @@
1
+ <% if Spree::Config[:product_properties_enabled] %>
2
+ <div class="card mb-6">
3
+ <div class="card-header flex items-center justify-between">
4
+ <h5 class="card-title"><%= Spree.t(:properties) %></h5>
5
+
6
+ <% if can?(:manage, Spree::Property) %>
7
+ <%= link_to_with_icon 'adjustments', Spree.t('admin.manage_properties'), spree.admin_properties_path, class: 'btn btn-sm btn-light' %>
8
+ <% end %>
9
+ </div>
10
+ <div class="card-body p-0">
11
+ <table class="table">
12
+ <thead>
13
+ <tr>
14
+ <th scope="col"><%= Spree.t(:property) %></th>
15
+ <th scope="col"><%= Spree.t(:value) %></th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <%= f.fields_for :product_properties, sorted_product_properties(f.object) do |product_property_form| %>
20
+ <% property = product_property_form.object.property %>
21
+ <%= product_property_form.hidden_field :id %>
22
+ <%= product_property_form.hidden_field :property_id %>
23
+ <tr>
24
+ <td class="align-top pt-4 w-1/4">
25
+ <code><%= property.name %></code>
26
+ </td>
27
+ <td class="w-3/4">
28
+ <div class="form-group">
29
+ <% if property.long_text? %>
30
+ <%= product_property_form.text_area :value, { class: 'form-input', data: { controller: 'textarea-autogrow'} } %>
31
+ <% elsif property.number? %>
32
+ <%= product_property_form.number_field :value, class: 'form-input' %>
33
+ <% elsif property.rich_text? %>
34
+ <div class="trix-container">
35
+ <%= product_property_form.rich_text_area :value %>
36
+ </div>
37
+ <% else %>
38
+ <%= product_property_form.text_field :value, class: 'form-input' %>
39
+ <% end %>
40
+ </div>
41
+ </td>
42
+ </tr>
43
+ <% end %>
44
+ </tbody>
45
+ </table>
46
+ </div>
47
+ </div>
48
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%= search_form_for [:admin, @search], class: "filter-wrap", data: {controller: "filters"} do |f| %>
2
+ <%= render 'spree/admin/shared/filters_search_bar', param: :name_cont, label: Spree.t(:name) %>
3
+ <%= render "spree/admin/shared/filter_badge_template" %>
4
+ <div data-filters-target="badgesContainer" class="filter-badges-container"></div>
5
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <div data-controller="slug-form" class="card mb-6">
2
+ <div class="card-body">
3
+ <%= f.spree_text_field :presentation, data: { slug_form_target: :name, action: 'input->slug-form#updateUrlFromName' }, required: true, autofocus: f.object.new_record? %>
4
+ <%= f.spree_text_field :name, label: Spree.t(:internal_name), data: { slug_form_target: :url }, required: true %>
5
+ <%= f.spree_select :kind, options_for_select(Spree::Property.kinds.map{ |k| [k.first.humanize, k.first.to_sym] }, @object.kind) %>
6
+ <%= f.spree_select :display_on, display_on_options, { label: Spree.t(:display_on) } %>
7
+ </div>
8
+ </div>
@@ -0,0 +1,9 @@
1
+ <tr>
2
+ <th class="no-border handel-head"></th>
3
+ <th scope="col"><%= Spree.t(:internal_name) %></th>
4
+ <th scope="col"><%= Spree.t(:presentation) %></th>
5
+ <th scope="col"><%= Spree.t(:kind) %></th>
6
+ <th scope="col"><%= Spree.t(:products) %></th>
7
+ <th scope="col"><%= Spree.t(:visibility) %></th>
8
+ <th scope="col"></th>
9
+ </tr>
@@ -0,0 +1,25 @@
1
+ <tr id="<%= spree_dom_id property %>" data-controller="row-link" data-sortable-update-url="<%= spree.admin_property_path(property) %>">
2
+ <td class="w-5 move-handle text-center">
3
+ <%= icon('grip-vertical', class: 'rounded-md hover:bg-gray-100 p-2') %>
4
+ </td>
5
+ <td class="w-20 cursor-pointer py-0" data-action="click->row-link#openLink">
6
+ <code><%= property.name %></code>
7
+ </td>
8
+ <td class="w-20 cursor-pointer" data-action="click->row-link#openLink">
9
+ <%= link_to property.presentation, spree.edit_admin_property_path(property), class: 'no-underline flex items-center font-bold text-gray-900', data: { row_link_target: :link, turbo_frame: '_top' } %>
10
+ </td>
11
+ <td class="w-20 cursor-pointer" data-action="click->row-link#openLink">
12
+ <span class="badge badge-light"><%= property.kind.humanize %></span>
13
+ </td>
14
+ <td class="w-10 cursor-pointer" data-action="click->row-link#openLink">
15
+ <%= property.products.count %>
16
+ </td>
17
+ <td>
18
+ <%= form_with model: property, url: admin_property_path(property), method: :patch, data: { controller: 'auto-submit' } do |f| %>
19
+ <%= f.select :display_on, display_on_options, {}, { class: 'form-select form-select-sm', data: { action: 'auto-submit#submit' } } %>
20
+ <% end %>
21
+ </td>
22
+ <td class="w-10 actions">
23
+ <%= link_to_edit(property, class: 'btn btn-light btn-sm', data: { turbo_frame: '_top' }) if can?(:edit, property) %>
24
+ </td>
25
+ </tr>
@@ -0,0 +1,2 @@
1
+
2
+ <%= render 'spree/admin/shared/edit_resource' %>
@@ -0,0 +1,22 @@
1
+ <% content_for(:page_title) do %>
2
+ <%= Spree.t(:properties) %>
3
+ <% end %>
4
+
5
+ <% content_for(:page_alerts) do %>
6
+ <div class="alert alert-info">
7
+ Properties allow you to enrich your Product information.
8
+ </div>
9
+ <% end %>
10
+
11
+ <% content_for :page_actions do %>
12
+ <%= render_admin_partials(:properties_actions_partials) %>
13
+ <%= link_to_with_icon 'plus', Spree.t(:new_property), new_object_url, class: "btn btn-primary" if can?(:create, Spree::Property) %>
14
+ <% end %>
15
+
16
+ <%= render_admin_partials(:properties_header_partials) %>
17
+
18
+ <%= render 'spree/admin/shared/index_table', sortable: true %>
19
+
20
+ <p class="documentation-link-container">
21
+ <%= external_link_to "Learn more about properties", "https://spreecommerce.org/docs/user/manage-products/product-properties" %>
22
+ </p>
@@ -0,0 +1 @@
1
+ <%= render 'spree/admin/shared/new_resource' %>
@@ -0,0 +1 @@
1
+ <%= turbo_render_alerts %>
@@ -0,0 +1,62 @@
1
+ Rails.application.config.after_initialize do
2
+ # Register product_properties_enabled preference if not already defined
3
+ unless Spree::Config.respond_to?(:product_properties_enabled)
4
+ Spree::Core::Configuration.preference :product_properties_enabled, :boolean, default: false
5
+ end
6
+
7
+ # Register permitted attributes for product properties
8
+ unless Spree::PermittedAttributes::ATTRIBUTES.include?(:product_properties_attributes)
9
+ Spree::PermittedAttributes::ATTRIBUTES.push(:product_properties_attributes, :property_attributes)
10
+
11
+ Spree::PermittedAttributes.class_eval do
12
+ mattr_accessor :product_properties_attributes
13
+ mattr_accessor :property_attributes
14
+ end
15
+
16
+ Spree::PermittedAttributes.product_properties_attributes = [
17
+ :property_name, :property_id, :value, :position, :_destroy
18
+ ]
19
+ Spree::PermittedAttributes.property_attributes = [
20
+ :name, :presentation, :position, :kind, :display_on
21
+ ]
22
+
23
+ # Re-delegate the new attributes
24
+ Spree::Core::ControllerHelpers::StrongParameters.delegate(
25
+ :permitted_product_properties_attributes,
26
+ :permitted_property_attributes,
27
+ to: :permitted_attributes,
28
+ prefix: false
29
+ )
30
+ end
31
+
32
+ # Override permitted_product_attributes to include product_properties_attributes
33
+ Spree::Core::ControllerHelpers::StrongParameters.module_eval do
34
+ def permitted_product_attributes
35
+ permitted_attributes.product_attributes + [
36
+ variants_attributes: permitted_variant_attributes + ['id', :_destroy],
37
+ master_attributes: permitted_variant_attributes + ['id'],
38
+ product_properties_attributes: permitted_product_properties_attributes + ['id', :_destroy]
39
+ ]
40
+ end
41
+ end
42
+
43
+ # Register product form partial for properties
44
+ if defined?(Spree::Admin) && Rails.application.config.respond_to?(:spree_admin)
45
+ Rails.application.config.spree_admin.product_form_partials << 'spree/admin/products/form/properties'
46
+ end
47
+
48
+ # Register admin navigation
49
+ if defined?(Spree::Admin) && Spree.respond_to?(:admin)
50
+ sidebar_nav = Spree.admin.navigation.sidebar
51
+
52
+ products_item = sidebar_nav.find(:products)
53
+ if products_item
54
+ builder = Spree::Admin::Navigation::Builder.new(sidebar_nav, products_item)
55
+ builder.add :properties,
56
+ label: :properties,
57
+ url: :admin_properties_path,
58
+ position: 50,
59
+ if: -> { can?(:manage, Spree::Property) && Spree::Config.product_properties_enabled }
60
+ end
61
+ end
62
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ Spree::Core::Engine.add_routes do
2
+ namespace :admin, path: Spree.admin_path do
3
+ resources :properties, except: :show
4
+ end
5
+ end
@@ -0,0 +1,113 @@
1
+ class CreateSpreePropertiesAndProductProperties < ActiveRecord::Migration[7.2]
2
+ def json_column_type
3
+ connection.adapter_name.downcase.include?('postgresql') ? :jsonb : :json
4
+ end
5
+
6
+ def change
7
+ unless table_exists?(:spree_properties)
8
+ create_table :spree_properties do |t|
9
+ t.string :name
10
+ t.string :presentation, null: false
11
+ t.string :filter_param
12
+ t.boolean :filterable, default: false, null: false
13
+ t.integer :kind, default: 0
14
+ t.string :display_on, default: 'both'
15
+ t.integer :position, default: 0
16
+ t.column :public_metadata, json_column_type
17
+ t.column :private_metadata, json_column_type
18
+
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :spree_properties, :name, unique: true
23
+ add_index :spree_properties, :filter_param
24
+ add_index :spree_properties, :filterable
25
+ add_index :spree_properties, :position
26
+ end
27
+
28
+ unless table_exists?(:spree_product_properties)
29
+ create_table :spree_product_properties do |t|
30
+ t.string :value
31
+ t.references :product, null: false
32
+ t.references :property, null: false
33
+ t.integer :position, default: 0
34
+ t.string :filter_param
35
+ t.boolean :show_property, default: true
36
+
37
+ t.timestamps
38
+ end
39
+
40
+ add_index :spree_product_properties, [:property_id, :product_id], unique: true
41
+ add_index :spree_product_properties, :filter_param
42
+ add_index :spree_product_properties, :position
43
+ end
44
+
45
+ unless table_exists?(:spree_property_prototypes)
46
+ create_table :spree_property_prototypes do |t|
47
+ t.references :prototype
48
+ t.references :property
49
+
50
+ t.timestamps
51
+ end
52
+
53
+ add_index :spree_property_prototypes, [:prototype_id, :property_id], unique: true, name: 'index_property_prototypes_on_prototype_id_and_property_id'
54
+ end
55
+
56
+ unless table_exists?(:spree_property_translations)
57
+ create_table :spree_property_translations do |t|
58
+ t.references :spree_property, null: false
59
+ t.string :locale, null: false
60
+ t.string :presentation
61
+
62
+ t.timestamps
63
+ end
64
+
65
+ add_index :spree_property_translations, :locale
66
+ add_index :spree_property_translations, [:spree_property_id, :locale], unique: true, name: 'unique_property_id_per_locale'
67
+ end
68
+
69
+ unless table_exists?(:spree_product_property_translations)
70
+ create_table :spree_product_property_translations do |t|
71
+ t.references :spree_product_property, null: false
72
+ t.string :locale, null: false
73
+ t.string :value
74
+
75
+ t.timestamps
76
+ end
77
+
78
+ add_index :spree_product_property_translations, :locale
79
+ add_index :spree_product_property_translations, [:spree_product_property_id, :locale], unique: true, name: 'unique_product_property_id_per_locale'
80
+ end
81
+
82
+ # For existing installations that already have these tables but may be missing columns
83
+ if table_exists?(:spree_properties)
84
+ unless column_exists?(:spree_properties, :public_metadata)
85
+ add_column :spree_properties, :public_metadata, json_column_type
86
+ end
87
+
88
+ unless column_exists?(:spree_properties, :private_metadata)
89
+ add_column :spree_properties, :private_metadata, json_column_type
90
+ end
91
+
92
+ unless column_exists?(:spree_properties, :display_on)
93
+ add_column :spree_properties, :display_on, :string, default: 'both'
94
+ end
95
+
96
+ unless column_exists?(:spree_properties, :position)
97
+ add_column :spree_properties, :position, :integer, default: 0
98
+ end
99
+
100
+ unless column_exists?(:spree_properties, :kind)
101
+ add_column :spree_properties, :kind, :integer, default: 0
102
+ end
103
+
104
+ unless column_exists?(:spree_properties, :filter_param)
105
+ add_column :spree_properties, :filter_param, :string
106
+ end
107
+
108
+ unless column_exists?(:spree_properties, :filterable)
109
+ add_column :spree_properties, :filterable, :boolean, default: false, null: false
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,20 @@
1
+ module SpreeLegacyProductProperties
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ class_option :migrate, type: :boolean, default: true
5
+
6
+ def add_migrations
7
+ run 'bundle exec rake railties:install:migrations FROM=spree_legacy_product_properties'
8
+ end
9
+
10
+ def run_migrations
11
+ run_migrations = options[:migrate] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
12
+ if run_migrations
13
+ run 'bin/rails db:migrate'
14
+ else
15
+ puts 'Skipping rails db:migrate, don\'t forget to run it!'
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeLegacyProductProperties
2
+ class Configuration < Spree::Preferences::Configuration
3
+
4
+ # Some example preferences are shown below, for more information visit:
5
+ # https://docs.spreecommerce.org/developer/contributing/creating-an-extension
6
+
7
+ # preference :enabled, :boolean, default: true
8
+ # preference :dark_chocolate, :boolean, default: true
9
+ # preference :color, :string, default: 'Red'
10
+ # preference :favorite_number, :integer
11
+ # preference :supported_locales, :array, default: [:en]
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module SpreeLegacyProductProperties
2
+ class Engine < Rails::Engine
3
+ require 'spree/core'
4
+ isolate_namespace Spree
5
+ engine_name 'spree_legacy_product_properties'
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+
11
+ initializer 'spree_legacy_product_properties.environment', before: :load_config_initializers do |_app|
12
+ SpreeLegacyProductProperties::Config = SpreeLegacyProductProperties::Configuration.new
13
+ end
14
+
15
+ def self.activate
16
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
17
+ Rails.configuration.cache_classes ? require(c) : load(c)
18
+ end
19
+ end
20
+
21
+ config.to_prepare(&method(:activate).to_proc)
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ FactoryBot.define do
2
+ factory :property, class: Spree::Property do
3
+ sequence(:name) { |n| "baseball_cap_color_#{n}" }
4
+ presentation { 'cap color' }
5
+
6
+ trait :filterable do
7
+ filterable { true }
8
+ end
9
+
10
+ trait :brand do
11
+ name { 'brand' }
12
+ presentation { 'Brand' }
13
+ filter_param { 'brand' }
14
+ end
15
+
16
+ trait :manufacturer do
17
+ name { 'manufacturer' }
18
+ presentation { 'Manufacturer' }
19
+ filter_param { 'manufacturer' }
20
+ end
21
+
22
+ trait :material do
23
+ name { 'material' }
24
+ presentation { 'Material' }
25
+ filter_param { 'material' }
26
+ end
27
+ end
28
+
29
+ factory :product_property, class: Spree::ProductProperty do
30
+ product
31
+ value { "val-#{rand(50)}" }
32
+ property
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ module SpreeLegacyProductProperties
2
+ VERSION = '1.0.0'.freeze
3
+
4
+ def gem_version
5
+ Gem::Version.new(VERSION)
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ require 'spree'
2
+ require 'spree_legacy_product_properties/engine'
3
+ require 'spree_legacy_product_properties/version'
4
+ require 'spree_legacy_product_properties/configuration'
5
+
6
+ module SpreeLegacyProductProperties
7
+ mattr_accessor :queue
8
+
9
+ def self.queue
10
+ @@queue ||= Spree.queues.default
11
+ end
12
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spree_legacy_product_properties
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Vendo Connect Inc., Vendo Sp. z o.o.
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: spree
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 5.4.0.beta
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 5.4.0.beta
26
+ - !ruby/object:Gem::Dependency
27
+ name: spree_admin
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 5.4.0.beta
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 5.4.0.beta
40
+ - !ruby/object:Gem::Dependency
41
+ name: spree_dev_tools
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: Legacy product properties system extracted from Spree core. Replaced
55
+ by Metafields in Spree 5.x.
56
+ email: hello@spreecommerce.org
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - LICENSE.md
62
+ - README.md
63
+ - Rakefile
64
+ - app/controllers/spree/admin/products_controller_decorator.rb
65
+ - app/controllers/spree/admin/properties_controller.rb
66
+ - app/finders/spree/product_properties/find_available.rb
67
+ - app/finders/spree/products/find_decorator.rb
68
+ - app/helpers/spree/admin/properties_helper.rb
69
+ - app/models/concerns/spree/filter_param.rb
70
+ - app/models/spree/permission_sets/property_management.rb
71
+ - app/models/spree/product_decorator.rb
72
+ - app/models/spree/product_property.rb
73
+ - app/models/spree/products/duplicator_decorator.rb
74
+ - app/models/spree/property.rb
75
+ - app/models/spree/property_prototype.rb
76
+ - app/models/spree/prototype_decorator.rb
77
+ - app/presenters/spree/filters/properties_presenter.rb
78
+ - app/presenters/spree/filters/property_presenter.rb
79
+ - app/services/spree/products/prepare_nested_attributes_decorator.rb
80
+ - app/views/spree/admin/products/form/_properties.html.erb
81
+ - app/views/spree/admin/properties/_filters.html.erb
82
+ - app/views/spree/admin/properties/_form.html.erb
83
+ - app/views/spree/admin/properties/_table_header.html.erb
84
+ - app/views/spree/admin/properties/_table_row.html.erb
85
+ - app/views/spree/admin/properties/edit.html.erb
86
+ - app/views/spree/admin/properties/index.html.erb
87
+ - app/views/spree/admin/properties/new.html.erb
88
+ - app/views/spree/admin/properties/update.turbo_stream.erb
89
+ - config/initializers/spree.rb
90
+ - config/routes.rb
91
+ - db/migrate/20221229000000_create_spree_properties_and_product_properties.rb
92
+ - lib/generators/spree_legacy_product_properties/install/install_generator.rb
93
+ - lib/spree_legacy_product_properties.rb
94
+ - lib/spree_legacy_product_properties/configuration.rb
95
+ - lib/spree_legacy_product_properties/engine.rb
96
+ - lib/spree_legacy_product_properties/factories.rb
97
+ - lib/spree_legacy_product_properties/version.rb
98
+ homepage: https://github.com/spree/spree-legacy-product-properties
99
+ licenses:
100
+ - MIT
101
+ metadata: {}
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '3.2'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements:
116
+ - none
117
+ rubygems_version: 4.0.2
118
+ specification_version: 4
119
+ summary: Legacy Product Properties for Spree Commerce
120
+ test_files: []