##// END OF EJS Templates
remove trailing white space from app/models/version.rb...
Toshi MARUYAMA -
r15778:cfda8cdb91bc
parent child
Show More
@@ -1,362 +1,362
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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
21 21 after_update :update_issues_from_sharing_change
22 22 after_save :update_default_project_version
23 23 before_destroy :nullify_projects_default_version
24 24
25 25 belongs_to :project
26 26 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
27 27 acts_as_customizable
28 28 acts_as_attachable :view_permission => :view_files,
29 29 :edit_permission => :manage_files,
30 30 :delete_permission => :manage_files
31 31
32 32 VERSION_STATUSES = %w(open locked closed)
33 33 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
34 34
35 35 validates_presence_of :name
36 36 validates_uniqueness_of :name, :scope => [:project_id]
37 37 validates_length_of :name, :maximum => 60
38 38 validates_length_of :description, :wiki_page_title, :maximum => 255
39 39 validates :effective_date, :date => true
40 40 validates_inclusion_of :status, :in => VERSION_STATUSES
41 41 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
42 42 attr_protected :id
43 43
44 44 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
45 45 scope :like, lambda {|arg|
46 46 if arg.present?
47 47 pattern = "%#{arg.to_s.strip}%"
48 48 where("LOWER(#{Version.table_name}.name) LIKE :p", :p => pattern)
49 49 end
50 50 }
51 51 scope :open, lambda { where(:status => 'open') }
52 52 scope :status, lambda {|status|
53 53 if status.present?
54 54 where(:status => status.to_s)
55 55 end
56 56 }
57 57 scope :visible, lambda {|*args|
58 58 joins(:project).
59 59 where(Project.allowed_to_condition(args.first || User.current, :view_issues))
60 60 }
61 61
62 62 safe_attributes 'name',
63 63 'description',
64 64 'effective_date',
65 65 'due_date',
66 66 'wiki_page_title',
67 67 'status',
68 68 'sharing',
69 69 'default_project_version',
70 70 'custom_field_values',
71 71 'custom_fields'
72 72
73 73 # Returns true if +user+ or current user is allowed to view the version
74 74 def visible?(user=User.current)
75 75 user.allowed_to?(:view_issues, self.project)
76 76 end
77 77
78 78 # Version files have same visibility as project files
79 79 def attachments_visible?(*args)
80 80 project.present? && project.attachments_visible?(*args)
81 81 end
82 82
83 83 def attachments_deletable?(usr=User.current)
84 84 project.present? && project.attachments_deletable?(usr)
85 85 end
86 86
87 87 alias :base_reload :reload
88 88 def reload(*args)
89 89 @default_project_version = nil
90 90 base_reload(*args)
91 91 end
92 92
93 93 def start_date
94 94 @start_date ||= fixed_issues.minimum('start_date')
95 95 end
96 96
97 97 def due_date
98 98 effective_date
99 99 end
100 100
101 101 def due_date=(arg)
102 102 self.effective_date=(arg)
103 103 end
104 104
105 105 # Returns the total estimated time for this version
106 106 # (sum of leaves estimated_hours)
107 107 def estimated_hours
108 108 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
109 109 end
110 110
111 111 # Returns the total reported time for this version
112 112 def spent_hours
113 113 @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
114 114 end
115 115
116 116 def closed?
117 117 status == 'closed'
118 118 end
119 119
120 120 def open?
121 121 status == 'open'
122 122 end
123 123
124 124 # Returns true if the version is completed: closed or due date reached and no open issues
125 125 def completed?
126 126 closed? || (effective_date && (effective_date < User.current.today) && (open_issues_count == 0))
127 127 end
128 128
129 129 def behind_schedule?
130 130 if completed_percent == 100
131 131 return false
132 132 elsif due_date && start_date
133 133 done_date = start_date + ((due_date - start_date+1)* completed_percent/100).floor
134 134 return done_date <= User.current.today
135 135 else
136 136 false # No issues so it's not late
137 137 end
138 138 end
139 139
140 140 # Returns the completion percentage of this version based on the amount of open/closed issues
141 141 # and the time spent on the open issues.
142 142 def completed_percent
143 143 if issues_count == 0
144 144 0
145 145 elsif open_issues_count == 0
146 146 100
147 147 else
148 148 issues_progress(false) + issues_progress(true)
149 149 end
150 150 end
151 151
152 152 # Returns the percentage of issues that have been marked as 'closed'.
153 153 def closed_percent
154 154 if issues_count == 0
155 155 0
156 156 else
157 157 issues_progress(false)
158 158 end
159 159 end
160 160
161 161 # Returns true if the version is overdue: due date reached and some open issues
162 162 def overdue?
163 163 effective_date && (effective_date < User.current.today) && (open_issues_count > 0)
164 164 end
165 165
166 166 # Returns assigned issues count
167 167 def issues_count
168 168 load_issue_counts
169 169 @issue_count
170 170 end
171 171
172 172 # Returns the total amount of open issues for this version.
173 173 def open_issues_count
174 174 load_issue_counts
175 175 @open_issues_count
176 176 end
177 177
178 178 # Returns the total amount of closed issues for this version.
179 179 def closed_issues_count
180 180 load_issue_counts
181 181 @closed_issues_count
182 182 end
183 183
184 184 def wiki_page
185 185 if project.wiki && !wiki_page_title.blank?
186 186 @wiki_page ||= project.wiki.find_page(wiki_page_title)
187 187 end
188 188 @wiki_page
189 189 end
190 190
191 191 def to_s; name end
192 192
193 193 def to_s_with_project
194 194 "#{project} - #{name}"
195 195 end
196 196
197 197 # Versions are sorted by effective_date and name
198 198 # Those with no effective_date are at the end, sorted by name
199 199 def <=>(version)
200 200 if self.effective_date
201 201 if version.effective_date
202 202 if self.effective_date == version.effective_date
203 203 name == version.name ? id <=> version.id : name <=> version.name
204 204 else
205 205 self.effective_date <=> version.effective_date
206 206 end
207 207 else
208 208 -1
209 209 end
210 210 else
211 211 if version.effective_date
212 212 1
213 213 else
214 214 name == version.name ? id <=> version.id : name <=> version.name
215 215 end
216 216 end
217 217 end
218 218
219 219 # Sort versions by status (open, locked then closed versions)
220 220 def self.sort_by_status(versions)
221 221 versions.sort do |a, b|
222 222 if a.status == b.status
223 223 a <=> b
224 224 else
225 225 b.status <=> a.status
226 226 end
227 227 end
228 228 end
229 229
230 230 def css_classes
231 231 [
232 232 completed? ? 'version-completed' : 'version-incompleted',
233 233 "version-#{status}"
234 234 ].join(' ')
235 235 end
236 236
237 237 def self.fields_for_order_statement(table=nil)
238 238 table ||= table_name
239 239 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
240 240 end
241 241
242 242 scope :sorted, lambda { order(fields_for_order_statement) }
243 243
244 244 # Returns the sharings that +user+ can set the version to
245 245 def allowed_sharings(user = User.current)
246 246 VERSION_SHARINGS.select do |s|
247 247 if sharing == s
248 248 true
249 249 else
250 250 case s
251 251 when 'system'
252 252 # Only admin users can set a systemwide sharing
253 253 user.admin?
254 254 when 'hierarchy', 'tree'
255 255 # Only users allowed to manage versions of the root project can
256 256 # set sharing to hierarchy or tree
257 257 project.nil? || user.allowed_to?(:manage_versions, project.root)
258 258 else
259 259 true
260 260 end
261 261 end
262 262 end
263 263 end
264 264
265 265 # Returns true if the version is shared, otherwise false
266 266 def shared?
267 267 sharing != 'none'
268 268 end
269 269
270 270 def deletable?
271 271 fixed_issues.empty? && !referenced_by_a_custom_field?
272 272 end
273 273
274 274 def default_project_version
275 275 if @default_project_version.nil?
276 276 project.present? && project.default_version == self
277 277 else
278 278 @default_project_version
279 279 end
280 280 end
281 281
282 282 def default_project_version=(arg)
283 @default_project_version = (arg == '1' || arg == true)
283 @default_project_version = (arg == '1' || arg == true)
284 284 end
285 285
286 286 private
287 287
288 288 def load_issue_counts
289 289 unless @issue_count
290 290 @open_issues_count = 0
291 291 @closed_issues_count = 0
292 292 fixed_issues.group(:status).count.each do |status, count|
293 293 if status.is_closed?
294 294 @closed_issues_count += count
295 295 else
296 296 @open_issues_count += count
297 297 end
298 298 end
299 299 @issue_count = @open_issues_count + @closed_issues_count
300 300 end
301 301 end
302 302
303 303 # Update the issue's fixed versions. Used if a version's sharing changes.
304 304 def update_issues_from_sharing_change
305 305 if sharing_changed?
306 306 if VERSION_SHARINGS.index(sharing_was).nil? ||
307 307 VERSION_SHARINGS.index(sharing).nil? ||
308 308 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
309 309 Issue.update_versions_from_sharing_change self
310 310 end
311 311 end
312 312 end
313 313
314 314 def update_default_project_version
315 315 if @default_project_version && project.present?
316 316 project.update_columns :default_version_id => id
317 317 end
318 318 end
319 319
320 320 # Returns the average estimated time of assigned issues
321 321 # or 1 if no issue has an estimated time
322 322 # Used to weight unestimated issues in progress calculation
323 323 def estimated_average
324 324 if @estimated_average.nil?
325 325 average = fixed_issues.average(:estimated_hours).to_f
326 326 if average == 0
327 327 average = 1
328 328 end
329 329 @estimated_average = average
330 330 end
331 331 @estimated_average
332 332 end
333 333
334 334 # Returns the total progress of open or closed issues. The returned percentage takes into account
335 335 # the amount of estimated time set for this version.
336 336 #
337 337 # Examples:
338 338 # issues_progress(true) => returns the progress percentage for open issues.
339 339 # issues_progress(false) => returns the progress percentage for closed issues.
340 340 def issues_progress(open)
341 341 @issues_progress ||= {}
342 342 @issues_progress[open] ||= begin
343 343 progress = 0
344 344 if issues_count > 0
345 345 ratio = open ? 'done_ratio' : 100
346 346
347 347 done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
348 348 progress = done / (estimated_average * issues_count)
349 349 end
350 350 progress
351 351 end
352 352 end
353 353
354 354 def referenced_by_a_custom_field?
355 355 CustomValue.joins(:custom_field).
356 356 where(:value => id.to_s, :custom_fields => {:field_format => 'version'}).any?
357 357 end
358 358
359 359 def nullify_projects_default_version
360 360 Project.where(:default_version_id => id).update_all(:default_version_id => nil)
361 361 end
362 362 end
General Comments 0
You need to be logged in to leave comments. Login now