File: app/models/version.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  class: Version#18
includes
  SafeAttributes ( Redmine )
inherits from
  Base ( ActiveRecord )
has properties
constant: VERSION_STATUSES #27
constant: VERSION_SHARINGS #28
method: visible? / 1 #52
method: attachments_visible? / 1 #57
method: start_date #61
method: due_date #65
method: due_date= / 1 #69
method: estimated_hours #75
method: spent_hours #80
method: closed? #84
method: open? #88
method: completed? #93
method: behind_schedule? #97
method: completed_pourcent #110
method: closed_pourcent #121
method: overdue? #130
method: issues_count #135
method: open_issues_count #141
method: closed_issues_count #147
method: wiki_page #152
method: to_s #159
method: to_s_with_project #161
method: <=> / 1 #167
method: allowed_sharings / 1 #188
method: load_issue_counts #210
method: update_issues_from_sharing_change #226
method: estimated_average #239
method: issues_progress / 1 #256

Class Hierarchy

Object ( Builtin-Module )
Base ( ActiveRecord )
  Version    #18

Code

   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