spree_api 5.5.3 → 5.6.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/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +3 -1
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +1 -1
- data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +4 -2
- data/app/controllers/concerns/spree/api/v3/locale_and_currency.rb +4 -1
- data/app/controllers/concerns/spree/api/v3/storefront_gating.rb +65 -0
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/categories/products_controller.rb +98 -0
- data/app/controllers/spree/api/v3/admin/categories_controller.rb +102 -2
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +8 -2
- data/app/controllers/spree/api/v3/admin/exports_controller.rb +3 -1
- data/app/controllers/spree/api/v3/admin/import_rows_controller.rb +49 -0
- data/app/controllers/spree/api/v3/admin/imports_controller.rb +274 -0
- data/app/controllers/spree/api/v3/admin/locales_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +39 -9
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +62 -2
- data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +105 -0
- data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +1 -8
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/products_controller.rb +8 -7
- data/app/controllers/spree/api/v3/admin/promotions_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/resource_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/store_controller.rb +3 -0
- data/app/controllers/spree/api/v3/admin/translatable_resources_controller.rb +38 -0
- data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +113 -0
- data/app/controllers/spree/api/v3/admin/translations_controller.rb +106 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +0 -2
- data/app/controllers/spree/api/v3/store/auth_controller.rb +3 -1
- data/app/controllers/spree/api/v3/store/base_controller.rb +5 -0
- data/app/controllers/spree/api/v3/store/carts/fulfillments_controller.rb +1 -3
- data/app/controllers/spree/api/v3/store/carts_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/countries_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/currencies_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +5 -1
- data/app/controllers/spree/api/v3/store/customers_controller.rb +3 -1
- data/app/controllers/spree/api/v3/store/digitals_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/locales_controller.rb +3 -8
- data/app/controllers/spree/api/v3/store/markets/countries_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/markets_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +5 -2
- data/app/controllers/spree/api/v3/store/products_controller.rb +1 -1
- data/app/controllers/spree/api/v3/store/resource_controller.rb +1 -0
- data/app/serializers/concerns/spree/api/v3/admin/translatable.rb +38 -0
- data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +7 -1
- data/app/serializers/spree/api/v3/admin/category_serializer.rb +5 -3
- data/app/serializers/spree/api/v3/admin/channel_serializer.rb +5 -1
- data/app/serializers/spree/api/v3/admin/customer_serializer.rb +7 -2
- data/app/serializers/spree/api/v3/admin/discount_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +9 -0
- data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +7 -0
- data/app/serializers/spree/api/v3/admin/import_mapping_serializer.rb +20 -0
- data/app/serializers/spree/api/v3/admin/import_row_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/admin/import_serializer.rb +70 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +11 -0
- data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +15 -1
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/product_serializer.rb +1 -0
- data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +8 -0
- data/app/serializers/spree/api/v3/admin/resource_translations_serializer.rb +76 -0
- data/app/serializers/spree/api/v3/admin/setup_task_serializer.rb +18 -0
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +27 -1
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +8 -1
- data/app/serializers/spree/api/v3/base_serializer.rb +13 -0
- data/app/serializers/spree/api/v3/cart_serializer.rb +25 -22
- data/app/serializers/spree/api/v3/discount_serializer.rb +6 -2
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +16 -13
- data/app/serializers/spree/api/v3/gift_card_serializer.rb +15 -11
- data/app/serializers/spree/api/v3/import_row_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/line_item_serializer.rb +35 -17
- data/app/serializers/spree/api/v3/locale_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/order_serializer.rb +25 -21
- data/app/serializers/spree/api/v3/payment_serializer.rb +6 -2
- data/app/serializers/spree/api/v3/product_serializer.rb +9 -1
- data/app/serializers/spree/api/v3/variant_serializer.rb +12 -1
- data/app/services/spree/api/v3/filters_aggregator.rb +4 -4
- data/app/subscribers/spree/webhook_event_subscriber.rb +6 -0
- data/config/initializers/typelizer.rb +10 -0
- data/config/locales/en.yml +4 -0
- data/config/routes.rb +54 -4
- data/lib/spree/api/dependencies.rb +5 -0
- data/lib/spree/api/openapi/schema_helper.rb +29 -0
- metadata +21 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 89ddc07a0f4f9334f2ecb79bf2283986e8078e5566d4a7b1c672fadd2f04453d
|
|
4
|
+
data.tar.gz: 1db32c6bf66382dc2786d1898aaaf5e6c3e1a04b721404fad423c37cc3e0326b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 001b8a751f6cd60f71ee162e4387570f8976bd47a3c6794f6d850ba42fe157c3850a137646fed9f6c22651e9d86bf08bad579f2a6854370442cc9dbec0ae1c3f
|
|
7
|
+
data.tar.gz: 6eb41a561383ce8ab469c21c1a341673164fadb3f5e738ccb7b0635bccd7ff56d6d280d30bcd3aa1bea852763b4f13908b96e45b071013736013c759f71f48fe
|
|
@@ -78,13 +78,15 @@ module Spree
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
# The caller's store-scoped role names, fetched once per request.
|
|
81
|
+
# Scoped by store_id so the caller's own privileges are recognized even
|
|
82
|
+
# when their role is held on a non-store resource bound to this store.
|
|
81
83
|
def caller_role_names
|
|
82
84
|
return @caller_role_names if defined?(@caller_role_names)
|
|
83
85
|
|
|
84
86
|
user = try_spree_current_user
|
|
85
87
|
@caller_role_names =
|
|
86
88
|
if user.respond_to?(:role_users)
|
|
87
|
-
user.role_users.where(
|
|
89
|
+
user.role_users.where(store: current_store).joins(:role).
|
|
88
90
|
pluck("#{Spree::Role.table_name}.name")
|
|
89
91
|
else
|
|
90
92
|
[]
|
|
@@ -64,7 +64,7 @@ module Spree
|
|
|
64
64
|
def current_user_member_of_store?
|
|
65
65
|
return false unless current_user.respond_to?(:role_users)
|
|
66
66
|
|
|
67
|
-
current_user.role_users.exists?(
|
|
67
|
+
current_user.role_users.exists?(store: current_store)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def set_no_store_cache
|
|
@@ -35,8 +35,10 @@ module Spree
|
|
|
35
35
|
# +Spree::Current.channel+ itself, which avoids one query per
|
|
36
36
|
# header-less API request.
|
|
37
37
|
def set_current_channel
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
# Memoize so a later +current_channel+ call (e.g. the storefront gate,
|
|
39
|
+
# serializer params) reuses this row instead of re-querying the header.
|
|
40
|
+
@current_channel = channel_from_header
|
|
41
|
+
Spree::Current.channel = @current_channel if @current_channel
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def channel_from_header
|
|
@@ -114,9 +114,12 @@ module Spree
|
|
|
114
114
|
|
|
115
115
|
private
|
|
116
116
|
|
|
117
|
-
# Sets +I18n.locale+ and +Spree::Current.locale+ from the resolved
|
|
117
|
+
# Sets +I18n.locale+ and +Spree::Current.locale+ from the resolved
|
|
118
|
+
# locale, and records the request's content locale — the store's
|
|
119
|
+
# default locale, in which base (untranslated) columns are authored.
|
|
118
120
|
def set_locale
|
|
119
121
|
Spree::Current.locale = current_locale
|
|
122
|
+
Spree::Current.content_locale = current_store&.default_locale
|
|
120
123
|
I18n.locale = current_locale
|
|
121
124
|
end
|
|
122
125
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
# Enforces channel-level storefront access gating on the Store API.
|
|
5
|
+
# The posture is resolved from the request's channel
|
|
6
|
+
# (+Spree::Channel#resolved_storefront_access+, with store fallback):
|
|
7
|
+
#
|
|
8
|
+
# - +login_required+ → 401 on every gated read for unauthenticated requests
|
|
9
|
+
# - +prices_hidden+ → price fields serialized as +null+ for guests
|
|
10
|
+
#
|
|
11
|
+
# "Guest" means no authenticated customer (publishable key or guest cart
|
|
12
|
+
# token without a customer JWT). Logged-in customers are never gated.
|
|
13
|
+
module StorefrontGating
|
|
14
|
+
extend ActiveSupport::Concern
|
|
15
|
+
|
|
16
|
+
included do
|
|
17
|
+
before_action :enforce_storefront_login_required!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class_methods do
|
|
21
|
+
# Opts a controller out of the +login_required+ gate. Use for endpoints
|
|
22
|
+
# that must stay reachable before authentication — sign in / register,
|
|
23
|
+
# password reset, and pre-login reference data (countries, currencies,
|
|
24
|
+
# locales, markets, newsletter, tokenized digital downloads).
|
|
25
|
+
def allow_guest_storefront_access!
|
|
26
|
+
skip_before_action :enforce_storefront_login_required!, raise: false
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
# @return [Boolean] whether prices must be hidden from this request.
|
|
33
|
+
def hide_prices?
|
|
34
|
+
try_spree_current_user.blank? && !!current_channel&.storefront_prices_hidden?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Injects the price-hiding flag so the shared +price_for+/+price_in+
|
|
38
|
+
# serializer helpers null prices for gated guests.
|
|
39
|
+
def serializer_params
|
|
40
|
+
super.merge(hide_prices: hide_prices?)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Renders a 401 with the shared +authentication_required+ error code.
|
|
44
|
+
# @param message_key [String] i18n key for the error message
|
|
45
|
+
# @param default_message [String] fallback when the key is missing
|
|
46
|
+
def render_authentication_required(message_key, default_message)
|
|
47
|
+
render_error(
|
|
48
|
+
code: ErrorHandler::ERROR_CODES[:authentication_required],
|
|
49
|
+
message: Spree.t(message_key, default: default_message),
|
|
50
|
+
status: :unauthorized
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def enforce_storefront_login_required!
|
|
57
|
+
return if try_spree_current_user.present?
|
|
58
|
+
return unless current_channel&.storefront_login_required?
|
|
59
|
+
|
|
60
|
+
render_authentication_required('api.errors.storefront_login_required', 'Authentication required to access this store')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
module Categories
|
|
6
|
+
# Manages the products classified under a category — the manual
|
|
7
|
+
# membership half of the old Rails admin's taxon "Products" panel.
|
|
8
|
+
# Listing is ordered by the classification position so the merchant's
|
|
9
|
+
# manual ordering round-trips; +reposition+ persists a drag-to-reorder.
|
|
10
|
+
class ProductsController < ResourceController
|
|
11
|
+
scoped_resource :products
|
|
12
|
+
|
|
13
|
+
# Skip the base single-resource load — membership actions resolve
|
|
14
|
+
# the product against the category's scope themselves (set_product).
|
|
15
|
+
skip_before_action :set_resource
|
|
16
|
+
before_action :set_product, only: [:destroy, :reposition]
|
|
17
|
+
|
|
18
|
+
# POST /api/v3/admin/categories/:category_id/products
|
|
19
|
+
# Body: { product_id: 'prod_…' }
|
|
20
|
+
def create
|
|
21
|
+
product = product_scope.find_by_prefix_id!(params[:product_id])
|
|
22
|
+
Spree::Taxons::AddProducts.call(taxons: [@category], products: [product])
|
|
23
|
+
render json: serialize_resource(product), status: :created
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# DELETE /api/v3/admin/categories/:category_id/products/:id
|
|
27
|
+
# Bulk removal goes through the existing
|
|
28
|
+
# POST /products/bulk_remove_from_categories ({ ids, category_ids }).
|
|
29
|
+
def destroy
|
|
30
|
+
Spree::Taxons::RemoveProducts.call(taxons: [@category], products: [@product])
|
|
31
|
+
head :no_content
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# PATCH /api/v3/admin/categories/:category_id/products/:id/reposition
|
|
35
|
+
# Body: { new_position: 0 } — 0-based index among the category's products.
|
|
36
|
+
# @product is already constrained to the category's scope, so a
|
|
37
|
+
# classification always exists.
|
|
38
|
+
def reposition
|
|
39
|
+
position = integer_param(:new_position)
|
|
40
|
+
return render_invalid_position if position.nil?
|
|
41
|
+
|
|
42
|
+
classification_for(@product).insert_at(position + 1)
|
|
43
|
+
head :no_content
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protected
|
|
47
|
+
|
|
48
|
+
def model_class
|
|
49
|
+
Spree::Product
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def serializer_class
|
|
53
|
+
Spree.api.admin_product_serializer
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The category's products, ordered by classification position.
|
|
57
|
+
def scope
|
|
58
|
+
product_scope.
|
|
59
|
+
joins(:classifications).
|
|
60
|
+
where(Spree::Classification.table_name => { taxon_id: @category.id }).
|
|
61
|
+
order(Spree::Classification.table_name => { position: :asc })
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A product is classified at most once per taxon (unique [taxon_id,
|
|
65
|
+
# product_id]), so the join can't duplicate rows — DISTINCT is
|
|
66
|
+
# unnecessary and breaks Postgres ("ORDER BY position must appear in
|
|
67
|
+
# the select list of a DISTINCT query").
|
|
68
|
+
def collection_distinct?
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Loads the parent category (runs before the base set_resource).
|
|
73
|
+
def set_parent
|
|
74
|
+
@category = Spree::Category.accessible_by(current_ability, :update).
|
|
75
|
+
for_store(current_store).
|
|
76
|
+
find_by_prefix_id!(params[:category_id])
|
|
77
|
+
authorize_parent!(@category)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def set_product
|
|
83
|
+
@product = scope.find_by_prefix_id!(params[:id])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def product_scope
|
|
87
|
+
current_store.products.accessible_by(current_ability, :show)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def classification_for(product)
|
|
91
|
+
Spree::Classification.find_by(taxon_id: @category.id, product_id: product.id)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -2,9 +2,46 @@ module Spree
|
|
|
2
2
|
module Api
|
|
3
3
|
module V3
|
|
4
4
|
module Admin
|
|
5
|
+
# Admin CRUD for categories — the hierarchical product classification
|
|
6
|
+
# half of today's dual-purpose Taxon. Rule-based / automatic taxons
|
|
7
|
+
# (the "collection" half moving to Spree::Collection in 6.0) are
|
|
8
|
+
# excluded from this API entirely (see +scope+), and none of the
|
|
9
|
+
# collection-bound attributes (automatic, sort_order, rules) are
|
|
10
|
+
# readable or writable here.
|
|
5
11
|
class CategoriesController < ResourceController
|
|
6
12
|
scoped_resource :categories
|
|
7
13
|
|
|
14
|
+
# PATCH /api/v3/admin/categories/:id/reposition
|
|
15
|
+
# Body: { new_parent_id: 'ctg_…' (omit for top level), new_position: 0 }
|
|
16
|
+
# Moves the category to a new parent (or the top level) and/or index.
|
|
17
|
+
def reposition
|
|
18
|
+
category = find_resource
|
|
19
|
+
authorize! :update, category
|
|
20
|
+
|
|
21
|
+
position = integer_param(:new_position)
|
|
22
|
+
return render_invalid_position if position.nil?
|
|
23
|
+
|
|
24
|
+
# Operate on a base Taxon instance (unscoped): the Category default
|
|
25
|
+
# i18n scope interferes with awesome_nested_set's lft/rgt reads, so
|
|
26
|
+
# the move silently no-ops on a scoped instance.
|
|
27
|
+
node = scope.find(category.id)
|
|
28
|
+
parent = reposition_parent
|
|
29
|
+
|
|
30
|
+
if parent
|
|
31
|
+
# move_to_child_with_index positions among the parent's children.
|
|
32
|
+
node.move_to_child_with_index(scope.find(parent.id), reposition_index(category, position, siblings_of(parent)))
|
|
33
|
+
else
|
|
34
|
+
move_to_root_at_index(node, reposition_index(category, position, siblings_of(nil)))
|
|
35
|
+
end
|
|
36
|
+
render json: serialize_resource(category.reload)
|
|
37
|
+
rescue CollectiveIdea::Acts::NestedSet::Move::ImpossibleMove => e
|
|
38
|
+
render_error(
|
|
39
|
+
code: ERROR_CODES[:validation_error],
|
|
40
|
+
message: e.message,
|
|
41
|
+
status: :unprocessable_content
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
8
45
|
protected
|
|
9
46
|
|
|
10
47
|
def model_class
|
|
@@ -15,8 +52,71 @@ module Spree
|
|
|
15
52
|
Spree.api.admin_category_serializer
|
|
16
53
|
end
|
|
17
54
|
|
|
18
|
-
|
|
19
|
-
|
|
55
|
+
# Category-half attributes only. Deliberately excludes the
|
|
56
|
+
# collection-bound fields (automatic, sort_order, rules_match_policy,
|
|
57
|
+
# taxon_rules) and taxonomy_id — those belong to Spree::Collection
|
|
58
|
+
# (6.0) and Taxonomy is being dropped. A prefixed parent_id resolves
|
|
59
|
+
# within scope (so a parent from another store or an automatic taxon
|
|
60
|
+
# can't be targeted).
|
|
61
|
+
def permitted_params
|
|
62
|
+
permitted = params.permit(
|
|
63
|
+
:name, :description, :permalink,
|
|
64
|
+
:meta_title, :meta_description, :meta_keywords,
|
|
65
|
+
:hide_from_nav, :image, :square_image,
|
|
66
|
+
# Inline custom field values keyed by definition id. The model
|
|
67
|
+
# setter (Spree::Metafields#custom_fields=) validates each entry
|
|
68
|
+
# against its definition. `value` is permitted as a scalar AND as
|
|
69
|
+
# `value: []` / `value: {}` so JSON metafields round-trip whether
|
|
70
|
+
# the parsed payload is an array or an object, while
|
|
71
|
+
# text/number/boolean ship scalars.
|
|
72
|
+
custom_fields: [:id, :custom_field_definition_id, :value, { value: [] }, { value: {} }]
|
|
73
|
+
)
|
|
74
|
+
permitted[:parent] = scope.find_by_prefix_id!(params[:parent_id]) if params[:parent_id].present?
|
|
75
|
+
permitted
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Target parent for a reposition — the requested category, or nil to
|
|
81
|
+
# promote to the top level.
|
|
82
|
+
# @return [Spree::Category, nil]
|
|
83
|
+
def reposition_parent
|
|
84
|
+
scope.find_by_prefix_id!(params[:new_parent_id]) if params[:new_parent_id].present?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The siblings among which the node is being placed, in tree order:
|
|
88
|
+
# the parent's children, or the store's root categories at the top
|
|
89
|
+
# level.
|
|
90
|
+
# @return [ActiveRecord::Relation]
|
|
91
|
+
def siblings_of(parent)
|
|
92
|
+
parent ? parent.children : scope.where(parent_id: nil).order(:lft)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Clamp the requested 0-based index into the valid range so an
|
|
96
|
+
# out-of-range index appends to the end instead of dereferencing a nil
|
|
97
|
+
# sibling. The category can occupy indices 0..N where N is the count of
|
|
98
|
+
# its *other* siblings; index N places it last.
|
|
99
|
+
# @return [Integer]
|
|
100
|
+
def reposition_index(category, position, siblings)
|
|
101
|
+
others = siblings.where.not(id: category.id).count
|
|
102
|
+
position.clamp(0, others)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Positions the node at +index+ among the store's root categories.
|
|
106
|
+
# awesome_nested_set's move_to_child_with_index(:root, ...) can't be
|
|
107
|
+
# used here: its +roots+ are global (every parentless taxon across all
|
|
108
|
+
# stores and taxonomies), whereas we order only this store's manual
|
|
109
|
+
# roots (the +scope+). So position relative to the target root sibling.
|
|
110
|
+
def move_to_root_at_index(node, index)
|
|
111
|
+
others = scope.where(parent_id: nil).where.not(id: node.id).order(:lft).to_a
|
|
112
|
+
target = others[index]
|
|
113
|
+
if target
|
|
114
|
+
node.move_to_left_of(target)
|
|
115
|
+
elsif others.any?
|
|
116
|
+
node.move_to_right_of(others.last)
|
|
117
|
+
else
|
|
118
|
+
node.move_to_root
|
|
119
|
+
end
|
|
20
120
|
end
|
|
21
121
|
end
|
|
22
122
|
end
|
|
@@ -74,12 +74,18 @@ module Spree
|
|
|
74
74
|
super.with_order_aggregates
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# `customer_groups` is preloaded because the serializer's always-on
|
|
78
|
+
# `customer_group_ids` attribute reads it for every row.
|
|
77
79
|
def collection_includes
|
|
78
|
-
[:rich_text_internal_note, taggings: :tag]
|
|
80
|
+
[:rich_text_internal_note, :customer_groups, taggings: :tag]
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
private
|
|
82
84
|
|
|
85
|
+
def scope_includes
|
|
86
|
+
[:customer_groups, :store_credits]
|
|
87
|
+
end
|
|
88
|
+
|
|
83
89
|
# Mirrors the products controller's resource-named key so SPA toasts
|
|
84
90
|
# can substitute `{customer_count}` instead of the generic
|
|
85
91
|
# `{record_count}` shipped by `Spree::Api::V3::BulkOperations`.
|
|
@@ -92,7 +98,7 @@ module Spree
|
|
|
92
98
|
:email, :first_name, :last_name, :phone,
|
|
93
99
|
:password, :password_confirmation, :selected_locale,
|
|
94
100
|
:avatar, :accepts_email_marketing, :internal_note,
|
|
95
|
-
metadata: {}, tags: []
|
|
101
|
+
metadata: {}, tags: [], customer_group_ids: []
|
|
96
102
|
)
|
|
97
103
|
end
|
|
98
104
|
|
|
@@ -87,6 +87,8 @@ module Spree
|
|
|
87
87
|
store: current_store,
|
|
88
88
|
user: try_spree_current_user
|
|
89
89
|
)
|
|
90
|
+
# The done email's download button target — allowed origins only.
|
|
91
|
+
attrs[:results_url] = validated_allowed_origin_url(attrs[:results_url])
|
|
90
92
|
klass.new(attrs)
|
|
91
93
|
end
|
|
92
94
|
|
|
@@ -96,7 +98,7 @@ module Spree
|
|
|
96
98
|
# is intentionally dropped — only CSV is supported and Rails' request
|
|
97
99
|
# format would otherwise overwrite the model's enum.
|
|
98
100
|
def permitted_params
|
|
99
|
-
attrs = params.permit(:type, :record_selection)
|
|
101
|
+
attrs = params.permit(:type, :record_selection, :results_url)
|
|
100
102
|
raw = params[:search_params]
|
|
101
103
|
attrs[:search_params] = raw.respond_to?(:to_unsafe_h) ? raw.to_unsafe_h : raw if raw.present?
|
|
102
104
|
attrs
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Row listing for one import — the failure report
|
|
6
|
+
# (`GET /admin/imports/:import_id/rows?q[status_eq]=failed`).
|
|
7
|
+
# Read-only; gated like its parent surface by the write scope of the
|
|
8
|
+
# imported resource, since rows expose the uploaded data.
|
|
9
|
+
class ImportRowsController < ResourceController
|
|
10
|
+
protected
|
|
11
|
+
|
|
12
|
+
def model_class
|
|
13
|
+
Spree::ImportRow
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def serializer_class
|
|
17
|
+
Spree.api.admin_import_row_serializer
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parent_association
|
|
21
|
+
:rows
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def set_parent
|
|
25
|
+
@parent = parent_import
|
|
26
|
+
authorize_parent!(@parent)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Resolved through the imports scope so store isolation matches the
|
|
30
|
+
# parent surface; memoized because the API-key scope gate also needs
|
|
31
|
+
# it (it runs before set_parent).
|
|
32
|
+
def parent_import
|
|
33
|
+
@parent_import ||= Spree::Import.for_store(current_store).
|
|
34
|
+
accessible_by(current_ability, :show).
|
|
35
|
+
find_by_prefix_id!(params[:import_id])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def action_kind
|
|
39
|
+
'write'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def scoped_resource_name
|
|
43
|
+
parent_import.class.required_scope || :all
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|