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
if: ${{ !cancelled() }}
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
id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)"
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache
uses: actions/cache@v3
@ -130,7 +130,7 @@ jobs:
shell: bash
run: |
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
- name: Plugin RSpec
@ -142,7 +142,7 @@ jobs:
shell: bash
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
echo "::set-output name=files_exist::true"
echo "files_exist=true" >> $GITHUB_OUTPUT
fi
- name: Plugin QUnit

View File

@ -1,2 +1,2 @@
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
source 'https://rubygems.org'
source "https://rubygems.org"
group :development do
gem 'rubocop-discourse'
gem "rubocop-discourse"
gem "syntax_tree"
end

View File

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

View File

@ -4,11 +4,9 @@ class FirstAcceptedPostSolutionValidator
def self.check(post, trust_level:)
return false if post.archetype != Archetype.default
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
end
return false if TrustLevel.compare(post&.user&.trust_level, trust_level.to_i)
if !UserAction.where(user_id: post&.user_id, action_type: UserAction::SOLVED).exists?
return true

View File

@ -12,9 +12,9 @@ class EnsuresUniqueAcceptedAnswerPostId < ActiveRecord::Migration[5.2]
SQL
add_index :topic_custom_fields,
:topic_id,
name: :idx_topic_custom_fields_accepted_answer,
unique: true,
where: "name = 'accepted_answer_post_id'"
:topic_id,
name: :idx_topic_custom_fields_accepted_answer,
unique: true,
where: "name = 'accepted_answer_post_id'"
end
end

View File

@ -18,7 +18,7 @@ class RenameBadges < ActiveRecord::Migration[6.1]
"sv" => "Kundtjänst",
"tr_TR" => "Yardım masası",
"zh_CN" => "帮助台",
"zh_TW" => "服務台"
"zh_TW" => "服務台",
}
TECH_SUPPORT_TRANSLATIONS = {
@ -43,11 +43,12 @@ class RenameBadges < ActiveRecord::Migration[6.1]
"sv" => "Teknisk support",
"tr_TR" => "Teknik Destek",
"zh_CN" => "技术支持",
"zh_TW" => "技術支援"
"zh_TW" => "技術支援",
}
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
UPDATE badges

417
plugin.rb
View File

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

View File

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

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'rails_helper'
require 'post_revisor'
require "rails_helper"
require "post_revisor"
describe PostRevisor do
fab!(:category) { Fabricate(:category_with_definition) }
@ -17,20 +17,22 @@ describe PostRevisor do
topic = Fabricate(:topic, category: Fabricate(:category_with_definition))
post = Fabricate(:post, topic: topic)
messages = MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, { category_id: category.id })
end
messages =
MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, { category_id: category.id })
end
expect(messages.first.data[:refresh_stream]).to eq(nil)
messages = MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, { category_id: category_solved.id })
end
messages =
MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, { category_id: category_solved.id })
end
expect(messages.first.data[:refresh_stream]).to eq(true)
end
describe 'Allowing solved via tags' do
describe "Allowing solved via tags" do
before do
SiteSetting.solved_enabled = true
SiteSetting.tagging_enabled = true
@ -42,22 +44,24 @@ describe PostRevisor do
fab!(:topic) { Fabricate(: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
messages = MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, tags: [tag1.name])
end
messages =
MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, tags: [tag1.name])
end
expect(messages.first.data[:refresh_stream]).to eq(true)
end
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('|')
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("|")
messages = MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, tags: [tag2.name])
end
messages =
MessageBus.track_publish("/topic/#{topic.id}") do
described_class.new(post).revise!(admin, tags: [tag2.name])
end
expect(messages.first.data[:refresh_stream]).to eq(true)
end

View File

@ -1,9 +1,7 @@
# frozen_string_literal: true
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|
web_hook.web_hook_event_types = [transients[:solved_hook]]
end
after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:solved_hook]] }
end

View File

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

View File

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

View File

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

View File

@ -1,21 +1,19 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe ListController do
fab!(:p1) { Fabricate(:post) }
fab!(:p2) { Fabricate(:post, topic: p1.topic) }
fab!(:p3) { Fabricate(:post, topic: p1.topic) }
before do
SiteSetting.allow_solved_on_all_topics = true
end
before { SiteSetting.allow_solved_on_all_topics = true }
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!
DiscourseSolved.accept_answer!(p3, p1.user, topic: p1.topic)
get '/latest.json'
get "/latest.json"
posters = response.parsed_body["topic_list"]["topics"].first["posters"]
expect(posters[0]["user_id"]).to eq(p1.user_id)
expect(posters[1]["user_id"]).to eq(p3.user_id)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
RSpec.describe TopicsController do
let(:p1) { Fabricate(:post, like_count: 1) }
@ -9,34 +9,37 @@ RSpec.describe TopicsController do
def schema_json(answerCount)
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_text: p2.excerpt,
answer_likes: p2.like_count,
answered_at: p2.created_at.as_json,
answer_url: p2.full_url,
username2: p2.user&.username
}
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_likes: p2.like_count,
answered_at: p2.created_at.as_json,
answer_url: p2.full_url,
username2: p2.user&.username,
}
else
answer_json = ""
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>' % {
title: topic.title,
question_text: p1.excerpt,
question_likes: p1.like_count,
answerCount: answerCount,
created_at: topic.created_at.as_json,
username1: topic.user&.name,
answer_json: answer_json
}
# 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,
question_text: p1.excerpt,
question_likes: p1.like_count,
answerCount: answerCount,
created_at: topic.created_at.as_json,
username1: topic.user&.name,
answer_json: answer_json,
}
end
context 'with solved enabled on every topic' do
before do
SiteSetting.allow_solved_on_all_topics = true
end
context "with solved enabled on every topic" do
before { SiteSetting.allow_solved_on_all_topics = true }
it 'should include correct schema information in header' do
it "should include correct schema information in header" do
get "/t/#{topic.slug}/#{topic.id}"
expect(response.body).to include(schema_json(0))
@ -51,7 +54,7 @@ RSpec.describe TopicsController do
expect(response.body).to include(schema_json(1))
end
it 'should include quoted content in schema information' do
it "should include quoted content in schema information" do
post = topic.first_post
post.raw = "[quote]This is a quoted text.[/quote]"
post.save!
@ -63,12 +66,12 @@ RSpec.describe TopicsController do
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) }
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])
p2.custom_fields["is_accepted_answer"] = true
p2.save_custom_fields

View File

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

View File

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