spree_core 5.4.3 → 5.5.0.rc1
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.
- checksums.yaml +4 -4
- data/app/helpers/spree/base_helper.rb +0 -82
- data/app/helpers/spree/currency_helper.rb +0 -12
- data/app/helpers/spree/products_helper.rb +0 -8
- data/app/jobs/spree/base_job.rb +18 -0
- data/app/jobs/spree/events/subscriber_job.rb +2 -1
- data/app/jobs/spree/exports/generate_job.rb +11 -0
- data/app/jobs/spree/images/save_from_url_job.rb +23 -8
- data/app/jobs/spree/imports/assign_tags_job.rb +11 -0
- data/app/jobs/spree/imports/base_job.rb +15 -0
- data/app/jobs/spree/imports/create_categories_job.rb +37 -0
- data/app/jobs/spree/imports/create_rows_job.rb +1 -3
- data/app/jobs/spree/imports/process_group_job.rb +8 -6
- data/app/jobs/spree/imports/process_rows_job.rb +1 -3
- data/app/jobs/spree/media/migrate_product_assets_job.rb +83 -0
- data/app/jobs/spree/products/refresh_metrics_job.rb +15 -4
- data/app/jobs/spree/reports/generate_job.rb +11 -0
- data/app/jobs/spree/search_provider/index_job.rb +5 -1
- data/app/jobs/spree/search_provider/remove_job.rb +4 -0
- data/app/jobs/spree/stock_reservations/expire_job.rb +11 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +34 -1
- data/app/models/concerns/spree/display_on.rb +31 -0
- data/app/models/concerns/spree/metafields.rb +167 -5
- data/app/models/concerns/spree/preference_schema.rb +191 -0
- data/app/models/concerns/spree/prefixed_id.rb +94 -11
- data/app/models/concerns/spree/product_scopes.rb +36 -17
- data/app/models/concerns/spree/ransackable_attributes.rb +5 -1
- data/app/models/concerns/spree/search_indexable.rb +8 -7
- data/app/models/concerns/spree/searchable.rb +11 -2
- data/app/models/concerns/spree/stores/channels.rb +20 -0
- data/app/models/concerns/spree/stores/markets.rb +21 -5
- data/app/models/concerns/spree/typed_associations.rb +120 -0
- data/app/models/concerns/spree/user_methods.rb +71 -12
- data/app/models/spree/ability.rb +4 -117
- data/app/models/spree/api_key.rb +53 -0
- data/app/models/spree/asset.rb +28 -5
- data/app/models/spree/authentication/strategy_registry.rb +72 -0
- data/app/models/spree/base.rb +18 -1
- data/app/models/spree/channel.rb +159 -0
- data/app/models/spree/country.rb +2 -0
- data/app/models/spree/current.rb +5 -1
- data/app/models/spree/custom_field.rb +9 -0
- data/app/models/spree/custom_field_definition.rb +7 -0
- data/app/models/spree/customer_group.rb +8 -2
- data/app/models/spree/export.rb +30 -3
- data/app/models/spree/gateway.rb +25 -0
- data/app/models/spree/gift_card.rb +1 -1
- data/app/models/spree/gift_card_batch.rb +4 -1
- data/app/models/spree/import.rb +5 -0
- data/app/models/spree/import_row.rb +12 -0
- data/app/models/spree/line_item.rb +6 -1
- data/app/models/spree/market.rb +32 -1
- data/app/models/spree/metafield.rb +38 -0
- data/app/models/spree/metafield_definition.rb +29 -6
- data/app/models/spree/metafields/json.rb +10 -0
- data/app/models/spree/newsletter_subscriber.rb +19 -3
- data/app/models/spree/option_type.rb +48 -7
- data/app/models/spree/order/checkout.rb +3 -3
- data/app/models/spree/order.rb +102 -6
- data/app/models/spree/order_approval.rb +19 -0
- data/app/models/spree/order_cancellation.rb +19 -0
- data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
- data/app/models/spree/order_routing/rules/default_location.rb +16 -0
- data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
- data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
- data/app/models/spree/order_routing/strategy/base.rb +47 -0
- data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
- data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
- data/app/models/spree/order_routing/strategy/rules.rb +81 -0
- data/app/models/spree/order_routing_rule.rb +75 -0
- data/app/models/spree/permission_sets/configuration_management.rb +16 -0
- data/app/models/spree/permission_sets/product_display.rb +2 -0
- data/app/models/spree/permission_sets/product_management.rb +2 -0
- data/app/models/spree/price.rb +14 -1
- data/app/models/spree/price_list.rb +129 -17
- data/app/models/spree/price_rule.rb +11 -1
- data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
- data/app/models/spree/price_rules/market_rule.rb +16 -1
- data/app/models/spree/price_rules/user_rule.rb +21 -2
- data/app/models/spree/product/channels.rb +149 -0
- data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
- data/app/models/spree/product/slugs.rb +1 -1
- data/app/models/spree/product.rb +172 -31
- data/app/models/spree/product_publication.rb +43 -0
- data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
- data/app/models/spree/promotion/rules/country.rb +40 -18
- data/app/models/spree/promotion/rules/customer_group.rb +10 -1
- data/app/models/spree/promotion/rules/product.rb +4 -0
- data/app/models/spree/promotion/rules/taxon.rb +24 -1
- data/app/models/spree/promotion/rules/user.rb +21 -0
- data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
- data/app/models/spree/promotion.rb +22 -1
- data/app/models/spree/promotion_action.rb +17 -11
- data/app/models/spree/promotion_rule.rb +17 -18
- data/app/models/spree/search_provider/meilisearch.rb +12 -2
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/quantifier.rb +89 -9
- data/app/models/spree/stock_item.rb +36 -0
- data/app/models/spree/stock_location.rb +52 -0
- data/app/models/spree/stock_reservation.rb +38 -0
- data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
- data/app/models/spree/store.rb +18 -72
- data/app/models/spree/store_credit.rb +0 -8
- data/app/models/spree/store_product.rb +11 -23
- data/app/models/spree/taxon.rb +0 -5
- data/app/models/spree/user_identity.rb +1 -2
- data/app/models/spree/variant.rb +132 -18
- data/app/models/spree/variant_media.rb +46 -0
- data/app/models/spree/webhook_delivery.rb +1 -1
- data/app/models/spree/webhook_endpoint.rb +24 -0
- data/app/models/spree/wished_item.rb +0 -13
- data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
- data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
- data/app/presenters/spree/variant_presenter.rb +4 -3
- data/app/services/spree/addresses/update.rb +6 -8
- data/app/services/spree/cart/add_item.rb +10 -0
- data/app/services/spree/cart/empty.rb +2 -0
- data/app/services/spree/cart/remove_line_item.rb +10 -0
- data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
- data/app/services/spree/cart/set_quantity.rb +10 -0
- data/app/services/spree/carts/complete.rb +1 -0
- data/app/services/spree/carts/create.rb +1 -0
- data/app/services/spree/carts/update.rb +18 -2
- data/app/services/spree/carts/upsert_items.rb +6 -6
- data/app/services/spree/imports/row_processors/customer.rb +4 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
- data/app/services/spree/newsletter/link_user.rb +53 -0
- data/app/services/spree/newsletter/subscribe.rb +31 -9
- data/app/services/spree/orders/approve.rb +27 -6
- data/app/services/spree/orders/build_shipments.rb +29 -0
- data/app/services/spree/orders/cancel.rb +34 -3
- data/app/services/spree/orders/complete.rb +53 -0
- data/app/services/spree/orders/create.rb +156 -0
- data/app/services/spree/orders/update.rb +51 -0
- data/app/services/spree/orders/upsert_items.rb +70 -0
- data/app/services/spree/prices/bulk_upsert.rb +201 -0
- data/app/services/spree/products/duplicator.rb +1 -1
- data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
- data/app/services/spree/sample_data/loader.rb +30 -0
- data/app/services/spree/stock_reservations/extend.rb +19 -0
- data/app/services/spree/stock_reservations/release.rb +12 -0
- data/app/services/spree/stock_reservations/reserve.rb +103 -0
- data/app/services/spree/taxons/remove_products.rb +7 -1
- data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
- data/config/locales/en.yml +27 -10
- data/config/routes.rb +9 -0
- data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
- data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
- data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
- data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
- data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
- data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
- data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
- data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
- data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
- data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
- data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
- data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
- data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
- data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
- data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
- data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
- data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
- data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
- data/db/sample_data/channels.rb +12 -0
- data/db/sample_data/orders.rb +1 -1
- data/db/sample_data/products.csv +212 -212
- data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
- data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
- data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
- data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
- data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +14 -0
- data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
- data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
- data/lib/generators/spree/model/model_generator.rb +73 -7
- data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
- data/lib/generators/spree/model/templates/model.rb.tt +28 -2
- data/lib/spree/core/configuration.rb +7 -0
- data/lib/spree/core/controller_helpers/auth.rb +0 -12
- data/lib/spree/core/controller_helpers/currency.rb +0 -17
- data/lib/spree/core/controller_helpers/order.rb +0 -19
- data/lib/spree/core/dependencies.rb +5 -2
- data/lib/spree/core/engine.rb +54 -7
- data/lib/spree/core/permission_configuration.rb +15 -0
- data/lib/spree/core/preferences/masking.rb +47 -0
- data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +56 -5
- data/lib/spree/permitted_attributes.rb +9 -7
- data/lib/spree/testing_support/factories/address_factory.rb +16 -9
- data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
- data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
- data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
- data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
- data/lib/spree/testing_support/factories/product_factory.rb +16 -7
- data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
- data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
- data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
- data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
- data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
- data/lib/spree/testing_support/order_walkthrough.rb +1 -1
- data/lib/spree/testing_support/store.rb +10 -0
- data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
- data/lib/tasks/channels.rake +94 -0
- data/lib/tasks/core.rake +1 -0
- data/lib/tasks/media.rake +27 -0
- data/lib/tasks/products.rake +4 -6
- data/lib/tasks/publications.rake +60 -0
- data/lib/tasks/upgrade.rake +211 -0
- metadata +83 -18
- data/app/finders/spree/variants/visible_finder.rb +0 -23
- data/app/paginators/spree/shared/paginate.rb +0 -30
- data/app/presenters/spree/filters/price_presenter.rb +0 -23
- data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
- data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
- data/app/presenters/spree/product_summary_presenter.rb +0 -27
- data/app/presenters/spree/variants/options_presenter.rb +0 -82
- data/app/services/spree/classifications/reposition.rb +0 -23
- data/app/sorters/spree/orders/sort.rb +0 -10
- data/lib/spree/core/controller_helpers/common.rb +0 -14
- data/lib/spree/core/token_generator.rb +0 -23
- data/lib/spree/database_type_utilities.rb +0 -22
- data/lib/spree/testing_support/bar_ability.rb +0 -14
- data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Stores
|
|
3
|
+
module Channels
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
has_many :channels, class_name: 'Spree::Channel', dependent: :destroy
|
|
8
|
+
has_one :default_channel, -> { default }, class_name: 'Spree::Channel'
|
|
9
|
+
|
|
10
|
+
after_create :ensure_default_channel
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def ensure_default_channel
|
|
14
|
+
return if default_channel
|
|
15
|
+
|
|
16
|
+
channels.create!(name: 'Online Store', code: Spree::Channel::DEFAULT_CODE)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -5,12 +5,9 @@ module Spree
|
|
|
5
5
|
|
|
6
6
|
included do
|
|
7
7
|
has_many :markets, class_name: 'Spree::Market', dependent: :destroy
|
|
8
|
-
|
|
8
|
+
has_one :default_market, -> { default }, class_name: 'Spree::Market'
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
# @return [Spree::Market, nil]
|
|
12
|
-
def default_market
|
|
13
|
-
@default_market ||= Spree::Market.default_for_store(self)
|
|
10
|
+
after_create :ensure_default_market
|
|
14
11
|
end
|
|
15
12
|
|
|
16
13
|
# Returns the default country, derived from the default market
|
|
@@ -112,6 +109,25 @@ module Spree
|
|
|
112
109
|
|
|
113
110
|
private
|
|
114
111
|
|
|
112
|
+
def ensure_default_market
|
|
113
|
+
return if markets.exists?
|
|
114
|
+
|
|
115
|
+
country = @default_country_for_market
|
|
116
|
+
return if country.blank?
|
|
117
|
+
|
|
118
|
+
iso_country = ISO3166::Country[country.iso]
|
|
119
|
+
|
|
120
|
+
Spree::Events.disable do
|
|
121
|
+
markets.create!(
|
|
122
|
+
name: country.name,
|
|
123
|
+
currency: iso_country&.currency_code || read_attribute(:default_currency) || 'USD',
|
|
124
|
+
default_locale: iso_country&.languages_official&.first || read_attribute(:default_locale) || 'en',
|
|
125
|
+
default: true,
|
|
126
|
+
countries: [country]
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
115
131
|
def legacy_supported_currencies_list
|
|
116
132
|
([default_currency] + read_attribute(:supported_currencies).to_s.split(',')).uniq.map(&:to_s).map do |code|
|
|
117
133
|
::Money::Currency.find(code.strip)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Shared flat-payload writer for `has_many` associations whose rows
|
|
3
|
+
# are STI-typed and carry preferences/calculator metadata. Used by
|
|
4
|
+
# {Spree::Promotion} (rules, actions) and {Spree::PriceList} (rules).
|
|
5
|
+
#
|
|
6
|
+
# The wire shape is `[{ type:, id?, preferences: {...}, calculator?: {...}, *_ids?: [...] }]`
|
|
7
|
+
# and the reconciliation is: existing rows update by `id`, new rows
|
|
8
|
+
# build via `find_by_api_type`, missing rows destroy. Falls through
|
|
9
|
+
# to AR's standard writer when assigned model instances (Rails
|
|
10
|
+
# internals do this on association swap).
|
|
11
|
+
#
|
|
12
|
+
# New-record parents defer the work into an `@pending_<assoc>` ivar
|
|
13
|
+
# so child rows can FK to a persisted parent — the consumer model
|
|
14
|
+
# is responsible for flushing those in an `after_save`.
|
|
15
|
+
module TypedAssociations
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Routes a flat-payload assignment to either the deferred buffer
|
|
21
|
+
# (new record) or {#reconcile_typed_association}.
|
|
22
|
+
#
|
|
23
|
+
# @param association [Symbol] e.g. `:promotion_rules`, `:price_rules`
|
|
24
|
+
# @param rows [Array<Hash>, Array<Spree::Base>, nil]
|
|
25
|
+
# @return [void]
|
|
26
|
+
def assign_typed_association(association, rows)
|
|
27
|
+
first = Array(rows).first
|
|
28
|
+
return public_send(:"#{association}=", rows) if first.nil? || first.is_a?(Spree.base_class)
|
|
29
|
+
|
|
30
|
+
pending = Array(rows).map { |entry| entry.respond_to?(:to_h) ? entry.to_h.with_indifferent_access : entry.with_indifferent_access }
|
|
31
|
+
|
|
32
|
+
if new_record?
|
|
33
|
+
instance_variable_set(:"@pending_#{association}", pending)
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
reconcile_typed_association(association, pending)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Flushes a pending payload from `@pending_<assoc>` (typically from
|
|
41
|
+
# an `after_save` hook) and clears the ivar.
|
|
42
|
+
#
|
|
43
|
+
# @param association [Symbol]
|
|
44
|
+
# @return [void]
|
|
45
|
+
def flush_pending_typed_association(association)
|
|
46
|
+
ivar = :"@pending_#{association}"
|
|
47
|
+
pending = instance_variable_get(ivar)
|
|
48
|
+
return unless pending
|
|
49
|
+
|
|
50
|
+
instance_variable_set(ivar, nil)
|
|
51
|
+
reconcile_typed_association(association, pending)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def reconcile_typed_association(association, rows)
|
|
55
|
+
collection = public_send(association)
|
|
56
|
+
kept_ids = rows.filter_map { |row| save_typed_association_row(collection, row) }
|
|
57
|
+
collection.where.not(id: kept_ids).destroy_all if kept_ids.any? || rows.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def save_typed_association_row(collection, row)
|
|
61
|
+
record = find_or_build_typed_association_row(collection, row)
|
|
62
|
+
return nil unless record
|
|
63
|
+
|
|
64
|
+
preferences = row[:preferences]
|
|
65
|
+
calculator = row[:calculator]
|
|
66
|
+
attrs = row.except(:id, :type, :preferences, :calculator)
|
|
67
|
+
|
|
68
|
+
# `*_ids` mass-assignment on a new record builds join rows with a
|
|
69
|
+
# nil parent FK and fails `presence: true` on autosave. Defer
|
|
70
|
+
# those until after the row itself is persisted.
|
|
71
|
+
deferred_ids, scalar_attrs = attrs.partition { |k, _| record.new_record? && k.to_s.end_with?('_ids') }
|
|
72
|
+
record.assign_attributes(scalar_attrs.to_h) if scalar_attrs.any?
|
|
73
|
+
|
|
74
|
+
preferences&.each do |key, value|
|
|
75
|
+
next unless record.has_preference?(key.to_sym)
|
|
76
|
+
|
|
77
|
+
record.set_preference(key.to_sym, decode_preference_value(key, value))
|
|
78
|
+
end
|
|
79
|
+
record.assign_calculator_attributes(calculator) if calculator.present? && record.respond_to?(:assign_calculator_attributes)
|
|
80
|
+
|
|
81
|
+
# Always save — `record.changed?` doesn't reflect preferences
|
|
82
|
+
# (serialized hash) or calculator association changes.
|
|
83
|
+
record.save!
|
|
84
|
+
|
|
85
|
+
deferred_ids.each { |key, value| record.public_send("#{key}=", value) }
|
|
86
|
+
record.save! if record.changed?
|
|
87
|
+
|
|
88
|
+
record.id
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Decode `*_ids` array preferences (`customer_group_ids`, `user_ids`,
|
|
92
|
+
# …) from prefixed strings to raw PKs. Plain-scalar / non-id
|
|
93
|
+
# preferences pass through unchanged.
|
|
94
|
+
def decode_preference_value(key, value)
|
|
95
|
+
return value unless key.to_s.end_with?('_ids') && value.is_a?(Array)
|
|
96
|
+
|
|
97
|
+
value.map do |v|
|
|
98
|
+
Spree::PrefixedId.prefixed_id?(v) ? Spree::PrefixedId.decode_prefixed_id(v) : v
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def find_or_build_typed_association_row(collection, row)
|
|
103
|
+
if row[:id].present?
|
|
104
|
+
id = Spree::PrefixedId.prefixed_id?(row[:id]) ? Spree::PrefixedId.decode_prefixed_id(row[:id]) : row[:id]
|
|
105
|
+
existing = collection.find { |r| r.id == id } || collection.find_by(id: id)
|
|
106
|
+
return existing if existing
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
klass = collection.proxy_association.klass.find_by_api_type(row[:type])
|
|
110
|
+
return nil unless klass
|
|
111
|
+
|
|
112
|
+
# `build(type:) + becomes(klass)` would leave a stale STI-parent stub
|
|
113
|
+
# in the collection target alongside the becomes'd copy; autosave then
|
|
114
|
+
# validates both under different identities and uniqueness checks fail.
|
|
115
|
+
record = klass.new
|
|
116
|
+
collection.proxy_association.add_to_target(record)
|
|
117
|
+
record
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -12,9 +12,35 @@ module Spree
|
|
|
12
12
|
include Spree::Searchable
|
|
13
13
|
include Spree::Publishable
|
|
14
14
|
|
|
15
|
+
# Opt-in escape hatch for admin-side customer creation. When the host
|
|
16
|
+
# `Spree::User` includes Devise's `:validatable`, password presence is
|
|
17
|
+
# enforced on every create; admin-created customers don't pick a
|
|
18
|
+
# password upfront — they claim the account later via password reset.
|
|
19
|
+
# The admin Customers controller flips `skip_password_validation = true`
|
|
20
|
+
# when no password was supplied; the storefront registration path never
|
|
21
|
+
# sets it, so customer self-signup still requires a password.
|
|
22
|
+
#
|
|
23
|
+
# Prepended (not just defined) so it sits above Devise's own
|
|
24
|
+
# `password_required?` and can fall through with `super` when the flag
|
|
25
|
+
# isn't set. On `LegacyUser` (gem default, no Devise) `super` raises
|
|
26
|
+
# NoMethodError if invoked — but the early return covers the only
|
|
27
|
+
# branch that ever reaches this method on that path, and `LegacyUser`
|
|
28
|
+
# doesn't call `password_required?` at all, so the override is a no-op
|
|
29
|
+
# there.
|
|
30
|
+
module SkipPasswordValidation
|
|
31
|
+
def password_required?
|
|
32
|
+
return false if skip_password_validation && encrypted_password.blank? && password.blank?
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
15
37
|
included do
|
|
16
38
|
has_prefix_id :cus # Stripe: cus_
|
|
17
39
|
|
|
40
|
+
attr_accessor :skip_password_validation
|
|
41
|
+
|
|
42
|
+
prepend SkipPasswordValidation
|
|
43
|
+
|
|
18
44
|
# Enable lifecycle events for user models
|
|
19
45
|
publishes_lifecycle_events
|
|
20
46
|
|
|
@@ -36,6 +62,10 @@ module Spree
|
|
|
36
62
|
normalizes :email, :first_name, :last_name, with: ->(value) { value&.to_s&.squish&.presence }
|
|
37
63
|
acts_as_taggable_on :tags
|
|
38
64
|
|
|
65
|
+
def tags=(tags)
|
|
66
|
+
self.tag_list = tags
|
|
67
|
+
end
|
|
68
|
+
|
|
39
69
|
#
|
|
40
70
|
# Associations
|
|
41
71
|
#
|
|
@@ -75,30 +105,59 @@ module Spree
|
|
|
75
105
|
end
|
|
76
106
|
|
|
77
107
|
def self.search(query)
|
|
78
|
-
sanitized_query = sanitize_query_for_search(query)
|
|
79
108
|
return none if query.blank?
|
|
80
109
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
# `search_condition` handles sanitization itself — it escapes LIKE
|
|
111
|
+
# wildcards for plain columns and compares encrypted columns by
|
|
112
|
+
# equality. Pass the raw query so it can branch correctly.
|
|
113
|
+
conditions = []
|
|
114
|
+
conditions << search_condition(self, :email, query)
|
|
115
|
+
conditions << search_condition(self, :first_name, query)
|
|
116
|
+
conditions << search_condition(self, :last_name, query)
|
|
85
117
|
|
|
86
|
-
full_name = NameOfPerson::PersonName.full(
|
|
118
|
+
full_name = NameOfPerson::PersonName.full(query.to_s.strip)
|
|
87
119
|
|
|
88
120
|
if full_name.first.present? && full_name.last.present?
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
conditions << search_condition(self, :first_name, full_name.first)
|
|
122
|
+
conditions << search_condition(self, :last_name, full_name.last)
|
|
91
123
|
end
|
|
92
124
|
|
|
93
|
-
where(
|
|
125
|
+
where(conditions.reduce(:or))
|
|
94
126
|
end
|
|
95
127
|
|
|
96
128
|
# Backward compatibility alias — remove in Spree 6.0
|
|
97
129
|
def self.multi_search(query) = search(query)
|
|
98
130
|
|
|
99
|
-
self.whitelisted_ransackable_associations = %w[bill_address ship_address addresses tags spree_roles]
|
|
100
|
-
self.whitelisted_ransackable_attributes = %w[id email first_name last_name accepts_email_marketing
|
|
101
|
-
|
|
131
|
+
self.whitelisted_ransackable_associations = %w[bill_address ship_address addresses tags spree_roles orders customer_groups]
|
|
132
|
+
self.whitelisted_ransackable_attributes = %w[id email first_name last_name phone accepts_email_marketing
|
|
133
|
+
created_at updated_at last_sign_in_at]
|
|
134
|
+
self.whitelisted_ransackable_scopes = %w[search multi_search with_min_total_spent]
|
|
135
|
+
|
|
136
|
+
scope :with_min_total_spent, ->(amount) {
|
|
137
|
+
joins(:orders).where(spree_orders: { state: 'complete' }).
|
|
138
|
+
group("#{table_name}.id").
|
|
139
|
+
having('SUM(spree_orders.total) >= ?', amount.to_d)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Precomputes orders_count, total_spent, and last_order_completed_at via a
|
|
143
|
+
# single aggregate join so list endpoints avoid 4 queries per user. The
|
|
144
|
+
# values land on the user as virtual attributes.
|
|
145
|
+
scope :with_order_aggregates, -> {
|
|
146
|
+
order_table = Spree::Order.table_name
|
|
147
|
+
select(
|
|
148
|
+
"#{table_name}.*, " \
|
|
149
|
+
"COALESCE(orders_agg.orders_count, 0) AS orders_count, " \
|
|
150
|
+
"COALESCE(orders_agg.total_spent, 0) AS total_spent, " \
|
|
151
|
+
"orders_agg.last_order_completed_at AS last_order_completed_at"
|
|
152
|
+
).joins(
|
|
153
|
+
"LEFT JOIN (" \
|
|
154
|
+
"SELECT user_id, COUNT(*) AS orders_count, SUM(total) AS total_spent, MAX(completed_at) AS last_order_completed_at " \
|
|
155
|
+
"FROM #{order_table} " \
|
|
156
|
+
"WHERE state = 'complete' AND user_id IS NOT NULL " \
|
|
157
|
+
"GROUP BY user_id" \
|
|
158
|
+
") orders_agg ON orders_agg.user_id = #{table_name}.id"
|
|
159
|
+
)
|
|
160
|
+
}
|
|
102
161
|
|
|
103
162
|
def self.with_email(query)
|
|
104
163
|
where("#{table_name}.email LIKE ?", "%#{query}%")
|
data/app/models/spree/ability.rb
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
# Implementation class for Cancan gem.
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
# The preferred way to add permissions is now through permission sets. See Spree::PermissionSets::Base
|
|
5
|
-
# for more details on creating custom permission sets.
|
|
1
|
+
# Implementation class for Cancan gem. Permissions are configured through
|
|
2
|
+
# permission sets — see Spree::PermissionSets::Base for details on creating
|
|
3
|
+
# custom ones.
|
|
6
4
|
#
|
|
7
5
|
# @example Configuring role permissions
|
|
8
6
|
# Spree.permissions.assign(:customer_service, [
|
|
@@ -17,37 +15,12 @@ module Spree
|
|
|
17
15
|
class Ability
|
|
18
16
|
include CanCan::Ability
|
|
19
17
|
|
|
20
|
-
class_attribute :abilities
|
|
21
|
-
self.abilities = Set.new
|
|
22
|
-
|
|
23
18
|
# @return [Object] the current user
|
|
24
19
|
attr_reader :user
|
|
25
20
|
|
|
26
21
|
# @return [Spree::Store, nil] the current store
|
|
27
22
|
attr_reader :store
|
|
28
23
|
|
|
29
|
-
# Allows us to go beyond the standard cancan initialize method which makes it difficult for engines to
|
|
30
|
-
# modify the default +Ability+ of an application. The +ability+ argument must be a class that includes
|
|
31
|
-
# the +CanCan::Ability+ module. The registered ability should behave properly as a stand-alone class
|
|
32
|
-
# and therefore should be easy to test in isolation.
|
|
33
|
-
# @deprecated Use Spree::PermissionSets instead. Will be removed in Spree 5.5.
|
|
34
|
-
def self.register_ability(ability)
|
|
35
|
-
Spree::Deprecation.warn(
|
|
36
|
-
'Spree::Ability.register_ability is deprecated and will be removed in Spree 5.5. ' \
|
|
37
|
-
'Please use Spree::PermissionSets instead. See Spree::PermissionSets::Base for details.'
|
|
38
|
-
)
|
|
39
|
-
abilities.add(ability)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# @deprecated Use Spree::PermissionSets instead. Will be removed in Spree 5.5.
|
|
43
|
-
def self.remove_ability(ability)
|
|
44
|
-
Spree::Deprecation.warn(
|
|
45
|
-
'Spree::Ability.remove_ability is deprecated and will be removed in Spree 5.5. ' \
|
|
46
|
-
'Please use Spree::PermissionSets instead. See Spree::PermissionSets::Base for details.'
|
|
47
|
-
)
|
|
48
|
-
abilities.delete(ability)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
24
|
def initialize(user, options = {})
|
|
52
25
|
alias_cancan_delete_action
|
|
53
26
|
|
|
@@ -55,24 +28,10 @@ module Spree
|
|
|
55
28
|
@store = options[:store] || Spree::Current.store
|
|
56
29
|
|
|
57
30
|
apply_permissions_from_sets
|
|
58
|
-
|
|
59
|
-
# Include any abilities registered by extensions, etc.
|
|
60
|
-
# this is legacy behaviour and should be removed in Spree 5.0
|
|
61
|
-
Ability.abilities.merge(abilities_to_register).each do |clazz|
|
|
62
|
-
Spree::Deprecation.warn("Ability merging is deprecated and will be removed in Spree 5.5. Please use Permission Sets")
|
|
63
|
-
|
|
64
|
-
merge clazz.new(@user)
|
|
65
|
-
end
|
|
66
31
|
end
|
|
67
32
|
|
|
68
33
|
protected
|
|
69
34
|
|
|
70
|
-
# you can override this method to register your abilities
|
|
71
|
-
# this method has to return array of classes
|
|
72
|
-
def abilities_to_register
|
|
73
|
-
[]
|
|
74
|
-
end
|
|
75
|
-
|
|
76
35
|
def alias_cancan_delete_action
|
|
77
36
|
alias_action :delete, to: :destroy
|
|
78
37
|
alias_action :create, :update, :destroy, to: :modify
|
|
@@ -82,13 +41,7 @@ module Spree
|
|
|
82
41
|
def apply_permissions_from_sets
|
|
83
42
|
role_names = determine_role_names
|
|
84
43
|
permission_sets = Spree.permissions.permission_sets_for_roles(role_names)
|
|
85
|
-
|
|
86
|
-
# If no permission sets are configured for the user's roles, use legacy behavior
|
|
87
|
-
if permission_sets.empty?
|
|
88
|
-
apply_legacy_permissions
|
|
89
|
-
else
|
|
90
|
-
activate_permission_sets(permission_sets)
|
|
91
|
-
end
|
|
44
|
+
activate_permission_sets(permission_sets)
|
|
92
45
|
end
|
|
93
46
|
|
|
94
47
|
# Determines the role names for the current user.
|
|
@@ -121,71 +74,5 @@ module Spree
|
|
|
121
74
|
permission_set.activate!
|
|
122
75
|
end
|
|
123
76
|
end
|
|
124
|
-
|
|
125
|
-
# Legacy permission application for backward compatibility.
|
|
126
|
-
# This is used when no permission sets are configured for the user's roles.
|
|
127
|
-
def apply_legacy_permissions
|
|
128
|
-
if @user.persisted? && @user.is_a?(Spree.admin_user_class) && @user.try(:spree_admin?, @store)
|
|
129
|
-
apply_admin_permissions(@user, { store: @store })
|
|
130
|
-
else
|
|
131
|
-
apply_user_permissions(@user, { store: @store })
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
protect_admin_role
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def apply_admin_permissions(_user, _options)
|
|
138
|
-
Spree::Deprecation.warn("Ability#apply_admin_permissions is deprecated and will be removed in Spree 5.5. Please use Permission Sets")
|
|
139
|
-
can :manage, :all
|
|
140
|
-
cannot :cancel, Spree::Order
|
|
141
|
-
can :cancel, Spree::Order, &:allow_cancel?
|
|
142
|
-
cannot :destroy, Spree::Order
|
|
143
|
-
can :destroy, Spree::Order, &:can_be_deleted?
|
|
144
|
-
cannot [:edit, :update], Spree::RefundReason, mutable: false
|
|
145
|
-
cannot [:edit, :update], Spree::ReimbursementType, mutable: false
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def apply_user_permissions(user, _options)
|
|
149
|
-
Spree::Deprecation.warn("Ability#apply_user_permissions is deprecated and will be removed in Spree 5.5. Please use Permission Sets")
|
|
150
|
-
|
|
151
|
-
can :read, ::Spree::Country
|
|
152
|
-
can :read, ::Spree::OptionType
|
|
153
|
-
can :read, ::Spree::OptionValue
|
|
154
|
-
can :create, ::Spree::Order
|
|
155
|
-
can :show, ::Spree::Order do |order, token|
|
|
156
|
-
order.user == user || order.token && token == order.token
|
|
157
|
-
end
|
|
158
|
-
can :update, ::Spree::Order do |order, token|
|
|
159
|
-
!order.completed? && (order.user == user || order.token && token == order.token)
|
|
160
|
-
end
|
|
161
|
-
# Address management - only for persisted users with matching user_id
|
|
162
|
-
can :manage, ::Spree::Address, user_id: user.id if user.persisted?
|
|
163
|
-
can [:read, :destroy], ::Spree::CreditCard, user_id: user.id
|
|
164
|
-
can :read, ::Spree::Product
|
|
165
|
-
can :create, ::Spree.user_class
|
|
166
|
-
can [:show, :update, :destroy], ::Spree.user_class, id: user.id
|
|
167
|
-
can :read, ::Spree::State
|
|
168
|
-
can :read, ::Spree::Store
|
|
169
|
-
can :read, ::Spree::Taxon
|
|
170
|
-
can :read, ::Spree::Taxonomy
|
|
171
|
-
can :read, ::Spree::Variant
|
|
172
|
-
can :read, ::Spree::Zone
|
|
173
|
-
can :manage, ::Spree::Wishlist, user_id: user.id
|
|
174
|
-
can :show, ::Spree::Wishlist do |wishlist|
|
|
175
|
-
wishlist.user == user || wishlist.is_private == false
|
|
176
|
-
end
|
|
177
|
-
can [:create, :update, :destroy], ::Spree::WishedItem do |wished_item|
|
|
178
|
-
wished_item.wishlist.user == user
|
|
179
|
-
end
|
|
180
|
-
can :accept, Spree::Invitation, invitee_id: [user.id, nil], invitee_type: user.class.name, status: 'pending'
|
|
181
|
-
can :show, ::Spree::DigitalLink do |digital_link, token|
|
|
182
|
-
digital_link.token == token
|
|
183
|
-
end
|
|
184
|
-
can :read, ::Spree::Policy
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def protect_admin_role
|
|
188
|
-
cannot [:update, :destroy], ::Spree::Role, name: ['admin']
|
|
189
|
-
end
|
|
190
77
|
end
|
|
191
78
|
end
|
data/app/models/spree/api_key.rb
CHANGED
|
@@ -6,6 +6,36 @@ module Spree
|
|
|
6
6
|
PREFIXES = { 'publishable' => 'pk_', 'secret' => 'sk_' }.freeze
|
|
7
7
|
TOKEN_LENGTH = 24
|
|
8
8
|
|
|
9
|
+
# Admin API authorization scopes. Granted to secret keys at creation; checked
|
|
10
|
+
# by ScopedAuthorization on every admin request. See
|
|
11
|
+
# docs/plans/5.5-admin-api-key-scopes.md for the full design.
|
|
12
|
+
SCOPES = %w[
|
|
13
|
+
read_orders write_orders
|
|
14
|
+
read_products write_products
|
|
15
|
+
read_customers write_customers
|
|
16
|
+
read_payments write_payments
|
|
17
|
+
read_fulfillments write_fulfillments
|
|
18
|
+
read_refunds write_refunds
|
|
19
|
+
read_gift_cards write_gift_cards
|
|
20
|
+
read_store_credits write_store_credits
|
|
21
|
+
read_stock write_stock
|
|
22
|
+
read_categories write_categories
|
|
23
|
+
read_custom_field_definitions write_custom_field_definitions
|
|
24
|
+
read_exports write_exports
|
|
25
|
+
read_settings write_settings
|
|
26
|
+
read_webhooks write_webhooks
|
|
27
|
+
read_dashboard
|
|
28
|
+
read_all write_all
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# Scopes are stored in a JSON column (jsonb on PostgreSQL, json elsewhere).
|
|
32
|
+
# The DB driver handles array <-> JSON conversion; no `serialize` needed.
|
|
33
|
+
attribute :scopes, default: []
|
|
34
|
+
|
|
35
|
+
def scopes=(value)
|
|
36
|
+
super(Array(value).map(&:to_s).reject(&:blank?))
|
|
37
|
+
end
|
|
38
|
+
|
|
9
39
|
# Returns the raw token value. For publishable keys this is the persisted
|
|
10
40
|
# +token+ column. For secret keys it is only available in memory immediately
|
|
11
41
|
# after creation (not persisted).
|
|
@@ -25,6 +55,8 @@ module Spree
|
|
|
25
55
|
validates :token_digest, presence: true, uniqueness: true, if: :secret?
|
|
26
56
|
validates :token_prefix, presence: true, if: :secret?
|
|
27
57
|
validates :store, presence: true
|
|
58
|
+
validates :scopes, presence: true, if: :secret?
|
|
59
|
+
validate :validate_known_scopes, if: :secret?
|
|
28
60
|
|
|
29
61
|
before_validation :generate_token, on: :create
|
|
30
62
|
|
|
@@ -83,8 +115,29 @@ module Spree
|
|
|
83
115
|
update!(revoked_at: Time.current, revoked_by: user)
|
|
84
116
|
end
|
|
85
117
|
|
|
118
|
+
# Whether this key carries the given scope. `write_*` implies the matching
|
|
119
|
+
# `read_*`; `read_all` / `write_all` aliases expand to every read / read+write
|
|
120
|
+
# scope respectively.
|
|
121
|
+
#
|
|
122
|
+
# @param scope [String]
|
|
123
|
+
# @return [Boolean]
|
|
124
|
+
def has_scope?(scope)
|
|
125
|
+
scope = scope.to_s
|
|
126
|
+
return true if scopes.include?(scope)
|
|
127
|
+
return true if scope.start_with?('read_') && scopes.include?("write_#{scope.delete_prefix('read_')}")
|
|
128
|
+
return true if scopes.include?('write_all')
|
|
129
|
+
return true if scope.start_with?('read_') && scopes.include?('read_all')
|
|
130
|
+
|
|
131
|
+
false
|
|
132
|
+
end
|
|
133
|
+
|
|
86
134
|
private
|
|
87
135
|
|
|
136
|
+
def validate_known_scopes
|
|
137
|
+
invalid = scopes - SCOPES
|
|
138
|
+
errors.add(:scopes, "contains unknown scopes: #{invalid.join(', ')}") if invalid.any?
|
|
139
|
+
end
|
|
140
|
+
|
|
88
141
|
# Generates the token on creation. For publishable keys, stores the raw token
|
|
89
142
|
# in the +token+ column. For secret keys, computes an HMAC-SHA256 digest stored
|
|
90
143
|
# in +token_digest+, saves the first 12 characters as +token_prefix+ for display,
|
data/app/models/spree/asset.rb
CHANGED
|
@@ -28,6 +28,9 @@ module Spree
|
|
|
28
28
|
after_initialize { self.media_type ||= 'image' }
|
|
29
29
|
|
|
30
30
|
belongs_to :viewable, polymorphic: true, touch: true
|
|
31
|
+
has_many :variant_media, class_name: 'Spree::VariantMedia', foreign_key: :media_id,
|
|
32
|
+
dependent: :destroy, inverse_of: :asset
|
|
33
|
+
has_many :variants, through: :variant_media, source: :variant, class_name: 'Spree::Variant'
|
|
31
34
|
acts_as_list scope: [:viewable_id, :viewable_type]
|
|
32
35
|
|
|
33
36
|
delegate :key, :attached?, :variant, :variable?, :blob, :filename, :variation, to: :attachment
|
|
@@ -91,6 +94,15 @@ module Spree
|
|
|
91
94
|
end
|
|
92
95
|
end
|
|
93
96
|
|
|
97
|
+
# Accepts prefixed IDs ("variant_abc") or raw IDs from admin forms.
|
|
98
|
+
# Variants from a different product are silently dropped — the security
|
|
99
|
+
# boundary against form tampering.
|
|
100
|
+
def variant_ids=(ids)
|
|
101
|
+
return if viewable_type != 'Spree::Product' || product.blank?
|
|
102
|
+
|
|
103
|
+
super(Spree::VariantMedia.resolve_variant_ids(product, ids || []))
|
|
104
|
+
end
|
|
105
|
+
|
|
94
106
|
def focal_point
|
|
95
107
|
return nil if focal_point_x.nil? || focal_point_y.nil?
|
|
96
108
|
|
|
@@ -142,14 +154,21 @@ module Spree
|
|
|
142
154
|
private
|
|
143
155
|
|
|
144
156
|
def touch_product_variants
|
|
145
|
-
viewable.product
|
|
157
|
+
product = viewable.is_a?(Spree::Product) ? viewable : viewable.product
|
|
158
|
+
product.variants.touch_all
|
|
146
159
|
end
|
|
147
160
|
|
|
148
161
|
def should_touch_product_variants?
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
return false unless saved_change_to_position?
|
|
163
|
+
|
|
164
|
+
case viewable
|
|
165
|
+
when Spree::Product
|
|
166
|
+
true
|
|
167
|
+
when Spree::Variant
|
|
168
|
+
viewable.is_master? && viewable.product.has_variants?
|
|
169
|
+
else
|
|
170
|
+
false
|
|
171
|
+
end
|
|
153
172
|
end
|
|
154
173
|
|
|
155
174
|
def increment_viewable_media_count
|
|
@@ -179,6 +198,10 @@ module Spree
|
|
|
179
198
|
viewable.product.update_thumbnail!
|
|
180
199
|
when Spree::Product
|
|
181
200
|
viewable.update_thumbnail!
|
|
201
|
+
# Linked variants resolve their own thumbnail through gallery_media,
|
|
202
|
+
# which sorts by this asset's product-level position. Reorders or
|
|
203
|
+
# destroys here can change a linked variant's first asset.
|
|
204
|
+
variants.find_each(&:update_thumbnail!)
|
|
182
205
|
end
|
|
183
206
|
end
|
|
184
207
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module Authentication
|
|
5
|
+
# Keyed registry of authentication strategy classes for the Store and Admin APIs.
|
|
6
|
+
#
|
|
7
|
+
# Strategies are dispatched by the `provider` value the client sends to the auth
|
|
8
|
+
# endpoint, so the registry is a key → class map. The `:email` key is reserved for
|
|
9
|
+
# the built-in {Spree::Authentication::Strategies::EmailPasswordStrategy}; integrators
|
|
10
|
+
# can override it by adding a different class under the same key.
|
|
11
|
+
#
|
|
12
|
+
# @example Registering a custom provider
|
|
13
|
+
# Spree.store_authentication_strategies.add(:auth0, MyApp::Auth::Auth0Strategy)
|
|
14
|
+
#
|
|
15
|
+
# @example Removing a provider
|
|
16
|
+
# Spree.store_authentication_strategies.remove(:email)
|
|
17
|
+
#
|
|
18
|
+
# @example Reading a strategy class
|
|
19
|
+
# Spree.store_authentication_strategies[:email]
|
|
20
|
+
class StrategyRegistry
|
|
21
|
+
extend Forwardable
|
|
22
|
+
include Enumerable
|
|
23
|
+
|
|
24
|
+
def_delegators :@strategies, :keys, :values, :each
|
|
25
|
+
|
|
26
|
+
def initialize(strategies = {})
|
|
27
|
+
@strategies = {}
|
|
28
|
+
strategies.each { |key, klass| add(key, klass) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Register a strategy class under the given provider key. Overwrites any
|
|
32
|
+
# existing entry for that key.
|
|
33
|
+
#
|
|
34
|
+
# @param key [Symbol, String] provider identifier sent by the client
|
|
35
|
+
# @param strategy_class [Class] strategy class (typically a subclass of
|
|
36
|
+
# {Spree::Authentication::Strategies::BaseStrategy})
|
|
37
|
+
# @return [Class] the registered class
|
|
38
|
+
def add(key, strategy_class)
|
|
39
|
+
@strategies[key.to_sym] = strategy_class
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Unregister a strategy. Idempotent — returns `nil` if the key is not present.
|
|
43
|
+
#
|
|
44
|
+
# @param key [Symbol, String]
|
|
45
|
+
# @return [Class, nil] the removed class, or nil if no such key
|
|
46
|
+
def remove(key)
|
|
47
|
+
@strategies.delete(key.to_sym)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Look up a registered strategy class.
|
|
51
|
+
#
|
|
52
|
+
# @param key [Symbol, String] provider identifier
|
|
53
|
+
# @return [Class, nil] the registered strategy class, or nil if no such key
|
|
54
|
+
def [](key)
|
|
55
|
+
@strategies[key.to_sym]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Whether a strategy is registered under the given provider key.
|
|
59
|
+
#
|
|
60
|
+
# @param key [Symbol, String]
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def key?(key)
|
|
63
|
+
@strategies.key?(key.to_sym)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Hash{Symbol => Class}] a shallow copy of the underlying map
|
|
67
|
+
def to_h
|
|
68
|
+
@strategies.dup
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|