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