spree_core 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/helpers/spree/base_helper.rb +12 -2
- data/app/helpers/spree/currency_helper.rb +1 -4
- data/app/helpers/spree/locale_helper.rb +18 -13
- data/app/helpers/spree/localized_names_helper.rb +25 -0
- data/app/mailers/spree/admin_user_mailer.rb +71 -0
- data/app/mailers/spree/base_mailer.rb +73 -11
- data/app/mailers/spree/export_mailer.rb +7 -7
- data/app/mailers/spree/import_mailer.rb +19 -0
- data/app/mailers/spree/invitation_mailer.rb +16 -11
- data/app/mailers/spree/report_mailer.rb +6 -6
- data/app/mailers/spree/webhook_mailer.rb +7 -6
- data/app/models/concerns/spree/admin_user_methods.rb +44 -0
- data/app/models/concerns/spree/legacy_multi_store_support.rb +48 -0
- data/app/models/concerns/spree/presentation_translatable.rb +39 -0
- data/app/models/concerns/spree/product_scopes.rb +21 -3
- data/app/models/concerns/spree/single_store_resource.rb +5 -0
- data/app/models/concerns/spree/store_scoped_resource.rb +10 -0
- data/app/models/concerns/spree/stores/markets.rb +9 -1
- data/app/models/concerns/spree/stores/setup.rb +37 -23
- data/app/models/concerns/spree/translatable_resource.rb +95 -5
- data/app/models/concerns/spree/user_methods.rb +27 -2
- data/app/models/spree/ability.rb +5 -4
- data/app/models/spree/allowed_origin.rb +31 -0
- data/app/models/spree/category.rb +12 -2
- data/app/models/spree/channel/gating.rb +67 -0
- data/app/models/spree/channel.rb +1 -0
- data/app/models/spree/classification.rb +12 -0
- data/app/models/spree/country.rb +22 -1
- data/app/models/spree/credit_card.rb +29 -0
- data/app/models/spree/currency.rb +47 -0
- data/app/models/spree/current.rb +12 -1
- data/app/models/spree/customer_group.rb +1 -0
- data/app/models/spree/export.rb +19 -0
- data/app/models/spree/exports/coupon_codes.rb +1 -2
- data/app/models/spree/import.rb +84 -1
- data/app/models/spree/import_row.rb +7 -1
- data/app/models/spree/imports/product_translations.rb +6 -0
- data/app/models/spree/locale.rb +104 -0
- data/app/models/spree/option_type.rb +11 -13
- data/app/models/spree/option_value.rb +5 -17
- data/app/models/spree/order.rb +19 -2
- data/app/models/spree/payment_method.rb +6 -5
- data/app/models/spree/policy.rb +2 -1
- data/app/models/spree/product.rb +23 -2
- data/app/models/spree/promotion/rules/channel.rb +37 -0
- data/app/models/spree/promotion/rules/market.rb +37 -0
- data/app/models/spree/promotion.rb +5 -3
- data/app/models/spree/promotion_handler/promotion_duplicator.rb +1 -1
- data/app/models/spree/role_user.rb +4 -0
- data/app/models/spree/search_provider/meilisearch.rb +22 -9
- data/app/models/spree/setup_task.rb +12 -0
- data/app/models/spree/setup_tasks.rb +86 -0
- data/app/models/spree/shipment.rb +38 -1
- data/app/models/spree/shipment_handler.rb +1 -1
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/quantifier.rb +21 -1
- data/app/models/spree/stock_item.rb +2 -1
- data/app/models/spree/stock_location.rb +4 -1
- data/app/models/spree/store.rb +45 -9
- data/app/models/spree/store_payment_method.rb +5 -1
- data/app/models/spree/store_promotion.rb +4 -0
- data/app/models/spree/taxon.rb +112 -9
- data/app/models/spree/taxonomy.rb +1 -1
- data/app/models/spree/translations/batch.rb +104 -0
- data/app/models/spree/variant.rb +29 -2
- data/app/presenters/spree/search_provider/product_presenter.rb +3 -0
- data/app/services/spree/carts/complete.rb +3 -0
- data/app/services/spree/fulfillments/create.rb +259 -0
- data/app/services/spree/locales/set_fallback_locale_for_store.rb +7 -1
- data/app/services/spree/locales.rb +52 -0
- data/app/services/spree/products/prepare_nested_attributes.rb +40 -12
- data/app/services/spree/shipments/update.rb +1 -1
- data/app/services/spree/stock_reservations/reserve.rb +4 -1
- data/app/services/spree/taxons/add_products.rb +3 -0
- data/app/services/spree/taxons/remove_products.rb +3 -0
- data/app/subscribers/spree/admin_user_email_subscriber.rb +29 -0
- data/app/subscribers/spree/import_email_subscriber.rb +26 -0
- data/app/views/layouts/spree/base_mailer.html.erb +14 -17
- data/app/views/spree/admin_user_mailer/confirmation_email.html.erb +7 -0
- data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +8 -0
- data/app/views/spree/export_mailer/export_done.html.erb +6 -14
- data/app/views/spree/import_mailer/import_done.html.erb +12 -0
- data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +4 -12
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +11 -24
- data/app/views/spree/report_mailer/report_done.html.erb +6 -14
- data/app/views/spree/shared/_base_mailer_footer.html.erb +15 -8
- data/app/views/spree/shared/_base_mailer_header.html.erb +7 -5
- data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +211 -273
- data/app/views/spree/shared/_mailer_button.html.erb +10 -0
- data/app/views/spree/shared/_mailer_hero.html.erb +12 -0
- data/app/views/spree/shared/_purchased_items_styles.html.erb +112 -0
- data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +39 -14
- data/config/initializers/spree_store_setup_tasks.rb +23 -0
- data/config/locales/en.yml +42 -992
- data/db/migrate/20260613000001_add_store_id_to_spree_role_users.rb +10 -0
- data/db/migrate/20260626000001_add_store_id_to_spree_taxons.rb +11 -0
- data/db/migrate/20260627000001_add_products_count_to_spree_taxons.rb +6 -0
- data/db/migrate/20260628000001_add_store_id_to_spree_promotions.rb +10 -0
- data/db/migrate/20260628000002_add_store_id_to_spree_payment_methods.rb +11 -0
- data/db/migrate/20260630000001_add_preorder_fields_to_spree_variants.rb +7 -0
- data/db/migrate/20260707000001_add_fingerprint_to_spree_credit_cards.rb +39 -0
- data/db/migrate/20260710000001_add_import_id_status_index_to_spree_import_rows.rb +7 -0
- data/db/migrate/20260711000001_add_preferences_to_spree_exports.rb +7 -0
- data/db/sample_data/promotions.rb +1 -1
- data/lib/generators/spree/authentication/dummy/templates/create_spree_admin_users.rb.tt +0 -1
- data/lib/mobility/plugins/store_based_fallbacks.rb +7 -5
- data/lib/spree/core/configuration.rb +1 -0
- data/lib/spree/core/controller_helpers/locale.rb +4 -1
- data/lib/spree/core/controller_helpers/strong_parameters.rb +1 -0
- data/lib/spree/core/dependencies.rb +3 -0
- data/lib/spree/core/engine.rb +24 -0
- data/lib/spree/core/preferences/preferable.rb +6 -1
- data/lib/spree/core/previews/admin_user_preview.rb +22 -0
- data/lib/spree/core/previews/export_preview.rb +37 -0
- data/lib/spree/core/previews/invitation_preview.rb +47 -0
- data/lib/spree/core/previews/preview_data.rb +52 -0
- data/lib/spree/core/previews/report_preview.rb +40 -0
- data/lib/spree/core/previews/webhook_preview.rb +34 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +22 -1
- data/lib/spree/permitted_attributes.rb +5 -4
- data/lib/spree/testing_support/factories/export_factory.rb +4 -0
- data/lib/spree/testing_support/factories/payment_method_factory.rb +1 -9
- data/lib/spree/testing_support/factories/promotion_factory.rb +1 -9
- data/lib/spree/testing_support/factories/promotion_rule_factory.rb +24 -0
- data/lib/spree/translations.rb +113 -0
- data/lib/spree/upgrades/5_5_to_5_6/manifest.yml +59 -0
- data/lib/tasks/role_users.rake +21 -0
- data/lib/tasks/single_store_associations.rake +65 -0
- data/lib/tasks/taxons.rake +62 -1
- metadata +47 -5
- data/app/models/spree/product/legacy_multi_store_support.rb +0 -40
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class AddStoreIdToSpreeRoleUsers < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
# Denormalizes the store a role assignment applies within, so role
|
|
4
|
+
# resolution (Spree::Ability) can scope by store without depending on the
|
|
5
|
+
# polymorphic resource. Kept null: true here; existing rows are backfilled
|
|
6
|
+
# by `spree:role_users:backfill_store_ids` and presence is enforced
|
|
7
|
+
# on the model.
|
|
8
|
+
add_reference :spree_role_users, :store, null: true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class AddStoreIdToSpreeTaxons < ActiveRecord::Migration[7.2]
|
|
2
|
+
# NOTE: After running this migration, existing taxons have +store_id IS NULL+
|
|
3
|
+
# and keep resolving their store through their taxonomy (Taxon.for_store falls
|
|
4
|
+
# back to the taxonomy join). Run the backfill to populate +store_id+ directly,
|
|
5
|
+
# which is what taxonomy-less categories (Spree::Category) rely on:
|
|
6
|
+
#
|
|
7
|
+
# bundle exec rake spree:taxons:backfill_store_id
|
|
8
|
+
def change
|
|
9
|
+
add_reference :spree_taxons, :store, null: true, if_not_exists: true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class AddStoreIdToSpreePromotions < ActiveRecord::Migration[7.2]
|
|
2
|
+
# NOTE: After running this migration, existing promotions have +store_id IS NULL+
|
|
3
|
+
# and are invisible to +Promotion.for_store+. Run the backfill immediately to
|
|
4
|
+
# copy ownership from the legacy +spree_promotions_stores+ join table:
|
|
5
|
+
#
|
|
6
|
+
# bundle exec rake spree:upgrade:populate_single_store_associations
|
|
7
|
+
def change
|
|
8
|
+
add_reference :spree_promotions, :store, null: true, if_not_exists: true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class AddStoreIdToSpreePaymentMethods < ActiveRecord::Migration[7.2]
|
|
2
|
+
# NOTE: After running this migration, existing payment methods have
|
|
3
|
+
# +store_id IS NULL+ and are invisible to +PaymentMethod.for_store+. Run the
|
|
4
|
+
# backfill immediately to copy ownership from the legacy
|
|
5
|
+
# +spree_payment_methods_stores+ join table:
|
|
6
|
+
#
|
|
7
|
+
# bundle exec rake spree:upgrade:populate_single_store_associations
|
|
8
|
+
def change
|
|
9
|
+
add_reference :spree_payment_methods, :store, null: true, if_not_exists: true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class AddFingerprintToSpreeCreditCards < ActiveRecord::Migration[7.2]
|
|
2
|
+
INDEX_NAME = 'index_spree_credit_cards_unique_fingerprint'.freeze
|
|
3
|
+
|
|
4
|
+
def up
|
|
5
|
+
add_column :spree_credit_cards, :fingerprint, :string
|
|
6
|
+
|
|
7
|
+
# Prevent duplicate saved cards (same gateway fingerprint + expiry) per user
|
|
8
|
+
# and payment method at the database level, backing the application-level
|
|
9
|
+
# check against concurrent writes. Only active, fingerprinted cards are
|
|
10
|
+
# constrained, so legacy/non-gateway cards (NULL fingerprint) and
|
|
11
|
+
# soft-deleted rows are left untouched.
|
|
12
|
+
if ActiveRecord::Base.connection.adapter_name == 'Mysql2'
|
|
13
|
+
# MySQL has no partial indexes, but treats NULL as distinct in unique
|
|
14
|
+
# indexes, so NULL fingerprints are naturally allowed. COALESCE on
|
|
15
|
+
# deleted_at keeps soft-deleted rows from colliding with active ones.
|
|
16
|
+
execute <<-SQL
|
|
17
|
+
CREATE UNIQUE INDEX #{INDEX_NAME}
|
|
18
|
+
ON spree_credit_cards(
|
|
19
|
+
user_id,
|
|
20
|
+
payment_method_id,
|
|
21
|
+
fingerprint,
|
|
22
|
+
month,
|
|
23
|
+
year,
|
|
24
|
+
(COALESCE(deleted_at, CAST('1970-01-01' AS DATETIME)))
|
|
25
|
+
)
|
|
26
|
+
SQL
|
|
27
|
+
else
|
|
28
|
+
add_index :spree_credit_cards, [:user_id, :payment_method_id, :fingerprint, :month, :year],
|
|
29
|
+
unique: true,
|
|
30
|
+
where: 'fingerprint IS NOT NULL AND deleted_at IS NULL',
|
|
31
|
+
name: INDEX_NAME
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def down
|
|
36
|
+
remove_index :spree_credit_cards, name: INDEX_NAME
|
|
37
|
+
remove_column :spree_credit_cards, :fingerprint
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class AddImportIdStatusIndexToSpreeImportRows < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
# Powers the Admin API's per-poll grouped status counts and the
|
|
4
|
+
# failed-rows listing without a full scan on large imports.
|
|
5
|
+
add_index :spree_import_rows, [:import_id, :status]
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class AddPreferencesToSpreeExports < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
# Same serialized preferences store spree_imports already has — first
|
|
4
|
+
# consumer is the `results_url` the export-done email links back to.
|
|
5
|
+
add_column :spree_exports, :preferences, :text
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -2,7 +2,7 @@ promotion = Spree::Promotion.where(
|
|
|
2
2
|
name: 'Free Shipping',
|
|
3
3
|
code: 'FREESHIP'
|
|
4
4
|
).first_or_create! do |promo|
|
|
5
|
-
promo.
|
|
5
|
+
promo.store = Spree::Store.default
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
Spree::Promotion::Actions::FreeShipping.where(promotion: promotion).first_or_create!
|
|
@@ -2,7 +2,6 @@ class CreateSpreeAdminUsers < ActiveRecord::Migration<%= migration_version %>
|
|
|
2
2
|
def change
|
|
3
3
|
create_table :spree_admin_users do |t|
|
|
4
4
|
t.string :encrypted_password, limit: 128
|
|
5
|
-
t.string :password_salt, limit: 128
|
|
6
5
|
t.string :email
|
|
7
6
|
t.string :remember_token
|
|
8
7
|
t.string :persistence_token
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'concurrent'
|
|
4
3
|
require 'mobility'
|
|
5
4
|
|
|
6
5
|
# The default implementation in the Mobility gem requires fallbacks to be defined when booting the application.
|
|
@@ -8,15 +7,18 @@ require 'mobility'
|
|
|
8
7
|
# The implementation is based on the default fallbacks plugin, with some changes around fetching the list of fallbacks to be used.
|
|
9
8
|
# https://github.com/shioyama/mobility/blob/master/lib/mobility/plugins/fallbacks.rb
|
|
10
9
|
module Mobility
|
|
11
|
-
@store_based_fallbacks = Concurrent::ThreadLocalVar.new(I18n::Locale::Fallbacks.new)
|
|
12
|
-
|
|
13
10
|
class << self
|
|
11
|
+
# Request-scoped via RequestStore (like Mobility.locale), so one request's
|
|
12
|
+
# store fallbacks can't leak into the next request served by the same
|
|
13
|
+
# thread. Outside a request RequestStore degrades to plain per-thread
|
|
14
|
+
# storage; Spree::BaseMailer#with_store_locale restores the previous value
|
|
15
|
+
# around mail rendering.
|
|
14
16
|
def store_based_fallbacks
|
|
15
|
-
|
|
17
|
+
RequestStore.store[:mobility_store_based_fallbacks] ||= I18n::Locale::Fallbacks.new
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def store_based_fallbacks=(value)
|
|
19
|
-
|
|
21
|
+
RequestStore.store[:mobility_store_based_fallbacks] = value
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
|
|
@@ -118,6 +118,7 @@ module Spree
|
|
|
118
118
|
preference :coupon_codes_total_limit, :integer, default: 5000 # the maximum number of coupon codes to be generated
|
|
119
119
|
|
|
120
120
|
# password reset
|
|
121
|
+
preference :admin_password_reset_expires_in, :integer, default: 15 # admin password reset token expiration time in minutes
|
|
121
122
|
preference :customer_password_reset_expires_in, :integer, default: 15 # password reset token expiration time in minutes
|
|
122
123
|
|
|
123
124
|
# gift cards
|
|
@@ -19,7 +19,10 @@ module Spree
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def set_locale
|
|
22
|
-
|
|
22
|
+
# Request-local content locale — the fallback for storeless hosts
|
|
23
|
+
# lives in the Spree::Current#content_locale reader; never write the
|
|
24
|
+
# process-global I18n.default_locale, which all threads share.
|
|
25
|
+
Spree::Current.content_locale = current_store&.default_locale
|
|
23
26
|
I18n.locale = current_locale
|
|
24
27
|
end
|
|
25
28
|
|
|
@@ -33,6 +33,7 @@ module Spree
|
|
|
33
33
|
|
|
34
34
|
def permitted_product_attributes
|
|
35
35
|
permitted_attributes.product_attributes + [
|
|
36
|
+
removed_variant_ids: [],
|
|
36
37
|
variants_attributes: permitted_variant_attributes + ['id', :_destroy],
|
|
37
38
|
master_attributes: permitted_variant_attributes + ['id']
|
|
38
39
|
]
|
|
@@ -49,6 +49,9 @@ module Spree
|
|
|
49
49
|
order_update_service: 'Spree::Orders::Update',
|
|
50
50
|
order_updater: 'Spree::OrderUpdater',
|
|
51
51
|
|
|
52
|
+
# fulfillment
|
|
53
|
+
fulfillment_create_service: 'Spree::Fulfillments::Create',
|
|
54
|
+
|
|
52
55
|
# shipment
|
|
53
56
|
shipment_change_state_service: 'Spree::Shipments::ChangeState',
|
|
54
57
|
shipment_create_service: 'Spree::Shipments::Create',
|
data/lib/spree/core/engine.rb
CHANGED
|
@@ -46,6 +46,14 @@ module Spree
|
|
|
46
46
|
# Add app/subscribers to autoload paths
|
|
47
47
|
config.paths.add 'app/subscribers', eager_load: true
|
|
48
48
|
|
|
49
|
+
# Register bundled ActionMailer previews so they show up at /rails/mailers
|
|
50
|
+
# without the host app having to copy any files.
|
|
51
|
+
initializer 'spree.mailer_previews' do |app|
|
|
52
|
+
if app.config.action_mailer.show_previews
|
|
53
|
+
app.config.action_mailer.preview_paths << File.expand_path('previews', __dir__)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
49
57
|
initializer 'spree.environment', before: :load_config_initializers do |app|
|
|
50
58
|
app.config.spree = Environment.new(SpreeCalculators.new, SpreeValidators.new, Spree::Core::Configuration.new, Spree::Core::Dependencies.new)
|
|
51
59
|
|
|
@@ -57,6 +65,18 @@ module Spree
|
|
|
57
65
|
Spree::Deprecation = ActiveSupport::Deprecation.new('6.0', 'Spree')
|
|
58
66
|
end
|
|
59
67
|
|
|
68
|
+
# I18n's config lives in fiber/thread-local storage that survives across
|
|
69
|
+
# requests on reused server threads, so a request that never assigns its
|
|
70
|
+
# own locale would render in whatever locale the previous request on the
|
|
71
|
+
# same thread set. The i18n gem ships a middleware that resets it after
|
|
72
|
+
# every request; Rails does not install it by default. Mobility's request
|
|
73
|
+
# state needs no counterpart here — Mobility.locale and Spree's
|
|
74
|
+
# Mobility.store_based_fallbacks live in RequestStore, which is cleared
|
|
75
|
+
# per request by request_store's own middleware.
|
|
76
|
+
initializer 'spree.locale_state_reset' do |app|
|
|
77
|
+
app.middleware.use ::I18n::Middleware
|
|
78
|
+
end
|
|
79
|
+
|
|
60
80
|
initializer 'spree.register.subscribers', before: :load_config_initializers do |app|
|
|
61
81
|
# Initialize subscribers array early so engines can add subscribers via initializers
|
|
62
82
|
app.config.spree.subscribers = []
|
|
@@ -179,6 +199,8 @@ module Spree
|
|
|
179
199
|
Rails.application.config.spree.promotions.rules.concat [
|
|
180
200
|
Spree::Promotion::Rules::Currency,
|
|
181
201
|
Spree::Promotion::Rules::Country,
|
|
202
|
+
Spree::Promotion::Rules::Channel,
|
|
203
|
+
Spree::Promotion::Rules::Market,
|
|
182
204
|
Spree::Promotion::Rules::ItemTotal,
|
|
183
205
|
Spree::Promotion::Rules::Product,
|
|
184
206
|
Spree::Promotion::Rules::User,
|
|
@@ -245,6 +267,7 @@ module Spree
|
|
|
245
267
|
|
|
246
268
|
Rails.application.config.spree.translatable_resources = [
|
|
247
269
|
Spree::OptionType,
|
|
270
|
+
Spree::OptionValue,
|
|
248
271
|
Spree::Product,
|
|
249
272
|
Spree::Taxon,
|
|
250
273
|
Spree::Taxonomy,
|
|
@@ -345,6 +368,7 @@ module Spree
|
|
|
345
368
|
Spree::ExportSubscriber,
|
|
346
369
|
Spree::ReportSubscriber,
|
|
347
370
|
Spree::InvitationEmailSubscriber,
|
|
371
|
+
Spree::AdminUserEmailSubscriber,
|
|
348
372
|
Spree::ProductMetricsSubscriber
|
|
349
373
|
]
|
|
350
374
|
|
|
@@ -135,7 +135,12 @@ module Spree::Preferences::Preferable
|
|
|
135
135
|
int_value ||= 0 unless nullable
|
|
136
136
|
int_value.present? ? int_value.to_i : int_value
|
|
137
137
|
when :boolean
|
|
138
|
-
|
|
138
|
+
# A nullable boolean keeps "unset" (nil / empty string) as nil so it can
|
|
139
|
+
# fall back to another value, instead of collapsing it to false. An
|
|
140
|
+
# explicit false is preserved (it is neither nil nor empty).
|
|
141
|
+
if nullable && (value.nil? || (value.respond_to?(:empty?) && value.empty?))
|
|
142
|
+
nil
|
|
143
|
+
elsif value.is_a?(FalseClass) ||
|
|
139
144
|
value.nil? ||
|
|
140
145
|
value == 0 ||
|
|
141
146
|
value&.to_s =~ /^(f|false|0)$/i ||
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require_relative 'preview_data'
|
|
2
|
+
|
|
3
|
+
# Preview Spree admin user auth emails at /rails/mailers/spree/admin_user
|
|
4
|
+
class Spree::AdminUserPreview < ActionMailer::Preview
|
|
5
|
+
include Spree::PreviewData::LocaleParam
|
|
6
|
+
|
|
7
|
+
def password_reset_email
|
|
8
|
+
Spree::AdminUserMailer.password_reset_email(
|
|
9
|
+
Spree::PreviewData.admin_user,
|
|
10
|
+
'preview-token',
|
|
11
|
+
Spree::PreviewData.store(locale)
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def confirmation_email
|
|
16
|
+
Spree::AdminUserMailer.confirmation_email(
|
|
17
|
+
Spree::PreviewData.admin_user,
|
|
18
|
+
'preview-token',
|
|
19
|
+
Spree::PreviewData.store(locale)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require_relative 'preview_data'
|
|
2
|
+
|
|
3
|
+
# Preview Spree export emails at /rails/mailers/spree/export
|
|
4
|
+
class Spree::ExportPreview < ActionMailer::Preview
|
|
5
|
+
include Spree::PreviewData::LocaleParam
|
|
6
|
+
|
|
7
|
+
def export_done
|
|
8
|
+
Spree::ExportMailer.export_done(export)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Reuse the most recent export, or build an in-memory example so the preview
|
|
14
|
+
# works on a database that has never run an export. When the preview toolbar
|
|
15
|
+
# requests a locale, always use the example so its store carries that locale.
|
|
16
|
+
# The example is never saved, so no `export.created` side effects fire.
|
|
17
|
+
def export
|
|
18
|
+
return example_export if locale.present?
|
|
19
|
+
|
|
20
|
+
Spree::Export.last || example_export
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def example_export
|
|
24
|
+
export = Spree::Exports::Products.new(
|
|
25
|
+
id: 0,
|
|
26
|
+
store: Spree::PreviewData.store(locale),
|
|
27
|
+
user: Spree::PreviewData.admin_user,
|
|
28
|
+
format: :csv
|
|
29
|
+
)
|
|
30
|
+
export.attachment.attach(
|
|
31
|
+
io: StringIO.new("id,name,price\n1,Example Product,19.99\n"),
|
|
32
|
+
filename: 'products-export.csv',
|
|
33
|
+
content_type: 'text/csv'
|
|
34
|
+
)
|
|
35
|
+
export
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require_relative 'preview_data'
|
|
2
|
+
|
|
3
|
+
# Preview Spree invitation emails at /rails/mailers/spree/invitation
|
|
4
|
+
class Spree::InvitationPreview < ActionMailer::Preview
|
|
5
|
+
include Spree::PreviewData::LocaleParam
|
|
6
|
+
|
|
7
|
+
def invitation_email
|
|
8
|
+
Spree::InvitationMailer.invitation_email(pending_invitation)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def invitation_accepted
|
|
12
|
+
Spree::InvitationMailer.invitation_accepted(accepted_invitation)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def pending_invitation
|
|
18
|
+
return example_invitation if locale.present?
|
|
19
|
+
|
|
20
|
+
Spree::Invitation.pending.last || example_invitation
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def accepted_invitation
|
|
24
|
+
return example_invitation(accepted: true) if locale.present?
|
|
25
|
+
|
|
26
|
+
Spree::Invitation.accepted.last || example_invitation(accepted: true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Build an in-memory invitation so the preview works on a database with no
|
|
30
|
+
# invitations. When the preview toolbar requests a locale, its store carries
|
|
31
|
+
# that locale. Never saved, so no records are created.
|
|
32
|
+
def example_invitation(accepted: false)
|
|
33
|
+
store = Spree::PreviewData.store(locale)
|
|
34
|
+
admin = Spree::PreviewData.admin_user
|
|
35
|
+
Spree::Invitation.new(
|
|
36
|
+
id: 0,
|
|
37
|
+
email: 'invitee@example.com',
|
|
38
|
+
resource: store,
|
|
39
|
+
inviter: admin,
|
|
40
|
+
invitee: accepted ? admin : nil,
|
|
41
|
+
role: Spree::Role.first || Spree::Role.new(name: 'admin'),
|
|
42
|
+
token: 'preview-token',
|
|
43
|
+
status: accepted ? 'accepted' : 'pending',
|
|
44
|
+
expires_at: 7.days.from_now
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Shared helpers for ActionMailer previews. Everything here is in-memory (never
|
|
2
|
+
# saved) so opening a mailer preview stays read-only.
|
|
3
|
+
module Spree
|
|
4
|
+
module PreviewData
|
|
5
|
+
# Include in ActionMailer::Preview subclasses to read the preview toolbar's
|
|
6
|
+
# locale dropdown (`?locale=`). The toolbar sends uppercase codes ("DE"),
|
|
7
|
+
# Spree locales are lowercase.
|
|
8
|
+
module LocaleParam
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def locale
|
|
12
|
+
@params[:locale]&.downcase
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# An admin user for previews that need `user.email` / `user.first_name`.
|
|
19
|
+
# Falls back to an unsaved instance when the database has no admin users.
|
|
20
|
+
def admin_user
|
|
21
|
+
Spree.admin_user_class.first ||
|
|
22
|
+
Spree.admin_user_class.new(email: 'admin@example.com', first_name: 'Alex', last_name: 'Doe')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A customer for previews of customer-facing account emails. Falls back to
|
|
26
|
+
# an unsaved instance when the database has no users.
|
|
27
|
+
def customer
|
|
28
|
+
Spree.user_class.first ||
|
|
29
|
+
Spree.user_class.new(email: 'customer@example.com').tap do |user|
|
|
30
|
+
user.first_name = 'Alex' if user.respond_to?(:first_name=)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The store previews render for. When the preview toolbar's locale dropdown
|
|
35
|
+
# sets `?locale=`, return an unsaved copy of the default store with that
|
|
36
|
+
# `default_locale`, so mailers that render in `store.default_locale` honour
|
|
37
|
+
# the toggle without mutating the real store.
|
|
38
|
+
#
|
|
39
|
+
# @param locale [String, Symbol, nil] the `?locale=` param
|
|
40
|
+
# @return [Spree::Store]
|
|
41
|
+
def store(locale = nil)
|
|
42
|
+
store = Spree::Store.default
|
|
43
|
+
return store if locale.blank? || store.nil?
|
|
44
|
+
|
|
45
|
+
dup = store.dup
|
|
46
|
+
dup.id = store.id # keep prefixed-id / URL helpers working
|
|
47
|
+
dup.default_locale = locale.to_s.downcase
|
|
48
|
+
dup.readonly!
|
|
49
|
+
dup
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative 'preview_data'
|
|
2
|
+
|
|
3
|
+
# Preview Spree report emails at /rails/mailers/spree/report
|
|
4
|
+
class Spree::ReportPreview < ActionMailer::Preview
|
|
5
|
+
include Spree::PreviewData::LocaleParam
|
|
6
|
+
|
|
7
|
+
def report_done
|
|
8
|
+
Spree::ReportMailer.report_done(report)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Reuse the most recent report, or build an in-memory example so the preview
|
|
14
|
+
# works on a database that has never run a report. When the preview toolbar
|
|
15
|
+
# requests a locale, always use the example so its store carries that locale.
|
|
16
|
+
# The example is never saved, so no `report.created` side effects fire.
|
|
17
|
+
def report
|
|
18
|
+
return example_report if locale.present?
|
|
19
|
+
|
|
20
|
+
Spree::Report.last || example_report
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def example_report
|
|
24
|
+
store = Spree::PreviewData.store(locale)
|
|
25
|
+
report = Spree::Reports::SalesTotal.new(
|
|
26
|
+
id: 0,
|
|
27
|
+
store: store,
|
|
28
|
+
user: Spree::PreviewData.admin_user,
|
|
29
|
+
date_from: 30.days.ago.to_date,
|
|
30
|
+
date_to: Date.current,
|
|
31
|
+
currency: store.default_currency
|
|
32
|
+
)
|
|
33
|
+
report.attachment.attach(
|
|
34
|
+
io: StringIO.new("date,total\n2026-01-01,1234.56\n"),
|
|
35
|
+
filename: 'sales-total.csv',
|
|
36
|
+
content_type: 'text/csv'
|
|
37
|
+
)
|
|
38
|
+
report
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative 'preview_data'
|
|
2
|
+
|
|
3
|
+
# Preview Spree webhook notification emails at /rails/mailers/spree/webhook
|
|
4
|
+
class Spree::WebhookPreview < ActionMailer::Preview
|
|
5
|
+
include Spree::PreviewData::LocaleParam
|
|
6
|
+
|
|
7
|
+
def endpoint_disabled
|
|
8
|
+
Spree::WebhookMailer.endpoint_disabled(webhook_endpoint)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Reuse the most recent endpoint, or build an in-memory disabled example so the
|
|
14
|
+
# preview works on a database with no webhook endpoints. When the preview
|
|
15
|
+
# toolbar requests a locale, always use the example so its store carries that
|
|
16
|
+
# locale. Never saved, so the admin webhook list stays clean.
|
|
17
|
+
def webhook_endpoint
|
|
18
|
+
return example_endpoint if locale.present?
|
|
19
|
+
|
|
20
|
+
Spree::WebhookEndpoint.last || example_endpoint
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def example_endpoint
|
|
24
|
+
Spree::WebhookEndpoint.new(
|
|
25
|
+
store: Spree::PreviewData.store(locale),
|
|
26
|
+
name: 'Example endpoint',
|
|
27
|
+
url: 'https://example.com/webhooks/spree',
|
|
28
|
+
active: false,
|
|
29
|
+
disabled_reason: 'Too many failed delivery attempts',
|
|
30
|
+
disabled_at: Time.current,
|
|
31
|
+
subscriptions: ['order.completed']
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/spree/core/version.rb
CHANGED
data/lib/spree/core.rb
CHANGED
|
@@ -165,7 +165,19 @@ module Spree
|
|
|
165
165
|
end
|
|
166
166
|
|
|
167
167
|
def self.use_translations?
|
|
168
|
-
Spree::Config.always_use_translations ||
|
|
168
|
+
Spree::Config.always_use_translations || Spree::Current.content_locale != I18n.locale.name
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Mobility +column_fallback+ option shared by every translatable model:
|
|
172
|
+
# read, write and query the base (untranslated) column when the target
|
|
173
|
+
# locale is the request's content locale — see Spree::Current#content_locale.
|
|
174
|
+
# Evaluated on every translated attribute read, so it must stay
|
|
175
|
+
# allocation-free and must not touch the database (Symbol#name returns the
|
|
176
|
+
# frozen interned string; Mobility always passes Symbol locales).
|
|
177
|
+
def self.mobility_column_fallback
|
|
178
|
+
return false if always_use_translations?
|
|
179
|
+
|
|
180
|
+
->(locale) { locale.name == Spree::Current.content_locale }
|
|
169
181
|
end
|
|
170
182
|
|
|
171
183
|
# Used to configure Spree.
|
|
@@ -321,6 +333,14 @@ module Spree
|
|
|
321
333
|
Rails.application.config.spree.reports = value
|
|
322
334
|
end
|
|
323
335
|
|
|
336
|
+
# Registry of the Getting Started onboarding tasks shown on the admin
|
|
337
|
+
# dashboard. See {Spree::SetupTasks} for the extension API.
|
|
338
|
+
#
|
|
339
|
+
# @return [Spree::SetupTasks]
|
|
340
|
+
def self.store_setup_tasks
|
|
341
|
+
@store_setup_tasks ||= Spree::SetupTasks.new
|
|
342
|
+
end
|
|
343
|
+
|
|
324
344
|
def self.translatable_resources
|
|
325
345
|
Rails.application.config.spree.translatable_resources
|
|
326
346
|
end
|
|
@@ -493,6 +513,7 @@ require 'spree/core/engine'
|
|
|
493
513
|
|
|
494
514
|
require 'spree/i18n'
|
|
495
515
|
require 'spree/localized_number'
|
|
516
|
+
require 'spree/translations'
|
|
496
517
|
require 'spree/money'
|
|
497
518
|
require 'spree/permitted_attributes'
|
|
498
519
|
require 'spree/service_module'
|
|
@@ -198,7 +198,7 @@ module Spree
|
|
|
198
198
|
]
|
|
199
199
|
|
|
200
200
|
@@promotion_attributes = [:name, :description, :starts_at, :expires_at, :code, :usage_limit, :path, :match_policy,
|
|
201
|
-
:advertise, :promotion_category_id, :code_prefix, :kind, :number_of_codes, :multi_codes
|
|
201
|
+
:advertise, :promotion_category_id, :code_prefix, :kind, :number_of_codes, :multi_codes]
|
|
202
202
|
|
|
203
203
|
@@promotion_rule_attributes = [:type, :preferred_match_policy, preferred_eligible_values: [], user_ids_to_add: [], product_ids_to_add: [], taxon_ids_to_add: []]
|
|
204
204
|
|
|
@@ -240,7 +240,8 @@ module Spree
|
|
|
240
240
|
|
|
241
241
|
@@shipping_category_attributes = [:name]
|
|
242
242
|
|
|
243
|
-
@@channel_attributes = [:name, :code, :active, :default, :preferred_order_routing_strategy
|
|
243
|
+
@@channel_attributes = [:name, :code, :active, :default, :preferred_order_routing_strategy,
|
|
244
|
+
:preferred_storefront_access, :preferred_guest_checkout]
|
|
244
245
|
|
|
245
246
|
@@shipping_method_attributes = [:name, :admin_name, :code, :tracking_url, :tax_category_id, :display_on,
|
|
246
247
|
:estimated_transit_business_days_min, :estimated_transit_business_days_max,
|
|
@@ -278,7 +279,7 @@ module Spree
|
|
|
278
279
|
:preferred_limit_digital_download_count, :preferred_limit_digital_download_days,
|
|
279
280
|
:preferred_digital_asset_link_expire_time,
|
|
280
281
|
:logo, :mailer_logo,
|
|
281
|
-
:preferred_guest_checkout]
|
|
282
|
+
:preferred_guest_checkout, :preferred_storefront_access]
|
|
282
283
|
|
|
283
284
|
@@store_credit_attributes = %i[amount currency category_id memo]
|
|
284
285
|
|
|
@@ -307,7 +308,7 @@ module Spree
|
|
|
307
308
|
|
|
308
309
|
@@variant_attributes = [
|
|
309
310
|
:name, :presentation, :cost_price, :discontinue_on, :lock_version,
|
|
310
|
-
:position, :track_inventory, :tax_category_id,
|
|
311
|
+
:position, :track_inventory, :preorderable, :preorder_ships_at, :backorder_limit, :tax_category_id,
|
|
311
312
|
:product_id, :product, :option_values_attributes, :price, :compare_at_price,
|
|
312
313
|
:weight, :height, :width, :depth, :sku, :barcode, :cost_currency,
|
|
313
314
|
:weight_unit, :dimensions_unit,
|
|
@@ -16,5 +16,9 @@ FactoryBot.define do
|
|
|
16
16
|
factory :customer_export, class: 'Spree::Exports::Customers', parent: :export do
|
|
17
17
|
type { 'Spree::Exports::Customers' }
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
factory :coupon_code_export, class: 'Spree::Exports::CouponCodes', parent: :export do
|
|
21
|
+
type { 'Spree::Exports::CouponCodes' }
|
|
22
|
+
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -2,15 +2,7 @@ FactoryBot.define do
|
|
|
2
2
|
factory :payment_method, class: Spree::PaymentMethod do
|
|
3
3
|
type { 'Spree::PaymentMethod' }
|
|
4
4
|
name { 'Test' }
|
|
5
|
-
|
|
6
|
-
before(:create) do |payment_method|
|
|
7
|
-
if payment_method.stores.empty?
|
|
8
|
-
default_store = Spree::Store.default.persisted? ? Spree::Store.default : nil
|
|
9
|
-
store = default_store || create(:store)
|
|
10
|
-
|
|
11
|
-
payment_method.stores << store
|
|
12
|
-
end
|
|
13
|
-
end
|
|
5
|
+
store { Spree::Store.find_by(default: true) || association(:store) }
|
|
14
6
|
end
|
|
15
7
|
|
|
16
8
|
factory :check_payment_method, parent: :payment_method, class: Spree::PaymentMethod::Check do
|