Move all custom field usage to tables

This commit is contained in:
Nat 2025-03-21 11:36:02 +08:00
parent e5699590ea
commit 8036f5b4b2
No known key found for this signature in database
GPG Key ID: 4938B35D927EC773
17 changed files with 186 additions and 282 deletions

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module DiscourseSolved
class SolvedTopic < ActiveRecord::Base
self.table_name = "discourse_solved_solved_topics"
belongs_to :topic, class_name: "Topic"
belongs_to :answer_post, class_name: "Post", foreign_key: "answer_post_id"
belongs_to :accepter, class_name: "User", foreign_key: "accepter_user_id"
belongs_to :topic_timer
validates :topic_id, presence: true
validates :answer_post_id, presence: true
end
end

View File

@ -7,7 +7,7 @@ module DiscourseSolved
end
def has_accepted_answer
object.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].present?
object&.solved.present?
end
def include_has_accepted_answer?

View File

@ -3,10 +3,10 @@
first_solution_query = <<~SQL
SELECT post_id, user_id, created_at AS granted_at
FROM (
SELECT p.id AS post_id, p.user_id, dss.created_at,
ROW_NUMBER() OVER (PARTITION BY p.user_id ORDER BY dss.created_at) AS row_number
FROM discourse_solved_solutions dss
JOIN badge_posts p ON dss.answer_post_id = p.id
SELECT p.id AS post_id, p.user_id, dsst.created_at,
ROW_NUMBER() OVER (PARTITION BY p.user_id ORDER BY dsst.created_at) AS row_number
FROM discourse_solved_solved_topics dsst
JOIN badge_posts p ON dsst.answer_post_id = p.id
JOIN topics t ON p.topic_id = t.id
WHERE p.user_id <> t.user_id -- ignore topics solved by OP
AND (:backfill OR p.id IN (:post_ids))
@ -31,9 +31,9 @@ end
def solved_query_with_count(min_count)
<<~SQL
SELECT p.user_id, MAX(dss.created_at) AS granted_at
FROM discourse_solved_solutions dss
JOIN badge_posts p ON dss.answer_post_id = p.id
SELECT p.user_id, MAX(dsst.created_at) AS granted_at
FROM discourse_solved_solved_topics dsst
JOIN badge_posts p ON dsst.answer_post_id = p.id
JOIN topics t ON p.topic_id = t.id
WHERE p.user_id <> t.user_id -- ignore topics solved by OP
AND (:backfill OR p.id IN (:post_ids))

View File

@ -6,7 +6,7 @@ class CopySolvedTopicCustomFieldToDiscourseSolvedTopics < ActiveRecord::Migratio
BATCH_SIZE = 5000
def up
create_table :discourse_solved_topics do |t|
create_table :discourse_solved_solved_topics do |t|
t.integer :topic_id, null: false
t.integer :answer_post_id, null: false
t.integer :accepter_user_id, null: false
@ -17,7 +17,7 @@ class CopySolvedTopicCustomFieldToDiscourseSolvedTopics < ActiveRecord::Migratio
last_id = 0
loop do
rows = DB.query(<<~SQL, last_id: last_id, batch_size: BATCH_SIZE)
INSERT INTO discourse_solved_topics (
INSERT INTO discourse_solved_solved_topics (
topic_id,
answer_post_id,
topic_timer_id,

View File

@ -4,7 +4,19 @@ class AddIndexForDiscourseSolvedTopics < ActiveRecord::Migration[7.2]
disable_ddl_transaction!
def change
add_index :discourse_solved_topics, :topic_id, unique: true, algorithm: :concurrently
add_index :discourse_solved_topics, :answer_post_id, unique: true, algorithm: :concurrently
remove_index :discourse_solved_solved_topics,
:topic_id,
algorithm: :concurrently,
if_exists: true
remove_index :discourse_solved_solved_topics,
:answer_post_id,
algorithm: :concurrently,
if_exists: true
add_index :discourse_solved_solved_topics, :topic_id, unique: true, algorithm: :concurrently
add_index :discourse_solved_solved_topics,
:answer_post_id,
unique: true,
algorithm: :concurrently
end
end

View File

@ -2,15 +2,15 @@
module DiscourseAssign
class EntryPoint
# TODO: These four plugin api usages should ideally be in the assign plugin, not the solved plugin.
# They have been moved here from plugin.rb as part of the custom fields migration.
def self.inject(plugin)
plugin.register_modifier(:assigns_reminder_assigned_topics_query) do |query|
next query if !SiteSetting.ignore_solved_topics_in_assigned_reminder
query.where.not(
id:
TopicCustomField.where(
name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
).pluck(:topic_id),
)
# TODO: this line was modified for the custom fields migration,
# but returning this huge array is not good at all.
query.where.not(id: DiscourseSolved::SolvedTopic.pluck(:topic_id))
end
plugin.register_modifier(:assigned_count_for_user_query) do |query, user|

View File

@ -40,10 +40,7 @@ class DiscourseSolved::BeforeHeadClose
},
}
if accepted_answer =
Post.find_by(
id: topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD],
)
if accepted_answer = topic.solved&.answer_post
question_json["answerCount"] = 1
question_json[:acceptedAnswer] = {
"@type" => "Answer",

View File

@ -4,24 +4,16 @@ module DiscourseSolved
class RegisterFilters
def self.register(plugin)
solved_callback = ->(scope) do
sql = <<~SQL
topics.id IN (
SELECT topic_id
FROM topic_custom_fields
WHERE name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
AND value IS NOT NULL
)
SQL
scope.where(sql).where("topics.archetype <> ?", Archetype.private_message)
scope.joins(
"INNER JOIN discourse_solved_solved_topics ON discourse_solved_solved_topics.topic_id = topics.id",
).where("topics.archetype <> ?", Archetype.private_message)
end
unsolved_callback = ->(scope) do
scope = scope.where <<~SQL
scope = scope.where(<<~SQL)
topics.id NOT IN (
SELECT topic_id
FROM topic_custom_fields
WHERE name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
AND value IS NOT NULL
FROM discourse_solved_solved_topics
)
SQL
@ -29,21 +21,21 @@ module DiscourseSolved
tag_ids = Tag.where(name: SiteSetting.enable_solved_tags.split("|")).pluck(:id)
scope = scope.where <<~SQL, tag_ids
topics.id IN (
SELECT t.id
FROM topics t
JOIN category_custom_fields cc
ON t.category_id = cc.category_id
AND cc.name = '#{::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD}'
AND cc.value = 'true'
)
OR
topics.id IN (
SELECT topic_id
FROM topic_tags
WHERE tag_id IN (?)
)
SQL
topics.id IN (
SELECT t.id
FROM topics t
JOIN category_custom_fields cc
ON t.category_id = cc.category_id
AND cc.name = '#{::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD}'
AND cc.value = 'true'
)
OR
topics.id IN (
SELECT topic_id
FROM topic_tags
WHERE tag_id IN (?)
)
SQL
end
scope.where("topics.archetype <> ?", Archetype.private_message)

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module DiscourseSolved::TopicExtension
extend ActiveSupport::Concern
prepended { has_one :solved, class_name: "DiscourseSolved::SolvedTopic", dependent: :destroy }
end

View File

@ -43,12 +43,6 @@ module DiscourseSolved::TopicViewSerializerExtension
end
def accepted_answer_post_id
id = object.topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD]
# a bit messy but race conditions can give us an array here, avoid
begin
id && id.to_i
rescue StandardError
nil
end
object.topic.solved&.answer_post_id
end
end

View File

@ -4,6 +4,6 @@ module DiscourseSolved::UserSummaryExtension
extend ActiveSupport::Concern
def solved_count
UserAction.where(user: @user).where(action_type: UserAction::SOLVED).count
DiscourseSolved::SolvedTopic.where(accepter: @user).count
end
end

194
plugin.rb
View File

@ -18,9 +18,11 @@ register_asset "stylesheets/mobile/solutions.scss", :mobile
module ::DiscourseSolved
PLUGIN_NAME = "discourse-solved"
ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD = "enable_accepted_answers"
# throw these
AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD = "solved_auto_close_topic_timer_id"
ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD = "accepted_answer_post_id"
ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD = "enable_accepted_answers"
IS_ACCEPTED_ANSWER_CUSTOM_FIELD = "is_accepted_answer"
end
@ -34,27 +36,25 @@ after_initialize do
topic ||= post.topic
DistributedMutex.synchronize("discourse_solved_toggle_answer_#{topic.id}") do
accepted_id = topic.custom_fields[ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].to_i
solved = topic.solved
if accepted_id > 0
if p2 = Post.find_by(id: accepted_id)
p2.custom_fields.delete(IS_ACCEPTED_ANSWER_CUSTOM_FIELD)
p2.save!
UserAction.where(action_type: UserAction::SOLVED, target_post_id: p2.id).destroy_all
end
if previous_accepted_post_id = solved&.answer_post_id
UserAction.where(
action_type: UserAction::SOLVED,
target_post_id: previous_accepted_post_id,
).destroy_all
else
UserAction.log_action!(
action_type: UserAction::SOLVED,
user_id: post.user_id,
acting_user_id: acting_user.id,
target_post_id: post.id,
target_topic_id: post.topic_id,
)
end
post.custom_fields[IS_ACCEPTED_ANSWER_CUSTOM_FIELD] = "true"
topic.custom_fields[ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD] = post.id
UserAction.log_action!(
action_type: UserAction::SOLVED,
user_id: post.user_id,
acting_user_id: acting_user.id,
target_post_id: post.id,
target_topic_id: post.topic_id,
)
solved ||=
DiscourseSolved::SolvedTopic.new(topic:, answer_post: post, accepter: acting_user)
notification_data = {
message: "solved.accepted_notification",
@ -99,14 +99,12 @@ after_initialize do
based_on_last_post: true,
duration_minutes: auto_close_hours * 60,
)
topic.custom_fields[AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD] = topic_timer.id
solved.topic_timer = topic_timer
MessageBus.publish("/topic/#{topic.id}", reload_topic: true)
end
topic.save!
post.save!
solved.save!
if WebHook.active_web_hooks(:accepted_solution).exists?
payload = WebHook.generate_payload(:post, post)
@ -120,41 +118,28 @@ after_initialize do
def self.unaccept_answer!(post, topic: nil)
topic ||= post.topic
topic ||= Topic.unscoped.find_by(id: post.topic_id)
return if topic.nil?
return if topic.solved.nil?
DistributedMutex.synchronize("discourse_solved_toggle_answer_#{topic.id}") do
post.custom_fields.delete(IS_ACCEPTED_ANSWER_CUSTOM_FIELD)
topic.custom_fields.delete(ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD)
solved = topic.solved
solved.topic_timer.destroy! if solved.topic_timer
if timer_id = topic.custom_fields[AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD]
topic_timer = TopicTimer.find_by(id: timer_id)
topic_timer.destroy! if topic_timer
topic.custom_fields.delete(AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD)
end
topic.save!
post.save!
# TODO remove_action! does not allow for this type of interface
UserAction.where(action_type: UserAction::SOLVED, target_post_id: post.id).destroy_all
# yank notification
notification =
Notification.find_by(
notification_type: Notification.types[:custom],
user_id: post.user_id,
topic_id: post.topic_id,
post_number: post.post_number,
)
notification.destroy! if notification
Notification.find_by(
notification_type: Notification.types[:custom],
user_id: post.user_id,
topic_id: post.topic_id,
post_number: post.post_number,
)&.destroy!
if WebHook.active_web_hooks(:unaccepted_solution).exists?
payload = WebHook.generate_payload(:post, post)
WebHook.enqueue_solved_hooks(:unaccepted_solution, post, payload)
end
solved.destroy!
DiscourseEvent.trigger(:unaccepted_solution, post)
end
end
@ -168,6 +153,7 @@ after_initialize do
::Guardian.prepend(DiscourseSolved::GuardianExtensions)
::WebHook.prepend(DiscourseSolved::WebHookExtension)
::TopicViewSerializer.prepend(DiscourseSolved::TopicViewSerializerExtension)
::Topic.prepend(DiscourseSolved::TopicExtension)
::Category.prepend(DiscourseSolved::CategoryExtension)
::PostSerializer.prepend(DiscourseSolved::PostSerializerExtension)
::UserSummary.prepend(DiscourseSolved::UserSummaryExtension)
@ -183,49 +169,15 @@ after_initialize do
].each { |klass| klass.include(DiscourseSolved::TopicAnswerMixin) }
end
# we got to do a one time upgrade
if !::DiscourseSolved.skip_db?
unless Discourse.redis.get("solved_already_upgraded")
unless UserAction.where(action_type: UserAction::SOLVED).exists?
Rails.logger.info("Upgrading storage for solved")
sql = <<~SQL
INSERT INTO user_actions(action_type,
user_id,
target_topic_id,
target_post_id,
acting_user_id,
created_at,
updated_at)
SELECT :solved,
p.user_id,
p.topic_id,
p.id,
t.user_id,
pc.created_at,
pc.updated_at
FROM
post_custom_fields pc
JOIN
posts p ON p.id = pc.post_id
JOIN
topics t ON t.id = p.topic_id
WHERE
pc.name = 'is_accepted_answer' AND
pc.value = 'true' AND
p.user_id IS NOT NULL
SQL
DB.exec(sql, solved: UserAction::SOLVED)
end
Discourse.redis.set("solved_already_upgraded", "true")
end
end
topic_view_post_custom_fields_allowlister { [::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] }
TopicList.preloaded_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
# TODO: Preload fields in
# - TopicList - answer_post_id
# - Search - answer_post_id
# - CategoryList - answer_post_id ?? for what
# topic_view_post_custom_fields_allowlister { [::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] }
# TopicList.preloaded_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
# Search.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
# CategoryList.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
Site.preloaded_category_custom_fields << ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD
Search.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
CategoryList.preloaded_topic_custom_fields << ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD
add_api_key_scope(
:solved,
@ -244,10 +196,10 @@ after_initialize do
report.data = []
accepted_solutions =
TopicCustomField
.joins(:topic)
.where("topics.archetype <> ?", Archetype.private_message)
.where(name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD)
DiscourseSolved::SolvedTopic.joins(:topic).where(
"topics.archetype <> ?",
Archetype.private_message,
)
category_id, include_subcategories = report.add_category_filter
if category_id
@ -263,17 +215,17 @@ after_initialize do
end
accepted_solutions
.where("topic_custom_fields.created_at >= ?", report.start_date)
.where("topic_custom_fields.created_at <= ?", report.end_date)
.group("DATE(topic_custom_fields.created_at)")
.order("DATE(topic_custom_fields.created_at)")
.where("discourse_solved_solved_topics.created_at >= ?", report.start_date)
.where("discourse_solved_solved_topics.created_at <= ?", report.end_date)
.group("DATE(discourse_solved_solved_topics.created_at)")
.order("DATE(discourse_solved_solved_topics.created_at)")
.count
.each { |date, count| report.data << { x: date, y: count } }
report.total = accepted_solutions.count
report.prev30Days =
accepted_solutions
.where("topic_custom_fields.created_at >= ?", report.start_date - 30.days)
.where("topic_custom_fields.created_at <= ?", report.start_date)
.where("discourse_solved_solved_topics.created_at >= ?", report.start_date - 30.days)
.where("discourse_solved_solved_topics.created_at <= ?", report.start_date)
.count
end
@ -282,10 +234,8 @@ after_initialize do
condition = <<~SQL
EXISTS (
SELECT 1
FROM topic_custom_fields
WHERE topic_id = topics.id
AND name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
AND value IS NOT NULL
FROM discourse_solved_solved_topics
WHERE discourse_solved_solved_topics.topic_id = topics.id
)
SQL
@ -315,18 +265,10 @@ after_initialize do
add_to_serializer(:post, :can_unaccept_answer) do
scope.can_accept_answer?(topic, object) && accepted_answer
end
add_to_serializer(:post, :accepted_answer) do
post_custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
end
add_to_serializer(:post, :topic_accepted_answer) do
topic&.custom_fields&.[](::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD).present?
end
add_to_serializer(:post, :accepted_answer) { topic&.solved&.answer_post_id == object.id }
add_to_serializer(:post, :topic_accepted_answer) { topic&.solved&.present? }
on(:post_destroyed) do |post|
if post.custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
::DiscourseSolved.unaccept_answer!(post)
end
end
on(:post_destroyed) { |post| ::DiscourseSolved.unaccept_answer!(post) }
on(:filter_auto_bump_topics) do |_category, filters|
filters.push(
@ -334,10 +276,8 @@ after_initialize do
sql = <<~SQL
NOT EXISTS (
SELECT 1
FROM topic_custom_fields
WHERE topic_id = topics.id
AND name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
AND value IS NOT NULL
FROM discourse_solved_solved_topics
WHERE discourse_solved_solved_topics.topic_id = topics.id
)
SQL
@ -390,7 +330,7 @@ after_initialize do
add_to_class(:composer_messages_finder, :check_topic_is_solved) do
return if !SiteSetting.solved_enabled || SiteSetting.disable_solved_education_message
return if !replying? || @topic.blank? || @topic.private_message?
return if @topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].blank?
return if @topic.solved.nil?
{
id: "solved_topic",
@ -402,15 +342,17 @@ after_initialize do
}
end
register_topic_list_preload_user_ids do |topics, user_ids, topic_list|
answer_post_ids =
TopicCustomField
.select("value::INTEGER")
.where(name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD)
register_topic_list_preload_user_ids do |topics, user_ids|
# [{ topic_id => answer_user_id }, ... ]
topics_with_answer_poster =
DiscourseSolved::SolvedTopic
.joins(:answer_post)
.where(topic_id: topics.map(&:id))
answer_user_ids = Post.where(id: answer_post_ids).pluck(:topic_id, :user_id).to_h
topics.each { |topic| topic.accepted_answer_user_id = answer_user_ids[topic.id] }
user_ids.concat(answer_user_ids.values)
.pluck(:topic_id, "posts.user_id")
.to_h
topics.each { |topic| topic.accepted_answer_user_id = topics_with_answer_poster[topic.id] }
user_ids.concat(topics_with_answer_poster.values)
end
DiscourseSolved::RegisterFilters.register(self)

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
Fabricator(:solved_topic, from: DiscourseSolved::SolvedTopic) do
topic
answer_post { Fabricate(:post) }
accepter { Fabricate(:user) }
end

View File

@ -30,31 +30,21 @@ RSpec.describe "Managing Posts solved status" do
fab!(:solvable_tag) { Fabricate(:tag) }
fab!(:solved_in_category) do
Fabricate(
:custom_topic,
category: solvable_category,
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
value: "42",
)
topic = Fabricate(:topic, category: solvable_category)
Fabricate(:solved_topic, topic:, answer_post: Fabricate(:post, topic:))
topic
end
fab!(:solved_in_tag) do
Fabricate(
:custom_topic,
tags: [solvable_tag],
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
value: "42",
)
topic = Fabricate(:topic, tags: [solvable_tag])
Fabricate(:solved_topic, topic:, answer_post: Fabricate(:post, topic:))
topic
end
fab!(:solved_pm) do
Fabricate(
:custom_topic,
category_id: nil,
archetype: Archetype.private_message,
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
value: "42",
)
topic = Fabricate(:topic, archetype: Archetype.private_message, category_id: nil)
Fabricate(:solved_topic, topic:, answer_post: Fabricate(:post, topic:))
topic
end
fab!(:unsolved_in_category) { Fabricate(:topic, category: solvable_category) }
@ -136,39 +126,15 @@ RSpec.describe "Managing Posts solved status" do
category
end
fab!(:tag)
fab!(:topic_unsolved) do
Fabricate(
:custom_topic,
user: user,
category: category_enabled,
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
)
end
fab!(:topic_unsolved) { Fabricate(:topic, user:, category: category_enabled) }
fab!(:topic_unsolved_2) { Fabricate(:topic, user: user, tags: [tag]) }
fab!(:topic_solved) do
Fabricate(
:custom_topic,
user: user,
category: category_enabled,
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
)
end
fab!(:topic_disabled_1) do
Fabricate(
:custom_topic,
user: user,
category: category_disabled,
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
)
end
fab!(:topic_disabled_2) do
Fabricate(
:custom_topic,
user: user,
category: category_disabled,
custom_topic_name: "another_custom_field",
)
topic = Fabricate(:topic, category: category_enabled)
Fabricate(:solved_topic, topic:, answer_post: Fabricate(:post, topic:))
topic
end
fab!(:topic_disabled_1) { Fabricate(:topic, category: category_disabled) }
fab!(:topic_disabled_2) { Fabricate(:topic, category: category_disabled) }
fab!(:post_unsolved) { Fabricate(:post, topic: topic_unsolved) }
fab!(:post_unsolved_2) { Fabricate(:post, topic: topic_unsolved_2) }
fab!(:post_solved) do
@ -258,18 +224,14 @@ RSpec.describe "Managing Posts solved status" do
post "/solution/accept.json", params: { id: p1.id }
expect(response.status).to eq(200)
expect(p1.reload.custom_fields["is_accepted_answer"]).to eq("true")
expect(topic.solved.answer_post_id).to eq(p1.id)
topic.reload
expect(topic.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
expect(topic.custom_fields["solved_auto_close_topic_timer_id"].to_i).to eq(
topic.public_topic_timer.id,
)
expect(topic.solved.topic_timer).to eq(topic.public_topic_timer)
expect(topic.public_topic_timer.execute_at).to eq_time(Time.zone.now + 2.hours)
expect(topic.public_topic_timer.based_on_last_post).to eq(true)
end
@ -284,18 +246,14 @@ RSpec.describe "Managing Posts solved status" do
post "/solution/accept.json", params: { id: post_2.id }
expect(response.status).to eq(200)
expect(post_2.reload.custom_fields["is_accepted_answer"]).to eq("true")
expect(topic_2.solved.answer_post_id).to eq(post_2.id)
topic_2.reload
expect(topic_2.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
expect(topic_2.custom_fields["solved_auto_close_topic_timer_id"].to_i).to eq(
topic_2.public_topic_timer.id,
)
expect(topic_2.solved.topic_timer).to eq(topic_2.public_topic_timer)
expect(topic_2.public_topic_timer.execute_at).to eq_time(Time.zone.now + 4.hours)
expect(topic_2.public_topic_timer.based_on_last_post).to eq(true)
end
@ -332,7 +290,7 @@ RSpec.describe "Managing Posts solved status" do
p1.reload
topic.reload
expect(p1.custom_fields["is_accepted_answer"]).to eq("true")
expect(topic.solved.answer_post_id).to eq(p1.id)
expect(topic.public_topic_timer).to eq(nil)
expect(topic.closed).to eq(true)
end
@ -348,7 +306,7 @@ RSpec.describe "Managing Posts solved status" do
expect(response.status).to eq(200)
p1.reload
expect(p1.custom_fields["is_accepted_answer"]).to eq("true")
expect(topic.solved.answer_post_id).to eq(p1.id)
end
it "removes the solution when the post is deleted" do
@ -357,15 +315,12 @@ RSpec.describe "Managing Posts solved status" do
post "/solution/accept.json", params: { id: reply.id }
expect(response.status).to eq(200)
reply.reload
expect(reply.custom_fields["is_accepted_answer"]).to eq("true")
expect(reply.topic.custom_fields["accepted_answer_post_id"].to_i).to eq(reply.id)
expect(topic.solved.answer_post_id).to eq(reply.id)
PostDestroyer.new(Discourse.system_user, reply).destroy
reply.topic.reload
reply.reload
expect(reply.custom_fields["is_accepted_answer"]).to eq(nil)
expect(reply.topic.custom_fields["accepted_answer_post_id"]).to eq(nil)
expect(topic.solved).to be(nil)
end
it "does not allow you to accept a whisper" do
@ -395,6 +350,7 @@ RSpec.describe "Managing Posts solved status" do
before do
SiteSetting.solved_topics_auto_close_hours = 2
DiscourseSolved.accept_answer!(p1, user)
topic.reload
end
it "should unmark the post as solved" do
@ -405,12 +361,13 @@ RSpec.describe "Managing Posts solved status" do
expect(response.status).to eq(200)
p1.reload
expect(p1.custom_fields["is_accepted_answer"]).to eq(nil)
expect(p1.topic.custom_fields["accepted_answer_post_id"]).to eq(nil)
expect(topic.solved).to be(nil)
end
end
it "triggers a webhook" do
DiscourseSolved.accept_answer!(p1, user)
Fabricate(:solved_web_hook)
post "/solution/unaccept.json", params: { id: p1.id }
@ -467,7 +424,7 @@ RSpec.describe "Managing Posts solved status" do
expect(p1.topic.assignment.status).to eq("New")
DiscourseSolved.accept_answer!(p1, user)
expect(p1.reload.custom_fields["is_accepted_answer"]).to eq("true")
expect(topic.solved.answer_post_id).to eq(p1.id)
expect(p1.topic.assignment.reload.status).to eq("Done")
end
@ -482,7 +439,6 @@ RSpec.describe "Managing Posts solved status" do
DiscourseSolved.unaccept_answer!(p1)
expect(p1.reload.custom_fields["is_accepted_answer"]).to eq(nil)
expect(p1.reload.topic.assignment.reload.status).to eq("New")
end

View File

@ -45,10 +45,7 @@ RSpec.describe TopicsController do
expect(response.body).to include(schema_json(0))
p2.custom_fields["is_accepted_answer"] = true
p2.save_custom_fields
topic.custom_fields["accepted_answer_post_id"] = p2.id
topic.save_custom_fields
Fabricate(:solved_topic, topic: topic, answer_post: p2)
get "/t/#{topic.slug}/#{topic.id}"
@ -68,10 +65,7 @@ RSpec.describe TopicsController do
it "should include user name in output with the corresponding site setting" do
SiteSetting.display_name_on_posts = true
p2.custom_fields["is_accepted_answer"] = true
p2.save_custom_fields
topic.custom_fields["accepted_answer_post_id"] = p2.id
topic.save_custom_fields
Fabricate(:solved_topic, topic: topic, answer_post: p2)
get "/t/#{topic.slug}/#{topic.id}.json"
@ -87,10 +81,7 @@ RSpec.describe TopicsController do
it "should not include user name when site setting is disabled" do
SiteSetting.display_name_on_posts = false
p2.custom_fields["is_accepted_answer"] = true
p2.save_custom_fields
topic.custom_fields["accepted_answer_post_id"] = p2.id
topic.save_custom_fields
Fabricate(:solved_topic, topic: topic, answer_post: p2)
get "/t/#{topic.slug}/#{topic.id}.json"
@ -106,10 +97,7 @@ RSpec.describe TopicsController do
it "includes the correct schema information" do
DiscourseTagging.add_or_create_tags_by_name(topic, [tag.name])
p2.custom_fields["is_accepted_answer"] = true
p2.save_custom_fields
topic.custom_fields["accepted_answer_post_id"] = p2.id
topic.save_custom_fields
Fabricate(:solved_topic, topic: topic, answer_post: p2)
get "/t/#{topic.slug}/#{topic.id}"
@ -120,10 +108,7 @@ RSpec.describe TopicsController do
another_tag = Fabricate(:tag)
DiscourseTagging.add_or_create_tags_by_name(topic, [another_tag.name])
p2.custom_fields["is_accepted_answer"] = true
p2.save_custom_fields
topic.custom_fields["accepted_answer_post_id"] = p2.id
topic.save_custom_fields
Fabricate(:solved_topic, topic: topic, answer_post: p2)
get "/t/#{topic.slug}/#{topic.id}"

View File

@ -7,10 +7,7 @@ describe DiscourseSolved::TopicAnswerMixin do
let(:post) { Fabricate(:post, topic: topic) }
let(:guardian) { Guardian.new }
before do
topic.custom_fields["accepted_answer_post_id"] = post.id
topic.save_custom_fields
end
before { Fabricate(:solved_topic, topic: topic, answer_post: post) }
it "should have true for `has_accepted_answer` field in each serializer" do
[

View File

@ -12,6 +12,7 @@ describe UserCardSerializer do
post1 = Fabricate(:post, user: user)
DiscourseSolved.accept_answer!(post1, Discourse.system_user)
post1.topic.reload
expect(serializer.as_json[:accepted_answers]).to eq(1)
post2 = Fabricate(:post, user: user)