diff --git a/.github/workflows/plugin-linting.yml b/.github/workflows/plugin-linting.yml index c807794..6d2bb97 100644 --- a/.github/workflows/plugin-linting.yml +++ b/.github/workflows/plugin-linting.yml @@ -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 diff --git a/.github/workflows/plugin-tests.yml b/.github/workflows/plugin-tests.yml index 9d390bc..f30a5be 100644 --- a/.github/workflows/plugin-tests.yml +++ b/.github/workflows/plugin-tests.yml @@ -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 diff --git a/.rubocop.yml b/.rubocop.yml index d46296c..fb14dfa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,2 +1,2 @@ inherit_gem: - rubocop-discourse: default.yml + rubocop-discourse: stree-compat.yml diff --git a/.streerc b/.streerc new file mode 100644 index 0000000..0bc4379 --- /dev/null +++ b/.streerc @@ -0,0 +1,2 @@ +--print-width=100 +--plugins=plugin/trailing_comma diff --git a/Gemfile b/Gemfile index 7da32ec..31d8bf7 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 87ff84d..478c5ee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/lib/first_accepted_post_solution_validator.rb b/app/lib/first_accepted_post_solution_validator.rb index 898e213..b3934fa 100644 --- a/app/lib/first_accepted_post_solution_validator.rb +++ b/app/lib/first_accepted_post_solution_validator.rb @@ -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 diff --git a/db/migrate/20191209095548_ensures_unique_accepted_answer_post_id.rb b/db/migrate/20191209095548_ensures_unique_accepted_answer_post_id.rb index 1ae1bf9..90f6aaa 100644 --- a/db/migrate/20191209095548_ensures_unique_accepted_answer_post_id.rb +++ b/db/migrate/20191209095548_ensures_unique_accepted_answer_post_id.rb @@ -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 diff --git a/db/migrate/20221121223417_rename_badges.rb b/db/migrate/20221121223417_rename_badges.rb index fceadaf..98c1db8 100644 --- a/db/migrate/20221121223417_rename_badges.rb +++ b/db/migrate/20221121223417_rename_badges.rb @@ -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 diff --git a/plugin.rb b/plugin.rb index 855e055..8dc6fc4 100644 --- a/plugin.rb +++ b/plugin.rb @@ -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 = < 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 - [''].join("") + [ + '", + ].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 diff --git a/spec/components/composer_messages_finder_spec.rb b/spec/components/composer_messages_finder_spec.rb index 9f17b07..8cd15ec 100644 --- a/spec/components/composer_messages_finder_spec.rb +++ b/spec/components/composer_messages_finder_spec.rb @@ -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 diff --git a/spec/components/post_revisor_spec.rb b/spec/components/post_revisor_spec.rb index e71588b..77a2df5 100644 --- a/spec/components/post_revisor_spec.rb +++ b/spec/components/post_revisor_spec.rb @@ -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 diff --git a/spec/fabricators/solved_hook_fabricator.rb b/spec/fabricators/solved_hook_fabricator.rb index 641fef8..fb76844 100644 --- a/spec/fabricators/solved_hook_fabricator.rb +++ b/spec/fabricators/solved_hook_fabricator.rb @@ -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 diff --git a/spec/integration/solved_spec.rb b/spec/integration/solved_spec.rb index 80c8696..40013cf 100644 --- a/spec/integration/solved_spec.rb +++ b/spec/integration/solved_spec.rb @@ -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 diff --git a/spec/lib/first_accepted_post_solution_validator_spec.rb b/spec/lib/first_accepted_post_solution_validator_spec.rb index 0188b29..1bb153a 100644 --- a/spec/lib/first_accepted_post_solution_validator_spec.rb +++ b/spec/lib/first_accepted_post_solution_validator_spec.rb @@ -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 'doesn’t validate the post' do + it "doesn’t 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 'doesn’t validate the post' do + context "when a user is above or equal max trust level" do + context "with no post accepted yet" do + it "doesn’t 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 'doesn’t validate the post' do + it "doesn’t 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 'doesn’t validate the post' do + context "when user is system" do + it "doesn’t 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 'doesn’t validate the post' do + context "when post is a PM" do + it "doesn’t 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 diff --git a/spec/models/site_spec.rb b/spec/models/site_spec.rb index 9b815b4..5b58c87 100644 --- a/spec/models/site_spec.rb +++ b/spec/models/site_spec.rb @@ -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 diff --git a/spec/requests/list_controller_spec.rb b/spec/requests/list_controller_spec.rb index c3803c6..386c40a 100644 --- a/spec/requests/list_controller_spec.rb +++ b/spec/requests/list_controller_spec.rb @@ -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) diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb index c7c624f..379160b 100644 --- a/spec/requests/topics_controller_spec.rb +++ b/spec/requests/topics_controller_spec.rb @@ -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 - '' % { - 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 + '' % + # 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 diff --git a/spec/serializers/topic_answer_mixin_spec.rb b/spec/serializers/topic_answer_mixin_spec.rb index 64b0faa..d92831c 100644 --- a/spec/serializers/topic_answer_mixin_spec.rb +++ b/spec/serializers/topic_answer_mixin_spec.rb @@ -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 diff --git a/spec/serializers/user_card_serializer_spec.rb b/spec/serializers/user_card_serializer_spec.rb index fe655d1..d0a9ce4 100644 --- a/spec/serializers/user_card_serializer_spec.rb +++ b/spec/serializers/user_card_serializer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" describe UserCardSerializer do let(:user) { Fabricate(:user) }