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