2025-07-18 17:35:57 +01:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
2011-05-29 13:11:52 -07:00
|
|
|
#-- copyright
|
2020-01-15 11:31:26 +01:00
|
|
|
# OpenProject is an open source project management software.
|
2024-07-30 13:42:36 +02:00
|
|
|
# Copyright (C) the OpenProject GmbH
|
2011-05-30 20:52:25 +02:00
|
|
|
#
|
2011-05-29 13:11:52 -07:00
|
|
|
# This program is free software; you can redistribute it and/or
|
2013-06-05 16:27:56 +02:00
|
|
|
# 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.
|
|
|
|
|
#
|
2021-09-02 21:49:06 +02:00
|
|
|
# See COPYRIGHT and LICENSE files for more details.
|
2011-05-29 13:11:52 -07:00
|
|
|
#++
|
|
|
|
|
|
2020-04-09 11:54:26 +02:00
|
|
|
class Version < ApplicationRecord
|
2020-03-24 10:01:52 +01:00
|
|
|
include ::Versions::ProjectSharing
|
2020-03-24 14:15:13 +01:00
|
|
|
include ::Scopes::Scoped
|
2014-12-16 15:59:43 +01:00
|
|
|
|
2007-03-12 17:59:02 +00: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
|
2007-03-12 17:59:02 +00:00
|
|
|
|
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,
|
2020-03-11 11:13:27 +01:00
|
|
|
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 }
|
2013-05-02 15:15:02 +02:00
|
|
|
validate :validate_start_date_before_effective_date
|
2009-11-08 13:03:41 +00:00
|
|
|
|
2025-06-27 19:45:00 +02:00
|
|
|
scopes :rolled_up,
|
2021-10-29 10:59:48 +02:00
|
|
|
:shared_with
|
2020-03-24 22:54:53 +01:00
|
|
|
|
2025-07-02 16:20:32 +03: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) {
|
2025-07-02 16:20:32 +03:00
|
|
|
user = args.first || User.current
|
2016-08-24 14:10:11 +02:00
|
|
|
joins(:project)
|
2025-07-02 16:20:32 +03:00
|
|
|
.merge(Project.allowed_to(user, :view_work_packages))
|
2026-01-19 10:34:31 +01:00
|
|
|
.or(Project.allowed_to(user, :manage_versions))
|
2025-03-28 18:59:11 +02:00
|
|
|
.or(Version.systemwide)
|
2025-07-02 16:20:32 +03:00
|
|
|
.or(Version.shared_via_work_packages(user))
|
2014-11-03 21:35:22 +01:00
|
|
|
}
|
2009-12-06 10:28:20 +00:00
|
|
|
|
2014-09-17 17:59:28 +02:00
|
|
|
scope :systemwide, -> { where(sharing: "system") }
|
|
|
|
|
|
2025-07-02 16:20:32 +03:00
|
|
|
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
|
|
|
|
|
|
2009-12-06 10:28:20 +00:00
|
|
|
# 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)
|
2025-07-03 11:18:36 +03:00
|
|
|
systemwide? ||
|
|
|
|
|
user.allowed_in_project?(:view_work_packages, project) ||
|
2026-01-19 10:34:31 +01:00
|
|
|
user.allowed_in_project?(:manage_versions, project) ||
|
2025-07-02 16:20:32 +03:00
|
|
|
work_packages.visible(user).exists?
|
2009-12-06 10:28:20 +00:00
|
|
|
end
|
2011-03-13 19:08:41 -07:00
|
|
|
|
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
|
2010-04-11 13:20:02 +00:00
|
|
|
# (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
|
2023-07-06 20:14:55 +02:00
|
|
|
.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")
|
2023-07-06 20:14:55 +02:00
|
|
|
.sum(:hours)
|
|
|
|
|
.to_f
|
2008-03-08 11:17:03 +00:00
|
|
|
end
|
2011-05-30 20:52:25 +02:00
|
|
|
|
2009-11-08 13:03:41 +00:00
|
|
|
def closed?
|
|
|
|
|
status == "closed"
|
|
|
|
|
end
|
2009-12-06 10:28:20 +00:00
|
|
|
|
|
|
|
|
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
|
2007-05-07 16:54:26 +00:00
|
|
|
def completed?
|
2017-06-09 14:55:35 +02:00
|
|
|
effective_date && (effective_date <= Date.today) && open_issues_count.zero?
|
2007-08-12 09:58:38 +00:00
|
|
|
end
|
2010-09-10 03:09:02 +00:00
|
|
|
|
2024-03-11 18:00:19 +01:00
|
|
|
def systemwide?
|
|
|
|
|
sharing == "system"
|
|
|
|
|
end
|
|
|
|
|
|
2009-03-21 00:39:53 +00:00
|
|
|
# Returns the completion percentage of this version based on the amount of open/closed issues
|
|
|
|
|
# and the time spent on the open issues.
|
2014-04-05 00:01:18 +02:00
|
|
|
def completed_percent
|
2017-06-09 14:55:35 +02:00
|
|
|
if issues_count.zero?
|
2007-11-17 15:34:10 +00:00
|
|
|
0
|
2017-06-09 14:55:35 +02:00
|
|
|
elsif open_issues_count.zero?
|
2007-11-17 15:34:10 +00:00
|
|
|
100
|
|
|
|
|
else
|
2009-02-01 18:54:05 +00:00
|
|
|
issues_progress(false) + issues_progress(true)
|
2007-11-17 15:34:10 +00:00
|
|
|
end
|
|
|
|
|
end
|
2011-05-30 20:52:25 +02:00
|
|
|
|
2009-03-21 00:39:53 +00:00
|
|
|
# Returns the percentage of issues that have been marked as 'closed'.
|
2014-04-05 00:01:18 +02:00
|
|
|
def closed_percent
|
2017-06-09 14:55:35 +02:00
|
|
|
if issues_count.zero?
|
2007-12-05 19:21:25 +00:00
|
|
|
0
|
|
|
|
|
else
|
2009-02-01 18:54:05 +00:00
|
|
|
issues_progress(false)
|
2007-12-05 19:21:25 +00:00
|
|
|
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
|
2007-08-12 09:58:38 +00:00
|
|
|
def overdue?
|
|
|
|
|
effective_date && (effective_date < Date.today) && (open_issues_count > 0)
|
|
|
|
|
end
|
2011-05-30 20:52:25 +02:00
|
|
|
|
2009-02-01 18:54:05 +00:00
|
|
|
# Returns assigned issues count
|
|
|
|
|
def issues_count
|
2020-03-26 15:37:24 +01:00
|
|
|
@issue_count ||= work_packages.count
|
2009-02-01 18:54:05 +00:00
|
|
|
end
|
2011-05-30 20:52:25 +02:00
|
|
|
|
2009-03-21 00:39:53 +00:00
|
|
|
# Returns the total amount of open issues for this version.
|
2007-08-12 09:58:38 +00:00
|
|
|
def open_issues_count
|
2017-09-06 16:05:54 +02:00
|
|
|
@open_issues_count ||= work_packages.merge(WorkPackage.with_status_open).size
|
2007-08-12 09:58:38 +00:00
|
|
|
end
|
|
|
|
|
|
2009-03-21 00:39:53 +00:00
|
|
|
# Returns the total amount of closed issues for this version.
|
2007-08-12 09:58:38 +00:00
|
|
|
def closed_issues_count
|
2017-09-06 16:05:54 +02:00
|
|
|
@closed_issues_count ||= work_packages.merge(WorkPackage.with_status_closed).size
|
2007-05-07 16:54:26 +00:00
|
|
|
end
|
2011-05-30 20:52:25 +02:00
|
|
|
|
2007-06-14 18:26:27 +00: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
|
|
|
|
2023-07-06 20:14:55 +02:00
|
|
|
def to_s
|
|
|
|
|
name
|
|
|
|
|
end
|
2010-09-10 03:09:11 +00:00
|
|
|
|
|
|
|
|
def to_s_with_project
|
|
|
|
|
"#{project} - #{name}"
|
|
|
|
|
end
|
2011-05-30 20:52:25 +02:00
|
|
|
|
2014-10-13 16:00:18 +03: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
|
|
|
|
2010-04-11 17:47:29 +00:00
|
|
|
private
|
2009-12-06 10:28:20 +00:00
|
|
|
|
2013-05-02 15:15:02 +02:00
|
|
|
def validate_start_date_before_effective_date
|
2014-11-03 21:35:22 +01:00
|
|
|
if effective_date && start_date && effective_date < start_date
|
2013-05-02 15:15:02 +02:00
|
|
|
errors.add :effective_date, :greater_than_start_date
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2009-02-01 18:54:05 +00:00
|
|
|
# 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
|
2009-02-01 18:54:05 +00:00
|
|
|
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?
|
2009-02-01 18:54:05 +00:00
|
|
|
average = 1
|
|
|
|
|
end
|
|
|
|
|
@estimated_average = average
|
|
|
|
|
end
|
|
|
|
|
@estimated_average
|
|
|
|
|
end
|
2011-05-30 20:52:25 +02:00
|
|
|
|
2009-03-21 00:39:53 +00: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.
|
2026-05-27 09:17:57 +02:00
|
|
|
def issues_progress(open) # rubocop:disable Metrics/AbcSize
|
2009-02-01 18:54:05 +00:00
|
|
|
@issues_progress ||= {}
|
|
|
|
|
@issues_progress[open] ||= begin
|
|
|
|
|
progress = 0
|
2019-04-30 09:57:37 +02:00
|
|
|
|
2009-02-01 18:54:05 +00:00
|
|
|
if issues_count > 0
|
|
|
|
|
ratio = open ? "done_ratio" : 100
|
2026-05-27 09:17:57 +02:00
|
|
|
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
|
2023-07-06 20:14:55 +02:00
|
|
|
.where(statuses: { is_closed: !open })
|
|
|
|
|
.includes(:status)
|
|
|
|
|
.sum(sum_sql)
|
2012-09-04 15:13:42 +02:00
|
|
|
progress = done.to_f / (estimated_average * issues_count)
|
2009-02-01 18:54:05 +00:00
|
|
|
end
|
|
|
|
|
progress
|
|
|
|
|
end
|
|
|
|
|
end
|
2006-06-28 18:11:03 +00:00
|
|
|
end
|