Prohibit scheduling automatically in API if no predecessors or children

Only when creating a work package. The only case where it's possible is
when creating a work package with a parent explicitly set, and that
parent is automatically scheduled and has at least one predecessor.

When copying work packages, this restriction is removed as predecessors
and children are added later in the process.
This commit is contained in:
Christophe Bliard
2025-05-26 10:48:59 +02:00
parent b440d672a8
commit 144d4975de
9 changed files with 40 additions and 14 deletions
@@ -36,5 +36,11 @@ module WorkPackages
# a scenario where this field must be writable
attribute :done_ratio,
writable: true
# Do not validate predecessors or children presence when copying: when
# copying, it's possible to create a work package in automatic scheduling
# mode even if it has no predecessors or children yet. They will be added
# later in the process.
def validate_has_predecessors_or_children; end
end
end
@@ -42,6 +42,10 @@ module WorkPackages
# Note that nil would not override and [] would ignore the default permission, so we use the default here:
permission: :add_work_packages
attribute :schedule_manually do
validate_has_predecessors_or_children if model.schedule_automatically?
end
default_attribute_permission :add_work_packages
validate :user_allowed_to_add
@@ -66,5 +70,11 @@ module WorkPackages
# lock version is initialized by AR itself
super - ["lock_version"]
end
def validate_has_predecessors_or_children
if Relation.used_for_scheduling_of(model.parent).empty? && model.children.empty?
errors.add(:schedule_manually, :cannot_be_automatically_scheduled)
end
end
end
end
@@ -43,6 +43,8 @@ module Relations::Scopes
# soon as a parent is manually scheduled, its predecessors and ancestors
# are not involved in scheduling anymore.
def used_for_scheduling_of(work_package)
return [] if work_package.nil?
automatically_scheduled_ancestors =
WorkPackageHierarchy.where(descendant_id: work_package.id)
.where.not(ancestor_id: manually_scheduled_ancestors(work_package).select(:ancestor_id))
+2
View File
@@ -1498,6 +1498,8 @@ en:
cannot_be_self_assigned: "cannot be assigned to itself."
cannot_be_in_another_project: "cannot be in another project."
not_a_valid_parent: "is invalid."
schedule_manually:
cannot_be_automatically_scheduled: "cannot be set to false (automatically scheduled) as it has no predecessors or children."
start_date:
violates_relationships: "can only be set to %{soonest_start} or later so as not to violate the work package's relationships."
cannot_be_null: "can not be set to null as finish date and duration are known."
@@ -28,7 +28,10 @@ properties:
- description: The work package description
scheduleManually:
type: boolean
description: If false (default) schedule automatically.
description: |-
Uses manual scheduling mode when true (default). Uses automatic scheduling
mode when false. Can be automatic only when predecessors or children are
present.
readonly:
type: boolean
description: If true, the work package is in a readonly status so with the exception of the status, no other property can be altered.
@@ -14,7 +14,10 @@ properties:
- description: The work package description
scheduleManually:
type: boolean
description: If false (default) schedule automatically.
description: |-
Uses manual scheduling mode when true (default). Uses automatic scheduling
mode when false. Can be automatic only when predecessors or children are
present.
startDate:
type:
- 'string'
@@ -41,9 +44,9 @@ properties:
description: |-
The amount of time in hours the work package needs to be completed. This value must be bigger or equal to `P1D`,
and any the value will get floored to the nearest day.
The duration has no effect, unless either a start date or a due date is set.
Not available for milestone type of work packages.
ignoreNonWorkingDays:
type: boolean
+3 -3
View File
@@ -53,7 +53,7 @@ description: |-
| subject | Work package subject | String | not null; 1 <= length <= 255 | READ / WRITE | |
| type | Name of the work package's type | String | not null | READ | |
| description | The work package description | Formattable | | READ / WRITE | |
| scheduleManually | If false (default) schedule automatically. | Boolean | | READ / WRITE | |
| scheduleManually | Uses manual scheduling mode when true (default). Uses automatic scheduling mode when false. Can be automatic only when predecessors or children are present. | Boolean | | READ / WRITE | |
| startDate | Scheduled beginning of a work package | Date | Cannot be set for parent work packages unless it is scheduled manually; must be equal or greater than the earliest possible start date; Exists only on work packages of a non milestone type | READ / WRITE | |
| dueDate | Scheduled end of a work package | Date | Cannot be set for parent work packages unless it is scheduled manually; must be greater than or equal to the start date; Exists only on work packages of a non milestone type | READ / WRITE | |
| date | Date on which a milestone is achieved | Date | Exists only on work packages of a milestone type | READ / WRITE | |
@@ -83,9 +83,9 @@ description: |-
Properties that cannot be set directly on parent work packages are inferred from their children instead:
* `startDate` is the earliest start date from its children if manual scheduling is activated.
* `startDate` is the earliest start date from its children if automatic scheduling is activated.
* `dueDate` is the latest finish date from its children if manual scheduling is activated.
* `dueDate` is the latest finish date from its children if automatic scheduling is activated.
* `derivedEstimatedTime` is the sum of estimated times from its children and the work package's own estimated time.
@@ -53,13 +53,13 @@ module API
call.result
end
post &::API::V3::Utilities::Endpoints::Create.new(model: WorkPackage,
post(&::API::V3::Utilities::Endpoints::Create.new(model: WorkPackage,
parse_service: WorkPackages::ParseParamsService,
params_modifier: ->(attributes) {
attributes[:send_notifications] = notify_according_to_params
attributes
})
.mount
.mount)
route_param :id, type: Integer, desc: "Work package ID" do
helpers WorkPackagesSharedHelpers
@@ -253,11 +253,11 @@ RSpec.describe "API v3 Work package resource",
end
context "when the work package has no direct or indirect predecessors and no children" do
# TODO: should the API return an error here?
it "does not set the scheduling mode to automatic as requested " \
"and keeps manual scheduling mode (schedule_manually: true)" do
expect(created_work_package.schedule_manually).to be true
end
it_behaves_like "error response",
422,
"PropertyConstraintViolation",
I18n.t("activerecord.errors.models.work_package.attributes." \
"schedule_manually.cannot_be_automatically_scheduled")
end
end