FIX: Restore trust level when leaving group (#17954)

If a user was granted a trust level, joined a group that granted a trust
level and left the group, the trust level was reset. This commit tries
to restore the last known trust level before joining the group by
looking into staff logs.

This commit also migrates old :change_trust_level user history records
to use previous_value and new_value fields.
This commit is contained in:
Bianca Nenciu 2022-08-29 13:00:48 +03:00 committed by GitHub
parent c4bb15441d
commit 0d8ecab362
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 90 additions and 44 deletions

View File

@ -120,7 +120,7 @@ class GroupUser < ActiveRecord::Base
return if group.grant_trust_level.nil? return if group.grant_trust_level.nil?
return if self.destroyed_by_association&.active_record == User # User is being destroyed, so don't try to recalculate return if self.destroyed_by_association&.active_record == User # User is being destroyed, so don't try to recalculate
Promotion.recalculate(user) Promotion.recalculate(user, use_previous_trust_level: true)
end end
def set_category_notifications def set_category_notifications

View File

@ -100,7 +100,8 @@ class StaffActionLogger
UserHistory.create!(params(opts).merge( UserHistory.create!(params(opts).merge(
action: UserHistory.actions[:change_trust_level], action: UserHistory.actions[:change_trust_level],
target_user_id: user.id, target_user_id: user.id,
details: "old trust level: #{old_trust_level}\nnew trust level: #{new_trust_level}" previous_value: old_trust_level,
new_value: new_trust_level,
)) ))
end end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class MoveTlUserHistoryToPreviousAndNewValue < ActiveRecord::Migration[7.0]
def change
execute <<~SQL
UPDATE user_histories
SET previous_value = old_tl,
new_value = new_tl,
details = NULL
FROM (
SELECT id user_history_id,
(REGEXP_MATCHES(details, 'old trust level: (\d+)', 'i'))[1] old_tl,
(REGEXP_MATCHES(details, 'new trust level: (\d+)', 'i'))[1] new_tl
FROM user_histories
WHERE action = 2
) trust_levels
WHERE user_histories.id = trust_levels.user_history_id
SQL
end
end

View File

@ -121,21 +121,12 @@ class Promotion
end end
# Figure out what a user's trust level should be from scratch # Figure out what a user's trust level should be from scratch
def self.recalculate(user, performed_by = nil) def self.recalculate(user, performed_by = nil, use_previous_trust_level: false)
# First, use the manual locked level granted_trust_level = TrustLevel.calculate(
unless user.manual_locked_trust_level.nil? user, use_previous_trust_level: use_previous_trust_level
return user.update!(
trust_level: user.manual_locked_trust_level
)
end
# Then consider the group locked level
user_group_granted_trust_level = user.group_granted_trust_level
if user_group_granted_trust_level.present?
return user.update(
trust_level: user_group_granted_trust_level
) )
if granted_trust_level.present?
return user.update(trust_level: granted_trust_level)
end end
user.update_column(:trust_level, TrustLevel[0]) user.update_column(:trust_level, TrustLevel[0])

View File

@ -3,33 +3,51 @@
class InvalidTrustLevel < StandardError; end class InvalidTrustLevel < StandardError; end
class TrustLevel class TrustLevel
def self.[](level)
class << self
def [](level)
raise InvalidTrustLevel if !valid?(level) raise InvalidTrustLevel if !valid?(level)
level level
end end
def levels def self.levels
@levels ||= Enum.new(:newuser, :basic, :member, :regular, :leader, start: 0) @levels ||= Enum.new(:newuser, :basic, :member, :regular, :leader, start: 0)
end end
def valid?(level) def self.valid?(level)
valid_range === level valid_range === level
end end
def valid_range def self.valid_range
(0..4) (0..4)
end end
def compare(current_level, level) def self.compare(current_level, level)
(current_level || 0) >= level (current_level || 0) >= level
end end
def name(level) def self.name(level)
I18n.t("js.trust_levels.names.#{levels[level]}") I18n.t("js.trust_levels.names.#{levels[level]}")
end end
def self.calculate(user, use_previous_trust_level: false)
# First, use the manual locked level
return user.manual_locked_trust_level if user.manual_locked_trust_level.present?
# Then consider the group locked level (or the previous trust level)
granted_trust_level = user.group_granted_trust_level || 0
previous_trust_level = use_previous_trust_level ? find_previous_trust_level(user) : 0
[granted_trust_level, previous_trust_level].max
end end
private
def self.find_previous_trust_level(user)
UserHistory
.where(action: UserHistory.actions[:change_trust_level])
.where(target_user_id: user.id)
.order(created_at: :desc)
.pluck_first(:new_value)
.to_i
end
end end

View File

@ -237,5 +237,20 @@ RSpec.describe GroupUser do
expect(user.primary_group_id).to be_nil expect(user.primary_group_id).to be_nil
expect(user.flair_group_id).to be_nil expect(user.flair_group_id).to be_nil
end end
it "restores previous trust level" do
user = Fabricate(:user)
expect(user.trust_level).to eq(1)
user.change_trust_level!(2, log_action_for: Discourse.system_user)
user.change_trust_level!(3, log_action_for: Discourse.system_user)
group.update!(grant_trust_level: 4)
group_user = Fabricate(:group_user, group: group, user: user)
expect(user.reload.trust_level).to eq(4)
group_user.destroy!
expect(user.reload.trust_level).to eq(3)
end
end end
end end

View File

@ -132,7 +132,8 @@ RSpec.describe StaffActionLogger do
it 'creates a new UserHistory record' do it 'creates a new UserHistory record' do
expect { log_trust_level_change }.to change { UserHistory.count }.by(1) expect { log_trust_level_change }.to change { UserHistory.count }.by(1)
expect(UserHistory.last.details).to include "new trust level: #{new_trust_level}" expect(UserHistory.last.previous_value).to eq(old_trust_level.to_s)
expect(UserHistory.last.new_value).to eq(new_trust_level.to_s)
end end
end end