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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8058b9d81b22cf12abe2193023f52faa57b5d757b99e0359e4592415b00a434
|
|
4
|
+
data.tar.gz: 13d6653cb85ab1364290621d668ed291aecbff9442ce205803595fe316ed4ea7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ecbab7dc49344e7c228237344ff01b7addacb6309e0e70cc6a42510823265cfd97827c2a00fb9c990a90b6ace535529b2d60e11c0b45bce745d3185a8422851a
|
|
7
|
+
data.tar.gz: a2657923c327254ee18b0b222aff4b6b7c3075b26d4c9ff001f66d3b2ad5b35f7daf6d36b5d16161cfaf62613707722ae8dcf55549c2a5c811c3333638240bab
|
|
@@ -8,7 +8,7 @@ module Spree
|
|
|
8
8
|
countries = current_store.countries_available_for_checkout
|
|
9
9
|
|
|
10
10
|
countries.collect do |country|
|
|
11
|
-
country.name =
|
|
11
|
+
country.name = localized_country_name(country)
|
|
12
12
|
country
|
|
13
13
|
end.sort_by { |c| c.name.parameterize }
|
|
14
14
|
end
|
|
@@ -17,7 +17,7 @@ module Spree
|
|
|
17
17
|
countries = Spree::Country.all
|
|
18
18
|
|
|
19
19
|
countries.collect do |country|
|
|
20
|
-
country.name =
|
|
20
|
+
country.name = localized_country_name(country)
|
|
21
21
|
country
|
|
22
22
|
end.sort_by { |c| c.name.parameterize }
|
|
23
23
|
end
|
|
@@ -112,6 +112,16 @@ module Spree
|
|
|
112
112
|
inline_svg "payment_icons/#{payment_method}.svg", opts
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# @param country [Spree::Country]
|
|
116
|
+
# @return [String]
|
|
117
|
+
def localized_country_name(country)
|
|
118
|
+
Spree.t(
|
|
119
|
+
country.iso,
|
|
120
|
+
scope: 'country_names',
|
|
121
|
+
default: country.localized_name
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
115
125
|
private
|
|
116
126
|
|
|
117
127
|
I18N_PLURAL_MANY_COUNT = 2.1
|
|
@@ -36,10 +36,7 @@ module Spree
|
|
|
36
36
|
# @param currency [String] the currency ISO code
|
|
37
37
|
# @return [Array] the currency presentation and ISO code
|
|
38
38
|
def currency_presentation(currency)
|
|
39
|
-
|
|
40
|
-
label = "#{currency_money.name} (#{currency_money.iso_code})"
|
|
41
|
-
|
|
42
|
-
[label, currency]
|
|
39
|
+
[Spree::Currency.new(code: currency).label, currency]
|
|
43
40
|
end
|
|
44
41
|
|
|
45
42
|
def currency_money(currency = current_currency)
|
|
@@ -4,10 +4,27 @@ module Spree
|
|
|
4
4
|
supported_locales_for_all_stores.map { |locale| locale_presentation(locale) }
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
# Locales the admin UI is translated into, as [name, code] pairs for a
|
|
8
|
+
# select. Self-contained (reads Spree.available_locales directly) so it
|
|
9
|
+
# works outside the storefront/admin controller stack — e.g. the pre-auth
|
|
10
|
+
# login screen, which has no current store.
|
|
11
|
+
def admin_locales_options
|
|
12
|
+
Spree.available_locales.map { |locale| locale_presentation(locale) }
|
|
13
|
+
end
|
|
14
|
+
|
|
7
15
|
def available_locales_options
|
|
8
16
|
available_locales.map { |locale| locale_presentation(locale) }
|
|
9
17
|
end
|
|
10
18
|
|
|
19
|
+
# Locales a merchant may translate **content** into, as [name, code] pairs
|
|
20
|
+
# for a select. Backed by `Spree::Locales::ALL` (the canonical translation
|
|
21
|
+
# locale set) rather than the installed UI-translation bundles, so a
|
|
22
|
+
# market/store can adopt any supported locale instead of only ones already
|
|
23
|
+
# in use.
|
|
24
|
+
def translation_locales_options
|
|
25
|
+
Spree::Locales::ALL.map { |locale| locale_presentation(locale) }
|
|
26
|
+
end
|
|
27
|
+
|
|
11
28
|
def supported_locales_options
|
|
12
29
|
return if current_store.nil?
|
|
13
30
|
|
|
@@ -15,19 +32,7 @@ module Spree
|
|
|
15
32
|
end
|
|
16
33
|
|
|
17
34
|
def locale_presentation(locale)
|
|
18
|
-
|
|
19
|
-
[locale_full_name(locale), locale.to_s]
|
|
20
|
-
elsif defined?(SpreeI18n::Locale) && (language_name = SpreeI18n::Locale.local_language_name(locale))
|
|
21
|
-
["#{language_name} (#{locale})", locale.to_s]
|
|
22
|
-
elsif locale.to_s == 'en'
|
|
23
|
-
['English (US)', 'en']
|
|
24
|
-
else
|
|
25
|
-
[locale, locale.to_s]
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def locale_full_name(locale)
|
|
30
|
-
Spree.t('i18n.this_file_language', locale: locale)
|
|
35
|
+
[Spree::Locale.new(code: locale).label, locale.to_s]
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def should_render_locale_dropdown?
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module LocalizedNamesHelper
|
|
3
|
+
# @param code [String, Symbol]
|
|
4
|
+
# @return [String]
|
|
5
|
+
def locale_display_label(code)
|
|
6
|
+
return if code.blank?
|
|
7
|
+
|
|
8
|
+
content_tag(
|
|
9
|
+
:span,
|
|
10
|
+
Spree::Locale.new(code: code).label,
|
|
11
|
+
data: {
|
|
12
|
+
controller: 'display-name',
|
|
13
|
+
display_name_type_value: 'language',
|
|
14
|
+
display_name_code_value: code
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param countries [Enumerable<Spree::Country>]
|
|
20
|
+
# @return [Array<Array>]
|
|
21
|
+
def country_select_options(countries)
|
|
22
|
+
countries.map { |country| [country.option_label, country.id] }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Auth emails for admin users. Reached two ways: the legacy Rails admin's
|
|
3
|
+
# Devise flow (via Spree::AdminUserMethods::DeviseNotifications) and the Admin
|
|
4
|
+
# API's `admin_user.password_reset_requested` event (dashboard SPA).
|
|
5
|
+
class AdminUserMailer < BaseMailer
|
|
6
|
+
def password_reset_email(admin_user, token, store, redirect_url: nil)
|
|
7
|
+
@user = admin_user
|
|
8
|
+
@current_store = store
|
|
9
|
+
@reset_url = password_reset_url(token, store, redirect_url)
|
|
10
|
+
|
|
11
|
+
with_store_locale(store, preferred_locale(admin_user, store)) do
|
|
12
|
+
mail(
|
|
13
|
+
to: admin_user.email,
|
|
14
|
+
subject: "#{store.name} #{Spree.t('admin_user_mailer.password_reset_email.subject')}",
|
|
15
|
+
store_url: store.formatted_url
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def confirmation_email(admin_user, token, store)
|
|
21
|
+
@user = admin_user
|
|
22
|
+
@current_store = store
|
|
23
|
+
@confirmation_url = confirmation_url(token, store)
|
|
24
|
+
|
|
25
|
+
with_store_locale(store, preferred_locale(admin_user, store)) do
|
|
26
|
+
mail(
|
|
27
|
+
to: admin_user.email,
|
|
28
|
+
subject: "#{store.name} #{Spree.t('admin_user_mailer.confirmation_email.subject')}",
|
|
29
|
+
store_url: store.formatted_url
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Locale chain for admin auth emails:
|
|
37
|
+
# the admin's own dashboard language (persisted by the profile/language
|
|
38
|
+
# switcher via `PATCH /api/v3/admin/me`) → the store's configured admin
|
|
39
|
+
# locale → nil, which lets with_store_locale fall back to the store's
|
|
40
|
+
# default (storefront) locale. Blank or unavailable values fall through.
|
|
41
|
+
def preferred_locale(admin_user, store)
|
|
42
|
+
[admin_user.try(:selected_locale), store&.preferred_admin_locale]
|
|
43
|
+
.find { |locale| available_locale?(locale) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def available_locale?(locale)
|
|
47
|
+
locale.present? && I18n.available_locales.map(&:to_s).include?(locale.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The dashboard SPA passes a validated redirect URL (token appended as a
|
|
51
|
+
# query param); the legacy Devise flow links to its own edit-password route
|
|
52
|
+
# when the host app installed it.
|
|
53
|
+
def password_reset_url(token, store, redirect_url)
|
|
54
|
+
return append_token(redirect_url, token) if redirect_url.present?
|
|
55
|
+
|
|
56
|
+
if spree.respond_to?(:edit_admin_user_password_url)
|
|
57
|
+
spree.edit_admin_user_password_url(reset_password_token: token, host: store.formatted_url)
|
|
58
|
+
else
|
|
59
|
+
append_token(store.formatted_url, token)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def confirmation_url(token, store)
|
|
64
|
+
if spree.respond_to?(:admin_user_confirmation_url)
|
|
65
|
+
spree.admin_user_confirmation_url(confirmation_token: token, host: store.formatted_url)
|
|
66
|
+
else
|
|
67
|
+
append_token(store.formatted_url, token)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -2,18 +2,46 @@ module Spree
|
|
|
2
2
|
class BaseMailer < ActionMailer::Base
|
|
3
3
|
helper Spree::ImagesHelper
|
|
4
4
|
|
|
5
|
+
default from: -> { from_address }, reply_to: -> { reply_to_address }
|
|
6
|
+
|
|
5
7
|
def current_store
|
|
6
8
|
@current_store ||= @order&.store.presence || Spree::Store.current || Spree::Store.default
|
|
7
9
|
end
|
|
8
10
|
|
|
9
11
|
helper_method :current_store
|
|
10
12
|
|
|
13
|
+
# Render an email in the given locale, with the store's translation fallbacks
|
|
14
|
+
# active, and restore both afterwards. Controllers set these fallbacks per
|
|
15
|
+
# request via `set_fallback_locale`, but mailers run in background jobs where
|
|
16
|
+
# that never happens — so without this, translatable attributes (store name,
|
|
17
|
+
# product names, taxon names, …) return nil under a non-default locale and
|
|
18
|
+
# leave e.g. the footer blank. Setting the fallbacks here mirrors a request,
|
|
19
|
+
# so reads fall back to the store's default-locale value.
|
|
20
|
+
#
|
|
21
|
+
# @param store [Spree::Store]
|
|
22
|
+
# @param locale [String, Symbol, nil] defaults to the store's default locale
|
|
23
|
+
def with_store_locale(store, locale = nil, &block)
|
|
24
|
+
locale = locale.presence || store&.default_locale
|
|
25
|
+
return yield if locale.blank?
|
|
26
|
+
|
|
27
|
+
previous_fallbacks = Mobility.store_based_fallbacks
|
|
28
|
+
previously_active = @_store_locale_active
|
|
29
|
+
@_store_locale_active = true
|
|
30
|
+
begin
|
|
31
|
+
Spree::Locales::SetFallbackLocaleForStore.new.call(store: store) if store
|
|
32
|
+
I18n.with_locale(locale, &block)
|
|
33
|
+
ensure
|
|
34
|
+
@_store_locale_active = previously_active
|
|
35
|
+
Mobility.store_based_fallbacks = previous_fallbacks
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
11
39
|
def from_address
|
|
12
40
|
current_store.mail_from_address
|
|
13
41
|
end
|
|
14
42
|
|
|
15
43
|
def reply_to_address
|
|
16
|
-
current_store.mail_from_address
|
|
44
|
+
current_store.customer_support_email.presence || current_store.mail_from_address
|
|
17
45
|
end
|
|
18
46
|
|
|
19
47
|
def money(amount, currency = nil)
|
|
@@ -29,8 +57,50 @@ module Spree
|
|
|
29
57
|
|
|
30
58
|
def mail(headers = {}, &block)
|
|
31
59
|
ensure_default_action_mailer_url_host(headers[:store_url])
|
|
32
|
-
|
|
33
|
-
|
|
60
|
+
return unless Spree::Config[:send_core_emails]
|
|
61
|
+
|
|
62
|
+
if @_store_locale_active
|
|
63
|
+
super
|
|
64
|
+
else
|
|
65
|
+
# Subclasses that call `mail` without wrapping their action in
|
|
66
|
+
# `with_store_locale` (e.g. Devise mailers, extensions) still get the
|
|
67
|
+
# store default locale, as `mail` applied before Spree 5.6.
|
|
68
|
+
with_store_locale(current_store) { super }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @deprecated Each mailer action now wraps its body in {#with_store_locale},
|
|
73
|
+
# which also activates the store's translation fallbacks and restores the
|
|
74
|
+
# previous locale afterwards. This method mutates `I18n.locale` for the rest
|
|
75
|
+
# of the thread without restoring it. Will be removed in Spree 6.0.
|
|
76
|
+
def set_email_locale
|
|
77
|
+
Spree::Deprecation.warn(
|
|
78
|
+
'Spree::BaseMailer#set_email_locale is deprecated and will be removed in Spree 6.0. ' \
|
|
79
|
+
'Wrap the mailer action body in `with_store_locale(store, locale) { ... }` instead.'
|
|
80
|
+
)
|
|
81
|
+
locale = @order&.locale.presence || @order&.store&.default_locale || current_store&.default_locale
|
|
82
|
+
I18n.locale = locale if locale.present?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
protected
|
|
86
|
+
|
|
87
|
+
# The "<store> <subject> #<number>" subject line shared by customer-facing
|
|
88
|
+
# order emails, with the optional [RESEND] prefix.
|
|
89
|
+
def order_email_subject(store, subject, number, resend: false)
|
|
90
|
+
"#{resend ? "[#{Spree.t(:resend).upcase}] " : ''}#{store.name} #{subject} ##{number}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# URI-based merge preserves existing query params and fragments so the token
|
|
94
|
+
# doesn't get swallowed by a `#section` or clobber an existing `?source=`.
|
|
95
|
+
def append_token(url, token)
|
|
96
|
+
uri = URI.parse(url.to_s)
|
|
97
|
+
params = URI.decode_www_form(uri.query || '')
|
|
98
|
+
params << ['token', token.to_s]
|
|
99
|
+
uri.query = URI.encode_www_form(params)
|
|
100
|
+
uri.to_s
|
|
101
|
+
rescue URI::InvalidURIError
|
|
102
|
+
separator = url.include?('?') ? '&' : '?'
|
|
103
|
+
"#{url}#{separator}token=#{CGI.escape(token.to_s)}"
|
|
34
104
|
end
|
|
35
105
|
|
|
36
106
|
private
|
|
@@ -46,13 +116,5 @@ module Spree
|
|
|
46
116
|
ActionMailer::Base.default_url_options ||= {}
|
|
47
117
|
ActionMailer::Base.default_url_options[:host] = host_url
|
|
48
118
|
end
|
|
49
|
-
|
|
50
|
-
# Sets the I18n locale for the email.
|
|
51
|
-
# Prefers the order's locale (the language the customer used),
|
|
52
|
-
# falls back to the store's default locale.
|
|
53
|
-
def set_email_locale
|
|
54
|
-
locale = @order&.locale.presence || @order&.store&.default_locale || current_store&.default_locale
|
|
55
|
-
I18n.locale = locale if locale.present?
|
|
56
|
-
end
|
|
57
119
|
end
|
|
58
120
|
end
|
|
@@ -3,13 +3,13 @@ module Spree
|
|
|
3
3
|
def export_done(export)
|
|
4
4
|
@export = export
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
with_store_locale(@export.store) do
|
|
7
|
+
mail(
|
|
8
|
+
to: @export.user.email,
|
|
9
|
+
subject: Spree.t('export_mailer.export_done.subject', export_number: @export.number).to_s,
|
|
10
|
+
store_url: current_store.url
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def current_store
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class ImportMailer < Spree::BaseMailer
|
|
3
|
+
def import_done(import)
|
|
4
|
+
@import = import
|
|
5
|
+
|
|
6
|
+
with_store_locale(@import.store) do
|
|
7
|
+
mail(
|
|
8
|
+
to: @import.user.email,
|
|
9
|
+
subject: Spree.t('import_mailer.import_done.subject', import_number: @import.number).to_s,
|
|
10
|
+
store_url: current_store.url
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def current_store
|
|
16
|
+
@current_store ||= @import.store
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -3,22 +3,27 @@ module Spree
|
|
|
3
3
|
# invitation email, sending email to the invited to let them know they have been invited to join a store/account/vendor
|
|
4
4
|
def invitation_email(invitation)
|
|
5
5
|
@invitation = invitation
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
# The shared header/footer and from/reply-to addresses read
|
|
7
|
+
# current_store — without this, background delivery falls back to the
|
|
8
|
+
# default store's branding on multi-store installs.
|
|
9
|
+
@current_store = invitation.store
|
|
10
|
+
with_store_locale(invitation.store) do
|
|
11
|
+
mail(to: invitation.email,
|
|
12
|
+
subject: Spree.t('invitation_mailer.invitation_email.subject',
|
|
13
|
+
resource_name: invitation.resource&.name))
|
|
14
|
+
end
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
# sending email to the inviter to let them know the invitee has accepted the invitation
|
|
14
18
|
def invitation_accepted(invitation)
|
|
15
19
|
@invitation = invitation
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
@current_store = invitation.store
|
|
21
|
+
with_store_locale(invitation.store) do
|
|
22
|
+
mail(to: invitation.inviter.email,
|
|
23
|
+
subject: Spree.t('invitation_mailer.invitation_accepted.subject',
|
|
24
|
+
invitee_name: invitation.invitee&.name,
|
|
25
|
+
resource_name: invitation.resource&.name))
|
|
26
|
+
end
|
|
22
27
|
end
|
|
23
28
|
end
|
|
24
29
|
end
|
|
@@ -3,12 +3,12 @@ module Spree
|
|
|
3
3
|
def report_done(report)
|
|
4
4
|
@report = report
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
with_store_locale(@report.store) do
|
|
7
|
+
mail(
|
|
8
|
+
to: @report.user.email,
|
|
9
|
+
subject: Spree.t('report_mailer.report_done.subject', report_name: @report.human_name).to_s
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
private
|
|
@@ -6,12 +6,13 @@ module Spree
|
|
|
6
6
|
@endpoint = webhook_endpoint
|
|
7
7
|
@current_store = webhook_endpoint.store
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
with_store_locale(webhook_endpoint.store) do
|
|
10
|
+
mail(
|
|
11
|
+
to: @current_store.new_order_notifications_email.presence || @current_store.mail_from_address,
|
|
12
|
+
subject: Spree.t('webhook_mailer.endpoint_disabled.subject', endpoint_name: @endpoint.name || @endpoint.url),
|
|
13
|
+
store_url: @current_store.formatted_url
|
|
14
|
+
)
|
|
15
|
+
end
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -7,12 +7,34 @@ module Spree
|
|
|
7
7
|
include Spree::RansackableAttributes
|
|
8
8
|
|
|
9
9
|
included do
|
|
10
|
+
# Wins over Devise's implementation regardless of module include order, so
|
|
11
|
+
# host apps that add `devise ...` after this concern still send admin auth
|
|
12
|
+
# emails through Spree's mailer.
|
|
13
|
+
prepend Spree::AdminUserMethods::DeviseNotifications
|
|
14
|
+
|
|
10
15
|
has_prefix_id :admin
|
|
11
16
|
|
|
12
17
|
has_person_name
|
|
13
18
|
|
|
14
19
|
normalizes :email, :first_name, :last_name, with: ->(value) { value&.to_s&.squish&.presence }
|
|
15
20
|
|
|
21
|
+
# Token auto-invalidates when password changes (salt changes)
|
|
22
|
+
# Expiration is configurable via Spree::Config.admin_password_reset_expires_in (in minutes)
|
|
23
|
+
generates_token_for :password_reset, expires_in: Spree::Config.admin_password_reset_expires_in.minutes do
|
|
24
|
+
# `try` rather than `&.`: models without a password_salt method or
|
|
25
|
+
# column at all (modern Devise) must fall through to encrypted_password
|
|
26
|
+
# — a bare call would raise NameError before the fallback.
|
|
27
|
+
try(:password_salt)&.last(10) || encrypted_password&.last(10)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.find_by_password_reset_token(token)
|
|
31
|
+
find_by_token_for(:password_reset, token)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.find_by_password_reset_token!(token)
|
|
35
|
+
find_by_token_for!(:password_reset, token)
|
|
36
|
+
end
|
|
37
|
+
|
|
16
38
|
# Associations
|
|
17
39
|
has_many :identities, class_name: 'Spree::UserIdentity', as: :user, dependent: :destroy
|
|
18
40
|
has_many :canceled_orders, class_name: 'Spree::Order', foreign_key: :canceler_id
|
|
@@ -33,6 +55,8 @@ module Spree
|
|
|
33
55
|
# Attachments
|
|
34
56
|
has_one_attached :avatar, service: Spree.public_storage_service_name
|
|
35
57
|
|
|
58
|
+
validates :avatar, content_type: Rails.application.config.active_storage.web_image_content_types
|
|
59
|
+
|
|
36
60
|
#
|
|
37
61
|
# Attributes
|
|
38
62
|
#
|
|
@@ -78,5 +102,25 @@ module Spree
|
|
|
78
102
|
reports.destroy_all
|
|
79
103
|
exports.destroy_all
|
|
80
104
|
end
|
|
105
|
+
|
|
106
|
+
# Routes Devise's auth notifications through Spree's own mailer so admin
|
|
107
|
+
# password reset and account confirmation emails carry the store design and
|
|
108
|
+
# locale instead of Devise's built-in templates. Other notification kinds
|
|
109
|
+
# (e.g. unlock instructions) fall through to Devise.
|
|
110
|
+
module DeviseNotifications
|
|
111
|
+
def send_devise_notification(notification, *args)
|
|
112
|
+
token = args.first
|
|
113
|
+
store = Spree::Current.store || Spree::Store.default
|
|
114
|
+
|
|
115
|
+
case notification
|
|
116
|
+
when :reset_password_instructions
|
|
117
|
+
Spree::AdminUserMailer.password_reset_email(self, token, store).deliver_later
|
|
118
|
+
when :confirmation_instructions
|
|
119
|
+
Spree::AdminUserMailer.confirmation_email(self, token, store).deliver_later
|
|
120
|
+
else
|
|
121
|
+
super
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
81
125
|
end
|
|
82
126
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Backwards-compatible shim for resources migrated from a many-to-many store
|
|
3
|
+
# relationship to a single-store +belongs_to :store+ FK (Spree::Product in 5.5,
|
|
4
|
+
# Spree::Promotion and Spree::PaymentMethod in 5.6). Maps the historic
|
|
5
|
+
# +stores+/+store_ids+ accessors onto the singular +store+/+store_id+, warning
|
|
6
|
+
# on use. The +spree_multi_store+ extension defines +SpreeMultiStore+ to
|
|
7
|
+
# suppress the include and restore the real +has_many :stores+ association.
|
|
8
|
+
module LegacyMultiStoreSupport
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
# Legacy accessors for the many-to-many store relationship. These are deprecated
|
|
13
|
+
# and will be removed in Spree 6.0 once the models finish migrating to single-store ownership.
|
|
14
|
+
# @return [ActiveRecord::Relation<Spree::Store>] the single store wrapped in a relation, or an empty relation if no store is set
|
|
15
|
+
def stores
|
|
16
|
+
Spree::Deprecation.warn(
|
|
17
|
+
"#{self.class.base_class}#stores is deprecated. Please use #{self.class.base_class}#store instead. " \
|
|
18
|
+
'If you want to continue using multiple stores please install spree_multi_store gem'
|
|
19
|
+
)
|
|
20
|
+
store ? Spree::Store.where(id: store.id) : Spree::Store.none
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def store_ids
|
|
24
|
+
Spree::Deprecation.warn(
|
|
25
|
+
"#{self.class.base_class}#store_ids is deprecated. Please use #{self.class.base_class}#store_id instead. " \
|
|
26
|
+
'If you want to continue using multiple stores please install spree_multi_store gem'
|
|
27
|
+
)
|
|
28
|
+
store_id ? [store_id] : []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stores=(stores)
|
|
32
|
+
Spree::Deprecation.warn(
|
|
33
|
+
"#{self.class.base_class}#stores= is deprecated. Please use #{self.class.base_class}#store= instead. " \
|
|
34
|
+
'If you want to continue using multiple stores please install spree_multi_store gem'
|
|
35
|
+
)
|
|
36
|
+
self.store = Array(stores).compact.first
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def store_ids=(store_ids)
|
|
40
|
+
Spree::Deprecation.warn(
|
|
41
|
+
"#{self.class.base_class}#store_ids= is deprecated. Please use #{self.class.base_class}#store_id= instead. " \
|
|
42
|
+
'If you want to continue using multiple stores please install spree_multi_store gem'
|
|
43
|
+
)
|
|
44
|
+
self.store_id = Array(store_ids).compact_blank.first
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Shared by models whose single translatable field is the legacy
|
|
3
|
+
# +presentation+ column exposed publicly as +label+ (OptionType, OptionValue).
|
|
4
|
+
# Carries the Mobility setup, the translation-table normalizer, the
|
|
5
|
+
# locale-aware +label+/+label=+ bridge, and the +label → presentation+
|
|
6
|
+
# translation-matrix alias in one place so the two models don't restate it.
|
|
7
|
+
module PresentationTranslatable
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
include Spree::TranslatableResource
|
|
10
|
+
|
|
11
|
+
TRANSLATABLE_FIELDS = %i[presentation].freeze
|
|
12
|
+
|
|
13
|
+
included do
|
|
14
|
+
translates(*TRANSLATABLE_FIELDS, column_fallback: Spree.mobility_column_fallback)
|
|
15
|
+
|
|
16
|
+
self::Translation.class_eval do
|
|
17
|
+
normalizes :presentation, with: ->(value) { value&.to_s&.squish&.presence }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class_methods do
|
|
22
|
+
# The translation matrix uses +label+ (the public API name) for read/write
|
|
23
|
+
# symmetry; the +label+/+label=+ bridge below routes it to +presentation+.
|
|
24
|
+
def translatable_field_aliases
|
|
25
|
+
{ label: :presentation }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# alias_attribute bypasses Mobility's locale-aware reader/writer, so these
|
|
30
|
+
# explicit delegations are required to keep +label+ translation-aware.
|
|
31
|
+
def label(*args, **kwargs)
|
|
32
|
+
presentation(*args, **kwargs)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def label=(value)
|
|
36
|
+
self.presentation = value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -308,7 +308,11 @@ module Spree
|
|
|
308
308
|
# +.available+ without args returned future-dated products too.
|
|
309
309
|
# @param currency [String, nil] currency to require a price in; nil
|
|
310
310
|
# falls back to the default store's default currency.
|
|
311
|
-
|
|
311
|
+
# @param include_preorderable [Boolean] when true (storefront), products
|
|
312
|
+
# scheduled to publish later are still included if they have an active
|
|
313
|
+
# pre-order variant, so a "coming soon" launch can be pre-ordered before
|
|
314
|
+
# its publish date. Channel context only.
|
|
315
|
+
def self.available(available_on = nil, currency = nil, include_preorderable: false)
|
|
312
316
|
cutoff = available_on
|
|
313
317
|
cutoff = cutoff.beginning_of_minute if cutoff.respond_to?(:beginning_of_minute)
|
|
314
318
|
|
|
@@ -316,8 +320,22 @@ module Spree
|
|
|
316
320
|
|
|
317
321
|
if cutoff
|
|
318
322
|
scope = if (channel = Spree::Current.channel)
|
|
319
|
-
|
|
320
|
-
|
|
323
|
+
pub = Spree::ProductPublication.table_name
|
|
324
|
+
channel_scope = scope.for_channel(channel)
|
|
325
|
+
if include_preorderable
|
|
326
|
+
variants = Spree::Variant.quoted_table_name
|
|
327
|
+
products = Product.quoted_table_name
|
|
328
|
+
channel_scope.where(
|
|
329
|
+
"#{pub}.published_at IS NULL OR #{pub}.published_at <= :cutoff OR " \
|
|
330
|
+
"EXISTS (SELECT 1 FROM #{variants} WHERE #{variants}.product_id = #{products}.id " \
|
|
331
|
+
"AND #{variants}.deleted_at IS NULL AND #{variants}.preorderable = :preorderable " \
|
|
332
|
+
"AND (#{variants}.discontinue_on IS NULL OR #{variants}.discontinue_on > :cutoff) " \
|
|
333
|
+
"AND (#{variants}.preorder_ships_at IS NULL OR #{variants}.preorder_ships_at > :cutoff))",
|
|
334
|
+
cutoff: cutoff, preorderable: true
|
|
335
|
+
)
|
|
336
|
+
else
|
|
337
|
+
channel_scope.where(pub => { published_at: [nil, ..cutoff] })
|
|
338
|
+
end
|
|
321
339
|
else
|
|
322
340
|
scope.where(Product.table_name => { available_on: ..cutoff })
|
|
323
341
|
end
|
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
+
before_validation :ensure_store, unless: :store_id?
|
|
6
7
|
validate :ensure_store_association_is_not_changed
|
|
7
8
|
|
|
8
9
|
scope :for_store, ->(store) { where(store_id: store.id) }
|
|
@@ -10,6 +11,10 @@ module Spree
|
|
|
10
11
|
|
|
11
12
|
protected
|
|
12
13
|
|
|
14
|
+
def ensure_store
|
|
15
|
+
self.store ||= Spree::Current.store
|
|
16
|
+
end
|
|
17
|
+
|
|
13
18
|
def ensure_store_association_is_not_changed
|
|
14
19
|
if store_id_changed? && persisted?
|
|
15
20
|
errors.add(:store, Spree.t('errors.messages.store_association_can_not_be_changed'))
|