2019-05-12 23:04:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2015-06-15 00:24:49 -04:00
|
|
|
# name: discourse-solved
|
2023-11-08 14:29:38 -05:00
|
|
|
# about: Allows users to accept solutions on topics in designated categories.
|
|
|
|
# meta_topic_id: 30155
|
2015-05-19 01:45:19 -04:00
|
|
|
# version: 0.1
|
|
|
|
# authors: Sam Saffron
|
2017-04-26 00:40:03 -04:00
|
|
|
# url: https://github.com/discourse/discourse-solved
|
2015-05-19 01:45:19 -04:00
|
|
|
|
2017-02-02 12:20:01 -05:00
|
|
|
enabled_site_setting :solved_enabled
|
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
register_svg_icon "far fa-check-square"
|
|
|
|
register_svg_icon "check-square"
|
|
|
|
register_svg_icon "far fa-square"
|
2018-11-07 21:08:46 -05:00
|
|
|
|
2022-12-23 15:36:08 -05:00
|
|
|
register_asset "stylesheets/solutions.scss"
|
|
|
|
register_asset "stylesheets/mobile/solutions.scss", :mobile
|
2015-05-19 01:45:19 -04:00
|
|
|
|
|
|
|
after_initialize do
|
2015-06-15 00:24:49 -04:00
|
|
|
module ::DiscourseSolved
|
2024-03-05 06:14:32 -05:00
|
|
|
PLUGIN_NAME = "discourse-solved"
|
|
|
|
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"
|
|
|
|
|
2015-05-19 01:45:19 -04:00
|
|
|
class Engine < ::Rails::Engine
|
|
|
|
engine_name PLUGIN_NAME
|
2015-06-15 00:24:49 -04:00
|
|
|
isolate_namespace DiscourseSolved
|
2015-05-19 01:45:19 -04:00
|
|
|
end
|
2024-03-05 06:14:32 -05:00
|
|
|
end
|
2015-08-05 02:21:16 -04:00
|
|
|
|
2024-03-05 06:14:32 -05:00
|
|
|
SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s
|
|
|
|
|
|
|
|
require_relative "app/controllers/answer_controller"
|
|
|
|
require_relative "app/lib/first_accepted_post_solution_validator"
|
|
|
|
require_relative "app/lib/accepted_answer_cache"
|
|
|
|
require_relative "app/lib/guardian_extensions"
|
|
|
|
require_relative "app/lib/before_head_close"
|
|
|
|
require_relative "app/lib/category_extension"
|
|
|
|
require_relative "app/lib/post_serializer_extension"
|
|
|
|
require_relative "app/lib/topic_posters_summary_extension"
|
|
|
|
require_relative "app/lib/topic_view_serializer_extension"
|
|
|
|
require_relative "app/lib/user_summary_extension"
|
|
|
|
require_relative "app/lib/web_hook_extension"
|
|
|
|
require_relative "app/serializers/concerns/topic_answer_mixin"
|
2018-08-31 00:03:42 -04:00
|
|
|
|
2024-04-26 12:21:09 -04:00
|
|
|
require_relative "app/lib/plugin_initializers/assigned_reminder_exclude_solved"
|
|
|
|
DiscourseSolved::AssignsReminderForTopicsQuery.new(self).apply_plugin_api
|
2024-10-30 10:16:46 -04:00
|
|
|
DiscourseSolved::AssignedCountForUserQuery.new(self).apply_plugin_api
|
2024-03-05 06:14:32 -05:00
|
|
|
module ::DiscourseSolved
|
2019-03-18 11:27:29 -04:00
|
|
|
def self.accept_answer!(post, acting_user, topic: nil)
|
|
|
|
topic ||= post.topic
|
2015-05-19 01:45:19 -04:00
|
|
|
|
2020-08-17 10:33:24 -04:00
|
|
|
DistributedMutex.synchronize("discourse_solved_toggle_answer_#{topic.id}") do
|
2023-05-02 21:56:10 -04:00
|
|
|
accepted_id = topic.custom_fields[ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].to_i
|
2015-05-19 01:45:19 -04:00
|
|
|
|
2020-08-17 10:33:24 -04:00
|
|
|
if accepted_id > 0
|
|
|
|
if p2 = Post.find_by(id: accepted_id)
|
2023-10-13 13:06:03 -04:00
|
|
|
p2.custom_fields.delete(IS_ACCEPTED_ANSWER_CUSTOM_FIELD)
|
2020-08-17 10:33:24 -04:00
|
|
|
p2.save!
|
2016-12-06 01:28:15 -05:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
UserAction.where(action_type: UserAction::SOLVED, target_post_id: p2.id).destroy_all
|
2020-08-17 10:33:24 -04:00
|
|
|
end
|
|
|
|
end
|
2015-06-15 00:24:49 -04:00
|
|
|
|
2023-10-13 13:06:03 -04:00
|
|
|
post.custom_fields[IS_ACCEPTED_ANSWER_CUSTOM_FIELD] = "true"
|
2023-05-02 21:56:10 -04:00
|
|
|
topic.custom_fields[ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD] = post.id
|
2018-08-31 00:03:42 -04:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
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,
|
|
|
|
)
|
2020-08-17 10:33:24 -04:00
|
|
|
|
2020-10-30 12:31:27 -04:00
|
|
|
notification_data = {
|
2022-12-23 15:36:08 -05:00
|
|
|
message: "solved.accepted_notification",
|
2020-10-30 12:31:27 -04:00
|
|
|
display_username: acting_user.username,
|
2022-12-23 15:36:08 -05:00
|
|
|
topic_title: topic.title,
|
2023-02-15 08:30:33 -05:00
|
|
|
title: "solved.notification.title",
|
2020-10-30 12:31:27 -04:00
|
|
|
}.to_json
|
|
|
|
|
2020-08-17 10:33:24 -04:00
|
|
|
unless acting_user.id == post.user_id
|
|
|
|
Notification.create!(
|
|
|
|
notification_type: Notification.types[:custom],
|
|
|
|
user_id: post.user_id,
|
|
|
|
topic_id: post.topic_id,
|
|
|
|
post_number: post.post_number,
|
2022-12-23 15:36:08 -05:00
|
|
|
data: notification_data,
|
2020-10-30 12:31:27 -04:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
if SiteSetting.notify_on_staff_accept_solved && acting_user.id != topic.user_id
|
|
|
|
Notification.create!(
|
|
|
|
notification_type: Notification.types[:custom],
|
|
|
|
user_id: topic.user_id,
|
|
|
|
topic_id: post.topic_id,
|
|
|
|
post_number: post.post_number,
|
2022-12-23 15:36:08 -05:00
|
|
|
data: notification_data,
|
2020-04-26 23:01:01 -04:00
|
|
|
)
|
|
|
|
end
|
2017-01-20 09:13:58 -05:00
|
|
|
|
2023-04-19 10:14:33 -04:00
|
|
|
auto_close_hours = 0
|
|
|
|
if topic&.category.present?
|
|
|
|
auto_close_hours = topic.category.custom_fields["solved_topics_auto_close_hours"].to_i
|
|
|
|
auto_close_hours = 175_200 if auto_close_hours > 175_200 # 20 years
|
|
|
|
end
|
|
|
|
|
|
|
|
auto_close_hours = SiteSetting.solved_topics_auto_close_hours if auto_close_hours == 0
|
2020-08-17 10:33:24 -04:00
|
|
|
|
|
|
|
if (auto_close_hours > 0) && !topic.closed
|
2022-12-23 15:36:08 -05:00
|
|
|
topic_timer =
|
|
|
|
topic.set_or_create_timer(
|
|
|
|
TopicTimer.types[:silent_close],
|
|
|
|
nil,
|
|
|
|
based_on_last_post: true,
|
|
|
|
duration_minutes: auto_close_hours * 60,
|
|
|
|
)
|
2018-08-31 00:03:42 -04:00
|
|
|
|
2022-12-23 15:36:08 -05:00
|
|
|
topic.custom_fields[AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD] = topic_timer.id
|
2017-05-22 05:09:40 -04:00
|
|
|
|
2020-08-17 10:33:24 -04:00
|
|
|
MessageBus.publish("/topic/#{topic.id}", reload_topic: true)
|
|
|
|
end
|
2018-08-31 00:03:42 -04:00
|
|
|
|
2020-08-17 10:33:24 -04:00
|
|
|
topic.save!
|
|
|
|
post.save!
|
FEATURE: Publish WebHook event when solving/unsolving (#85)
* FEATURE: Publish WebHook event when solving/unsolving
This feature will publish a post edit webhook event whenever a solution
is accepted or unaccepted.
I went ahead and used the existing post-edit webhook because all the
post custom fields for the solved plugin are already included in the
post-edit serializer.
* Create Solved Event Webhook
This commit adds a solved event webhook that will only trigger when an
answer has been marked as accepted or unaccepted.
It uses 100 as the webhook ID. This way any new webhooks in core can
keep using lower numbers like 11, 12, 13, but plugins can use 101, 102,
etc.
* Removed functionality that was added to core
This [PR][1] to discourse core adds what what removed in this commit.
It is better to have this logic in core so that it is discoverable and
future webhooks won't end up accidentally using the same ID.
[1]: https://github.com/discourse/discourse/pull/9110
* UX: Add "solved" status filter in advanced search page.
And rename `in:solved` to `status:solved`.
* FEATURE: Publish WebHook event when solving/unsolving
This feature will publish a post edit webhook event whenever a solution
is accepted or unaccepted.
I went ahead and used the existing post-edit webhook because all the
post custom fields for the solved plugin are already included in the
post-edit serializer.
* Create Solved Event Webhook
This commit adds a solved event webhook that will only trigger when an
answer has been marked as accepted or unaccepted.
It uses 100 as the webhook ID. This way any new webhooks in core can
keep using lower numbers like 11, 12, 13, but plugins can use 101, 102,
etc.
* Removed functionality that was added to core
This [PR][1] to discourse core adds what what removed in this commit.
It is better to have this logic in core so that it is discoverable and
future webhooks won't end up accidentally using the same ID.
[1]: https://github.com/discourse/discourse/pull/9110
Co-authored-by: Vinoth Kannan <vinothkannan@vinkas.com>
2020-03-06 13:28:29 -05:00
|
|
|
|
2023-10-08 23:26:29 -04:00
|
|
|
if WebHook.active_web_hooks(:accepted_solution).exists?
|
2020-08-17 10:33:24 -04:00
|
|
|
payload = WebHook.generate_payload(:post, post)
|
|
|
|
WebHook.enqueue_solved_hooks(:accepted_solution, post, payload)
|
|
|
|
end
|
|
|
|
|
|
|
|
DiscourseEvent.trigger(:accepted_solution, post)
|
|
|
|
end
|
2015-05-19 01:45:19 -04:00
|
|
|
end
|
|
|
|
|
2019-03-18 11:27:29 -04:00
|
|
|
def self.unaccept_answer!(post, topic: nil)
|
|
|
|
topic ||= post.topic
|
2024-06-17 06:41:13 -04:00
|
|
|
topic ||= Topic.unscoped.find_by(id: post.topic_id)
|
|
|
|
|
|
|
|
return if topic.nil?
|
2018-08-31 00:03:42 -04:00
|
|
|
|
2020-08-17 10:33:24 -04:00
|
|
|
DistributedMutex.synchronize("discourse_solved_toggle_answer_#{topic.id}") do
|
2023-10-13 13:06:03 -04:00
|
|
|
post.custom_fields.delete(IS_ACCEPTED_ANSWER_CUSTOM_FIELD)
|
2023-05-02 21:56:10 -04:00
|
|
|
topic.custom_fields.delete(ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD)
|
2018-08-31 00:03:42 -04:00
|
|
|
|
2020-08-17 10:33:24 -04:00
|
|
|
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
|
2021-05-05 06:19:00 -04:00
|
|
|
topic.custom_fields.delete(AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD)
|
2020-08-17 10:33:24 -04:00
|
|
|
end
|
2015-05-19 01:45:19 -04:00
|
|
|
|
2020-08-17 10:33:24 -04:00
|
|
|
topic.save!
|
|
|
|
post.save!
|
|
|
|
|
|
|
|
# TODO remove_action! does not allow for this type of interface
|
2024-04-26 10:17:50 -04:00
|
|
|
UserAction.where(action_type: UserAction::SOLVED, target_post_id: post.id).destroy_all
|
2020-08-17 10:33:24 -04:00
|
|
|
|
|
|
|
# yank notification
|
2022-12-23 15:36:08 -05:00
|
|
|
notification =
|
|
|
|
Notification.find_by(
|
|
|
|
notification_type: Notification.types[:custom],
|
|
|
|
user_id: post.user_id,
|
|
|
|
topic_id: post.topic_id,
|
|
|
|
post_number: post.post_number,
|
|
|
|
)
|
2016-12-06 01:28:15 -05:00
|
|
|
|
2020-08-17 10:33:24 -04:00
|
|
|
notification.destroy! if notification
|
2015-06-21 20:06:18 -04:00
|
|
|
|
2023-10-08 23:26:29 -04:00
|
|
|
if WebHook.active_web_hooks(:unaccepted_solution).exists?
|
2020-08-17 10:33:24 -04:00
|
|
|
payload = WebHook.generate_payload(:post, post)
|
|
|
|
WebHook.enqueue_solved_hooks(:unaccepted_solution, post, payload)
|
|
|
|
end
|
FEATURE: Publish WebHook event when solving/unsolving (#85)
* FEATURE: Publish WebHook event when solving/unsolving
This feature will publish a post edit webhook event whenever a solution
is accepted or unaccepted.
I went ahead and used the existing post-edit webhook because all the
post custom fields for the solved plugin are already included in the
post-edit serializer.
* Create Solved Event Webhook
This commit adds a solved event webhook that will only trigger when an
answer has been marked as accepted or unaccepted.
It uses 100 as the webhook ID. This way any new webhooks in core can
keep using lower numbers like 11, 12, 13, but plugins can use 101, 102,
etc.
* Removed functionality that was added to core
This [PR][1] to discourse core adds what what removed in this commit.
It is better to have this logic in core so that it is discoverable and
future webhooks won't end up accidentally using the same ID.
[1]: https://github.com/discourse/discourse/pull/9110
* UX: Add "solved" status filter in advanced search page.
And rename `in:solved` to `status:solved`.
* FEATURE: Publish WebHook event when solving/unsolving
This feature will publish a post edit webhook event whenever a solution
is accepted or unaccepted.
I went ahead and used the existing post-edit webhook because all the
post custom fields for the solved plugin are already included in the
post-edit serializer.
* Create Solved Event Webhook
This commit adds a solved event webhook that will only trigger when an
answer has been marked as accepted or unaccepted.
It uses 100 as the webhook ID. This way any new webhooks in core can
keep using lower numbers like 11, 12, 13, but plugins can use 101, 102,
etc.
* Removed functionality that was added to core
This [PR][1] to discourse core adds what what removed in this commit.
It is better to have this logic in core so that it is discoverable and
future webhooks won't end up accidentally using the same ID.
[1]: https://github.com/discourse/discourse/pull/9110
Co-authored-by: Vinoth Kannan <vinothkannan@vinkas.com>
2020-03-06 13:28:29 -05:00
|
|
|
|
2020-08-17 10:33:24 -04:00
|
|
|
DiscourseEvent.trigger(:unaccepted_solution, post)
|
FEATURE: Publish WebHook event when solving/unsolving (#85)
* FEATURE: Publish WebHook event when solving/unsolving
This feature will publish a post edit webhook event whenever a solution
is accepted or unaccepted.
I went ahead and used the existing post-edit webhook because all the
post custom fields for the solved plugin are already included in the
post-edit serializer.
* Create Solved Event Webhook
This commit adds a solved event webhook that will only trigger when an
answer has been marked as accepted or unaccepted.
It uses 100 as the webhook ID. This way any new webhooks in core can
keep using lower numbers like 11, 12, 13, but plugins can use 101, 102,
etc.
* Removed functionality that was added to core
This [PR][1] to discourse core adds what what removed in this commit.
It is better to have this logic in core so that it is discoverable and
future webhooks won't end up accidentally using the same ID.
[1]: https://github.com/discourse/discourse/pull/9110
* UX: Add "solved" status filter in advanced search page.
And rename `in:solved` to `status:solved`.
* FEATURE: Publish WebHook event when solving/unsolving
This feature will publish a post edit webhook event whenever a solution
is accepted or unaccepted.
I went ahead and used the existing post-edit webhook because all the
post custom fields for the solved plugin are already included in the
post-edit serializer.
* Create Solved Event Webhook
This commit adds a solved event webhook that will only trigger when an
answer has been marked as accepted or unaccepted.
It uses 100 as the webhook ID. This way any new webhooks in core can
keep using lower numbers like 11, 12, 13, but plugins can use 101, 102,
etc.
* Removed functionality that was added to core
This [PR][1] to discourse core adds what what removed in this commit.
It is better to have this logic in core so that it is discoverable and
future webhooks won't end up accidentally using the same ID.
[1]: https://github.com/discourse/discourse/pull/9110
Co-authored-by: Vinoth Kannan <vinothkannan@vinkas.com>
2020-03-06 13:28:29 -05:00
|
|
|
end
|
2018-07-17 20:56:56 -04:00
|
|
|
end
|
2019-03-18 11:27:29 -04:00
|
|
|
|
2024-03-05 06:14:32 -05:00
|
|
|
def self.skip_db?
|
|
|
|
defined?(GlobalSetting.skip_db?) && GlobalSetting.skip_db?
|
2018-07-17 20:56:56 -04:00
|
|
|
end
|
2024-03-05 06:14:32 -05:00
|
|
|
end
|
2018-07-17 20:56:56 -04:00
|
|
|
|
2024-03-05 06:14:32 -05:00
|
|
|
reloadable_patch do |plugin|
|
|
|
|
::Guardian.prepend(DiscourseSolved::GuardianExtensions)
|
|
|
|
::WebHook.prepend(DiscourseSolved::WebHookExtension)
|
|
|
|
::TopicViewSerializer.prepend(DiscourseSolved::TopicViewSerializerExtension)
|
|
|
|
::Category.prepend(DiscourseSolved::CategoryExtension)
|
|
|
|
::PostSerializer.prepend(DiscourseSolved::PostSerializerExtension)
|
2024-04-26 10:17:50 -04:00
|
|
|
::UserSummary.prepend(DiscourseSolved::UserSummaryExtension)
|
|
|
|
::Topic.attr_accessor(:accepted_answer_user_id)
|
|
|
|
::TopicPostersSummary.alias_method(:old_user_ids, :user_ids)
|
|
|
|
::TopicPostersSummary.prepend(DiscourseSolved::TopicPostersSummaryExtension)
|
2024-03-05 06:14:32 -05:00
|
|
|
[
|
|
|
|
::TopicListItemSerializer,
|
|
|
|
::SearchTopicListItemSerializer,
|
|
|
|
::SuggestedTopicSerializer,
|
|
|
|
::UserSummarySerializer::TopicSerializer,
|
|
|
|
::ListableTopicSerializer,
|
|
|
|
].each { |klass| klass.include(TopicAnswerMixin) }
|
|
|
|
end
|
2015-08-05 02:21:16 -04:00
|
|
|
|
2024-03-05 06:14:32 -05:00
|
|
|
# we got to do a one time upgrade
|
2024-04-26 10:17:50 -04:00
|
|
|
if !::DiscourseSolved.skip_db?
|
2024-03-05 06:14:32 -05:00
|
|
|
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")
|
2015-08-05 02:21:16 -04:00
|
|
|
end
|
2015-05-19 01:45:19 -04:00
|
|
|
end
|
|
|
|
|
2015-06-15 00:24:49 -04:00
|
|
|
DiscourseSolved::Engine.routes.draw do
|
2015-05-19 01:45:19 -04:00
|
|
|
post "/accept" => "answer#accept"
|
|
|
|
post "/unaccept" => "answer#unaccept"
|
|
|
|
end
|
|
|
|
|
2022-12-23 15:36:08 -05:00
|
|
|
Discourse::Application.routes.append { mount ::DiscourseSolved::Engine, at: "solution" }
|
2015-05-19 01:45:19 -04:00
|
|
|
|
2023-10-13 13:06:03 -04:00
|
|
|
on(:post_destroyed) do |post|
|
|
|
|
if post.custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
|
|
|
|
::DiscourseSolved.unaccept_answer!(post)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-02-23 17:00:14 -05:00
|
|
|
add_api_key_scope(
|
|
|
|
:solved,
|
|
|
|
{ answer: { actions: %w[discourse_solved/answer#accept discourse_solved/answer#unaccept] } },
|
|
|
|
)
|
|
|
|
|
2023-10-13 13:06:03 -04:00
|
|
|
topic_view_post_custom_fields_allowlister { [::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] }
|
2015-05-19 01:45:19 -04:00
|
|
|
|
2022-12-23 15:36:08 -05:00
|
|
|
register_html_builder("server:before-head-close-crawler") do |controller|
|
2024-03-05 06:14:32 -05:00
|
|
|
DiscourseSolved::BeforeHeadClose.new(controller).html
|
2018-11-01 18:38:16 -04:00
|
|
|
end
|
|
|
|
|
2022-12-23 15:36:08 -05:00
|
|
|
register_html_builder("server:before-head-close") do |controller|
|
2024-03-05 06:14:32 -05:00
|
|
|
DiscourseSolved::BeforeHeadClose.new(controller).html
|
2018-11-01 18:38:16 -04:00
|
|
|
end
|
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
Report.add_report("accepted_solutions") do |report|
|
|
|
|
report.data = []
|
2019-05-01 16:24:09 -04:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
accepted_solutions =
|
|
|
|
TopicCustomField.where(name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD)
|
2019-05-01 16:24:09 -04:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
category_id, include_subcategories = report.add_category_filter
|
|
|
|
if category_id
|
|
|
|
if include_subcategories
|
|
|
|
accepted_solutions =
|
|
|
|
accepted_solutions.joins(:topic).where(
|
|
|
|
"topics.category_id IN (?)",
|
|
|
|
Category.subcategory_ids(category_id),
|
|
|
|
)
|
|
|
|
else
|
|
|
|
accepted_solutions =
|
|
|
|
accepted_solutions.joins(:topic).where("topics.category_id = ?", category_id)
|
2019-05-01 16:24:09 -04:00
|
|
|
end
|
2024-04-26 10:17:50 -04:00
|
|
|
end
|
2019-05-01 16:24:09 -04:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
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)")
|
|
|
|
.count
|
|
|
|
.each { |date, count| report.data << { x: date, y: count } }
|
|
|
|
report.total = accepted_solutions.count
|
|
|
|
report.prev30Days =
|
2022-12-23 15:36:08 -05:00
|
|
|
accepted_solutions
|
2024-04-26 10:17:50 -04:00
|
|
|
.where("topic_custom_fields.created_at >= ?", report.start_date - 30.days)
|
|
|
|
.where("topic_custom_fields.created_at <= ?", report.start_date)
|
2017-08-02 02:09:07 -04:00
|
|
|
.count
|
2015-06-24 20:41:24 -04:00
|
|
|
end
|
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
register_modifier(:search_rank_sort_priorities) do |priorities, _search|
|
|
|
|
if SiteSetting.prioritize_solved_topics_in_search
|
|
|
|
condition = <<~SQL
|
|
|
|
EXISTS (
|
2024-05-08 14:17:45 -04:00
|
|
|
SELECT 1
|
2024-04-26 10:17:50 -04:00
|
|
|
FROM topic_custom_fields
|
|
|
|
WHERE topic_id = topics.id
|
|
|
|
AND name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
|
|
|
|
AND value IS NOT NULL
|
|
|
|
)
|
2023-05-02 21:56:10 -04:00
|
|
|
SQL
|
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
priorities.push([condition, 1.1])
|
|
|
|
else
|
|
|
|
priorities
|
2023-05-02 21:56:10 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
add_to_serializer(:user_summary, :solved_count) { object.solved_count }
|
2024-03-05 06:14:32 -05:00
|
|
|
add_to_serializer(:post, :can_accept_answer) do
|
|
|
|
scope.can_accept_answer?(topic, object) && object.post_number > 1 && !accepted_answer
|
FEATURE: Publish WebHook event when solving/unsolving (#85)
* FEATURE: Publish WebHook event when solving/unsolving
This feature will publish a post edit webhook event whenever a solution
is accepted or unaccepted.
I went ahead and used the existing post-edit webhook because all the
post custom fields for the solved plugin are already included in the
post-edit serializer.
* Create Solved Event Webhook
This commit adds a solved event webhook that will only trigger when an
answer has been marked as accepted or unaccepted.
It uses 100 as the webhook ID. This way any new webhooks in core can
keep using lower numbers like 11, 12, 13, but plugins can use 101, 102,
etc.
* Removed functionality that was added to core
This [PR][1] to discourse core adds what what removed in this commit.
It is better to have this logic in core so that it is discoverable and
future webhooks won't end up accidentally using the same ID.
[1]: https://github.com/discourse/discourse/pull/9110
* UX: Add "solved" status filter in advanced search page.
And rename `in:solved` to `status:solved`.
* FEATURE: Publish WebHook event when solving/unsolving
This feature will publish a post edit webhook event whenever a solution
is accepted or unaccepted.
I went ahead and used the existing post-edit webhook because all the
post custom fields for the solved plugin are already included in the
post-edit serializer.
* Create Solved Event Webhook
This commit adds a solved event webhook that will only trigger when an
answer has been marked as accepted or unaccepted.
It uses 100 as the webhook ID. This way any new webhooks in core can
keep using lower numbers like 11, 12, 13, but plugins can use 101, 102,
etc.
* Removed functionality that was added to core
This [PR][1] to discourse core adds what what removed in this commit.
It is better to have this logic in core so that it is discoverable and
future webhooks won't end up accidentally using the same ID.
[1]: https://github.com/discourse/discourse/pull/9110
Co-authored-by: Vinoth Kannan <vinothkannan@vinkas.com>
2020-03-06 13:28:29 -05:00
|
|
|
end
|
2024-03-05 06:14:32 -05:00
|
|
|
add_to_serializer(:post, :can_unaccept_answer) do
|
|
|
|
scope.can_accept_answer?(topic, object) && accepted_answer
|
2015-05-19 01:45:19 -04:00
|
|
|
end
|
2024-03-05 06:14:32 -05:00
|
|
|
add_to_serializer(:post, :accepted_answer) do
|
|
|
|
post_custom_fields[::DiscourseSolved::IS_ACCEPTED_ANSWER_CUSTOM_FIELD] == "true"
|
2015-06-09 16:09:20 -04:00
|
|
|
end
|
2024-03-05 06:14:32 -05:00
|
|
|
add_to_serializer(:post, :topic_accepted_answer) do
|
|
|
|
topic&.custom_fields&.[](::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD).present?
|
2015-06-15 02:26:40 -04:00
|
|
|
end
|
2015-06-22 22:31:10 -04:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
solved_callback = ->(scope) do
|
|
|
|
sql = <<~SQL
|
|
|
|
topics.id IN (
|
|
|
|
SELECT topic_id
|
|
|
|
FROM topic_custom_fields
|
2024-05-08 14:17:45 -04:00
|
|
|
WHERE name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
|
2024-04-26 10:17:50 -04:00
|
|
|
AND value IS NOT NULL
|
2022-12-23 15:36:08 -05:00
|
|
|
)
|
2024-04-26 10:17:50 -04:00
|
|
|
SQL
|
2015-06-22 22:56:22 -04:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
scope.where(sql)
|
|
|
|
end
|
|
|
|
|
|
|
|
unsolved_callback = ->(scope) do
|
|
|
|
scope = scope.where <<~SQL
|
|
|
|
topics.id NOT IN (
|
|
|
|
SELECT topic_id
|
|
|
|
FROM topic_custom_fields
|
2024-05-08 14:17:45 -04:00
|
|
|
WHERE name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
|
2024-04-26 10:17:50 -04:00
|
|
|
AND value IS NOT NULL
|
|
|
|
)
|
|
|
|
SQL
|
|
|
|
|
|
|
|
if !SiteSetting.allow_solved_on_all_topics
|
|
|
|
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
|
2024-05-08 14:17:45 -04:00
|
|
|
JOIN category_custom_fields cc
|
2024-04-26 10:17:50 -04:00
|
|
|
ON t.category_id = cc.category_id
|
2024-05-08 14:17:45 -04:00
|
|
|
AND cc.name = '#{::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD}'
|
2024-04-26 10:17:50 -04:00
|
|
|
AND cc.value = 'true'
|
2024-05-08 14:17:45 -04:00
|
|
|
)
|
|
|
|
OR
|
2024-04-26 10:17:50 -04:00
|
|
|
topics.id IN (
|
2024-05-08 14:17:45 -04:00
|
|
|
SELECT topic_id
|
|
|
|
FROM topic_tags
|
2024-04-26 10:17:50 -04:00
|
|
|
WHERE tag_id IN (?)
|
2023-06-20 10:52:02 -04:00
|
|
|
)
|
2024-04-26 10:17:50 -04:00
|
|
|
SQL
|
2015-06-22 22:56:22 -04:00
|
|
|
end
|
2024-04-26 10:17:50 -04:00
|
|
|
|
|
|
|
scope
|
2015-06-22 22:31:10 -04:00
|
|
|
end
|
2015-06-15 02:26:40 -04:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
register_custom_filter_by_status("solved", &solved_callback)
|
|
|
|
register_custom_filter_by_status("unsolved", &unsolved_callback)
|
|
|
|
|
|
|
|
register_search_advanced_filter(/status:solved/, &solved_callback)
|
|
|
|
register_search_advanced_filter(/status:unsolved/, &unsolved_callback)
|
|
|
|
|
|
|
|
TopicQuery.add_custom_filter(:solved) do |results, topic_query|
|
|
|
|
if topic_query.options[:solved] == "yes"
|
|
|
|
solved_callback.call(results)
|
|
|
|
elsif topic_query.options[:solved] == "no"
|
|
|
|
unsolved_callback.call(results)
|
|
|
|
else
|
2017-02-27 14:51:50 -05:00
|
|
|
results
|
2017-02-27 13:19:21 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
TopicList.preloaded_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
|
2015-06-15 02:26:40 -04:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
on(:filter_auto_bump_topics) do |_category, filters|
|
|
|
|
filters.push(
|
|
|
|
->(r) do
|
|
|
|
sql = <<~SQL
|
|
|
|
NOT EXISTS (
|
2024-05-08 14:17:45 -04:00
|
|
|
SELECT 1
|
2024-04-26 10:17:50 -04:00
|
|
|
FROM topic_custom_fields
|
|
|
|
WHERE topic_id = topics.id
|
|
|
|
AND name = '#{::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD}'
|
|
|
|
AND value IS NOT NULL
|
|
|
|
)
|
|
|
|
SQL
|
2017-08-01 04:25:54 -04:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
r.where(sql)
|
|
|
|
end,
|
|
|
|
)
|
|
|
|
end
|
2021-01-20 05:26:37 -05:00
|
|
|
|
|
|
|
on(:before_post_publish_changes) do |post_changes, topic_changes, options|
|
2022-12-23 15:36:08 -05:00
|
|
|
category_id_changes = topic_changes.diff["category_id"].to_a
|
|
|
|
tag_changes = topic_changes.diff["tags"].to_a
|
2021-01-20 05:26:37 -05:00
|
|
|
|
2023-12-06 01:26:40 -05:00
|
|
|
old_allowed = Guardian.new.allow_accepted_answers?(category_id_changes[0], tag_changes[0])
|
|
|
|
new_allowed = Guardian.new.allow_accepted_answers?(category_id_changes[1], tag_changes[1])
|
2021-01-20 05:26:37 -05:00
|
|
|
|
2021-09-17 10:12:47 -04:00
|
|
|
options[:refresh_stream] = true if old_allowed != new_allowed
|
2021-01-20 05:26:37 -05:00
|
|
|
end
|
2021-06-14 06:36:55 -04:00
|
|
|
|
2021-07-15 09:34:28 -04:00
|
|
|
on(:after_populate_dev_records) do |records, type|
|
|
|
|
next unless SiteSetting.solved_enabled
|
|
|
|
|
|
|
|
if type == :category
|
|
|
|
next if SiteSetting.allow_solved_on_all_topics
|
|
|
|
|
2022-12-23 15:36:08 -05:00
|
|
|
solved_category =
|
|
|
|
DiscourseDev::Record.random(
|
|
|
|
Category.where(read_restricted: false, id: records.pluck(:id), parent_category_id: nil),
|
|
|
|
)
|
|
|
|
CategoryCustomField.create!(
|
|
|
|
category_id: solved_category.id,
|
2023-06-20 10:52:02 -04:00
|
|
|
name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD,
|
2022-12-23 15:36:08 -05:00
|
|
|
value: "true",
|
|
|
|
)
|
2021-07-15 09:34:28 -04:00
|
|
|
puts "discourse-solved enabled on category '#{solved_category.name}' (#{solved_category.id})."
|
|
|
|
elsif type == :topic
|
|
|
|
topics = Topic.where(id: records.pluck(:id))
|
|
|
|
|
|
|
|
unless SiteSetting.allow_solved_on_all_topics
|
2022-12-23 15:36:08 -05:00
|
|
|
solved_category_id =
|
|
|
|
CategoryCustomField
|
2023-06-20 10:52:02 -04:00
|
|
|
.where(name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD, value: "true")
|
2022-12-23 15:36:08 -05:00
|
|
|
.first
|
|
|
|
.category_id
|
2021-07-15 09:34:28 -04:00
|
|
|
|
|
|
|
unless topics.exists?(category_id: solved_category_id)
|
|
|
|
topics.last.update(category_id: solved_category_id)
|
|
|
|
end
|
|
|
|
|
|
|
|
topics = topics.where(category_id: solved_category_id)
|
|
|
|
end
|
|
|
|
|
|
|
|
solved_topic = DiscourseDev::Record.random(topics)
|
|
|
|
post = nil
|
|
|
|
|
|
|
|
if solved_topic.posts_count > 1
|
|
|
|
post = DiscourseDev::Record.random(solved_topic.posts.where.not(post_number: 1))
|
|
|
|
else
|
|
|
|
post = DiscourseDev::Post.new(solved_topic, 1).create!
|
|
|
|
end
|
|
|
|
|
|
|
|
DiscourseSolved.accept_answer!(post, post.topic.user, topic: post.topic)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-12-23 15:36:08 -05:00
|
|
|
query =
|
|
|
|
"
|
2021-06-22 14:18:23 -04:00
|
|
|
WITH x AS (SELECT
|
|
|
|
u.id user_id,
|
|
|
|
COUNT(DISTINCT ua.id) AS solutions
|
|
|
|
FROM users AS u
|
|
|
|
LEFT OUTER JOIN user_actions AS ua ON ua.user_id = u.id AND ua.action_type = #{UserAction::SOLVED} AND COALESCE(ua.created_at, :since) > :since
|
|
|
|
WHERE u.active
|
|
|
|
AND u.silenced_till IS NULL
|
|
|
|
AND u.id > 0
|
|
|
|
GROUP BY u.id
|
|
|
|
)
|
|
|
|
UPDATE directory_items di SET
|
|
|
|
solutions = x.solutions
|
|
|
|
FROM x
|
|
|
|
WHERE x.user_id = di.user_id
|
|
|
|
AND di.period_type = :period_type
|
|
|
|
AND di.solutions <> x.solutions
|
|
|
|
"
|
2024-04-26 10:17:50 -04:00
|
|
|
add_directory_column("solutions", query: query)
|
2021-06-22 14:18:23 -04:00
|
|
|
|
2021-06-14 06:36:55 -04:00
|
|
|
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?
|
2023-05-02 21:56:10 -04:00
|
|
|
return if @topic.custom_fields[::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD].blank?
|
2021-06-14 06:36:55 -04:00
|
|
|
|
|
|
|
{
|
2022-12-23 15:36:08 -05:00
|
|
|
id: "solved_topic",
|
|
|
|
templateName: "education",
|
2021-08-19 00:03:00 -04:00
|
|
|
wait_for_typing: true,
|
2022-12-23 15:36:08 -05:00
|
|
|
extraClass: "education-message",
|
2021-08-19 00:03:00 -04:00
|
|
|
hide_if_whisper: true,
|
2022-12-23 15:36:08 -05:00
|
|
|
body: PrettyText.cook(I18n.t("education.topic_is_solved", base_url: Discourse.base_url)),
|
2021-06-14 06:36:55 -04:00
|
|
|
}
|
|
|
|
end
|
2021-08-26 05:53:46 -04:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
add_to_serializer(:user_card, :accepted_answers) do
|
|
|
|
UserAction.where(user_id: object.id).where(action_type: UserAction::SOLVED).count
|
2021-08-26 07:25:39 -04:00
|
|
|
end
|
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
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)
|
|
|
|
.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)
|
2021-08-26 05:53:46 -04:00
|
|
|
end
|
2021-11-12 10:00:48 -05:00
|
|
|
|
|
|
|
if defined?(DiscourseAutomation)
|
2024-04-26 10:17:50 -04:00
|
|
|
on(:accepted_solution) do |post|
|
|
|
|
# testing directly automation is prone to issues
|
|
|
|
# we prefer to abstract logic in service object and test this
|
|
|
|
next if Rails.env.test?
|
|
|
|
|
|
|
|
name = "first_accepted_solution"
|
|
|
|
DiscourseAutomation::Automation
|
|
|
|
.where(trigger: name, enabled: true)
|
|
|
|
.find_each do |automation|
|
|
|
|
maximum_trust_level = automation.trigger_field("maximum_trust_level")&.dig("value")
|
|
|
|
if FirstAcceptedPostSolutionValidator.check(post, trust_level: maximum_trust_level)
|
|
|
|
automation.trigger!(
|
|
|
|
"kind" => name,
|
|
|
|
"accepted_post_id" => post.id,
|
|
|
|
"usernames" => [post.user.username],
|
|
|
|
"placeholders" => {
|
|
|
|
"post_url" => Discourse.base_url + post.url,
|
|
|
|
},
|
|
|
|
)
|
2021-11-12 10:00:48 -05:00
|
|
|
end
|
2024-04-26 10:17:50 -04:00
|
|
|
end
|
|
|
|
end
|
2021-11-12 10:00:48 -05:00
|
|
|
|
2024-04-26 10:17:50 -04:00
|
|
|
add_triggerable_to_scriptable(:first_accepted_solution, :send_pms)
|
|
|
|
|
|
|
|
DiscourseAutomation::Triggerable.add(:first_accepted_solution) do
|
|
|
|
placeholder :post_url
|
|
|
|
|
|
|
|
field :maximum_trust_level,
|
|
|
|
component: :choices,
|
|
|
|
extra: {
|
|
|
|
content: [
|
|
|
|
{
|
|
|
|
id: 1,
|
|
|
|
name:
|
|
|
|
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl1",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 2,
|
|
|
|
name:
|
|
|
|
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl2",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 3,
|
|
|
|
name:
|
|
|
|
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl3",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 4,
|
|
|
|
name:
|
|
|
|
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl4",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: "any",
|
|
|
|
name:
|
|
|
|
"discourse_automation.triggerables.first_accepted_solution.max_trust_level.any",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
required: true
|
2021-11-12 10:00:48 -05:00
|
|
|
end
|
|
|
|
end
|
2024-04-26 12:21:09 -04:00
|
|
|
|
|
|
|
if defined?(DiscourseAssign)
|
|
|
|
on(:accepted_solution) do |post|
|
|
|
|
next if SiteSetting.assignment_status_on_solve.blank?
|
2024-05-08 14:17:45 -04:00
|
|
|
assignments = Assignment.includes(:target).where(topic: post.topic)
|
|
|
|
assignments.each do |assignment|
|
|
|
|
assigned_user = User.find_by(id: assignment.assigned_to_id)
|
|
|
|
Assigner.new(assignment.target, assigned_user).assign(
|
|
|
|
assigned_user,
|
|
|
|
status: SiteSetting.assignment_status_on_solve,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
on(:unaccepted_solution) do |post|
|
|
|
|
next if SiteSetting.assignment_status_on_unsolve.blank?
|
|
|
|
assignments = Assignment.includes(:target).where(topic: post.topic)
|
|
|
|
assignments.each do |assignment|
|
|
|
|
assigned_user = User.find_by(id: assignment.assigned_to_id)
|
|
|
|
Assigner.new(assignment.target, assigned_user).assign(
|
|
|
|
assigned_user,
|
|
|
|
status: SiteSetting.assignment_status_on_unsolve,
|
|
|
|
)
|
|
|
|
end
|
2024-04-26 12:21:09 -04:00
|
|
|
end
|
|
|
|
end
|
2015-05-19 01:45:19 -04:00
|
|
|
end
|