kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
402 lines
12 KiB
402 lines
12 KiB
#-- encoding: UTF-8
|
|
|
|
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) 2012-2020 the OpenProject GmbH
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License version 3.
|
|
#
|
|
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
|
# 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 docs/COPYRIGHT.rdoc for more details.
|
|
#++
|
|
|
|
module WorkPackages
|
|
class BaseContract < ::ModelContract
|
|
include ::Attachments::ValidateReplacements
|
|
include AssignableValuesContract
|
|
|
|
attribute :subject
|
|
attribute :description
|
|
attribute :status_id,
|
|
writeable: ->(*) {
|
|
# If we did not change into the status,
|
|
# mark unwritable if status and version is closed
|
|
model.status_id_change || !closed_version_and_status?
|
|
}
|
|
attribute :type_id
|
|
attribute :priority_id
|
|
attribute :category_id
|
|
attribute :version_id,
|
|
permission: :assign_versions do
|
|
validate_version_is_assignable
|
|
end
|
|
|
|
validate :validate_no_reopen_on_closed_version
|
|
|
|
attribute :project_id
|
|
|
|
attribute :done_ratio,
|
|
writeable: ->(*) {
|
|
model.leaf? && Setting.work_package_done_ratio == 'field'
|
|
}
|
|
|
|
attribute :estimated_hours
|
|
attribute :derived_estimated_hours, writeable: false
|
|
|
|
attribute :parent_id,
|
|
permission: :manage_subtasks
|
|
|
|
attribute :assigned_to_id do
|
|
next unless model.project
|
|
|
|
validate_people_visible :assigned_to,
|
|
'assigned_to_id',
|
|
model.project.possible_assignee_members
|
|
end
|
|
|
|
attribute :responsible_id do
|
|
next unless model.project
|
|
|
|
validate_people_visible :responsible,
|
|
'responsible_id',
|
|
model.project.possible_responsible_members
|
|
end
|
|
|
|
attribute :schedule_manually
|
|
|
|
attribute :start_date,
|
|
writeable: ->(*) {
|
|
model.leaf? || model.schedule_manually?
|
|
} do
|
|
if !model.schedule_manually? && start_before_soonest_start?
|
|
message = I18n.t('activerecord.errors.models.work_package.attributes.start_date.violates_relationships',
|
|
soonest_start: model.soonest_start)
|
|
|
|
errors.add :start_date, message, error_symbol: :violates_relationships
|
|
end
|
|
end
|
|
|
|
attribute :due_date,
|
|
writeable: ->(*) {
|
|
model.leaf? || model.schedule_manually?
|
|
}
|
|
|
|
validates :due_date,
|
|
date: { after_or_equal_to: :start_date,
|
|
message: :greater_than_or_equal_to_start_date,
|
|
allow_blank: true },
|
|
unless: Proc.new { |wp| wp.start_date.blank? }
|
|
|
|
validate :validate_enabled_type
|
|
validate :validate_type_exists
|
|
|
|
validate :validate_milestone_constraint
|
|
validate :validate_parent_not_milestone
|
|
|
|
validate :validate_parent_exists
|
|
validate :validate_parent_in_same_project
|
|
validate :validate_parent_not_subtask
|
|
|
|
validate :validate_status_exists
|
|
validate :validate_status_transition
|
|
|
|
validate :validate_active_priority
|
|
validate :validate_priority_exists
|
|
|
|
validate :validate_category
|
|
validate :validate_estimated_hours
|
|
|
|
validate :validate_assigned_to_exists
|
|
|
|
def initialize(work_package, user, options: {})
|
|
super
|
|
|
|
@can = WorkPackagePolicy.new(user)
|
|
end
|
|
|
|
def writable_attributes
|
|
ret = super
|
|
|
|
# If we're in a readonly status and did not move into that status right now
|
|
# only allow other status transitions. But also prevent that if the associated version is closed.
|
|
if model.readonly_status? && !model.status_id_change
|
|
ret &= %w(status status_id)
|
|
end
|
|
|
|
ret
|
|
end
|
|
|
|
def assignable_statuses(include_default = false)
|
|
# Do not allow skipping statuses without intermediately saving the work package.
|
|
# We therefore take the original status of the work_package, while preserving all
|
|
# other changes to it (e.g. type, assignee, etc.)
|
|
status = if model.persisted? && model.status_id_changed?
|
|
Status.find_by(id: model.status_id_was)
|
|
else
|
|
model.status
|
|
end
|
|
|
|
statuses = new_statuses_allowed_from(status)
|
|
|
|
statuses = statuses.or(Status.where_default) if include_default
|
|
|
|
statuses.order_by_position
|
|
end
|
|
|
|
def assignable_types
|
|
scope = if model.project.nil?
|
|
Type
|
|
else
|
|
model.project.types.includes(:color)
|
|
end
|
|
|
|
scope.includes(:color)
|
|
end
|
|
|
|
def assignable_categories
|
|
model.project.categories if model.project.respond_to?(:categories)
|
|
end
|
|
|
|
def assignable_priorities
|
|
IssuePriority.active
|
|
end
|
|
|
|
def assignable_versions
|
|
model.try(:assignable_versions) if model.project
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :can
|
|
|
|
def validate_estimated_hours
|
|
if !model.estimated_hours.nil? && model.estimated_hours < 0
|
|
errors.add :estimated_hours, :only_values_greater_or_equal_zeroes_allowed
|
|
end
|
|
end
|
|
|
|
def validate_enabled_type
|
|
# Checks that the issue can not be added/moved to a disabled type
|
|
if type_context_changed?
|
|
errors.add :type_id, :inclusion unless model.project.types.include?(model.type)
|
|
end
|
|
end
|
|
|
|
def validate_assigned_to_exists
|
|
errors.add :assigned_to, :does_not_exist if model.assigned_to&.is_a?(User::InexistentUser)
|
|
end
|
|
|
|
def validate_type_exists
|
|
errors.add :type, :does_not_exist if type_inexistent?
|
|
end
|
|
|
|
def validate_milestone_constraint
|
|
if model.is_milestone? && model.due_date && model.start_date && model.start_date != model.due_date
|
|
errors.add :due_date, :not_start_date
|
|
end
|
|
end
|
|
|
|
def validate_parent_not_milestone
|
|
if model.parent&.is_milestone?
|
|
errors.add :parent, :cannot_be_milestone
|
|
end
|
|
end
|
|
|
|
def validate_parent_exists
|
|
if model.parent&.is_a?(WorkPackage::InexistentWorkPackage)
|
|
|
|
errors.add :parent, :does_not_exist
|
|
end
|
|
end
|
|
|
|
def validate_parent_in_same_project
|
|
if parent_in_different_project?
|
|
errors.add :parent, :cannot_be_in_another_project
|
|
end
|
|
end
|
|
|
|
# have to validate ourself as the parent relation is created after saving
|
|
def validate_parent_not_subtask
|
|
if model.parent_id_changed? && model.parent && invalid_relations_with_new_hierarchy.exists?
|
|
errors.add :base, :cant_link_a_work_package_with_a_descendant
|
|
end
|
|
end
|
|
|
|
def validate_status_exists
|
|
errors.add :status, :does_not_exist if model.status && !status_exists?
|
|
end
|
|
|
|
def validate_status_transition
|
|
if status_changed? && status_exists? && !(model.type_id_changed? || status_transition_exists?)
|
|
errors.add :status_id, :status_transition_invalid
|
|
end
|
|
end
|
|
|
|
def validate_active_priority
|
|
if model.priority && !model.priority.active? && model.priority_id_changed?
|
|
errors.add :priority_id, :only_active_priorities_allowed
|
|
end
|
|
end
|
|
|
|
def validate_priority_exists
|
|
errors.add :priority, :does_not_exist if model.priority&.is_a?(Priority::InexistentPriority)
|
|
end
|
|
|
|
def validate_category
|
|
if inexistent_category?
|
|
errors.add :category, :does_not_exist
|
|
elsif category_not_of_project?
|
|
errors.add :category, :only_same_project_categories_allowed
|
|
end
|
|
end
|
|
|
|
def validate_version_is_assignable
|
|
if model.version_id && !model.assignable_versions.map(&:id).include?(model.version_id)
|
|
errors.add :version_id, :inclusion
|
|
end
|
|
end
|
|
|
|
def validate_no_reopen_on_closed_version
|
|
if model.version_id && model.reopened? && model.version.closed?
|
|
errors.add :base, I18n.t(:error_can_not_reopen_work_package_on_closed_version)
|
|
end
|
|
end
|
|
|
|
def validate_people_visible(attribute, id_attribute, list)
|
|
id = model[id_attribute]
|
|
|
|
return if id.nil? || !model.changed.include?(id_attribute)
|
|
|
|
unless principal_visible?(id, list)
|
|
errors.add attribute,
|
|
I18n.t('api_v3.errors.validation.invalid_user_assigned_to_work_package',
|
|
property: I18n.t("attributes.#{attribute}"))
|
|
end
|
|
end
|
|
|
|
def principal_visible?(id, list)
|
|
list.exists?(user_id: id)
|
|
end
|
|
|
|
def start_before_soonest_start?
|
|
model.start_date &&
|
|
model.soonest_start &&
|
|
model.start_date < model.soonest_start
|
|
end
|
|
|
|
def parent_in_different_project?
|
|
model.parent &&
|
|
model.parent.project != model.project &&
|
|
!Setting.cross_project_work_package_relations? &&
|
|
!model.parent.is_a?(WorkPackage::InexistentWorkPackage)
|
|
end
|
|
|
|
def inexistent_category?
|
|
model.category_id.present? && !model.category
|
|
end
|
|
|
|
def category_not_of_project?
|
|
model.category && !model.project.categories.include?(model.category)
|
|
end
|
|
|
|
def status_changed?
|
|
model.status_id_was != 0 && model.status_id_changed?
|
|
end
|
|
|
|
def status_exists?
|
|
model.status_id && model.status && !model.status.is_a?(Status::InexistentStatus)
|
|
end
|
|
|
|
def status_transition_exists?
|
|
assignable_statuses.exists?(model.status_id)
|
|
end
|
|
|
|
def invalid_relations_with_new_hierarchy
|
|
query = Relation.from_parent_to_self_and_descendants(model)
|
|
.or(Relation.from_self_and_descendants_to_ancestors(model))
|
|
.direct
|
|
|
|
# Ignore the immediate relation from the old parent to the model
|
|
# since that will still exist before saving.
|
|
old_parent_id = model.parent_id_was
|
|
|
|
if old_parent_id.present?
|
|
query
|
|
.where.not(hierarchy: 1)
|
|
.where.not(from_id: old_parent_id)
|
|
.where.not(to_id: model.id)
|
|
else
|
|
query
|
|
end
|
|
end
|
|
|
|
def type_context_changed?
|
|
model.project && !type_inexistent? && (model.type_id_changed? || model.project_id_changed?)
|
|
end
|
|
|
|
def type_inexistent?
|
|
model.type.is_a?(Type::InexistentType)
|
|
end
|
|
|
|
# Returns a scope of status the user is able to apply
|
|
def new_statuses_allowed_from(status)
|
|
return Status.where('1=0') if status.nil?
|
|
|
|
current_status = Status.where(id: status.id)
|
|
|
|
return current_status if closed_version_and_status?(status)
|
|
|
|
statuses = new_statuses_by_workflow(status)
|
|
.or(current_status)
|
|
|
|
statuses = statuses.where(is_closed: false) if model.blocked?
|
|
|
|
statuses
|
|
end
|
|
|
|
def closed_version_and_status?(status = model.status)
|
|
model.version&.closed? && status.is_closed?
|
|
end
|
|
|
|
def new_statuses_by_workflow(status)
|
|
workflows = Workflow
|
|
.from_status(status.id,
|
|
model.type_id,
|
|
users_roles_in_project.map(&:id),
|
|
user_is_author?,
|
|
user_was_or_is_assignee?)
|
|
|
|
Status.where(id: workflows.select(:new_status_id))
|
|
end
|
|
|
|
def user_was_or_is_assignee?
|
|
model.assigned_to_id_changed? ? model.assigned_to_id_was == user.id : model.assigned_to_id == user.id
|
|
end
|
|
|
|
def user_is_author?
|
|
model.author == user
|
|
end
|
|
|
|
def users_roles_in_project
|
|
user.roles_for_project(model.project)
|
|
end
|
|
end
|
|
end
|
|
|