spree_core 4.0.7 → 4.1.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/spree.js +0 -1
- data/app/finders/spree/products/find.rb +19 -6
- data/app/finders/spree/variants/option_types_finder.rb +21 -0
- data/app/finders/spree/variants/visible_finder.rb +22 -0
- data/app/helpers/spree/base_helper.rb +48 -18
- data/app/helpers/spree/products_helper.rb +76 -8
- data/app/models/concerns/spree/user_methods.rb +2 -2
- data/app/models/spree/app_dependencies.rb +1 -7
- data/app/models/spree/credit_card.rb +4 -5
- data/app/models/spree/image.rb +52 -3
- data/app/models/spree/image/configuration/active_storage.rb +9 -1
- data/app/models/spree/line_item.rb +1 -2
- data/app/models/spree/option_type.rb +4 -0
- data/app/models/spree/order.rb +12 -12
- data/app/models/spree/order/address_book.rb +20 -7
- data/app/models/spree/payment_method.rb +8 -0
- data/app/models/spree/preferences/preferable.rb +1 -1
- data/app/models/spree/product.rb +7 -6
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +1 -1
- data/app/models/spree/promotion_handler/coupon.rb +2 -1
- data/app/models/spree/return_item/eligibility_validator/base_validator.rb +1 -1
- data/app/models/spree/store.rb +2 -1
- data/app/models/spree/taxon.rb +2 -6
- data/app/models/spree/variant.rb +2 -14
- data/app/models/spree/zone.rb +6 -3
- data/app/presenters/spree/product_summary_presenter.rb +26 -0
- data/app/presenters/spree/variant_presenter.rb +69 -0
- data/app/presenters/spree/variants/option_types_presenter.rb +74 -0
- data/app/presenters/spree/variants/options_presenter.rb +49 -0
- data/app/services/spree/checkout/get_shipping_rates.rb +10 -7
- data/app/services/spree/checkout/update.rb +2 -13
- data/config/locales/en.yml +156 -14
- data/db/default/spree/stores.rb +8 -3
- data/db/migrate/20140309033438_create_store_from_preferences.rb +8 -4
- data/db/migrate/20191005121504_add_store_id_to_payment_methods.rb +7 -0
- data/db/migrate/20191016134113_add_deafult_value_for_store_default_currency.rb +5 -0
- data/db/migrate/20200102141311_add_social_to_spree_stores.rb +7 -0
- data/lib/generators/spree/dummy/dummy_generator.rb +2 -1
- data/lib/generators/spree/dummy/templates/initializers/bullet.rb +5 -0
- data/lib/generators/spree/install/install_generator.rb +13 -5
- data/lib/generators/spree/install/templates/config/initializers/spree.rb +0 -1
- data/lib/generators/spree/install/templates/config/initializers/spree_storefront.rb +1 -0
- data/lib/generators/spree/install/templates/config/spree_storefront.yml +67 -0
- data/lib/spree/core.rb +0 -1
- data/lib/spree/core/controller_helpers/order.rb +12 -6
- data/lib/spree/core/controller_helpers/store.rb +2 -2
- data/lib/spree/core/search/base.rb +59 -22
- data/lib/spree/core/version.rb +1 -3
- data/lib/spree/money.rb +8 -1
- data/lib/spree/permitted_attributes.rb +3 -2
- data/lib/spree/testing_support/capybara_ext.rb +0 -45
- data/lib/spree/testing_support/common_rake.rb +1 -1
- data/lib/spree/testing_support/factories/store_factory.rb +3 -0
- data/lib/spree/testing_support/order_walkthrough.rb +7 -3
- data/spree_core.gemspec +8 -14
- metadata +45 -24
- data/app/finders/spree/addresses/find.rb +0 -17
- data/app/services/spree/account/addresses/base.rb +0 -39
- data/app/services/spree/account/addresses/create.rb +0 -18
- data/app/services/spree/account/addresses/update.rb +0 -18
- data/lib/spree/database_type_utilities.rb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: db73357d472e46822ad95d6ee2071b01413e2f1ab743177f7cfc8845c6830092
|
4
|
+
data.tar.gz: 83c396103e3809183ac12c21f72e4831cb2c10d1089232bdecd58395459a961b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f57f8d0b1bee1cb0b0d77b7aa82ac8f62d89a783e07dbd1e5f2a8a6d780eaf8de46ccf5e898420d423fe1d133ce2645615cd4611580ca92008d006c059667e1a
|
7
|
+
data.tar.gz: 7e3aee2e5bcc514a78dcc8da3cfb5b35635f691fc2fe9ac68002c9cd208bc96b5df4f784da4612cf1ad409dea467e0a5c29cc13e156e25aa7baeb5d72f367f47
|
@@ -112,17 +112,30 @@ module Spree
|
|
112
112
|
def by_options(products)
|
113
113
|
return products unless options?
|
114
114
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
end.flatten.compact.uniq
|
119
|
-
)
|
115
|
+
options.map do |key, value|
|
116
|
+
products.with_option_value(key, value)
|
117
|
+
end.inject(:&)
|
120
118
|
end
|
121
119
|
|
122
120
|
def by_option_value_ids(products)
|
123
121
|
return products unless option_value_ids?
|
124
122
|
|
125
|
-
|
123
|
+
product_ids = Spree::Product.
|
124
|
+
joins(variants: :option_values).
|
125
|
+
where(spree_option_values: { id: option_value_ids }).
|
126
|
+
group("#{Spree::Product.table_name}.id, #{Spree::Variant.table_name}.id").
|
127
|
+
having('COUNT(spree_option_values.option_type_id) = ?', option_types_count(option_value_ids)).
|
128
|
+
distinct.
|
129
|
+
ids
|
130
|
+
|
131
|
+
products.where(id: product_ids)
|
132
|
+
end
|
133
|
+
|
134
|
+
def option_types_count(option_value_ids)
|
135
|
+
Spree::OptionValue.
|
136
|
+
where(id: option_value_ids).
|
137
|
+
distinct.
|
138
|
+
count(:option_type_id)
|
126
139
|
end
|
127
140
|
|
128
141
|
def ordered(products)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Spree
|
2
|
+
module Variants
|
3
|
+
class OptionTypesFinder
|
4
|
+
COLOR_TYPE = 'color'.freeze
|
5
|
+
|
6
|
+
def initialize(variant_ids:)
|
7
|
+
@variant_ids = variant_ids
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute
|
11
|
+
Spree::OptionType.includes(option_values: :variants).where(spree_variants: { id: variant_ids }).
|
12
|
+
reorder('spree_option_types.position ASC, spree_option_values.position ASC').
|
13
|
+
partition { |option_type| option_type.name.downcase == COLOR_TYPE }.flatten
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :variant_ids
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Spree
|
2
|
+
module Variants
|
3
|
+
class VisibleFinder
|
4
|
+
def initialize(scope:, current_currency:)
|
5
|
+
@scope = scope
|
6
|
+
@current_currency = current_currency
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute
|
10
|
+
Spree::Variant.where(id: active_variants).joins(:option_values).order('spree_option_values.position ASC')
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_reader :scope, :current_currency
|
16
|
+
|
17
|
+
def active_variants
|
18
|
+
scope.active(current_currency).unscope(:order)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -32,8 +32,12 @@ module Spree
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
def logo(image_path = Spree::Config[:logo])
|
36
|
-
|
35
|
+
def logo(image_path = Spree::Config[:logo], options = {})
|
36
|
+
path = spree.respond_to?(:root_path) ? spree.root_path : main_app.root_path
|
37
|
+
|
38
|
+
link_to path, 'aria-label': current_store.name, method: options[:method] do
|
39
|
+
image_tag image_path, alt: current_store.name, title: current_store.name
|
40
|
+
end
|
37
41
|
end
|
38
42
|
|
39
43
|
def meta_data
|
@@ -50,15 +54,32 @@ module Spree
|
|
50
54
|
end
|
51
55
|
|
52
56
|
if meta[:keywords].blank? || meta[:description].blank?
|
53
|
-
|
54
|
-
|
57
|
+
if object && object[:name].present?
|
58
|
+
meta.reverse_merge!(keywords: [object.name, current_store.meta_keywords].reject(&:blank?).join(', '),
|
59
|
+
description: [object.name, current_store.meta_description].reject(&:blank?).join(', '))
|
60
|
+
else
|
61
|
+
meta.reverse_merge!(keywords: (current_store.meta_keywords || current_store.seo_title),
|
62
|
+
description: (current_store.meta_description || current_store.seo_title))
|
63
|
+
end
|
55
64
|
end
|
56
65
|
meta
|
57
66
|
end
|
58
67
|
|
68
|
+
def meta_image_url_path
|
69
|
+
object = instance_variable_get('@' + controller_name.singularize)
|
70
|
+
return unless object.is_a?(Spree::Product)
|
71
|
+
|
72
|
+
image = default_image_for_product_or_variant(object)
|
73
|
+
image&.attachment.present? ? main_app.url_for(image.attachment) : asset_path(Spree::Config[:logo])
|
74
|
+
end
|
75
|
+
|
76
|
+
def meta_image_data_tag
|
77
|
+
tag('meta', property: 'og:image', content: meta_image_url_path) if meta_image_url_path
|
78
|
+
end
|
79
|
+
|
59
80
|
def meta_data_tags
|
60
81
|
meta_data.map do |name, content|
|
61
|
-
tag('meta', name: name, content: content)
|
82
|
+
tag('meta', name: name, content: content) unless name.nil? || content.nil?
|
62
83
|
end.join("\n")
|
63
84
|
end
|
64
85
|
|
@@ -75,14 +96,30 @@ module Spree
|
|
75
96
|
[I18n.l(time.to_date, format: :long), time.strftime('%l:%M %p')].join(' ')
|
76
97
|
end
|
77
98
|
|
78
|
-
def seo_url(taxon)
|
79
|
-
spree.nested_taxons_path(taxon.permalink)
|
99
|
+
def seo_url(taxon, options = nil)
|
100
|
+
spree.nested_taxons_path(taxon.permalink, options)
|
80
101
|
end
|
81
102
|
|
82
103
|
def frontend_available?
|
83
104
|
Spree::Core::Engine.frontend_available?
|
84
105
|
end
|
85
106
|
|
107
|
+
def default_image_for_product_or_variant(product_or_variant)
|
108
|
+
if product_or_variant.images.empty?
|
109
|
+
if product_or_variant.is_a?(Spree::Product) && product_or_variant.variant_images.any?
|
110
|
+
product_or_variant.variant_images.first
|
111
|
+
elsif product_or_variant.is_a?(Spree::Variant) && product_or_variant.product.variant_images.any?
|
112
|
+
product_or_variant.product.variant_images.first
|
113
|
+
end
|
114
|
+
else
|
115
|
+
product_or_variant.images.first
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def base_cache_key
|
120
|
+
[I18n.locale, current_currency]
|
121
|
+
end
|
122
|
+
|
86
123
|
private
|
87
124
|
|
88
125
|
def create_product_image_tag(image, product, options, style)
|
@@ -94,18 +131,11 @@ module Spree
|
|
94
131
|
self.class.send :define_method, "#{style}_image" do |product, *options|
|
95
132
|
options = options.first || {}
|
96
133
|
options[:alt] ||= product.name
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
else
|
101
|
-
if product.is_a?(Variant) && !product.product.variant_images.empty?
|
102
|
-
create_product_image_tag(product.product.variant_images.first, product, options, style)
|
103
|
-
else
|
104
|
-
image_tag "noimage/#{style}.png", options
|
105
|
-
end
|
106
|
-
end
|
134
|
+
image_path = default_image_for_product_or_variant(product)
|
135
|
+
if image_path.present?
|
136
|
+
create_product_image_tag image_path, product, options, style
|
107
137
|
else
|
108
|
-
|
138
|
+
image_tag "noimage/#{style}.png", options
|
109
139
|
end
|
110
140
|
end
|
111
141
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Spree
|
2
2
|
module ProductsHelper
|
3
|
+
include BaseHelper
|
4
|
+
|
3
5
|
# returns the formatted price for the specified variant as a full price or a difference depending on configuration
|
4
6
|
def variant_price(variant)
|
5
7
|
if Spree::Config[:show_variant_full_price]
|
@@ -29,6 +31,14 @@ module Spree
|
|
29
31
|
end
|
30
32
|
end
|
31
33
|
|
34
|
+
def default_variant(variants)
|
35
|
+
variants_option_types_presenter(variants).default_variant || variants.find(&:is_master)
|
36
|
+
end
|
37
|
+
|
38
|
+
def used_variants_options(variants)
|
39
|
+
variants_option_types_presenter(variants).options
|
40
|
+
end
|
41
|
+
|
32
42
|
# converts line breaks in product description into <p> tags (for html display purposes)
|
33
43
|
def product_description(product)
|
34
44
|
description = if Spree::Config[:show_raw_product_description]
|
@@ -47,15 +57,27 @@ module Spree
|
|
47
57
|
end
|
48
58
|
end
|
49
59
|
|
50
|
-
def cache_key_for_products
|
51
|
-
count =
|
52
|
-
max_updated_at = (
|
53
|
-
products_cache_keys = "spree/products/all-#{params[:page]}-#{max_updated_at}-#{count}"
|
54
|
-
(common_product_cache_keys + [products_cache_keys]).compact.join('/')
|
60
|
+
def cache_key_for_products(products = @products, additional_cache_key = nil)
|
61
|
+
count = products.count
|
62
|
+
max_updated_at = (products.maximum(:updated_at) || Date.today).to_s(:number)
|
63
|
+
products_cache_keys = "spree/products/all-#{params[:page]}-#{params[:sort_by]}-#{max_updated_at}-#{count}-#{@taxon&.id}"
|
64
|
+
(common_product_cache_keys + [products_cache_keys] + [additional_cache_key]).compact.join('/')
|
55
65
|
end
|
56
66
|
|
57
67
|
def cache_key_for_product(product = @product)
|
58
|
-
|
68
|
+
cache_key_elements = common_product_cache_keys
|
69
|
+
cache_key_elements += [
|
70
|
+
product.cache_key_with_version,
|
71
|
+
product.possible_promotions
|
72
|
+
]
|
73
|
+
|
74
|
+
cache_key_elements.compact.join('/')
|
75
|
+
end
|
76
|
+
|
77
|
+
def limit_descritpion(string)
|
78
|
+
return string if string.length <= 450
|
79
|
+
|
80
|
+
string.slice(0..449) + '...'
|
59
81
|
end
|
60
82
|
|
61
83
|
def available_status(product) # will return a human readable string
|
@@ -71,16 +93,62 @@ module Spree
|
|
71
93
|
end
|
72
94
|
end
|
73
95
|
|
74
|
-
|
96
|
+
def product_images(product, variants)
|
97
|
+
variants = if product.variants_and_option_values(current_currency).any?
|
98
|
+
variants.reject(&:is_master)
|
99
|
+
else
|
100
|
+
variants
|
101
|
+
end
|
102
|
+
|
103
|
+
variants.map(&:images).flatten
|
104
|
+
end
|
105
|
+
|
106
|
+
def product_variants_matrix(is_product_available_in_currency)
|
107
|
+
Spree::VariantPresenter.new(
|
108
|
+
variants: @variants,
|
109
|
+
is_product_available_in_currency: is_product_available_in_currency,
|
110
|
+
current_currency: current_currency,
|
111
|
+
current_price_options: current_price_options
|
112
|
+
).call.to_json
|
113
|
+
end
|
114
|
+
|
115
|
+
def related_products
|
116
|
+
return [] unless @product.respond_to?(:has_related_products?) && @product.has_related_products?(:related_products)
|
117
|
+
|
118
|
+
@_related_products ||= @product.
|
119
|
+
related_products.
|
120
|
+
includes(
|
121
|
+
:tax_category,
|
122
|
+
master: [
|
123
|
+
:prices,
|
124
|
+
images: { attachment_attachment: :blob },
|
125
|
+
]
|
126
|
+
).
|
127
|
+
limit(Spree::Config[:products_per_page])
|
128
|
+
end
|
129
|
+
|
130
|
+
def product_available_in_currency?(product)
|
131
|
+
!(product.price_in(current_currency).amount.nil? || product.price_in(current_currency).amount.zero?)
|
132
|
+
end
|
75
133
|
|
76
134
|
def common_product_cache_keys
|
77
|
-
|
135
|
+
base_cache_key + price_options_cache_key
|
78
136
|
end
|
79
137
|
|
138
|
+
private
|
139
|
+
|
80
140
|
def price_options_cache_key
|
81
141
|
current_price_options.sort.map(&:last).map do |value|
|
82
142
|
value.try(:cache_key) || value
|
83
143
|
end
|
84
144
|
end
|
145
|
+
|
146
|
+
def variants_option_types_presenter(variants)
|
147
|
+
@_variants_option_types_presenter ||= begin
|
148
|
+
option_types = Spree::Variants::OptionTypesFinder.new(variant_ids: variants.map(&:id)).execute
|
149
|
+
|
150
|
+
Spree::Variants::OptionTypesPresenter.new(option_types)
|
151
|
+
end
|
152
|
+
end
|
85
153
|
end
|
86
154
|
end
|
@@ -47,9 +47,9 @@ module Spree
|
|
47
47
|
spree_roles.any? { |role| role.name == role_in_question.to_s }
|
48
48
|
end
|
49
49
|
|
50
|
-
def last_incomplete_spree_order(store)
|
50
|
+
def last_incomplete_spree_order(store, options = {})
|
51
51
|
orders.where(store: store).incomplete.
|
52
|
-
includes(
|
52
|
+
includes(options[:includes]).
|
53
53
|
order('created_at DESC').
|
54
54
|
first
|
55
55
|
end
|
@@ -11,8 +11,7 @@ module Spree
|
|
11
11
|
:checkout_remove_store_credit_service, :checkout_get_shipping_rates_service,
|
12
12
|
:coupon_handler, :country_finder, :current_order_finder, :credit_card_finder,
|
13
13
|
:completed_order_finder, :order_sorter, :cart_compare_line_items_service, :collection_paginator, :products_sorter,
|
14
|
-
:products_finder, :taxon_finder, :line_item_by_variant_finder, :cart_estimate_shipping_rates_service
|
15
|
-
:account_create_address_service, :account_update_address_service, :address_finder
|
14
|
+
:products_finder, :taxon_finder, :line_item_by_variant_finder, :cart_estimate_shipping_rates_service
|
16
15
|
].freeze
|
17
16
|
|
18
17
|
attr_accessor *INJECTION_POINTS
|
@@ -60,14 +59,9 @@ module Spree
|
|
60
59
|
# coupons
|
61
60
|
# TODO: we should split this service into 2 seperate - Add and Remove
|
62
61
|
@coupon_handler = 'Spree::PromotionHandler::Coupon'
|
63
|
-
|
64
|
-
# account
|
65
|
-
@account_create_address_service = 'Spree::Account::Addresses::Create'
|
66
|
-
@account_update_address_service = 'Spree::Account::Addresses::Update'
|
67
62
|
end
|
68
63
|
|
69
64
|
def set_default_finders
|
70
|
-
@address_finder = 'Spree::Addresses::Find'
|
71
65
|
@country_finder = 'Spree::Countries::Find'
|
72
66
|
@current_order_finder = 'Spree::Orders::FindCurrent'
|
73
67
|
@completed_order_finder = 'Spree::Orders::FindComplete'
|
@@ -24,9 +24,10 @@ module Spree
|
|
24
24
|
attribute :month, ActiveRecord::Type::Integer.new
|
25
25
|
attribute :year, ActiveRecord::Type::Integer.new
|
26
26
|
|
27
|
-
attr_reader :number
|
27
|
+
attr_reader :number
|
28
28
|
attr_accessor :encrypted_data,
|
29
29
|
:imported,
|
30
|
+
:verification_value,
|
30
31
|
:manual_entry
|
31
32
|
|
32
33
|
with_options if: :require_card_numbers?, on: :create do
|
@@ -100,11 +101,9 @@ module Spree
|
|
100
101
|
end
|
101
102
|
end
|
102
103
|
|
103
|
-
def verification_value=(value)
|
104
|
-
@verification_value = value.to_s.gsub(/\s/, '')
|
105
|
-
end
|
106
|
-
|
107
104
|
def set_last_digits
|
105
|
+
number.to_s.gsub!(/\s/, '')
|
106
|
+
verification_value.to_s.gsub!(/\s/, '')
|
108
107
|
self.last_digits ||= number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1)
|
109
108
|
end
|
110
109
|
|
data/app/models/spree/image.rb
CHANGED
@@ -2,7 +2,7 @@ module Spree
|
|
2
2
|
class Image < Asset
|
3
3
|
include Configuration::ActiveStorage
|
4
4
|
include Rails.application.routes.url_helpers
|
5
|
-
|
5
|
+
|
6
6
|
# In Rails 5.x class constants are being undefined/redefined during the code reloading process
|
7
7
|
# in a rails development environment, after which the actual ruby objects stored in those class constants
|
8
8
|
# are no longer equal (subclass == self) what causes error ActiveRecord::SubclassNotFound
|
@@ -12,14 +12,63 @@ module Spree
|
|
12
12
|
|
13
13
|
def styles
|
14
14
|
self.class.styles.map do |_, size|
|
15
|
-
width, height = size
|
15
|
+
width, height = size.chop.split('x')
|
16
16
|
|
17
17
|
{
|
18
|
-
url: polymorphic_path(attachment.variant(
|
18
|
+
url: polymorphic_path(attachment.variant(combine_options: {
|
19
|
+
gravity: 'center',
|
20
|
+
resize: size,
|
21
|
+
extent: size,
|
22
|
+
background: 'snow2',
|
23
|
+
quality: 80
|
24
|
+
}), only_path: true),
|
19
25
|
width: width,
|
20
26
|
height: height
|
21
27
|
}
|
22
28
|
end
|
23
29
|
end
|
30
|
+
|
31
|
+
def style(name)
|
32
|
+
size = self.class.styles[name]
|
33
|
+
return unless size
|
34
|
+
|
35
|
+
width, height = size.chop.split('x')
|
36
|
+
|
37
|
+
{
|
38
|
+
url: polymorphic_path(attachment.variant(combine_options: {
|
39
|
+
gravity: 'center',
|
40
|
+
resize: size,
|
41
|
+
extent: size,
|
42
|
+
background: 'snow2',
|
43
|
+
quality: 80
|
44
|
+
}), only_path: true),
|
45
|
+
size: size,
|
46
|
+
width: width,
|
47
|
+
height: height
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def style_dimensions(name)
|
52
|
+
size = self.class.styles[name]
|
53
|
+
width, height = size.chop.split('x')
|
54
|
+
|
55
|
+
{
|
56
|
+
width: width,
|
57
|
+
height: height
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def plp_url
|
62
|
+
size = self.class.styles[:plp_and_carousel]
|
63
|
+
variant = attachment.variant(
|
64
|
+
gravity: 'center',
|
65
|
+
resize: size,
|
66
|
+
extent: size,
|
67
|
+
background: 'snow2',
|
68
|
+
quality: 80
|
69
|
+
)
|
70
|
+
|
71
|
+
polymorphic_path(variant, only_path: true)
|
72
|
+
end
|
24
73
|
end
|
25
74
|
end
|