]> source.dussan.org Git - redmine.git/commitdiff
Adds REST API for versions (#7403).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Mon, 4 Jul 2011 19:34:58 +0000 (19:34 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Mon, 4 Jul 2011 19:34:58 +0000 (19:34 +0000)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@6180 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/controllers/versions_controller.rb
app/views/projects/settings/_versions.rhtml
app/views/versions/index.api.rsb [new file with mode: 0644]
app/views/versions/show.api.rsb [new file with mode: 0644]
app/views/versions/show.rhtml
test/integration/api_test/versions_test.rb [new file with mode: 0644]
test/integration/routing_test.rb
test/unit/version_test.rb

index 30d8f3bf2a28b35b83dbc250cb0ca7e2c1b664d4..9eacc0ca20e6df1b6c855a03cfdaf292ca6a7a24 100644 (file)
@@ -1,5 +1,5 @@
-# redMine - project management software
-# Copyright (C) 2006  Jean-Philippe Lang
+# 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
@@ -23,37 +23,51 @@ class VersionsController < ApplicationController
   before_filter :find_project, :only => [:index, :new, :create, :close_completed]
   before_filter :authorize
 
+  accept_key_auth :index, :create, :update, :destroy
+  
   helper :custom_fields
   helper :projects
 
   def index
-    @trackers = @project.trackers.find(:all, :order => 'position')
-    retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
-    @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
-    project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
-    
-    @versions = @project.shared_versions || []
-    @versions += @project.rolled_up_versions.visible if @with_subprojects
-    @versions = @versions.uniq.sort
-    @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
-    
-    @issues_by_version = {}
-    unless @selected_tracker_ids.empty?
-      @versions.each do |version|
-        issues = version.fixed_issues.visible.find(:all,
-                                                   :include => [:project, :status, :tracker, :priority],
-                                                   :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
-                                                   :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
-        @issues_by_version[version] = issues
-      end
+    respond_to do |format|
+      format.html {
+        @trackers = @project.trackers.find(:all, :order => 'position')
+        retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
+        @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
+        project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
+        
+        @versions = @project.shared_versions || []
+        @versions += @project.rolled_up_versions.visible if @with_subprojects
+        @versions = @versions.uniq.sort
+        @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
+        
+        @issues_by_version = {}
+        unless @selected_tracker_ids.empty?
+          @versions.each do |version|
+            issues = version.fixed_issues.visible.find(:all,
+                                                       :include => [:project, :status, :tracker, :priority],
+                                                       :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
+                                                       :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
+            @issues_by_version[version] = issues
+          end
+        end
+        @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
+      }
+      format.api {
+        @versions = @project.shared_versions.all
+      }
     end
-    @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
   end
   
   def show
-    @issues = @version.fixed_issues.visible.find(:all,
-      :include => [:status, :tracker, :priority],
-      :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
+    respond_to do |format|
+      format.html {
+        @issues = @version.fixed_issues.visible.find(:all,
+          :include => [:status, :tracker, :priority],
+          :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
+      }
+      format.api
+    end
   end
   
   def new
@@ -87,6 +101,9 @@ class VersionsController < ApplicationController
               content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
             }
           end
+          format.api do
+            render :action => 'show', :status => :created, :location => project_version_url(@project, @version)
+          end
         end
       else
         respond_to do |format|
@@ -94,6 +111,7 @@ class VersionsController < ApplicationController
           format.js do
             render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
           end
+          format.api  { render_validation_errors(@version) }
         end
       end
     end
@@ -107,11 +125,17 @@ class VersionsController < ApplicationController
       attributes = params[:version].dup
       attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
       if @version.update_attributes(attributes)
-        flash[:notice] = l(:notice_successful_update)
-        redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+        respond_to do |format|
+          format.html {
+            flash[:notice] = l(:notice_successful_update)
+            redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+          }
+          format.api  { head :ok }
+        end
       else
         respond_to do |format|
           format.html { render :action => 'edit' }
+          format.api  { render_validation_errors(@version) }
         end
       end
     end
@@ -124,13 +148,22 @@ class VersionsController < ApplicationController
     redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
   end
 
+  verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
   def destroy
     if @version.fixed_issues.empty?
       @version.destroy
-      redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+      respond_to do |format|
+        format.html { redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project }
+        format.api  { head :ok }
+      end
     else
-      flash[:error] = l(:notice_unable_delete_version)
-      redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+      respond_to do |format|
+        format.html {
+          flash[:error] = l(:notice_unable_delete_version)
+          redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+        }
+        format.api  { head :unprocessable_entity }
+      end
     end
   end
   
index f9a4dd1098f8894e609ffa8445059a0959dee61e..f00fe5bd22f6096b73f5329a82916c5aa05bbb4e 100644 (file)
@@ -19,9 +19,9 @@
     <td class="sharing"><%=h format_version_sharing(version.sharing) %></td>
     <td><%= link_to_if_authorized(h(version.wiki_page_title), {:controller => 'wiki', :action => 'show', :project_id => version.project, :id => Wiki.titleize(version.wiki_page_title)}) || h(version.wiki_page_title) unless version.wiki_page_title.blank? || version.project.wiki.nil? %></td>
     <td class="buttons">
-       <% if version.project == @project %>
-               <%= link_to_if_authorized l(:button_edit),   {:controller => 'versions', :action => 'edit', :id => version }, :class => 'icon icon-edit' %>
-               <%= link_to_if_authorized l(:button_delete), {:controller => 'versions', :action => 'destroy', :id => version}, :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %>
+       <% if version.project == @project && User.current.allowed_to?(:manage_versions, @project) %>
+               <%= link_to l(:button_edit), edit_project_version_path(@project, version), :class => 'icon icon-edit' %>
+               <%= link_to l(:button_delete), project_version_path(@project, version), :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %>
        <% end %>
        </td>
     </tr>
diff --git a/app/views/versions/index.api.rsb b/app/views/versions/index.api.rsb
new file mode 100644 (file)
index 0000000..cf00e1a
--- /dev/null
@@ -0,0 +1,18 @@
+api.array :versions, api_meta(:total_count => @versions.size) do
+  @versions.each do |version|
+    api.version do
+      api.id version.id
+      api.project(:id => version.project_id, :name => version.project.name) unless version.project.nil?
+      
+      api.name                 version.name
+      api.description version.description
+      api.status      version.status
+      api.due_date    version.effective_date
+      
+      render_api_custom_values version.custom_field_values, api
+      
+      api.created_on version.created_on
+      api.updated_on version.updated_on
+    end
+  end
+end
diff --git a/app/views/versions/show.api.rsb b/app/views/versions/show.api.rsb
new file mode 100644 (file)
index 0000000..e52c52e
--- /dev/null
@@ -0,0 +1,14 @@
+api.version do
+  api.id @version.id
+  api.project(:id => @version.project_id, :name => @version.project.name) unless @version.project.nil?
+  
+  api.name                     @version.name
+  api.description @version.description
+  api.status      @version.status
+  api.due_date    @version.effective_date
+  
+  render_api_custom_values @version.custom_field_values, api
+  
+  api.created_on @version.created_on
+  api.updated_on @version.updated_on
+end
index 5fcdf8b826f44c4d4ee7f7aa82d2e63289705044..72ae8021801984f91580653c41f01f0db430e9b8 100644 (file)
@@ -1,8 +1,8 @@
 <div class="contextual">
-<%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => @version}, :class => 'icon icon-edit' %>
+<%= link_to(l(:button_edit), edit_project_version_path(@version.project, @version), :class => 'icon icon-edit') if User.current.allowed_to?(:manage_versions, @version.project) %>
 <%= link_to_if_authorized(l(:button_edit_associated_wikipage, :page_title => @version.wiki_page_title), {:controller => 'wiki', :action => 'edit', :project_id => @version.project, :id => Wiki.titleize(@version.wiki_page_title)}, :class => 'icon icon-edit') unless @version.wiki_page_title.blank? || @version.project.wiki.nil? %>
-<%= link_to_if_authorized l(:button_delete), {:controller => 'versions', :action => 'destroy', :id => @version, :back_url => url_for(:controller => 'versions', :action => 'index', :project_id => @version.project)}, 
-  :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %>
+<%= link_to(l(:button_delete), project_version_path(@version.project, @version, :back_url => url_for(:controller => 'versions', :action => 'index', :project_id => @version.project)),
+  :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del') if User.current.allowed_to?(:manage_versions, @version.project) %>
 <%= call_hook(:view_versions_show_contextual, { :version => @version, :project => @project }) %>
 </div>
 
diff --git a/test/integration/api_test/versions_test.rb b/test/integration/api_test/versions_test.rb
new file mode 100644 (file)
index 0000000..3872676
--- /dev/null
@@ -0,0 +1,120 @@
+# 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::VersionsTest < ActionController::IntegrationTest
+  fixtures :all
+
+  def setup
+    Setting.rest_api_enabled = '1'
+  end
+
+  context "/projects/:project_id/versions" do
+    context "GET" do
+      should "return project versions" do
+        get '/projects/1/versions.xml'
+        
+        assert_response :success
+        assert_equal 'application/xml', @response.content_type
+        assert_tag :tag => 'versions',
+          :attributes => {:type => 'array'},
+          :child => {
+            :tag => 'version',
+            :child => {
+              :tag => 'id',
+              :content => '2',
+              :sibling => {
+                :tag => 'name',
+                :content => '1.0'
+              }
+            }
+          }
+      end
+    end
+    
+    context "POST" do
+      should "create the version" do
+        assert_difference 'Version.count' do
+          post '/projects/1/versions.xml', {:version => {:name => 'API test'}}, :authorization => credentials('jsmith')
+        end
+        
+        version = Version.first(:order => 'id DESC')
+        assert_equal 'API test', version.name
+        
+        assert_response :created
+        assert_equal 'application/xml', @response.content_type
+        assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s}
+      end
+      
+      context "with failure" do
+        should "return the errors" do
+          assert_no_difference('Version.count') do
+            post '/projects/1/versions.xml', {:version => {:name => ''}}, :authorization => credentials('jsmith')
+          end
+
+          assert_response :unprocessable_entity
+          assert_tag :errors, :child => {:tag => 'error', :content => "Name can't be blank"}
+        end
+      end
+    end
+  end
+  
+  context "/projects/:project_id/versions/:id" do
+    context "GET" do
+      should "return the version" do
+        get '/projects/1/versions/2.xml'
+        
+        assert_response :success
+        assert_equal 'application/xml', @response.content_type
+        assert_tag 'version', 
+          :child => {
+            :tag => 'id',
+            :content => '2',
+            :sibling => {
+              :tag => 'name',
+              :content => '1.0'
+            }
+          }
+      end
+    end
+    
+    context "PUT" do
+      should "update the version" do
+        put '/projects/1/versions/2.xml', {:version => {:name => 'API update'}}, :authorization => credentials('jsmith')
+        
+        assert_response :ok
+        assert_equal 'API update', Version.find(2).name
+      end
+    end
+    
+    context "DELETE" do
+      should "destroy the version" do
+        assert_difference 'Version.count', -1 do
+          delete '/projects/1/versions/3.xml', {}, :authorization => credentials('jsmith')
+        end
+        
+        assert_response :ok
+        assert_nil Version.find_by_id(3)
+      end
+    end
+  end
+  
+  def credentials(user, password=nil)
+    ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
+  end
+end
index 1b1b36ea542171d87d783f38cdfb9ecb4ccd10d1..a53c46567dba502c4aca33794150208e13b1c802 100644 (file)
@@ -1,5 +1,5 @@
-# redMine - project management software
-# Copyright (C) 2006-2010  Jean-Philippe Lang
+# 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
@@ -326,16 +326,33 @@ class RoutingTest < ActionController::IntegrationTest
     should_route :delete, "/users/44.xml", :controller => 'users', :action => 'destroy', :id => '44', :format => 'xml'
   end
 
-  # TODO: should they all be scoped under /projects/:project_id ?
   context "versions" do
+    # /projects/foo/versions is /projects/foo/roadmap
+    should_route :get, "/projects/foo/versions.xml", :controller => 'versions', :action => 'index', :project_id => 'foo', :format => 'xml'
+    should_route :get, "/projects/foo/versions.json", :controller => 'versions', :action => 'index', :project_id => 'foo', :format => 'json'
+    
     should_route :get, "/projects/foo/versions/new", :controller => 'versions', :action => 'new', :project_id => 'foo'
-    should_route :get, "/versions/show/1", :controller => 'versions', :action => 'show', :id => '1'
-    should_route :get, "/versions/edit/1", :controller => 'versions', :action => 'edit', :id => '1'
-
+    
     should_route :post, "/projects/foo/versions", :controller => 'versions', :action => 'create', :project_id => 'foo'
-    should_route :post, "/versions/update/1", :controller => 'versions', :action => 'update', :id => '1'
-
-    should_route :delete, "/versions/destroy/1", :controller => 'versions', :action => 'destroy', :id => '1'
+    should_route :post, "/projects/foo/versions.xml", :controller => 'versions', :action => 'create', :project_id => 'foo', :format => 'xml'
+    should_route :post, "/projects/foo/versions.json", :controller => 'versions', :action => 'create', :project_id => 'foo', :format => 'json'
+    
+    should_route :get, "/projects/foo/versions/1", :controller => 'versions', :action => 'show', :project_id => 'foo', :id => '1'
+    should_route :get, "/projects/foo/versions/1.xml", :controller => 'versions', :action => 'show', :project_id => 'foo', :id => '1', :format => 'xml'
+    should_route :get, "/projects/foo/versions/1.json", :controller => 'versions', :action => 'show', :project_id => 'foo', :id => '1', :format => 'json'
+    
+    should_route :get, "/projects/foo/versions/1/edit", :controller => 'versions', :action => 'edit', :project_id => 'foo', :id => '1'
+    
+    should_route :put, "/projects/foo/versions/1", :controller => 'versions', :action => 'update', :project_id => 'foo', :id => '1'
+    should_route :put, "/projects/foo/versions/1.xml", :controller => 'versions', :action => 'update', :project_id => 'foo', :id => '1', :format => 'xml'
+    should_route :put, "/projects/foo/versions/1.json", :controller => 'versions', :action => 'update', :project_id => 'foo', :id => '1', :format => 'json'
+    
+    should_route :delete, "/projects/foo/versions/1", :controller => 'versions', :action => 'destroy', :project_id => 'foo', :id => '1'
+    should_route :delete, "/projects/foo/versions/1.xml", :controller => 'versions', :action => 'destroy', :project_id => 'foo', :id => '1', :format => 'xml'
+    should_route :delete, "/projects/foo/versions/1.json", :controller => 'versions', :action => 'destroy', :project_id => 'foo', :id => '1', :format => 'json'
+    
+    should_route :put, "/projects/foo/versions/close_completed", :controller => 'versions', :action => 'close_completed', :project_id => 'foo'
+    should_route :post, "/projects/foo/versions/1/status_by", :controller => 'versions', :action => 'status_by', :project_id => 'foo', :id => '1'
   end
 
   context "wiki (singular, project's pages)" do
index 4621ad733efa8676a9fa26dd3a82731869475f12..0cc45ccc4ab22d757be2ef094a0a6215a24eb26c 100644 (file)
@@ -1,5 +1,5 @@
 # Redmine - project management software
-# Copyright (C) 2006-2008  Jean-Philippe Lang
+# 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
@@ -27,6 +27,7 @@ class VersionTest < ActiveSupport::TestCase
     v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '2011-03-25')
     assert v.save
     assert_equal 'open', v.status
+    assert_equal 'none', v.sharing
   end
   
   def test_invalid_effective_date_validation