From 68a4cd38f54316e6d93ce69b6e25afc329652013 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 14 Jan 2010 20:00:17 +0000 Subject: [PATCH] XML REST API for Projects (#296). git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@3313 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/application_controller.rb | 1 + app/controllers/projects_controller.rb | 56 +++++++-- app/views/projects/index.xml.builder | 18 +++ app/views/projects/show.xml.builder | 16 +++ config/routes.rb | 7 ++ test/fixtures/custom_fields.yml | 2 +- test/integration/projects_api_test.rb | 134 ++++++++++++++++++++++ 7 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 app/views/projects/index.xml.builder create mode 100644 app/views/projects/show.xml.builder create mode 100644 test/integration/projects_api_test.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d696955c..9f3169a4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,6 +22,7 @@ class ApplicationController < ActionController::Base include Redmine::I18n layout 'base' + exempt_from_layout 'builder' # Remove broken cookie after upgrade from 0.8.x (#4292) # See https://rails.lighthouseapp.com/projects/8994/tickets/3360 diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e908388e..bd2e2a8d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -53,6 +53,9 @@ class ProjectsController < ApplicationController format.html { @projects = Project.visible.find(:all, :order => 'lft') } + format.xml { + @projects = Project.visible.find(:all, :order => 'lft') + } format.atom { projects = Project.visible.find(:all, :order => 'created_on DESC', :limit => Setting.feeds_limit.to_i) @@ -81,8 +84,18 @@ class ProjectsController < ApplicationController m = Member.new(:user => User.current, :roles => [r]) @project.members << m end - flash[:notice] = l(:notice_successful_create) - redirect_to :controller => 'projects', :action => 'settings', :id => @project + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'projects', :action => 'settings', :id => @project + } + format.xml { head :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) } + end + else + respond_to do |format| + format.html + format.xml { render :xml => @project.errors, :status => :unprocessable_entity } + end end end end @@ -147,6 +160,11 @@ class ProjectsController < ApplicationController :conditions => cond).to_f end @key = User.current.rss_key + + respond_to do |format| + format.html + format.xml + end end def settings @@ -160,15 +178,26 @@ class ProjectsController < ApplicationController # Edit @project def edit - if request.post? + if request.get? + else @project.attributes = params[:project] if validate_parent_id && @project.save @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'settings', :id => @project + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'settings', :id => @project + } + format.xml { head :ok } + end else - settings - render :action => 'settings' + respond_to do |format| + format.html { + settings + render :action => 'settings' + } + format.xml { render :xml => @project.errors, :status => :unprocessable_entity } + end end end end @@ -195,9 +224,16 @@ class ProjectsController < ApplicationController # Delete @project def destroy @project_to_destroy = @project - if request.post? and params[:confirm] - @project_to_destroy.destroy - redirect_to :controller => 'admin', :action => 'projects' + if request.get? + # display confirmation view + else + if params[:format] == 'xml' || params[:confirm] + @project_to_destroy.destroy + respond_to do |format| + format.html { redirect_to :controller => 'admin', :action => 'projects' } + format.xml { head :ok } + end + end end # hide project in layout @project = nil diff --git a/app/views/projects/index.xml.builder b/app/views/projects/index.xml.builder new file mode 100644 index 00000000..637ae7ef --- /dev/null +++ b/app/views/projects/index.xml.builder @@ -0,0 +1,18 @@ +xml.instruct! +xml.projects :type => 'array' do + @projects.each do |project| + xml.project :id => project.id do + xml.name project.name + xml.identifier project.identifier + xml.description project.description + xml.parent(:id => project.parent_id, :name => project.parent.name) unless project.parent.nil? + xml.custom_fields do + project.custom_field_values.each do |custom_value| + xml.custom_field custom_value.value, :id => custom_value.custom_field_id, :name => custom_value.custom_field.name + end + end unless project.custom_field_values.empty? + xml.created_on project.created_on + xml.updated_on project.updated_on + end + end +end diff --git a/app/views/projects/show.xml.builder b/app/views/projects/show.xml.builder new file mode 100644 index 00000000..3c4241d4 --- /dev/null +++ b/app/views/projects/show.xml.builder @@ -0,0 +1,16 @@ +xml.instruct! +xml.project :id => @project.id do + xml.name @project.name + xml.identifier @project.identifier + xml.description @project.description + xml.homepage @project.homepage + + xml.custom_fields do + @project.custom_field_values.each do |custom_value| + xml.custom_field custom_value.value, :id => custom_value.custom_field_id, :name => custom_value.custom_field.name + end + end unless @project.custom_field_values.empty? + + xml.created_on @project.created_on + xml.updated_on @project.updated_on +end diff --git a/config/routes.rb b/config/routes.rb index d64fad79..bf9966cb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -186,6 +186,7 @@ ActionController::Routing::Routes.draw do |map| project_views.connect 'projects.:format', :action => 'index' project_views.connect 'projects/new', :action => 'add' project_views.connect 'projects/:id', :action => 'show' + project_views.connect 'projects/:id.:format', :action => 'show' project_views.connect 'projects/:id/:action', :action => /roadmap|destroy|settings/ project_views.connect 'projects/:id/files', :action => 'list_files' project_views.connect 'projects/:id/files/new', :action => 'add_file' @@ -204,6 +205,7 @@ ActionController::Routing::Routes.draw do |map| projects.with_options :conditions => {:method => :post} do |project_actions| project_actions.connect 'projects/new', :action => 'add' project_actions.connect 'projects', :action => 'add' + project_actions.connect 'projects.:format', :action => 'add', :format => /xml/ project_actions.connect 'projects/:id/:action', :action => /destroy|archive|unarchive/ project_actions.connect 'projects/:id/files/new', :action => 'add_file' project_actions.connect 'projects/:id/versions/new', :action => 'add_version' @@ -211,7 +213,12 @@ ActionController::Routing::Routes.draw do |map| project_actions.connect 'projects/:id/activities/save', :action => 'save_activities' end + projects.with_options :conditions => {:method => :put} do |project_actions| + project_actions.conditions 'projects/:id.:format', :action => 'edit', :format => /xml/ + end + projects.with_options :conditions => {:method => :delete} do |project_actions| + project_actions.conditions 'projects/:id.:format', :action => 'destroy', :format => /xml/ project_actions.conditions 'projects/:id/reset_activities', :action => 'reset_activities' end end diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml index 4400f1da..c80f85df 100644 --- a/test/fixtures/custom_fields.yml +++ b/test/fixtures/custom_fields.yml @@ -44,7 +44,7 @@ custom_fields_003: - Alpha - Planning id: 3 - is_required: true + is_required: false field_format: list default_value: "" editable: true diff --git a/test/integration/projects_api_test.rb b/test/integration/projects_api_test.rb new file mode 100644 index 00000000..a93532a5 --- /dev/null +++ b/test/integration/projects_api_test.rb @@ -0,0 +1,134 @@ +# Redmine - project management software +# Copyright (C) 2006-2010 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.dirname(__FILE__)}/../test_helper" + +class ProjectsApiTest < ActionController::IntegrationTest + fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, + :attachments, :custom_fields, :custom_values, :time_entries + + def setup + Setting.rest_api_enabled = '1' + end + + def test_index_routing + assert_routing( + {:method => :get, :path => '/projects.xml'}, + :controller => 'projects', :action => 'index', :format => 'xml' + ) + end + + def test_index + get '/projects.xml' + assert_response :success + assert_equal 'application/xml', @response.content_type + end + + def test_show_routing + assert_routing( + {:method => :get, :path => '/projects/1.xml'}, + :controller => 'projects', :action => 'show', :id => '1', :format => 'xml' + ) + end + + def test_show + get '/projects/1.xml' + assert_response :success + assert_equal 'application/xml', @response.content_type + end + + def test_create_routing + assert_routing( + {:method => :post, :path => '/projects.xml'}, + :controller => 'projects', :action => 'add', :format => 'xml' + ) + end + + def test_create + attributes = {:name => 'API test', :identifier => 'api-test'} + assert_difference 'Project.count' do + post '/projects.xml', {:project => attributes}, :authorization => credentials('admin') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + project = Project.first(:order => 'id DESC') + attributes.each do |attribute, value| + assert_equal value, project.send(attribute) + end + end + + def test_create_failure + attributes = {:name => 'API test'} + assert_no_difference 'Project.count' do + post '/projects.xml', {:project => attributes}, :authorization => credentials('admin') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag :errors, :child => {:tag => 'error', :content => "Identifier can't be blank"} + end + + def test_update_routing + assert_routing( + {:method => :put, :path => '/projects/1.xml'}, + :controller => 'projects', :action => 'edit', :id => '1', :format => 'xml' + ) + end + + def test_update + attributes = {:name => 'API update'} + assert_no_difference 'Project.count' do + put '/projects/1.xml', {:project => attributes}, :authorization => credentials('jsmith') + end + assert_response :ok + assert_equal 'application/xml', @response.content_type + project = Project.find(1) + attributes.each do |attribute, value| + assert_equal value, project.send(attribute) + end + end + + def test_update_failure + attributes = {:name => ''} + assert_no_difference 'Project.count' do + put '/projects/1.xml', {:project => attributes}, :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 + + def test_destroy_routing + assert_routing( + {:method => :delete, :path => '/projects/1.xml'}, + :controller => 'projects', :action => 'destroy', :id => '1', :format => 'xml' + ) + end + + def test_destroy + assert_difference 'Project.count', -1 do + delete '/projects/2.xml', {}, :authorization => credentials('admin') + end + assert_response :ok + assert_equal 'application/xml', @response.content_type + assert_nil Project.find_by_id(2) + end + + def credentials(user, password=nil) + ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user) + end +end