DEV: Introduce syntax_tree for ruby formatting (#217)

This commit is contained in:
David Taylor 2022-12-23 20:36:08 +00:00 committed by GitHub
parent d07ffb6f7a
commit 14e0800a29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 406 additions and 338 deletions

View File

@ -55,3 +55,12 @@ jobs:
- name: Rubocop - name: Rubocop
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
run: bundle exec rubocop . run: bundle exec rubocop .
- name: Syntax Tree
if: ${{ !cancelled() }}
run: |
if test -f .streerc; then
bundle exec stree check Gemfile $(git ls-files '*.rb') $(git ls-files '*.rake')
else
echo "Stree config not detected for this repository. Skipping."
fi

View File

@ -80,7 +80,7 @@ jobs:
- name: Get yarn cache directory - name: Get yarn cache directory
id: yarn-cache-dir id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache - name: Yarn cache
uses: actions/cache@v3 uses: actions/cache@v3
@ -130,7 +130,7 @@ jobs:
shell: bash shell: bash
run: | run: |
if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/spec -type f -name "*.rb" 2> /dev/null | wc -l) ]; then if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/spec -type f -name "*.rb" 2> /dev/null | wc -l) ]; then
echo "::set-output name=files_exist::true" echo "files_exist=true" >> $GITHUB_OUTPUT
fi fi
- name: Plugin RSpec - name: Plugin RSpec
@ -142,7 +142,7 @@ jobs:
shell: bash shell: bash
run: | run: |
if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/test/javascripts -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/test/javascripts -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then
echo "::set-output name=files_exist::true" echo "files_exist=true" >> $GITHUB_OUTPUT
fi fi
- name: Plugin QUnit - name: Plugin QUnit

View File

@ -1,2 +1,2 @@
inherit_gem: inherit_gem:
rubocop-discourse: default.yml rubocop-discourse: stree-compat.yml

2
.streerc Normal file
View File

@ -0,0 +1,2 @@
--print-width=100
--plugins=plugin/trailing_comma

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
source 'https://rubygems.org' source "https://rubygems.org"
group :development do group :development do
gem 'rubocop-discourse' gem "rubocop-discourse"
gem "syntax_tree"
end end

View File

@ -6,6 +6,7 @@ GEM
parallel (1.22.1) parallel (1.22.1)
parser (3.1.2.1) parser (3.1.2.1)
ast (~> 2.4.1) ast (~> 2.4.1)
prettier_print (1.1.0)
rainbow (3.1.1) rainbow (3.1.1)
regexp_parser (2.6.0) regexp_parser (2.6.0)
rexml (3.2.5) rexml (3.2.5)
@ -27,6 +28,8 @@ GEM
rubocop-rspec (2.13.2) rubocop-rspec (2.13.2)
rubocop (~> 1.33) rubocop (~> 1.33)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
syntax_tree (5.0.1)
prettier_print (>= 1.1.0)
unicode-display_width (2.3.0) unicode-display_width (2.3.0)
PLATFORMS PLATFORMS
@ -34,6 +37,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
rubocop-discourse rubocop-discourse
syntax_tree
BUNDLED WITH BUNDLED WITH
2.1.4 2.1.4

View File

@ -4,11 +4,9 @@ class FirstAcceptedPostSolutionValidator
def self.check(post, trust_level:) def self.check(post, trust_level:)
return false if post.archetype != Archetype.default return false if post.archetype != Archetype.default
return false if !post&.user&.human? return false if !post&.user&.human?
return true if trust_level == 'any' return true if trust_level == "any"
if TrustLevel.compare(post&.user&.trust_level, trust_level.to_i) return false if TrustLevel.compare(post&.user&.trust_level, trust_level.to_i)
return false
end
if !UserAction.where(user_id: post&.user_id, action_type: UserAction::SOLVED).exists? if !UserAction.where(user_id: post&.user_id, action_type: UserAction::SOLVED).exists?
return true return true

View File

@ -18,7 +18,7 @@ class RenameBadges < ActiveRecord::Migration[6.1]
"sv" => "Kundtjänst", "sv" => "Kundtjänst",
"tr_TR" => "Yardım masası", "tr_TR" => "Yardım masası",
"zh_CN" => "帮助台", "zh_CN" => "帮助台",
"zh_TW" => "服務台" "zh_TW" => "服務台",
} }
TECH_SUPPORT_TRANSLATIONS = { TECH_SUPPORT_TRANSLATIONS = {
@ -43,11 +43,12 @@ class RenameBadges < ActiveRecord::Migration[6.1]
"sv" => "Teknisk support", "sv" => "Teknisk support",
"tr_TR" => "Teknik Destek", "tr_TR" => "Teknik Destek",
"zh_CN" => "技术支持", "zh_CN" => "技术支持",
"zh_TW" => "技術支援" "zh_TW" => "技術支援",
} }
def up def up
default_locale = DB.query_single("SELECT value FROM site_settings WHERE name = 'default_locale'").first || "en" default_locale =
DB.query_single("SELECT value FROM site_settings WHERE name = 'default_locale'").first || "en"
sql = <<~SQL sql = <<~SQL
UPDATE badges UPDATE badges

377
plugin.rb
View File

@ -17,23 +17,22 @@ end
PLUGIN_NAME = "discourse_solved".freeze PLUGIN_NAME = "discourse_solved".freeze
register_asset 'stylesheets/solutions.scss' register_asset "stylesheets/solutions.scss"
register_asset 'stylesheets/mobile/solutions.scss', :mobile register_asset "stylesheets/mobile/solutions.scss", :mobile
after_initialize do after_initialize do
SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s
[ %w[
'../app/lib/first_accepted_post_solution_validator.rb', ../app/lib/first_accepted_post_solution_validator.rb
'../app/serializers/concerns/topic_answer_mixin.rb' ../app/serializers/concerns/topic_answer_mixin.rb
].each { |path| load File.expand_path(path, __FILE__) } ].each { |path| load File.expand_path(path, __FILE__) }
skip_db = defined?(GlobalSetting.skip_db?) && GlobalSetting.skip_db? skip_db = defined?(GlobalSetting.skip_db?) && GlobalSetting.skip_db?
# we got to do a one time upgrade # we got to do a one time upgrade
if !skip_db && defined?(UserAction::SOLVED) if !skip_db && defined?(UserAction::SOLVED)
unless Discourse.redis.get('solved_already_upgraded') unless Discourse.redis.get("solved_already_upgraded")
unless UserAction.where(action_type: UserAction::SOLVED).exists? unless UserAction.where(action_type: UserAction::SOLVED).exists?
Rails.logger.info("Upgrading storage for solved") Rails.logger.info("Upgrading storage for solved")
sql = <<SQL sql = <<SQL
@ -89,10 +88,7 @@ SQL
p2.save! p2.save!
if defined?(UserAction::SOLVED) if defined?(UserAction::SOLVED)
UserAction.where( UserAction.where(action_type: UserAction::SOLVED, target_post_id: p2.id).destroy_all
action_type: UserAction::SOLVED,
target_post_id: p2.id
).destroy_all
end end
end end
end end
@ -106,14 +102,14 @@ SQL
user_id: post.user_id, user_id: post.user_id,
acting_user_id: acting_user.id, acting_user_id: acting_user.id,
target_post_id: post.id, target_post_id: post.id,
target_topic_id: post.topic_id target_topic_id: post.topic_id,
) )
end end
notification_data = { notification_data = {
message: 'solved.accepted_notification', message: "solved.accepted_notification",
display_username: acting_user.username, display_username: acting_user.username,
topic_title: topic.title topic_title: topic.title,
}.to_json }.to_json
unless acting_user.id == post.user_id unless acting_user.id == post.user_id
@ -122,7 +118,7 @@ SQL
user_id: post.user_id, user_id: post.user_id,
topic_id: post.topic_id, topic_id: post.topic_id,
post_number: post.post_number, post_number: post.post_number,
data: notification_data data: notification_data,
) )
end end
@ -132,23 +128,22 @@ SQL
user_id: topic.user_id, user_id: topic.user_id,
topic_id: post.topic_id, topic_id: post.topic_id,
post_number: post.post_number, post_number: post.post_number,
data: notification_data data: notification_data,
) )
end end
auto_close_hours = SiteSetting.solved_topics_auto_close_hours auto_close_hours = SiteSetting.solved_topics_auto_close_hours
if (auto_close_hours > 0) && !topic.closed if (auto_close_hours > 0) && !topic.closed
topic_timer = topic.set_or_create_timer( topic_timer =
topic.set_or_create_timer(
TopicTimer.types[:silent_close], TopicTimer.types[:silent_close],
nil, nil,
based_on_last_post: true, based_on_last_post: true,
duration_minutes: auto_close_hours * 60 duration_minutes: auto_close_hours * 60,
) )
topic.custom_fields[ topic.custom_fields[AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD] = topic_timer.id
AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD
] = topic_timer.id
MessageBus.publish("/topic/#{topic.id}", reload_topic: true) MessageBus.publish("/topic/#{topic.id}", reload_topic: true)
end end
@ -182,19 +177,17 @@ SQL
post.save! post.save!
# TODO remove_action! does not allow for this type of interface # TODO remove_action! does not allow for this type of interface
if defined? UserAction::SOLVED if defined?(UserAction::SOLVED)
UserAction.where( UserAction.where(action_type: UserAction::SOLVED, target_post_id: post.id).destroy_all
action_type: UserAction::SOLVED,
target_post_id: post.id
).destroy_all
end end
# yank notification # yank notification
notification = Notification.find_by( notification =
Notification.find_by(
notification_type: Notification.types[:custom], notification_type: Notification.types[:custom],
user_id: post.user_id, user_id: post.user_id,
topic_id: post.topic_id, topic_id: post.topic_id,
post_number: post.post_number post_number: post.post_number,
) )
notification.destroy! if notification notification.destroy! if notification
@ -212,7 +205,6 @@ SQL
require_dependency "application_controller" require_dependency "application_controller"
class DiscourseSolved::AnswerController < ::ApplicationController class DiscourseSolved::AnswerController < ::ApplicationController
def accept def accept
limit_accepts limit_accepts
@ -254,14 +246,13 @@ SQL
post "/unaccept" => "answer#unaccept" post "/unaccept" => "answer#unaccept"
end end
Discourse::Application.routes.append do Discourse::Application.routes.append { mount ::DiscourseSolved::Engine, at: "solution" }
mount ::DiscourseSolved::Engine, at: "solution"
end
topic_view_post_custom_fields_allowlister { ["is_accepted_answer"] } topic_view_post_custom_fields_allowlister { ["is_accepted_answer"] }
def get_schema_text(post) def get_schema_text(post)
post.excerpt(nil, keep_onebox_body: true).presence || post.excerpt(nil, keep_onebox_body: true, keep_quotes: true) post.excerpt(nil, keep_onebox_body: true).presence ||
post.excerpt(nil, keep_onebox_body: true, keep_quotes: true)
end end
def before_head_close_meta(controller) def before_head_close_meta(controller)
@ -276,59 +267,63 @@ SQL
return "" if SiteSetting.solved_add_schema_markup == "never" return "" if SiteSetting.solved_add_schema_markup == "never"
allowed = controller allowed =
.guardian controller.guardian.allow_accepted_answers?(topic.category_id, topic.tags.pluck(:name))
.allow_accepted_answers?(
topic.category_id, topic.tags.pluck(:name)
)
return "" if !allowed return "" if !allowed
first_post = topic_view.posts&.first first_post = topic_view.posts&.first
return "" if first_post&.post_number != 1 return "" if first_post&.post_number != 1
question_json = { question_json = {
'@type' => 'Question', "@type" => "Question",
'name' => topic.title, "name" => topic.title,
'text' => get_schema_text(first_post), "text" => get_schema_text(first_post),
'upvoteCount' => first_post.like_count, "upvoteCount" => first_post.like_count,
'answerCount' => 0, "answerCount" => 0,
'dateCreated' => topic.created_at, "dateCreated" => topic.created_at,
'author' => { "author" => {
'@type' => 'Person', "@type" => "Person",
'name' => topic.user&.name "name" => topic.user&.name,
} },
} }
if accepted_answer = Post.find_by(id: topic.custom_fields["accepted_answer_post_id"]) if accepted_answer = Post.find_by(id: topic.custom_fields["accepted_answer_post_id"])
question_json['answerCount'] = 1 question_json["answerCount"] = 1
question_json[:acceptedAnswer] = { question_json[:acceptedAnswer] = {
'@type' => 'Answer', "@type" => "Answer",
'text' => get_schema_text(accepted_answer), "text" => get_schema_text(accepted_answer),
'upvoteCount' => accepted_answer.like_count, "upvoteCount" => accepted_answer.like_count,
'dateCreated' => accepted_answer.created_at, "dateCreated" => accepted_answer.created_at,
'url' => accepted_answer.full_url, "url" => accepted_answer.full_url,
'author' => { "author" => {
'@type' => 'Person', "@type" => "Person",
'name' => accepted_answer.user&.username "name" => accepted_answer.user&.username,
} },
} }
else else
return "" if SiteSetting.solved_add_schema_markup == "answered only" return "" if SiteSetting.solved_add_schema_markup == "answered only"
end end
['<script type="application/ld+json">', MultiJson.dump( [
'@context' => 'http://schema.org', '<script type="application/ld+json">',
'@type' => 'QAPage', MultiJson
'name' => topic&.title, .dump(
'mainEntity' => question_json "@context" => "http://schema.org",
).gsub("</", "<\\/").html_safe, '</script>'].join("") "@type" => "QAPage",
"name" => topic&.title,
"mainEntity" => question_json,
)
.gsub("</", "<\\/")
.html_safe,
"</script>",
].join("")
end end
register_html_builder('server:before-head-close-crawler') do |controller| register_html_builder("server:before-head-close-crawler") do |controller|
before_head_close_meta(controller) before_head_close_meta(controller)
end end
register_html_builder('server:before-head-close') do |controller| register_html_builder("server:before-head-close") do |controller|
before_head_close_meta(controller) before_head_close_meta(controller)
end end
@ -341,39 +336,42 @@ SQL
category_id, include_subcategories = report.add_category_filter category_id, include_subcategories = report.add_category_filter
if category_id if category_id
if include_subcategories if include_subcategories
accepted_solutions = accepted_solutions.joins(:topic).where('topics.category_id IN (?)', Category.subcategory_ids(category_id)) accepted_solutions =
accepted_solutions.joins(:topic).where(
"topics.category_id IN (?)",
Category.subcategory_ids(category_id),
)
else else
accepted_solutions = accepted_solutions.joins(:topic).where('topics.category_id = ?', category_id) accepted_solutions =
accepted_solutions.joins(:topic).where("topics.category_id = ?", category_id)
end end
end end
accepted_solutions.where("topic_custom_fields.created_at >= ?", report.start_date) accepted_solutions
.where("topic_custom_fields.created_at >= ?", report.start_date)
.where("topic_custom_fields.created_at <= ?", report.end_date) .where("topic_custom_fields.created_at <= ?", report.end_date)
.group("DATE(topic_custom_fields.created_at)") .group("DATE(topic_custom_fields.created_at)")
.order("DATE(topic_custom_fields.created_at)") .order("DATE(topic_custom_fields.created_at)")
.count .count
.each do |date, count| .each { |date, count| report.data << { x: date, y: count } }
report.data << { x: date, y: count }
end
report.total = accepted_solutions.count report.total = accepted_solutions.count
report.prev30Days = accepted_solutions.where("topic_custom_fields.created_at >= ?", report.start_date - 30.days) 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("topic_custom_fields.created_at <= ?", report.start_date)
.count .count
end end
end end
if defined?(UserAction::SOLVED) if defined?(UserAction::SOLVED)
require_dependency 'user_summary' require_dependency "user_summary"
class ::UserSummary class ::UserSummary
def solved_count def solved_count
UserAction UserAction.where(user: @user).where(action_type: UserAction::SOLVED).count
.where(user: @user)
.where(action_type: UserAction::SOLVED)
.count
end end
end end
require_dependency 'user_summary_serializer' require_dependency "user_summary_serializer"
class ::UserSummarySerializer class ::UserSummarySerializer
attributes :solved_count attributes :solved_count
@ -385,20 +383,22 @@ SQL
class ::WebHook class ::WebHook
def self.enqueue_solved_hooks(event, post, payload = nil) def self.enqueue_solved_hooks(event, post, payload = nil)
if active_web_hooks('solved').exists? && post.present? if active_web_hooks("solved").exists? && post.present?
payload ||= WebHook.generate_payload(:post, post) payload ||= WebHook.generate_payload(:post, post)
WebHook.enqueue_hooks(:solved, event, WebHook.enqueue_hooks(
:solved,
event,
id: post.id, id: post.id,
category_id: post.topic&.category_id, category_id: post.topic&.category_id,
tag_ids: post.topic&.tags&.pluck(:id), tag_ids: post.topic&.tags&.pluck(:id),
payload: payload payload: payload,
) )
end end
end end
end end
require_dependency 'topic_view_serializer' require_dependency "topic_view_serializer"
class ::TopicViewSerializer class ::TopicViewSerializer
attributes :accepted_answer attributes :accepted_answer
@ -408,19 +408,17 @@ SQL
def accepted_answer def accepted_answer
if info = accepted_answer_post_info if info = accepted_answer_post_info
{ { post_number: info[0], username: info[1], excerpt: info[2] }
post_number: info[0],
username: info[1],
excerpt: info[2]
}
end end
end end
def accepted_answer_post_info def accepted_answer_post_info
# TODO: we may already have it in the stream ... so bypass query here # TODO: we may already have it in the stream ... so bypass query here
postInfo = Post.where(id: accepted_answer_post_id, topic_id: object.topic.id) postInfo =
Post
.where(id: accepted_answer_post_id, topic_id: object.topic.id)
.joins(:user) .joins(:user)
.pluck('post_number', 'username', 'cooked') .pluck("post_number", "username", "cooked")
.first .first
if postInfo if postInfo
@ -436,31 +434,33 @@ SQL
def accepted_answer_post_id def accepted_answer_post_id
id = object.topic.custom_fields["accepted_answer_post_id"] id = object.topic.custom_fields["accepted_answer_post_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 begin
id && id.to_i
rescue StandardError
nil
end
end end
end end
class ::Category class ::Category
after_save :reset_accepted_cache after_save :reset_accepted_cache
protected protected
def reset_accepted_cache def reset_accepted_cache
::Guardian.reset_accepted_answer_cache ::Guardian.reset_accepted_answer_cache
end end
end end
class ::Guardian class ::Guardian
@@allowed_accepted_cache = DistributedCache.new("allowed_accepted") @@allowed_accepted_cache = DistributedCache.new("allowed_accepted")
def self.reset_accepted_answer_cache def self.reset_accepted_answer_cache
@@allowed_accepted_cache["allowed"] = @@allowed_accepted_cache["allowed"] = begin
begin
Set.new( Set.new(
CategoryCustomField CategoryCustomField.where(name: "enable_accepted_answers", value: "true").pluck(
.where(name: "enable_accepted_answers", value: "true") :category_id,
.pluck(:category_id) ),
) )
end end
end end
@ -469,7 +469,7 @@ SQL
return true if SiteSetting.allow_solved_on_all_topics return true if SiteSetting.allow_solved_on_all_topics
if SiteSetting.enable_solved_tags.present? && tag_names.present? if SiteSetting.enable_solved_tags.present? && tag_names.present?
allowed_tags = SiteSetting.enable_solved_tags.split('|') allowed_tags = SiteSetting.enable_solved_tags.split("|")
is_allowed = (tag_names & allowed_tags).present? is_allowed = (tag_names & allowed_tags).present?
return true if is_allowed return true if is_allowed
@ -496,7 +496,7 @@ SQL
end end
end end
require_dependency 'post_serializer' require_dependency "post_serializer"
class ::PostSerializer class ::PostSerializer
attributes :can_accept_answer, :can_unaccept_answer, :accepted_answer attributes :can_accept_answer, :can_unaccept_answer, :accepted_answer
@ -510,67 +510,76 @@ SQL
def can_unaccept_answer def can_unaccept_answer
if topic = (topic_view && topic_view.topic) || object.topic if topic = (topic_view && topic_view.topic) || object.topic
scope.can_accept_answer?(topic, object) && (post_custom_fields["is_accepted_answer"] == 'true') scope.can_accept_answer?(topic, object) &&
(post_custom_fields["is_accepted_answer"] == "true")
end end
end end
def accepted_answer def accepted_answer
post_custom_fields["is_accepted_answer"] == 'true' post_custom_fields["is_accepted_answer"] == "true"
end end
end end
require_dependency 'search' require_dependency "search"
#TODO Remove when plugin is 1.0 #TODO Remove when plugin is 1.0
if Search.respond_to? :advanced_filter if Search.respond_to? :advanced_filter
Search.advanced_filter(/status:solved/) do |posts| Search.advanced_filter(/status:solved/) do |posts|
posts.where("topics.id IN ( posts.where(
"topics.id IN (
SELECT tc.topic_id SELECT tc.topic_id
FROM topic_custom_fields tc FROM topic_custom_fields tc
WHERE tc.name = 'accepted_answer_post_id' AND WHERE tc.name = 'accepted_answer_post_id' AND
tc.value IS NOT NULL tc.value IS NOT NULL
)") )",
)
end end
Search.advanced_filter(/status:unsolved/) do |posts| Search.advanced_filter(/status:unsolved/) do |posts|
posts.where("topics.id NOT IN ( posts.where(
"topics.id NOT IN (
SELECT tc.topic_id SELECT tc.topic_id
FROM topic_custom_fields tc FROM topic_custom_fields tc
WHERE tc.name = 'accepted_answer_post_id' AND WHERE tc.name = 'accepted_answer_post_id' AND
tc.value IS NOT NULL tc.value IS NOT NULL
)") )",
)
end end
end end
if Discourse.has_needed_version?(Discourse::VERSION::STRING, '1.8.0.beta6') if Discourse.has_needed_version?(Discourse::VERSION::STRING, "1.8.0.beta6")
require_dependency 'topic_query' require_dependency "topic_query"
TopicQuery.add_custom_filter(:solved) do |results, topic_query| TopicQuery.add_custom_filter(:solved) do |results, topic_query|
if topic_query.options[:solved] == 'yes' if topic_query.options[:solved] == "yes"
results = results.where("topics.id IN ( results =
results.where(
"topics.id IN (
SELECT tc.topic_id SELECT tc.topic_id
FROM topic_custom_fields tc FROM topic_custom_fields tc
WHERE tc.name = 'accepted_answer_post_id' AND WHERE tc.name = 'accepted_answer_post_id' AND
tc.value IS NOT NULL tc.value IS NOT NULL
)") )",
elsif topic_query.options[:solved] == 'no' )
results = results.where("topics.id NOT IN ( elsif topic_query.options[:solved] == "no"
results =
results.where(
"topics.id NOT IN (
SELECT tc.topic_id SELECT tc.topic_id
FROM topic_custom_fields tc FROM topic_custom_fields tc
WHERE tc.name = 'accepted_answer_post_id' AND WHERE tc.name = 'accepted_answer_post_id' AND
tc.value IS NOT NULL tc.value IS NOT NULL
)") )",
)
end end
results results
end end
end end
require_dependency 'topic_list_item_serializer' require_dependency "topic_list_item_serializer"
require_dependency 'search_topic_list_item_serializer' require_dependency "search_topic_list_item_serializer"
require_dependency 'suggested_topic_serializer' require_dependency "suggested_topic_serializer"
require_dependency 'user_summary_serializer' require_dependency "user_summary_serializer"
class ::TopicListItemSerializer class ::TopicListItemSerializer
include TopicAnswerMixin include TopicAnswerMixin
@ -592,16 +601,21 @@ SQL
include TopicAnswerMixin include TopicAnswerMixin
end end
TopicList.preloaded_custom_fields << "accepted_answer_post_id" if TopicList.respond_to? :preloaded_custom_fields if TopicList.respond_to? :preloaded_custom_fields
Site.preloaded_category_custom_fields << "enable_accepted_answers" if Site.respond_to? :preloaded_category_custom_fields TopicList.preloaded_custom_fields << "accepted_answer_post_id"
Search.preloaded_topic_custom_fields << "accepted_answer_post_id" if Search.respond_to? :preloaded_topic_custom_fields end
if Site.respond_to? :preloaded_category_custom_fields
Site.preloaded_category_custom_fields << "enable_accepted_answers"
end
if Search.respond_to? :preloaded_topic_custom_fields
Search.preloaded_topic_custom_fields << "accepted_answer_post_id"
end
if CategoryList.respond_to?(:preloaded_topic_custom_fields) if CategoryList.respond_to?(:preloaded_topic_custom_fields)
CategoryList.preloaded_topic_custom_fields << "accepted_answer_post_id" CategoryList.preloaded_topic_custom_fields << "accepted_answer_post_id"
end end
on(:filter_auto_bump_topics) do |_category, filters| on(:filter_auto_bump_topics) { |_category, filters| filters.push(->(r) { r.where(<<~SQL) }) }
filters.push(->(r) { r.where(<<~SQL)
NOT EXISTS( NOT EXISTS(
SELECT 1 FROM topic_custom_fields SELECT 1 FROM topic_custom_fields
WHERE topic_id = topics.id WHERE topic_id = topics.id
@ -609,12 +623,10 @@ SQL
AND value IS NOT NULL AND value IS NOT NULL
) )
SQL SQL
})
end
on(:before_post_publish_changes) do |post_changes, topic_changes, options| on(:before_post_publish_changes) do |post_changes, topic_changes, options|
category_id_changes = topic_changes.diff['category_id'].to_a category_id_changes = topic_changes.diff["category_id"].to_a
tag_changes = topic_changes.diff['tags'].to_a tag_changes = topic_changes.diff["tags"].to_a
old_allowed = Guardian.new.allow_accepted_answers?(category_id_changes[0], tag_changes[0]) 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]) new_allowed = Guardian.new.allow_accepted_answers?(category_id_changes[1], tag_changes[1])
@ -628,14 +640,25 @@ SQL
if type == :category if type == :category
next if SiteSetting.allow_solved_on_all_topics next if SiteSetting.allow_solved_on_all_topics
solved_category = DiscourseDev::Record.random(Category.where(read_restricted: false, id: records.pluck(:id), parent_category_id: nil)) solved_category =
CategoryCustomField.create!(category_id: solved_category.id, name: "enable_accepted_answers", value: "true") DiscourseDev::Record.random(
Category.where(read_restricted: false, id: records.pluck(:id), parent_category_id: nil),
)
CategoryCustomField.create!(
category_id: solved_category.id,
name: "enable_accepted_answers",
value: "true",
)
puts "discourse-solved enabled on category '#{solved_category.name}' (#{solved_category.id})." puts "discourse-solved enabled on category '#{solved_category.name}' (#{solved_category.id})."
elsif type == :topic elsif type == :topic
topics = Topic.where(id: records.pluck(:id)) topics = Topic.where(id: records.pluck(:id))
unless SiteSetting.allow_solved_on_all_topics unless SiteSetting.allow_solved_on_all_topics
solved_category_id = CategoryCustomField.where(name: "enable_accepted_answers", value: "true").first.category_id solved_category_id =
CategoryCustomField
.where(name: "enable_accepted_answers", value: "true")
.first
.category_id
unless topics.exists?(category_id: solved_category_id) unless topics.exists?(category_id: solved_category_id)
topics.last.update(category_id: solved_category_id) topics.last.update(category_id: solved_category_id)
@ -657,7 +680,8 @@ SQL
end end
end end
query = " query =
"
WITH x AS (SELECT WITH x AS (SELECT
u.id user_id, u.id user_id,
COUNT(DISTINCT ua.id) AS solutions COUNT(DISTINCT ua.id) AS solutions
@ -675,9 +699,7 @@ SQL
AND di.period_type = :period_type AND di.period_type = :period_type
AND di.solutions <> x.solutions AND di.solutions <> x.solutions
" "
if respond_to?(:add_directory_column) add_directory_column("solutions", query: query) if respond_to?(:add_directory_column)
add_directory_column("solutions", query: query)
end
add_to_class(:composer_messages_finder, :check_topic_is_solved) do add_to_class(:composer_messages_finder, :check_topic_is_solved) do
return if !SiteSetting.solved_enabled || SiteSetting.disable_solved_education_message return if !SiteSetting.solved_enabled || SiteSetting.disable_solved_education_message
@ -685,21 +707,18 @@ SQL
return if @topic.custom_fields["accepted_answer_post_id"].blank? return if @topic.custom_fields["accepted_answer_post_id"].blank?
{ {
id: 'solved_topic', id: "solved_topic",
templateName: 'education', templateName: "education",
wait_for_typing: true, wait_for_typing: true,
extraClass: 'education-message', extraClass: "education-message",
hide_if_whisper: true, hide_if_whisper: true,
body: PrettyText.cook(I18n.t('education.topic_is_solved', base_url: Discourse.base_url)) body: PrettyText.cook(I18n.t("education.topic_is_solved", base_url: Discourse.base_url)),
} }
end end
if defined?(UserAction::SOLVED) if defined?(UserAction::SOLVED)
add_to_serializer(:user_card, :accepted_answers) do add_to_serializer(:user_card, :accepted_answers) do
UserAction UserAction.where(user_id: object.id).where(action_type: UserAction::SOLVED).count
.where(user_id: object.id)
.where(action_type: UserAction::SOLVED)
.count
end end
end end
@ -709,21 +728,19 @@ SQL
end end
register_topic_list_preload_user_ids do |topics, user_ids, topic_list| register_topic_list_preload_user_ids do |topics, user_ids, topic_list|
answer_post_ids = TopicCustomField answer_post_ids =
.select('value::INTEGER') TopicCustomField
.where(name: 'accepted_answer_post_id') .select("value::INTEGER")
.where(name: "accepted_answer_post_id")
.where(topic_id: topics.map(&:id)) .where(topic_id: topics.map(&:id))
answer_user_ids = Post answer_user_ids = Post.where(id: answer_post_ids).pluck(:topic_id, :user_id).to_h
.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] } topics.each { |topic| topic.accepted_answer_user_id = answer_user_ids[topic.id] }
user_ids.concat(answer_user_ids.values) user_ids.concat(answer_user_ids.values)
end end
module AddSolvedToTopicPostersSummary module AddSolvedToTopicPostersSummary
def descriptions_by_id def descriptions_by_id
if !defined? @descriptions_by_id if !defined?(@descriptions_by_id)
super(ids: old_user_ids) super(ids: old_user_ids)
if id = topic.accepted_answer_user_id if id = topic.accepted_answer_user_id
@ -749,7 +766,7 @@ SQL
end end
TopicPostersSummary.class_eval do TopicPostersSummary.class_eval do
alias :old_user_ids :user_ids alias old_user_ids user_ids
prepend AddSolvedToTopicPostersSummary prepend AddSolvedToTopicPostersSummary
end end
@ -762,28 +779,45 @@ SQL
# we prefer to abstract logic in service object and test this # we prefer to abstract logic in service object and test this
next if Rails.env.test? next if Rails.env.test?
name = 'first_accepted_solution' name = "first_accepted_solution"
DiscourseAutomation::Automation.where(trigger: name, enabled: true).find_each do |automation| DiscourseAutomation::Automation
maximum_trust_level = automation.trigger_field('maximum_trust_level')&.dig('value') .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) if FirstAcceptedPostSolutionValidator.check(post, trust_level: maximum_trust_level)
automation.trigger!( automation.trigger!(
'kind' => name, "kind" => name,
'accepted_post_id' => post.id, "accepted_post_id" => post.id,
'usernames' => [post.user.username], "usernames" => [post.user.username],
'placeholders' => { "placeholders" => {
'post_url' => Discourse.base_url + post.url "post_url" => Discourse.base_url + post.url,
} },
) )
end end
end end
end end
TRUST_LEVELS = [ TRUST_LEVELS = [
{ 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: 1,
{ id: 3, name: 'discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl3' }, name: "discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl1",
{ 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' }, {
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",
},
] ]
add_triggerable_to_scriptable(:first_accepted_solution, :send_pms) add_triggerable_to_scriptable(:first_accepted_solution, :send_pms)
@ -791,7 +825,12 @@ SQL
DiscourseAutomation::Triggerable.add(:first_accepted_solution) do DiscourseAutomation::Triggerable.add(:first_accepted_solution) do
placeholder :post_url placeholder :post_url
field :maximum_trust_level, component: :choices, extra: { content: TRUST_LEVELS }, required: true field :maximum_trust_level,
component: :choices,
extra: {
content: TRUST_LEVELS,
},
required: true
end end
end end
end end

View File

@ -1,38 +1,55 @@
# encoding: utf-8 # encoding: utf-8
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
require 'composer_messages_finder' require "composer_messages_finder"
describe ComposerMessagesFinder do describe ComposerMessagesFinder do
describe '.check_topic_is_solved' do describe ".check_topic_is_solved" do
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
fab!(:topic) { Fabricate(:topic) } fab!(:topic) { Fabricate(:topic) }
fab!(:post) { Fabricate(:post, topic: topic, user: Fabricate(:user)) } fab!(:post) { Fabricate(:post, topic: topic, user: Fabricate(:user)) }
before do before { SiteSetting.disable_solved_education_message = false }
SiteSetting.disable_solved_education_message = false
end
it "does not show message without a topic id" do it "does not show message without a topic id" do
expect(described_class.new(user, composer_action: 'createTopic').check_topic_is_solved).to be_blank expect(
expect(described_class.new(user, composer_action: 'reply').check_topic_is_solved).to be_blank described_class.new(user, composer_action: "createTopic").check_topic_is_solved,
).to be_blank
expect(described_class.new(user, composer_action: "reply").check_topic_is_solved).to be_blank
end end
describe "a reply" do describe "a reply" do
it "does not show message if topic is not solved" do it "does not show message if topic is not solved" do
expect(described_class.new(user, composer_action: 'reply', topic_id: topic.id).check_topic_is_solved).to be_blank expect(
described_class.new(
user,
composer_action: "reply",
topic_id: topic.id,
).check_topic_is_solved,
).to be_blank
end end
it "does not show message if disable_solved_education_message is true" do it "does not show message if disable_solved_education_message is true" do
SiteSetting.disable_solved_education_message = true SiteSetting.disable_solved_education_message = true
DiscourseSolved.accept_answer!(post, Discourse.system_user) DiscourseSolved.accept_answer!(post, Discourse.system_user)
expect(described_class.new(user, composer_action: 'reply', topic_id: topic.id).check_topic_is_solved).to be_blank expect(
described_class.new(
user,
composer_action: "reply",
topic_id: topic.id,
).check_topic_is_solved,
).to be_blank
end end
it "shows message if the topic is solved" do it "shows message if the topic is solved" do
DiscourseSolved.accept_answer!(post, Discourse.system_user) DiscourseSolved.accept_answer!(post, Discourse.system_user)
message = described_class.new(user, composer_action: 'reply', topic_id: topic.id).check_topic_is_solved message =
described_class.new(
user,
composer_action: "reply",
topic_id: topic.id,
).check_topic_is_solved
expect(message).not_to be_blank expect(message).not_to be_blank
expect(message[:body]).to include("This topic has been solved") expect(message[:body]).to include("This topic has been solved")
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
require 'post_revisor' require "post_revisor"
describe PostRevisor do describe PostRevisor do
fab!(:category) { Fabricate(:category_with_definition) } fab!(:category) { Fabricate(:category_with_definition) }
@ -17,20 +17,22 @@ describe PostRevisor do
topic = Fabricate(:topic, category: Fabricate(:category_with_definition)) topic = Fabricate(:topic, category: Fabricate(:category_with_definition))
post = Fabricate(:post, topic: topic) post = Fabricate(:post, topic: topic)
messages = MessageBus.track_publish("/topic/#{topic.id}") do messages =
MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, { category_id: category.id }) described_class.new(post).revise!(admin, { category_id: category.id })
end end
expect(messages.first.data[:refresh_stream]).to eq(nil) expect(messages.first.data[:refresh_stream]).to eq(nil)
messages = MessageBus.track_publish("/topic/#{topic.id}") do messages =
MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, { category_id: category_solved.id }) described_class.new(post).revise!(admin, { category_id: category_solved.id })
end end
expect(messages.first.data[:refresh_stream]).to eq(true) expect(messages.first.data[:refresh_stream]).to eq(true)
end end
describe 'Allowing solved via tags' do describe "Allowing solved via tags" do
before do before do
SiteSetting.solved_enabled = true SiteSetting.solved_enabled = true
SiteSetting.tagging_enabled = true SiteSetting.tagging_enabled = true
@ -42,20 +44,22 @@ describe PostRevisor do
fab!(:topic) { Fabricate(:topic) } fab!(:topic) { Fabricate(:topic) }
let(:post) { Fabricate(:post, topic: topic) } let(:post) { Fabricate(:post, topic: topic) }
it 'sets the refresh option after adding an allowed tag' do it "sets the refresh option after adding an allowed tag" do
SiteSetting.enable_solved_tags = tag1.name SiteSetting.enable_solved_tags = tag1.name
messages = MessageBus.track_publish("/topic/#{topic.id}") do messages =
MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, tags: [tag1.name]) described_class.new(post).revise!(admin, tags: [tag1.name])
end end
expect(messages.first.data[:refresh_stream]).to eq(true) expect(messages.first.data[:refresh_stream]).to eq(true)
end end
it 'sets the refresh option if the added tag matches any of the allowed tags' do it "sets the refresh option if the added tag matches any of the allowed tags" do
SiteSetting.enable_solved_tags = [tag1, tag2].map(&:name).join('|') SiteSetting.enable_solved_tags = [tag1, tag2].map(&:name).join("|")
messages = MessageBus.track_publish("/topic/#{topic.id}") do messages =
MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, tags: [tag2.name]) described_class.new(post).revise!(admin, tags: [tag2.name])
end end

View File

@ -1,9 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
Fabricator(:solved_web_hook, from: :web_hook) do Fabricator(:solved_web_hook, from: :web_hook) do
transient solved_hook: WebHookEventType.find_by(name: 'solved') transient solved_hook: WebHookEventType.find_by(name: "solved")
after_build do |web_hook, transients| after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:solved_hook]] }
web_hook.web_hook_event_types = [transients[:solved_hook]]
end
end end

View File

@ -1,18 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe "Managing Posts solved status" do RSpec.describe "Managing Posts solved status" do
let(:topic) { Fabricate(:topic) } let(:topic) { Fabricate(:topic) }
let(:user) { Fabricate(:trust_level_4) } let(:user) { Fabricate(:trust_level_4) }
let(:p1) { Fabricate(:post, topic: topic) } let(:p1) { Fabricate(:post, topic: topic) }
before do before { SiteSetting.allow_solved_on_all_topics = true }
SiteSetting.allow_solved_on_all_topics = true
end
describe 'auto bump' do describe "auto bump" do
it 'does not automatically bump solved topics' do it "does not automatically bump solved topics" do
category = Fabricate(:category_with_definition) category = Fabricate(:category_with_definition)
post = create_post(category: category) post = create_post(category: category)
@ -36,13 +34,13 @@ RSpec.describe "Managing Posts solved status" do
end end
end end
describe 'accepting a post as the answer' do describe "accepting a post as the answer" do
before do before do
sign_in(user) sign_in(user)
SiteSetting.solved_topics_auto_close_hours = 2 SiteSetting.solved_topics_auto_close_hours = 2
end end
it 'can mark a post as the accepted answer correctly' do it "can mark a post as the accepted answer correctly" do
freeze_time freeze_time
post "/solution/accept.json", params: { id: p1.id } post "/solution/accept.json", params: { id: p1.id }
@ -52,20 +50,18 @@ RSpec.describe "Managing Posts solved status" do
topic.reload topic.reload
expect(topic.public_topic_timer.status_type) expect(topic.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
.to eq(TopicTimer.types[:silent_close])
expect(topic.custom_fields[ expect(topic.custom_fields[DiscourseSolved::AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD].to_i).to eq(
DiscourseSolved::AUTO_CLOSE_TOPIC_TIMER_CUSTOM_FIELD topic.public_topic_timer.id,
].to_i).to eq(topic.public_topic_timer.id) )
expect(topic.public_topic_timer.execute_at) expect(topic.public_topic_timer.execute_at).to eq_time(Time.zone.now + 2.hours)
.to eq_time(Time.zone.now + 2.hours)
expect(topic.public_topic_timer.based_on_last_post).to eq(true) expect(topic.public_topic_timer.based_on_last_post).to eq(true)
end end
it 'sends notifications to correct users' do it "sends notifications to correct users" do
SiteSetting.notify_on_staff_accept_solved = true SiteSetting.notify_on_staff_accept_solved = true
user = Fabricate(:user) user = Fabricate(:user)
topic = Fabricate(:topic, user: user) topic = Fabricate(:topic, user: user)
@ -74,11 +70,9 @@ RSpec.describe "Managing Posts solved status" do
op = topic.user op = topic.user
user = post.user user = post.user
expect { expect { DiscourseSolved.accept_answer!(post, Discourse.system_user) }.to change {
DiscourseSolved.accept_answer!(post, Discourse.system_user) user.notifications.count
}.to \ }.by(1) & change { op.notifications.count }.by(1)
change { user.notifications.count }.by(1) &
change { op.notifications.count }.by(1)
notification = user.notifications.last notification = user.notifications.last
expect(notification.notification_type).to eq(Notification.types[:custom]) expect(notification.notification_type).to eq(Notification.types[:custom])
@ -91,7 +85,7 @@ RSpec.describe "Managing Posts solved status" do
expect(notification.post_number).to eq(post.post_number) expect(notification.post_number).to eq(post.post_number)
end end
it 'does not set a timer when the topic is closed' do it "does not set a timer when the topic is closed" do
topic.update!(closed: true) topic.update!(closed: true)
post "/solution/accept.json", params: { id: p1.id } post "/solution/accept.json", params: { id: p1.id }
@ -105,7 +99,7 @@ RSpec.describe "Managing Posts solved status" do
expect(topic.closed).to eq(true) expect(topic.closed).to eq(true)
end end
it 'works with staff and trashed topics' do it "works with staff and trashed topics" do
topic.trash!(Discourse.system_user) topic.trash!(Discourse.system_user)
post "/solution/accept.json", params: { id: p1.id } post "/solution/accept.json", params: { id: p1.id }
@ -119,7 +113,7 @@ RSpec.describe "Managing Posts solved status" do
expect(p1.custom_fields["is_accepted_answer"]).to eq("true") expect(p1.custom_fields["is_accepted_answer"]).to eq("true")
end end
it 'does not allow you to accept a whisper' do it "does not allow you to accept a whisper" do
whisper = Fabricate(:post, topic: topic, post_type: Post.types[:whisper]) whisper = Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
sign_in(Fabricate(:admin)) sign_in(Fabricate(:admin))
@ -127,7 +121,7 @@ RSpec.describe "Managing Posts solved status" do
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
it 'triggers a webhook' do it "triggers a webhook" do
Fabricate(:solved_web_hook) Fabricate(:solved_web_hook)
post "/solution/accept.json", params: { id: p1.id } post "/solution/accept.json", params: { id: p1.id }
@ -139,21 +133,19 @@ RSpec.describe "Managing Posts solved status" do
end end
end end
describe '#unaccept' do describe "#unaccept" do
before do before { sign_in(user) }
sign_in(user)
end
describe 'when solved_topics_auto_close_hours is enabled' do describe "when solved_topics_auto_close_hours is enabled" do
before do before do
SiteSetting.solved_topics_auto_close_hours = 2 SiteSetting.solved_topics_auto_close_hours = 2
DiscourseSolved.accept_answer!(p1, user) DiscourseSolved.accept_answer!(p1, user)
end end
it 'should unmark the post as solved' do it "should unmark the post as solved" do
expect do expect do post "/solution/unaccept.json", params: { id: p1.id } end.to change {
post "/solution/unaccept.json", params: { id: p1.id } topic.reload.public_topic_timer
end.to change { topic.reload.public_topic_timer }.to(nil) }.to(nil)
expect(response.status).to eq(200) expect(response.status).to eq(200)
p1.reload p1.reload
@ -161,10 +153,9 @@ RSpec.describe "Managing Posts solved status" do
expect(p1.custom_fields["is_accepted_answer"]).to eq(nil) expect(p1.custom_fields["is_accepted_answer"]).to eq(nil)
expect(p1.topic.custom_fields["accepted_answer_post_id"]).to eq(nil) expect(p1.topic.custom_fields["accepted_answer_post_id"]).to eq(nil)
end end
end end
it 'triggers a webhook' do it "triggers a webhook" do
Fabricate(:solved_web_hook) Fabricate(:solved_web_hook)
post "/solution/unaccept.json", params: { id: p1.id } post "/solution/unaccept.json", params: { id: p1.id }
@ -176,7 +167,7 @@ RSpec.describe "Managing Posts solved status" do
end end
end end
context 'with group moderators' do context "with group moderators" do
fab!(:group_user) { Fabricate(:group_user) } fab!(:group_user) { Fabricate(:group_user) }
let(:user_gm) { group_user.user } let(:user_gm) { group_user.user }
let(:group) { group_user.group } let(:group) { group_user.group }
@ -187,7 +178,7 @@ RSpec.describe "Managing Posts solved status" do
sign_in(user_gm) sign_in(user_gm)
end end
it 'can accept a solution' do it "can accept a solution" do
post "/solution/accept.json", params: { id: p1.id } post "/solution/accept.json", params: { id: p1.id }
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end

View File

@ -1,71 +1,76 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
describe FirstAcceptedPostSolutionValidator do describe FirstAcceptedPostSolutionValidator do
fab!(:user_tl1) { Fabricate(:user, trust_level: TrustLevel[1]) } fab!(:user_tl1) { Fabricate(:user, trust_level: TrustLevel[1]) }
context 'when user is under max trust level' do context "when user is under max trust level" do
context 'with no post accepted yet' do context "with no post accepted yet" do
it 'validates the post' do it "validates the post" do
post_1 = create_post(user: user_tl1) post_1 = create_post(user: user_tl1)
expect(described_class.check(post_1, trust_level: TrustLevel[2])).to eq(true) expect(described_class.check(post_1, trust_level: TrustLevel[2])).to eq(true)
end end
end end
context 'with already had accepted posts' do context "with already had accepted posts" do
before do before do
accepted_post = create_post(user: user_tl1) accepted_post = create_post(user: user_tl1)
DiscourseSolved.accept_answer!(accepted_post, Discourse.system_user) DiscourseSolved.accept_answer!(accepted_post, Discourse.system_user)
end end
it 'doesnt validate the post' do it "doesnt validate the post" do
post_1 = create_post(user: user_tl1) post_1 = create_post(user: user_tl1)
expect(described_class.check(post_1, trust_level: TrustLevel[2])).to eq(false) expect(described_class.check(post_1, trust_level: TrustLevel[2])).to eq(false)
end end
end end
end end
context 'when a user is above or equal max trust level' do context "when a user is above or equal max trust level" do
context 'with no post accepted yet' do context "with no post accepted yet" do
it 'doesnt validate the post' do it "doesnt validate the post" do
post_1 = create_post(user: user_tl1) post_1 = create_post(user: user_tl1)
expect(described_class.check(post_1, trust_level: TrustLevel[1])).to eq(false) expect(described_class.check(post_1, trust_level: TrustLevel[1])).to eq(false)
end end
end end
context 'when a post is already accepted' do context "when a post is already accepted" do
before do before do
accepted_post = create_post(user: user_tl1) accepted_post = create_post(user: user_tl1)
DiscourseSolved.accept_answer!(accepted_post, Discourse.system_user) DiscourseSolved.accept_answer!(accepted_post, Discourse.system_user)
end end
it 'doesnt validate the post' do it "doesnt validate the post" do
post_1 = create_post(user: user_tl1) post_1 = create_post(user: user_tl1)
expect(described_class.check(post_1, trust_level: TrustLevel[1])).to eq(false) expect(described_class.check(post_1, trust_level: TrustLevel[1])).to eq(false)
end end
end end
end end
context 'when using any trust level' do context "when using any trust level" do
it 'validates the post' do it "validates the post" do
post_1 = create_post(user: user_tl1) post_1 = create_post(user: user_tl1)
expect(described_class.check(post_1, trust_level: 'any')).to eq(true) expect(described_class.check(post_1, trust_level: "any")).to eq(true)
end end
end end
context 'when user is system' do context "when user is system" do
it 'doesnt validate the post' do it "doesnt validate the post" do
post_1 = create_post(user: Discourse.system_user) post_1 = create_post(user: Discourse.system_user)
expect(described_class.check(post_1, trust_level: 'any')).to eq(false) expect(described_class.check(post_1, trust_level: "any")).to eq(false)
end end
end end
context 'when post is a PM' do context "when post is a PM" do
it 'doesnt validate the post' do it "doesnt validate the post" do
Group.refresh_automatic_groups! Group.refresh_automatic_groups!
post_1 = create_post(user: user_tl1, target_usernames: [user_tl1.username], archetype: Archetype.private_message) post_1 =
expect(described_class.check(post_1, trust_level: 'any')).to eq(false) create_post(
user: user_tl1,
target_usernames: [user_tl1.username],
archetype: Archetype.private_message,
)
expect(described_class.check(post_1, trust_level: "any")).to eq(false)
end end
end end
end end

View File

@ -1,15 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
require_dependency 'site' require_dependency "site"
describe Site do describe Site do
let(:category) { Fabricate(:category) } let(:category) { Fabricate(:category) }
let(:guardian) { Guardian.new } let(:guardian) { Guardian.new }
before do before { SiteSetting.show_filter_by_solved_status = true }
SiteSetting.show_filter_by_solved_status = true
end
it "includes `enable_accepted_answers` custom field for categories" do it "includes `enable_accepted_answers` custom field for categories" do
category.custom_fields["enable_accepted_answers"] = true category.custom_fields["enable_accepted_answers"] = true

View File

@ -1,21 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe ListController do RSpec.describe ListController do
fab!(:p1) { Fabricate(:post) } fab!(:p1) { Fabricate(:post) }
fab!(:p2) { Fabricate(:post, topic: p1.topic) } fab!(:p2) { Fabricate(:post, topic: p1.topic) }
fab!(:p3) { Fabricate(:post, topic: p1.topic) } fab!(:p3) { Fabricate(:post, topic: p1.topic) }
before do before { SiteSetting.allow_solved_on_all_topics = true }
SiteSetting.allow_solved_on_all_topics = true
end
it 'shows the user who posted the accepted answer second' do it "shows the user who posted the accepted answer second" do
TopicFeaturedUsers.ensure_consistency! TopicFeaturedUsers.ensure_consistency!
DiscourseSolved.accept_answer!(p3, p1.user, topic: p1.topic) DiscourseSolved.accept_answer!(p3, p1.user, topic: p1.topic)
get '/latest.json' get "/latest.json"
posters = response.parsed_body["topic_list"]["topics"].first["posters"] posters = response.parsed_body["topic_list"]["topics"].first["posters"]
expect(posters[0]["user_id"]).to eq(p1.user_id) expect(posters[0]["user_id"]).to eq(p1.user_id)
expect(posters[1]["user_id"]).to eq(p3.user_id) expect(posters[1]["user_id"]).to eq(p3.user_id)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe TopicsController do RSpec.describe TopicsController do
let(:p1) { Fabricate(:post, like_count: 1) } let(:p1) { Fabricate(:post, like_count: 1) }
@ -9,34 +9,37 @@ RSpec.describe TopicsController do
def schema_json(answerCount) def schema_json(answerCount)
if answerCount > 0 if answerCount > 0
answer_json = ',"acceptedAnswer":{"@type":"Answer","text":"%{answer_text}","upvoteCount":%{answer_likes},"dateCreated":"%{answered_at}","url":"%{answer_url}","author":{"@type":"Person","name":"%{username2}"}}' % { answer_json =
',"acceptedAnswer":{"@type":"Answer","text":"%{answer_text}","upvoteCount":%{answer_likes},"dateCreated":"%{answered_at}","url":"%{answer_url}","author":{"@type":"Person","name":"%{username2}"}}' %
{
answer_text: p2.excerpt, answer_text: p2.excerpt,
answer_likes: p2.like_count, answer_likes: p2.like_count,
answered_at: p2.created_at.as_json, answered_at: p2.created_at.as_json,
answer_url: p2.full_url, answer_url: p2.full_url,
username2: p2.user&.username username2: p2.user&.username,
} }
else else
answer_json = "" answer_json = ""
end end
'<script type="application/ld+json">{"@context":"http://schema.org","@type":"QAPage","name":"%{title}","mainEntity":{"@type":"Question","name":"%{title}","text":"%{question_text}","upvoteCount":%{question_likes},"answerCount":%{answerCount},"dateCreated":"%{created_at}","author":{"@type":"Person","name":"%{username1}"}%{answer_json}}}</script>' % { # rubocop:todo Layout/LineLength
'<script type="application/ld+json">{"@context":"http://schema.org","@type":"QAPage","name":"%{title}","mainEntity":{"@type":"Question","name":"%{title}","text":"%{question_text}","upvoteCount":%{question_likes},"answerCount":%{answerCount},"dateCreated":"%{created_at}","author":{"@type":"Person","name":"%{username1}"}%{answer_json}}}</script>' %
# rubocop:enable Layout/LineLength
{
title: topic.title, title: topic.title,
question_text: p1.excerpt, question_text: p1.excerpt,
question_likes: p1.like_count, question_likes: p1.like_count,
answerCount: answerCount, answerCount: answerCount,
created_at: topic.created_at.as_json, created_at: topic.created_at.as_json,
username1: topic.user&.name, username1: topic.user&.name,
answer_json: answer_json answer_json: answer_json,
} }
end end
context 'with solved enabled on every topic' do context "with solved enabled on every topic" do
before do before { SiteSetting.allow_solved_on_all_topics = true }
SiteSetting.allow_solved_on_all_topics = true
end
it 'should include correct schema information in header' do it "should include correct schema information in header" do
get "/t/#{topic.slug}/#{topic.id}" get "/t/#{topic.slug}/#{topic.id}"
expect(response.body).to include(schema_json(0)) expect(response.body).to include(schema_json(0))
@ -51,7 +54,7 @@ RSpec.describe TopicsController do
expect(response.body).to include(schema_json(1)) expect(response.body).to include(schema_json(1))
end end
it 'should include quoted content in schema information' do it "should include quoted content in schema information" do
post = topic.first_post post = topic.first_post
post.raw = "[quote]This is a quoted text.[/quote]" post.raw = "[quote]This is a quoted text.[/quote]"
post.save! post.save!
@ -63,12 +66,12 @@ RSpec.describe TopicsController do
end end
end end
context 'with solved enabled for topics with specific tags' do context "with solved enabled for topics with specific tags" do
let(:tag) { Fabricate(:tag) } let(:tag) { Fabricate(:tag) }
before { SiteSetting.enable_solved_tags = tag.name } before { SiteSetting.enable_solved_tags = tag.name }
it 'includes the correct schema information' do it "includes the correct schema information" do
DiscourseTagging.add_or_create_tags_by_name(topic, [tag.name]) DiscourseTagging.add_or_create_tags_by_name(topic, [tag.name])
p2.custom_fields["is_accepted_answer"] = true p2.custom_fields["is_accepted_answer"] = true
p2.save_custom_fields p2.save_custom_fields

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
describe TopicAnswerMixin do describe TopicAnswerMixin do
let(:topic) { Fabricate(:topic) } let(:topic) { Fabricate(:topic) }
@ -17,7 +17,7 @@ describe TopicAnswerMixin do
TopicListItemSerializer, TopicListItemSerializer,
SearchTopicListItemSerializer, SearchTopicListItemSerializer,
SuggestedTopicSerializer, SuggestedTopicSerializer,
UserSummarySerializer::TopicSerializer UserSummarySerializer::TopicSerializer,
].each do |serializer| ].each do |serializer|
json = serializer.new(topic, scope: guardian, root: false).as_json json = serializer.new(topic, scope: guardian, root: false).as_json
expect(json[:has_accepted_answer]).to be_truthy expect(json[:has_accepted_answer]).to be_truthy

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
describe UserCardSerializer do describe UserCardSerializer do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }