From 42a758b843270841162b13cfdeaf6a3d8af11a1e Mon Sep 17 00:00:00 2001 From: MAEDA Go Date: Thu, 4 Jun 2026 18:24:56 +0900 Subject: [PATCH 1/2] Add REST API for boards --- app/controllers/boards_controller.rb | 67 +++++++++-- app/helpers/boards_helper.rb | 20 ++++ app/views/boards/index.api.rsb | 7 ++ app/views/boards/show.api.rsb | 3 + config/routes.rb | 4 + test/integration/api_test/api_routing_test.rb | 9 ++ test/integration/api_test/boards_test.rb | 104 ++++++++++++++++++ 7 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 app/views/boards/index.api.rsb create mode 100644 app/views/boards/show.api.rsb create mode 100644 test/integration/api_test/boards_test.rb diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 8798c818b..b29e639c0 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -19,19 +19,27 @@ class BoardsController < ApplicationController default_search_scope :messages - before_action :find_project_by_project_id, :find_board_if_available, :authorize + before_action :find_project_by_project_id, :only => [:index, :new, :create] + before_action :find_board_if_available, :only => [:index, :show, :edit, :update, :destroy] + before_action :authorize accept_atom_auth :index, :show + accept_api_auth :index, :show, :create, :update, :destroy helper :sort include SortHelper helper :watchers def index - @boards = @project.boards.preload(:last_message => :author).to_a + @boards = @project.boards.preload(:parent, {:last_message => :author}).to_a # show the board if there is only one - if @boards.size == 1 + if !api_request? && @boards.size == 1 @board = @boards.first show + else + respond_to do |format| + format.html + format.api + end end end @@ -63,6 +71,7 @@ class BoardsController < ApplicationController to_a render_feed(messages, :title => "#{@project}: #{@board}") end + format.api end end @@ -75,10 +84,24 @@ class BoardsController < ApplicationController @board = @project.boards.build @board.safe_attributes = params[:board] if @board.save - flash[:notice] = l(:notice_successful_create) - redirect_to_settings_in_projects + respond_to do |format| + format.html do + flash[:notice] = l(:notice_successful_create) + redirect_to_settings_in_projects + end + format.api do + render( + :action => 'show', + :status => :created, + :location => board_url(@board) + ) + end + end else - render :action => 'new' + respond_to do |format| + format.html {render :action => 'new'} + format.api {render_validation_errors(@board)} + end end end @@ -94,20 +117,36 @@ class BoardsController < ApplicationController redirect_to_settings_in_projects end format.js {head :ok} + format.api {render_api_ok} end else respond_to do |format| format.html {render :action => 'edit'} format.js {head :unprocessable_content} + format.api {render_validation_errors(@board)} end end end def destroy - if @board.destroy - flash[:notice] = l(:notice_successful_delete) + destroyed = @board.destroy + respond_to do |format| + format.html do + if destroyed + flash[:notice] = l(:notice_successful_delete) + else + flash[:error] = @board.errors.full_messages.to_sentence + end + redirect_to_settings_in_projects + end + format.api do + if destroyed + render_api_ok + else + render_validation_errors(@board) + end + end end - redirect_to_settings_in_projects end private @@ -117,7 +156,15 @@ class BoardsController < ApplicationController end def find_board_if_available - @board = @project.boards.find(params[:id]) if params[:id] + return if params[:id].blank? + + if params[:project_id] + @project = Project.find(params[:project_id]) + @board = @project.boards.find(params[:id]) + else + @board = Board.includes(:project).find(params[:id]) + @project = @board.project + end rescue ActiveRecord::RecordNotFound render_404 end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index bfa9525fb..dc3da4df2 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -38,4 +38,24 @@ module BoardsHelper end options end + + def render_api_board(board, api) + api.id board.id + api.project(:id => board.project_id, :name => board.project.name) unless board.project.nil? + api.name board.name + api.description board.description + api.parent(:id => board.parent_id, :name => board.parent.name) unless board.parent.nil? + api.position board.position + api.topics_count board.topics_count + api.messages_count board.messages_count + render_api_board_last_message(board.last_message, api) unless board.last_message.nil? + end + + def render_api_board_last_message(message, api) + api.last_message(:id => message.id) do + api.author(:id => message.author_id, :name => message.author.name) unless message.author.nil? + api.subject message.subject + api.created_on message.created_on + end + end end diff --git a/app/views/boards/index.api.rsb b/app/views/boards/index.api.rsb new file mode 100644 index 000000000..26961b6c8 --- /dev/null +++ b/app/views/boards/index.api.rsb @@ -0,0 +1,7 @@ +api.array :boards, api_meta(:total_count => @boards.size) do + @boards.each do |board| + api.board do + render_api_board(board, api) + end + end +end diff --git a/app/views/boards/show.api.rsb b/app/views/boards/show.api.rsb new file mode 100644 index 000000000..31eeba4a2 --- /dev/null +++ b/app/views/boards/show.api.rsb @@ -0,0 +1,3 @@ +api.board do + render_api_board(@board, api) +end diff --git a/config/routes.rb b/config/routes.rb index 9edb2f855..1cc23df27 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,6 +52,10 @@ Rails.application.routes.draw do post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply' post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit' post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy' + get 'boards/:id.:format', :to => 'boards#show', :as => 'board', :constraints => {:format => /xml|json/} + put 'boards/:id.:format', :to => 'boards#update', :constraints => {:format => /xml|json/} + patch 'boards/:id.:format', :to => 'boards#update', :constraints => {:format => /xml|json/} + delete 'boards/:id.:format', :to => 'boards#destroy', :constraints => {:format => /xml|json/} # Auto complete routes match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues' diff --git a/test/integration/api_test/api_routing_test.rb b/test/integration/api_test/api_routing_test.rb index 5544d0396..c99b0f264 100644 --- a/test/integration/api_test/api_routing_test.rb +++ b/test/integration/api_test/api_routing_test.rb @@ -27,6 +27,15 @@ class Redmine::ApiTest::ApiRoutingTest < Redmine::ApiTest::Routing should_route 'POST /uploads' => 'attachments#upload' end + def test_boards + should_route 'GET /projects/foo/boards' => 'boards#index', :project_id => 'foo' + should_route 'POST /projects/foo/boards' => 'boards#create', :project_id => 'foo' + + should_route 'GET /boards/1' => 'boards#show', :id => '1' + should_route 'PUT /boards/1' => 'boards#update', :id => '1' + should_route 'DELETE /boards/1' => 'boards#destroy', :id => '1' + end + def test_custom_fields should_route 'GET /custom_fields' => 'custom_fields#index' end diff --git a/test/integration/api_test/boards_test.rb b/test/integration/api_test/boards_test.rb new file mode 100644 index 000000000..ae1693c37 --- /dev/null +++ b/test/integration/api_test/boards_test.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require_relative '../../test_helper' + +class Redmine::ApiTest::BoardsTest < Redmine::ApiTest::Base + test "GET /projects/:project_id/boards.xml should return the boards" do + get '/projects/1/boards.xml', :headers => credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.media_type + assert_select 'boards[type=array] board id', :text => '1' + assert_select 'boards[type=array] board last_message[id="6"] subject', :text => 'RE: post 2' + end + + test "GET /boards/:id.xml should return the board" do + get '/boards/1.xml', :headers => credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.media_type + assert_select 'board' do + assert_select 'id', :text => '1' + assert_select 'project[id="1"][name="eCookbook"]' + assert_select 'name', :text => 'Help' + assert_select 'description', :text => 'Help board' + assert_select 'topics_count', :text => '2' + assert_select 'messages_count', :text => '6' + end + end + + test "POST /projects/:project_id/boards.xml should create the board" do + assert_difference 'Board.count' do + post( + '/projects/1/boards.xml', + :params => { + :board => { + :name => 'API', + :description => 'API board', + :parent_id => 2 + } + }, + :headers => credentials('jsmith')) + end + + board = Board.order(id: :desc).first + assert_response :created + assert_equal 'application/xml', @response.media_type + assert_equal 'API', board.name + assert_equal 'API board', board.description + assert_equal Board.find(2), board.parent + assert_select 'board id', :text => board.id.to_s + end + + test "POST /projects/:project_id/boards.xml with invalid parameters should return errors" do + assert_no_difference 'Board.count' do + post( + '/projects/1/boards.xml', + :params => {:board => {:name => '', :description => 'API board'}}, + :headers => credentials('jsmith')) + end + + assert_response :unprocessable_content + assert_equal 'application/xml', @response.media_type + assert_select 'errors error', :text => 'Name cannot be blank' + end + + test "PUT /boards/:id.xml should update the board" do + put( + '/boards/2.xml', + :params => {:board => {:name => 'API Update', :description => 'Updated board'}}, + :headers => credentials('jsmith')) + + assert_response :no_content + assert_equal '', @response.body + assert_equal 'API Update', Board.find(2).name + assert_equal 'Updated board', Board.find(2).description + end + + test "DELETE /boards/:id.xml should destroy the board" do + assert_difference 'Board.count', -1 do + delete '/boards/2.xml', :headers => credentials('jsmith') + end + + assert_response :no_content + assert_equal '', @response.body + assert_nil Board.find_by_id(2) + end +end -- 2.50.1