To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / .svn / pristine / 0c / 0c683f18d0752b5b1c900f3d3bf34e0652a0c13f.svn-base @ 1298:4f746d8966dd

History | View | Annotate | Download (8.46 KB)

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