spree_core 5.0.6 → 5.1.0.beta
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/assets/images/logo.png +0 -0
- data/app/finders/spree/products/find.rb +28 -1
- data/app/finders/spree/taxons/find.rb +1 -1
- data/app/helpers/spree/base_helper.rb +1 -6
- data/app/helpers/spree/images_helper.rb +12 -13
- data/app/helpers/spree/integrations_helper.rb +15 -0
- data/app/helpers/spree/mail_helper.rb +3 -4
- data/app/javascript/spree/core/controllers/disable_submit_button_controller.js +19 -0
- data/app/mailers/spree/invitation_mailer.rb +24 -0
- data/app/models/concerns/spree/integrations_concern.rb +11 -0
- data/app/models/concerns/spree/product_scopes.rb +6 -6
- data/app/models/concerns/spree/translatable_resource.rb +4 -0
- data/app/models/concerns/spree/translatable_resource_scopes.rb +17 -3
- data/app/models/concerns/spree/unique_name.rb +2 -0
- data/app/models/concerns/spree/user_management.rb +33 -0
- data/app/models/concerns/spree/user_methods.rb +19 -0
- data/app/models/concerns/spree/user_roles.rb +43 -17
- data/app/models/spree/ability.rb +8 -6
- data/app/models/spree/asset.rb +1 -6
- data/app/models/spree/base.rb +1 -0
- data/app/models/spree/base_analytics_event_handler.rb +7 -2
- data/app/models/spree/classification.rb +13 -0
- data/app/models/spree/credit_card.rb +24 -2
- data/app/models/spree/custom_domain.rb +3 -2
- data/app/models/spree/export.rb +1 -1
- data/app/models/spree/integration.rb +63 -0
- data/app/models/spree/invitation.rb +153 -0
- data/app/models/spree/invitations/store.rb +6 -0
- data/app/models/spree/option_value.rb +2 -2
- data/app/models/spree/order.rb +16 -5
- data/app/models/spree/order_merger.rb +7 -5
- data/app/models/spree/page_sections/featured_posts.rb +0 -4
- data/app/models/spree/payment.rb +1 -2
- data/app/models/spree/payment_source.rb +0 -21
- data/app/models/spree/post.rb +0 -1
- data/app/models/spree/product.rb +75 -5
- data/app/models/spree/product_property.rb +1 -1
- data/app/models/spree/promotion/rules/item_total.rb +12 -12
- data/app/models/spree/property.rb +3 -1
- data/app/models/spree/reports/sales_total.rb +5 -1
- data/app/models/spree/role.rb +16 -0
- data/app/models/spree/role_user.rb +32 -1
- data/app/models/spree/shipment_handler.rb +1 -0
- data/app/models/spree/shipping_method.rb +2 -2
- data/app/models/spree/store.rb +9 -4
- data/app/models/spree/store_credit_category.rb +4 -0
- data/app/models/spree/taxon.rb +4 -7
- data/app/models/spree/theme.rb +1 -1
- data/app/models/spree/wishlist.rb +0 -7
- data/app/services/spree/country_to_timezone.rb +273 -0
- data/app/services/spree/products/prepare_nested_attributes.rb +2 -9
- data/app/services/spree/seeds/admin_user.rb +4 -2
- data/app/services/spree/seeds/all.rb +3 -1
- data/app/services/spree/seeds/digital_delivery.rb +20 -0
- data/app/services/spree/seeds/returns_environment.rb +27 -0
- data/app/services/spree/seeds/tax_categories.rb +12 -0
- data/app/services/spree/stores/settings_defaults_by_country.rb +38 -0
- data/app/services/spree/tags/bulk_add.rb +13 -7
- data/app/views/spree/addresses/_form.html.erb +1 -1
- data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +12 -0
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +21 -0
- data/app/views/spree/shared/_payment.html.erb +0 -9
- data/config/locales/en.yml +48 -20
- data/db/migrate/20250407085228_create_spree_integrations.rb +12 -0
- data/db/migrate/20250410061306_create_spree_invitations.rb +20 -0
- data/db/migrate/20250418174652_add_resource_to_spree_role_users.rb +8 -0
- data/db/migrate/20250508060800_add_selected_locale_to_spree_admin_users.rb +8 -0
- data/db/migrate/20250509143831_add_session_id_to_spree_assets.rb +5 -0
- data/lib/generators/spree/authentication/devise/templates/authentication_helpers.rb.tt +3 -3
- data/lib/spree/core/controller_helpers/auth.rb +15 -14
- data/lib/spree/core/controller_helpers/currency.rb +11 -0
- data/lib/spree/core/controller_helpers/strong_parameters.rb +3 -2
- data/lib/spree/core/engine.rb +13 -3
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +1 -0
- data/lib/spree/permitted_attributes.rb +111 -13
- data/lib/spree/testing_support/common_rake.rb +7 -25
- data/lib/spree/testing_support/extension_rake.rb +1 -1
- data/lib/spree/testing_support/factories/integration_factory.rb +7 -0
- data/lib/spree/testing_support/factories/invitation_factory.rb +6 -0
- data/lib/spree/testing_support/factories/page_section_factory.rb +0 -4
- data/lib/spree/testing_support/factories/payment_factory.rb +0 -5
- data/lib/spree/testing_support/factories/payment_method_factory.rb +0 -5
- data/lib/spree/testing_support/factories/promotion_action_factory.rb +4 -0
- data/lib/spree/testing_support/factories/taxon_factory.rb +0 -6
- data/lib/spree/testing_support/factories/user_factory.rb +14 -1
- data/lib/spree/translation_migrations.rb +27 -15
- data/lib/tasks/core.rake +8 -0
- metadata +41 -8
- data/app/models/concerns/spree/payment_source_concern.rb +0 -39
- data/app/models/spree/gateway/custom_payment_source_method.rb +0 -33
- data/app/models/spree/product/slugs.rb +0 -112
- data/lib/spree/testing_support/factories/payment_source_factory.rb +0 -5
@@ -2,8 +2,6 @@ module Spree
|
|
2
2
|
class CreditCard < Spree.base_class
|
3
3
|
include ActiveMerchant::Billing::CreditCardMethods
|
4
4
|
include Spree::Metadata
|
5
|
-
include Spree::PaymentSourceConcern
|
6
|
-
|
7
5
|
if defined?(Spree::Webhooks::HasWebhooks)
|
8
6
|
include Spree::Webhooks::HasWebhooks
|
9
7
|
end
|
@@ -137,6 +135,30 @@ module Spree
|
|
137
135
|
brand.present? ? brand.upcase : Spree.t(:no_cc_type)
|
138
136
|
end
|
139
137
|
|
138
|
+
def actions
|
139
|
+
%w{capture void credit}
|
140
|
+
end
|
141
|
+
|
142
|
+
# Indicates whether its possible to capture the payment
|
143
|
+
def can_capture?(payment)
|
144
|
+
payment.pending? || payment.checkout?
|
145
|
+
end
|
146
|
+
|
147
|
+
# Indicates whether its possible to void the payment.
|
148
|
+
def can_void?(payment)
|
149
|
+
!payment.failed? && !payment.void?
|
150
|
+
end
|
151
|
+
|
152
|
+
# Indicates whether its possible to credit the payment. Note that most gateways require that the
|
153
|
+
# payment be settled first which generally happens within 12-24 hours of the transaction.
|
154
|
+
def can_credit?(payment)
|
155
|
+
payment.completed? && payment.credit_allowed > 0
|
156
|
+
end
|
157
|
+
|
158
|
+
def has_payment_profile?
|
159
|
+
gateway_customer_profile_id.present? || gateway_payment_profile_id.present?
|
160
|
+
end
|
161
|
+
|
140
162
|
# ActiveMerchant needs first_name/last_name because we pass it a Spree::CreditCard and it calls those methods on it.
|
141
163
|
# Looking at the ActiveMerchant source code we should probably be calling #to_active_merchant before passing
|
142
164
|
# the object to ActiveMerchant but this should do for now.
|
@@ -14,7 +14,7 @@ module Spree
|
|
14
14
|
# Validations
|
15
15
|
#
|
16
16
|
validates :url, presence: true, uniqueness: true, format: {
|
17
|
-
with: %r{[a-
|
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,9 +26,10 @@ 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
|
-
errors.add(:url, 'use domain or subdomain') if parts.size > 4 || parts.size < 2
|
32
|
+
errors.add(:url, 'use domain or subdomain') if (parts[0] != 'www' && parts.size > 3) || (parts[0] == 'www' && parts.size > 4) || parts.size < 2
|
32
33
|
end
|
33
34
|
|
34
35
|
def ensure_default
|
data/app/models/spree/export.rb
CHANGED
@@ -124,7 +124,7 @@ module Spree
|
|
124
124
|
end
|
125
125
|
|
126
126
|
def current_ability
|
127
|
-
@current_ability ||= Spree::Dependencies.ability_class.constantize.new(user)
|
127
|
+
@current_ability ||= Spree::Dependencies.ability_class.constantize.new(user, { store: store })
|
128
128
|
end
|
129
129
|
|
130
130
|
# eg. Spree::Exports::Products => products-store-my-store-code-20241030133348.csv
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Spree
|
2
|
+
class Integration < Spree.base_class
|
3
|
+
include Spree::SingleStoreResource
|
4
|
+
|
5
|
+
#
|
6
|
+
# Associations
|
7
|
+
#
|
8
|
+
belongs_to :store, class_name: 'Spree::Store', touch: true
|
9
|
+
|
10
|
+
#
|
11
|
+
# Validations
|
12
|
+
#
|
13
|
+
validates :type, presence: true
|
14
|
+
validates :store, presence: true, uniqueness: { scope: :type }
|
15
|
+
|
16
|
+
#
|
17
|
+
# Scopes
|
18
|
+
#
|
19
|
+
scope :active, -> { where(active: true) }
|
20
|
+
|
21
|
+
# This attribute is used to temporarily store connection-related error messages
|
22
|
+
# that can be displayed to users when testing or validating integration connections.
|
23
|
+
# It is not persisted to the database and is reset on each new connection attempt.
|
24
|
+
# @param message [String, nil] The error message to be stored
|
25
|
+
# @return [String, nil] The current error message
|
26
|
+
attr_accessor :connection_error_message
|
27
|
+
|
28
|
+
# Associates the integration to a group.
|
29
|
+
# The name here will be used as Spree.t key to display the group name.
|
30
|
+
# Leave blank to leave the integration ungrouped.
|
31
|
+
def self.integration_group
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.icon_path
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.integration_name
|
40
|
+
name.demodulize.titleize.strip
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.integration_key
|
44
|
+
name.demodulize.underscore
|
45
|
+
end
|
46
|
+
|
47
|
+
def name
|
48
|
+
self.class.integration_name
|
49
|
+
end
|
50
|
+
|
51
|
+
def key
|
52
|
+
self.class.integration_key
|
53
|
+
end
|
54
|
+
|
55
|
+
# Checks if the integration can establish a connection.
|
56
|
+
# This is a base implementation that always returns true.
|
57
|
+
# Subclasses should override this method to implement their own connection validation logic.
|
58
|
+
# @return [Boolean] true if the integration can connect, false otherwise
|
59
|
+
def can_connect?
|
60
|
+
true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module Spree
|
2
|
+
class Invitation < Spree.base_class
|
3
|
+
has_secure_token
|
4
|
+
acts_as_paranoid
|
5
|
+
|
6
|
+
#
|
7
|
+
# Virtual Attributes
|
8
|
+
#
|
9
|
+
attribute :skip_email, :boolean, default: false
|
10
|
+
|
11
|
+
#
|
12
|
+
# Associations
|
13
|
+
#
|
14
|
+
belongs_to :resource, polymorphic: true # eg. Store, Vendor, Account
|
15
|
+
belongs_to :inviter, polymorphic: true # User or AdminUser
|
16
|
+
belongs_to :invitee, polymorphic: true, optional: true # User or AdminUser
|
17
|
+
belongs_to :role, class_name: 'Spree::Role'
|
18
|
+
has_one :role_user, dependent: :destroy, class_name: 'Spree::RoleUser'
|
19
|
+
|
20
|
+
#
|
21
|
+
# Validations
|
22
|
+
#
|
23
|
+
validates :email, email: true, presence: true
|
24
|
+
validates :token, presence: true, uniqueness: true
|
25
|
+
validates :inviter, :resource, :role, presence: true
|
26
|
+
validate :invitee_is_not_inviter, on: :create
|
27
|
+
validate :invitee_already_exists, on: :create
|
28
|
+
|
29
|
+
#
|
30
|
+
# Scopes
|
31
|
+
#
|
32
|
+
scope :pending, -> { where(status: 'pending') }
|
33
|
+
scope :accepted, -> { where(status: 'accepted') }
|
34
|
+
scope :not_expired, -> { where('expires_at > ?', Time.current) }
|
35
|
+
|
36
|
+
#
|
37
|
+
# State Machine
|
38
|
+
#
|
39
|
+
state_machine initial: :pending, attribute: :status do
|
40
|
+
state :accepted do
|
41
|
+
validate :accept_invitation_within_time_limit
|
42
|
+
validates :invitee, presence: true
|
43
|
+
end
|
44
|
+
|
45
|
+
event :accept do
|
46
|
+
transition pending: :accepted
|
47
|
+
end
|
48
|
+
after_transition to: :accepted, do: :after_accept
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Callbacks
|
53
|
+
#
|
54
|
+
after_initialize :set_defaults, if: :new_record?
|
55
|
+
before_validation :set_invitee_from_email, on: :create
|
56
|
+
after_create :send_invitation_email, unless: :skip_email
|
57
|
+
|
58
|
+
# returns the store for the invitation
|
59
|
+
# if the resource is a store, return the resource
|
60
|
+
# if the resource responds to store, return the store
|
61
|
+
# otherwise, return the current store
|
62
|
+
# @return [Spree::Store]
|
63
|
+
def store
|
64
|
+
if resource.is_a?(Spree::Store)
|
65
|
+
resource
|
66
|
+
elsif resource.respond_to?(:store)
|
67
|
+
resource.store
|
68
|
+
else
|
69
|
+
Spree::Store.current
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# returns true if the invitation has expired
|
74
|
+
# @return [Boolean]
|
75
|
+
def expired?
|
76
|
+
expires_at < Time.current
|
77
|
+
end
|
78
|
+
|
79
|
+
# Resends the invitation email if the invitation is pending and not expired
|
80
|
+
def resend!
|
81
|
+
return if expired? || deleted? || accepted?
|
82
|
+
|
83
|
+
send_invitation_email
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# this method can be extended by developers now
|
89
|
+
def after_accept
|
90
|
+
create_role_user
|
91
|
+
set_accepted_at
|
92
|
+
send_acceptance_notification
|
93
|
+
end
|
94
|
+
|
95
|
+
def send_invitation_email
|
96
|
+
Spree::InvitationMailer.invitation_email(self).deliver_later
|
97
|
+
end
|
98
|
+
|
99
|
+
def send_acceptance_notification
|
100
|
+
Spree::InvitationMailer.invitation_accepted(self).deliver_later
|
101
|
+
end
|
102
|
+
|
103
|
+
def set_defaults
|
104
|
+
self.expires_at ||= 2.weeks.from_now
|
105
|
+
self.resource ||= Spree::Store.current
|
106
|
+
self.role ||= Spree::Role.default_admin_role
|
107
|
+
end
|
108
|
+
|
109
|
+
def invitee_is_not_inviter
|
110
|
+
if invitee == inviter
|
111
|
+
errors.add(:invitee, 'cannot be the same as the inviter')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def invitee_already_exists
|
116
|
+
return if resource.blank?
|
117
|
+
|
118
|
+
exists = if invitee.present?
|
119
|
+
resource.users.include?(invitee)
|
120
|
+
else
|
121
|
+
resource.users.exists?(email: email)
|
122
|
+
end
|
123
|
+
|
124
|
+
if exists
|
125
|
+
errors.add(:email, 'already exists')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def set_accepted_at
|
130
|
+
update!(accepted_at: Time.current)
|
131
|
+
end
|
132
|
+
|
133
|
+
def create_role_user
|
134
|
+
return if invitee.blank?
|
135
|
+
|
136
|
+
role_user = resource.add_user(invitee, role)
|
137
|
+
self.role_user = role_user
|
138
|
+
save!
|
139
|
+
end
|
140
|
+
|
141
|
+
def set_invitee_from_email
|
142
|
+
return if invitee.present?
|
143
|
+
|
144
|
+
self.invitee = Spree.admin_user_class.find_by(email: email)
|
145
|
+
end
|
146
|
+
|
147
|
+
def accept_invitation_within_time_limit
|
148
|
+
if Time.current > expires_at
|
149
|
+
errors.add(:base, 'Invitation expired')
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -62,9 +62,9 @@ module Spree
|
|
62
62
|
delegate :name, :presentation, to: :option_type, prefix: true, allow_nil: true
|
63
63
|
|
64
64
|
def self.to_tom_select_json
|
65
|
-
all.pluck(:
|
65
|
+
all.pluck(:id, :presentation).map do |id, presentation|
|
66
66
|
{
|
67
|
-
id:
|
67
|
+
id: id,
|
68
68
|
name: presentation
|
69
69
|
}
|
70
70
|
end
|
data/app/models/spree/order.rb
CHANGED
@@ -91,6 +91,8 @@ module Spree
|
|
91
91
|
acts_as_taggable_on :tags
|
92
92
|
acts_as_taggable_tenant :store_id
|
93
93
|
|
94
|
+
ASSOCIATED_USER_ATTRIBUTES = [:user_id, :email, :created_by_id, :bill_address_id, :ship_address_id]
|
95
|
+
|
94
96
|
belongs_to :user, class_name: "::#{Spree.user_class}", optional: true, autosave: true
|
95
97
|
belongs_to :created_by, class_name: "::#{Spree.admin_user_class}", optional: true
|
96
98
|
belongs_to :approver, class_name: "::#{Spree.admin_user_class}", optional: true
|
@@ -247,6 +249,13 @@ module Spree
|
|
247
249
|
pre_tax_item_amount + shipments.sum(:pre_tax_amount)
|
248
250
|
end
|
249
251
|
|
252
|
+
# Returns the subtotal used for analytics integrations
|
253
|
+
# It's a sum of the item total and the promo total
|
254
|
+
# @return [Float]
|
255
|
+
def analytics_subtotal
|
256
|
+
(item_total + line_items.sum(:promo_total)).to_f
|
257
|
+
end
|
258
|
+
|
250
259
|
def shipping_discount
|
251
260
|
shipment_adjustments.non_tax.eligible.sum(:amount) * - 1
|
252
261
|
end
|
@@ -355,7 +364,7 @@ module Spree
|
|
355
364
|
self.bill_address ||= user.bill_address
|
356
365
|
self.ship_address ||= user.ship_address
|
357
366
|
|
358
|
-
changes = slice(
|
367
|
+
changes = slice(*ASSOCIATED_USER_ATTRIBUTES)
|
359
368
|
|
360
369
|
# immediately persist the changes we just made, but don't use save
|
361
370
|
# since we might have an invalid address associated
|
@@ -364,6 +373,12 @@ module Spree
|
|
364
373
|
end
|
365
374
|
end
|
366
375
|
|
376
|
+
def disassociate_user!
|
377
|
+
nullified_attributes = ASSOCIATED_USER_ATTRIBUTES.index_with(nil)
|
378
|
+
|
379
|
+
update!(nullified_attributes)
|
380
|
+
end
|
381
|
+
|
367
382
|
def quantity_of(variant, options = {})
|
368
383
|
line_item = find_line_item_by_variant(variant, options)
|
369
384
|
line_item ? line_item.quantity : 0
|
@@ -815,10 +830,6 @@ module Spree
|
|
815
830
|
csv_lines
|
816
831
|
end
|
817
832
|
|
818
|
-
def all_line_items
|
819
|
-
line_items
|
820
|
-
end
|
821
|
-
|
822
833
|
private
|
823
834
|
|
824
835
|
def link_by_email
|
@@ -7,7 +7,7 @@ module Spree
|
|
7
7
|
@order = order
|
8
8
|
end
|
9
9
|
|
10
|
-
def merge!(other_order, user = nil)
|
10
|
+
def merge!(other_order, user = nil, discard_merged: true)
|
11
11
|
other_order.line_items.each do |other_order_line_item|
|
12
12
|
next unless other_order_line_item.currency == order.currency
|
13
13
|
|
@@ -16,12 +16,14 @@ module Spree
|
|
16
16
|
end
|
17
17
|
|
18
18
|
set_user(user)
|
19
|
-
clear_addresses(other_order)
|
19
|
+
clear_addresses(other_order) if discard_merged
|
20
20
|
persist_merge
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
if discard_merged
|
23
|
+
# So that the destroy doesn't take out line items which may have been re-assigned
|
24
|
+
other_order.line_items.reload
|
25
|
+
other_order.destroy
|
26
|
+
end
|
25
27
|
end
|
26
28
|
|
27
29
|
# Compare the line item of the other order with mine.
|
data/app/models/spree/payment.rb
CHANGED
@@ -302,8 +302,7 @@ module Spree
|
|
302
302
|
# Payment profile cannot be created without source
|
303
303
|
return unless source
|
304
304
|
# Imported payments shouldn't create a payment profile.
|
305
|
-
|
306
|
-
return if source.respond_to?(:imported) && source.imported
|
305
|
+
return if source.imported
|
307
306
|
|
308
307
|
payment_method.create_profile(self)
|
309
308
|
rescue ActiveMerchant::ConnectionError => e
|
@@ -1,31 +1,10 @@
|
|
1
|
-
# This model is used to store payment sources for non-credit card payments, eg wallet, account, etc.
|
2
1
|
module Spree
|
3
2
|
class PaymentSource < Spree.base_class
|
4
3
|
include Spree::Metadata
|
5
|
-
include Spree::PaymentSourceConcern
|
6
4
|
|
7
|
-
#
|
8
|
-
# Associations
|
9
|
-
#
|
10
5
|
belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
|
11
6
|
belongs_to :user, class_name: Spree.user_class.to_s, optional: true
|
12
7
|
|
13
|
-
#
|
14
|
-
# Validations
|
15
|
-
#
|
16
8
|
validates_uniqueness_of :gateway_payment_profile_id, scope: :type
|
17
|
-
|
18
|
-
#
|
19
|
-
# Delegations
|
20
|
-
#
|
21
|
-
delegate :profile_id, to: :gateway_customer, prefix: true, allow_nil: true
|
22
|
-
|
23
|
-
# Returns the gateway customer for the user.
|
24
|
-
# @return [Spree::GatewayCustomer]
|
25
|
-
def gateway_customer
|
26
|
-
return if user.blank?
|
27
|
-
|
28
|
-
payment_method.gateway_customers.find_by(user: user)
|
29
|
-
end
|
30
9
|
end
|
31
10
|
end
|
data/app/models/spree/post.rb
CHANGED
@@ -54,7 +54,6 @@ module Spree
|
|
54
54
|
# Scopes
|
55
55
|
#
|
56
56
|
scope :published, -> { where(published_at: [..Time.current]) }
|
57
|
-
scope :by_newest, -> { order(created_at: :desc) }
|
58
57
|
|
59
58
|
delegate :name, to: :author, prefix: true, allow_nil: true
|
60
59
|
delegate :title, to: :post_category, prefix: true, allow_nil: true
|
data/app/models/spree/product.rb
CHANGED
@@ -20,17 +20,14 @@
|
|
20
20
|
|
21
21
|
module Spree
|
22
22
|
class Product < Spree.base_class
|
23
|
-
|
24
|
-
acts_as_taggable_on :tags, :labels
|
25
|
-
auto_strip_attributes :name
|
26
|
-
|
23
|
+
extend FriendlyId
|
27
24
|
include Spree::ProductScopes
|
28
25
|
include Spree::MultiStoreResource
|
29
26
|
include Spree::TranslatableResource
|
27
|
+
include Spree::TranslatableResourceSlug
|
30
28
|
include Spree::MemoizedData
|
31
29
|
include Spree::Metadata
|
32
30
|
include Spree::Product::Webhooks
|
33
|
-
include Spree::Product::Slugs
|
34
31
|
if defined?(Spree::VendorConcern)
|
35
32
|
include Spree::VendorConcern
|
36
33
|
end
|
@@ -56,8 +53,33 @@ module Spree
|
|
56
53
|
pg_search_scope :search_by_name, against: { name: 'A', meta_title: 'B' },
|
57
54
|
using: { tsearch: { prefix: true, any_word: true } }
|
58
55
|
end
|
56
|
+
|
57
|
+
before_save :set_slug
|
58
|
+
acts_as_paranoid
|
59
|
+
# deleted translation values also need to be accessible for index views listing deleted resources
|
60
|
+
default_scope { unscope(where: :deleted_at) }
|
61
|
+
def set_slug
|
62
|
+
self.slug = generate_slug
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def generate_slug
|
68
|
+
if name.blank? && slug.blank?
|
69
|
+
translated_model.name.to_url
|
70
|
+
elsif slug.blank?
|
71
|
+
name.to_url
|
72
|
+
else
|
73
|
+
slug.to_url
|
74
|
+
end
|
75
|
+
end
|
59
76
|
end
|
60
77
|
|
78
|
+
friendly_id :slug_candidates, use: [:history, :scoped, :mobility], scope: spree_base_uniqueness_scope
|
79
|
+
acts_as_paranoid
|
80
|
+
auto_strip_attributes :name
|
81
|
+
acts_as_taggable_on :tags, :labels
|
82
|
+
|
61
83
|
# we need to have this callback before any dependent: :destroy associations
|
62
84
|
# https://github.com/rails/rails/issues/3458
|
63
85
|
before_destroy :ensure_not_in_complete_orders
|
@@ -123,12 +145,17 @@ module Spree
|
|
123
145
|
after_initialize :ensure_master
|
124
146
|
after_initialize :assign_default_tax_category
|
125
147
|
|
148
|
+
before_validation :downcase_slug
|
149
|
+
before_validation :normalize_slug, on: :update
|
126
150
|
before_validation :validate_master
|
127
151
|
before_validation :ensure_default_shipping_category
|
128
152
|
|
129
153
|
after_create :add_associations_from_prototype
|
130
154
|
after_create :build_variants_from_option_values_hash, if: :option_values_hash
|
131
155
|
|
156
|
+
after_destroy :punch_slug
|
157
|
+
after_restore :update_slug_history
|
158
|
+
|
132
159
|
after_save :save_master
|
133
160
|
after_save :run_touch_callbacks, if: :anything_changed?
|
134
161
|
after_save :reset_nested_changes
|
@@ -146,6 +173,7 @@ module Spree
|
|
146
173
|
validates :price, if: :requires_price?
|
147
174
|
end
|
148
175
|
|
176
|
+
validates :slug, presence: true, uniqueness: { allow_blank: true, case_sensitive: true, scope: spree_base_uniqueness_scope }
|
149
177
|
validate :discontinue_on_must_be_later_than_make_active_at, if: -> { make_active_at && discontinue_on }
|
150
178
|
|
151
179
|
scope :for_store, ->(store) { joins(:store_products).where(StoreProduct.table_name => { store_id: store.id }) }
|
@@ -484,6 +512,17 @@ module Spree
|
|
484
512
|
where conditions.inject(:or)
|
485
513
|
end
|
486
514
|
|
515
|
+
def self.slug_available?(slug, id)
|
516
|
+
!where(slug: slug).where.not(id: id).exists?
|
517
|
+
end
|
518
|
+
|
519
|
+
def ensure_slug_is_unique(candidate_slug)
|
520
|
+
return slug if candidate_slug.blank? || slug.blank?
|
521
|
+
return candidate_slug if self.class.slug_available?(candidate_slug, id)
|
522
|
+
|
523
|
+
normalize_friendly_id([candidate_slug, uuid_for_friendly_id])
|
524
|
+
end
|
525
|
+
|
487
526
|
# Suitable for displaying only variants that has at least one option value.
|
488
527
|
# There may be scenarios where an option type is removed and along with it
|
489
528
|
# all option values. At that point all variants associated with only those
|
@@ -699,6 +738,25 @@ module Spree
|
|
699
738
|
self.tax_category = Spree::TaxCategory.default if new_record?
|
700
739
|
end
|
701
740
|
|
741
|
+
def normalize_slug
|
742
|
+
self.slug = normalize_friendly_id(slug)
|
743
|
+
end
|
744
|
+
|
745
|
+
def punch_slug
|
746
|
+
# punch slug with date prefix to allow reuse of original
|
747
|
+
return if frozen?
|
748
|
+
|
749
|
+
update_column(:slug, "#{Time.current.to_i}_#{slug}"[0..254])
|
750
|
+
|
751
|
+
translations.with_deleted.each do |t|
|
752
|
+
t.update_column :slug, "#{Time.current.to_i}_#{t.slug}"[0..254]
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
def update_slug_history
|
757
|
+
save!
|
758
|
+
end
|
759
|
+
|
702
760
|
def anything_changed?
|
703
761
|
saved_changes? || @nested_changes
|
704
762
|
end
|
@@ -754,6 +812,14 @@ module Spree
|
|
754
812
|
end
|
755
813
|
end
|
756
814
|
|
815
|
+
# Try building a slug based on the following fields in increasing order of specificity.
|
816
|
+
def slug_candidates
|
817
|
+
[
|
818
|
+
:name,
|
819
|
+
[:name, :sku]
|
820
|
+
]
|
821
|
+
end
|
822
|
+
|
757
823
|
def run_touch_callbacks
|
758
824
|
run_callbacks(:touch)
|
759
825
|
end
|
@@ -801,6 +867,10 @@ module Spree
|
|
801
867
|
previously_new_record? || tag_list_previously_changed? || available_on_previously_changed?
|
802
868
|
end
|
803
869
|
|
870
|
+
def downcase_slug
|
871
|
+
slug&.downcase!
|
872
|
+
end
|
873
|
+
|
804
874
|
def after_activate
|
805
875
|
# Implement your logic here
|
806
876
|
end
|
@@ -33,7 +33,7 @@ module Spree
|
|
33
33
|
scope :filterable, -> { joins(:property).where(Property.table_name => { filterable: true }) }
|
34
34
|
scope :for_products, ->(products) { joins(:product).merge(products) }
|
35
35
|
scope :sort_by_property_position, -> {
|
36
|
-
joins(:property).order("spree_properties.position ASC")
|
36
|
+
unscope(:order).joins(:property).order("spree_properties.position ASC")
|
37
37
|
}
|
38
38
|
|
39
39
|
self.whitelisted_ransackable_attributes = ['value', 'filter_param']
|
@@ -27,39 +27,39 @@ module Spree
|
|
27
27
|
upper_limit_condition = true
|
28
28
|
end
|
29
29
|
|
30
|
-
eligibility_errors.add(:base, ineligible_message_max
|
31
|
-
eligibility_errors.add(:base, ineligible_message_min
|
30
|
+
eligibility_errors.add(:base, ineligible_message_max) unless upper_limit_condition
|
31
|
+
eligibility_errors.add(:base, ineligible_message_min) unless lower_limit_condition
|
32
32
|
|
33
33
|
eligibility_errors.empty?
|
34
34
|
end
|
35
35
|
|
36
36
|
private
|
37
37
|
|
38
|
-
def formatted_amount_min
|
39
|
-
Spree::Money.new(preferred_amount_min
|
38
|
+
def formatted_amount_min
|
39
|
+
Spree::Money.new(preferred_amount_min).to_s
|
40
40
|
end
|
41
41
|
|
42
|
-
def formatted_amount_max
|
42
|
+
def formatted_amount_max
|
43
43
|
if preferred_amount_max.present?
|
44
|
-
Spree::Money.new(preferred_amount_max
|
44
|
+
Spree::Money.new(preferred_amount_max).to_s
|
45
45
|
else
|
46
46
|
Spree.t('no_maximum')
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
def ineligible_message_max
|
50
|
+
def ineligible_message_max
|
51
51
|
if preferred_operator_max == 'lt'
|
52
|
-
eligibility_error_message(:item_total_more_than_or_equal, amount: formatted_amount_max
|
52
|
+
eligibility_error_message(:item_total_more_than_or_equal, amount: formatted_amount_max)
|
53
53
|
else
|
54
|
-
eligibility_error_message(:item_total_more_than, amount: formatted_amount_max
|
54
|
+
eligibility_error_message(:item_total_more_than, amount: formatted_amount_max)
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
def ineligible_message_min
|
58
|
+
def ineligible_message_min
|
59
59
|
if preferred_operator_min == 'gte'
|
60
|
-
eligibility_error_message(:item_total_less_than, amount: formatted_amount_min
|
60
|
+
eligibility_error_message(:item_total_less_than, amount: formatted_amount_min)
|
61
61
|
else
|
62
|
-
eligibility_error_message(:item_total_less_than_or_equal, amount: formatted_amount_min
|
62
|
+
eligibility_error_message(:item_total_less_than_or_equal, amount: formatted_amount_min)
|
63
63
|
end
|
64
64
|
end
|
65
65
|
end
|
@@ -39,8 +39,10 @@ module Spree
|
|
39
39
|
KIND_OPTIONS = { short_text: 0, long_text: 1, number: 2, rich_text: 3 }.freeze
|
40
40
|
enum :kind, KIND_OPTIONS
|
41
41
|
|
42
|
+
DEPENDENCY_UPDATE_FIELDS = [:presentation, :name, :kind, :filterable, :display_on, :position].freeze
|
43
|
+
|
42
44
|
after_touch :touch_all_products
|
43
|
-
after_update :touch_all_products, if: -> { saved_changes.key?(
|
45
|
+
after_update :touch_all_products, if: -> { DEPENDENCY_UPDATE_FIELDS.any? { |field| saved_changes.key?(field) } }
|
44
46
|
after_save :ensure_product_properties_have_filter_params
|
45
47
|
|
46
48
|
self.whitelisted_ransackable_attributes = ['presentation', 'filterable']
|