DEV: Refactor STI/polymorphic associations in chat (#20789)

This commit is contained in:
Loïc Guitaut 2023-04-17 15:41:56 +02:00 committed by GitHub
parent 87515b1aa0
commit a5235f7d16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 82 additions and 130 deletions

View File

@ -22,7 +22,7 @@ class Bookmark < ActiveRecord::Base
end end
def self.valid_bookmarkable_types def self.valid_bookmarkable_types
Bookmark.registered_bookmarkables.map { |bm| bm.model.to_s } Bookmark.registered_bookmarkables.map { |bm| bm.model.polymorphic_name }
end end
belongs_to :user belongs_to :user

View File

@ -129,7 +129,7 @@ class Reviewable < ActiveRecord::Base
update_args = { update_args = {
status: statuses[:pending], status: statuses[:pending],
id: target.id, id: target.id,
type: target.class.sti_name, type: target.class.polymorphic_name,
potential_spam: potential_spam == true ? true : nil, potential_spam: potential_spam == true ? true : nil,
} }

View File

@ -46,7 +46,7 @@ module Jobs
# by the CleanUpUploads job in core # by the CleanUpUploads job in core
::UploadReference.where( ::UploadReference.where(
target_id: message_ids, target_id: message_ids,
target_type: ::Chat::Message.sti_name, target_type: ::Chat::Message.polymorphic_name,
).delete_all ).delete_all
# only the messages and the channel are Trashable, everything else gets # only the messages and the channel are Trashable, everything else gets

View File

@ -7,10 +7,6 @@ module Chat
delegate :read_restricted?, to: :category delegate :read_restricted?, to: :category
delegate :url, to: :chatable, prefix: true delegate :url, to: :chatable, prefix: true
def self.polymorphic_class_for(name)
Chat::Chatable.polymorphic_class_for(name) || super(name)
end
%i[category_channel? public_channel? chatable_has_custom_fields?].each do |name| %i[category_channel? public_channel? chatable_has_custom_fields?].each do |name|
define_method(name) { true } define_method(name) { true }
end end

View File

@ -3,19 +3,11 @@
module Chat module Chat
class Channel < ActiveRecord::Base class Channel < ActiveRecord::Base
include Trashable include Trashable
include TypeMappable
self.table_name = "chat_channels" self.table_name = "chat_channels"
belongs_to :chatable, polymorphic: true belongs_to :chatable, polymorphic: true
def self.sti_class_for(type_name)
Chat::Chatable.sti_class_for(type_name) || super(type_name)
end
def self.sti_name
Chat::Chatable.sti_name_for(self) || super
end
belongs_to :direct_message, belongs_to :direct_message,
class_name: "Chat::DirectMessage", class_name: "Chat::DirectMessage",
foreign_key: :chatable_id, foreign_key: :chatable_id,
@ -51,6 +43,14 @@ module Chat
delegate :empty?, to: :chat_messages, prefix: true delegate :empty?, to: :chat_messages, prefix: true
class << self class << self
def sti_class_mapping =
{
"CategoryChannel" => Chat::CategoryChannel,
"DirectMessageChannel" => Chat::DirectMessageChannel,
}
def polymorphic_class_mapping = { "DirectMessage" => Chat::DirectMessage }
def editable_statuses def editable_statuses
statuses.filter { |k, _| !%w[read_only archived].include?(k) } statuses.filter { |k, _| !%w[read_only archived].include?(k) }
end end

View File

@ -5,10 +5,7 @@ module Chat
self.table_name = "direct_message_channels" self.table_name = "direct_message_channels"
include Chatable include Chatable
include TypeMappable
def self.polymorphic_name
Chat::Chatable.polymorphic_name_for(self) || super
end
has_many :direct_message_users, has_many :direct_message_users,
class_name: "Chat::DirectMessageUser", class_name: "Chat::DirectMessageUser",
@ -17,11 +14,15 @@ module Chat
has_one :direct_message_channel, as: :chatable, class_name: "Chat::DirectMessageChannel" has_one :direct_message_channel, as: :chatable, class_name: "Chat::DirectMessageChannel"
def self.for_user_ids(user_ids) class << self
joins(:users) def polymorphic_class_mapping = { "DirectMessage" => Chat::DirectMessage }
.group("direct_message_channels.id")
.having("ARRAY[?] = ARRAY_AGG(users.id ORDER BY users.id)", user_ids.sort) def for_user_ids(user_ids)
&.first joins(:users)
.group("direct_message_channels.id")
.having("ARRAY[?] = ARRAY_AGG(users.id ORDER BY users.id)", user_ids.sort)
.first
end
end end
def user_can_access?(user) def user_can_access?(user)

View File

@ -4,10 +4,6 @@ module Chat
class DirectMessageChannel < Channel class DirectMessageChannel < Channel
alias_attribute :direct_message, :chatable alias_attribute :direct_message, :chatable
def self.polymorphic_class_for(name)
Chat::Chatable.polymorphic_class_for(name) || super(name)
end
def direct_message_channel? def direct_message_channel?
true true
end end

View File

@ -3,13 +3,14 @@
module Chat module Chat
class Message < ActiveRecord::Base class Message < ActiveRecord::Base
include Trashable include Trashable
include TypeMappable
self.table_name = "chat_messages" self.table_name = "chat_messages"
attribute :has_oneboxes, default: false
BAKED_VERSION = 2 BAKED_VERSION = 2
attribute :has_oneboxes, default: false
belongs_to :chat_channel, class_name: "Chat::Channel" belongs_to :chat_channel, class_name: "Chat::Channel"
belongs_to :user belongs_to :user
belongs_to :in_reply_to, class_name: "Chat::Message" belongs_to :in_reply_to, class_name: "Chat::Message"
@ -30,36 +31,18 @@ module Chat
foreign_key: :chat_message_id foreign_key: :chat_message_id
has_many :bookmarks, has_many :bookmarks,
-> { -> {
unscope(where: :bookmarkable_type).where(bookmarkable_type: Chat::Message.sti_name) unscope(where: :bookmarkable_type).where(
bookmarkable_type: Chat::Message.polymorphic_name,
)
}, },
as: :bookmarkable, as: :bookmarkable,
dependent: :destroy dependent: :destroy
has_many :upload_references, has_many :upload_references,
-> { unscope(where: :target_type).where(target_type: Chat::Message.sti_name) }, -> { unscope(where: :target_type).where(target_type: Chat::Message.polymorphic_name) },
dependent: :destroy, dependent: :destroy,
foreign_key: :target_id foreign_key: :target_id
has_many :uploads, through: :upload_references, class_name: "::Upload" has_many :uploads, through: :upload_references, class_name: "::Upload"
CLASS_MAPPING = { "ChatMessage" => Chat::Message }
# the model used when loading type column
def self.sti_class_for(name)
CLASS_MAPPING[name] if CLASS_MAPPING.key?(name)
end
# the type column value
def self.sti_name
CLASS_MAPPING.invert.fetch(self)
end
# the model used when loading chatable_type column
def self.polymorphic_class_for(name)
CLASS_MAPPING[name] if CLASS_MAPPING.key?(name)
end
# the type stored in *_type column of polymorphic associations
def self.polymorphic_name
CLASS_MAPPING.invert.fetch(self) || super
end
has_one :chat_webhook_event, has_one :chat_webhook_event,
dependent: :destroy, dependent: :destroy,
class_name: "Chat::WebhookEvent", class_name: "Chat::WebhookEvent",
@ -77,7 +60,6 @@ module Chat
}, },
) )
} }
scope :in_dm_channel, scope :in_dm_channel,
-> { -> {
joins(:chat_channel).where( joins(:chat_channel).where(
@ -86,11 +68,13 @@ module Chat
}, },
) )
} }
scope :created_before, ->(date) { where("chat_messages.created_at < ?", date) } scope :created_before, ->(date) { where("chat_messages.created_at < ?", date) }
scope :uncooked, -> { where("cooked_version <> ? or cooked_version IS NULL", BAKED_VERSION) }
before_save { ensure_last_editor_id } before_save { ensure_last_editor_id }
def self.polymorphic_class_mapping = { "ChatMessage" => Chat::Message }
def validate_message(has_uploads:) def validate_message(has_uploads:)
WatchedWordsValidator.new(attributes: [:message]).validate(self) WatchedWordsValidator.new(attributes: [:message]).validate(self)
@ -125,7 +109,7 @@ module Chat
{ {
upload_id: upload.id, upload_id: upload.id,
target_id: self.id, target_id: self.id,
target_type: self.class.sti_name, target_type: self.class.polymorphic_name,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
} }
@ -193,10 +177,6 @@ module Chat
Jobs.enqueue(Jobs::Chat::ProcessMessage, args) Jobs.enqueue(Jobs::Chat::ProcessMessage, args)
end end
def self.uncooked
where("cooked_version <> ? or cooked_version IS NULL", BAKED_VERSION)
end
MARKDOWN_FEATURES = %w[ MARKDOWN_FEATURES = %w[
anchor anchor
bbcode-block bbcode-block

View File

@ -56,7 +56,7 @@ module Chat
sql, sql,
pending: ReviewableScore.statuses[:pending], pending: ReviewableScore.statuses[:pending],
message_ids: @chat_messages.map(&:id), message_ids: @chat_messages.map(&:id),
target_type: Chat::Message.sti_name, target_type: Chat::Message.polymorphic_name,
) )
.each { |row| ids[row.target_id] = row.reviewable_id } .each { |row| ids[row.target_id] = row.reviewable_id }
@ -85,7 +85,7 @@ module Chat
sql, sql,
message_ids: @chat_messages.map(&:id), message_ids: @chat_messages.map(&:id),
user_id: @user.id, user_id: @user.id,
target_type: Chat::Message.sti_name, target_type: Chat::Message.polymorphic_name,
) )
.each { |row| statuses[row.target_id] = row.status } .each { |row| statuses[row.target_id] = row.status }

View File

@ -4,33 +4,6 @@ module Chat
module Chatable module Chatable
extend ActiveSupport::Concern extend ActiveSupport::Concern
STI_CLASS_MAPPING = {
"CategoryChannel" => Chat::CategoryChannel,
"DirectMessageChannel" => Chat::DirectMessageChannel,
}
# the model used when loading type column
def self.sti_class_for(name)
STI_CLASS_MAPPING[name] if STI_CLASS_MAPPING.key?(name)
end
# the type column value
def self.sti_name_for(klass)
STI_CLASS_MAPPING.invert.fetch(klass)
end
POLYMORPHIC_CLASS_MAPPING = { "DirectMessage" => Chat::DirectMessage }
# the model used when loading chatable_type column
def self.polymorphic_class_for(name)
POLYMORPHIC_CLASS_MAPPING[name] if POLYMORPHIC_CLASS_MAPPING.key?(name)
end
# the chatable_type column value
def self.polymorphic_name_for(klass)
POLYMORPHIC_CLASS_MAPPING.invert.fetch(klass)
end
def chat_channel def chat_channel
channel_class.new(chatable: self) channel_class.new(chatable: self)
end end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module Chat
module TypeMappable
extend ActiveSupport::Concern
class_methods do
def sti_class_mapping = {}
def polymorphic_class_mapping = {}
# the model used when loading type column
def sti_class_for(name)
sti_class_mapping[name] || super
end
# the type column value
def sti_name
sti_class_mapping.invert[self] || super
end
# the model used when loading *_type column (e.g. 'chatable_type')
def polymorphic_class_for(name)
polymorphic_class_mapping[name] || super
end
# the *_type column value (e.g. 'chatable_type')
def polymorphic_name
polymorphic_class_mapping.invert[self] || super
end
end
end
end

View File

@ -4,19 +4,8 @@ module Chat
module BookmarkExtension module BookmarkExtension
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended { include TypeMappable }
def valid_bookmarkable_type
return true if self.bookmarkable_type == Chat::Message.sti_name
super if defined?(super)
end
CLASS_MAPPING = { "ChatMessage" => Chat::Message } class_methods { def polymorphic_class_mapping = { "ChatMessage" => Chat::Message } }
# the model used when loading chatable_type column
def self.polymorphic_class_for(name)
return CLASS_MAPPING[name] if CLASS_MAPPING.key?(name)
super if defined?(super)
end
end
end end
end end

View File

@ -6,10 +6,6 @@ module Chat
include Chat::Chatable include Chat::Chatable
def self.polymorphic_name
Chat::Chatable.polymorphic_name_for(self) || super
end
prepended do prepended do
has_one :category_channel, has_one :category_channel,
as: :chatable, as: :chatable,

View File

@ -23,12 +23,12 @@ module Chat
:sanitize_sql_array, :sanitize_sql_array,
[ [
"INNER JOIN chat_messages ON chat_messages.id = bookmarks.bookmarkable_id AND chat_messages.deleted_at IS NULL AND bookmarks.bookmarkable_type = ?", "INNER JOIN chat_messages ON chat_messages.id = bookmarks.bookmarkable_id AND chat_messages.deleted_at IS NULL AND bookmarks.bookmarkable_type = ?",
Chat::Message.sti_name, Chat::Message.polymorphic_name,
], ],
) )
user user
.bookmarks_of_type(Chat::Message.sti_name) .bookmarks_of_type(Chat::Message.polymorphic_name)
.joins(joins) .joins(joins)
.where("chat_messages.chat_channel_id IN (?)", accessible_channel_ids) .where("chat_messages.chat_channel_id IN (?)", accessible_channel_ids)
end end
@ -66,7 +66,7 @@ module Chat
end end
def self.cleanup_deleted def self.cleanup_deleted
DB.query(<<~SQL, grace_time: 3.days.ago, bookmarkable_type: Chat::Message.sti_name) DB.query(<<~SQL, grace_time: 3.days.ago, bookmarkable_type: Chat::Message.polymorphic_name)
DELETE FROM bookmarks b DELETE FROM bookmarks b
USING chat_messages cm USING chat_messages cm
WHERE b.bookmarkable_id = cm.id WHERE b.bookmarkable_id = cm.id

View File

@ -143,7 +143,7 @@ module Chat
WHERE cmr.chat_message_id = mm.old_chat_message_id WHERE cmr.chat_message_id = mm.old_chat_message_id
SQL SQL
DB.exec(<<~SQL, target_type: Chat::Message.sti_name) DB.exec(<<~SQL, target_type: Chat::Message.polymorphic_name)
UPDATE upload_references uref UPDATE upload_references uref
SET target_id = mm.new_chat_message_id SET target_id = mm.new_chat_message_id
FROM moved_chat_messages mm FROM moved_chat_messages mm

View File

@ -4,24 +4,11 @@ module Chat
module ReviewableExtension module ReviewableExtension
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended { include TypeMappable }
# the model used when loading type column
def self.sti_class_for(name)
return Chat::ReviewableMessage if name == "ReviewableChatMessage"
super(name)
end
# the model used when loading target_type column class_methods do
def self.polymorphic_class_for(name) def sti_class_mapping = { "ReviewableChatMessage" => Chat::ReviewableMessage }
return Chat::Message if name == Chat::Message.sti_name def polymorphic_class_mapping = { "ChatMessage" => Chat::Message }
super(name)
end
# the type column value when saving a Chat::ReviewableMessage
def self.sti_name
return "ReviewableChatMessage" if self.to_s == "Chat::ReviewableMessage"
super
end
end end
end end
end end

View File

@ -87,7 +87,6 @@ Fabricator(:chat_reviewable_message, class_name: "Chat::ReviewableMessage") do
reviewable_by_moderator true reviewable_by_moderator true
type "ReviewableChatMessage" type "ReviewableChatMessage"
created_by { Fabricate(:user) } created_by { Fabricate(:user) }
target_type Chat::Message.sti_name
target { Fabricate(:chat_message) } target { Fabricate(:chat_message) }
reviewable_scores { |p| [Fabricate.build(:reviewable_score, reviewable_id: p[:id])] } reviewable_scores { |p| [Fabricate.build(:reviewable_score, reviewable_id: p[:id])] }
end end

View File

@ -62,7 +62,10 @@ describe Jobs::Chat::ChannelDelete do
revisions: Chat::MessageRevision.where(chat_message_id: @message_ids).count, revisions: Chat::MessageRevision.where(chat_message_id: @message_ids).count,
mentions: Chat::Mention.where(chat_message_id: @message_ids).count, mentions: Chat::Mention.where(chat_message_id: @message_ids).count,
upload_references: upload_references:
UploadReference.where(target_id: @message_ids, target_type: Chat::Message.sti_name).count, UploadReference.where(
target_id: @message_ids,
target_type: Chat::Message.polymorphic_name,
).count,
messages: Chat::Message.where(id: @message_ids).count, messages: Chat::Message.where(id: @message_ids).count,
reactions: Chat::MessageReaction.where(chat_message_id: @message_ids).count, reactions: Chat::MessageReaction.where(chat_message_id: @message_ids).count,
} }

View File

@ -533,7 +533,7 @@ describe Chat::Message do
upload_references = UploadReference.where(upload_id: [upload_1, upload_2]) upload_references = UploadReference.where(upload_id: [upload_1, upload_2])
expect(upload_references.count).to eq(2) expect(upload_references.count).to eq(2)
expect(upload_references.map(&:target_id).uniq).to eq([chat_message.id]) expect(upload_references.map(&:target_id).uniq).to eq([chat_message.id])
expect(upload_references.map(&:target_type).uniq).to eq([Chat::Message.sti_name]) expect(upload_references.map(&:target_type).uniq).to eq([described_class.polymorphic_name])
end end
it "does nothing if the message record is new" do it "does nothing if the message record is new" do