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