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