Files

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

246 lines
7.4 KiB
Ruby
Raw Permalink Normal View History

2025-07-18 17:35:57 +01:00
# frozen_string_literal: true
#-- copyright
2020-01-15 11:31:26 +01:00
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
2011-05-30 20:52:25 +02:00
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
2011-05-30 20:52:25 +02:00
#
2013-09-16 17:59:31 +02:00
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
2021-01-13 17:47:45 +01:00
# Copyright (C) 2006-2013 Jean-Philippe Lang
2013-09-16 17:59:31 +02:00
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Version < ApplicationRecord
2020-03-24 10:01:52 +01:00
include ::Versions::ProjectSharing
include ::Scopes::Scoped
2014-12-16 15:59:43 +01:00
belongs_to :project
2020-03-26 15:37:24 +01:00
has_many :work_packages, dependent: :nullify
2009-11-15 15:22:55 +00:00
acts_as_customizable
2017-06-09 14:55:35 +02:00
VERSION_STATUSES = %w(open locked closed).freeze
VERSION_SHARINGS = %w(none descendants hierarchy tree system).freeze
2011-05-30 20:52:25 +02:00
2019-09-20 08:34:51 +02:00
validates :name,
presence: true,
uniqueness: { scope: [:project_id], case_sensitive: false }
2019-09-20 08:34:51 +02:00
2014-11-03 21:10:39 +01:00
validates :effective_date, format: { with: /\A\d{4}-\d{2}-\d{2}\z/, message: :not_a_date, allow_nil: true }
validates :start_date, format: { with: /\A\d{4}-\d{2}-\d{2}\z/, message: :not_a_date, allow_nil: true }
validates :status, inclusion: { in: VERSION_STATUSES }
validate :validate_start_date_before_effective_date
scopes :rolled_up,
2021-10-29 10:59:48 +02:00
:shared_with
2020-03-24 22:54:53 +01:00
# Returns versions that are either:
# - from projects the user can see (via :view_work_packages)
# - systemwide versions
# - or referenced by a work package visible to the user (e.g., via sharing)
2014-11-08 19:35:16 +01:00
scope :visible, ->(*args) {
user = args.first || User.current
2016-08-24 14:10:11 +02:00
joins(:project)
.merge(Project.allowed_to(user, :view_work_packages))
2026-01-19 10:34:31 +01:00
.or(Project.allowed_to(user, :manage_versions))
.or(Version.systemwide)
.or(Version.shared_via_work_packages(user))
2014-11-03 21:35:22 +01:00
}
scope :systemwide, -> { where(sharing: "system") }
scope :shared_via_work_packages, ->(*args) {
user = args.first || User.current
where(id: WorkPackage.visible(user).where.not(version_id: nil).distinct.select(:version_id))
}
2017-09-06 16:05:54 +02:00
def self.with_status_open
where(status: "open")
end
# Returns true if +user+ or current user is allowed to view the version
2014-11-03 21:35:22 +01:00
def visible?(user = User.current)
systemwide? ||
user.allowed_in_project?(:view_work_packages, project) ||
2026-01-19 10:34:31 +01:00
user.allowed_in_project?(:manage_versions, project) ||
work_packages.visible(user).exists?
end
2007-04-07 12:09:01 +00:00
def due_date
effective_date
end
2011-05-30 20:52:25 +02:00
2008-03-08 11:17:03 +00:00
# Returns the total estimated time for this version
# (sum of leaves estimated_hours)
2008-03-08 11:17:03 +00:00
def estimated_hours
2022-03-19 23:26:32 +01:00
@estimated_hours ||= work_packages.leaves.sum(:estimated_hours).to_f
2008-03-08 11:17:03 +00:00
end
2011-05-30 20:52:25 +02:00
2008-03-08 11:17:03 +00:00
# Returns the total reported time for this version
def spent_hours
2017-06-09 14:55:35 +02:00
@spent_hours ||= TimeEntry
.not_ongoing
2025-05-09 12:20:37 +02:00
.joins("INNER JOIN work_packages ON entity_type = 'WorkPackage' AND work_packages.id = entity_id")
.where(work_packages: { version_id: id }, entity_type: "WorkPackage")
.sum(:hours)
.to_f
2008-03-08 11:17:03 +00:00
end
2011-05-30 20:52:25 +02:00
def closed?
status == "closed"
end
def open?
status == "open"
end
2011-05-30 20:52:25 +02:00
2018-09-10 15:12:07 +02:00
# Returns true if the version is completed: finish date reached and no open issues
def completed?
2017-06-09 14:55:35 +02:00
effective_date && (effective_date <= Date.today) && open_issues_count.zero?
end
2010-09-10 03:09:02 +00:00
def systemwide?
sharing == "system"
end
# Returns the completion percentage of this version based on the amount of open/closed issues
# and the time spent on the open issues.
def completed_percent
2017-06-09 14:55:35 +02:00
if issues_count.zero?
0
2017-06-09 14:55:35 +02:00
elsif open_issues_count.zero?
100
else
issues_progress(false) + issues_progress(true)
end
end
2011-05-30 20:52:25 +02:00
# Returns the percentage of issues that have been marked as 'closed'.
def closed_percent
2017-06-09 14:55:35 +02:00
if issues_count.zero?
0
else
issues_progress(false)
end
end
2011-05-30 20:52:25 +02:00
2018-09-10 15:12:07 +02:00
# Returns true if the version is overdue: finish date reached and some open issues
def overdue?
effective_date && (effective_date < Date.today) && (open_issues_count > 0)
end
2011-05-30 20:52:25 +02:00
# Returns assigned issues count
def issues_count
2020-03-26 15:37:24 +01:00
@issue_count ||= work_packages.count
end
2011-05-30 20:52:25 +02:00
# Returns the total amount of open issues for this version.
def open_issues_count
2017-09-06 16:05:54 +02:00
@open_issues_count ||= work_packages.merge(WorkPackage.with_status_open).size
end
# Returns the total amount of closed issues for this version.
def closed_issues_count
2017-09-06 16:05:54 +02:00
@closed_issues_count ||= work_packages.merge(WorkPackage.with_status_closed).size
end
2011-05-30 20:52:25 +02:00
def wiki_page
if project.wiki && wiki_page_title.present?
@wiki_page ||= project.wiki.find_page(wiki_page_title)
end
@wiki_page
end
2011-05-30 20:52:25 +02:00
def to_s
name
end
def to_s_with_project
"#{project} - #{name}"
end
2011-05-30 20:52:25 +02:00
def to_s_for_project(other_project)
if other_project == project
name
else
to_s_with_project
end
end
2015-08-06 08:17:34 +02:00
# Versions are sorted by "Project Name - Version name"
2018-02-06 16:08:59 +01:00
def <=>(other)
2015-08-06 08:17:34 +02:00
# using string interpolation for comparison is not efficient
# (see to_s_with_project's implementation) but I wanted to
# tie the comparison to the presentation as sorting is mostly
# used within sorted tables.
# Thus, when the representation changes, the sorting will change as well.
2018-02-06 16:08:59 +01:00
to_s_with_project.downcase <=> other.to_s_with_project.downcase
2007-05-20 17:46:02 +00:00
end
2011-05-30 20:52:25 +02:00
private
def validate_start_date_before_effective_date
2014-11-03 21:35:22 +01:00
if effective_date && start_date && effective_date < start_date
errors.add :effective_date, :greater_than_start_date
end
end
# Returns the average estimated time of assigned issues
# or 1 if no issue has an estimated time
2014-04-06 23:34:45 +09:00
# Used to weight unestimated issues in progress calculation
def estimated_average
if @estimated_average.nil?
2020-03-26 15:37:24 +01:00
average = work_packages.average(:estimated_hours).to_f
2017-06-09 14:55:35 +02:00
if average.zero?
average = 1
end
@estimated_average = average
end
@estimated_average
end
2011-05-30 20:52:25 +02:00
# Returns the total progress of open or closed issues. The returned percentage takes into account
# the amount of estimated time set for this version.
#
# Examples:
# issues_progress(true) => returns the progress percentage for open issues.
# issues_progress(false) => returns the progress percentage for closed issues.
def issues_progress(open) # rubocop:disable Metrics/AbcSize
@issues_progress ||= {}
@issues_progress[open] ||= begin
progress = 0
2019-04-30 09:57:37 +02:00
if issues_count > 0
ratio = open ? "done_ratio" : 100
sum_sql = OpenProject::SqlSanitization.sanitize(
"COALESCE(#{WorkPackage.table_name}.estimated_hours, ?) * #{ratio}", estimated_average
2019-04-30 09:57:37 +02:00
)
2011-05-30 20:52:25 +02:00
2020-03-26 15:37:24 +01:00
done = work_packages
.where(statuses: { is_closed: !open })
.includes(:status)
.sum(sum_sql)
progress = done.to_f / (estimated_average * issues_count)
end
progress
end
end
2006-06-28 18:11:03 +00:00
end