FIX: can now correctly unassign corrupt topics
if a topic is somehow assigned to an array we can correctly unassign
This commit is contained in:
parent
6539c98768
commit
8ad7304bdd
|
@ -1,14 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_dependency 'email/sender'
|
require_dependency 'email/sender'
|
||||||
|
|
||||||
class ::TopicAssigner
|
class ::TopicAssigner
|
||||||
|
|
||||||
|
ASSIGNED_TO_ID = 'assigned_to_id'
|
||||||
|
ASSIGNED_BY_ID = 'assigned_by_id'
|
||||||
|
|
||||||
def self.unassign_all(user, assigned_by)
|
def self.unassign_all(user, assigned_by)
|
||||||
topic_ids = TopicCustomField.where(name: 'assigned_to_id', value: user.id).pluck(:topic_id)
|
topic_ids = TopicCustomField.where(name: ASSIGNED_TO_ID, value: user.id).pluck(:topic_id)
|
||||||
|
|
||||||
# Fast path: by doing this we can instantly refresh for the user showing no assigned topics
|
# Fast path: by doing this we can instantly refresh for the user showing no assigned topics
|
||||||
# while doing the "full" removal asynchronously.
|
# while doing the "full" removal asynchronously.
|
||||||
TopicCustomField.where(
|
TopicCustomField.where(
|
||||||
name: ['assigned_to_id', 'assigned_by_id'],
|
name: [ASSIGNED_TO_ID, ASSIGNED_BY_ID],
|
||||||
topic_id: topic_ids
|
topic_id: topic_ids
|
||||||
).delete_all
|
).delete_all
|
||||||
|
|
||||||
|
@ -30,7 +35,7 @@ class ::TopicAssigner
|
||||||
SELECT p.topic_id, MAX(post_number) post_number
|
SELECT p.topic_id, MAX(post_number) post_number
|
||||||
FROM posts p
|
FROM posts p
|
||||||
JOIN topics t ON t.id = p.topic_id
|
JOIN topics t ON t.id = p.topic_id
|
||||||
LEFT JOIN topic_custom_fields tc ON tc.name = 'assigned_to_id' AND tc.topic_id = p.topic_id
|
LEFT JOIN topic_custom_fields tc ON tc.name = '#{ASSIGNED_TO_ID}' AND tc.topic_id = p.topic_id
|
||||||
WHERE p.user_id IN (SELECT id FROM users WHERE moderator OR admin) AND
|
WHERE p.user_id IN (SELECT id FROM users WHERE moderator OR admin) AND
|
||||||
( #{staff_mention} ) AND tc.value IS NULL AND NOT t.closed AND t.deleted_at IS NULL
|
( #{staff_mention} ) AND tc.value IS NULL AND NOT t.closed AND t.deleted_at IS NULL
|
||||||
GROUP BY p.topic_id
|
GROUP BY p.topic_id
|
||||||
|
@ -68,7 +73,7 @@ SQL
|
||||||
return unless SiteSetting.assigns_by_staff_mention
|
return unless SiteSetting.assigns_by_staff_mention
|
||||||
|
|
||||||
if post.user && post.topic && post.user.staff?
|
if post.user && post.topic && post.user.staff?
|
||||||
can_assign = force || post.topic.custom_fields["assigned_to_id"].nil?
|
can_assign = force || post.topic.custom_fields[ASSIGNED_TO_ID].nil?
|
||||||
|
|
||||||
assign_other = assign_other_passes?(post) && mentioned_staff(post)
|
assign_other = assign_other_passes?(post) && mentioned_staff(post)
|
||||||
assign_self = assign_self_passes?(post) && post.user
|
assign_self = assign_self_passes?(post) && post.user
|
||||||
|
@ -125,8 +130,8 @@ SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign(assign_to, silent: false)
|
def assign(assign_to, silent: false)
|
||||||
@topic.custom_fields["assigned_to_id"] = assign_to.id
|
@topic.custom_fields[ASSIGNED_TO_ID] = assign_to.id
|
||||||
@topic.custom_fields["assigned_by_id"] = @assigned_by.id
|
@topic.custom_fields[ASSIGNED_BY_ID] = @assigned_by.id
|
||||||
@topic.save_custom_fields
|
@topic.save_custom_fields
|
||||||
|
|
||||||
first_post = @topic.posts.find_by(post_number: 1)
|
first_post = @topic.posts.find_by(post_number: 1)
|
||||||
|
@ -198,10 +203,29 @@ SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def unassign(silent: false)
|
def unassign(silent: false)
|
||||||
if assigned_to_id = @topic.custom_fields["assigned_to_id"]
|
if assigned_to_id = @topic.custom_fields[ASSIGNED_TO_ID]
|
||||||
@topic.custom_fields["assigned_to_id"] = nil
|
|
||||||
@topic.custom_fields["assigned_by_id"] = nil
|
# TODO core needs an API for this stuff badly
|
||||||
@topic.save_custom_fields
|
# currently there is no 100% surefire way of deleting a custom field
|
||||||
|
TopicCustomField.where(
|
||||||
|
topic_id: @topic.id
|
||||||
|
).where(
|
||||||
|
'name in (?)', [ASSIGNED_BY_ID, ASSIGNED_TO_ID]
|
||||||
|
).destroy_all
|
||||||
|
|
||||||
|
if Array === assigned_to_id
|
||||||
|
# more custom field mess, try to recover
|
||||||
|
assigned_to_id = assigned_to_id.first
|
||||||
|
end
|
||||||
|
|
||||||
|
# clean up in memory object
|
||||||
|
@topic.custom_fields[ASSIGNED_TO_ID] = nil
|
||||||
|
@topic.custom_fields[ASSIGNED_BY_ID] = nil
|
||||||
|
|
||||||
|
if !assigned_to_id
|
||||||
|
# nothing to do here
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
post = @topic.posts.where(post_number: 1).first
|
post = @topic.posts.where(post_number: 1).first
|
||||||
return unless post.present?
|
return unless post.present?
|
||||||
|
|
18
plugin.rb
18
plugin.rb
|
@ -27,7 +27,7 @@ after_initialize do
|
||||||
if SiteSetting.assign_locks_flags?
|
if SiteSetting.assign_locks_flags?
|
||||||
|
|
||||||
if custom_fields = args[:post].topic.custom_fields
|
if custom_fields = args[:post].topic.custom_fields
|
||||||
if assigned_to_id = custom_fields['assigned_to_id']
|
if assigned_to_id = custom_fields[TopicAssigner::ASSIGNED_TO_ID]
|
||||||
unless assigned_to_id.to_i == args[:user].id
|
unless assigned_to_id.to_i == args[:user].id
|
||||||
raise Discourse::InvalidAccess.new(
|
raise Discourse::InvalidAccess.new(
|
||||||
"That flag has been assigned to another user",
|
"That flag has been assigned to another user",
|
||||||
|
@ -47,7 +47,7 @@ after_initialize do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
TopicList.preloaded_custom_fields << "assigned_to_id"
|
TopicList.preloaded_custom_fields << TopicAssigner::ASSIGNED_TO_ID
|
||||||
|
|
||||||
TopicList.on_preload do |topics, topic_list|
|
TopicList.on_preload do |topics, topic_list|
|
||||||
if SiteSetting.assign_enabled?
|
if SiteSetting.assign_enabled?
|
||||||
|
@ -67,7 +67,7 @@ after_initialize do
|
||||||
users.each { |u| map[u.id] = u }
|
users.each { |u| map[u.id] = u }
|
||||||
|
|
||||||
topics.each do |t|
|
topics.each do |t|
|
||||||
if id = t.custom_fields['assigned_to_id']
|
if id = t.custom_fields[TopicAssigner::ASSIGNED_TO_ID]
|
||||||
t.preload_assigned_to_user(map[id.to_i])
|
t.preload_assigned_to_user(map[id.to_i])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -143,7 +143,7 @@ after_initialize do
|
||||||
|
|
||||||
add_to_class(:topic, :assigned_to_user) do
|
add_to_class(:topic, :assigned_to_user) do
|
||||||
@assigned_to_user ||
|
@assigned_to_user ||
|
||||||
if user_id = custom_fields["assigned_to_id"]
|
if user_id = custom_fields[TopicAssigner::ASSIGNED_TO_ID]
|
||||||
@assigned_to_user = User.find_by(id: user_id)
|
@assigned_to_user = User.find_by(id: user_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -176,7 +176,7 @@ after_initialize do
|
||||||
end
|
end
|
||||||
|
|
||||||
add_to_class(:topic_view_serializer, :assigned_to_user_id) do
|
add_to_class(:topic_view_serializer, :assigned_to_user_id) do
|
||||||
id = object.topic.custom_fields["assigned_to_id"]
|
id = object.topic.custom_fields[TopicAssigner::ASSIGNED_TO_ID]
|
||||||
# a bit messy but race conditions can give us an array here, avoid
|
# a bit messy but race conditions can give us an array here, avoid
|
||||||
id && id.to_i rescue nil
|
id && id.to_i rescue nil
|
||||||
end
|
end
|
||||||
|
@ -184,7 +184,7 @@ after_initialize do
|
||||||
add_to_serializer(:topic_view, 'include_assigned_to_user?') do
|
add_to_serializer(:topic_view, 'include_assigned_to_user?') do
|
||||||
if SiteSetting.assigns_public || scope.is_staff?
|
if SiteSetting.assigns_public || scope.is_staff?
|
||||||
# subtle but need to catch cases where stuff is not assigned
|
# subtle but need to catch cases where stuff is not assigned
|
||||||
object.topic.custom_fields.keys.include?("assigned_to_id")
|
object.topic.custom_fields.keys.include?(TopicAssigner::ASSIGNED_TO_ID)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -193,7 +193,7 @@ after_initialize do
|
||||||
end
|
end
|
||||||
|
|
||||||
add_to_serializer(:flagged_topic, :assigned_to_user_id) do
|
add_to_serializer(:flagged_topic, :assigned_to_user_id) do
|
||||||
id = object.custom_fields["assigned_to_id"]
|
id = object.custom_fields[TopicAssigner::ASSIGNED_TO_ID]
|
||||||
# a bit messy but race conditions can give us an array here, avoid
|
# a bit messy but race conditions can give us an array here, avoid
|
||||||
id && id.to_i rescue nil
|
id && id.to_i rescue nil
|
||||||
end
|
end
|
||||||
|
@ -238,7 +238,7 @@ after_initialize do
|
||||||
on(:move_to_inbox) do |info|
|
on(:move_to_inbox) do |info|
|
||||||
topic = info[:topic]
|
topic = info[:topic]
|
||||||
|
|
||||||
if (assigned_id = topic.custom_fields["assigned_to_id"].to_i) == info[:user]&.id
|
if (assigned_id = topic.custom_fields[TopicAssigner::ASSIGNED_TO_ID].to_i) == info[:user]&.id
|
||||||
TopicTrackingState.publish_assigned_private_message(topic, assigned_id)
|
TopicTrackingState.publish_assigned_private_message(topic, assigned_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ after_initialize do
|
||||||
|
|
||||||
on(:archive_message) do |info|
|
on(:archive_message) do |info|
|
||||||
topic = info[:topic]
|
topic = info[:topic]
|
||||||
user_id = topic.custom_fields["assigned_to_id"].to_i
|
user_id = topic.custom_fields[TopicAssigner::ASSIGNED_TO_ID].to_i
|
||||||
|
|
||||||
if user_id == info[:user]&.id
|
if user_id == info[:user]&.id
|
||||||
TopicTrackingState.publish_assigned_private_message(topic, user_id)
|
TopicTrackingState.publish_assigned_private_message(topic, user_id)
|
||||||
|
|
|
@ -13,6 +13,19 @@ describe 'integration tests' do
|
||||||
# should not explode for now
|
# should not explode for now
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'data consistency' do
|
||||||
|
it 'can deal with problem custom fields' do
|
||||||
|
post = Fabricate(:post)
|
||||||
|
post.topic.custom_fields[TopicAssigner::ASSIGNED_TO_ID] = [nil, nil]
|
||||||
|
post.topic.save_custom_fields
|
||||||
|
|
||||||
|
TopicAssigner.new(Topic.find(post.topic_id), Discourse.system_user).unassign
|
||||||
|
|
||||||
|
post.topic.reload
|
||||||
|
expect(post.topic.custom_fields).to eq({})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'for a private message' do
|
describe 'for a private message' do
|
||||||
let(:post) { Fabricate(:private_message_post) }
|
let(:post) { Fabricate(:private_message_post) }
|
||||||
let(:pm) { post.topic }
|
let(:pm) { post.topic }
|
||||||
|
@ -25,7 +38,7 @@ describe 'integration tests' do
|
||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
|
|
||||||
message = messages.find { |message| message.channel == channel }
|
message = messages.find { |m| m.channel == channel }
|
||||||
|
|
||||||
expect(message.data[:topic_id]).to eq(topic.id)
|
expect(message.data[:topic_id]).to eq(topic.id)
|
||||||
expect(message.user_ids).to eq([user.id])
|
expect(message.user_ids).to eq([user.id])
|
||||||
|
|
Loading…
Reference in New Issue