diff options
-rw-r--r-- | app/controllers/application_controller.rb | 29 | ||||
-rw-r--r-- | app/controllers/issues_controller.rb | 50 | ||||
-rw-r--r-- | app/views/issues/index.xml.builder | 31 | ||||
-rw-r--r-- | app/views/issues/show.xml.builder | 54 | ||||
-rw-r--r-- | config/routes.rb | 8 | ||||
-rw-r--r-- | test/integration/issues_api_test.rb | 158 |
6 files changed, 314 insertions, 16 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 20a8e5760..d696955c3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -70,8 +70,8 @@ class ApplicationController < ActionController::Base elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action]) # RSS key authentication does not start a session User.find_by_rss_key(params[:key]) - elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format]) && accept_key_auth_actions.include?(params[:action]) - if params[:key].present? + elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format]) + if params[:key].present? && accept_key_auth_actions.include?(params[:action]) # Use API key User.find_by_api_key(params[:key]) else @@ -194,18 +194,35 @@ class ApplicationController < ActionController::Base def render_403 @project = nil - render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403 + respond_to do |format| + format.html { render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403 } + format.atom { head 403 } + format.xml { head 403 } + format.json { head 403 } + end return false end def render_404 - render :template => "common/404", :layout => !request.xhr?, :status => 404 + respond_to do |format| + format.html { render :template => "common/404", :layout => !request.xhr?, :status => 404 } + format.atom { head 404 } + format.xml { head 404 } + format.json { head 404 } + end return false end def render_error(msg) - flash.now[:error] = msg - render :text => '', :layout => !request.xhr?, :status => 500 + respond_to do |format| + format.html { + flash.now[:error] = msg + render :text => '', :layout => !request.xhr?, :status => 500 + } + format.atom { head 500 } + format.xml { head 500 } + format.json { head 500 } + end end def invalid_authenticity_token diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index bb2e55fd3..5da0aa283 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -46,7 +46,7 @@ class IssuesController < ApplicationController helper :timelog include Redmine::Export::PDF - verify :method => :post, + verify :method => [:post, :delete], :only => :destroy, :render => { :nothing => true, :status => :method_not_allowed } @@ -59,6 +59,7 @@ class IssuesController < ApplicationController limit = per_page_option respond_to do |format| format.html { } + format.xml { } format.atom { limit = Setting.feeds_limit.to_i } format.csv { limit = Setting.issues_export_limit.to_i } format.pdf { limit = Setting.issues_export_limit.to_i } @@ -74,6 +75,7 @@ class IssuesController < ApplicationController respond_to do |format| format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } + format.xml { render :layout => false } format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') } format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') } @@ -113,6 +115,7 @@ class IssuesController < ApplicationController @time_entry = TimeEntry.new respond_to do |format| format.html { render :template => 'issues/show.rhtml' } + format.xml { render :layout => false } format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' } format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } end @@ -155,10 +158,20 @@ class IssuesController < ApplicationController attach_files(@issue, params[:attachments]) flash[:notice] = l(:notice_successful_create) call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) - redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } : - { :action => 'show', :id => @issue }) + respond_to do |format| + format.html { + redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } : + { :action => 'show', :id => @issue }) + } + format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) } + end return - end + else + respond_to do |format| + format.html { } + format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return } + end + end end @priorities = IssuePriority.all render :layout => !request.xhr? @@ -184,7 +197,9 @@ class IssuesController < ApplicationController @issue.safe_attributes = attrs end - if request.post? + if request.get? + # nop + else @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) @time_entry.attributes = params[:time_entry] if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid? @@ -201,9 +216,18 @@ class IssuesController < ApplicationController flash[:notice] = l(:notice_successful_update) end call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal}) - redirect_to(params[:back_to] || {:action => 'show', :id => @issue}) + respond_to do |format| + format.html { redirect_to(params[:back_to] || {:action => 'show', :id => @issue}) } + format.xml { head :ok } + end + return end end + # failure + respond_to do |format| + format.html { } + format.xml { render :xml => @issue.errors, :status => :unprocessable_entity } + end end rescue ActiveRecord::StaleObjectError # Optimistic locking exception @@ -346,12 +370,17 @@ class IssuesController < ApplicationController TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues]) end else - # display the destroy form - return + unless params[:format] == 'xml' + # display the destroy form if it's a user request + return + end end end @issues.each(&:destroy) - redirect_to :action => 'index', :project_id => @project + respond_to do |format| + format.html { redirect_to :action => 'index', :project_id => @project } + format.xml { head :ok } + end end def gantt @@ -484,7 +513,8 @@ private end def find_project - @project = Project.find(params[:project_id]) + project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id] + @project = Project.find(project_id) rescue ActiveRecord::RecordNotFound render_404 end diff --git a/app/views/issues/index.xml.builder b/app/views/issues/index.xml.builder new file mode 100644 index 000000000..4c848b54d --- /dev/null +++ b/app/views/issues/index.xml.builder @@ -0,0 +1,31 @@ +xml.instruct! +xml.issues :type => 'array', :count => @issue_count do + @issues.each do |issue| + xml.issue :id => issue.id do + xml.project(:id => issue.project_id, :name => issue.project.name) unless issue.project.nil? + xml.tracker(:id => issue.tracker_id, :name => issue.tracker.name) unless issue.tracker.nil? + xml.status(:id => issue.status_id, :name => issue.status.name) unless issue.status.nil? + xml.priority(:id => issue.priority_id, :name => issue.priority.name) unless issue.priority.nil? + xml.author(:id => issue.author_id, :name => issue.author.name) unless issue.author.nil? + xml.assigned_to(:id => issue.assigned_to_id, :name => issue.assigned_to.name) unless issue.assigned_to.nil? + xml.category(:id => issue.category_id, :name => issue.category.name) unless issue.category.nil? + xml.fixed_version(:id => issue.fixed_version_id, :name => issue.fixed_version.name) unless issue.fixed_version.nil? + + xml.subject issue.subject + xml.description issue.description + xml.start_date issue.start_date + xml.due_date issue.due_date + xml.done_ratio issue.done_ratio + xml.estimated_hours issue.estimated_hours + + xml.custom_fields do + issue.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 + + xml.created_on issue.created_on + xml.updated_on issue.updated_on + end + end +end diff --git a/app/views/issues/show.xml.builder b/app/views/issues/show.xml.builder new file mode 100644 index 000000000..b73f9844d --- /dev/null +++ b/app/views/issues/show.xml.builder @@ -0,0 +1,54 @@ +xml.instruct! +xml.issue :id => @issue.id do + xml.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil? + xml.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil? + xml.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil? + xml.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil? + xml.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil? + xml.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil? + xml.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil? + xml.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil? + + xml.subject @issue.subject + xml.description @issue.description + xml.start_date @issue.start_date + xml.due_date @issue.due_date + xml.done_ratio @issue.done_ratio + xml.estimated_hours @issue.estimated_hours + if User.current.allowed_to?(:view_time_entries, @project) + xml.spent_hours @issue.spent_hours + end + + xml.custom_fields do + @issue.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 @issue.custom_field_values.empty? + + xml.created_on @issue.created_on + xml.updated_on @issue.updated_on + + xml.changesets do + @issue.changesets.each do |changeset| + xml.changeset :revision => changeset.revision do + xml.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil? + xml.comments changeset.comments + xml.committed_on changeset.committed_on + end + end + end if User.current.allowed_to?(:view_changesets, @project) && @issue.changesets.any? + + xml.journals do + @issue.journals.each do |journal| + xml.journal :id => journal.id do + xml.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil? + xml.notes journal.notes + xml.details do + journal.details.each do |detail| + xml.detail :property => detail.property, :name => detail.prop_key, :old => detail.old_value, :new => detail.value + end + end + end + end + end unless @issue.journals.empty? +end diff --git a/config/routes.rb b/config/routes.rb index e2560c183..d64fad792 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -119,9 +119,17 @@ ActionController::Routing::Routes.draw do |map| issues_views.connect 'issues/:id/move', :action => 'move', :id => /\d+/ end issues_routes.with_options :conditions => {:method => :post} do |issues_actions| + issues_actions.connect 'issues', :action => 'index' issues_actions.connect 'projects/:project_id/issues', :action => 'new' issues_actions.connect 'issues/:id/quoted', :action => 'reply', :id => /\d+/ issues_actions.connect 'issues/:id/:action', :action => /edit|move|destroy/, :id => /\d+/ + issues_actions.connect 'issues.:format', :action => 'new', :format => /xml/ + end + issues_routes.with_options :conditions => {:method => :put} do |issues_actions| + issues_actions.connect 'issues/:id.:format', :action => 'edit', :id => /\d+/, :format => /xml/ + end + issues_routes.with_options :conditions => {:method => :delete} do |issues_actions| + issues_actions.connect 'issues/:id.:format', :action => 'destroy', :id => /\d+/, :format => /xml/ end issues_routes.connect 'issues/:action' end diff --git a/test/integration/issues_api_test.rb b/test/integration/issues_api_test.rb new file mode 100644 index 000000000..4406f3651 --- /dev/null +++ b/test/integration/issues_api_test.rb @@ -0,0 +1,158 @@ +# 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 IssuesApiTest < ActionController::IntegrationTest + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + def setup + Setting.rest_api_enabled = '1' + end + + def test_index_routing + assert_routing( + {:method => :get, :path => '/issues.xml'}, + :controller => 'issues', :action => 'index', :format => 'xml' + ) + end + + def test_index + get '/issues.xml' + assert_response :success + assert_equal 'application/xml', @response.content_type + end + + def test_show_routing + assert_routing( + {:method => :get, :path => '/issues/1.xml'}, + :controller => 'issues', :action => 'show', :id => '1', :format => 'xml' + ) + end + + def test_show + get '/issues/1.xml' + assert_response :success + assert_equal 'application/xml', @response.content_type + end + + def test_create_routing + assert_routing( + {:method => :post, :path => '/issues.xml'}, + :controller => 'issues', :action => 'new', :format => 'xml' + ) + end + + def test_create + attributes = {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3} + assert_difference 'Issue.count' do + post '/issues.xml', {:issue => attributes}, :authorization => credentials('jsmith') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + issue = Issue.first(:order => 'id DESC') + attributes.each do |attribute, value| + assert_equal value, issue.send(attribute) + end + end + + def test_create_failure + attributes = {:project_id => 1} + assert_no_difference 'Issue.count' do + post '/issues.xml', {:issue => attributes}, :authorization => credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"} + end + + def test_update_routing + assert_routing( + {:method => :put, :path => '/issues/1.xml'}, + :controller => 'issues', :action => 'edit', :id => '1', :format => 'xml' + ) + end + + def test_update + attributes = {:subject => 'API update'} + assert_no_difference 'Issue.count' do + assert_difference 'Journal.count' do + put '/issues/1.xml', {:issue => attributes}, :authorization => credentials('jsmith') + end + end + assert_response :ok + assert_equal 'application/xml', @response.content_type + issue = Issue.find(1) + attributes.each do |attribute, value| + assert_equal value, issue.send(attribute) + end + end + + def test_update_failure + attributes = {:subject => ''} + assert_no_difference 'Issue.count' do + assert_no_difference 'Journal.count' do + put '/issues/1.xml', {:issue => attributes}, :authorization => credentials('jsmith') + end + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"} + end + + def test_destroy_routing + assert_routing( + {:method => :delete, :path => '/issues/1.xml'}, + :controller => 'issues', :action => 'destroy', :id => '1', :format => 'xml' + ) + end + + def test_destroy + assert_difference 'Issue.count', -1 do + delete '/issues/1.xml', {}, :authorization => credentials('jsmith') + end + assert_response :ok + assert_equal 'application/xml', @response.content_type + assert_nil Issue.find_by_id(1) + end + + def credentials(user, password=nil) + ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user) + end +end |