From 34c73c7573c3459620435e71470794f221a86b7b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Nov 2011 17:09:01 +0000 Subject: [PATCH] REST API for issue categories (#9553). git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@7882 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- .../issue_categories_controller.rb | 60 ++++++--- app/models/issue_category.rb | 2 + app/views/issue_categories/edit.html.erb | 2 +- app/views/issue_categories/index.api.rsb | 10 ++ app/views/issue_categories/new.html.erb | 2 +- app/views/issue_categories/show.api.rsb | 6 + lib/redmine.rb | 2 +- .../issue_categories_controller_test.rb | 10 +- .../api_test/issue_categories_test.rb | 127 ++++++++++++++++++ test/integration/routing_test.rb | 24 +++- 10 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 app/views/issue_categories/index.api.rsb create mode 100644 app/views/issue_categories/show.api.rsb create mode 100644 test/integration/api_test/issue_categories_test.rb diff --git a/app/controllers/issue_categories_controller.rb b/app/controllers/issue_categories_controller.rb index b0d11dd8f..c83246f0b 100644 --- a/app/controllers/issue_categories_controller.rb +++ b/app/controllers/issue_categories_controller.rb @@ -18,18 +18,33 @@ class IssueCategoriesController < ApplicationController menu_item :settings model_object IssueCategory - before_filter :find_model_object, :except => [:new, :create] - before_filter :find_project_from_association, :except => [:new, :create] - before_filter :find_project, :only => [:new, :create] + before_filter :find_model_object, :except => [:index, :new, :create] + before_filter :find_project_from_association, :except => [:index, :new, :create] + before_filter :find_project, :only => [:index, :new, :create] before_filter :authorize + accept_api_auth :index, :show, :create, :update, :destroy + + def index + respond_to do |format| + format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project } + format.api { @categories = @project.issue_categories.all } + end + end + + def show + respond_to do |format| + format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project } + format.api + end + end def new - @category = @project.issue_categories.build(params[:category]) + @category = @project.issue_categories.build(params[:issue_category]) end verify :method => :post, :only => :create def create - @category = @project.issue_categories.build(params[:category]) + @category = @project.issue_categories.build(params[:issue_category]) if @category.save respond_to do |format| format.html do @@ -42,6 +57,7 @@ class IssueCategoriesController < ApplicationController content_tag('select', '' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]') } end + format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) } end else respond_to do |format| @@ -49,6 +65,7 @@ class IssueCategoriesController < ApplicationController format.js do render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) } end + format.api { render_validation_errors(@category) } end end end @@ -58,26 +75,35 @@ class IssueCategoriesController < ApplicationController verify :method => :put, :only => :update def update - if @category.update_attributes(params[:category]) - flash[:notice] = l(:notice_successful_update) - redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project + if @category.update_attributes(params[:issue_category]) + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project + } + format.api { head :ok } + end else - render :action => 'edit' + respond_to do |format| + format.html { render :action => 'edit' } + format.api { render_validation_errors(@category) } + end end end verify :method => :delete, :only => :destroy def destroy @issue_count = @category.issues.size - if @issue_count == 0 - # No issue assigned to this category - @category.destroy - redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' - return - elsif params[:todo] - reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign' + if @issue_count == 0 || params[:todo] || api_request? + reassign_to = nil + if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?) + reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) + end @category.destroy(reassign_to) - redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' + respond_to do |format| + format.html { redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' } + format.api { head :ok } + end return end @categories = @project.issue_categories - [@category] diff --git a/app/models/issue_category.rb b/app/models/issue_category.rb index d6db3ed8d..b05f58dd4 100644 --- a/app/models/issue_category.rb +++ b/app/models/issue_category.rb @@ -23,6 +23,8 @@ class IssueCategory < ActiveRecord::Base validates_presence_of :name validates_uniqueness_of :name, :scope => [:project_id] validates_length_of :name, :maximum => 30 + + attr_protected :project_id named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}} diff --git a/app/views/issue_categories/edit.html.erb b/app/views/issue_categories/edit.html.erb index 1e1a8fe7d..917429640 100644 --- a/app/views/issue_categories/edit.html.erb +++ b/app/views/issue_categories/edit.html.erb @@ -1,6 +1,6 @@

<%=l(:label_issue_category)%>

-<% labelled_tabular_form_for :category, @category, :url => issue_category_path(@category), :html => {:method => :put} do |f| %> +<% labelled_tabular_form_for :issue_category, @category, :url => issue_category_path(@category), :html => {:method => :put} do |f| %> <%= render :partial => 'issue_categories/form', :locals => { :f => f } %> <%= submit_tag l(:button_save) %> <% end %> diff --git a/app/views/issue_categories/index.api.rsb b/app/views/issue_categories/index.api.rsb new file mode 100644 index 000000000..685d8a560 --- /dev/null +++ b/app/views/issue_categories/index.api.rsb @@ -0,0 +1,10 @@ +api.array :issue_categories, api_meta(:total_count => @categories.size) do + @categories.each do |category| + api.issue_category do + api.id category.id + api.project(:id => category.project_id, :name => category.project.name) unless category.project.nil? + api.name category.name + api.assigned_to(:id => category.assigned_to_id, :name => category.assigned_to.name) unless category.assigned_to.nil? + end + end +end diff --git a/app/views/issue_categories/new.html.erb b/app/views/issue_categories/new.html.erb index ccd88c5cd..f99c9b358 100644 --- a/app/views/issue_categories/new.html.erb +++ b/app/views/issue_categories/new.html.erb @@ -1,6 +1,6 @@

<%=l(:label_issue_category_new)%>

-<% labelled_tabular_form_for :category, @category, :url => project_issue_categories_path(@project) do |f| %> +<% labelled_tabular_form_for :issue_category, @category, :url => project_issue_categories_path(@project) do |f| %> <%= render :partial => 'issue_categories/form', :locals => { :f => f } %> <%= submit_tag l(:button_create) %> <% end %> diff --git a/app/views/issue_categories/show.api.rsb b/app/views/issue_categories/show.api.rsb new file mode 100644 index 000000000..cefa7c8b8 --- /dev/null +++ b/app/views/issue_categories/show.api.rsb @@ -0,0 +1,6 @@ +api.issue_category do + api.id @category.id + api.project(:id => @category.project_id, :name => @category.project.name) unless @category.project.nil? + api.name @category.name + api.assigned_to(:id => @category.assigned_to_id, :name => @category.assigned_to.name) unless @category.assigned_to.nil? +end diff --git a/lib/redmine.rb b/lib/redmine.rb index 746b623ec..129b39315 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -58,7 +58,7 @@ Redmine::AccessControl.map do |map| map.project_module :issue_tracking do |map| # Issue categories - map.permission :manage_categories, {:projects => :settings, :issue_categories => [:new, :create, :edit, :update, :destroy]}, :require => :member + map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member # Issues map.permission :view_issues, {:issues => [:index, :show], :auto_complete => [:issues], diff --git a/test/functional/issue_categories_controller_test.rb b/test/functional/issue_categories_controller_test.rb index c9dbbe1f8..221d5d1cc 100644 --- a/test/functional/issue_categories_controller_test.rb +++ b/test/functional/issue_categories_controller_test.rb @@ -42,7 +42,7 @@ class IssueCategoriesControllerTest < ActionController::TestCase def test_create @request.session[:user_id] = 2 # manager assert_difference 'IssueCategory.count' do - post :create, :project_id => '1', :category => {:name => 'New category'} + post :create, :project_id => '1', :issue_category => {:name => 'New category'} end assert_redirected_to '/projects/ecookbook/settings/categories' category = IssueCategory.find_by_name('New category') @@ -52,7 +52,7 @@ class IssueCategoriesControllerTest < ActionController::TestCase def test_create_failure @request.session[:user_id] = 2 - post :create, :project_id => '1', :category => {:name => ''} + post :create, :project_id => '1', :issue_category => {:name => ''} assert_response :success assert_template 'new' end @@ -66,20 +66,20 @@ class IssueCategoriesControllerTest < ActionController::TestCase def test_update assert_no_difference 'IssueCategory.count' do - put :update, :id => 2, :category => { :name => 'Testing' } + put :update, :id => 2, :issue_category => { :name => 'Testing' } end assert_redirected_to '/projects/ecookbook/settings/categories' assert_equal 'Testing', IssueCategory.find(2).name end def test_update_failure - put :update, :id => 2, :category => { :name => '' } + put :update, :id => 2, :issue_category => { :name => '' } assert_response :success assert_template 'edit' end def test_update_not_found - put :update, :id => 97, :category => { :name => 'Testing' } + put :update, :id => 97, :issue_category => { :name => 'Testing' } assert_response 404 end diff --git a/test/integration/api_test/issue_categories_test.rb b/test/integration/api_test/issue_categories_test.rb new file mode 100644 index 000000000..df5967a34 --- /dev/null +++ b/test/integration/api_test/issue_categories_test.rb @@ -0,0 +1,127 @@ +# Redmine - project management software +# Copyright (C) 2006-2011 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 File.expand_path('../../../test_helper', __FILE__) + +class ApiTest::IssueCategoriesTest < ActionController::IntegrationTest + fixtures :projects, :users, :issue_categories, :issues, + :roles, + :member_roles, + :members, + :enabled_modules + + def setup + Setting.rest_api_enabled = '1' + end + + context "GET /projects/:project_id/issue_categories.xml" do + should "return issue categories" do + get '/projects/1/issue_categories.xml', {}, :authorization => credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_categories', + :child => {:tag => 'issue_category', :child => {:tag => 'id', :content => '2'}} + end + end + + context "GET /issue_categories/2.xml" do + should "return requested issue category" do + get '/issue_categories/2.xml', {}, :authorization => credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_category', + :child => {:tag => 'id', :content => '2'} + end + end + + context "POST /projects/:project_id/issue_categories.xml" do + should "return create issue category" do + assert_difference 'IssueCategory.count' do + post '/projects/1/issue_categories.xml', {:issue_category => {:name => 'API'}}, :authorization => credentials('jsmith') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + + category = IssueCategory.first(:order => 'id DESC') + assert_equal 'API', category.name + assert_equal 1, category.project_id + end + + context "with invalid parameters" do + should "return errors" do + assert_no_difference 'IssueCategory.count' do + post '/projects/1/issue_categories.xml', {:issue_category => {:name => ''}}, :authorization => credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + end + end + + context "PUT /issue_categories/2.xml" do + context "with valid parameters" do + should "update issue category" do + assert_no_difference 'IssueCategory.count' do + put '/issue_categories/2.xml', {:issue_category => {:name => 'API Update'}}, :authorization => credentials('jsmith') + end + assert_response :ok + assert_equal 'API Update', IssueCategory.find(2).name + end + end + + context "with invalid parameters" do + should "return errors" do + assert_no_difference 'IssueCategory.count' do + put '/issue_categories/2.xml', {:issue_category => {:name => ''}}, :authorization => credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + end + end + + context "DELETE /issue_categories/1.xml" do + should "destroy issue categories" do + assert_difference 'IssueCategory.count', -1 do + delete '/issue_categories/1.xml', {}, :authorization => credentials('jsmith') + end + assert_response :ok + assert_nil IssueCategory.find_by_id(1) + end + + should "reassign issues with :reassign_to_id param" do + issue_count = Issue.count(:conditions => {:category_id => 1}) + assert issue_count > 0 + + assert_difference 'IssueCategory.count', -1 do + assert_difference 'Issue.count(:conditions => {:category_id => 2})', 3 do + delete '/issue_categories/1.xml', {:reassign_to_id => 2}, :authorization => credentials('jsmith') + end + end + assert_response :ok + assert_nil IssueCategory.find_by_id(1) + end + end + + def credentials(user, password=nil) + ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user) + end +end diff --git a/test/integration/routing_test.rb b/test/integration/routing_test.rb index 1f978c36e..7fb277808 100644 --- a/test/integration/routing_test.rb +++ b/test/integration/routing_test.rb @@ -114,9 +114,29 @@ class RoutingTest < ActionController::IntegrationTest end context "issue categories" do - should_route :get, "/projects/test/issue_categories/new", :controller => 'issue_categories', :action => 'new', :project_id => 'test' + should_route :get, "/projects/foo/issue_categories", :controller => 'issue_categories', :action => 'index', :project_id => 'foo' + should_route :get, "/projects/foo/issue_categories.xml", :controller => 'issue_categories', :action => 'index', :project_id => 'foo', :format => 'xml' + should_route :get, "/projects/foo/issue_categories.json", :controller => 'issue_categories', :action => 'index', :project_id => 'foo', :format => 'json' - should_route :post, "/projects/test/issue_categories/new", :controller => 'issue_categories', :action => 'new', :project_id => 'test' + should_route :get, "/projects/foo/issue_categories/new", :controller => 'issue_categories', :action => 'new', :project_id => 'foo' + + should_route :post, "/projects/foo/issue_categories", :controller => 'issue_categories', :action => 'create', :project_id => 'foo' + should_route :post, "/projects/foo/issue_categories.xml", :controller => 'issue_categories', :action => 'create', :project_id => 'foo', :format => 'xml' + should_route :post, "/projects/foo/issue_categories.json", :controller => 'issue_categories', :action => 'create', :project_id => 'foo', :format => 'json' + + should_route :get, "/issue_categories/1", :controller => 'issue_categories', :action => 'show', :id => '1' + should_route :get, "/issue_categories/1.xml", :controller => 'issue_categories', :action => 'show', :id => '1', :format => 'xml' + should_route :get, "/issue_categories/1.json", :controller => 'issue_categories', :action => 'show', :id => '1', :format => 'json' + + should_route :get, "/issue_categories/1/edit", :controller => 'issue_categories', :action => 'edit', :id => '1' + + should_route :put, "/issue_categories/1", :controller => 'issue_categories', :action => 'update', :id => '1' + should_route :put, "/issue_categories/1.xml", :controller => 'issue_categories', :action => 'update', :id => '1', :format => 'xml' + should_route :put, "/issue_categories/1.json", :controller => 'issue_categories', :action => 'update', :id => '1', :format => 'json' + + should_route :delete, "/issue_categories/1", :controller => 'issue_categories', :action => 'destroy', :id => '1' + should_route :delete, "/issue_categories/1.xml", :controller => 'issue_categories', :action => 'destroy', :id => '1', :format => 'xml' + should_route :delete, "/issue_categories/1.json", :controller => 'issue_categories', :action => 'destroy', :id => '1', :format => 'json' end context "issue relations" do -- 2.39.5