##// END OF EJS Templates
Add option to set a new version as default directly from New Version page (#24011)....
Jean-Philippe Lang -
r15540:eb023bdcce2b
parent child
Show More
@@ -1,336 +1,362
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 after_update :update_issues_from_sharing_change
21 after_update :update_issues_from_sharing_change
22 after_save :update_default_project_version
22 before_destroy :nullify_projects_default_version
23 before_destroy :nullify_projects_default_version
23
24
24 belongs_to :project
25 belongs_to :project
25 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
26 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
26 acts_as_customizable
27 acts_as_customizable
27 acts_as_attachable :view_permission => :view_files,
28 acts_as_attachable :view_permission => :view_files,
28 :edit_permission => :manage_files,
29 :edit_permission => :manage_files,
29 :delete_permission => :manage_files
30 :delete_permission => :manage_files
30
31
31 VERSION_STATUSES = %w(open locked closed)
32 VERSION_STATUSES = %w(open locked closed)
32 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
33 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
33
34
34 validates_presence_of :name
35 validates_presence_of :name
35 validates_uniqueness_of :name, :scope => [:project_id]
36 validates_uniqueness_of :name, :scope => [:project_id]
36 validates_length_of :name, :maximum => 60
37 validates_length_of :name, :maximum => 60
37 validates_length_of :description, :maximum => 255
38 validates_length_of :description, :maximum => 255
38 validates :effective_date, :date => true
39 validates :effective_date, :date => true
39 validates_inclusion_of :status, :in => VERSION_STATUSES
40 validates_inclusion_of :status, :in => VERSION_STATUSES
40 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
41 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
41 attr_protected :id
42 attr_protected :id
42
43
43 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
44 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
44 scope :like, lambda {|arg|
45 scope :like, lambda {|arg|
45 if arg.present?
46 if arg.present?
46 pattern = "%#{arg.to_s.strip}%"
47 pattern = "%#{arg.to_s.strip}%"
47 where("LOWER(#{Version.table_name}.name) LIKE :p", :p => pattern)
48 where("LOWER(#{Version.table_name}.name) LIKE :p", :p => pattern)
48 end
49 end
49 }
50 }
50 scope :open, lambda { where(:status => 'open') }
51 scope :open, lambda { where(:status => 'open') }
51 scope :status, lambda {|status|
52 scope :status, lambda {|status|
52 if status.present?
53 if status.present?
53 where(:status => status.to_s)
54 where(:status => status.to_s)
54 end
55 end
55 }
56 }
56 scope :visible, lambda {|*args|
57 scope :visible, lambda {|*args|
57 joins(:project).
58 joins(:project).
58 where(Project.allowed_to_condition(args.first || User.current, :view_issues))
59 where(Project.allowed_to_condition(args.first || User.current, :view_issues))
59 }
60 }
60
61
61 safe_attributes 'name',
62 safe_attributes 'name',
62 'description',
63 'description',
63 'effective_date',
64 'effective_date',
64 'due_date',
65 'due_date',
65 'wiki_page_title',
66 'wiki_page_title',
66 'status',
67 'status',
67 'sharing',
68 'sharing',
69 'default_project_version',
68 'custom_field_values',
70 'custom_field_values',
69 'custom_fields'
71 'custom_fields'
70
72
71 # Returns true if +user+ or current user is allowed to view the version
73 # Returns true if +user+ or current user is allowed to view the version
72 def visible?(user=User.current)
74 def visible?(user=User.current)
73 user.allowed_to?(:view_issues, self.project)
75 user.allowed_to?(:view_issues, self.project)
74 end
76 end
75
77
76 # Version files have same visibility as project files
78 # Version files have same visibility as project files
77 def attachments_visible?(*args)
79 def attachments_visible?(*args)
78 project.present? && project.attachments_visible?(*args)
80 project.present? && project.attachments_visible?(*args)
79 end
81 end
80
82
81 def attachments_deletable?(usr=User.current)
83 def attachments_deletable?(usr=User.current)
82 project.present? && project.attachments_deletable?(usr)
84 project.present? && project.attachments_deletable?(usr)
83 end
85 end
84
86
87 alias :base_reload :reload
88 def reload(*args)
89 @default_project_version = nil
90 base_reload(*args)
91 end
92
85 def start_date
93 def start_date
86 @start_date ||= fixed_issues.minimum('start_date')
94 @start_date ||= fixed_issues.minimum('start_date')
87 end
95 end
88
96
89 def due_date
97 def due_date
90 effective_date
98 effective_date
91 end
99 end
92
100
93 def due_date=(arg)
101 def due_date=(arg)
94 self.effective_date=(arg)
102 self.effective_date=(arg)
95 end
103 end
96
104
97 # Returns the total estimated time for this version
105 # Returns the total estimated time for this version
98 # (sum of leaves estimated_hours)
106 # (sum of leaves estimated_hours)
99 def estimated_hours
107 def estimated_hours
100 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
108 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
101 end
109 end
102
110
103 # Returns the total reported time for this version
111 # Returns the total reported time for this version
104 def spent_hours
112 def spent_hours
105 @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
113 @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
106 end
114 end
107
115
108 def closed?
116 def closed?
109 status == 'closed'
117 status == 'closed'
110 end
118 end
111
119
112 def open?
120 def open?
113 status == 'open'
121 status == 'open'
114 end
122 end
115
123
116 # Returns true if the version is completed: closed or due date reached and no open issues
124 # Returns true if the version is completed: closed or due date reached and no open issues
117 def completed?
125 def completed?
118 closed? || (effective_date && (effective_date < User.current.today) && (open_issues_count == 0))
126 closed? || (effective_date && (effective_date < User.current.today) && (open_issues_count == 0))
119 end
127 end
120
128
121 def behind_schedule?
129 def behind_schedule?
122 if completed_percent == 100
130 if completed_percent == 100
123 return false
131 return false
124 elsif due_date && start_date
132 elsif due_date && start_date
125 done_date = start_date + ((due_date - start_date+1)* completed_percent/100).floor
133 done_date = start_date + ((due_date - start_date+1)* completed_percent/100).floor
126 return done_date <= User.current.today
134 return done_date <= User.current.today
127 else
135 else
128 false # No issues so it's not late
136 false # No issues so it's not late
129 end
137 end
130 end
138 end
131
139
132 # Returns the completion percentage of this version based on the amount of open/closed issues
140 # Returns the completion percentage of this version based on the amount of open/closed issues
133 # and the time spent on the open issues.
141 # and the time spent on the open issues.
134 def completed_percent
142 def completed_percent
135 if issues_count == 0
143 if issues_count == 0
136 0
144 0
137 elsif open_issues_count == 0
145 elsif open_issues_count == 0
138 100
146 100
139 else
147 else
140 issues_progress(false) + issues_progress(true)
148 issues_progress(false) + issues_progress(true)
141 end
149 end
142 end
150 end
143
151
144 # Returns the percentage of issues that have been marked as 'closed'.
152 # Returns the percentage of issues that have been marked as 'closed'.
145 def closed_percent
153 def closed_percent
146 if issues_count == 0
154 if issues_count == 0
147 0
155 0
148 else
156 else
149 issues_progress(false)
157 issues_progress(false)
150 end
158 end
151 end
159 end
152
160
153 # Returns true if the version is overdue: due date reached and some open issues
161 # Returns true if the version is overdue: due date reached and some open issues
154 def overdue?
162 def overdue?
155 effective_date && (effective_date < User.current.today) && (open_issues_count > 0)
163 effective_date && (effective_date < User.current.today) && (open_issues_count > 0)
156 end
164 end
157
165
158 # Returns assigned issues count
166 # Returns assigned issues count
159 def issues_count
167 def issues_count
160 load_issue_counts
168 load_issue_counts
161 @issue_count
169 @issue_count
162 end
170 end
163
171
164 # Returns the total amount of open issues for this version.
172 # Returns the total amount of open issues for this version.
165 def open_issues_count
173 def open_issues_count
166 load_issue_counts
174 load_issue_counts
167 @open_issues_count
175 @open_issues_count
168 end
176 end
169
177
170 # Returns the total amount of closed issues for this version.
178 # Returns the total amount of closed issues for this version.
171 def closed_issues_count
179 def closed_issues_count
172 load_issue_counts
180 load_issue_counts
173 @closed_issues_count
181 @closed_issues_count
174 end
182 end
175
183
176 def wiki_page
184 def wiki_page
177 if project.wiki && !wiki_page_title.blank?
185 if project.wiki && !wiki_page_title.blank?
178 @wiki_page ||= project.wiki.find_page(wiki_page_title)
186 @wiki_page ||= project.wiki.find_page(wiki_page_title)
179 end
187 end
180 @wiki_page
188 @wiki_page
181 end
189 end
182
190
183 def to_s; name end
191 def to_s; name end
184
192
185 def to_s_with_project
193 def to_s_with_project
186 "#{project} - #{name}"
194 "#{project} - #{name}"
187 end
195 end
188
196
189 # Versions are sorted by effective_date and name
197 # Versions are sorted by effective_date and name
190 # Those with no effective_date are at the end, sorted by name
198 # Those with no effective_date are at the end, sorted by name
191 def <=>(version)
199 def <=>(version)
192 if self.effective_date
200 if self.effective_date
193 if version.effective_date
201 if version.effective_date
194 if self.effective_date == version.effective_date
202 if self.effective_date == version.effective_date
195 name == version.name ? id <=> version.id : name <=> version.name
203 name == version.name ? id <=> version.id : name <=> version.name
196 else
204 else
197 self.effective_date <=> version.effective_date
205 self.effective_date <=> version.effective_date
198 end
206 end
199 else
207 else
200 -1
208 -1
201 end
209 end
202 else
210 else
203 if version.effective_date
211 if version.effective_date
204 1
212 1
205 else
213 else
206 name == version.name ? id <=> version.id : name <=> version.name
214 name == version.name ? id <=> version.id : name <=> version.name
207 end
215 end
208 end
216 end
209 end
217 end
210
218
211 # Sort versions by status (open, locked then closed versions)
219 # Sort versions by status (open, locked then closed versions)
212 def self.sort_by_status(versions)
220 def self.sort_by_status(versions)
213 versions.sort do |a, b|
221 versions.sort do |a, b|
214 if a.status == b.status
222 if a.status == b.status
215 a <=> b
223 a <=> b
216 else
224 else
217 b.status <=> a.status
225 b.status <=> a.status
218 end
226 end
219 end
227 end
220 end
228 end
221
229
222 def css_classes
230 def css_classes
223 [
231 [
224 completed? ? 'version-completed' : 'version-incompleted',
232 completed? ? 'version-completed' : 'version-incompleted',
225 "version-#{status}"
233 "version-#{status}"
226 ].join(' ')
234 ].join(' ')
227 end
235 end
228
236
229 def self.fields_for_order_statement(table=nil)
237 def self.fields_for_order_statement(table=nil)
230 table ||= table_name
238 table ||= table_name
231 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
239 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
232 end
240 end
233
241
234 scope :sorted, lambda { order(fields_for_order_statement) }
242 scope :sorted, lambda { order(fields_for_order_statement) }
235
243
236 # Returns the sharings that +user+ can set the version to
244 # Returns the sharings that +user+ can set the version to
237 def allowed_sharings(user = User.current)
245 def allowed_sharings(user = User.current)
238 VERSION_SHARINGS.select do |s|
246 VERSION_SHARINGS.select do |s|
239 if sharing == s
247 if sharing == s
240 true
248 true
241 else
249 else
242 case s
250 case s
243 when 'system'
251 when 'system'
244 # Only admin users can set a systemwide sharing
252 # Only admin users can set a systemwide sharing
245 user.admin?
253 user.admin?
246 when 'hierarchy', 'tree'
254 when 'hierarchy', 'tree'
247 # Only users allowed to manage versions of the root project can
255 # Only users allowed to manage versions of the root project can
248 # set sharing to hierarchy or tree
256 # set sharing to hierarchy or tree
249 project.nil? || user.allowed_to?(:manage_versions, project.root)
257 project.nil? || user.allowed_to?(:manage_versions, project.root)
250 else
258 else
251 true
259 true
252 end
260 end
253 end
261 end
254 end
262 end
255 end
263 end
256
264
257 # Returns true if the version is shared, otherwise false
265 # Returns true if the version is shared, otherwise false
258 def shared?
266 def shared?
259 sharing != 'none'
267 sharing != 'none'
260 end
268 end
261
269
262 def deletable?
270 def deletable?
263 fixed_issues.empty? && !referenced_by_a_custom_field?
271 fixed_issues.empty? && !referenced_by_a_custom_field?
264 end
272 end
265
273
274 def default_project_version
275 if @default_project_version.nil?
276 project.present? && project.default_version == self
277 else
278 @default_project_version
279 end
280 end
281
282 def default_project_version=(arg)
283 @default_project_version = (arg == '1' || arg == true)
284 end
285
266 private
286 private
267
287
268 def load_issue_counts
288 def load_issue_counts
269 unless @issue_count
289 unless @issue_count
270 @open_issues_count = 0
290 @open_issues_count = 0
271 @closed_issues_count = 0
291 @closed_issues_count = 0
272 fixed_issues.group(:status).count.each do |status, count|
292 fixed_issues.group(:status).count.each do |status, count|
273 if status.is_closed?
293 if status.is_closed?
274 @closed_issues_count += count
294 @closed_issues_count += count
275 else
295 else
276 @open_issues_count += count
296 @open_issues_count += count
277 end
297 end
278 end
298 end
279 @issue_count = @open_issues_count + @closed_issues_count
299 @issue_count = @open_issues_count + @closed_issues_count
280 end
300 end
281 end
301 end
282
302
283 # Update the issue's fixed versions. Used if a version's sharing changes.
303 # Update the issue's fixed versions. Used if a version's sharing changes.
284 def update_issues_from_sharing_change
304 def update_issues_from_sharing_change
285 if sharing_changed?
305 if sharing_changed?
286 if VERSION_SHARINGS.index(sharing_was).nil? ||
306 if VERSION_SHARINGS.index(sharing_was).nil? ||
287 VERSION_SHARINGS.index(sharing).nil? ||
307 VERSION_SHARINGS.index(sharing).nil? ||
288 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
308 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
289 Issue.update_versions_from_sharing_change self
309 Issue.update_versions_from_sharing_change self
290 end
310 end
291 end
311 end
292 end
312 end
293
313
314 def update_default_project_version
315 if @default_project_version && project.present?
316 project.update_columns :default_version_id => id
317 end
318 end
319
294 # Returns the average estimated time of assigned issues
320 # Returns the average estimated time of assigned issues
295 # or 1 if no issue has an estimated time
321 # or 1 if no issue has an estimated time
296 # Used to weight unestimated issues in progress calculation
322 # Used to weight unestimated issues in progress calculation
297 def estimated_average
323 def estimated_average
298 if @estimated_average.nil?
324 if @estimated_average.nil?
299 average = fixed_issues.average(:estimated_hours).to_f
325 average = fixed_issues.average(:estimated_hours).to_f
300 if average == 0
326 if average == 0
301 average = 1
327 average = 1
302 end
328 end
303 @estimated_average = average
329 @estimated_average = average
304 end
330 end
305 @estimated_average
331 @estimated_average
306 end
332 end
307
333
308 # Returns the total progress of open or closed issues. The returned percentage takes into account
334 # Returns the total progress of open or closed issues. The returned percentage takes into account
309 # the amount of estimated time set for this version.
335 # the amount of estimated time set for this version.
310 #
336 #
311 # Examples:
337 # Examples:
312 # issues_progress(true) => returns the progress percentage for open issues.
338 # issues_progress(true) => returns the progress percentage for open issues.
313 # issues_progress(false) => returns the progress percentage for closed issues.
339 # issues_progress(false) => returns the progress percentage for closed issues.
314 def issues_progress(open)
340 def issues_progress(open)
315 @issues_progress ||= {}
341 @issues_progress ||= {}
316 @issues_progress[open] ||= begin
342 @issues_progress[open] ||= begin
317 progress = 0
343 progress = 0
318 if issues_count > 0
344 if issues_count > 0
319 ratio = open ? 'done_ratio' : 100
345 ratio = open ? 'done_ratio' : 100
320
346
321 done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
347 done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
322 progress = done / (estimated_average * issues_count)
348 progress = done / (estimated_average * issues_count)
323 end
349 end
324 progress
350 progress
325 end
351 end
326 end
352 end
327
353
328 def referenced_by_a_custom_field?
354 def referenced_by_a_custom_field?
329 CustomValue.joins(:custom_field).
355 CustomValue.joins(:custom_field).
330 where(:value => id.to_s, :custom_fields => {:field_format => 'version'}).any?
356 where(:value => id.to_s, :custom_fields => {:field_format => 'version'}).any?
331 end
357 end
332
358
333 def nullify_projects_default_version
359 def nullify_projects_default_version
334 Project.where(:default_version_id => id).update_all(:default_version_id => nil)
360 Project.where(:default_version_id => id).update_all(:default_version_id => nil)
335 end
361 end
336 end
362 end
@@ -1,18 +1,21
1 <%= back_url_hidden_field_tag %>
1 <%= back_url_hidden_field_tag %>
2 <%= error_messages_for 'version' %>
2 <%= error_messages_for 'version' %>
3
3
4 <div class="box tabular">
4 <div class="box tabular">
5 <p><%= f.text_field :name, :size => 60, :required => true %></p>
5 <p><%= f.text_field :name, :size => 60, :required => true %></p>
6 <p><%= f.text_field :description, :size => 60 %></p>
6 <p><%= f.text_field :description, :size => 60 %></p>
7 <% unless @version.new_record? %>
7 <% unless @version.new_record? %>
8 <p><%= f.select :status, Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %></p>
8 <p><%= f.select :status, Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %></p>
9 <% end %>
9 <% end %>
10 <p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
10 <p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
11 <p><%= f.date_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
11 <p><%= f.date_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
12 <p><%= f.select :sharing, @version.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %></p>
12 <p><%= f.select :sharing, @version.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %></p>
13 <% if @version.new_record? %>
14 <p><%= f.check_box :default_project_version, :label => :field_default_version %></p>
15 <% end %>
13
16
14 <% @version.custom_field_values.each do |value| %>
17 <% @version.custom_field_values.each do |value| %>
15 <p><%= custom_field_tag_with_label :version, value %></p>
18 <p><%= custom_field_tag_with_label :version, value %></p>
16 <% end %>
19 <% end %>
17
20
18 </div>
21 </div>
@@ -1,277 +1,293
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 VersionTest < ActiveSupport::TestCase
20 class VersionTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :issues, :issue_statuses, :trackers,
21 fixtures :projects, :users, :issues, :issue_statuses, :trackers,
22 :enumerations, :versions, :projects_trackers
22 :enumerations, :versions, :projects_trackers
23
23
24 def test_create
24 def test_create
25 v = Version.new(:project => Project.find(1), :name => '1.1',
25 v = Version.new(:project => Project.find(1), :name => '1.1',
26 :effective_date => '2011-03-25')
26 :effective_date => '2011-03-25')
27 assert v.save
27 assert v.save
28 assert_equal 'open', v.status
28 assert_equal 'open', v.status
29 assert_equal 'none', v.sharing
29 assert_equal 'none', v.sharing
30 end
30 end
31
31
32 def test_create_as_default_project_version
33 project = Project.find(1)
34 v = Version.new(:project => project, :name => '1.1',
35 :default_project_version => '1')
36 assert v.save
37 assert_equal v, project.reload.default_version
38 end
39
40 def test_create_not_as_default_project_version
41 project = Project.find(1)
42 v = Version.new(:project => project, :name => '1.1',
43 :default_project_version => '0')
44 assert v.save
45 assert_nil project.reload.default_version
46 end
47
32 def test_invalid_effective_date_validation
48 def test_invalid_effective_date_validation
33 v = Version.new(:project => Project.find(1), :name => '1.1',
49 v = Version.new(:project => Project.find(1), :name => '1.1',
34 :effective_date => '99999-01-01')
50 :effective_date => '99999-01-01')
35 assert !v.valid?
51 assert !v.valid?
36 v.effective_date = '2012-11-33'
52 v.effective_date = '2012-11-33'
37 assert !v.valid?
53 assert !v.valid?
38 v.effective_date = '2012-31-11'
54 v.effective_date = '2012-31-11'
39 assert !v.valid?
55 assert !v.valid?
40 v.effective_date = '-2012-31-11'
56 v.effective_date = '-2012-31-11'
41 assert !v.valid?
57 assert !v.valid?
42 v.effective_date = 'ABC'
58 v.effective_date = 'ABC'
43 assert !v.valid?
59 assert !v.valid?
44 assert_include I18n.translate('activerecord.errors.messages.not_a_date'),
60 assert_include I18n.translate('activerecord.errors.messages.not_a_date'),
45 v.errors[:effective_date]
61 v.errors[:effective_date]
46 end
62 end
47
63
48 def test_progress_should_be_0_with_no_assigned_issues
64 def test_progress_should_be_0_with_no_assigned_issues
49 project = Project.find(1)
65 project = Project.find(1)
50 v = Version.create!(:project => project, :name => 'Progress')
66 v = Version.create!(:project => project, :name => 'Progress')
51 assert_equal 0, v.completed_percent
67 assert_equal 0, v.completed_percent
52 assert_equal 0, v.closed_percent
68 assert_equal 0, v.closed_percent
53 end
69 end
54
70
55 def test_progress_should_be_0_with_unbegun_assigned_issues
71 def test_progress_should_be_0_with_unbegun_assigned_issues
56 project = Project.find(1)
72 project = Project.find(1)
57 v = Version.create!(:project => project, :name => 'Progress')
73 v = Version.create!(:project => project, :name => 'Progress')
58 add_issue(v)
74 add_issue(v)
59 add_issue(v, :done_ratio => 0)
75 add_issue(v, :done_ratio => 0)
60 assert_progress_equal 0, v.completed_percent
76 assert_progress_equal 0, v.completed_percent
61 assert_progress_equal 0, v.closed_percent
77 assert_progress_equal 0, v.closed_percent
62 end
78 end
63
79
64 def test_progress_should_be_100_with_closed_assigned_issues
80 def test_progress_should_be_100_with_closed_assigned_issues
65 project = Project.find(1)
81 project = Project.find(1)
66 status = IssueStatus.where(:is_closed => true).first
82 status = IssueStatus.where(:is_closed => true).first
67 v = Version.create!(:project => project, :name => 'Progress')
83 v = Version.create!(:project => project, :name => 'Progress')
68 add_issue(v, :status => status)
84 add_issue(v, :status => status)
69 add_issue(v, :status => status, :done_ratio => 20)
85 add_issue(v, :status => status, :done_ratio => 20)
70 add_issue(v, :status => status, :done_ratio => 70, :estimated_hours => 25)
86 add_issue(v, :status => status, :done_ratio => 70, :estimated_hours => 25)
71 add_issue(v, :status => status, :estimated_hours => 15)
87 add_issue(v, :status => status, :estimated_hours => 15)
72 assert_progress_equal 100.0, v.completed_percent
88 assert_progress_equal 100.0, v.completed_percent
73 assert_progress_equal 100.0, v.closed_percent
89 assert_progress_equal 100.0, v.closed_percent
74 end
90 end
75
91
76 def test_progress_should_consider_done_ratio_of_open_assigned_issues
92 def test_progress_should_consider_done_ratio_of_open_assigned_issues
77 project = Project.find(1)
93 project = Project.find(1)
78 v = Version.create!(:project => project, :name => 'Progress')
94 v = Version.create!(:project => project, :name => 'Progress')
79 add_issue(v)
95 add_issue(v)
80 add_issue(v, :done_ratio => 20)
96 add_issue(v, :done_ratio => 20)
81 add_issue(v, :done_ratio => 70)
97 add_issue(v, :done_ratio => 70)
82 assert_progress_equal (0.0 + 20.0 + 70.0)/3, v.completed_percent
98 assert_progress_equal (0.0 + 20.0 + 70.0)/3, v.completed_percent
83 assert_progress_equal 0, v.closed_percent
99 assert_progress_equal 0, v.closed_percent
84 end
100 end
85
101
86 def test_progress_should_consider_closed_issues_as_completed
102 def test_progress_should_consider_closed_issues_as_completed
87 project = Project.find(1)
103 project = Project.find(1)
88 v = Version.create!(:project => project, :name => 'Progress')
104 v = Version.create!(:project => project, :name => 'Progress')
89 add_issue(v)
105 add_issue(v)
90 add_issue(v, :done_ratio => 20)
106 add_issue(v, :done_ratio => 20)
91 add_issue(v, :status => IssueStatus.where(:is_closed => true).first)
107 add_issue(v, :status => IssueStatus.where(:is_closed => true).first)
92 assert_progress_equal (0.0 + 20.0 + 100.0)/3, v.completed_percent
108 assert_progress_equal (0.0 + 20.0 + 100.0)/3, v.completed_percent
93 assert_progress_equal (100.0)/3, v.closed_percent
109 assert_progress_equal (100.0)/3, v.closed_percent
94 end
110 end
95
111
96 def test_progress_should_consider_estimated_hours_to_weight_issues
112 def test_progress_should_consider_estimated_hours_to_weight_issues
97 project = Project.find(1)
113 project = Project.find(1)
98 v = Version.create!(:project => project, :name => 'Progress')
114 v = Version.create!(:project => project, :name => 'Progress')
99 add_issue(v, :estimated_hours => 10)
115 add_issue(v, :estimated_hours => 10)
100 add_issue(v, :estimated_hours => 20, :done_ratio => 30)
116 add_issue(v, :estimated_hours => 20, :done_ratio => 30)
101 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
117 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
102 add_issue(v, :estimated_hours => 25, :status => IssueStatus.where(:is_closed => true).first)
118 add_issue(v, :estimated_hours => 25, :status => IssueStatus.where(:is_closed => true).first)
103 assert_progress_equal (10.0*0 + 20.0*0.3 + 40*0.1 + 25.0*1)/95.0*100, v.completed_percent
119 assert_progress_equal (10.0*0 + 20.0*0.3 + 40*0.1 + 25.0*1)/95.0*100, v.completed_percent
104 assert_progress_equal 25.0/95.0*100, v.closed_percent
120 assert_progress_equal 25.0/95.0*100, v.closed_percent
105 end
121 end
106
122
107 def test_progress_should_consider_average_estimated_hours_to_weight_unestimated_issues
123 def test_progress_should_consider_average_estimated_hours_to_weight_unestimated_issues
108 project = Project.find(1)
124 project = Project.find(1)
109 v = Version.create!(:project => project, :name => 'Progress')
125 v = Version.create!(:project => project, :name => 'Progress')
110 add_issue(v, :done_ratio => 20)
126 add_issue(v, :done_ratio => 20)
111 add_issue(v, :status => IssueStatus.where(:is_closed => true).first)
127 add_issue(v, :status => IssueStatus.where(:is_closed => true).first)
112 add_issue(v, :estimated_hours => 10, :done_ratio => 30)
128 add_issue(v, :estimated_hours => 10, :done_ratio => 30)
113 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
129 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
114 assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_percent
130 assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_percent
115 assert_progress_equal 25.0/100.0*100, v.closed_percent
131 assert_progress_equal 25.0/100.0*100, v.closed_percent
116 end
132 end
117
133
118 def test_should_sort_scheduled_then_unscheduled_versions
134 def test_should_sort_scheduled_then_unscheduled_versions
119 Version.delete_all
135 Version.delete_all
120 v4 = Version.create!(:project_id => 1, :name => 'v4')
136 v4 = Version.create!(:project_id => 1, :name => 'v4')
121 v3 = Version.create!(:project_id => 1, :name => 'v2', :effective_date => '2012-07-14')
137 v3 = Version.create!(:project_id => 1, :name => 'v2', :effective_date => '2012-07-14')
122 v2 = Version.create!(:project_id => 1, :name => 'v1')
138 v2 = Version.create!(:project_id => 1, :name => 'v1')
123 v1 = Version.create!(:project_id => 1, :name => 'v3', :effective_date => '2012-08-02')
139 v1 = Version.create!(:project_id => 1, :name => 'v3', :effective_date => '2012-08-02')
124 v5 = Version.create!(:project_id => 1, :name => 'v5', :effective_date => '2012-07-02')
140 v5 = Version.create!(:project_id => 1, :name => 'v5', :effective_date => '2012-07-02')
125
141
126 assert_equal [v5, v3, v1, v2, v4], [v1, v2, v3, v4, v5].sort
142 assert_equal [v5, v3, v1, v2, v4], [v1, v2, v3, v4, v5].sort
127 assert_equal [v5, v3, v1, v2, v4], Version.sorted.to_a
143 assert_equal [v5, v3, v1, v2, v4], Version.sorted.to_a
128 end
144 end
129
145
130 def test_should_sort_versions_with_same_date_by_name
146 def test_should_sort_versions_with_same_date_by_name
131 v1 = Version.new(:effective_date => '2014-12-03', :name => 'v2')
147 v1 = Version.new(:effective_date => '2014-12-03', :name => 'v2')
132 v2 = Version.new(:effective_date => '2014-12-03', :name => 'v1')
148 v2 = Version.new(:effective_date => '2014-12-03', :name => 'v1')
133 assert_equal [v2, v1], [v1, v2].sort
149 assert_equal [v2, v1], [v1, v2].sort
134 end
150 end
135
151
136 def test_completed_should_be_false_when_due_today
152 def test_completed_should_be_false_when_due_today
137 version = Version.create!(:project_id => 1, :effective_date => Date.today, :name => 'Due today')
153 version = Version.create!(:project_id => 1, :effective_date => Date.today, :name => 'Due today')
138 assert_equal false, version.completed?
154 assert_equal false, version.completed?
139 end
155 end
140
156
141 def test_completed_should_be_true_when_closed
157 def test_completed_should_be_true_when_closed
142 version = Version.create!(:project_id => 1, :status => 'closed', :name => 'Closed')
158 version = Version.create!(:project_id => 1, :status => 'closed', :name => 'Closed')
143 assert_equal true, version.completed?
159 assert_equal true, version.completed?
144 end
160 end
145
161
146 test "#behind_schedule? should be false if there are no issues assigned" do
162 test "#behind_schedule? should be false if there are no issues assigned" do
147 version = Version.generate!(:effective_date => Date.yesterday)
163 version = Version.generate!(:effective_date => Date.yesterday)
148 assert_equal false, version.behind_schedule?
164 assert_equal false, version.behind_schedule?
149 end
165 end
150
166
151 test "#behind_schedule? should be false if there is no effective_date" do
167 test "#behind_schedule? should be false if there is no effective_date" do
152 version = Version.generate!(:effective_date => nil)
168 version = Version.generate!(:effective_date => nil)
153 assert_equal false, version.behind_schedule?
169 assert_equal false, version.behind_schedule?
154 end
170 end
155
171
156 test "#behind_schedule? should be false if all of the issues are ahead of schedule" do
172 test "#behind_schedule? should be false if all of the issues are ahead of schedule" do
157 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
173 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
158 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
174 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
159 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
175 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
160 assert_equal 60, version.completed_percent
176 assert_equal 60, version.completed_percent
161 assert_equal false, version.behind_schedule?
177 assert_equal false, version.behind_schedule?
162 end
178 end
163
179
164 test "#behind_schedule? should be true if any of the issues are behind schedule" do
180 test "#behind_schedule? should be true if any of the issues are behind schedule" do
165 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
181 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
166 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
182 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
167 add_issue(version, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left
183 add_issue(version, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left
168 assert_equal 40, version.completed_percent
184 assert_equal 40, version.completed_percent
169 assert_equal true, version.behind_schedule?
185 assert_equal true, version.behind_schedule?
170 end
186 end
171
187
172 test "#behind_schedule? should be false if all of the issues are complete" do
188 test "#behind_schedule? should be false if all of the issues are complete" do
173 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
189 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
174 add_issue(version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
190 add_issue(version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
175 add_issue(version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
191 add_issue(version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
176 assert_equal 100, version.completed_percent
192 assert_equal 100, version.completed_percent
177 assert_equal false, version.behind_schedule?
193 assert_equal false, version.behind_schedule?
178 end
194 end
179
195
180 test "#estimated_hours should return 0 with no assigned issues" do
196 test "#estimated_hours should return 0 with no assigned issues" do
181 version = Version.generate!
197 version = Version.generate!
182 assert_equal 0, version.estimated_hours
198 assert_equal 0, version.estimated_hours
183 end
199 end
184
200
185 test "#estimated_hours should return 0 with no estimated hours" do
201 test "#estimated_hours should return 0 with no estimated hours" do
186 version = Version.create!(:project_id => 1, :name => 'test')
202 version = Version.create!(:project_id => 1, :name => 'test')
187 add_issue(version)
203 add_issue(version)
188 assert_equal 0, version.estimated_hours
204 assert_equal 0, version.estimated_hours
189 end
205 end
190
206
191 test "#estimated_hours should return return the sum of estimated hours" do
207 test "#estimated_hours should return return the sum of estimated hours" do
192 version = Version.create!(:project_id => 1, :name => 'test')
208 version = Version.create!(:project_id => 1, :name => 'test')
193 add_issue(version, :estimated_hours => 2.5)
209 add_issue(version, :estimated_hours => 2.5)
194 add_issue(version, :estimated_hours => 5)
210 add_issue(version, :estimated_hours => 5)
195 assert_equal 7.5, version.estimated_hours
211 assert_equal 7.5, version.estimated_hours
196 end
212 end
197
213
198 test "#estimated_hours should return the sum of leaves estimated hours" do
214 test "#estimated_hours should return the sum of leaves estimated hours" do
199 version = Version.create!(:project_id => 1, :name => 'test')
215 version = Version.create!(:project_id => 1, :name => 'test')
200 parent = add_issue(version)
216 parent = add_issue(version)
201 add_issue(version, :estimated_hours => 2.5, :parent_issue_id => parent.id)
217 add_issue(version, :estimated_hours => 2.5, :parent_issue_id => parent.id)
202 add_issue(version, :estimated_hours => 5, :parent_issue_id => parent.id)
218 add_issue(version, :estimated_hours => 5, :parent_issue_id => parent.id)
203 assert_equal 7.5, version.estimated_hours
219 assert_equal 7.5, version.estimated_hours
204 end
220 end
205
221
206 test "should update all issue's fixed_version associations in case the hierarchy changed XXX" do
222 test "should update all issue's fixed_version associations in case the hierarchy changed XXX" do
207 User.current = User.find(1) # Need the admin's permissions
223 User.current = User.find(1) # Need the admin's permissions
208
224
209 @version = Version.find(7)
225 @version = Version.find(7)
210 # Separate hierarchy
226 # Separate hierarchy
211 project_1_issue = Issue.find(1)
227 project_1_issue = Issue.find(1)
212 project_1_issue.fixed_version = @version
228 project_1_issue.fixed_version = @version
213 assert project_1_issue.save, project_1_issue.errors.full_messages.to_s
229 assert project_1_issue.save, project_1_issue.errors.full_messages.to_s
214
230
215 project_5_issue = Issue.find(6)
231 project_5_issue = Issue.find(6)
216 project_5_issue.fixed_version = @version
232 project_5_issue.fixed_version = @version
217 assert project_5_issue.save
233 assert project_5_issue.save
218
234
219 # Project
235 # Project
220 project_2_issue = Issue.find(4)
236 project_2_issue = Issue.find(4)
221 project_2_issue.fixed_version = @version
237 project_2_issue.fixed_version = @version
222 assert project_2_issue.save
238 assert project_2_issue.save
223
239
224 # Update the sharing
240 # Update the sharing
225 @version.sharing = 'none'
241 @version.sharing = 'none'
226 assert @version.save
242 assert @version.save
227
243
228 # Project 1 now out of the shared scope
244 # Project 1 now out of the shared scope
229 project_1_issue.reload
245 project_1_issue.reload
230 assert_equal nil, project_1_issue.fixed_version,
246 assert_equal nil, project_1_issue.fixed_version,
231 "Fixed version is still set after changing the Version's sharing"
247 "Fixed version is still set after changing the Version's sharing"
232
248
233 # Project 5 now out of the shared scope
249 # Project 5 now out of the shared scope
234 project_5_issue.reload
250 project_5_issue.reload
235 assert_equal nil, project_5_issue.fixed_version,
251 assert_equal nil, project_5_issue.fixed_version,
236 "Fixed version is still set after changing the Version's sharing"
252 "Fixed version is still set after changing the Version's sharing"
237
253
238 # Project 2 issue remains
254 # Project 2 issue remains
239 project_2_issue.reload
255 project_2_issue.reload
240 assert_equal @version, project_2_issue.fixed_version
256 assert_equal @version, project_2_issue.fixed_version
241 end
257 end
242
258
243 def test_deletable_should_return_true_when_not_referenced
259 def test_deletable_should_return_true_when_not_referenced
244 version = Version.generate!
260 version = Version.generate!
245
261
246 assert_equal true, version.deletable?
262 assert_equal true, version.deletable?
247 end
263 end
248
264
249 def test_deletable_should_return_false_when_referenced_by_an_issue
265 def test_deletable_should_return_false_when_referenced_by_an_issue
250 version = Version.generate!
266 version = Version.generate!
251 Issue.generate!(:fixed_version => version)
267 Issue.generate!(:fixed_version => version)
252
268
253 assert_equal false, version.deletable?
269 assert_equal false, version.deletable?
254 end
270 end
255
271
256 def test_deletable_should_return_false_when_referenced_by_a_custom_field
272 def test_deletable_should_return_false_when_referenced_by_a_custom_field
257 version = Version.generate!
273 version = Version.generate!
258 field = IssueCustomField.generate!(:field_format => 'version')
274 field = IssueCustomField.generate!(:field_format => 'version')
259 value = CustomValue.create!(:custom_field => field, :customized => Issue.first, :value => version.id)
275 value = CustomValue.create!(:custom_field => field, :customized => Issue.first, :value => version.id)
260
276
261 assert_equal false, version.deletable?
277 assert_equal false, version.deletable?
262 end
278 end
263
279
264 private
280 private
265
281
266 def add_issue(version, attributes={})
282 def add_issue(version, attributes={})
267 Issue.create!({:project => version.project,
283 Issue.create!({:project => version.project,
268 :fixed_version => version,
284 :fixed_version => version,
269 :subject => 'Test',
285 :subject => 'Test',
270 :author => User.first,
286 :author => User.first,
271 :tracker => version.project.trackers.first}.merge(attributes))
287 :tracker => version.project.trackers.first}.merge(attributes))
272 end
288 end
273
289
274 def assert_progress_equal(expected_float, actual_float, message="")
290 def assert_progress_equal(expected_float, actual_float, message="")
275 assert_in_delta(expected_float, actual_float, 0.000001, message="")
291 assert_in_delta(expected_float, actual_float, 0.000001, message="")
276 end
292 end
277 end
293 end
General Comments 0
You need to be logged in to leave comments. Login now