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