From ab62656bfa2d44b9c889ab964c8e11683231469d Mon Sep 17 00:00:00 2001 From: MAEDA Go Date: Fri, 5 Jun 2026 17:24:44 +0900 Subject: [PATCH] Add reactions to REST API responses for issues and news --- app/helpers/reactions_helper.rb | 12 ++++ app/views/issues/show.api.rsb | 3 + app/views/news/show.api.rsb | 3 + test/integration/api_test/issues_test.rb | 90 ++++++++++++++++++++++++ test/integration/api_test/news_test.rb | 24 +++++++ 5 files changed, 132 insertions(+) diff --git a/app/helpers/reactions_helper.rb b/app/helpers/reactions_helper.rb index 461d9dad1..48e65f52e 100644 --- a/app/helpers/reactions_helper.rb +++ b/app/helpers/reactions_helper.rb @@ -47,6 +47,18 @@ module ReactionsHelper dom_id(object, :reaction) end + def render_api_reactions(object, api) + return unless Redmine::Reaction.visible?(object, User.current) + + api.array :reactions do + object.reaction_detail.visible_users.each do |user| + api.reaction do + api.user :id => user.id, :name => user.name + end + end + end + end + private def reaction_button_reacted(object, reaction, count, tooltip) diff --git a/app/views/issues/show.api.rsb b/app/views/issues/show.api.rsb index 2c341163a..5d874ec22 100644 --- a/app/views/issues/show.api.rsb +++ b/app/views/issues/show.api.rsb @@ -29,6 +29,8 @@ api.issue do api.updated_on @issue.updated_on api.closed_on @issue.closed_on + render_api_reactions(@issue, api) if include_in_api_response?('reactions') + render_api_issue_children(@issue, api) if include_in_api_response?('children') api.array :attachments do @@ -62,6 +64,7 @@ api.issue do api.updated_on journal.updated_on api.updated_by(:id => journal.updated_by.id, :name => journal.updated_by.name) unless journal.updated_by.nil? api.private_notes journal.private_notes + render_api_reactions(journal, api) if include_in_api_response?('reactions') api.array :details do journal.visible_details.each do |detail| api.detail :property => detail.property, :name => detail.prop_key do diff --git a/app/views/news/show.api.rsb b/app/views/news/show.api.rsb index 0dd3a3b78..5ed661a0a 100644 --- a/app/views/news/show.api.rsb +++ b/app/views/news/show.api.rsb @@ -7,6 +7,8 @@ api.news do api.description @news.description api.created_on @news.created_on + render_api_reactions(@news, api) if include_in_api_response?('reactions') + api.array :attachments do @news.attachments.each do |attachment| render_api_attachment(attachment, api) @@ -18,6 +20,7 @@ api.news do api.comment :id => comment.id do api.author(:id => comment.author_id, :name => comment.author.name) unless comment.author.nil? api.content comment.content + render_api_reactions(comment, api) if include_in_api_response?('reactions') end end end if include_in_api_response?('comments') diff --git a/test/integration/api_test/issues_test.rb b/test/integration/api_test/issues_test.rb index cadadc1ec..9f3356fbd 100644 --- a/test/integration/api_test/issues_test.rb +++ b/test/integration/api_test/issues_test.rb @@ -379,6 +379,96 @@ class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base assert_select 'issue>status[is_closed=false]' end + test "GET /issues/:id.xml should not include reactions by default" do + get '/issues/1.xml', :headers => credentials('admin') + + assert_response :ok + assert_select 'issue>reactions', 0 + end + + test "GET /issues/:id.xml?include=reactions should include reactions" do + get '/issues/1.xml?include=reactions', :headers => credentials('admin') + + assert_response :ok + assert_equal 'application/xml', response.media_type + assert_select 'issue>reactions[type=array]' do + assert_select 'reaction', 3 + assert_select 'reaction>user[id="1"][name="Redmine Admin"]' + assert_select 'reaction>user[id="2"][name="John Smith"]' + assert_select 'reaction>user[id="3"][name="Dave Lopper"]' + end + end + + test "GET /issues/:id.json?include=reactions should include reactions" do + get '/issues/1.json?include=reactions', :headers => credentials('admin') + + assert_response :ok + assert_equal 'application/json', response.media_type + json = ActiveSupport::JSON.decode(response.body) + reactions = json['issue']['reactions'].sort_by {|reaction| reaction['user']['id']} + + assert_equal( + [ + {'user' => {'id' => 1, 'name' => 'Redmine Admin'}}, + {'user' => {'id' => 2, 'name' => 'John Smith'}}, + {'user' => {'id' => 3, 'name' => 'Dave Lopper'}} + ], + reactions + ) + end + + test "GET /issues/:id.json?include=reactions should return an empty array when there are no reactions" do + get '/issues/2.json?include=reactions', :headers => credentials('admin') + + assert_response :ok + json = ActiveSupport::JSON.decode(response.body) + + assert_equal [], json['issue']['reactions'] + end + + test "GET /issues/:id.xml?include=journals,reactions should include journal reactions" do + get '/issues/1.xml?include=journals,reactions', :headers => credentials('admin') + + assert_response :ok + assert_select 'issue journals[type=array]' do + assert_select 'journal[id="1"]>reactions[type=array]' do + assert_select 'reaction', 1 + assert_select 'reaction>user[id="2"][name="John Smith"]' + end + end + end + + test "GET /issues/:id.xml?include=reactions should not include reactions when reactions are disabled" do + with_settings :reactions_enabled => '0' do + get '/issues/1.xml?include=reactions', :headers => credentials('admin') + end + + assert_response :ok + assert_select 'issue>reactions', 0 + end + + test "GET /issues/:id.xml?include=reactions should not disclose hidden reaction users" do + user = User.generate! do |u| + u.password = 'secret123' + u.password_confirmation = 'secret123' + end + role = Role.generate!( + :users_visibility => 'members_of_visible_projects', + :permissions => [:view_issues] + ) + User.add_to_project(user, projects(:projects_001), role) + + get '/issues/1.xml?include=reactions', :headers => credentials(user.login, 'secret123') + + assert_response :ok + assert_select 'issue>reactions[type=array]' do + assert_select 'reaction', 2 + assert_select 'user[id="1"]', 0 + assert_select 'reaction>user[id="2"][name="John Smith"]' + assert_select 'reaction>user[id="3"][name="Dave Lopper"]' + end + end + test "GET /issues/:id.xml?include=watchers should include watchers" do Watcher.create!(:user_id => 3, :watchable => Issue.find(1)) diff --git a/test/integration/api_test/news_test.rb b/test/integration/api_test/news_test.rb index 399b2b347..7ee27b679 100644 --- a/test/integration/api_test/news_test.rb +++ b/test/integration/api_test/news_test.rb @@ -75,6 +75,17 @@ class Redmine::ApiTest::NewsTest < Redmine::ApiTest::Base assert_equal 1, json['news']['id'] end + test "GET /news/:id.xml?include=reactions should include reactions" do + get "/news/1.xml?include=reactions", :headers => credentials('admin') + + assert_response :success + assert_equal 'application/xml', response.media_type + assert_select 'news>reactions[type=array]' do + assert_select 'reaction', 1 + assert_select 'reaction>user[id="1"][name="Redmine Admin"]' + end + end + test "GET /news/:id.xml with attachments" do news = News.find(1) attachment = Attachment.first @@ -105,6 +116,19 @@ class Redmine::ApiTest::NewsTest < Redmine::ApiTest::Base end end + test "GET /news/:id.xml?include=comments,reactions should include comment reactions" do + get "/news/1.xml?include=comments,reactions", :headers => credentials('admin') + + assert_response :success + assert_equal 'application/xml', response.media_type + assert_select 'news comments[type=array]' do + assert_select 'comment[id="1"]>reactions[type=array]' do + assert_select 'reaction', 1 + assert_select 'reaction>user[id="2"][name="John Smith"]' + end + end + end + test "POST /project/:project_id/news.xml should create a news with the attributes" do payload = <<~XML -- 2.50.1