spree_cm_commissioner 2.12.0 → 2.12.2
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/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/users_controller_decorator.rb +9 -0
- data/app/controllers/spree/api/v2/storefront/queue_draft_orders_controller.rb +5 -3
- data/app/jobs/spree_cm_commissioner/queue_draft_orders/process_job.rb +7 -3
- data/app/models/concerns/spree_cm_commissioner/user_identity.rb +21 -2
- data/app/models/spree_cm_commissioner/product_decorator.rb +4 -12
- data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +5 -0
- data/app/models/spree_cm_commissioner/user_decorator.rb +1 -1
- data/app/overrides/spree/admin/users/_form/user_add_on.html.erb.deface +2 -2
- data/app/overrides/spree/admin/users/index/body.html.erb.deface +3 -0
- data/app/overrides/spree/admin/users/index/headers.html.erb.deface +3 -0
- data/app/overrides/spree/admin/users/index/ransack.html.erb.deface +6 -0
- data/app/services/spree_cm_commissioner/users/dedupe_logins.rb +48 -0
- data/app/services/spree_cm_commissioner/users/qr_data/verify.rb +1 -1
- data/app/services/spree_cm_commissioner/users/username/generate.rb +1 -1
- data/app/views/spree/admin/user_security/show.html.erb +46 -5
- data/config/locales/en.yml +5 -4
- data/config/locales/km.yml +1 -0
- data/db/migrate/20260828080349_add_normalized_login_to_spree_users.rb +39 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +4 -3
- data/app/overrides/spree/admin/users/edit/account_status.html.erb.deface +0 -42
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 518b50594502cdd95eb7710824cb6f8a104974d44e9602f2babb3235bcc17559
|
|
4
|
+
data.tar.gz: 4906dc6a8f57707ebf5fcb9712b6a074bbef1af0437e14923f61f2ff6606c32e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67a54630258eda9e5bf72e26ce22177d6b1ce8da399d71434319403f1df8d4a71469ec115f7015b7b9064544688fe833f3230329e456e09eeb88b4f8a488212c
|
|
7
|
+
data.tar.gz: 6aa524ac8653f151fd629c4bef58258212ee3eb67f5f1bba328f7e3e964c4efb9f67ec9692c359c80f4b2d8abac088541b7b692503d02bb8afecedc21e550072
|
data/Gemfile.lock
CHANGED
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
module UsersControllerDecorator
|
|
4
4
|
def self.prepended(base)
|
|
5
5
|
base.before_action :build_profile, only: %i[create update]
|
|
6
|
+
base.before_action :downcase_normalized_login_search_param, only: :index
|
|
6
7
|
end
|
|
7
8
|
|
|
8
9
|
# override
|
|
@@ -81,6 +82,14 @@ module Spree
|
|
|
81
82
|
|
|
82
83
|
@user.build_profile(attachment: permitted_resource_params.delete(:profile))
|
|
83
84
|
end
|
|
85
|
+
|
|
86
|
+
# normalized_login is stored lowercase, so a mixed-case search term would never
|
|
87
|
+
# match under a plain LIKE/ILIKE — downcase it before Ransack builds the query.
|
|
88
|
+
def downcase_normalized_login_search_param
|
|
89
|
+
return unless params[:q]&.key?(:normalized_login_cont)
|
|
90
|
+
|
|
91
|
+
params[:q][:normalized_login_cont] = params[:q][:normalized_login_cont].to_s.downcase
|
|
92
|
+
end
|
|
84
93
|
end
|
|
85
94
|
end
|
|
86
95
|
end
|
|
@@ -3,8 +3,8 @@ module Spree
|
|
|
3
3
|
module V2
|
|
4
4
|
module Storefront
|
|
5
5
|
class QueueDraftOrdersController < ::Spree::Api::V2::ResourceController
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Sets Current.load_test_bypass from the X-Request-Verify secret. The hold is acquired later
|
|
7
|
+
# in ProcessJob, so `create` passes this flag through options for the job to restore there.
|
|
8
8
|
include SpreeCmCommissioner::LoadTestBypassable
|
|
9
9
|
|
|
10
10
|
# Accepts a draft order request without building it: writes a queued Firestore status
|
|
@@ -27,7 +27,9 @@ module Spree
|
|
|
27
27
|
options: {
|
|
28
28
|
device_id: device_id,
|
|
29
29
|
last_ip_address: request.remote_ip,
|
|
30
|
-
private_sale_token: params[:st]
|
|
30
|
+
private_sale_token: params[:st],
|
|
31
|
+
# Carried into the job so it can bypass hold limits for a load test (see ProcessJob).
|
|
32
|
+
load_test_bypass: SpreeCmCommissioner::Current.load_test_bypass
|
|
31
33
|
}
|
|
32
34
|
},
|
|
33
35
|
locale: I18n.locale.to_s
|
|
@@ -13,9 +13,13 @@ module SpreeCmCommissioner
|
|
|
13
13
|
|
|
14
14
|
def perform(document_path, create_type, creation_params, locale = nil)
|
|
15
15
|
I18n.with_locale(locale.presence || I18n.default_locale) do
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
)
|
|
16
|
+
# Current doesn't cross into the job, so restore the load-test-bypass flag here — otherwise
|
|
17
|
+
# ValidateLimits would reject a trusted load test.
|
|
18
|
+
SpreeCmCommissioner::Current.set(load_test_bypass: creation_params.dig(:options, :load_test_bypass) || false) do
|
|
19
|
+
SpreeCmCommissioner::QueueDraftOrders::Process.call(
|
|
20
|
+
document_path: document_path, create_type: create_type, creation_params: creation_params
|
|
21
|
+
)
|
|
22
|
+
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
end
|
|
@@ -21,8 +21,10 @@ module SpreeCmCommissioner
|
|
|
21
21
|
validates :phone_number, uniqueness: { scope: :tenant_id, allow_blank: true }
|
|
22
22
|
validates :phone_number, format: { with: /^\d{8,12}$/, :multiline => true, allow_blank: true }
|
|
23
23
|
|
|
24
|
+
before_validation :normalize_login
|
|
25
|
+
|
|
24
26
|
validates :login, presence: true, if: -> (u) { u.require_login_identity_all_blank_for(:login) && !u.registering_via_social_network }
|
|
25
|
-
|
|
27
|
+
validate :validate_normalized_login_uniqueness
|
|
26
28
|
|
|
27
29
|
_validators.reject! { |key, _| key == :email }
|
|
28
30
|
_validate_callbacks.each { |c| c.filter.attributes.delete(:email) if c.filter.respond_to?(:attributes) }
|
|
@@ -46,7 +48,7 @@ module SpreeCmCommissioner
|
|
|
46
48
|
elsif login =~ URI::MailTo::EMAIL_REGEXP
|
|
47
49
|
scope.find_by(email: login)
|
|
48
50
|
else
|
|
49
|
-
scope.find_by(
|
|
51
|
+
scope.find_by(normalized_login: login)
|
|
50
52
|
end
|
|
51
53
|
end
|
|
52
54
|
end
|
|
@@ -58,6 +60,23 @@ module SpreeCmCommissioner
|
|
|
58
60
|
|
|
59
61
|
protected
|
|
60
62
|
|
|
63
|
+
# Keeps a plain, indexable lowercase copy of `login` so uniqueness and lookups can
|
|
64
|
+
# use a straight equality match instead of LOWER(login) everywhere, without ever
|
|
65
|
+
# rewriting the user's own `login` value (shown to them as a real handle elsewhere).
|
|
66
|
+
def normalize_login
|
|
67
|
+
self.normalized_login = login.present? ? login.downcase : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Attaches the error to :login (the field shown to and edited by the user), not
|
|
71
|
+
# :normalized_login, so existing forms/specs keyed on `errors[:login]` keep working.
|
|
72
|
+
def validate_normalized_login_uniqueness
|
|
73
|
+
return if normalized_login.blank?
|
|
74
|
+
|
|
75
|
+
scope = self.class.where(tenant_id: tenant_id, normalized_login: normalized_login)
|
|
76
|
+
scope = scope.where.not(id: id) if persisted?
|
|
77
|
+
errors.add(:login, :taken) if scope.exists?
|
|
78
|
+
end
|
|
79
|
+
|
|
61
80
|
def require_login_identity_all_blank_for(field)
|
|
62
81
|
required_fields = %i[phone_number email user_identity_providers login].reject { |item| item == field }
|
|
63
82
|
required_fields.each do |required_field|
|
|
@@ -195,18 +195,10 @@ module SpreeCmCommissioner
|
|
|
195
195
|
end
|
|
196
196
|
end
|
|
197
197
|
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
# one place — mirrors VariantDecorator#discontinued?'s window check.
|
|
203
|
-
def available?
|
|
204
|
-
return false unless active?
|
|
205
|
-
return false if deleted?
|
|
206
|
-
return false if discontinued?
|
|
207
|
-
return false if available_on.present? && available_on > Time.current
|
|
208
|
-
|
|
209
|
-
true
|
|
198
|
+
# Not yet on sale. Kept separate from available? so a scheduled product's real stock still
|
|
199
|
+
# shows via in_stock? — Stock::AvailabilityValidatorDecorator checks this to block add-to-cart.
|
|
200
|
+
def scheduled?
|
|
201
|
+
available_on.present? && available_on > Time.current
|
|
210
202
|
end
|
|
211
203
|
|
|
212
204
|
private
|
|
@@ -15,6 +15,11 @@ module SpreeCmCommissioner
|
|
|
15
15
|
# skipping it here means we never even build a checker for them.
|
|
16
16
|
return if line_item.order&.ticket_transfer?
|
|
17
17
|
|
|
18
|
+
# scheduled? isn't checked anywhere else (product.available?/variant.discontinued? are, via
|
|
19
|
+
# can_supply? below) — a scheduled product must still read in_stock? true, so its sale
|
|
20
|
+
# window is enforced here instead.
|
|
21
|
+
return add_inactive_product_error(line_item) if !line_item.product.available? || line_item.product.scheduled?
|
|
22
|
+
|
|
18
23
|
checker = SpreeCmCommissioner::Stock::LineItemAvailabilityChecker.new(line_item).checker
|
|
19
24
|
return if checker.can_supply?(quantity)
|
|
20
25
|
|
|
@@ -94,7 +94,7 @@ module SpreeCmCommissioner
|
|
|
94
94
|
base.before_save :update_otp_enabled
|
|
95
95
|
|
|
96
96
|
base.attr_accessor :assigned_roles
|
|
97
|
-
base.whitelisted_ransackable_attributes = %w[email first_name last_name gender phone_number blocked_at]
|
|
97
|
+
base.whitelisted_ransackable_attributes = %w[email first_name last_name gender phone_number blocked_at normalized_login]
|
|
98
98
|
base.devise :two_factor_authenticatable
|
|
99
99
|
def base.end_users
|
|
100
100
|
joins('LEFT JOIN spree_vendor_users ON spree_users.id = spree_vendor_users.user_id').where(spree_vendor_users: { user_id: nil })
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
service_types = SpreeCmCommissioner::ServiceType.translated_service_types.map { |key, label| [label, key.to_s] }
|
|
5
5
|
%>
|
|
6
6
|
|
|
7
|
-
<div data-hook="admin_user_form_user_section" class="p-3 mt-4 rounded
|
|
7
|
+
<div data-hook="admin_user_form_user_section" class="p-3 mt-4 border rounded form-group bg-light">
|
|
8
8
|
<strong class="mb-3 d-block"><%= Spree.t(:add_on) %></strong>
|
|
9
9
|
|
|
10
10
|
<%= f.field_container :primary_service_type do %>
|
|
11
11
|
<%= f.label :primary_service_type, Spree.t(:primary_service_type) %>
|
|
12
12
|
<%= f.select :primary_service_type,
|
|
13
|
-
options_for_select(service_types, f.object.primary_service_type),
|
|
13
|
+
options_for_select(service_types, f.object.primary_service_type || service_types.first&.last),
|
|
14
14
|
{ include_blank: Spree.t(:select_service_type) },
|
|
15
15
|
class: 'form-control select2-clear' %>
|
|
16
16
|
<%= f.error_message_on :primary_service_type %>
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
<th>
|
|
5
5
|
<%= sort_link @search, :email, Spree.t(:email), {}, {title: 'users_email_title'} %>
|
|
6
6
|
</th>
|
|
7
|
+
<th>
|
|
8
|
+
<%= sort_link @search, :login, Spree.t(:login), {}, {title: 'login_title'} %>
|
|
9
|
+
</th>
|
|
7
10
|
<th>
|
|
8
11
|
<%= sort_link @search, :first_name, Spree.t(:first_name), {}, {title: 'first_name_title'} %>
|
|
9
12
|
</th>
|
|
@@ -39,6 +39,12 @@
|
|
|
39
39
|
<%= f.telephone_field :phone_number_cont, class: 'form-control js-filterable', autofocus: true, placeholder: 'xxx-xxx-xxxx'%>
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
42
|
+
<div class="col-12 col-lg-6">
|
|
43
|
+
<div class="form-group">
|
|
44
|
+
<%= f.label :normalized_login_cont, Spree.t(:login) %>
|
|
45
|
+
<%= f.text_field :normalized_login_cont, class: 'form-control js-filterable' %>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
42
48
|
<div class="col-12 col-lg-6">
|
|
43
49
|
<div class="form-group">
|
|
44
50
|
<%# blocked_at_null: false = blocked, true = active. Ransack drops the blank option, so an
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Users
|
|
3
|
+
class DedupeLogins
|
|
4
|
+
prepend Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
def call
|
|
7
|
+
renamed_count = duplicate_groups.sum { |tenant_id, normalized_login| rename_duplicates(tenant_id, normalized_login) }
|
|
8
|
+
success(renamed_count: renamed_count)
|
|
9
|
+
rescue StandardError => e
|
|
10
|
+
failure(nil, e.message)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# login uniqueness is scoped to tenant_id, so duplicates must be grouped the same way
|
|
16
|
+
# or users in different tenants would be falsely flagged as colliding.
|
|
17
|
+
def duplicate_groups
|
|
18
|
+
Spree::User.where.not(normalized_login: [nil, ''])
|
|
19
|
+
.group(:tenant_id, :normalized_login)
|
|
20
|
+
.having('count(*) > 1')
|
|
21
|
+
.pluck(:tenant_id, :normalized_login)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def rename_duplicates(tenant_id, normalized_login)
|
|
25
|
+
# keep the oldest account's login untouched; rename the rest
|
|
26
|
+
duplicates = Spree::User.where(tenant_id: tenant_id, normalized_login: normalized_login)
|
|
27
|
+
.order(:created_at)
|
|
28
|
+
.offset(1)
|
|
29
|
+
.to_a
|
|
30
|
+
|
|
31
|
+
duplicates.each { |user| rename_user(user, tenant_id) }
|
|
32
|
+
duplicates.size
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def rename_user(user, tenant_id)
|
|
36
|
+
old_login = user.login
|
|
37
|
+
new_login = SpreeCmCommissioner::Users::Username::Generate.call(user: user)
|
|
38
|
+
# update_column skips callbacks, so normalized_login must be set explicitly here too.
|
|
39
|
+
user.update_columns(login: new_login, normalized_login: new_login.downcase) # rubocop:disable Rails/SkipsModelValidations
|
|
40
|
+
|
|
41
|
+
Rails.logger.info do
|
|
42
|
+
"[SpreeCmCommissioner::Users::DedupeLogins] user_id=#{user.id} tenant_id=#{tenant_id.inspect} " \
|
|
43
|
+
"login=#{old_login.inspect} -> #{new_login.inspect}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -18,7 +18,7 @@ module SpreeCmCommissioner
|
|
|
18
18
|
login = payload['login']
|
|
19
19
|
|
|
20
20
|
# we don't reveal whether user exists or not in error messages for security reasons.
|
|
21
|
-
user = Spree::User.find_by(
|
|
21
|
+
user = Spree::User.find_by(normalized_login: login.to_s.downcase)
|
|
22
22
|
raise JWT::DecodeError, 'QR code is invalid' if user.nil?
|
|
23
23
|
|
|
24
24
|
JWT.decode(qr_data, user.secure_token, true, { algorithm: 'HS256' })
|
|
@@ -27,7 +27,7 @@ module SpreeCmCommissioner
|
|
|
27
27
|
def call
|
|
28
28
|
@max_attempts.times do
|
|
29
29
|
username = generate_clean_username
|
|
30
|
-
return username unless Spree::User.exists?(
|
|
30
|
+
return username unless Spree::User.exists?(normalized_login: username.downcase)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Last resort: use extended number range
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
<%= render partial: 'spree/admin/users/tabs', locals: { current: :security } %>
|
|
2
2
|
|
|
3
|
-
<div class="
|
|
3
|
+
<div class="shadow-sm card">
|
|
4
4
|
<div class="card-header bg-light">
|
|
5
|
-
<h5 class="card-title
|
|
5
|
+
<h5 class="mb-0 card-title h6">
|
|
6
6
|
<%= Spree.t(:two_factor_authentication) %>
|
|
7
7
|
</h5>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
|
-
<div class="card-body
|
|
10
|
+
<div class="text-center card-body">
|
|
11
11
|
<% if @user.otp_secret.present? && @user.otp_required_for_login == true %>
|
|
12
|
-
<p class="text-muted
|
|
12
|
+
<p class="mb-3 text-muted">
|
|
13
13
|
Two-factor authentication is currently active for this user.
|
|
14
14
|
</p>
|
|
15
15
|
<%= button_to disable_authenticator_admin_user_security_path, method: :delete, class: "btn btn-danger btn-md",
|
|
@@ -17,9 +17,50 @@
|
|
|
17
17
|
Disable 2FA
|
|
18
18
|
<% end %>
|
|
19
19
|
<% else %>
|
|
20
|
-
<p class="text-muted
|
|
20
|
+
<p class="mb-0 text-muted">
|
|
21
21
|
Two-factor authentication is <strong>not enabled</strong> for this user.
|
|
22
22
|
</p>
|
|
23
23
|
<% end %>
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|
|
26
|
+
|
|
27
|
+
<div data-hook="admin_user_account_status" class="mt-3 shadow-sm card">
|
|
28
|
+
<div class="card-header bg-light">
|
|
29
|
+
<h5 class="mb-0 card-title h6">
|
|
30
|
+
<%= I18n.t('spree_user.account_status') %>
|
|
31
|
+
</h5>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="card-body">
|
|
35
|
+
<% if @user.blocked? %>
|
|
36
|
+
<p>
|
|
37
|
+
<span class="badge badge-danger text-uppercase"><%= I18n.t('spree_user.blocked') %></span>
|
|
38
|
+
<span class="ml-2 text-muted"><%= I18n.t('spree_user.blocked_at', time: l(@user.blocked_at, format: :long)) %></span>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<% if @user.blocked_reason.present? %>
|
|
42
|
+
<p class="text-muted"><%= @user.blocked_reason %></p>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
<%= form_tag spree.unblock_admin_user_path(@user), method: :put do %>
|
|
46
|
+
<%= button_tag I18n.t('spree_user.unblock_user'),
|
|
47
|
+
type: 'submit',
|
|
48
|
+
class: 'btn btn-primary',
|
|
49
|
+
data: { confirm: I18n.t('spree_user.unblock_confirmation') } %>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% else %>
|
|
52
|
+
<%= form_tag spree.block_admin_user_path(@user), method: :put do %>
|
|
53
|
+
<div class="form-group">
|
|
54
|
+
<%= label_tag :blocked_reason, I18n.t('spree_user.blocked_reason') %>
|
|
55
|
+
<%= text_field_tag :blocked_reason, nil, class: 'form-control' %>
|
|
56
|
+
<small class="form-text text-muted"><%= I18n.t('spree_user.blocked_reason_hint') %></small>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<%= button_tag I18n.t('spree_user.block_user'),
|
|
60
|
+
type: 'submit',
|
|
61
|
+
class: 'btn btn-danger',
|
|
62
|
+
data: { confirm: I18n.t('spree_user.block_confirmation') } %>
|
|
63
|
+
<% end %>
|
|
64
|
+
<% end %>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
data/config/locales/en.yml
CHANGED
|
@@ -497,6 +497,7 @@ en:
|
|
|
497
497
|
# Overrides Spree core's "selected of %{item} is not available." Must stay under
|
|
498
498
|
# `spree:` -- Spree::Stock::AvailabilityValidator looks it up as Spree.t(:selected_quantity_not_available).
|
|
499
499
|
selected_quantity_not_available: "Sorry, not enough %{item} left. Please choose fewer or try again in a few minutes."
|
|
500
|
+
only_active_products_can_be_added_to_cart: "Sorry, this product isn't available for purchase right now."
|
|
500
501
|
auto_apply: "Auto apply"
|
|
501
502
|
auto_apply_info: "Path & code will be removed"
|
|
502
503
|
transit:
|
|
@@ -769,11 +770,11 @@ en:
|
|
|
769
770
|
attributes:
|
|
770
771
|
base:
|
|
771
772
|
last_contestant_in_parent_session: 'cannot remove the last contestant from "%{session_name}" because child sessions depend on it'
|
|
772
|
-
advanced_from_session:
|
|
773
|
-
pre_vote_session:
|
|
774
|
-
has_advanced_to_session:
|
|
773
|
+
advanced_from_session: "was advanced from a previous session and cannot be removed directly. Clear the advancement in the source session instead."
|
|
774
|
+
pre_vote_session: "cannot be removed from a pre-vote session."
|
|
775
|
+
has_advanced_to_session: "has been assigned to advance to another session. Clear the advancement first before removing this contestant."
|
|
775
776
|
advanced_to_id:
|
|
776
|
-
same_as_advanced_from:
|
|
777
|
+
same_as_advanced_from: "cannot be the same as the contestant it was advanced from"
|
|
777
778
|
spree_cm_commissioner/voting_credit:
|
|
778
779
|
attributes:
|
|
779
780
|
free_votes_amount:
|
data/config/locales/km.yml
CHANGED
|
@@ -403,6 +403,7 @@ km:
|
|
|
403
403
|
upsupported_payment: "មិនស្គាល់ការបង់ប្រាក់"
|
|
404
404
|
selected_item_not_available_on_date: "Selected item not available on %{date}"
|
|
405
405
|
selected_quantity_not_available: "សូមអភ័យទោស %{item} នៅសល់មិនគ្រប់គ្រាន់។ សូមរើសចំនួនតិច ឬព្យាយាមម្តងទៀត។"
|
|
406
|
+
only_active_products_can_be_added_to_cart: "សូមអភ័យទោស ទំនិញនេះមិនទាន់អាចទិញបានទេនៅពេលនេះ។"
|
|
406
407
|
auto_apply: "ដាក់ប្រើដោយស្វ័យប្រវត្តិ"
|
|
407
408
|
auto_apply_info: "Path & code will be removed"
|
|
408
409
|
device_tokens: "Device Tokens"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class AddNormalizedLoginToSpreeUsers < ActiveRecord::Migration[7.0]
|
|
2
|
+
def up
|
|
3
|
+
add_column :spree_users, :normalized_login, :string unless column_exists?(:spree_users, :normalized_login)
|
|
4
|
+
|
|
5
|
+
execute <<~SQL.squish
|
|
6
|
+
UPDATE spree_users SET normalized_login = LOWER(login)
|
|
7
|
+
WHERE login IS NOT NULL AND login != ''
|
|
8
|
+
SQL
|
|
9
|
+
|
|
10
|
+
# Case-insensitive login uniqueness has only ever been enforced by the Rails
|
|
11
|
+
# validation, not the DB, so case-variant duplicates already exist. Clean them
|
|
12
|
+
# up first or the unique index below fails to create.
|
|
13
|
+
result = SpreeCmCommissioner::Users::DedupeLogins.call
|
|
14
|
+
raise "Failed to dedupe user logins before indexing: #{result.error}" if result.failure?
|
|
15
|
+
|
|
16
|
+
unless index_exists?(:spree_users, %i[tenant_id normalized_login], name: 'index_spree_users_on_tenant_id_and_normalized_login')
|
|
17
|
+
add_index :spree_users, %i[tenant_id normalized_login],
|
|
18
|
+
unique: true,
|
|
19
|
+
where: 'tenant_id IS NOT NULL AND normalized_login IS NOT NULL',
|
|
20
|
+
name: 'index_spree_users_on_tenant_id_and_normalized_login'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# tenant_id IS NULL rows aren't covered by the composite index above (NULL never
|
|
24
|
+
# equals NULL in a unique index), but the validation's `scope: :tenant_id` treats
|
|
25
|
+
# all null-tenant users as one group, so they need their own partial index to match.
|
|
26
|
+
return if index_exists?(:spree_users, :normalized_login, name: 'index_spree_users_on_normalized_login_when_tenant_id_null')
|
|
27
|
+
|
|
28
|
+
add_index :spree_users, :normalized_login,
|
|
29
|
+
unique: true,
|
|
30
|
+
where: 'tenant_id IS NULL AND normalized_login IS NOT NULL',
|
|
31
|
+
name: 'index_spree_users_on_normalized_login_when_tenant_id_null'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def down
|
|
35
|
+
remove_index :spree_users, name: 'index_spree_users_on_tenant_id_and_normalized_login', if_exists: true
|
|
36
|
+
remove_index :spree_users, name: 'index_spree_users_on_normalized_login_when_tenant_id_null', if_exists: true
|
|
37
|
+
remove_column :spree_users, :normalized_login if column_exists?(:spree_users, :normalized_login)
|
|
38
|
+
end
|
|
39
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_cm_commissioner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.12.
|
|
4
|
+
version: 2.12.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: spree
|
|
@@ -1938,7 +1938,6 @@ files:
|
|
|
1938
1938
|
- app/overrides/spree/admin/users/_form/roles_fields.html.erb.deface
|
|
1939
1939
|
- app/overrides/spree/admin/users/_form/user_add_on.html.erb.deface
|
|
1940
1940
|
- app/overrides/spree/admin/users/_tabs/tabs.html.erb.deface
|
|
1941
|
-
- app/overrides/spree/admin/users/edit/account_status.html.erb.deface
|
|
1942
1941
|
- app/overrides/spree/admin/users/index/body.html.erb.deface
|
|
1943
1942
|
- app/overrides/spree/admin/users/index/headers.html.erb.deface
|
|
1944
1943
|
- app/overrides/spree/admin/users/index/ransack.html.erb.deface
|
|
@@ -2424,6 +2423,7 @@ files:
|
|
|
2424
2423
|
- app/services/spree_cm_commissioner/user_counters_service.rb
|
|
2425
2424
|
- app/services/spree_cm_commissioner/user_roles_assigner.rb
|
|
2426
2425
|
- app/services/spree_cm_commissioner/users/block.rb
|
|
2426
|
+
- app/services/spree_cm_commissioner/users/dedupe_logins.rb
|
|
2427
2427
|
- app/services/spree_cm_commissioner/users/qr_data/extract_login.rb
|
|
2428
2428
|
- app/services/spree_cm_commissioner/users/qr_data/generate.rb
|
|
2429
2429
|
- app/services/spree_cm_commissioner/users/qr_data/invalidate.rb
|
|
@@ -3547,6 +3547,7 @@ files:
|
|
|
3547
3547
|
- db/migrate/20260820100000_create_cm_private_sales.rb
|
|
3548
3548
|
- db/migrate/20260824120000_add_user_id_to_cm_waiting_room_sessions.rb
|
|
3549
3549
|
- db/migrate/20260825092416_add_live_stock_window_to_spree_taxons.rb
|
|
3550
|
+
- db/migrate/20260828080349_add_normalized_login_to_spree_users.rb
|
|
3550
3551
|
- docker-compose.yml
|
|
3551
3552
|
- docs/api/scoped-access-token-endpoints.md
|
|
3552
3553
|
- docs/option_types/attr_types.md
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
<!-- insert_after "[data-hook='admin_user_edit_general_settings']" -->
|
|
2
|
-
|
|
3
|
-
<div data-hook="admin_user_account_status" class="card mb-3">
|
|
4
|
-
<div class="card-header">
|
|
5
|
-
<h5 class="card-title mb-0 h6">
|
|
6
|
-
<%= I18n.t('spree_user.account_status') %>
|
|
7
|
-
</h5>
|
|
8
|
-
</div>
|
|
9
|
-
|
|
10
|
-
<div class="card-body">
|
|
11
|
-
<% if @user.blocked? %>
|
|
12
|
-
<p>
|
|
13
|
-
<span class="badge badge-danger text-uppercase"><%= I18n.t('spree_user.blocked') %></span>
|
|
14
|
-
<span class="text-muted ml-2"><%= I18n.t('spree_user.blocked_at', time: l(@user.blocked_at, format: :long)) %></span>
|
|
15
|
-
</p>
|
|
16
|
-
|
|
17
|
-
<% if @user.blocked_reason.present? %>
|
|
18
|
-
<p class="text-muted"><%= @user.blocked_reason %></p>
|
|
19
|
-
<% end %>
|
|
20
|
-
|
|
21
|
-
<%= form_tag spree.unblock_admin_user_path(@user), method: :put do %>
|
|
22
|
-
<%= button_tag I18n.t('spree_user.unblock_user'),
|
|
23
|
-
type: 'submit',
|
|
24
|
-
class: 'btn btn-primary',
|
|
25
|
-
data: { confirm: I18n.t('spree_user.unblock_confirmation') } %>
|
|
26
|
-
<% end %>
|
|
27
|
-
<% else %>
|
|
28
|
-
<%= form_tag spree.block_admin_user_path(@user), method: :put do %>
|
|
29
|
-
<div class="form-group">
|
|
30
|
-
<%= label_tag :blocked_reason, I18n.t('spree_user.blocked_reason') %>
|
|
31
|
-
<%= text_field_tag :blocked_reason, nil, class: 'form-control' %>
|
|
32
|
-
<small class="form-text text-muted"><%= I18n.t('spree_user.blocked_reason_hint') %></small>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<%= button_tag I18n.t('spree_user.block_user'),
|
|
36
|
-
type: 'submit',
|
|
37
|
-
class: 'btn btn-danger',
|
|
38
|
-
data: { confirm: I18n.t('spree_user.block_confirmation') } %>
|
|
39
|
-
<% end %>
|
|
40
|
-
<% end %>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|