##// END OF EJS Templates
Makes Version REST API accept due_date attribute (#10013)....
Jean-Philippe Lang -
r8566:11725be2788b
parent child
Show More
@@ -1,239 +1,243
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Version < ActiveRecord::Base
18 class Version < ActiveRecord::Base
19 after_update :update_issues_from_sharing_change
19 after_update :update_issues_from_sharing_change
20 belongs_to :project
20 belongs_to :project
21 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
21 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
22 acts_as_customizable
22 acts_as_customizable
23 acts_as_attachable :view_permission => :view_files,
23 acts_as_attachable :view_permission => :view_files,
24 :delete_permission => :manage_files
24 :delete_permission => :manage_files
25
25
26 VERSION_STATUSES = %w(open locked closed)
26 VERSION_STATUSES = %w(open locked closed)
27 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
27 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
28
28
29 validates_presence_of :name
29 validates_presence_of :name
30 validates_uniqueness_of :name, :scope => [:project_id]
30 validates_uniqueness_of :name, :scope => [:project_id]
31 validates_length_of :name, :maximum => 60
31 validates_length_of :name, :maximum => 60
32 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
32 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
33 validates_inclusion_of :status, :in => VERSION_STATUSES
33 validates_inclusion_of :status, :in => VERSION_STATUSES
34 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
34 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
35
35
36 named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
36 named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
37 named_scope :open, :conditions => {:status => 'open'}
37 named_scope :open, :conditions => {:status => 'open'}
38 named_scope :visible, lambda {|*args| { :include => :project,
38 named_scope :visible, lambda {|*args| { :include => :project,
39 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
39 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
40
40
41 # Returns true if +user+ or current user is allowed to view the version
41 # Returns true if +user+ or current user is allowed to view the version
42 def visible?(user=User.current)
42 def visible?(user=User.current)
43 user.allowed_to?(:view_issues, self.project)
43 user.allowed_to?(:view_issues, self.project)
44 end
44 end
45
45
46 # Version files have same visibility as project files
46 # Version files have same visibility as project files
47 def attachments_visible?(*args)
47 def attachments_visible?(*args)
48 project.present? && project.attachments_visible?(*args)
48 project.present? && project.attachments_visible?(*args)
49 end
49 end
50
50
51 def start_date
51 def start_date
52 @start_date ||= fixed_issues.minimum('start_date')
52 @start_date ||= fixed_issues.minimum('start_date')
53 end
53 end
54
54
55 def due_date
55 def due_date
56 effective_date
56 effective_date
57 end
57 end
58
58
59 def due_date=(arg)
60 self.effective_date=(arg)
61 end
62
59 # Returns the total estimated time for this version
63 # Returns the total estimated time for this version
60 # (sum of leaves estimated_hours)
64 # (sum of leaves estimated_hours)
61 def estimated_hours
65 def estimated_hours
62 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
66 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
63 end
67 end
64
68
65 # Returns the total reported time for this version
69 # Returns the total reported time for this version
66 def spent_hours
70 def spent_hours
67 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
71 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
68 end
72 end
69
73
70 def closed?
74 def closed?
71 status == 'closed'
75 status == 'closed'
72 end
76 end
73
77
74 def open?
78 def open?
75 status == 'open'
79 status == 'open'
76 end
80 end
77
81
78 # Returns true if the version is completed: due date reached and no open issues
82 # Returns true if the version is completed: due date reached and no open issues
79 def completed?
83 def completed?
80 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
84 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
81 end
85 end
82
86
83 def behind_schedule?
87 def behind_schedule?
84 if completed_pourcent == 100
88 if completed_pourcent == 100
85 return false
89 return false
86 elsif due_date && start_date
90 elsif due_date && start_date
87 done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
91 done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
88 return done_date <= Date.today
92 return done_date <= Date.today
89 else
93 else
90 false # No issues so it's not late
94 false # No issues so it's not late
91 end
95 end
92 end
96 end
93
97
94 # Returns the completion percentage of this version based on the amount of open/closed issues
98 # Returns the completion percentage of this version based on the amount of open/closed issues
95 # and the time spent on the open issues.
99 # and the time spent on the open issues.
96 def completed_pourcent
100 def completed_pourcent
97 if issues_count == 0
101 if issues_count == 0
98 0
102 0
99 elsif open_issues_count == 0
103 elsif open_issues_count == 0
100 100
104 100
101 else
105 else
102 issues_progress(false) + issues_progress(true)
106 issues_progress(false) + issues_progress(true)
103 end
107 end
104 end
108 end
105
109
106 # Returns the percentage of issues that have been marked as 'closed'.
110 # Returns the percentage of issues that have been marked as 'closed'.
107 def closed_pourcent
111 def closed_pourcent
108 if issues_count == 0
112 if issues_count == 0
109 0
113 0
110 else
114 else
111 issues_progress(false)
115 issues_progress(false)
112 end
116 end
113 end
117 end
114
118
115 # Returns true if the version is overdue: due date reached and some open issues
119 # Returns true if the version is overdue: due date reached and some open issues
116 def overdue?
120 def overdue?
117 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
121 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
118 end
122 end
119
123
120 # Returns assigned issues count
124 # Returns assigned issues count
121 def issues_count
125 def issues_count
122 @issue_count ||= fixed_issues.count
126 @issue_count ||= fixed_issues.count
123 end
127 end
124
128
125 # Returns the total amount of open issues for this version.
129 # Returns the total amount of open issues for this version.
126 def open_issues_count
130 def open_issues_count
127 @open_issues_count ||= Issue.open.count(:all, :conditions => ["fixed_version_id = ?", self.id])
131 @open_issues_count ||= Issue.open.count(:all, :conditions => ["fixed_version_id = ?", self.id])
128 end
132 end
129
133
130 # Returns the total amount of closed issues for this version.
134 # Returns the total amount of closed issues for this version.
131 def closed_issues_count
135 def closed_issues_count
132 @closed_issues_count ||= Issue.open(false).count(:all, :conditions => ["fixed_version_id = ?", self.id])
136 @closed_issues_count ||= Issue.open(false).count(:all, :conditions => ["fixed_version_id = ?", self.id])
133 end
137 end
134
138
135 def wiki_page
139 def wiki_page
136 if project.wiki && !wiki_page_title.blank?
140 if project.wiki && !wiki_page_title.blank?
137 @wiki_page ||= project.wiki.find_page(wiki_page_title)
141 @wiki_page ||= project.wiki.find_page(wiki_page_title)
138 end
142 end
139 @wiki_page
143 @wiki_page
140 end
144 end
141
145
142 def to_s; name end
146 def to_s; name end
143
147
144 def to_s_with_project
148 def to_s_with_project
145 "#{project} - #{name}"
149 "#{project} - #{name}"
146 end
150 end
147
151
148 # Versions are sorted by effective_date and "Project Name - Version name"
152 # Versions are sorted by effective_date and "Project Name - Version name"
149 # Those with no effective_date are at the end, sorted by "Project Name - Version name"
153 # Those with no effective_date are at the end, sorted by "Project Name - Version name"
150 def <=>(version)
154 def <=>(version)
151 if self.effective_date
155 if self.effective_date
152 if version.effective_date
156 if version.effective_date
153 if self.effective_date == version.effective_date
157 if self.effective_date == version.effective_date
154 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
158 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
155 else
159 else
156 self.effective_date <=> version.effective_date
160 self.effective_date <=> version.effective_date
157 end
161 end
158 else
162 else
159 -1
163 -1
160 end
164 end
161 else
165 else
162 if version.effective_date
166 if version.effective_date
163 1
167 1
164 else
168 else
165 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
169 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
166 end
170 end
167 end
171 end
168 end
172 end
169
173
170 # Returns the sharings that +user+ can set the version to
174 # Returns the sharings that +user+ can set the version to
171 def allowed_sharings(user = User.current)
175 def allowed_sharings(user = User.current)
172 VERSION_SHARINGS.select do |s|
176 VERSION_SHARINGS.select do |s|
173 if sharing == s
177 if sharing == s
174 true
178 true
175 else
179 else
176 case s
180 case s
177 when 'system'
181 when 'system'
178 # Only admin users can set a systemwide sharing
182 # Only admin users can set a systemwide sharing
179 user.admin?
183 user.admin?
180 when 'hierarchy', 'tree'
184 when 'hierarchy', 'tree'
181 # Only users allowed to manage versions of the root project can
185 # Only users allowed to manage versions of the root project can
182 # set sharing to hierarchy or tree
186 # set sharing to hierarchy or tree
183 project.nil? || user.allowed_to?(:manage_versions, project.root)
187 project.nil? || user.allowed_to?(:manage_versions, project.root)
184 else
188 else
185 true
189 true
186 end
190 end
187 end
191 end
188 end
192 end
189 end
193 end
190
194
191 private
195 private
192
196
193 # Update the issue's fixed versions. Used if a version's sharing changes.
197 # Update the issue's fixed versions. Used if a version's sharing changes.
194 def update_issues_from_sharing_change
198 def update_issues_from_sharing_change
195 if sharing_changed?
199 if sharing_changed?
196 if VERSION_SHARINGS.index(sharing_was).nil? ||
200 if VERSION_SHARINGS.index(sharing_was).nil? ||
197 VERSION_SHARINGS.index(sharing).nil? ||
201 VERSION_SHARINGS.index(sharing).nil? ||
198 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
202 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
199 Issue.update_versions_from_sharing_change self
203 Issue.update_versions_from_sharing_change self
200 end
204 end
201 end
205 end
202 end
206 end
203
207
204 # Returns the average estimated time of assigned issues
208 # Returns the average estimated time of assigned issues
205 # or 1 if no issue has an estimated time
209 # or 1 if no issue has an estimated time
206 # Used to weigth unestimated issues in progress calculation
210 # Used to weigth unestimated issues in progress calculation
207 def estimated_average
211 def estimated_average
208 if @estimated_average.nil?
212 if @estimated_average.nil?
209 average = fixed_issues.average(:estimated_hours).to_f
213 average = fixed_issues.average(:estimated_hours).to_f
210 if average == 0
214 if average == 0
211 average = 1
215 average = 1
212 end
216 end
213 @estimated_average = average
217 @estimated_average = average
214 end
218 end
215 @estimated_average
219 @estimated_average
216 end
220 end
217
221
218 # Returns the total progress of open or closed issues. The returned percentage takes into account
222 # Returns the total progress of open or closed issues. The returned percentage takes into account
219 # the amount of estimated time set for this version.
223 # the amount of estimated time set for this version.
220 #
224 #
221 # Examples:
225 # Examples:
222 # issues_progress(true) => returns the progress percentage for open issues.
226 # issues_progress(true) => returns the progress percentage for open issues.
223 # issues_progress(false) => returns the progress percentage for closed issues.
227 # issues_progress(false) => returns the progress percentage for closed issues.
224 def issues_progress(open)
228 def issues_progress(open)
225 @issues_progress ||= {}
229 @issues_progress ||= {}
226 @issues_progress[open] ||= begin
230 @issues_progress[open] ||= begin
227 progress = 0
231 progress = 0
228 if issues_count > 0
232 if issues_count > 0
229 ratio = open ? 'done_ratio' : 100
233 ratio = open ? 'done_ratio' : 100
230
234
231 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
235 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
232 :include => :status,
236 :include => :status,
233 :conditions => ["is_closed = ?", !open]).to_f
237 :conditions => ["is_closed = ?", !open]).to_f
234 progress = done / (estimated_average * issues_count)
238 progress = done / (estimated_average * issues_count)
235 end
239 end
236 progress
240 progress
237 end
241 end
238 end
242 end
239 end
243 end
@@ -1,124 +1,138
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::VersionsTest < ActionController::IntegrationTest
20 class ApiTest::VersionsTest < ActionController::IntegrationTest
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :enumerations, :users, :issue_categories,
22 :enumerations, :users, :issue_categories,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :enabled_modules,
27 :enabled_modules,
28 :workflows,
28 :workflows,
29 :versions
29 :versions
30
30
31 def setup
31 def setup
32 Setting.rest_api_enabled = '1'
32 Setting.rest_api_enabled = '1'
33 end
33 end
34
34
35 context "/projects/:project_id/versions" do
35 context "/projects/:project_id/versions" do
36 context "GET" do
36 context "GET" do
37 should "return project versions" do
37 should "return project versions" do
38 get '/projects/1/versions.xml'
38 get '/projects/1/versions.xml'
39
39
40 assert_response :success
40 assert_response :success
41 assert_equal 'application/xml', @response.content_type
41 assert_equal 'application/xml', @response.content_type
42 assert_tag :tag => 'versions',
42 assert_tag :tag => 'versions',
43 :attributes => {:type => 'array'},
43 :attributes => {:type => 'array'},
44 :child => {
44 :child => {
45 :tag => 'version',
45 :tag => 'version',
46 :child => {
46 :child => {
47 :tag => 'id',
47 :tag => 'id',
48 :content => '2',
48 :content => '2',
49 :sibling => {
49 :sibling => {
50 :tag => 'name',
50 :tag => 'name',
51 :content => '1.0'
51 :content => '1.0'
52 }
52 }
53 }
53 }
54 }
54 }
55 end
55 end
56 end
56 end
57
57
58 context "POST" do
58 context "POST" do
59 should "create the version" do
59 should "create the version" do
60 assert_difference 'Version.count' do
60 assert_difference 'Version.count' do
61 post '/projects/1/versions.xml', {:version => {:name => 'API test'}}, credentials('jsmith')
61 post '/projects/1/versions.xml', {:version => {:name => 'API test'}}, credentials('jsmith')
62 end
62 end
63
63
64 version = Version.first(:order => 'id DESC')
64 version = Version.first(:order => 'id DESC')
65 assert_equal 'API test', version.name
65 assert_equal 'API test', version.name
66
66
67 assert_response :created
67 assert_response :created
68 assert_equal 'application/xml', @response.content_type
68 assert_equal 'application/xml', @response.content_type
69 assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s}
69 assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s}
70 end
70 end
71
71
72 should "create the version with due date" do
73 assert_difference 'Version.count' do
74 post '/projects/1/versions.xml', {:version => {:name => 'API test', :due_date => '2012-01-24'}}, credentials('jsmith')
75 end
76
77 version = Version.first(:order => 'id DESC')
78 assert_equal 'API test', version.name
79 assert_equal Date.parse('2012-01-24'), version.due_date
80
81 assert_response :created
82 assert_equal 'application/xml', @response.content_type
83 assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s}
84 end
85
72 context "with failure" do
86 context "with failure" do
73 should "return the errors" do
87 should "return the errors" do
74 assert_no_difference('Version.count') do
88 assert_no_difference('Version.count') do
75 post '/projects/1/versions.xml', {:version => {:name => ''}}, credentials('jsmith')
89 post '/projects/1/versions.xml', {:version => {:name => ''}}, credentials('jsmith')
76 end
90 end
77
91
78 assert_response :unprocessable_entity
92 assert_response :unprocessable_entity
79 assert_tag :errors, :child => {:tag => 'error', :content => "Name can't be blank"}
93 assert_tag :errors, :child => {:tag => 'error', :content => "Name can't be blank"}
80 end
94 end
81 end
95 end
82 end
96 end
83 end
97 end
84
98
85 context "/versions/:id" do
99 context "/versions/:id" do
86 context "GET" do
100 context "GET" do
87 should "return the version" do
101 should "return the version" do
88 get '/versions/2.xml'
102 get '/versions/2.xml'
89
103
90 assert_response :success
104 assert_response :success
91 assert_equal 'application/xml', @response.content_type
105 assert_equal 'application/xml', @response.content_type
92 assert_tag 'version',
106 assert_tag 'version',
93 :child => {
107 :child => {
94 :tag => 'id',
108 :tag => 'id',
95 :content => '2',
109 :content => '2',
96 :sibling => {
110 :sibling => {
97 :tag => 'name',
111 :tag => 'name',
98 :content => '1.0'
112 :content => '1.0'
99 }
113 }
100 }
114 }
101 end
115 end
102 end
116 end
103
117
104 context "PUT" do
118 context "PUT" do
105 should "update the version" do
119 should "update the version" do
106 put '/versions/2.xml', {:version => {:name => 'API update'}}, credentials('jsmith')
120 put '/versions/2.xml', {:version => {:name => 'API update'}}, credentials('jsmith')
107
121
108 assert_response :ok
122 assert_response :ok
109 assert_equal 'API update', Version.find(2).name
123 assert_equal 'API update', Version.find(2).name
110 end
124 end
111 end
125 end
112
126
113 context "DELETE" do
127 context "DELETE" do
114 should "destroy the version" do
128 should "destroy the version" do
115 assert_difference 'Version.count', -1 do
129 assert_difference 'Version.count', -1 do
116 delete '/versions/3.xml', {}, credentials('jsmith')
130 delete '/versions/3.xml', {}, credentials('jsmith')
117 end
131 end
118
132
119 assert_response :ok
133 assert_response :ok
120 assert_nil Version.find_by_id(3)
134 assert_nil Version.find_by_id(3)
121 end
135 end
122 end
136 end
123 end
137 end
124 end
138 end
General Comments 0
You need to be logged in to leave comments. Login now