##// END OF EJS Templates
Sort issues by scheduled versions first then unscheduled versions....
Jean-Philippe Lang -
r9968:34cdac166949
parent child
Show More
@@ -1,276 +1,278
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 after_update :update_issues_from_sharing_change
20 after_update :update_issues_from_sharing_change
21 belongs_to :project
21 belongs_to :project
22 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
22 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
23 acts_as_customizable
23 acts_as_customizable
24 acts_as_attachable :view_permission => :view_files,
24 acts_as_attachable :view_permission => :view_files,
25 :delete_permission => :manage_files
25 :delete_permission => :manage_files
26
26
27 VERSION_STATUSES = %w(open locked closed)
27 VERSION_STATUSES = %w(open locked closed)
28 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
28 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
29
29
30 validates_presence_of :name
30 validates_presence_of :name
31 validates_uniqueness_of :name, :scope => [:project_id]
31 validates_uniqueness_of :name, :scope => [:project_id]
32 validates_length_of :name, :maximum => 60
32 validates_length_of :name, :maximum => 60
33 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
33 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
34 validates_inclusion_of :status, :in => VERSION_STATUSES
34 validates_inclusion_of :status, :in => VERSION_STATUSES
35 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
35 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
36
36
37 scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
37 scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
38 scope :open, :conditions => {:status => 'open'}
38 scope :open, :conditions => {:status => 'open'}
39 scope :visible, lambda {|*args| { :include => :project,
39 scope :visible, lambda {|*args| { :include => :project,
40 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
40 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
41
41
42 safe_attributes 'name',
42 safe_attributes 'name',
43 'description',
43 'description',
44 'effective_date',
44 'effective_date',
45 'due_date',
45 'due_date',
46 'wiki_page_title',
46 'wiki_page_title',
47 'status',
47 'status',
48 'sharing',
48 'sharing',
49 'custom_field_values'
49 'custom_field_values'
50
50
51 # Returns true if +user+ or current user is allowed to view the version
51 # Returns true if +user+ or current user is allowed to view the version
52 def visible?(user=User.current)
52 def visible?(user=User.current)
53 user.allowed_to?(:view_issues, self.project)
53 user.allowed_to?(:view_issues, self.project)
54 end
54 end
55
55
56 # Version files have same visibility as project files
56 # Version files have same visibility as project files
57 def attachments_visible?(*args)
57 def attachments_visible?(*args)
58 project.present? && project.attachments_visible?(*args)
58 project.present? && project.attachments_visible?(*args)
59 end
59 end
60
60
61 def start_date
61 def start_date
62 @start_date ||= fixed_issues.minimum('start_date')
62 @start_date ||= fixed_issues.minimum('start_date')
63 end
63 end
64
64
65 def due_date
65 def due_date
66 effective_date
66 effective_date
67 end
67 end
68
68
69 def due_date=(arg)
69 def due_date=(arg)
70 self.effective_date=(arg)
70 self.effective_date=(arg)
71 end
71 end
72
72
73 # Returns the total estimated time for this version
73 # Returns the total estimated time for this version
74 # (sum of leaves estimated_hours)
74 # (sum of leaves estimated_hours)
75 def estimated_hours
75 def estimated_hours
76 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
76 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
77 end
77 end
78
78
79 # Returns the total reported time for this version
79 # Returns the total reported time for this version
80 def spent_hours
80 def spent_hours
81 @spent_hours ||= TimeEntry.sum(:hours, :joins => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
81 @spent_hours ||= TimeEntry.sum(:hours, :joins => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
82 end
82 end
83
83
84 def closed?
84 def closed?
85 status == 'closed'
85 status == 'closed'
86 end
86 end
87
87
88 def open?
88 def open?
89 status == 'open'
89 status == 'open'
90 end
90 end
91
91
92 # Returns true if the version is completed: due date reached and no open issues
92 # Returns true if the version is completed: due date reached and no open issues
93 def completed?
93 def completed?
94 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
94 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
95 end
95 end
96
96
97 def behind_schedule?
97 def behind_schedule?
98 if completed_pourcent == 100
98 if completed_pourcent == 100
99 return false
99 return false
100 elsif due_date && start_date
100 elsif due_date && start_date
101 done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
101 done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
102 return done_date <= Date.today
102 return done_date <= Date.today
103 else
103 else
104 false # No issues so it's not late
104 false # No issues so it's not late
105 end
105 end
106 end
106 end
107
107
108 # Returns the completion percentage of this version based on the amount of open/closed issues
108 # Returns the completion percentage of this version based on the amount of open/closed issues
109 # and the time spent on the open issues.
109 # and the time spent on the open issues.
110 def completed_pourcent
110 def completed_pourcent
111 if issues_count == 0
111 if issues_count == 0
112 0
112 0
113 elsif open_issues_count == 0
113 elsif open_issues_count == 0
114 100
114 100
115 else
115 else
116 issues_progress(false) + issues_progress(true)
116 issues_progress(false) + issues_progress(true)
117 end
117 end
118 end
118 end
119
119
120 # Returns the percentage of issues that have been marked as 'closed'.
120 # Returns the percentage of issues that have been marked as 'closed'.
121 def closed_pourcent
121 def closed_pourcent
122 if issues_count == 0
122 if issues_count == 0
123 0
123 0
124 else
124 else
125 issues_progress(false)
125 issues_progress(false)
126 end
126 end
127 end
127 end
128
128
129 # Returns true if the version is overdue: due date reached and some open issues
129 # Returns true if the version is overdue: due date reached and some open issues
130 def overdue?
130 def overdue?
131 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
131 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
132 end
132 end
133
133
134 # Returns assigned issues count
134 # Returns assigned issues count
135 def issues_count
135 def issues_count
136 load_issue_counts
136 load_issue_counts
137 @issue_count
137 @issue_count
138 end
138 end
139
139
140 # Returns the total amount of open issues for this version.
140 # Returns the total amount of open issues for this version.
141 def open_issues_count
141 def open_issues_count
142 load_issue_counts
142 load_issue_counts
143 @open_issues_count
143 @open_issues_count
144 end
144 end
145
145
146 # Returns the total amount of closed issues for this version.
146 # Returns the total amount of closed issues for this version.
147 def closed_issues_count
147 def closed_issues_count
148 load_issue_counts
148 load_issue_counts
149 @closed_issues_count
149 @closed_issues_count
150 end
150 end
151
151
152 def wiki_page
152 def wiki_page
153 if project.wiki && !wiki_page_title.blank?
153 if project.wiki && !wiki_page_title.blank?
154 @wiki_page ||= project.wiki.find_page(wiki_page_title)
154 @wiki_page ||= project.wiki.find_page(wiki_page_title)
155 end
155 end
156 @wiki_page
156 @wiki_page
157 end
157 end
158
158
159 def to_s; name end
159 def to_s; name end
160
160
161 def to_s_with_project
161 def to_s_with_project
162 "#{project} - #{name}"
162 "#{project} - #{name}"
163 end
163 end
164
164
165 # Versions are sorted by effective_date and "Project Name - Version name"
165 # Versions are sorted by effective_date and "Project Name - Version name"
166 # Those with no effective_date are at the end, sorted by "Project Name - Version name"
166 # Those with no effective_date are at the end, sorted by "Project Name - Version name"
167 def <=>(version)
167 def <=>(version)
168 if self.effective_date
168 if self.effective_date
169 if version.effective_date
169 if version.effective_date
170 if self.effective_date == version.effective_date
170 if self.effective_date == version.effective_date
171 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
171 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
172 else
172 else
173 self.effective_date <=> version.effective_date
173 self.effective_date <=> version.effective_date
174 end
174 end
175 else
175 else
176 -1
176 -1
177 end
177 end
178 else
178 else
179 if version.effective_date
179 if version.effective_date
180 1
180 1
181 else
181 else
182 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
182 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
183 end
183 end
184 end
184 end
185 end
185 end
186
186
187 def self.fields_for_order_statement(table=nil)
187 def self.fields_for_order_statement(table=nil)
188 table ||= table_name
188 table ||= table_name
189 %w(effective_date name).map {|field| "#{table}.#{field}"}
189 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
190 end
190 end
191
191
192 scope :sorted, order(fields_for_order_statement)
193
192 # Returns the sharings that +user+ can set the version to
194 # Returns the sharings that +user+ can set the version to
193 def allowed_sharings(user = User.current)
195 def allowed_sharings(user = User.current)
194 VERSION_SHARINGS.select do |s|
196 VERSION_SHARINGS.select do |s|
195 if sharing == s
197 if sharing == s
196 true
198 true
197 else
199 else
198 case s
200 case s
199 when 'system'
201 when 'system'
200 # Only admin users can set a systemwide sharing
202 # Only admin users can set a systemwide sharing
201 user.admin?
203 user.admin?
202 when 'hierarchy', 'tree'
204 when 'hierarchy', 'tree'
203 # Only users allowed to manage versions of the root project can
205 # Only users allowed to manage versions of the root project can
204 # set sharing to hierarchy or tree
206 # set sharing to hierarchy or tree
205 project.nil? || user.allowed_to?(:manage_versions, project.root)
207 project.nil? || user.allowed_to?(:manage_versions, project.root)
206 else
208 else
207 true
209 true
208 end
210 end
209 end
211 end
210 end
212 end
211 end
213 end
212
214
213 private
215 private
214
216
215 def load_issue_counts
217 def load_issue_counts
216 unless @issue_count
218 unless @issue_count
217 @open_issues_count = 0
219 @open_issues_count = 0
218 @closed_issues_count = 0
220 @closed_issues_count = 0
219 fixed_issues.count(:all, :group => :status).each do |status, count|
221 fixed_issues.count(:all, :group => :status).each do |status, count|
220 if status.is_closed?
222 if status.is_closed?
221 @closed_issues_count += count
223 @closed_issues_count += count
222 else
224 else
223 @open_issues_count += count
225 @open_issues_count += count
224 end
226 end
225 end
227 end
226 @issue_count = @open_issues_count + @closed_issues_count
228 @issue_count = @open_issues_count + @closed_issues_count
227 end
229 end
228 end
230 end
229
231
230 # Update the issue's fixed versions. Used if a version's sharing changes.
232 # Update the issue's fixed versions. Used if a version's sharing changes.
231 def update_issues_from_sharing_change
233 def update_issues_from_sharing_change
232 if sharing_changed?
234 if sharing_changed?
233 if VERSION_SHARINGS.index(sharing_was).nil? ||
235 if VERSION_SHARINGS.index(sharing_was).nil? ||
234 VERSION_SHARINGS.index(sharing).nil? ||
236 VERSION_SHARINGS.index(sharing).nil? ||
235 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
237 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
236 Issue.update_versions_from_sharing_change self
238 Issue.update_versions_from_sharing_change self
237 end
239 end
238 end
240 end
239 end
241 end
240
242
241 # Returns the average estimated time of assigned issues
243 # Returns the average estimated time of assigned issues
242 # or 1 if no issue has an estimated time
244 # or 1 if no issue has an estimated time
243 # Used to weigth unestimated issues in progress calculation
245 # Used to weigth unestimated issues in progress calculation
244 def estimated_average
246 def estimated_average
245 if @estimated_average.nil?
247 if @estimated_average.nil?
246 average = fixed_issues.average(:estimated_hours).to_f
248 average = fixed_issues.average(:estimated_hours).to_f
247 if average == 0
249 if average == 0
248 average = 1
250 average = 1
249 end
251 end
250 @estimated_average = average
252 @estimated_average = average
251 end
253 end
252 @estimated_average
254 @estimated_average
253 end
255 end
254
256
255 # Returns the total progress of open or closed issues. The returned percentage takes into account
257 # Returns the total progress of open or closed issues. The returned percentage takes into account
256 # the amount of estimated time set for this version.
258 # the amount of estimated time set for this version.
257 #
259 #
258 # Examples:
260 # Examples:
259 # issues_progress(true) => returns the progress percentage for open issues.
261 # issues_progress(true) => returns the progress percentage for open issues.
260 # issues_progress(false) => returns the progress percentage for closed issues.
262 # issues_progress(false) => returns the progress percentage for closed issues.
261 def issues_progress(open)
263 def issues_progress(open)
262 @issues_progress ||= {}
264 @issues_progress ||= {}
263 @issues_progress[open] ||= begin
265 @issues_progress[open] ||= begin
264 progress = 0
266 progress = 0
265 if issues_count > 0
267 if issues_count > 0
266 ratio = open ? 'done_ratio' : 100
268 ratio = open ? 'done_ratio' : 100
267
269
268 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
270 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
269 :joins => :status,
271 :joins => :status,
270 :conditions => ["#{IssueStatus.table_name}.is_closed = ?", !open]).to_f
272 :conditions => ["#{IssueStatus.table_name}.is_closed = ?", !open]).to_f
271 progress = done / (estimated_average * issues_count)
273 progress = done / (estimated_average * issues_count)
272 end
274 end
273 progress
275 progress
274 end
276 end
275 end
277 end
276 end
278 end
@@ -1,229 +1,240
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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, :enumerations, :versions
21 fixtures :projects, :users, :issues, :issue_statuses, :trackers, :enumerations, :versions
22
22
23 def setup
23 def setup
24 end
24 end
25
25
26 def test_create
26 def test_create
27 v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '2011-03-25')
27 v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '2011-03-25')
28 assert v.save
28 assert v.save
29 assert_equal 'open', v.status
29 assert_equal 'open', v.status
30 assert_equal 'none', v.sharing
30 assert_equal 'none', v.sharing
31 end
31 end
32
32
33 def test_invalid_effective_date_validation
33 def test_invalid_effective_date_validation
34 v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '99999-01-01')
34 v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '99999-01-01')
35 assert !v.save
35 assert !v.save
36 assert_include I18n.translate('activerecord.errors.messages.not_a_date'),
36 assert_include I18n.translate('activerecord.errors.messages.not_a_date'),
37 v.errors[:effective_date]
37 v.errors[:effective_date]
38 end
38 end
39
39
40 def test_progress_should_be_0_with_no_assigned_issues
40 def test_progress_should_be_0_with_no_assigned_issues
41 project = Project.find(1)
41 project = Project.find(1)
42 v = Version.create!(:project => project, :name => 'Progress')
42 v = Version.create!(:project => project, :name => 'Progress')
43 assert_equal 0, v.completed_pourcent
43 assert_equal 0, v.completed_pourcent
44 assert_equal 0, v.closed_pourcent
44 assert_equal 0, v.closed_pourcent
45 end
45 end
46
46
47 def test_progress_should_be_0_with_unbegun_assigned_issues
47 def test_progress_should_be_0_with_unbegun_assigned_issues
48 project = Project.find(1)
48 project = Project.find(1)
49 v = Version.create!(:project => project, :name => 'Progress')
49 v = Version.create!(:project => project, :name => 'Progress')
50 add_issue(v)
50 add_issue(v)
51 add_issue(v, :done_ratio => 0)
51 add_issue(v, :done_ratio => 0)
52 assert_progress_equal 0, v.completed_pourcent
52 assert_progress_equal 0, v.completed_pourcent
53 assert_progress_equal 0, v.closed_pourcent
53 assert_progress_equal 0, v.closed_pourcent
54 end
54 end
55
55
56 def test_progress_should_be_100_with_closed_assigned_issues
56 def test_progress_should_be_100_with_closed_assigned_issues
57 project = Project.find(1)
57 project = Project.find(1)
58 status = IssueStatus.find(:first, :conditions => {:is_closed => true})
58 status = IssueStatus.find(:first, :conditions => {:is_closed => true})
59 v = Version.create!(:project => project, :name => 'Progress')
59 v = Version.create!(:project => project, :name => 'Progress')
60 add_issue(v, :status => status)
60 add_issue(v, :status => status)
61 add_issue(v, :status => status, :done_ratio => 20)
61 add_issue(v, :status => status, :done_ratio => 20)
62 add_issue(v, :status => status, :done_ratio => 70, :estimated_hours => 25)
62 add_issue(v, :status => status, :done_ratio => 70, :estimated_hours => 25)
63 add_issue(v, :status => status, :estimated_hours => 15)
63 add_issue(v, :status => status, :estimated_hours => 15)
64 assert_progress_equal 100.0, v.completed_pourcent
64 assert_progress_equal 100.0, v.completed_pourcent
65 assert_progress_equal 100.0, v.closed_pourcent
65 assert_progress_equal 100.0, v.closed_pourcent
66 end
66 end
67
67
68 def test_progress_should_consider_done_ratio_of_open_assigned_issues
68 def test_progress_should_consider_done_ratio_of_open_assigned_issues
69 project = Project.find(1)
69 project = Project.find(1)
70 v = Version.create!(:project => project, :name => 'Progress')
70 v = Version.create!(:project => project, :name => 'Progress')
71 add_issue(v)
71 add_issue(v)
72 add_issue(v, :done_ratio => 20)
72 add_issue(v, :done_ratio => 20)
73 add_issue(v, :done_ratio => 70)
73 add_issue(v, :done_ratio => 70)
74 assert_progress_equal (0.0 + 20.0 + 70.0)/3, v.completed_pourcent
74 assert_progress_equal (0.0 + 20.0 + 70.0)/3, v.completed_pourcent
75 assert_progress_equal 0, v.closed_pourcent
75 assert_progress_equal 0, v.closed_pourcent
76 end
76 end
77
77
78 def test_progress_should_consider_closed_issues_as_completed
78 def test_progress_should_consider_closed_issues_as_completed
79 project = Project.find(1)
79 project = Project.find(1)
80 v = Version.create!(:project => project, :name => 'Progress')
80 v = Version.create!(:project => project, :name => 'Progress')
81 add_issue(v)
81 add_issue(v)
82 add_issue(v, :done_ratio => 20)
82 add_issue(v, :done_ratio => 20)
83 add_issue(v, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
83 add_issue(v, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
84 assert_progress_equal (0.0 + 20.0 + 100.0)/3, v.completed_pourcent
84 assert_progress_equal (0.0 + 20.0 + 100.0)/3, v.completed_pourcent
85 assert_progress_equal (100.0)/3, v.closed_pourcent
85 assert_progress_equal (100.0)/3, v.closed_pourcent
86 end
86 end
87
87
88 def test_progress_should_consider_estimated_hours_to_weigth_issues
88 def test_progress_should_consider_estimated_hours_to_weigth_issues
89 project = Project.find(1)
89 project = Project.find(1)
90 v = Version.create!(:project => project, :name => 'Progress')
90 v = Version.create!(:project => project, :name => 'Progress')
91 add_issue(v, :estimated_hours => 10)
91 add_issue(v, :estimated_hours => 10)
92 add_issue(v, :estimated_hours => 20, :done_ratio => 30)
92 add_issue(v, :estimated_hours => 20, :done_ratio => 30)
93 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
93 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
94 add_issue(v, :estimated_hours => 25, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
94 add_issue(v, :estimated_hours => 25, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
95 assert_progress_equal (10.0*0 + 20.0*0.3 + 40*0.1 + 25.0*1)/95.0*100, v.completed_pourcent
95 assert_progress_equal (10.0*0 + 20.0*0.3 + 40*0.1 + 25.0*1)/95.0*100, v.completed_pourcent
96 assert_progress_equal 25.0/95.0*100, v.closed_pourcent
96 assert_progress_equal 25.0/95.0*100, v.closed_pourcent
97 end
97 end
98
98
99 def test_progress_should_consider_average_estimated_hours_to_weigth_unestimated_issues
99 def test_progress_should_consider_average_estimated_hours_to_weigth_unestimated_issues
100 project = Project.find(1)
100 project = Project.find(1)
101 v = Version.create!(:project => project, :name => 'Progress')
101 v = Version.create!(:project => project, :name => 'Progress')
102 add_issue(v, :done_ratio => 20)
102 add_issue(v, :done_ratio => 20)
103 add_issue(v, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
103 add_issue(v, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
104 add_issue(v, :estimated_hours => 10, :done_ratio => 30)
104 add_issue(v, :estimated_hours => 10, :done_ratio => 30)
105 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
105 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
106 assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent
106 assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent
107 assert_progress_equal 25.0/100.0*100, v.closed_pourcent
107 assert_progress_equal 25.0/100.0*100, v.closed_pourcent
108 end
108 end
109
109
110 def test_should_sort_scheduled_then_unscheduled_versions
111 Version.delete_all
112 v4 = Version.create!(:project_id => 1, :name => 'v4')
113 v3 = Version.create!(:project_id => 1, :name => 'v2', :effective_date => '2012-07-14')
114 v2 = Version.create!(:project_id => 1, :name => 'v1')
115 v1 = Version.create!(:project_id => 1, :name => 'v3', :effective_date => '2012-08-02')
116 v5 = Version.create!(:project_id => 1, :name => 'v5', :effective_date => '2012-07-02')
117
118 assert_equal [v5, v3, v1, v2, v4], Version.sorted.all
119 end
120
110 context "#behind_schedule?" do
121 context "#behind_schedule?" do
111 setup do
122 setup do
112 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
123 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
113 @project = Project.create!(:name => 'test0', :identifier => 'test0')
124 @project = Project.create!(:name => 'test0', :identifier => 'test0')
114 @project.trackers << Tracker.create!(:name => 'track')
125 @project.trackers << Tracker.create!(:name => 'track')
115
126
116 @version = Version.create!(:project => @project, :effective_date => nil, :name => 'version')
127 @version = Version.create!(:project => @project, :effective_date => nil, :name => 'version')
117 end
128 end
118
129
119 should "be false if there are no issues assigned" do
130 should "be false if there are no issues assigned" do
120 @version.update_attribute(:effective_date, Date.yesterday)
131 @version.update_attribute(:effective_date, Date.yesterday)
121 assert_equal false, @version.behind_schedule?
132 assert_equal false, @version.behind_schedule?
122 end
133 end
123
134
124 should "be false if there is no effective_date" do
135 should "be false if there is no effective_date" do
125 assert_equal false, @version.behind_schedule?
136 assert_equal false, @version.behind_schedule?
126 end
137 end
127
138
128 should "be false if all of the issues are ahead of schedule" do
139 should "be false if all of the issues are ahead of schedule" do
129 @version.update_attribute(:effective_date, 7.days.from_now.to_date)
140 @version.update_attribute(:effective_date, 7.days.from_now.to_date)
130 add_issue(@version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
141 add_issue(@version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
131 add_issue(@version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
142 add_issue(@version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
132 assert_equal 60, @version.completed_pourcent
143 assert_equal 60, @version.completed_pourcent
133 assert_equal false, @version.behind_schedule?
144 assert_equal false, @version.behind_schedule?
134 end
145 end
135
146
136 should "be true if any of the issues are behind schedule" do
147 should "be true if any of the issues are behind schedule" do
137 @version.update_attribute(:effective_date, 7.days.from_now.to_date)
148 @version.update_attribute(:effective_date, 7.days.from_now.to_date)
138 add_issue(@version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
149 add_issue(@version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
139 add_issue(@version, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left
150 add_issue(@version, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left
140 assert_equal 40, @version.completed_pourcent
151 assert_equal 40, @version.completed_pourcent
141 assert_equal true, @version.behind_schedule?
152 assert_equal true, @version.behind_schedule?
142 end
153 end
143
154
144 should "be false if all of the issues are complete" do
155 should "be false if all of the issues are complete" do
145 @version.update_attribute(:effective_date, 7.days.from_now.to_date)
156 @version.update_attribute(:effective_date, 7.days.from_now.to_date)
146 add_issue(@version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
157 add_issue(@version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
147 add_issue(@version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
158 add_issue(@version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
148 assert_equal 100, @version.completed_pourcent
159 assert_equal 100, @version.completed_pourcent
149 assert_equal false, @version.behind_schedule?
160 assert_equal false, @version.behind_schedule?
150 end
161 end
151 end
162 end
152
163
153 context "#estimated_hours" do
164 context "#estimated_hours" do
154 setup do
165 setup do
155 @version = Version.create!(:project_id => 1, :name => '#estimated_hours')
166 @version = Version.create!(:project_id => 1, :name => '#estimated_hours')
156 end
167 end
157
168
158 should "return 0 with no assigned issues" do
169 should "return 0 with no assigned issues" do
159 assert_equal 0, @version.estimated_hours
170 assert_equal 0, @version.estimated_hours
160 end
171 end
161
172
162 should "return 0 with no estimated hours" do
173 should "return 0 with no estimated hours" do
163 add_issue(@version)
174 add_issue(@version)
164 assert_equal 0, @version.estimated_hours
175 assert_equal 0, @version.estimated_hours
165 end
176 end
166
177
167 should "return the sum of estimated hours" do
178 should "return the sum of estimated hours" do
168 add_issue(@version, :estimated_hours => 2.5)
179 add_issue(@version, :estimated_hours => 2.5)
169 add_issue(@version, :estimated_hours => 5)
180 add_issue(@version, :estimated_hours => 5)
170 assert_equal 7.5, @version.estimated_hours
181 assert_equal 7.5, @version.estimated_hours
171 end
182 end
172
183
173 should "return the sum of leaves estimated hours" do
184 should "return the sum of leaves estimated hours" do
174 parent = add_issue(@version)
185 parent = add_issue(@version)
175 add_issue(@version, :estimated_hours => 2.5, :parent_issue_id => parent.id)
186 add_issue(@version, :estimated_hours => 2.5, :parent_issue_id => parent.id)
176 add_issue(@version, :estimated_hours => 5, :parent_issue_id => parent.id)
187 add_issue(@version, :estimated_hours => 5, :parent_issue_id => parent.id)
177 assert_equal 7.5, @version.estimated_hours
188 assert_equal 7.5, @version.estimated_hours
178 end
189 end
179 end
190 end
180
191
181 test "should update all issue's fixed_version associations in case the hierarchy changed XXX" do
192 test "should update all issue's fixed_version associations in case the hierarchy changed XXX" do
182 User.current = User.find(1) # Need the admin's permissions
193 User.current = User.find(1) # Need the admin's permissions
183
194
184 @version = Version.find(7)
195 @version = Version.find(7)
185 # Separate hierarchy
196 # Separate hierarchy
186 project_1_issue = Issue.find(1)
197 project_1_issue = Issue.find(1)
187 project_1_issue.fixed_version = @version
198 project_1_issue.fixed_version = @version
188 assert project_1_issue.save, project_1_issue.errors.full_messages.to_s
199 assert project_1_issue.save, project_1_issue.errors.full_messages.to_s
189
200
190 project_5_issue = Issue.find(6)
201 project_5_issue = Issue.find(6)
191 project_5_issue.fixed_version = @version
202 project_5_issue.fixed_version = @version
192 assert project_5_issue.save
203 assert project_5_issue.save
193
204
194 # Project
205 # Project
195 project_2_issue = Issue.find(4)
206 project_2_issue = Issue.find(4)
196 project_2_issue.fixed_version = @version
207 project_2_issue.fixed_version = @version
197 assert project_2_issue.save
208 assert project_2_issue.save
198
209
199 # Update the sharing
210 # Update the sharing
200 @version.sharing = 'none'
211 @version.sharing = 'none'
201 assert @version.save
212 assert @version.save
202
213
203 # Project 1 now out of the shared scope
214 # Project 1 now out of the shared scope
204 project_1_issue.reload
215 project_1_issue.reload
205 assert_equal nil, project_1_issue.fixed_version, "Fixed version is still set after changing the Version's sharing"
216 assert_equal nil, project_1_issue.fixed_version, "Fixed version is still set after changing the Version's sharing"
206
217
207 # Project 5 now out of the shared scope
218 # Project 5 now out of the shared scope
208 project_5_issue.reload
219 project_5_issue.reload
209 assert_equal nil, project_5_issue.fixed_version, "Fixed version is still set after changing the Version's sharing"
220 assert_equal nil, project_5_issue.fixed_version, "Fixed version is still set after changing the Version's sharing"
210
221
211 # Project 2 issue remains
222 # Project 2 issue remains
212 project_2_issue.reload
223 project_2_issue.reload
213 assert_equal @version, project_2_issue.fixed_version
224 assert_equal @version, project_2_issue.fixed_version
214 end
225 end
215
226
216 private
227 private
217
228
218 def add_issue(version, attributes={})
229 def add_issue(version, attributes={})
219 Issue.create!({:project => version.project,
230 Issue.create!({:project => version.project,
220 :fixed_version => version,
231 :fixed_version => version,
221 :subject => 'Test',
232 :subject => 'Test',
222 :author => User.find(:first),
233 :author => User.find(:first),
223 :tracker => version.project.trackers.find(:first)}.merge(attributes))
234 :tracker => version.project.trackers.find(:first)}.merge(attributes))
224 end
235 end
225
236
226 def assert_progress_equal(expected_float, actual_float, message="")
237 def assert_progress_equal(expected_float, actual_float, message="")
227 assert_in_delta(expected_float, actual_float, 0.000001, message="")
238 assert_in_delta(expected_float, actual_float, 0.000001, message="")
228 end
239 end
229 end
240 end
General Comments 0
You need to be logged in to leave comments. Login now