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