spree_core 5.5.2 → 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/jobs/spree/imports/process_group_job.rb +20 -9
- data/app/jobs/spree/imports/process_rows_job.rb +12 -4
- 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 +93 -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/imports/row_processors/base.rb +9 -0
- data/app/services/spree/imports/row_processors/product_variant.rb +37 -19
- 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 +6 -5
- 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
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Imports
|
|
3
3
|
class ProcessGroupJob < Spree::Imports::BaseJob
|
|
4
|
+
# Rows are loaded in slices so a large group never holds every ImportRow
|
|
5
|
+
# (and its raw CSV data) in memory for the whole job.
|
|
6
|
+
ROWS_BATCH_SIZE = 100
|
|
7
|
+
|
|
4
8
|
def perform(import_id, row_ids)
|
|
5
9
|
import = Spree::Import.find(import_id)
|
|
6
10
|
Spree::Current.store = import.store
|
|
@@ -8,16 +12,23 @@ module Spree
|
|
|
8
12
|
mappings = import.mappings.mapped.to_a
|
|
9
13
|
schema_fields = import.schema_fields
|
|
10
14
|
large = import.large_import?
|
|
11
|
-
# Skip rows already completed on a prior attempt so retries don't double-process them.
|
|
12
|
-
rows = import.rows.where(id: row_ids).pending_and_failed.order(:row_number)
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
row_ids.each_slice(ROWS_BATCH_SIZE) do |ids|
|
|
17
|
+
# Skip rows already completed on a prior attempt so retries don't double-process them.
|
|
18
|
+
rows = import.rows.where(id: ids).pending_and_failed.order(:row_number).to_a
|
|
19
|
+
# Share the already-loaded import across rows: each row's processor reads
|
|
20
|
+
# `row.import` (store, ability, lookup cache), and without this every row
|
|
21
|
+
# lazily loads its own Import instance and rebuilds all of that per row.
|
|
22
|
+
rows.each { |row| row.association(:import).target = import }
|
|
23
|
+
|
|
24
|
+
if large
|
|
25
|
+
Spree::Events.disable do
|
|
26
|
+
rows.each { |row| row.bulk_process!(mappings: mappings, schema_fields: schema_fields) }
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
rows.each do |row|
|
|
30
|
+
row.process!(mappings: mappings, schema_fields: schema_fields)
|
|
31
|
+
end
|
|
21
32
|
end
|
|
22
33
|
end
|
|
23
34
|
|
|
@@ -2,6 +2,7 @@ module Spree
|
|
|
2
2
|
module Imports
|
|
3
3
|
class ProcessRowsJob < Spree::Imports::BaseJob
|
|
4
4
|
BATCH_SIZE = 100
|
|
5
|
+
UNGROUPED_KEY = '__ungrouped__'.freeze
|
|
5
6
|
|
|
6
7
|
def perform(import_id)
|
|
7
8
|
import = Spree::Import.find(import_id)
|
|
@@ -27,20 +28,27 @@ module Spree
|
|
|
27
28
|
|
|
28
29
|
import.rows.pending_and_failed.order(:row_number).pluck(:id, :data).each do |id, data|
|
|
29
30
|
parsed = JSON.parse(data)
|
|
30
|
-
key = parsed[file_column].to_s.strip.downcase.presence ||
|
|
31
|
+
key = parsed[file_column].to_s.strip.downcase.presence || UNGROUPED_KEY
|
|
31
32
|
groups[key] << id
|
|
32
33
|
rescue JSON::ParserError
|
|
33
|
-
groups[
|
|
34
|
+
groups[UNGROUPED_KEY] << id
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
# Rows without a group value don't depend on each other, so they don't have
|
|
38
|
+
# to share a single job — split them into bounded batches. Real groups stay
|
|
39
|
+
# intact: their rows must run sequentially (product row before variant rows).
|
|
40
|
+
ungrouped = groups.delete(UNGROUPED_KEY)
|
|
41
|
+
batches = groups.values
|
|
42
|
+
ungrouped&.each_slice(BATCH_SIZE) { |row_ids| batches << row_ids }
|
|
43
|
+
|
|
36
44
|
# Set count before enqueuing so workers can't complete prematurely
|
|
37
45
|
import.update_columns(
|
|
38
|
-
processing_groups_count:
|
|
46
|
+
processing_groups_count: batches.size,
|
|
39
47
|
completed_groups_count: 0,
|
|
40
48
|
updated_at: Time.current
|
|
41
49
|
)
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
batches.each { |row_ids| ProcessGroupJob.perform_later(import.id, row_ids) }
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
def dispatch_batched(import)
|
|
@@ -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
|