From adba4d9d6e246ea9d128a8ee4c9b673b224c2cc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 18:00:04 +0200 Subject: [PATCH 01/12] Bump rack from 3.1.12 to 3.1.16 (#371) Bumps [rack](https://github.com/rack/rack) from 3.1.12 to 3.1.16. - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](https://github.com/rack/rack/compare/v3.1.12...v3.1.16) --- updated-dependencies: - dependency-name: rack dependency-version: 3.1.16 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c5be1a9..4b6dbc6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -35,7 +35,7 @@ GEM prettier_print (1.2.1) prism (1.4.0) racc (1.8.1) - rack (3.1.15) + rack (3.1.16) rainbow (3.1.1) regexp_parser (2.10.0) rubocop (1.75.8) From cee0ffc19906edd7469301fc96bb492b89d21b88 Mon Sep 17 00:00:00 2001 From: Discourse Translator Bot Date: Tue, 24 Jun 2025 07:29:51 -0700 Subject: [PATCH 02/12] Update translations (#375) --- config/locales/client.el.yml | 16 ++++++++++++++++ config/locales/server.el.yml | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 5ed917f..e2329f7 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -10,6 +10,7 @@ el: alt: solved: accepted_notification: "αποδεκτό" + solutions: "Λύσεις" solved: title: "Λύθηκε" allow_accepted_answers: "Επιτρέψτε στον κάτοχο και το προσωπικό να επισημάνει μια απάντηση ως λύση" @@ -29,6 +30,13 @@ el: all: "όλα" solved: "λύθηκε" unsolved: "άλυτο" + no_solved_topics_title: "Δεν έχετε λύσει κανένα θέμα ακόμα" + no_solved_topics_title_others: "Ο χρήστης %{username} δεν έχει λύσει κανένα θέμα ακόμα" + no_answer: + title: Έχει απαντηθεί η ερώτηση σας; + description: "Επισημάνετε την απάντηση και βοηθήστε άλλους χρησιμοποιώντας το κουμπί λύσης κάτω από τη σωστή απάντηση." + notification: + title: "η ανάρτηση σας επισημάνθηκε ως λύση" topic_statuses: solved: help: "Αυτό το θέμα έχει μια λύση" @@ -36,13 +44,21 @@ el: advanced: statuses: solved: "έχουν επιλυθεί" + unsolved: "δεν έχουν επιλυθεί" admin: web_hooks: solved_event: group_name: "Επιλύθηκε γεγονός" + api: + scopes: + descriptions: + solved: + answer: Αποδοχή/Απόρριψη μίας λύσης. discourse_automation: triggerables: first_accepted_solution: + max_trust_level: + any: Οποιοδήποτε fields: maximum_trust_level: label: Επίπεδο Εμπιστοσύνης diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index 049ae28..ce69947 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -5,6 +5,7 @@ # https://translate.discourse.org/ el: + accepted_answer: "Αποδεκτή Απάντηση" site_settings: solved_enabled: "Ενεργοποιήστε το πρόσθετο solved, επιτρέψτε στους χρήστες να επιλέγουν λύσεις για θέματα" accept_all_solutions_trust_level: "Απαιτείται ελάχιστο επίπεδο εμπιστοσύνης για αποδοχή λύσεων σε οποιοδήποτε θέμα (ακόμα και όταν δεν είναι OP)" @@ -23,3 +24,7 @@ el: badges: solved_1: name: "Λύθηκε!" + discourse_automation: + triggerables: + first_accepted_solution: + title: Πρώτη αποδεκτή λύση From 0350b46bdafc2cb55ab7ff01dead6bdd2cea1f09 Mon Sep 17 00:00:00 2001 From: Natalie Tay Date: Mon, 30 Jun 2025 15:40:16 +0800 Subject: [PATCH 03/12] FIX: Check if user has existing solution despite trust_level=any for first accepted solution validator used in automation (#377) There exists a bug in the following trigger (see screenshot) where if the user has an existing solution already, they will still pass validation for "first accepted solution" due to the trust level being "any". --- .../first_accepted_post_solution_validator.rb | 12 ++++++------ .../first_accepted_post_solution_validator_spec.rb | 9 +++++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/discourse_solved/first_accepted_post_solution_validator.rb b/lib/discourse_solved/first_accepted_post_solution_validator.rb index 9c1deae..62c8fb6 100644 --- a/lib/discourse_solved/first_accepted_post_solution_validator.rb +++ b/lib/discourse_solved/first_accepted_post_solution_validator.rb @@ -5,15 +5,15 @@ module DiscourseSolved 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 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 + if trust_level != "any" && TrustLevel.compare(post&.user&.trust_level, trust_level.to_i) + return false end - false + !DiscourseSolved::SolvedTopic + .joins(:answer_post) + .where("posts.user_id = ?", post.user_id) + .exists? end end end diff --git a/spec/lib/first_accepted_post_solution_validator_spec.rb b/spec/lib/first_accepted_post_solution_validator_spec.rb index 75139a9..db80a18 100644 --- a/spec/lib/first_accepted_post_solution_validator_spec.rb +++ b/spec/lib/first_accepted_post_solution_validator_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "rails_helper" - describe DiscourseSolved::FirstAcceptedPostSolutionValidator do fab!(:user_tl1) { Fabricate(:user, trust_level: TrustLevel[1], refresh_auto_groups: true) } @@ -52,6 +50,13 @@ describe DiscourseSolved::FirstAcceptedPostSolutionValidator do post_1 = create_post(user: user_tl1) expect(described_class.check(post_1, trust_level: "any")).to eq(true) end + + it "invalidates if post user already has an accepted post" do + accepted_post = create_post(user: user_tl1) + DiscourseSolved.accept_answer!(accepted_post, Discourse.system_user) + post_1 = create_post(user: user_tl1) + expect(described_class.check(post_1, trust_level: "any")).to eq(false) + end end context "when user is system" do From 041b58eed15e4b000f7c4d51025fc24f99bef5e2 Mon Sep 17 00:00:00 2001 From: Natalie Tay Date: Wed, 2 Jul 2025 11:45:14 +0800 Subject: [PATCH 04/12] FIX: Exclude the first post itself when checking if an existing solution from the user exists (#378) This commit makes sure to exclude the first solution when checking for an existing solution and has better tests -- the existing test does not accept the post prior to making the check. --- .../first_accepted_post_solution_validator.rb | 2 +- ...t_accepted_post_solution_validator_spec.rb | 80 +++++++------------ 2 files changed, 32 insertions(+), 50 deletions(-) diff --git a/lib/discourse_solved/first_accepted_post_solution_validator.rb b/lib/discourse_solved/first_accepted_post_solution_validator.rb index 62c8fb6..ae2b4b4 100644 --- a/lib/discourse_solved/first_accepted_post_solution_validator.rb +++ b/lib/discourse_solved/first_accepted_post_solution_validator.rb @@ -12,7 +12,7 @@ module DiscourseSolved !DiscourseSolved::SolvedTopic .joins(:answer_post) - .where("posts.user_id = ?", post.user_id) + .where("posts.user_id = ? AND posts.id != ?", post.user_id, post.id) .exists? end end diff --git a/spec/lib/first_accepted_post_solution_validator_spec.rb b/spec/lib/first_accepted_post_solution_validator_spec.rb index db80a18..87bd027 100644 --- a/spec/lib/first_accepted_post_solution_validator_spec.rb +++ b/spec/lib/first_accepted_post_solution_validator_spec.rb @@ -3,59 +3,41 @@ describe DiscourseSolved::FirstAcceptedPostSolutionValidator do fab!(:user_tl1) { Fabricate(:user, trust_level: TrustLevel[1], refresh_auto_groups: true) } - 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 - 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 - 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 - 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 - 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 - 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 + context "when trust level is 'any'" 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) + post = Fabricate(:post, user: user_tl1) + DiscourseSolved.accept_answer!(post, Discourse.system_user) + + expect(described_class.check(post, trust_level: "any")).to eq(true) end it "invalidates if post user already has an accepted post" do - accepted_post = create_post(user: user_tl1) - DiscourseSolved.accept_answer!(accepted_post, Discourse.system_user) - post_1 = create_post(user: user_tl1) - expect(described_class.check(post_1, trust_level: "any")).to eq(false) + previously_accepted_post = Fabricate(:post, user: user_tl1) + DiscourseSolved.accept_answer!(previously_accepted_post, Discourse.system_user) + + newly_accepted_post = Fabricate(:post, user: user_tl1) + DiscourseSolved.accept_answer!(newly_accepted_post, Discourse.system_user) + + expect(described_class.check(newly_accepted_post, trust_level: "any")).to eq(false) + end + end + + context "with specified trust level that is not 'any'" do + # the automation indicates "users under this Trust Level will trigger this automation" + + it "invalidates if the user is higher than or equal to the specified trust level" do + post = Fabricate(:post, user: user_tl1) + DiscourseSolved.accept_answer!(post, Discourse.system_user) + + expect(described_class.check(post, trust_level: TrustLevel[0])).to eq(false) + expect(described_class.check(post, trust_level: TrustLevel[1])).to eq(false) + end + + it "validates the post when user is under specified trust level" do + post = Fabricate(:post, user: user_tl1) + DiscourseSolved.accept_answer!(post, Discourse.system_user) + + expect(described_class.check(post, trust_level: TrustLevel[2])).to eq(true) end end From f96aceb5f40fc9e478e233bc86aa75a17867e56a Mon Sep 17 00:00:00 2001 From: Natalie Tay Date: Wed, 2 Jul 2025 16:56:12 +0800 Subject: [PATCH 05/12] FIX: Use SolvedTopics to list posts in /activity/solved instead of user actions (#376) In https://github.com/discourse/discourse-solved/pull/342 we moved solutions away from topic_custom_fields into proper tables, with the tables as the proper source of truth to a topic's solution. The user's /my/activity/solved route uses user_actions which is not accurate, and a user has reported a bug where their solution is not reflected there (user actions are not a good representation of what a topic's solution is). This commit introduces - a new route to get solutions, and is mindful `hide_user_profiles_from_public` and such settings - also mindful of PMs and private categories - a new template that makes use of the `` to load posts safely and avoid reimplementation --- .../solved_topics_controller.rb | 39 ++++++ .../solved_post_serializer.rb | 87 +++++++++++++ .../discourse/routes/user-activity-solved.js | 105 +++++++++++++++- .../templates/user-activity-solved.gjs | 16 +++ config/routes.rb | 2 + .../requests/solved_topics_controller_spec.rb | 117 ++++++++++++++++++ spec/system/solved_spec.rb | 13 +- .../acceptance/user-activity-solved-test.js | 4 +- 8 files changed, 374 insertions(+), 9 deletions(-) create mode 100644 app/controllers/discourse_solved/solved_topics_controller.rb create mode 100644 app/serializers/discourse_solved/solved_post_serializer.rb create mode 100644 assets/javascripts/discourse/templates/user-activity-solved.gjs create mode 100644 spec/requests/solved_topics_controller_spec.rb diff --git a/app/controllers/discourse_solved/solved_topics_controller.rb b/app/controllers/discourse_solved/solved_topics_controller.rb new file mode 100644 index 0000000..8b6dcd4 --- /dev/null +++ b/app/controllers/discourse_solved/solved_topics_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class DiscourseSolved::SolvedTopicsController < ::ApplicationController + requires_plugin DiscourseSolved::PLUGIN_NAME + + def by_user + params.require(:username) + user = + fetch_user_from_params( + include_inactive: + current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts), + ) + raise Discourse::NotFound unless guardian.public_can_see_profiles? + raise Discourse::NotFound unless guardian.can_see_profile?(user) + + offset = [0, params[:offset].to_i].max + limit = params.fetch(:limit, 30).to_i + + posts = + Post + .joins( + "INNER JOIN discourse_solved_solved_topics ON discourse_solved_solved_topics.answer_post_id = posts.id", + ) + .joins(:topic) + .joins("LEFT JOIN categories ON categories.id = topics.category_id") + .where(user_id: user.id, deleted_at: nil) + .where(topics: { archetype: Archetype.default, deleted_at: nil }) + .where( + "topics.category_id IS NULL OR NOT categories.read_restricted OR topics.category_id IN (:secure_category_ids)", + secure_category_ids: guardian.secure_category_ids, + ) + .includes(:user, topic: %i[category tags]) + .order("discourse_solved_solved_topics.created_at DESC") + .offset(offset) + .limit(limit) + + render_serialized(posts, DiscourseSolved::SolvedPostSerializer, root: "user_solved_posts") + end +end diff --git a/app/serializers/discourse_solved/solved_post_serializer.rb b/app/serializers/discourse_solved/solved_post_serializer.rb new file mode 100644 index 0000000..b19a7c9 --- /dev/null +++ b/app/serializers/discourse_solved/solved_post_serializer.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +class DiscourseSolved::SolvedPostSerializer < ApplicationSerializer + attributes :created_at, + :archived, + :avatar_template, + :category_id, + :closed, + :cooked, + :excerpt, + :name, + :post_id, + :post_number, + :post_type, + :raw, + :slug, + :topic_id, + :topic_title, + :truncated, + :url, + :user_id, + :username + + def archived + object.topic.archived + end + + def avatar_template + object.user&.avatar_template + end + + def category_id + object.topic.category_id + end + + def closed + object.topic.closed + end + + def excerpt + @excerpt ||= PrettyText.excerpt(cooked, 300, keep_emoji_images: true) + end + + def name + object.user&.name + end + + def include_name? + SiteSetting.enable_names? + end + + def post_id + object.id + end + + def slug + Slug.for(object.topic.title) + end + + def include_slug? + object.topic.title.present? + end + + def topic_title + object.topic.title + end + + def truncated + true + end + + def include_truncated? + cooked.length > 300 + end + + def url + "#{Discourse.base_url}#{object.url}" + end + + def user_id + object.user_id + end + + def username + object.user&.username + end +end diff --git a/assets/javascripts/discourse/routes/user-activity-solved.js b/assets/javascripts/discourse/routes/user-activity-solved.js index ef41d18..62783a8 100644 --- a/assets/javascripts/discourse/routes/user-activity-solved.js +++ b/assets/javascripts/discourse/routes/user-activity-solved.js @@ -1,15 +1,110 @@ -import UserActivityStreamRoute from "discourse/routes/user-activity-stream"; +import { tracked } from "@glimmer/tracking"; +import EmberObject from "@ember/object"; +import { service } from "@ember/service"; +import { Promise } from "rsvp"; +import { ajax } from "discourse/lib/ajax"; +import DiscourseRoute from "discourse/routes/discourse"; import { i18n } from "discourse-i18n"; -export default class UserActivitySolved extends UserActivityStreamRoute { - userActionType = 15; - noContentHelpKey = "solved.no_solutions"; +class SolvedPostsStream { + @tracked content = []; + @tracked loading = false; + @tracked loaded = false; + @tracked itemsLoaded = 0; + @tracked canLoadMore = true; + + constructor({ username, siteCategories }) { + this.username = username; + this.siteCategories = siteCategories; + } + + get noContent() { + return this.loaded && this.content.length === 0; + } + + findItems() { + if (this.loading || !this.canLoadMore) { + return Promise.resolve(); + } + + this.loading = true; + + const limit = 20; + return ajax( + `/solution/by_user.json?username=${this.username}&offset=${this.itemsLoaded}&limit=${limit}` + ) + .then((result) => { + const userSolvedPosts = result.user_solved_posts || []; + + if (userSolvedPosts.length === 0) { + this.canLoadMore = false; + return; + } + + const posts = userSolvedPosts.map((p) => { + const post = EmberObject.create(p); + post.set("titleHtml", post.topic_title); + post.set("postUrl", post.url); + + if (post.category_id && this.siteCategories) { + post.set( + "category", + this.siteCategories.find((c) => c.id === post.category_id) + ); + } + return post; + }); + + this.content = [...this.content, ...posts]; + this.itemsLoaded = this.itemsLoaded + userSolvedPosts.length; + + if (userSolvedPosts.length < limit) { + this.canLoadMore = false; + } + }) + .finally(() => { + this.loaded = true; + this.loading = false; + }); + } +} + +export default class UserActivitySolved extends DiscourseRoute { + @service site; + @service currentUser; + + model() { + const user = this.modelFor("user"); + + const stream = new SolvedPostsStream({ + username: user.username, + siteCategories: this.site.categories, + }); + + return stream.findItems().then(() => { + return { + stream, + emptyState: this.emptyState(), + }; + }); + } + + setupController(controller, model) { + controller.setProperties({ + model, + emptyState: this.emptyState(), + }); + } + + renderTemplate() { + this.render("user-activity-solved"); + } emptyState() { const user = this.modelFor("user"); let title, body; - if (this.isCurrentUser(user)) { + if (this.currentUser && user.id === this.currentUser.id) { title = i18n("solved.no_solved_topics_title"); body = i18n("solved.no_solved_topics_body"); } else { diff --git a/assets/javascripts/discourse/templates/user-activity-solved.gjs b/assets/javascripts/discourse/templates/user-activity-solved.gjs new file mode 100644 index 0000000..a94eb84 --- /dev/null +++ b/assets/javascripts/discourse/templates/user-activity-solved.gjs @@ -0,0 +1,16 @@ +import RouteTemplate from "ember-route-template"; +import EmptyState from "discourse/components/empty-state"; +import UserStream from "discourse/components/user-stream"; + +export default RouteTemplate( + +); diff --git a/config/routes.rb b/config/routes.rb index 2bfd416..2569aa2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,8 @@ DiscourseSolved::Engine.routes.draw do post "/accept" => "answer#accept" post "/unaccept" => "answer#unaccept" + + get "/by_user" => "solved_topics#by_user" end Discourse::Application.routes.draw { mount ::DiscourseSolved::Engine, at: "solution" } diff --git a/spec/requests/solved_topics_controller_spec.rb b/spec/requests/solved_topics_controller_spec.rb new file mode 100644 index 0000000..2cd1ce2 --- /dev/null +++ b/spec/requests/solved_topics_controller_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +describe DiscourseSolved::SolvedTopicsController do + fab!(:user) + fab!(:another_user) { Fabricate(:user) } + fab!(:admin) + fab!(:topic) + fab!(:post) { Fabricate(:post, topic:) } + fab!(:answer_post) { Fabricate(:post, topic:, user:) } + fab!(:solved_topic) { Fabricate(:solved_topic, topic:, answer_post:) } + + describe "#by_user" do + context "when accessing with username" do + it "returns solved posts for the specified user" do + sign_in(admin) + + get "/solution/by_user.json", params: { username: user.username } + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result["user_solved_posts"]).to be_present + expect(result["user_solved_posts"].length).to eq(1) + expect(result["user_solved_posts"][0]["post_id"]).to eq(answer_post.id) + end + + it "returns 404 for a non-existent user" do + sign_in(admin) + + get "/solution/by_user.json", params: { username: "non-existent-user" } + + expect(response.status).to eq(404) + end + + it "correctly handles the offset parameter" do + sign_in(admin) + + get "/solution/by_user.json", params: { username: user.username, offset: 1 } + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result["user_solved_posts"]).to be_empty + end + + it "correctly handles the limit parameter" do + Fabricate(:solved_topic, answer_post: Fabricate(:post, user:)) + + sign_in(admin) + + get "/solution/by_user.json", params: { username: user.username, limit: 1 } + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result["user_solved_posts"].length).to eq(1) + end + end + + context "when accessing without username" do + it "returns 400 for the current user" do + sign_in(user) + + get "/solution/by_user.json" + + expect(response.status).to eq(400) + end + + it "returns 400 if not logged in" do + get "/solution/by_user.json" + + expect(response.status).to eq(400) + end + end + + context "with visibility restrictions" do + context "with private category solved topic" do + fab!(:group) { Fabricate(:group).tap { |g| g.add(user) } } + fab!(:private_category) { Fabricate(:private_category, group:) } + fab!(:private_topic) { Fabricate(:topic, category: private_category) } + fab!(:private_post) { Fabricate(:post, topic: private_topic) } + fab!(:private_answer_post) { Fabricate(:post, topic: private_topic, user: user) } + fab!(:private_solved_topic) do + Fabricate(:solved_topic, topic: private_topic, answer_post: private_answer_post) + end + + it "respects category permissions" do + sign_in(another_user) + + get "/solution/by_user.json", params: { username: user.username } + + expect(response.status).to eq(200) + result = response.parsed_body + # admin sees both solutions + expect(result["user_solved_posts"].length).to eq(1) + + sign_in(user) + + get "/solution/by_user.json", params: { username: user.username } + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result["user_solved_posts"].length).to eq(2) + end + end + + it "does not return PMs" do + topic.update(archetype: Archetype.private_message, category: nil) + + sign_in(user) + + get "/solution/by_user.json", params: { username: user.username } + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result["user_solved_posts"]).to be_empty + end + end + end +end diff --git a/spec/system/solved_spec.rb b/spec/system/solved_spec.rb index dc17f7a..c1cf526 100644 --- a/spec/system/solved_spec.rb +++ b/spec/system/solved_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -describe "About page", type: :system do +describe "Solved", type: :system do fab!(:admin) fab!(:solver) { Fabricate(:user) } fab!(:accepter) { Fabricate(:user) } fab!(:topic) { Fabricate(:post, user: admin).topic } - fab!(:post1) { Fabricate(:post, topic:, user: solver, cooked: "The answer is 42") } + fab!(:solver_post) { Fabricate(:post, topic:, user: solver, cooked: "The answer is 42") } let(:topic_page) { PageObjects::Pages::Topic.new } before do @@ -39,4 +39,13 @@ describe "About page", type: :system do end end end + + it "shows the solved post in user activity at /my/activity/solved" do + Fabricate(:solved_topic, topic:, answer_post: solver_post, accepter:) + + sign_in(solver) + visit "/my/activity/solved" + + expect(page.find(".post-list")).to have_content(solver_post.cooked) + end end diff --git a/test/javascripts/acceptance/user-activity-solved-test.js b/test/javascripts/acceptance/user-activity-solved-test.js index ce55280..e329bca 100644 --- a/test/javascripts/acceptance/user-activity-solved-test.js +++ b/test/javascripts/acceptance/user-activity-solved-test.js @@ -9,8 +9,8 @@ acceptance( needs.user(); needs.pretender((server, helper) => { - server.get("/user_actions.json", () => - helper.response({ user_actions: [] }) + server.get("/solution/by_user.json", () => + helper.response({ user_solved_posts: [] }) ); }); From 1d038963fdfa290dd77dc95371403d31eace5611 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 9 Jul 2025 10:24:06 -0400 Subject: [PATCH 06/12] TEMP: skip tests due to core change (#379) --- test/javascripts/acceptance/user-activity-solved-test.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/javascripts/acceptance/user-activity-solved-test.js b/test/javascripts/acceptance/user-activity-solved-test.js index e329bca..9a43b17 100644 --- a/test/javascripts/acceptance/user-activity-solved-test.js +++ b/test/javascripts/acceptance/user-activity-solved-test.js @@ -1,5 +1,5 @@ import { visit } from "@ember/test-helpers"; -import { test } from "qunit"; +import { skip } from "qunit"; import { acceptance } from "discourse/tests/helpers/qunit-helpers"; import { i18n } from "discourse-i18n"; @@ -14,7 +14,9 @@ acceptance( ); }); - test("When looking at own activity", async function (assert) { + /* disabled temporarily for core updates https://github.com/discourse/discourse/pull/33455 */ + + skip("When looking at own activity", async function (assert) { await visit(`/u/eviltrout/activity/solved`); assert @@ -25,7 +27,7 @@ acceptance( .hasText(i18n("solved.no_solved_topics_body")); }); - test("When looking at another user's activity", async function (assert) { + skip("When looking at another user's activity", async function (assert) { await visit(`/u/charlie/activity/solved`); assert.dom("div.empty-state span.empty-state-title").hasText( From e4b4a97d9e901884bebfcb82f2c82bcd0befa9ca Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 9 Jul 2025 13:58:35 -0400 Subject: [PATCH 07/12] DEV: re-enable and update empty state tests (#380) --- .../acceptance/user-activity-solved-test.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/javascripts/acceptance/user-activity-solved-test.js b/test/javascripts/acceptance/user-activity-solved-test.js index 9a43b17..7a28167 100644 --- a/test/javascripts/acceptance/user-activity-solved-test.js +++ b/test/javascripts/acceptance/user-activity-solved-test.js @@ -1,5 +1,5 @@ import { visit } from "@ember/test-helpers"; -import { skip } from "qunit"; +import { test } from "qunit"; import { acceptance } from "discourse/tests/helpers/qunit-helpers"; import { i18n } from "discourse-i18n"; @@ -14,28 +14,26 @@ acceptance( ); }); - /* disabled temporarily for core updates https://github.com/discourse/discourse/pull/33455 */ - - skip("When looking at own activity", async function (assert) { + test("When looking at own activity", async function (assert) { await visit(`/u/eviltrout/activity/solved`); assert - .dom("div.empty-state span.empty-state-title") + .dom(".empty-state .empty-state__title") .hasText(i18n("solved.no_solved_topics_title")); assert - .dom("div.empty-state div.empty-state-body") + .dom(".empty-state .empty-state__body") .hasText(i18n("solved.no_solved_topics_body")); }); - skip("When looking at another user's activity", async function (assert) { + test("When looking at another user's activity", async function (assert) { await visit(`/u/charlie/activity/solved`); - assert.dom("div.empty-state span.empty-state-title").hasText( + assert.dom(".empty-state .empty-state__title").hasText( i18n("solved.no_solved_topics_title_others", { username: "charlie", }) ); - assert.dom("div.empty-state div.empty-state-body").hasNoText(); + assert.dom(".empty-state .empty-state__body").doesNotExist(); }); } ); From 0f03ec45c848b607b8e7905a1976235e7ed58d80 Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 10 Jul 2025 16:21:49 -0400 Subject: [PATCH 08/12] A11Y: improve expand/collapse labeling on embedded solution post (#381) --- .../discourse/components/solved-accepted-answer.gjs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/assets/javascripts/discourse/components/solved-accepted-answer.gjs b/assets/javascripts/discourse/components/solved-accepted-answer.gjs index fcafef5..31ed2b0 100644 --- a/assets/javascripts/discourse/components/solved-accepted-answer.gjs +++ b/assets/javascripts/discourse/components/solved-accepted-answer.gjs @@ -128,14 +128,17 @@ export default class SolvedAcceptedAnswer extends Component {
{{/if}} From 0ab400221d1d48e9c0fe13c02d6249afccc75884 Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Mon, 14 Jul 2025 16:09:53 +0200 Subject: [PATCH 09/12] DEV: Deprecate the plugin (#382) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index af4a250..6c8c440 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ ## Discourse Solved +> [!IMPORTANT] +> This plugin has now been bundled into Discourse core. See: https://meta.discourse.org/t/bundling-more-popular-plugins-with-discourse-core/373574 + Provides a solved button on designated categories ## How to Install this Plugin From 4f490c6d61c0fdecc2b150e3d9d7f3f89331203e Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Tue, 15 Jul 2025 11:25:35 +0200 Subject: [PATCH 10/12] Revert "DEV: Deprecate the plugin (#382)" (#383) This reverts commit 0ab400221d1d48e9c0fe13c02d6249afccc75884. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 6c8c440..af4a250 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ ## Discourse Solved -> [!IMPORTANT] -> This plugin has now been bundled into Discourse core. See: https://meta.discourse.org/t/bundling-more-popular-plugins-with-discourse-core/373574 - Provides a solved button on designated categories ## How to Install this Plugin From fca9d22750cc77e3d3acce596f1fbc8772ff54cb Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Tue, 15 Jul 2025 17:15:15 +0200 Subject: [PATCH 11/12] DEV: Remove bundled plugins from about.json (#384) --- about.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 about.json diff --git a/about.json b/about.json deleted file mode 100644 index 0ab5dbc..0000000 --- a/about.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "tests": { - "requiredPlugins": [ - "https://github.com/discourse/discourse-assign" - ] - } -} From e7b882d0d3aeb3d2dff6d53c8be0dec4d373115b Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Mon, 21 Jul 2025 18:20:10 +0200 Subject: [PATCH 12/12] DEV: Re-add about.json (#385) --- about.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 about.json diff --git a/about.json b/about.json new file mode 100644 index 0000000..17f1798 --- /dev/null +++ b/about.json @@ -0,0 +1,7 @@ +{ + "tests": { + "requiredPlugins": [ + "discourse-assign" + ] + } +}