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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +3 -1
  3. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +1 -1
  4. data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +4 -2
  5. data/app/controllers/concerns/spree/api/v3/locale_and_currency.rb +4 -1
  6. data/app/controllers/concerns/spree/api/v3/storefront_gating.rb +65 -0
  7. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +1 -1
  8. data/app/controllers/spree/api/v3/admin/categories/products_controller.rb +98 -0
  9. data/app/controllers/spree/api/v3/admin/categories_controller.rb +102 -2
  10. data/app/controllers/spree/api/v3/admin/customers_controller.rb +8 -2
  11. data/app/controllers/spree/api/v3/admin/exports_controller.rb +3 -1
  12. data/app/controllers/spree/api/v3/admin/import_rows_controller.rb +49 -0
  13. data/app/controllers/spree/api/v3/admin/imports_controller.rb +274 -0
  14. data/app/controllers/spree/api/v3/admin/locales_controller.rb +29 -0
  15. data/app/controllers/spree/api/v3/admin/me_controller.rb +39 -9
  16. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +62 -2
  17. data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +105 -0
  18. data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +1 -8
  19. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +1 -1
  20. data/app/controllers/spree/api/v3/admin/products_controller.rb +8 -7
  21. data/app/controllers/spree/api/v3/admin/promotions_controller.rb +1 -1
  22. data/app/controllers/spree/api/v3/admin/resource_controller.rb +31 -0
  23. data/app/controllers/spree/api/v3/admin/store_controller.rb +3 -0
  24. data/app/controllers/spree/api/v3/admin/translatable_resources_controller.rb +38 -0
  25. data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +113 -0
  26. data/app/controllers/spree/api/v3/admin/translations_controller.rb +106 -0
  27. data/app/controllers/spree/api/v3/resource_controller.rb +0 -2
  28. data/app/controllers/spree/api/v3/store/auth_controller.rb +3 -1
  29. data/app/controllers/spree/api/v3/store/base_controller.rb +5 -0
  30. data/app/controllers/spree/api/v3/store/carts/fulfillments_controller.rb +1 -3
  31. data/app/controllers/spree/api/v3/store/carts_controller.rb +4 -0
  32. data/app/controllers/spree/api/v3/store/countries_controller.rb +1 -0
  33. data/app/controllers/spree/api/v3/store/currencies_controller.rb +1 -0
  34. data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +5 -1
  35. data/app/controllers/spree/api/v3/store/customers_controller.rb +3 -1
  36. data/app/controllers/spree/api/v3/store/digitals_controller.rb +1 -0
  37. data/app/controllers/spree/api/v3/store/locales_controller.rb +3 -8
  38. data/app/controllers/spree/api/v3/store/markets/countries_controller.rb +1 -0
  39. data/app/controllers/spree/api/v3/store/markets_controller.rb +1 -0
  40. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +1 -0
  41. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +5 -2
  42. data/app/controllers/spree/api/v3/store/products_controller.rb +1 -1
  43. data/app/controllers/spree/api/v3/store/resource_controller.rb +1 -0
  44. data/app/serializers/concerns/spree/api/v3/admin/translatable.rb +38 -0
  45. data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +7 -1
  46. data/app/serializers/spree/api/v3/admin/category_serializer.rb +5 -3
  47. data/app/serializers/spree/api/v3/admin/channel_serializer.rb +5 -1
  48. data/app/serializers/spree/api/v3/admin/customer_serializer.rb +7 -2
  49. data/app/serializers/spree/api/v3/admin/discount_serializer.rb +4 -0
  50. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +9 -0
  51. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +7 -0
  52. data/app/serializers/spree/api/v3/admin/import_mapping_serializer.rb +20 -0
  53. data/app/serializers/spree/api/v3/admin/import_row_serializer.rb +17 -0
  54. data/app/serializers/spree/api/v3/admin/import_serializer.rb +70 -0
  55. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +11 -0
  56. data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +2 -0
  57. data/app/serializers/spree/api/v3/admin/order_serializer.rb +15 -1
  58. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +4 -0
  59. data/app/serializers/spree/api/v3/admin/product_serializer.rb +1 -0
  60. data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +8 -0
  61. data/app/serializers/spree/api/v3/admin/resource_translations_serializer.rb +76 -0
  62. data/app/serializers/spree/api/v3/admin/setup_task_serializer.rb +18 -0
  63. data/app/serializers/spree/api/v3/admin/store_serializer.rb +27 -1
  64. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +8 -1
  65. data/app/serializers/spree/api/v3/base_serializer.rb +13 -0
  66. data/app/serializers/spree/api/v3/cart_serializer.rb +25 -22
  67. data/app/serializers/spree/api/v3/discount_serializer.rb +6 -2
  68. data/app/serializers/spree/api/v3/fulfillment_serializer.rb +16 -13
  69. data/app/serializers/spree/api/v3/gift_card_serializer.rb +15 -11
  70. data/app/serializers/spree/api/v3/import_row_serializer.rb +1 -1
  71. data/app/serializers/spree/api/v3/line_item_serializer.rb +35 -17
  72. data/app/serializers/spree/api/v3/locale_serializer.rb +6 -1
  73. data/app/serializers/spree/api/v3/order_serializer.rb +25 -21
  74. data/app/serializers/spree/api/v3/payment_serializer.rb +6 -2
  75. data/app/serializers/spree/api/v3/product_serializer.rb +9 -1
  76. data/app/serializers/spree/api/v3/variant_serializer.rb +12 -1
  77. data/app/services/spree/api/v3/filters_aggregator.rb +4 -4
  78. data/app/subscribers/spree/webhook_event_subscriber.rb +6 -0
  79. data/config/initializers/typelizer.rb +10 -0
  80. data/config/locales/en.yml +4 -0
  81. data/config/routes.rb +54 -4
  82. data/lib/spree/api/dependencies.rb +5 -0
  83. data/lib/spree/api/openapi/schema_helper.rb +29 -0
  84. metadata +21 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6fe1449422b9662417af9cc9e31ea0b9dc08624e7ad70b0a0e1570cf0f98698
4
- data.tar.gz: b859292883306b7128b3057e2df54a298469ea94cb246bb064e58b16e21d9f3c
3
+ metadata.gz: 89ddc07a0f4f9334f2ecb79bf2283986e8078e5566d4a7b1c672fadd2f04453d
4
+ data.tar.gz: 1db32c6bf66382dc2786d1898aaaf5e6c3e1a04b721404fad423c37cc3e0326b
5
5
  SHA512:
6
- metadata.gz: d4b81e56588d8ea4afa9756d6d3d128ff11222fd3fe34acf52ffc1715e83a87e58b4749f8b0dcfc7254fedad589133c15b5c828ae1e501b4f94df0bdc753afd3
7
- data.tar.gz: 4b28146ceb74aa7114c2ffea51f1fa4b107f5a65c48ed331c59354da6bd5bf99ed38242a298643832622a14e69958c8b4038af72c338430dc5c6ff72acc5b810
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(resource: current_store).joins(:role).
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?(resource: current_store)
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
- channel = channel_from_header
39
- Spree::Current.channel = channel if channel
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 locale.
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
@@ -60,7 +60,7 @@ module Spree
60
60
  end
61
61
 
62
62
  def collection_includes
63
- [{ role_users: :role }]
63
+ [{ role_users: :role, avatar_attachment: :blob }]
64
64
  end
65
65
 
66
66
  # Restrict to users with a role assignment on the current store.
@@ -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
- def scope
19
- super.where(taxonomy: current_store.taxonomies)
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