spree_core 5.0.4 → 5.1.0.beta2

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/assets/images/logo.png +0 -0
  3. data/app/finders/spree/countries/find.rb +0 -8
  4. data/app/finders/spree/products/find.rb +28 -1
  5. data/app/finders/spree/taxons/find.rb +1 -1
  6. data/app/helpers/spree/base_helper.rb +4 -16
  7. data/app/helpers/spree/integrations_helper.rb +15 -0
  8. data/app/helpers/spree/mail_helper.rb +4 -3
  9. data/app/helpers/spree/products_helper.rb +1 -4
  10. data/app/javascript/spree/core/controllers/disable_submit_button_controller.js +19 -0
  11. data/app/mailers/spree/invitation_mailer.rb +24 -0
  12. data/app/models/concerns/spree/integrations_concern.rb +11 -0
  13. data/app/models/concerns/spree/product_scopes.rb +6 -6
  14. data/app/models/concerns/spree/translatable_resource.rb +4 -0
  15. data/app/models/concerns/spree/translatable_resource_scopes.rb +17 -3
  16. data/app/models/concerns/spree/unique_name.rb +2 -0
  17. data/app/models/concerns/spree/user_management.rb +33 -0
  18. data/app/models/concerns/spree/user_methods.rb +19 -0
  19. data/app/models/concerns/spree/user_roles.rb +43 -17
  20. data/app/models/spree/ability.rb +8 -6
  21. data/app/models/spree/asset.rb +1 -6
  22. data/app/models/spree/base.rb +1 -0
  23. data/app/models/spree/base_analytics_event_handler.rb +7 -2
  24. data/app/models/spree/classification.rb +13 -0
  25. data/app/models/spree/custom_domain.rb +2 -1
  26. data/app/models/spree/export.rb +1 -1
  27. data/app/models/spree/integration.rb +63 -0
  28. data/app/models/spree/invitation.rb +153 -0
  29. data/app/models/spree/invitations/store.rb +6 -0
  30. data/app/models/spree/order.rb +16 -1
  31. data/app/models/spree/order_merger.rb +7 -5
  32. data/app/models/spree/product_property.rb +1 -14
  33. data/app/models/spree/property.rb +3 -1
  34. data/app/models/spree/reports/sales_total.rb +5 -1
  35. data/app/models/spree/role.rb +16 -0
  36. data/app/models/spree/role_user.rb +32 -1
  37. data/app/models/spree/shipment_handler.rb +1 -0
  38. data/app/models/spree/shipping_method.rb +1 -1
  39. data/app/models/spree/store.rb +9 -4
  40. data/app/models/spree/store_credit_category.rb +4 -0
  41. data/app/models/spree/taxon.rb +4 -3
  42. data/app/services/spree/country_to_timezone.rb +273 -0
  43. data/app/services/spree/seeds/admin_user.rb +4 -2
  44. data/app/services/spree/seeds/all.rb +3 -1
  45. data/app/services/spree/seeds/digital_delivery.rb +20 -0
  46. data/app/services/spree/seeds/returns_environment.rb +27 -0
  47. data/app/services/spree/seeds/tax_categories.rb +12 -0
  48. data/app/services/spree/stores/settings_defaults_by_country.rb +38 -0
  49. data/app/services/spree/tags/bulk_add.rb +13 -7
  50. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +12 -0
  51. data/app/views/spree/invitation_mailer/invitation_email.html.erb +21 -0
  52. data/config/locales/en.yml +43 -9
  53. data/db/migrate/20250407085228_create_spree_integrations.rb +12 -0
  54. data/db/migrate/20250410061306_create_spree_invitations.rb +20 -0
  55. data/db/migrate/20250418174652_add_resource_to_spree_role_users.rb +8 -0
  56. data/db/migrate/20250508060800_add_selected_locale_to_spree_admin_users.rb +8 -0
  57. data/db/migrate/20250509143831_add_session_id_to_spree_assets.rb +5 -0
  58. data/lib/generators/spree/authentication/devise/devise_generator.rb +5 -2
  59. data/lib/generators/spree/authentication/devise/templates/authentication_helpers.rb.tt +3 -3
  60. data/lib/generators/spree/dummy/dummy_generator.rb +1 -1
  61. data/lib/generators/spree/dummy/templates/app/assets/config/manifest.js +0 -0
  62. data/lib/generators/spree/dummy/templates/rails/application.rb +1 -2
  63. data/lib/generators/spree/install/install_generator.rb +5 -0
  64. data/lib/spree/core/controller_helpers/auth.rb +15 -14
  65. data/lib/spree/core/controller_helpers/common.rb +0 -71
  66. data/lib/spree/core/controller_helpers/currency.rb +11 -0
  67. data/lib/spree/core/controller_helpers/strong_parameters.rb +3 -2
  68. data/lib/spree/core/engine.rb +14 -3
  69. data/lib/spree/core/preferences/preferable.rb +10 -0
  70. data/lib/spree/core/preferences/preferable_class_methods.rb +52 -0
  71. data/lib/spree/core/version.rb +1 -1
  72. data/lib/spree/core.rb +1 -0
  73. data/lib/spree/permitted_attributes.rb +121 -13
  74. data/lib/spree/testing_support/controller_requests.rb +0 -92
  75. data/lib/spree/testing_support/factories/integration_factory.rb +7 -0
  76. data/lib/spree/testing_support/factories/invitation_factory.rb +6 -0
  77. data/lib/spree/testing_support/factories/page_block_factory.rb +1 -0
  78. data/lib/spree/testing_support/factories/promotion_action_factory.rb +4 -0
  79. data/lib/spree/testing_support/factories/user_factory.rb +14 -1
  80. data/lib/spree/translation_migrations.rb +27 -15
  81. data/lib/tasks/core.rake +8 -0
  82. metadata +43 -7
  83. data/app/validators/db_maximum_length_validator.rb +0 -16
  84. data/lib/generators/spree/dummy/templates/rails/script/rails +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8dad7a44435e17f7a0b717356dfe3f8f70cac0e9a18f38f505ded42f3908faf5
4
- data.tar.gz: 109859dd8b1790ab9c0a22c1457e03e81708d79007664f0c8afae841573dd43e
3
+ metadata.gz: f8150046e1776256aacec11512b848d9cbd5074d17b7aacafd7b2736cfd934c3
4
+ data.tar.gz: 8a04db5d73b2abf21a587ea66313d4fb7ff7120ed46ef9d22f38ddf1b3c5f295
5
5
  SHA512:
6
- metadata.gz: f09637caac881e203c1ff4f1104c2035c89df6cf671d619b7b9e4786a82a4d65332e6537fa4932545e1782203c82f1b41bb6e4c416674df57de95ccbb014410f
7
- data.tar.gz: 2e499043d9f6393d208ce6cdb823059e734748e79924612d408e6de48d5e36c89cb1799405dd62da06e53aa6b8df3693033a267948fb3e6a4fc2077d4efbce2d
6
+ metadata.gz: 8024d8aa8b617d7be12713acf1787a7c4569dc708d74cbf34850ec6bc8fcea5630c15fdfe6efa4796c82825fc9724d637b219da887f888f909dd29d22582178f
7
+ data.tar.gz: 60db3009f9a9a30bfca2b8c6895b7777a35eaac8819ca06a112e2315e234137ee87e5fea3796335e53e8a8e5c562f22183d7d8c6c3c3b1f8e9466d76ba3bbde5
Binary file
@@ -7,14 +7,6 @@ module Spree
7
7
  @shippable = String(params[:filter][:shippable]) unless params[:filter].nil?
8
8
  end
9
9
 
10
- def call
11
- Spree::Deprecation.warn(<<-DEPRECATION, caller)
12
- Spree::Countries::Find.new.call is deprecated and will be removed in Spree 5.0.
13
- Please use Spree::Countries::Find.new.execute instead
14
- DEPRECATION
15
- execute
16
- end
17
-
18
10
  def execute
19
11
  countries = by_shippability(scope)
20
12
 
@@ -11,6 +11,7 @@ module Spree
11
11
  @currency = params.dig(:filter, :currency) || params[:currency] || Spree::Store.default.default_currency
12
12
  @taxons = taxon_ids(params.dig(:filter, :taxons))
13
13
  @concat_taxons = taxon_ids(params.dig(:filter, :concat_taxons))
14
+ @taxonomies = params.dig(:filter, :taxonomy_ids).to_h
14
15
  @name = params.dig(:filter, :name)
15
16
  @slug = params.dig(:filter, :slug)
16
17
  @options = params.dig(:filter, :options).try(:to_unsafe_hash)
@@ -52,6 +53,7 @@ module Spree
52
53
  products = show_only_backorderable(products)
53
54
  products = show_only_purchasable(products)
54
55
  products = show_only_out_of_stock(products)
56
+ products = by_taxonomies(products)
55
57
  products = ordered(products)
56
58
  products = by_vendor_ids(products)
57
59
 
@@ -62,7 +64,7 @@ module Spree
62
64
 
63
65
  attr_reader :ids, :skus, :price, :currency, :taxons, :concat_taxons, :name, :options, :option_value_ids, :scope,
64
66
  :sort_by, :deleted, :discontinued, :properties, :store, :in_stock, :backorderable, :purchasable, :tags,
65
- :query, :vendor_ids, :out_of_stock, :slug
67
+ :query, :vendor_ids, :out_of_stock, :slug, :taxonomies
66
68
 
67
69
  def query?
68
70
  query.present?
@@ -169,6 +171,21 @@ module Spree
169
171
  products.where(id: product_ids)
170
172
  end
171
173
 
174
+ def by_taxonomies(products)
175
+ return products if taxonomies.none?
176
+
177
+ taxon_groups = taxonomies.values.map { |taxonomy| taxon_ids(taxonomy[:taxon_ids].join(',')) }.compact_blank
178
+
179
+ return products if taxon_groups.empty?
180
+
181
+ taxonomies_products = products.joins(:classifications).where(Classification.table_name => { taxon_id: taxon_groups.flatten.uniq })
182
+
183
+ # No need to filter if there is only one taxonomy
184
+ return taxonomies_products if taxonomies.size == 1
185
+
186
+ products.where(id: products_matching_all_taxonomies_ids(taxonomies_products.ids, taxon_groups))
187
+ end
188
+
172
189
  def by_name(products)
173
190
  return products unless name?
174
191
 
@@ -336,6 +353,16 @@ module Spree
336
353
  def order_by_best_selling(scope)
337
354
  scope.by_best_selling(:desc)
338
355
  end
356
+
357
+ def products_matching_all_taxonomies_ids(products_ids, taxon_groups)
358
+ classifications = Spree::Classification.grouped_taxon_ids_for_products(products_ids, taxon_groups.flatten)
359
+ classifications_hash = classifications.to_h.transform_values { |taxon_ids| taxon_ids.split(',') }
360
+
361
+ # Find products ids that match all taxonomies to tighten filter results
362
+ classifications_hash.filter_map do |product_id, product_taxon_ids|
363
+ product_id if taxon_groups.all? { |group| group.intersect?(product_taxon_ids) }
364
+ end
365
+ end
339
366
  end
340
367
  end
341
368
  end
@@ -68,7 +68,7 @@ module Spree
68
68
  if Spree.use_translations?
69
69
  taxons.joins(:parent).
70
70
  join_translation_table(Taxon, 'parents_spree_taxons').
71
- where(["#{Taxon.translation_table_alias}.permalink = ?", parent_permalink])
71
+ where(Taxon.translation_table_alias => { permalink: parent_permalink })
72
72
  else
73
73
  taxons.joins(:parent).where(parent: { permalink: parent_permalink })
74
74
  end
@@ -136,10 +136,7 @@ module Spree
136
136
  def pretty_time(time)
137
137
  return '' if time.blank?
138
138
 
139
- Spree::Deprecation.warn(<<-DEPRECATION, caller)
140
- `BaseHelper#pretty_time` is deprecated and will be removed in Spree 6.0.
141
- Please add `local_time` gem to your Gemfile and use `local_time(time)` instead
142
- DEPRECATION
139
+ Spree::Deprecation.warn('BaseHelper#pretty_time is deprecated and will be removed in Spree 6.0. Please add `local_time` gem to your Gemfile and use `local_time(time)` instead')
143
140
 
144
141
  [I18n.l(time.to_date, format: :long), time.strftime('%l:%M %p %Z')].join(' ')
145
142
  end
@@ -147,10 +144,7 @@ module Spree
147
144
  def pretty_date(date)
148
145
  return '' if date.blank?
149
146
 
150
- Spree::Deprecation.warn(<<-DEPRECATION, caller)
151
- `BaseHelper#pretty_date` is deprecated and will be removed in Spree 6.0.
152
- Please add `local_time` gem to your Gemfile and use `local_date(date)` instead
153
- DEPRECATION
147
+ Spree::Deprecation.warn('BaseHelper#pretty_date is deprecated and will be removed in Spree 6.0. Please add `local_time` gem to your Gemfile and use `local_date(date)` instead')
154
148
 
155
149
  [I18n.l(date.to_date, format: :long)].join(' ')
156
150
  end
@@ -219,19 +213,13 @@ module Spree
219
213
  # we should always try to render image of the default variant
220
214
  # same as it's done on PDP
221
215
  def default_image_for_product(product)
222
- Spree::Deprecation.warn(<<-DEPRECATION, caller)
223
- `BaseHelper#default_image_for_product` is deprecated and will be removed in Spree 6.0.
224
- Please use `product.default_image` instead
225
- DEPRECATION
216
+ Spree::Deprecation.warn('BaseHelper#default_image_for_product is deprecated and will be removed in Spree 6.0. Please use product.default_image instead')
226
217
 
227
218
  product.default_image
228
219
  end
229
220
 
230
221
  def default_image_for_product_or_variant(product_or_variant)
231
- Spree::Deprecation.warn(<<-DEPRECATION, caller)
232
- `BaseHelper#default_image_for_product_or_variant` is deprecated and will be removed in Spree 6.0.
233
- Please use `product_or_variant.default_image` instead
234
- DEPRECATION
222
+ Spree::Deprecation.warn('BaseHelper#default_image_for_product_or_variant is deprecated and will be removed in Spree 6.0. Please use product_or_variant.default_image instead')
235
223
 
236
224
  product_or_variant.default_image
237
225
  end
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module IntegrationsHelper
3
+ def store_integrations
4
+ @store_integrations ||= current_store.integrations.active.to_a
5
+ end
6
+
7
+ def store_integration(name)
8
+ store_integrations.find { |integration| integration.type.to_s.demodulize.underscore == name }
9
+ end
10
+
11
+ def grouped_available_store_integrations
12
+ Rails.application.config.spree.integrations.group_by(&:integration_group).sort_by { |group, _| group }
13
+ end
14
+ end
15
+ end
@@ -1,10 +1,11 @@
1
1
  module Spree
2
2
  module MailHelper
3
- include BaseHelper
3
+ include Spree::BaseHelper
4
+ include Spree::ImagesHelper
4
5
 
5
6
  def variant_image_url(variant)
6
- image = default_image_for_product_or_variant(variant)
7
- image ? main_app.cdn_image_url(image.url(:small)) : image_url('noimage/small.png')
7
+ image = variant.default_image
8
+ image.present? && image.attached? ? spree_image_url(image, width: 100, height: 100) : image_url('noimage/small.png')
8
9
  end
9
10
 
10
11
  def name_for(order)
@@ -121,10 +121,7 @@ module Spree
121
121
  end
122
122
 
123
123
  def related_products
124
- Spree::Deprecation.warn(<<-DEPRECATION, caller)
125
- ProductsHelper#related_products is deprecated and will be removed in Spree 5.0.
126
- Please use ProductsHelper#relations from now on.
127
- DEPRECATION
124
+ Spree::Deprecation.warn('ProductsHelper#related_products is deprecated and will be removed in Spree 6.0. Please use ProductsHelper#relations from now on.')
128
125
 
129
126
  return [] unless @product.respond_to?(:has_related_products?)
130
127
 
@@ -0,0 +1,19 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["button"]
5
+
6
+ connect() {
7
+ this.element.addEventListener('submit', this.toggleDisabledButton)
8
+ }
9
+
10
+ disconnect() {
11
+ this.element.removeEventListener('submit', this.toggleDisabledButton)
12
+ }
13
+
14
+ toggleDisabledButton = () => {
15
+ if (this.hasButtonTarget) {
16
+ this.buttonTarget.disabled = !this.buttonTarget.disabled
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,24 @@
1
+ module Spree
2
+ class InvitationMailer < BaseMailer
3
+ # invitation email, sending email to the invited to let them know they have been invited to join a store/account/vendor
4
+ def invitation_email(invitation)
5
+ @invitation = invitation
6
+ mail(to: invitation.email,
7
+ from: from_address,
8
+ reply_to: reply_to_address,
9
+ subject: Spree.t('invitation_mailer.invitation_email.subject',
10
+ resource_name: invitation.resource&.name))
11
+ end
12
+
13
+ # sending email to the inviter to let them know the invitee has accepted the invitation
14
+ def invitation_accepted(invitation)
15
+ @invitation = invitation
16
+ mail(to: invitation.inviter.email,
17
+ from: from_address,
18
+ reply_to: reply_to_address,
19
+ subject: Spree.t('invitation_mailer.invitation_accepted.subject',
20
+ invitee_name: invitation.invitee&.name,
21
+ resource_name: invitation.resource&.name))
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ module Spree
2
+ module IntegrationsConcern
3
+ def store_integrations
4
+ @store_integrations ||= Spree::Store.current.integrations.active.to_a
5
+ end
6
+
7
+ def store_integration(name)
8
+ store_integrations.find { |integration| integration.type.to_s.demodulize.underscore == name }
9
+ end
10
+ end
11
+ end
@@ -49,11 +49,11 @@ module Spree
49
49
  add_simple_scopes simple_scopes
50
50
 
51
51
  add_search_scope :ascend_by_master_price do
52
- order("#{price_table_name}.amount ASC")
52
+ order(price_table_name => { amount: :asc })
53
53
  end
54
54
 
55
55
  add_search_scope :descend_by_master_price do
56
- order("#{price_table_name}.amount DESC")
56
+ order(price_table_name => { amount: :desc })
57
57
  end
58
58
 
59
59
  add_search_scope :price_between do |low, high|
@@ -61,11 +61,11 @@ module Spree
61
61
  end
62
62
 
63
63
  add_search_scope :master_price_lte do |price|
64
- where("#{price_table_name}.amount <= ?", price)
64
+ where(Price.table_name => { amount: ..price })
65
65
  end
66
66
 
67
67
  add_search_scope :master_price_gte do |price|
68
- where("#{price_table_name}.amount >= ?", price)
68
+ where(Price.table_name => { amount: price.. })
69
69
  end
70
70
 
71
71
  add_search_scope :in_stock do
@@ -137,11 +137,11 @@ module Spree
137
137
  joins(:properties).
138
138
  join_translation_table(Property).
139
139
  join_translation_table(ProductProperty).
140
- where("#{ProductProperty.translation_table_alias}.value = ?", value).
140
+ where(ProductProperty.translation_table_alias => { value: value }).
141
141
  where(property_conditions(property))
142
142
  else
143
143
  joins(:properties).
144
- where("#{ProductProperty.table_name}.value = ?", value).
144
+ where(ProductProperty.table_name => { value: value }).
145
145
  where(property_conditions(property))
146
146
  end
147
147
  end
@@ -20,6 +20,10 @@ module Spree
20
20
  def translation_table_alias
21
21
  "#{self::Translation.table_name}_#{Mobility.normalize_locale(Mobility.locale)}"
22
22
  end
23
+
24
+ def arel_table_alias
25
+ Arel::Table.new(translation_table_alias)
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -15,9 +15,23 @@ module Spree
15
15
  end
16
16
  translatable_class_foreign_key = "#{translatable_class.table_name.singularize}_id"
17
17
 
18
- joins("LEFT OUTER JOIN #{translatable_class::Translation.table_name} #{translatable_class.translation_table_alias}
19
- ON #{translatable_class.translation_table_alias}.#{translatable_class_foreign_key} = #{join_on_table_name}.id
20
- AND #{translatable_class.translation_table_alias}.locale = '#{Mobility.locale}'")
18
+ joins(
19
+ Arel::Nodes::OuterJoin.new(
20
+ Arel::Table.new(translatable_class::Translation.table_name).alias(translatable_class.translation_table_alias),
21
+ Arel::Nodes::On.new(
22
+ Arel::Nodes::And.new([
23
+ Arel::Nodes::Equality.new(
24
+ Arel::Table.new(translatable_class.translation_table_alias)[translatable_class_foreign_key],
25
+ Arel::Table.new(join_on_table_name)[:id]
26
+ ),
27
+ Arel::Nodes::Equality.new(
28
+ Arel::Table.new(translatable_class.translation_table_alias)[:locale],
29
+ Arel::Nodes::Quoted.new(Mobility.locale.to_s)
30
+ )
31
+ ])
32
+ )
33
+ )
34
+ )
21
35
  end
22
36
  end
23
37
  end
@@ -3,6 +3,8 @@ module Spree
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
+ auto_strip_attributes :name
7
+
6
8
  validates :name, presence: true,
7
9
  uniqueness: { case_sensitive: false, allow_blank: true, scope: spree_base_uniqueness_scope }
8
10
  end
@@ -0,0 +1,33 @@
1
+ module Spree
2
+ module UserManagement
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :role_users, class_name: 'Spree::RoleUser', as: :resource, dependent: :destroy
7
+ has_many :users, through: :role_users, source: :user, source_type: Spree.admin_user_class.to_s
8
+ has_many :invitations, class_name: 'Spree::Invitation', as: :resource, dependent: :destroy
9
+ end
10
+
11
+ # Adds a user to the resource with the default user role
12
+ # If no role is provided, the default user role will be used
13
+ # If a role is provided, it will be used instead of the default user role
14
+ # @param user [Spree.admin_user_class] The user to add to the resource
15
+ # @param role [Spree::Role] The role to add the user to
16
+ def add_user(user, role = nil)
17
+ role = role || default_user_role
18
+ role_users.find_or_create_by!(user: user, role: role)
19
+ end
20
+
21
+ # Revokes a user's access to the resource
22
+ # @param user [Spree.admin_user_class] The user to remove from the resource
23
+ # @return [void]
24
+ def remove_user(user)
25
+ role_users.where(user: user).destroy_all
26
+ end
27
+
28
+ # this can be overridden in the base model to use a different user role, eg. 'vendor'
29
+ def default_user_role
30
+ Spree::Role.default_admin_role
31
+ end
32
+ end
33
+ end
@@ -84,6 +84,11 @@ module Spree
84
84
  end
85
85
  end
86
86
 
87
+ # Returns the last incomplete spree order for the current store
88
+ # @param [Spree::Store] store
89
+ # @param [Hash] options
90
+ # @option options [Array<Symbol>] :includes
91
+ # @return [Spree::Order]
87
92
  def last_incomplete_spree_order(store, options = {})
88
93
  orders.where(store: store).incomplete.not_canceled.
89
94
  includes(options[:includes]).
@@ -91,12 +96,19 @@ module Spree
91
96
  first
92
97
  end
93
98
 
99
+ # Returns the total available store credit for the current store per currency
100
+ # @param [Spree::Store] store
101
+ # @param [String] currency
102
+ # @return [Float]
94
103
  def total_available_store_credit(currency = nil, store = nil)
95
104
  store ||= Store.default
96
105
  currency ||= store.default_currency
97
106
  store_credits.for_store(store).where(currency: currency).reload.to_a.sum(&:amount_remaining)
98
107
  end
99
108
 
109
+ # Returns the available store credits for the current store per currency
110
+ # @param [Spree::Store] store
111
+ # @return [Array<Spree::Money>]
100
112
  def available_store_credits(store)
101
113
  store ||= Store.default
102
114
 
@@ -105,12 +117,18 @@ module Spree
105
117
  end
106
118
  end
107
119
 
120
+ # Returns the default wishlist for the current store
121
+ # if no default wishlist exists, it creates one
122
+ # @param [Spree::Store] current_store
123
+ # @return [Spree::Wishlist]
108
124
  def default_wishlist_for_store(current_store)
109
125
  wishlists.find_by(is_default: true, store_id: current_store.id) || ActiveRecord::Base.connected_to(role: :writing) do
110
126
  wishlists.create!(store: current_store, is_default: true, name: Spree.t(:default_wishlist_name))
111
127
  end
112
128
  end
113
129
 
130
+ # Returns true if the user can be deleted
131
+ # @return [Boolean]
114
132
  def can_be_deleted?
115
133
  orders.complete.none?
116
134
  end
@@ -140,6 +158,7 @@ module Spree
140
158
  use_billing.in?([true, 'true', '1'])
141
159
  end
142
160
 
161
+ # Scrambles the email and names of the user
143
162
  def scramble_email_and_names
144
163
  self.email = "#{SecureRandom.uuid}@example.net"
145
164
  self.first_name = 'Deleted'
@@ -5,38 +5,64 @@ module Spree
5
5
  included do
6
6
  has_many :role_users, class_name: 'Spree::RoleUser', foreign_key: :user_id, as: :user, dependent: :destroy
7
7
  has_many :spree_roles, through: :role_users, class_name: 'Spree::Role', source: :role
8
+ has_many :stores, through: :role_users, source: :resource, source_type: 'Spree::Store'
9
+ has_many :invitations, class_name: 'Spree::Invitation', foreign_key: :invitee_id, dependent: :destroy
8
10
 
9
- scope :spree_admin, -> { joins(:spree_roles).where(Spree::Role.table_name => { name: 'admin' }) }
11
+ scope :spree_admin, -> { joins(:spree_roles).where(Spree::Role.table_name => { name: Spree::Role::ADMIN_ROLE }) }
10
12
 
11
- # has_spree_role? simply needs to return true or false whether a user has a role or not.
12
- def has_spree_role?(role_name)
13
- spree_roles.exists?(name: role_name)
14
- end
15
-
16
- def self.admin
17
- Spree::Deprecation.warn('`User#admin` is deprecated and will be removed in Spree 5. Please use `User#spree_admin`')
13
+ # Adds a role to a resource
14
+ #
15
+ # @param role_name [String] The name of the role to add, eg. 'admin'
16
+ # @param resource [Spree::Base] The resource to add the role to
17
+ # @return [Spree::RoleUser] The role user created
18
+ def add_role(role_name, resource = nil)
19
+ resource ||= Spree::Store.current
20
+ role = Spree::Role.find_by(name: role_name)
21
+ return if role.nil?
18
22
 
19
- spree_admin
23
+ role_users.find_or_create_by!(role: role, resource: resource)
20
24
  end
21
25
 
22
- def self.admin_created?
23
- Spree::Deprecation.warn('`User#admin_created?` is deprecated and will be removed in Spree 5. Please use `User#spree_admin_created?`')
26
+ # Removes a role from a resource
27
+ #
28
+ # @param role_name [String] The name of the role to remove, eg. 'admin'
29
+ # @param resource [Spree::Base] The resource to remove the role from
30
+ def remove_role(role_name, resource = nil)
31
+ resource ||= Spree::Store.current
32
+ role = Spree::Role.find_by(name: role_name)
33
+ return if role.nil?
24
34
 
25
- spree_admin_created?
35
+ role_users.where(role: role, resource: resource).destroy_all
26
36
  end
27
37
 
28
- def admin?
29
- Spree::Deprecation.warn('`User#admin?` is deprecated and will be removed in Spree 5. Please use `User#spree_admin?`')
38
+ # has_spree_role? simply needs to return true or false whether a user has a role or not.
39
+ #
40
+ # @param role_name [String] The name of the role to check for
41
+ # @param resource [Spree::Base] The resource to get roles for
42
+ # @return [Boolean] Whether the user has the role for the resource
43
+ def has_spree_role?(role_name, resource = nil)
44
+ resource ||= Spree::Store.current
30
45
 
31
- spree_admin?
46
+ role_users.where(resource: resource).joins(:role).where(Spree::Role.table_name => { name: role_name }).exists?
32
47
  end
33
48
 
34
49
  def self.spree_admin_created?
35
50
  spree_admin.exists?
36
51
  end
37
52
 
38
- def spree_admin?
39
- has_spree_role?('admin')
53
+ # Returns true if the user has the admin role for a given resource
54
+ #
55
+ # @param resource [Spree::Base] The resource to check the admin role for
56
+ # @return [Boolean] Whether the user has the admin role for the resource
57
+ def spree_admin?(resource = nil)
58
+ resource ||= Spree::Store.current
59
+ has_spree_role?(Spree::Role::ADMIN_ROLE, resource)
60
+ end
61
+
62
+ # Returns the user who invited the current user
63
+ # @return [Spree.admin_user_class]
64
+ def invited_by
65
+ invitations.first&.inviter
40
66
  end
41
67
  end
42
68
  end
@@ -23,15 +23,16 @@ module Spree
23
23
  abilities.delete(ability)
24
24
  end
25
25
 
26
- def initialize(user)
26
+ def initialize(user, options = {})
27
27
  alias_cancan_delete_action
28
28
 
29
29
  user ||= Spree.user_class.new
30
+ store ||= options[:store] || Spree::Current.store
30
31
 
31
- if user.persisted? && user.try(:spree_admin?)
32
- apply_admin_permissions(user)
32
+ if user.persisted? && user.is_a?(Spree.admin_user_class) && user.try(:spree_admin?, store)
33
+ apply_admin_permissions(user, options)
33
34
  else
34
- apply_user_permissions(user)
35
+ apply_user_permissions(user, options)
35
36
  end
36
37
 
37
38
  # Include any abilities registered by extensions, etc.
@@ -56,7 +57,7 @@ module Spree
56
57
  alias_action :create, :update, :destroy, to: :modify
57
58
  end
58
59
 
59
- def apply_admin_permissions(user)
60
+ def apply_admin_permissions(_user, _options)
60
61
  can :manage, :all
61
62
  cannot :cancel, Spree::Order
62
63
  can :cancel, Spree::Order, &:allow_cancel?
@@ -64,7 +65,7 @@ module Spree
64
65
  cannot [:edit, :update], Spree::ReimbursementType, mutable: false
65
66
  end
66
67
 
67
- def apply_user_permissions(user)
68
+ def apply_user_permissions(user, _options)
68
69
  can :read, ::Spree::Country
69
70
  can :read, ::Spree::OptionType
70
71
  can :read, ::Spree::OptionValue
@@ -95,6 +96,7 @@ module Spree
95
96
  can [:create, :update, :destroy], ::Spree::WishedItem do |wished_item|
96
97
  wished_item.wishlist.user == user
97
98
  end
99
+ can :accept, Spree::Invitation, invitee_id: [user.id, nil], invitee_type: user.class.name, status: 'pending'
98
100
  end
99
101
 
100
102
  def protect_admin_role
@@ -17,12 +17,7 @@ module Spree
17
17
 
18
18
  store_accessor :private_metadata, :session_uploaded_assets_uuid
19
19
  scope :with_session_uploaded_assets_uuid, lambda { |uuid|
20
- case ActiveRecord::Base.connection.adapter_name
21
- when 'PostgreSQL'
22
- where("#{table_name}.private_metadata @> ?", { session_uploaded_assets_uuid: uuid }.to_json)
23
- when 'Mysql2', 'SQLite'
24
- where("JSON_EXTRACT(private_metadata, '$.session_uploaded_assets_uuid') = '#{uuid}'")
25
- end
20
+ where(session_id: uuid)
26
21
  }
27
22
 
28
23
  def product
@@ -2,6 +2,7 @@ class Spree::Base < ApplicationRecord
2
2
  include Spree::Preferences::Preferable
3
3
  include Spree::RansackableAttributes
4
4
  include Spree::TranslatableResourceScopes
5
+ include Spree::IntegrationsConcern
5
6
 
6
7
  after_initialize do
7
8
  if has_attribute?(:preferences) && !preferences.nil?
@@ -5,13 +5,16 @@ module Spree
5
5
  # @param opts [Hash] The options
6
6
  # @option opts [User] :user
7
7
  # @option opts [String] :session_id
8
+ # @option opts [Spree::Store] :store
8
9
  def initialize(opts = {})
9
10
  @user = opts[:user]
10
11
  @session = opts[:session]
11
12
  @request = opts[:request]
13
+ @store = opts[:store]
14
+ @visitor_id = opts[:visitor_id]
12
15
  end
13
16
 
14
- attr_reader :user, :session, :request
17
+ attr_reader :user, :session, :request, :store, :visitor_id
15
18
 
16
19
  # Returns the client
17
20
  # @return [Object] The client object
@@ -42,7 +45,9 @@ module Spree
42
45
  def identity_hash
43
46
  {
44
47
  user_id: user&.id,
45
- session_id: session&.id
48
+ # session.id is a custom class (not a string), which has overridden the `to_json` method, we have to convert it to a string first so it does not send garbage to the analytics service
49
+ session_id: session&.id&.to_s,
50
+ visitor_id: visitor_id
46
51
  }
47
52
  end
48
53
  end
@@ -23,5 +23,18 @@ module Spree
23
23
  group("#{Spree::Classification.table_name}.id").
24
24
  reorder(completed_orders_count: order_direction, completed_orders_total: order_direction)
25
25
  }
26
+
27
+ scope :grouped_taxon_ids_for_products, lambda { |product_ids, taxon_groups|
28
+ where(product_id: product_ids, taxon_id: taxon_groups).
29
+ group(:product_id).
30
+ then do |query|
31
+ case ActiveRecord::Base.connection.adapter_name
32
+ when 'PostgreSQL'
33
+ query.pluck(:product_id, Arel.sql("STRING_AGG(taxon_id::text, ',')"))
34
+ when 'Mysql2', 'SQLite'
35
+ query.pluck(:product_id, Arel.sql('GROUP_CONCAT(taxon_id)'))
36
+ end
37
+ end
38
+ }
26
39
  end
27
40
  end
@@ -14,7 +14,7 @@ module Spree
14
14
  # Validations
15
15
  #
16
16
  validates :url, presence: true, uniqueness: true, format: {
17
- with: %r{[a-z][a-z0-9-]*[a-z0-9]}i
17
+ with: %r{\A(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z}i
18
18
  }, length: { in: 1..63 }
19
19
  validate :url_is_valid
20
20
 
@@ -26,6 +26,7 @@ module Spree
26
26
  after_validation :ensure_default, on: :create
27
27
 
28
28
  def url_is_valid
29
+ return if url.blank?
29
30
  parts = url.split('.')
30
31
 
31
32
  errors.add(:url, 'use domain or subdomain') if (parts[0] != 'www' && parts.size > 3) || (parts[0] == 'www' && parts.size > 4) || parts.size < 2