mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Add multi-step "Allocate resource" dialog
Implement the dialog for creating a resource allocation as a two-step
Primer dialog driven by Turbo streams:
1. Kind step: choose between an explicit principal and a
filter-criteria placeholder.
2. Allocation step: the per-kind form (principal/work package, date
range, hours, filter name) swapped in without navigation.
Backend changes:
- Controller `new`/`step`/`create` actions render and update the
dialog via Turbo streams; entity and principal are resolved against
the project and the user's visibility.
- Add `principal_explicit` to distinguish an assigned principal from a
named filter placeholder, with validations and a
`needs_principal_assignment` scope on ResourceAllocation.
- Add the `step` collection route.
- ViewComponents and form objects under app/components and app/forms.
- Locales for the dialog copy.
Add request, feature, and model specs covering the flow.
This commit is contained in:
+63
@@ -0,0 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module AllocationStep
|
||||
class FooterComponent < ApplicationComponent
|
||||
include OpTurbo::Streamable
|
||||
include OpPrimer::ComponentHelpers
|
||||
|
||||
def wrapper_key
|
||||
ResourceAllocations::NewDialogComponent::FOOTER_ID
|
||||
end
|
||||
|
||||
def call
|
||||
component_wrapper do
|
||||
component_collection do |buttons|
|
||||
buttons.with_component(
|
||||
Primer::Beta::Button.new(
|
||||
data: { "close-dialog-id": ResourceAllocations::NewDialogComponent::DIALOG_ID },
|
||||
mr: 1
|
||||
)
|
||||
) { I18n.t(:button_cancel) }
|
||||
|
||||
buttons.with_component(
|
||||
Primer::Beta::Button.new(
|
||||
scheme: :primary,
|
||||
form: ResourceAllocations::NewDialogComponent::FORM_ID,
|
||||
type: :submit
|
||||
)
|
||||
) { I18n.t("resource_management.allocate_resource_dialog.submit") }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<%=
|
||||
component_wrapper do
|
||||
primer_form_with(
|
||||
model: @allocation,
|
||||
scope: :resource_allocation,
|
||||
url: project_resource_allocations_path(@project),
|
||||
method: :post,
|
||||
html: {
|
||||
data: { turbo_stream: true },
|
||||
id: ResourceAllocations::NewDialogComponent::FORM_ID
|
||||
}
|
||||
) do |f|
|
||||
render(form_list_component(f))
|
||||
end
|
||||
end
|
||||
%>
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module AllocationStep
|
||||
# Step 2 of the dialog: the allocation details. Shares the body wrapper key
|
||||
# with the step 1 form so the controller can swap one for the other via a
|
||||
# Turbo stream. For the filter kind it also renders the criteria builder
|
||||
# (`Filters::FilterForm`) over a UserQuery.
|
||||
class FormComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
include OpTurbo::Streamable
|
||||
include OpPrimer::ComponentHelpers
|
||||
|
||||
def initialize(allocation:, project:, allocation_kind:)
|
||||
super
|
||||
@allocation = allocation
|
||||
@project = project
|
||||
@allocation_kind = allocation_kind
|
||||
end
|
||||
|
||||
def wrapper_key
|
||||
ResourceAllocations::NewDialogComponent::BODY_ID
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_based?
|
||||
@allocation_kind.to_s == "filter"
|
||||
end
|
||||
|
||||
def dialog_id
|
||||
ResourceAllocations::NewDialogComponent::DIALOG_ID
|
||||
end
|
||||
|
||||
def form_list_component(form)
|
||||
result = [
|
||||
ResourceAllocations::Forms::WorkPackageForm.new(form, project: @project, dialog_id: dialog_id),
|
||||
ResourceAllocations::Forms::DateRangeForm.new(form, dialog_id: dialog_id),
|
||||
ResourceAllocations::Forms::HoursForm.new(form),
|
||||
ResourceAllocations::Forms::AllocationKindForm.new(form, allocation_kind: @allocation_kind)
|
||||
|
||||
]
|
||||
result = if filter_based?
|
||||
[
|
||||
ResourceAllocations::Forms::FilterNameForm.new(form),
|
||||
::Filters::FilterForm.new(
|
||||
form,
|
||||
query: @allocation.candidate_query,
|
||||
wrap_with_controller: true,
|
||||
hidden_input_name: "filters",
|
||||
output_format: :json,
|
||||
autocomplete_append_to: "##{dialog_id}"
|
||||
)
|
||||
] + result
|
||||
else
|
||||
[
|
||||
ResourceAllocations::Forms::PrincipalForm.new(
|
||||
form,
|
||||
project: @project,
|
||||
dialog_id: dialog_id
|
||||
)
|
||||
] + result
|
||||
end
|
||||
|
||||
Primer::Forms::FormList.new(*result)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module KindStep
|
||||
class FooterComponent < ApplicationComponent
|
||||
include OpTurbo::Streamable
|
||||
include OpPrimer::ComponentHelpers
|
||||
|
||||
def wrapper_key
|
||||
ResourceAllocations::NewDialogComponent::FOOTER_ID
|
||||
end
|
||||
|
||||
def call
|
||||
component_wrapper do
|
||||
component_collection do |buttons|
|
||||
buttons.with_component(
|
||||
Primer::Beta::Button.new(
|
||||
data: { "close-dialog-id": ResourceAllocations::NewDialogComponent::DIALOG_ID },
|
||||
mr: 1
|
||||
)
|
||||
) { I18n.t(:button_cancel) }
|
||||
|
||||
buttons.with_component(
|
||||
Primer::Beta::Button.new(
|
||||
scheme: :primary,
|
||||
form: ResourceAllocations::NewDialogComponent::FORM_ID,
|
||||
type: :submit
|
||||
)
|
||||
) { I18n.t("button_next") }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<%=
|
||||
component_wrapper do
|
||||
primer_form_with(
|
||||
url: step_project_resource_allocations_path(@project),
|
||||
method: :get,
|
||||
html: {
|
||||
data: { turbo_stream: true },
|
||||
id: ResourceAllocations::NewDialogComponent::FORM_ID
|
||||
}
|
||||
) do |f|
|
||||
render(ResourceAllocations::Forms::KindSelectForm.new(f, work_package: @work_package))
|
||||
end
|
||||
end
|
||||
%>
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module KindStep
|
||||
# Step 1 of the dialog: the kind selection. Submits via GET to #new, which
|
||||
# swaps in the step 2 form keyed on the chosen `allocation_kind`.
|
||||
class FormComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
include OpTurbo::Streamable
|
||||
include OpPrimer::ComponentHelpers
|
||||
|
||||
def initialize(project:, work_package: nil)
|
||||
super
|
||||
@project = project
|
||||
@work_package = work_package
|
||||
end
|
||||
|
||||
def wrapper_key
|
||||
ResourceAllocations::NewDialogComponent::BODY_ID
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%=
|
||||
render(
|
||||
Primer::Alpha::Dialog.new(
|
||||
id: DIALOG_ID,
|
||||
title:,
|
||||
size: :large,
|
||||
position: :right,
|
||||
data: { "keep-open-on-submit": true }
|
||||
)
|
||||
) do |dialog|
|
||||
dialog.with_header(variant: :large)
|
||||
|
||||
# Gives the user/work-package autocompleter dropdowns room in this
|
||||
# content-sized, centered modal.
|
||||
dialog.with_body(classes: "Overlay-body_autocomplete_height") do
|
||||
render(
|
||||
ResourceAllocations::KindStep::FormComponent.new(
|
||||
project: @project,
|
||||
work_package: @work_package
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
dialog.with_footer do
|
||||
render(ResourceAllocations::KindStep::FooterComponent.new)
|
||||
end
|
||||
end
|
||||
%>
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
class NewDialogComponent < ApplicationComponent
|
||||
include OpTurbo::Streamable
|
||||
include OpPrimer::ComponentHelpers
|
||||
|
||||
DIALOG_ID = "allocate-resource-dialog"
|
||||
FORM_ID = "allocate-resource-form"
|
||||
FOOTER_ID = "allocate-resource-footer"
|
||||
# Shared by both step forms so swapping step 1 for step 2 targets the same
|
||||
# Turbo stream wrapper.
|
||||
BODY_ID = "allocate-resource-dialog-body"
|
||||
|
||||
def initialize(project:, work_package: nil)
|
||||
super
|
||||
|
||||
@project = project
|
||||
@work_package = work_package
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def title
|
||||
I18n.t("resource_management.allocate_resource_dialog.title")
|
||||
end
|
||||
end
|
||||
end
|
||||
+10
-2
@@ -51,7 +51,12 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
"aria-label": t("resource_management.work_package_list.subheader.add")
|
||||
}
|
||||
) do |menu|
|
||||
menu.with_item(label: t("resource_management.work_package_list.subheader.allocate")) do |item|
|
||||
menu.with_item(
|
||||
label: t("resource_management.work_package_list.subheader.allocate"),
|
||||
tag: :a,
|
||||
href: new_project_resource_allocation_path(@project),
|
||||
content_arguments: { data: { controller: "async-dialog" } }
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: :people)
|
||||
end
|
||||
|
||||
@@ -68,7 +73,10 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
subheader.with_action_button(
|
||||
leading_icon: :plus,
|
||||
scheme: :primary,
|
||||
label: t("resource_management.work_package_list.subheader.allocate")
|
||||
label: t("resource_management.work_package_list.subheader.allocate"),
|
||||
tag: :a,
|
||||
href: new_project_resource_allocation_path(@project),
|
||||
data: { controller: "async-dialog" }
|
||||
) do
|
||||
t("resource_management.work_package_list.subheader.allocate")
|
||||
end
|
||||
|
||||
@@ -35,6 +35,7 @@ module ResourceAllocations
|
||||
end
|
||||
|
||||
attribute :principal
|
||||
attribute :principal_explicit
|
||||
attribute :state
|
||||
attribute :start_date
|
||||
attribute :end_date
|
||||
|
||||
+114
-5
@@ -36,18 +36,127 @@ module ::ResourceManagement
|
||||
before_action :find_project_by_project_id
|
||||
before_action :authorize
|
||||
|
||||
# The modals and the ResourceAllocations::* contracts/services are wired up
|
||||
# in follow-up work. For now these are stubs so the routes and the
|
||||
# `allocate_user_resources` permission have something to bind to.
|
||||
# Step 1 of the "Allocate resource" dialog: open it on the kind selection
|
||||
# (explicit user vs filter-criteria placeholder).
|
||||
def new
|
||||
respond_with_dialog ResourceAllocations::NewDialogComponent.new(
|
||||
project: @project,
|
||||
work_package: context_work_package
|
||||
)
|
||||
end
|
||||
|
||||
def new; end
|
||||
# Step 2: the kind selection submits here, swapping the dialog body and
|
||||
# footer for the allocation form of the chosen `allocation_kind` via Turbo
|
||||
# streams (no navigation).
|
||||
def step
|
||||
# Seed the entity from the originating context (if any) so the work
|
||||
# package autocompleter renders pre-selected.
|
||||
render_allocation_step(ResourceAllocation.new(entity: context_work_package))
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def create; end
|
||||
def create
|
||||
call = ResourceAllocations::CreateService
|
||||
.new(user: current_user, model: ResourceAllocation.new)
|
||||
.call(create_params)
|
||||
|
||||
if call.success?
|
||||
render_create_success
|
||||
else
|
||||
render_allocation_step(call.result, status: :unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
def update; end
|
||||
|
||||
def destroy; end
|
||||
|
||||
private
|
||||
|
||||
def render_allocation_step(allocation, status: :ok)
|
||||
replace_via_turbo_stream(
|
||||
component: ResourceAllocations::AllocationStep::FormComponent.new(
|
||||
allocation:,
|
||||
project: @project,
|
||||
allocation_kind:
|
||||
),
|
||||
status:
|
||||
)
|
||||
replace_via_turbo_stream(component: ResourceAllocations::AllocationStep::FooterComponent.new)
|
||||
respond_with_turbo_streams(status:)
|
||||
end
|
||||
|
||||
def render_create_success
|
||||
render_success_flash_message_via_turbo_stream(
|
||||
message: I18n.t("resource_management.allocate_resource_dialog.success_message")
|
||||
)
|
||||
close_dialog_via_turbo_stream("##{ResourceAllocations::NewDialogComponent::DIALOG_ID}")
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
def allocation_kind
|
||||
params[:allocation_kind].presence || "principal"
|
||||
end
|
||||
|
||||
def filter_based_kind?
|
||||
allocation_kind == "filter"
|
||||
end
|
||||
|
||||
# The work package the dialog was opened from (e.g. a timeline row), used
|
||||
# to pre-select the autocompleter. It arrives as `work_package_id`. Always
|
||||
# scoped to the current project and the user's visibility.
|
||||
def context_work_package
|
||||
return @context_work_package if defined?(@context_work_package)
|
||||
|
||||
@context_work_package = resolve_entity("WorkPackage", params[:work_package_id])
|
||||
end
|
||||
|
||||
def create_params
|
||||
permitted = params
|
||||
.expect(resource_allocation: %i[principal_id filter_name start_date end_date allocated_hours
|
||||
entity_type entity_id])
|
||||
.to_h
|
||||
.symbolize_keys
|
||||
|
||||
principal_id = permitted.delete(:principal_id)
|
||||
entity = resolve_entity(permitted.delete(:entity_type), permitted.delete(:entity_id))
|
||||
permitted.merge(entity:, **resource_params(principal_id))
|
||||
end
|
||||
|
||||
# Resolves the polymorphic entity from the submitted type/id pair, scoped to
|
||||
# the current project and the user's visibility. The type is checked against
|
||||
# the model's allow-list before it is constantized. Returns nil for an
|
||||
# unknown type or unreachable id so the `entity` presence/type validations
|
||||
# surface the error.
|
||||
def resolve_entity(entity_type, entity_id)
|
||||
return if entity_id.blank?
|
||||
return unless ResourceAllocation::ALLOWED_ENTITY_TYPES.include?(entity_type)
|
||||
|
||||
entity_type.constantize.visible(current_user).where(project: @project).find_by(id: entity_id)
|
||||
end
|
||||
|
||||
# The kind drives which side of the allocation is populated and is recorded
|
||||
# on the model via `principal_explicit`: an explicit principal, or a named
|
||||
# filter placeholder.
|
||||
def resource_params(principal_id)
|
||||
if filter_based_kind?
|
||||
{ principal_explicit: false, principal: nil, user_filter: parsed_user_filter }
|
||||
else
|
||||
{ principal_explicit: true, principal: User.find_by(id: principal_id), filter_name: nil, user_filter: [] }
|
||||
end
|
||||
end
|
||||
|
||||
# Turns the FilterForm's JSON payload into UserQuery filter objects, which
|
||||
# is the shape `ResourceAllocation#user_filter` serializes.
|
||||
def parsed_user_filter
|
||||
return [] if params[:filters].blank?
|
||||
|
||||
query = UserQuery.new
|
||||
::Queries::ParamsParser.parse(filters: params[:filters])
|
||||
.fetch(:filters, [])
|
||||
.each { |f| query.where(f[:attribute], f[:operator], f[:values]) }
|
||||
query.filters
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module Forms
|
||||
class AllocationKindForm < ApplicationForm
|
||||
def initialize(allocation_kind:)
|
||||
@allocation_kind = allocation_kind
|
||||
super()
|
||||
end
|
||||
|
||||
form do |f|
|
||||
f.hidden(
|
||||
name: :allocation_kind,
|
||||
value: @allocation_kind,
|
||||
scope_name_to_model: false
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module Forms
|
||||
# The allocation's start and finish dates, shown side by side. The date
|
||||
# pickers are told they live in the dialog so their popovers are not clipped.
|
||||
class DateRangeForm < ApplicationForm
|
||||
form do |f|
|
||||
f.group(layout: :horizontal) do |dates|
|
||||
dates.single_date_picker(
|
||||
name: :start_date,
|
||||
label: ResourceAllocation.human_attribute_name(:start_date),
|
||||
required: true,
|
||||
value: model.start_date&.iso8601,
|
||||
datepicker_options: { inDialog: @dialog_id }
|
||||
)
|
||||
dates.single_date_picker(
|
||||
name: :end_date,
|
||||
label: ResourceAllocation.human_attribute_name(:end_date),
|
||||
required: true,
|
||||
value: model.end_date&.iso8601,
|
||||
datepicker_options: { inDialog: @dialog_id }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(dialog_id:)
|
||||
super()
|
||||
@dialog_id = dialog_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module Forms
|
||||
# The placeholder's display name shown for a `filter` allocation, rendered
|
||||
# above the filter-criteria builder (`Filters::FilterForm`).
|
||||
class FilterNameForm < ApplicationForm
|
||||
form do |f|
|
||||
f.text_field(
|
||||
name: :filter_name,
|
||||
label: ResourceAllocation.human_attribute_name(:filter_name),
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,72 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module Forms
|
||||
# The allocated duration, entered in chronic-duration syntax (e.g. "40h",
|
||||
# "1d 4h"). The `chronic-duration` Stimulus controller normalises the input
|
||||
# to a canonical hours string on blur.
|
||||
class HoursForm < ApplicationForm
|
||||
form do |f|
|
||||
f.text_field(
|
||||
name: :allocated_hours,
|
||||
label: ResourceAllocation.human_attribute_name(:allocated_hours),
|
||||
required: true,
|
||||
value: formatted_hours,
|
||||
invalid: allocated_time_error.present?,
|
||||
validation_message: allocated_time_error,
|
||||
data: { controller: "chronic-duration" }
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# The duration is entered as `allocated_hours` but stored and validated as
|
||||
# `allocated_time`. Surface that attribute's errors on this field, each
|
||||
# formatted like Primer's own field errors ("Hours can't be blank.").
|
||||
def allocated_time_error
|
||||
label = ResourceAllocation.human_attribute_name(:allocated_hours)
|
||||
model.errors.messages_for(:allocated_time)
|
||||
.map { |message| "#{label} #{message}" }
|
||||
.join(" ")
|
||||
.presence
|
||||
end
|
||||
|
||||
# Renders the stored duration as e.g. "40h", matching what the model's
|
||||
# `allocated_hours=` setter accepts back. Only relevant when re-rendering
|
||||
# the step after a validation error.
|
||||
def formatted_hours
|
||||
return if model.allocated_hours.nil?
|
||||
|
||||
DurationConverter.output(model.allocated_hours)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module Forms
|
||||
class KindSelectForm < ApplicationForm
|
||||
def initialize(work_package:)
|
||||
super()
|
||||
|
||||
@work_package = work_package
|
||||
end
|
||||
|
||||
form do |f|
|
||||
f.hidden name: :work_package_id,
|
||||
value: @work_package&.id,
|
||||
scope_name_to_model: false
|
||||
|
||||
f.advanced_radio_button_group(
|
||||
name: :allocation_kind,
|
||||
label: I18n.t("resource_management.allocate_resource_dialog.kind.label"),
|
||||
visually_hide_label: true,
|
||||
scope_name_to_model: false
|
||||
) do |group|
|
||||
group.radio_button(
|
||||
value: "principal",
|
||||
checked: true,
|
||||
label: I18n.t("resource_management.allocate_resource_dialog.kind.principal.label"),
|
||||
caption: I18n.t("resource_management.allocate_resource_dialog.kind.principal.caption")
|
||||
)
|
||||
group.radio_button(
|
||||
value: "filter",
|
||||
label: I18n.t("resource_management.allocate_resource_dialog.kind.filter.label"),
|
||||
caption: I18n.t("resource_management.allocate_resource_dialog.kind.filter.caption")
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module Forms
|
||||
# The explicit-user picker shown for a `principal` allocation. The typeahead
|
||||
# is scoped to active users who are members of the current project.
|
||||
class PrincipalForm < ApplicationForm
|
||||
form do |f|
|
||||
f.autocompleter(
|
||||
name: :principal_id,
|
||||
label: ResourceAllocation.human_attribute_name(:principal),
|
||||
required: true,
|
||||
invalid: principal_error.present?,
|
||||
validation_message: principal_error,
|
||||
autocomplete_options: {
|
||||
component: "opce-user-autocompleter",
|
||||
url: ::API::V3::Utilities::PathHelper::ApiV3Path.principals,
|
||||
resource: "principals",
|
||||
searchKey: "any_name_attribute",
|
||||
filters: principal_filters,
|
||||
defaultData: true,
|
||||
focusDirectly: false,
|
||||
multiple: false,
|
||||
appendTo: "##{@dialog_id}"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def initialize(project:, dialog_id:)
|
||||
super()
|
||||
@project = project
|
||||
@dialog_id = dialog_id
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# The picker submits `principal_id`; any model error is keyed on the
|
||||
# `principal` association. Surface those errors on this field, each
|
||||
# formatted like Primer's own field errors ("Assignee can't be blank.").
|
||||
def principal_error
|
||||
label = ResourceAllocation.human_attribute_name(:principal)
|
||||
model.errors.messages_for(:principal)
|
||||
.map { |message| "#{label} #{message}" }
|
||||
.join(" ")
|
||||
.presence
|
||||
end
|
||||
|
||||
def principal_filters
|
||||
[
|
||||
{ name: "type", operator: "=", values: %w[User] },
|
||||
{ name: "status", operator: "=", values: [Principal.statuses[:active]] },
|
||||
{ name: "member", operator: "=", values: [@project.id.to_s] }
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,77 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ResourceAllocations
|
||||
module Forms
|
||||
# The work package the allocation is for. It is the polymorphic `entity`,
|
||||
# submitted as `entity_type` + `entity_id` and resolved back to a WorkPackage
|
||||
# in the controller. A remote autocompleter over all work packages in the
|
||||
# project; pre-selection flows through the bound model's `entity_id`.
|
||||
class WorkPackageForm < ApplicationForm
|
||||
form do |f|
|
||||
f.hidden name: :entity_type, value: "WorkPackage"
|
||||
f.work_package_autocompleter(
|
||||
name: :entity_id,
|
||||
label: WorkPackage.model_name.human,
|
||||
required: true,
|
||||
invalid: entity_error.present?,
|
||||
validation_message: entity_error,
|
||||
autocomplete_options: {
|
||||
openDirectly: false,
|
||||
focusDirectly: false,
|
||||
dropdownPosition: "bottom",
|
||||
appendTo: "##{@dialog_id}",
|
||||
filters: [{ name: "project_id", operator: "=", values: [@project.id.to_s] }]
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def initialize(project:, dialog_id:)
|
||||
super()
|
||||
@project = project
|
||||
@dialog_id = dialog_id
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# The work package is submitted as `entity_id`, but the model keys the
|
||||
# polymorphic association's presence/type errors on `entity`/`entity_type`.
|
||||
# Surface them on this field, each formatted like Primer's own field errors
|
||||
# ("Work package must exist.").
|
||||
def entity_error
|
||||
messages = model.errors.messages_for(:entity) + model.errors.messages_for(:entity_type)
|
||||
messages
|
||||
.map { |message| "#{WorkPackage.model_name.human} #{message}" }
|
||||
.join(" ")
|
||||
.presence
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -55,6 +55,8 @@ class ResourceAllocation < ApplicationRecord
|
||||
canceled: "canceled"
|
||||
}
|
||||
|
||||
scope :needs_principal_assignment, -> { where(principal_explicit: false, principal_id: nil) }
|
||||
|
||||
validates :state, :start_date, :end_date, presence: true
|
||||
validates :allocated_time,
|
||||
presence: true,
|
||||
@@ -64,7 +66,13 @@ class ResourceAllocation < ApplicationRecord
|
||||
inclusion: { in: ALLOWED_ENTITY_TYPES },
|
||||
allow_blank: true
|
||||
|
||||
validates :filter_name, presence: true, if: :filter_based?
|
||||
with_options if: :principal_explicit? do
|
||||
validates :principal, presence: true
|
||||
validates :filter_name, absence: true
|
||||
validates :user_filter, absence: true
|
||||
end
|
||||
|
||||
validates :filter_name, presence: true, unless: :principal_explicit?
|
||||
|
||||
validate :end_date_after_start_date
|
||||
|
||||
@@ -87,13 +95,17 @@ class ResourceAllocation < ApplicationRecord
|
||||
end
|
||||
|
||||
def filter_based?
|
||||
user_filter.present?
|
||||
!principal_explicit?
|
||||
end
|
||||
|
||||
def user_assigned?
|
||||
principal_id.present?
|
||||
end
|
||||
|
||||
def needs_principal_assignment?
|
||||
!principal_explicit? && principal_id.blank?
|
||||
end
|
||||
|
||||
def candidate_query
|
||||
UserQuery.new.tap do |query|
|
||||
user_filter.each do |filter|
|
||||
|
||||
@@ -3,9 +3,11 @@ en:
|
||||
activerecord:
|
||||
attributes:
|
||||
resource_allocation:
|
||||
allocated_hours: Hours
|
||||
allocated_time: Allocated time
|
||||
end_date: Finish date
|
||||
entity: Entity
|
||||
filter_name: Resource filter name
|
||||
principal: Assignee
|
||||
start_date: Start date
|
||||
state: State
|
||||
@@ -66,6 +68,18 @@ en:
|
||||
make_private: Make private
|
||||
make_public: Make public
|
||||
unfavorite: Remove from favorites
|
||||
allocate_resource_dialog:
|
||||
title: Allocate resource
|
||||
submit: Allocate
|
||||
success_message: Resource allocated.
|
||||
kind:
|
||||
label: Allocation type
|
||||
principal:
|
||||
label: User
|
||||
caption: Allocate hours for a specific user.
|
||||
filter:
|
||||
label: Filter criteria
|
||||
caption: Set filter criteria based on user attributes to create a placeholder resource.
|
||||
blankslate:
|
||||
desc: Create a resource planner to start planning capacity for this project.
|
||||
title: No resource planners yet
|
||||
|
||||
@@ -65,6 +65,12 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :resource_allocations,
|
||||
controller: "resource_management/resource_allocations",
|
||||
only: %i[new create edit update destroy]
|
||||
only: %i[new create edit update destroy] do
|
||||
collection do
|
||||
# Step 2 of the "Allocate resource" dialog: swaps the kind selection for
|
||||
# the allocation form of the chosen `allocation_kind`.
|
||||
get :step
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
class AddPrincipalExplicitToResourceAllocations < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :resource_allocations, :principal_explicit, :boolean, null: false, default: true
|
||||
add_column :resource_allocation_journals, :principal_explicit, :boolean
|
||||
|
||||
# Existing placeholders are identified by a stored user filter.
|
||||
up_only do
|
||||
execute(<<~SQL.squish)
|
||||
UPDATE resource_allocations SET principal_explicit = false WHERE user_filter <> '[]'::jsonb
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -77,7 +77,7 @@ module OpenProject::ResourceManagement
|
||||
# it through `allowed_in_project?`). The `contract_actions` map keeps the
|
||||
# permission discoverable for API contracts.
|
||||
permission :allocate_user_resources,
|
||||
{ "resource_management/resource_allocations": %i[new create edit update destroy] },
|
||||
{ "resource_management/resource_allocations": %i[new step create edit update destroy] },
|
||||
permissible_on: :project,
|
||||
dependencies: %i[view_resource_planners],
|
||||
contract_actions: { resource_allocation: %i[create update destroy] }
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ RSpec.describe ResourceAllocations::CreateContract do
|
||||
end
|
||||
|
||||
it "allows principal, state, dates, allocated_time, and user_filter" do
|
||||
%i[principal state start_date end_date allocated_time user_filter].each do |attr|
|
||||
%i[principal principal_explicit state start_date end_date allocated_time user_filter].each do |attr|
|
||||
expect(contract.writable?(attr)).to be(true), "expected #{attr} to be writable"
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ RSpec.describe ResourceAllocations::UpdateContract do
|
||||
end
|
||||
|
||||
it "allows principal, state, dates, allocated_time, and user_filter" do
|
||||
%i[principal state start_date end_date allocated_time user_filter].each do |attr|
|
||||
%i[principal principal_explicit state start_date end_date allocated_time user_filter].each do |attr|
|
||||
expect(contract.writable?(attr)).to be(true), "expected #{attr} to be writable"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,10 +39,12 @@ FactoryBot.define do
|
||||
end_date { Date.new(2026, 1, 9) }
|
||||
allocated_time { 5 * 8 * 60 } # 5 days of 8 hours in minutes
|
||||
user_filter { [] }
|
||||
principal_explicit { true }
|
||||
|
||||
traits_for_enum :state
|
||||
|
||||
trait :with_user_filter do
|
||||
principal_explicit { false }
|
||||
principal { nil }
|
||||
filter_name { "Full stack Developer (DE-EN)" }
|
||||
transient do
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "Allocate resource dialog", :js do
|
||||
shared_let(:project) { create(:project, enabled_module_names: %w[resource_management work_package_tracking]) }
|
||||
shared_let(:user) do
|
||||
create(:user,
|
||||
member_with_permissions: { project => %i[view_resource_planners allocate_user_resources view_work_packages] })
|
||||
end
|
||||
shared_let(:resource_planner) { create(:resource_planner, project:, principal: user) }
|
||||
shared_let(:view) do
|
||||
ResourceWorkPackageList.create!(name: "WP list", parent: resource_planner, project:, principal: user)
|
||||
end
|
||||
|
||||
before do
|
||||
login_as user
|
||||
visit project_resource_planner_view_path(project, resource_planner, view)
|
||||
end
|
||||
|
||||
it "opens the dialog and advances from the kind step to the allocation step" do
|
||||
click_on I18n.t("resource_management.work_package_list.subheader.allocate")
|
||||
|
||||
within_dialog do
|
||||
expect(page).to have_text(I18n.t("resource_management.allocate_resource_dialog.title"))
|
||||
expect(page).to have_text(I18n.t("resource_management.allocate_resource_dialog.kind.principal.label"))
|
||||
expect(page).to have_text(I18n.t("resource_management.allocate_resource_dialog.kind.filter.label"))
|
||||
|
||||
# "User" is selected by default — advance to step 2.
|
||||
click_on I18n.t("button_next")
|
||||
|
||||
expect(page).to have_field(WorkPackage.model_name.human)
|
||||
expect(page).to have_field(ResourceAllocation.human_attribute_name(:allocated_hours))
|
||||
expect(page).to have_button(I18n.t("resource_management.allocate_resource_dialog.submit"))
|
||||
end
|
||||
end
|
||||
|
||||
it "shows the filter criteria builder on the filter step" do
|
||||
click_on I18n.t("resource_management.work_package_list.subheader.allocate")
|
||||
|
||||
within_dialog do
|
||||
choose I18n.t("resource_management.allocate_resource_dialog.kind.filter.label")
|
||||
click_on I18n.t("button_next")
|
||||
|
||||
expect(page).to have_field(ResourceAllocation.human_attribute_name(:filter_name))
|
||||
# The blank UserQuery filter form renders its "add filter" selector.
|
||||
expect(page).to have_css(".op-filters-form")
|
||||
end
|
||||
end
|
||||
|
||||
def within_dialog(&)
|
||||
within("##{ResourceAllocations::NewDialogComponent::DIALOG_ID}", &)
|
||||
end
|
||||
end
|
||||
@@ -102,41 +102,51 @@ RSpec.describe ResourceAllocation do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#user_assigned? / #filter_based?" do
|
||||
describe "#user_assigned? / #filter_based? / #needs_principal_assignment?" do
|
||||
let(:assignee) { build_stubbed(:user) }
|
||||
let(:filter) do
|
||||
UserQuery.new.filter_for(:name).tap do |f|
|
||||
f.operator = "~"
|
||||
f.values = ["alice"]
|
||||
end
|
||||
end
|
||||
|
||||
context "with an explicit user (no filter)" do
|
||||
subject(:allocation) { described_class.new(principal: assignee, user_filter: []) }
|
||||
context "with an explicit user allocation" do
|
||||
subject(:allocation) { described_class.new(principal_explicit: true, principal: assignee) }
|
||||
|
||||
it { is_expected.to be_user_assigned }
|
||||
it { is_expected.not_to be_filter_based }
|
||||
it { is_expected.not_to be_needs_principal_assignment }
|
||||
end
|
||||
|
||||
context "with a filter placeholder (no principal)" do
|
||||
subject(:allocation) { described_class.new(principal: nil, user_filter: [filter]) }
|
||||
context "with an unassigned filter placeholder" do
|
||||
subject(:allocation) { described_class.new(principal_explicit: false, principal: nil) }
|
||||
|
||||
it { is_expected.not_to be_user_assigned }
|
||||
it { is_expected.to be_filter_based }
|
||||
it { is_expected.to be_needs_principal_assignment }
|
||||
end
|
||||
|
||||
context "with a real user assigned to a filter allocation" do
|
||||
subject(:allocation) { described_class.new(principal: assignee, user_filter: [filter]) }
|
||||
context "with a filter placeholder that has a principal assigned" do
|
||||
subject(:allocation) { described_class.new(principal_explicit: false, principal: assignee) }
|
||||
|
||||
it { is_expected.to be_user_assigned }
|
||||
it { is_expected.to be_filter_based }
|
||||
it { is_expected.not_to be_needs_principal_assignment }
|
||||
end
|
||||
end
|
||||
|
||||
describe ".needs_principal_assignment" do
|
||||
shared_let(:project) { create(:project) }
|
||||
shared_let(:work_package) { create(:work_package, project:) }
|
||||
|
||||
let!(:unassigned_placeholder) do
|
||||
create(:resource_allocation, entity: work_package, principal_explicit: false, principal: nil, filter_name: "Devs")
|
||||
end
|
||||
|
||||
context "with neither a principal nor a filter" do
|
||||
subject(:allocation) { described_class.new(principal: nil, user_filter: []) }
|
||||
before do
|
||||
# An explicit allocation and an already-assigned placeholder must be excluded.
|
||||
create(:resource_allocation, entity: work_package)
|
||||
create(:resource_allocation, entity: work_package,
|
||||
principal_explicit: false, principal: create(:user), filter_name: "Devs")
|
||||
end
|
||||
|
||||
it { is_expected.not_to be_user_assigned }
|
||||
it { is_expected.not_to be_filter_based }
|
||||
it "returns only filter placeholders without a principal" do
|
||||
expect(described_class.needs_principal_assignment).to contain_exactly(unassigned_placeholder)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -223,8 +233,17 @@ RSpec.describe ResourceAllocation do
|
||||
expect(allocation.errors[:allocated_time]).to be_present
|
||||
end
|
||||
|
||||
it "does not require principal (column is nullable)" do
|
||||
it "requires a principal for an explicit allocation" do
|
||||
allocation.principal_explicit = true
|
||||
allocation.principal = nil
|
||||
expect(allocation).not_to be_valid
|
||||
expect(allocation.errors.symbols_for(:principal)).to include(:blank)
|
||||
end
|
||||
|
||||
it "does not require a principal for a filter placeholder" do
|
||||
allocation.principal_explicit = false
|
||||
allocation.principal = nil
|
||||
allocation.filter_name = "Devs"
|
||||
expect(allocation).to be_valid
|
||||
end
|
||||
end
|
||||
@@ -302,7 +321,7 @@ RSpec.describe ResourceAllocation do
|
||||
end
|
||||
end
|
||||
|
||||
describe "filter_name (filter-based allocations)" do
|
||||
describe "allocation kind (principal_explicit)" do
|
||||
let(:filter) do
|
||||
UserQuery.new.filter_for(:name).tap do |f|
|
||||
f.operator = "~"
|
||||
@@ -310,35 +329,49 @@ RSpec.describe ResourceAllocation do
|
||||
end
|
||||
end
|
||||
|
||||
it "is not filter-based and needs no filter_name with only a principal" do
|
||||
allocation.user_filter = []
|
||||
expect(allocation).to be_valid
|
||||
expect(allocation).not_to be_filter_based
|
||||
context "when explicit (principal_explicit: true)" do
|
||||
before { allocation.principal_explicit = true }
|
||||
|
||||
it "is valid with a principal and no filter" do
|
||||
expect(allocation).to be_valid
|
||||
end
|
||||
|
||||
it "rejects a filter_name" do
|
||||
allocation.filter_name = "Devs"
|
||||
expect(allocation).not_to be_valid
|
||||
expect(allocation.errors.symbols_for(:filter_name)).to include(:present)
|
||||
end
|
||||
|
||||
it "rejects a user_filter" do
|
||||
allocation.user_filter = [filter]
|
||||
expect(allocation).not_to be_valid
|
||||
expect(allocation.errors.symbols_for(:user_filter)).to include(:present)
|
||||
end
|
||||
end
|
||||
|
||||
it "requires a filter_name once a user_filter is present" do
|
||||
allocation.user_filter = [filter]
|
||||
allocation.filter_name = nil
|
||||
context "when filter-based (principal_explicit: false)" do
|
||||
before do
|
||||
allocation.principal_explicit = false
|
||||
allocation.principal = nil
|
||||
end
|
||||
|
||||
expect(allocation).to be_filter_based
|
||||
expect(allocation).not_to be_valid
|
||||
expect(allocation.errors.symbols_for(:filter_name)).to include(:blank)
|
||||
end
|
||||
it "requires a filter_name" do
|
||||
allocation.filter_name = nil
|
||||
expect(allocation).not_to be_valid
|
||||
expect(allocation.errors.symbols_for(:filter_name)).to include(:blank)
|
||||
end
|
||||
|
||||
it "is valid as a placeholder (filter, no principal) with a name" do
|
||||
allocation.principal = nil
|
||||
allocation.user_filter = [filter]
|
||||
allocation.filter_name = "Full stack Developer (DE-EN)"
|
||||
it "is valid as an unassigned placeholder with a name" do
|
||||
allocation.filter_name = "Full stack Developer (DE-EN)"
|
||||
expect(allocation).to be_valid
|
||||
end
|
||||
|
||||
expect(allocation).to be_valid
|
||||
end
|
||||
|
||||
it "allows a real principal alongside a named filter (assigned placeholder)" do
|
||||
allocation.principal = owner
|
||||
allocation.user_filter = [filter]
|
||||
allocation.filter_name = "Full stack Developer (DE-EN)"
|
||||
|
||||
expect(allocation).to be_valid
|
||||
it "allows a real principal alongside a named filter (assigned placeholder)" do
|
||||
allocation.principal = owner
|
||||
allocation.filter_name = "Full stack Developer (DE-EN)"
|
||||
allocation.user_filter = [filter]
|
||||
expect(allocation).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -364,7 +397,8 @@ RSpec.describe ResourceAllocation do
|
||||
|
||||
allocation = create(:resource_allocation,
|
||||
entity: work_package,
|
||||
principal: owner,
|
||||
principal_explicit: false,
|
||||
principal: nil,
|
||||
filter_name: "Alices",
|
||||
user_filter: [filter])
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "ResourceAllocations requests",
|
||||
:skip_csrf,
|
||||
type: :rails_request do
|
||||
shared_let(:project) { create(:project, enabled_module_names: %w[resource_management work_package_tracking]) }
|
||||
shared_let(:user) do
|
||||
create(:user,
|
||||
member_with_permissions: { project => %i[view_resource_planners allocate_user_resources view_work_packages] })
|
||||
end
|
||||
shared_let(:assignee) { create(:user, member_with_permissions: { project => %i[view_work_packages] }) }
|
||||
shared_let(:work_package) { create(:work_package, project:) }
|
||||
|
||||
before { login_as user }
|
||||
|
||||
describe "GET new" do
|
||||
it "opens the dialog on the kind-selection step" do
|
||||
get new_project_resource_allocation_path(project), as: :turbo_stream
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('value="principal"')
|
||||
expect(response.body).to include('value="filter"')
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET step" do
|
||||
context "with allocation_kind=principal" do
|
||||
it "renders the allocation step with a user picker" do
|
||||
get step_project_resource_allocations_path(project, allocation_kind: "principal"), as: :turbo_stream
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
# Autocompleters render as Angular custom elements carrying the field
|
||||
# name in `data-input-name` rather than a plain `name` attribute.
|
||||
expect(response.body).to include("opce-user-autocompleter")
|
||||
expect(response.body).to include("resource_allocation[principal_id]")
|
||||
expect(response.body).to include("resource_allocation[entity_id]")
|
||||
expect(response.body).to include("resource_allocation[allocated_hours]")
|
||||
end
|
||||
end
|
||||
|
||||
context "with allocation_kind=filter" do
|
||||
it "renders the allocation step with a filter name and the filter form" do
|
||||
get step_project_resource_allocations_path(project, allocation_kind: "filter"), as: :turbo_stream
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include("resource_allocation[filter_name]")
|
||||
expect(response.body).to include('name="filters"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST create" do
|
||||
context "for an explicit user" do
|
||||
subject(:perform) do
|
||||
post project_resource_allocations_path(project),
|
||||
params: {
|
||||
allocation_kind: "principal",
|
||||
resource_allocation: {
|
||||
principal_id: assignee.id,
|
||||
entity_type: "WorkPackage",
|
||||
entity_id: work_package.id,
|
||||
start_date: "2026-03-02",
|
||||
end_date: "2026-03-03",
|
||||
allocated_hours: "40h"
|
||||
}
|
||||
},
|
||||
as: :turbo_stream
|
||||
end
|
||||
|
||||
it "creates a resource allocation for the principal" do
|
||||
expect { perform }.to change(ResourceAllocation, :count).by(1)
|
||||
|
||||
allocation = ResourceAllocation.last
|
||||
expect(allocation.entity).to eq(work_package)
|
||||
expect(allocation.principal).to eq(assignee)
|
||||
expect(allocation).to be_principal_explicit
|
||||
expect(allocation.allocated_time).to eq(40 * 60)
|
||||
expect(allocation.filter_name).to be_nil
|
||||
expect(allocation.user_filter).to eq([])
|
||||
expect(allocation.requested_by).to eq(user)
|
||||
end
|
||||
end
|
||||
|
||||
context "for a filter-criteria placeholder" do
|
||||
subject(:perform) do
|
||||
post project_resource_allocations_path(project),
|
||||
params: {
|
||||
allocation_kind: "filter",
|
||||
filters: [{ login: { operator: "~", values: ["dev"] } }].to_json,
|
||||
resource_allocation: {
|
||||
filter_name: "Full stack Developer (DE-EN)",
|
||||
entity_type: "WorkPackage",
|
||||
entity_id: work_package.id,
|
||||
start_date: "2026-03-02",
|
||||
end_date: "2026-03-03",
|
||||
allocated_hours: "40h"
|
||||
}
|
||||
},
|
||||
as: :turbo_stream
|
||||
end
|
||||
|
||||
it "creates a placeholder allocation carrying the user filter" do
|
||||
expect { perform }.to change(ResourceAllocation, :count).by(1)
|
||||
|
||||
allocation = ResourceAllocation.last
|
||||
expect(allocation.principal).to be_nil
|
||||
expect(allocation).not_to be_principal_explicit
|
||||
expect(allocation).to be_needs_principal_assignment
|
||||
expect(allocation.filter_name).to eq("Full stack Developer (DE-EN)")
|
||||
expect(allocation.user_filter.map(&:name)).to contain_exactly(:login)
|
||||
expect(allocation.user_filter.first.values).to eq(["dev"])
|
||||
end
|
||||
end
|
||||
|
||||
context "with invalid input" do
|
||||
subject(:perform) do
|
||||
post project_resource_allocations_path(project),
|
||||
params: {
|
||||
allocation_kind: "principal",
|
||||
resource_allocation: {
|
||||
principal_id: assignee.id,
|
||||
entity_type: "WorkPackage",
|
||||
entity_id: work_package.id,
|
||||
start_date: "2026-03-03",
|
||||
end_date: "2026-03-02", # before start_date
|
||||
allocated_hours: "40h"
|
||||
}
|
||||
},
|
||||
as: :turbo_stream
|
||||
end
|
||||
|
||||
it "does not create an allocation and re-renders the step" do
|
||||
expect { perform }.not_to change(ResourceAllocation, :count)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a work package the user cannot reach in this project" do
|
||||
shared_let(:other_work_package) { create(:work_package) }
|
||||
|
||||
subject(:perform) do
|
||||
post project_resource_allocations_path(project),
|
||||
params: {
|
||||
allocation_kind: "principal",
|
||||
resource_allocation: {
|
||||
principal_id: assignee.id,
|
||||
entity_type: "WorkPackage",
|
||||
entity_id: other_work_package.id,
|
||||
start_date: "2026-03-02",
|
||||
end_date: "2026-03-03",
|
||||
allocated_hours: "40h"
|
||||
}
|
||||
},
|
||||
as: :turbo_stream
|
||||
end
|
||||
|
||||
it "does not create an allocation and re-renders the step" do
|
||||
expect { perform }.not_to change(ResourceAllocation, :count)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an entity type outside the allow-list" do
|
||||
subject(:perform) do
|
||||
post project_resource_allocations_path(project),
|
||||
params: {
|
||||
allocation_kind: "principal",
|
||||
resource_allocation: {
|
||||
principal_id: assignee.id,
|
||||
entity_type: "Project",
|
||||
entity_id: project.id,
|
||||
start_date: "2026-03-02",
|
||||
end_date: "2026-03-03",
|
||||
allocated_hours: "40h"
|
||||
}
|
||||
},
|
||||
as: :turbo_stream
|
||||
end
|
||||
|
||||
it "does not create an allocation and re-renders the step" do
|
||||
expect { perform }.not_to change(ResourceAllocation, :count)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without the allocate_user_resources permission" do
|
||||
shared_let(:viewer) { create(:user, member_with_permissions: { project => %i[view_resource_planners] }) }
|
||||
|
||||
before { login_as viewer }
|
||||
|
||||
it "denies access to the new dialog" do
|
||||
get new_project_resource_allocation_path(project), as: :turbo_stream
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user