mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Merge branch 'release/17.0' into dev
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
<%#-- 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.
|
||||
|
||||
++#%>
|
||||
|
||||
<%=
|
||||
with_enterprise_banner_guard(:calculated_values, variant: :large, image: "enterprise/calculated-values.png") do
|
||||
component_wrapper do
|
||||
settings_primer_form_with(
|
||||
model:,
|
||||
scope: :custom_field,
|
||||
id: "custom_field_form",
|
||||
url: form_url,
|
||||
method: form_method
|
||||
) { |form| render CustomFields::CalculatedValues::DetailsForm.new(form) }
|
||||
end
|
||||
end
|
||||
%>
|
||||
@@ -1,16 +1,16 @@
|
||||
<%=
|
||||
with_enterprise_banner_guard(enterprise_addon[:key], variant: :large, image: enterprise_addon[:image]) do
|
||||
with_enterprise_banner_guard(enterprise_addon_key, image: enterprise_addon_image, inactive_guard: no_enterprise_feature?, variant: :large) do
|
||||
component_wrapper do
|
||||
flex_layout do |content|
|
||||
if persisted_cf_has_no_items_or_projects?
|
||||
if show_top_banner?
|
||||
content.with_row(mb: 3) do
|
||||
render Primer::Alpha::Banner.new(
|
||||
scheme: :default,
|
||||
icon: :info,
|
||||
dismiss_scheme: :hide,
|
||||
test_selector: "op-custom-fields--new-hierarchy-banner"
|
||||
test_selector: "op-custom-fields--top-banner"
|
||||
) do
|
||||
I18n.t("custom_fields.admin.notice.remember_items_and_projects")
|
||||
top_banner_text
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,15 +35,20 @@ module CustomFields
|
||||
include OpPrimer::ComponentHelpers
|
||||
include OpTurbo::Streamable
|
||||
|
||||
alias_method :custom_field, :model
|
||||
ENTERPRISE_GUARDED = {
|
||||
"calculated_value" => { key: :calculated_values, image: "enterprise/calculated-values.png" },
|
||||
"hierarchy" => { key: :custom_field_hierarchies, image: "enterprise/hierarchies.png" },
|
||||
"weighted_item_list" => { key: :weighted_item_lists, image: "enterprise/weighted_item_lists.png" }
|
||||
}.freeze
|
||||
|
||||
def persisted_cf_has_no_items_or_projects?
|
||||
custom_field.persisted? &&
|
||||
custom_field.hierarchical_list? &&
|
||||
custom_field.hierarchy_root.children.empty? &&
|
||||
custom_field.projects.empty?
|
||||
class << self
|
||||
def supported?(custom_field)
|
||||
custom_field.field_format.in?(%w[bool calculated_value hierarchy weighted_item_list])
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :custom_field, :model
|
||||
|
||||
def form_url
|
||||
model.new_record? ? custom_fields_path : custom_field_path(model)
|
||||
end
|
||||
@@ -52,15 +57,39 @@ module CustomFields
|
||||
model.new_record? ? :post : :put
|
||||
end
|
||||
|
||||
def enterprise_addon
|
||||
@enterprise_addon ||= case custom_field.field_format
|
||||
when "hierarchy"
|
||||
{ key: :custom_field_hierarchies, image: "enterprise/hierarchies.png" }
|
||||
when "weighted_item_list"
|
||||
{ key: :weighted_item_lists, image: "enterprise/weighted_item_lists.png" }
|
||||
else
|
||||
raise "Custom fields of format #{custom_field.field_format} are not supported by #{self.class.name}"
|
||||
end
|
||||
def enterprise_addon_key
|
||||
ENTERPRISE_GUARDED.dig(custom_field.field_format, :key)
|
||||
end
|
||||
|
||||
def enterprise_addon_image
|
||||
ENTERPRISE_GUARDED.dig(custom_field.field_format, :image)
|
||||
end
|
||||
|
||||
def no_enterprise_feature?
|
||||
ENTERPRISE_GUARDED[custom_field.field_format].nil?
|
||||
end
|
||||
|
||||
def show_top_banner?
|
||||
case custom_field.field_format
|
||||
when "hierarchy", "weighted_item_list"
|
||||
persisted_cf_has_no_items_or_projects?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def top_banner_text
|
||||
case custom_field.field_format
|
||||
when "hierarchy", "weighted_item_list"
|
||||
I18n.t("custom_fields.admin.notice.remember_items_and_projects")
|
||||
end
|
||||
end
|
||||
|
||||
def persisted_cf_has_no_items_or_projects?
|
||||
custom_field.persisted? &&
|
||||
custom_field.hierarchical_list? &&
|
||||
custom_field.hierarchy_root.children.empty? &&
|
||||
custom_field.projects.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+7
-1
@@ -16,6 +16,13 @@
|
||||
helpers.label_for_custom_field_format(@project_custom_field.field_format)
|
||||
end
|
||||
end
|
||||
if @project_custom_field.is_for_all?
|
||||
title_container.with_column(pt: 1, mr: 2) do
|
||||
render(Primer::Beta::Text.new(font_size: :small)) do
|
||||
t("settings.project_attributes.label_for_all_projects")
|
||||
end
|
||||
end
|
||||
end
|
||||
if @project_custom_field.required?
|
||||
title_container.with_column(pt: 1) do
|
||||
render(Primer::Beta::Label.new(scheme: :attention, size: :medium)) do
|
||||
@@ -41,4 +48,3 @@
|
||||
end
|
||||
end
|
||||
%>
|
||||
|
||||
|
||||
+4
@@ -57,6 +57,10 @@ module Projects
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_enabled?
|
||||
!@project_custom_field.required?
|
||||
end
|
||||
|
||||
def toggle_data_attributes
|
||||
{
|
||||
"turbo-method": :post,
|
||||
|
||||
+7
@@ -16,6 +16,13 @@
|
||||
helpers.label_for_custom_field_format(@project_custom_field.field_format)
|
||||
end
|
||||
end
|
||||
if @project_custom_field.is_for_all?
|
||||
title_container.with_column(pt: 1, mr: 2) do
|
||||
render(Primer::Beta::Text.new(font_size: :small)) do
|
||||
t("settings.project_attributes.label_for_all_projects")
|
||||
end
|
||||
end
|
||||
end
|
||||
if @project_custom_field.required?
|
||||
title_container.with_column(pt: 1) do
|
||||
render(Primer::Beta::Label.new(scheme: :attention, size: :medium)) do
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ module Projects
|
||||
end
|
||||
|
||||
def toggle_enabled?
|
||||
!@project_custom_field.required?
|
||||
!@project_custom_field.is_for_all?
|
||||
end
|
||||
|
||||
def toggle_data_attributes
|
||||
|
||||
+5
-4
@@ -24,10 +24,11 @@
|
||||
end
|
||||
content_container.with_column(mr: 2) do
|
||||
render(Primer::Beta::Text.new(font_size: :small)) do
|
||||
t(
|
||||
"project.count",
|
||||
count: @project_custom_field.project_custom_field_project_mappings.size
|
||||
)
|
||||
if @project_custom_field.is_for_all?
|
||||
t("settings.project_attributes.label_for_all_projects")
|
||||
else
|
||||
t("project.count", count: @project_custom_field.project_custom_field_project_mappings.size)
|
||||
end
|
||||
end
|
||||
end
|
||||
if @project_custom_field.required?
|
||||
|
||||
-4
@@ -32,10 +32,6 @@ module Settings
|
||||
module ProjectCustomFields
|
||||
module ProjectCustomFieldMapping
|
||||
class NewProjectMappingComponent < Admin::CustomFields::CustomFieldProjects::NewCustomFieldProjectsModalComponent
|
||||
def render?
|
||||
!custom_field.required?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def form_modal_component
|
||||
|
||||
@@ -37,7 +37,9 @@ module CustomFields
|
||||
attribute :field_format
|
||||
attribute :is_filter
|
||||
attribute :is_for_all
|
||||
attribute :is_required
|
||||
attribute :is_required do
|
||||
validate_non_true_for_some_formats
|
||||
end
|
||||
attribute :max_length
|
||||
attribute :min_length
|
||||
attribute :name
|
||||
@@ -47,10 +49,15 @@ module CustomFields
|
||||
attribute :searchable
|
||||
attribute :admin_only
|
||||
attribute :default_value
|
||||
attribute :possible_values
|
||||
attribute :multi_value
|
||||
attribute :content_right_to_left
|
||||
attribute :custom_field_section_id
|
||||
attribute :allow_non_open_versions
|
||||
|
||||
def validate_non_true_for_some_formats
|
||||
return unless %w[bool calculated_value].include?(field_format)
|
||||
|
||||
errors.add(:is_required, :cannot_be_true) if is_required == true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,8 +34,8 @@ module ProjectCustomFieldProjectMappings
|
||||
attribute :custom_field_id
|
||||
|
||||
validate :select_project_custom_fields_permission
|
||||
validate :not_required
|
||||
validate :visbile_to_user
|
||||
validate :not_for_all_projects
|
||||
validate :visible_to_user
|
||||
|
||||
def select_project_custom_fields_permission
|
||||
return if user.allowed_in_project?(:select_project_custom_fields, model.project)
|
||||
@@ -43,15 +43,15 @@ module ProjectCustomFieldProjectMappings
|
||||
errors.add :base, :error_unauthorized
|
||||
end
|
||||
|
||||
def not_required
|
||||
# only mappings of custom fields which are not required can be manipulated by the user
|
||||
# enabling a custom field which is required happens in an after_save hook within the custom field model itself
|
||||
return if model.project_custom_field.nil? || !model.project_custom_field.required?
|
||||
def not_for_all_projects
|
||||
# only mappings of custom fields which are not activated for all projects can be manipulated by the user
|
||||
# enabling a custom field which is force-active happens in an after_save hook within the custom field model itself
|
||||
return if model.project_custom_field.nil? || !model.project_custom_field.is_for_all?
|
||||
|
||||
errors.add :custom_field_id, :cannot_delete_mapping
|
||||
end
|
||||
|
||||
def visbile_to_user
|
||||
def visible_to_user
|
||||
# "invisible" custom fields can only be seen and edited by admins
|
||||
# using visible scope to check if the custom field is actually visible to the user
|
||||
return if model.project_custom_field.nil? ||
|
||||
|
||||
@@ -39,7 +39,6 @@ module WorkPackageTypes
|
||||
attribute :project_ids
|
||||
attribute :attribute_groups
|
||||
|
||||
validates :name, presence: true, length: { maximum: 255 }
|
||||
validates :is_default, :is_milestone, :is_in_roadmap, inclusion: { in: [true, false] }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
# 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 CustomFields
|
||||
module CalculatedValues
|
||||
class DetailsForm < ApplicationForm
|
||||
form do |details_form|
|
||||
if model.new_record?
|
||||
details_form.hidden(name: :field_format)
|
||||
details_form.hidden(name: :type, scope_name_to_model: false)
|
||||
end
|
||||
|
||||
details_form.text_field(
|
||||
name: :name,
|
||||
label: I18n.t(:label_name),
|
||||
required: true
|
||||
)
|
||||
|
||||
details_form.select_list(
|
||||
name: :custom_field_section_id,
|
||||
label: I18n.t("activerecord.attributes.project_custom_field.custom_field_section"),
|
||||
required: true
|
||||
) do |li|
|
||||
ProjectCustomFieldSection.find_each do |cs|
|
||||
li.option(value: cs.id, label: cs.name)
|
||||
end
|
||||
end
|
||||
|
||||
details_form.pattern_input(
|
||||
name: :formula,
|
||||
value: model.formula_string,
|
||||
suggestions: formula_suggestions,
|
||||
label: I18n.t(:label_formula),
|
||||
required: true,
|
||||
caption: I18n.t("custom_fields.instructions.formula")
|
||||
)
|
||||
|
||||
details_form.check_box(
|
||||
name: :is_required,
|
||||
label: I18n.t("activerecord.attributes.project_custom_field.is_required"),
|
||||
caption: I18n.t("custom_fields.instructions.is_required_for_project")
|
||||
)
|
||||
|
||||
details_form.check_box(
|
||||
name: :admin_only,
|
||||
label: I18n.t("activerecord.attributes.custom_field.admin_only"),
|
||||
caption: I18n.t("custom_fields.instructions.admin_only")
|
||||
)
|
||||
|
||||
details_form.submit(name: :submit, label: I18n.t(:button_save), scheme: :primary)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def formula_suggestions
|
||||
operators = CustomField::CalculatedValue::MATH_OPERATORS_FOR_FORMULA
|
||||
# Hide % from the suggestions as it can be used as either modulo or percentage.
|
||||
.reject { it == "%" }
|
||||
.map do |op|
|
||||
# Insert operators as plain text nodes instead of tokens, since displaying them as tokens would result
|
||||
# in too much visual clutter. We still want to offer autocompletion for them.
|
||||
{ key: op, label: op, insert_as_text: true, enabled: true }
|
||||
end
|
||||
|
||||
custom_fields = model.usable_custom_field_references_for_formula.map do |cf|
|
||||
{ key: "cf_#{cf.id}", label: cf.name, enabled: true }
|
||||
end
|
||||
|
||||
{
|
||||
custom_fields: { title: I18n.t("label_custom_field_plural"), tokens: custom_fields },
|
||||
operators: { title: I18n.t("label_mathematical_operators"), tokens: operators }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -48,7 +48,7 @@ module CustomFields
|
||||
required: true
|
||||
)
|
||||
|
||||
if model.is_a?(ProjectCustomField)
|
||||
if show_section_field?
|
||||
details_form.select_list(
|
||||
name: :custom_field_section_id,
|
||||
label: I18n.t("activerecord.attributes.project_custom_field.custom_field_section"),
|
||||
@@ -60,33 +60,143 @@ module CustomFields
|
||||
end
|
||||
end
|
||||
|
||||
if model.multi_value_possible?
|
||||
if show_multi_value_field?
|
||||
details_form.check_box(
|
||||
name: :multi_value,
|
||||
label: I18n.t("activerecord.attributes.custom_field.multi_value"),
|
||||
caption: I18n.t("custom_fields.instructions.multi_select")
|
||||
label: label(:multi_value),
|
||||
caption: instructions(:multi_select)
|
||||
)
|
||||
end
|
||||
|
||||
details_form.check_box(
|
||||
name: :is_required,
|
||||
label: I18n.t("activerecord.attributes.custom_field.is_required"),
|
||||
caption: I18n.t("custom_fields.instructions.is_required")
|
||||
)
|
||||
if show_formula_field?
|
||||
details_form.pattern_input(
|
||||
name: :formula,
|
||||
value: model.formula_string,
|
||||
suggestions: formula_suggestions,
|
||||
label: I18n.t(:label_formula),
|
||||
required: true,
|
||||
caption: instructions(:formula)
|
||||
)
|
||||
end
|
||||
|
||||
details_form.check_box(
|
||||
name: :is_for_all,
|
||||
label: I18n.t("activerecord.attributes.custom_field.is_for_all"),
|
||||
caption: I18n.t("custom_fields.instructions.is_for_all")
|
||||
)
|
||||
if show_default_bool_field?
|
||||
details_form.check_box(
|
||||
name: :default_value,
|
||||
label: label(:default_value)
|
||||
)
|
||||
end
|
||||
|
||||
details_form.check_box(
|
||||
name: :is_filter,
|
||||
label: I18n.t("activerecord.attributes.custom_field.is_filter"),
|
||||
caption: I18n.t("custom_fields.instructions.is_filter")
|
||||
)
|
||||
if show_is_required_field?
|
||||
details_form.check_box(
|
||||
name: :is_required,
|
||||
label: label(:is_required),
|
||||
caption: instructions(:is_required)
|
||||
)
|
||||
end
|
||||
|
||||
if show_is_for_all_field?
|
||||
details_form.check_box(
|
||||
name: :is_for_all,
|
||||
label: label(:is_for_all),
|
||||
caption: instructions(:is_for_all)
|
||||
)
|
||||
end
|
||||
|
||||
if show_is_filter_field?
|
||||
details_form.check_box(
|
||||
name: :is_filter,
|
||||
label: label(:is_filter),
|
||||
caption: instructions(:is_filter)
|
||||
)
|
||||
end
|
||||
|
||||
if show_admin_only_field?
|
||||
details_form.check_box(
|
||||
name: :admin_only,
|
||||
label: label(:admin_only),
|
||||
caption: instructions(:admin_only)
|
||||
)
|
||||
end
|
||||
|
||||
if show_editable_field?
|
||||
details_form.check_box(
|
||||
name: :editable,
|
||||
label: label(:editable),
|
||||
caption: instructions(:editable)
|
||||
)
|
||||
end
|
||||
|
||||
details_form.submit(name: :submit, label: I18n.t(:button_save), scheme: :default)
|
||||
end
|
||||
|
||||
def label(field)
|
||||
I18n.t("activerecord.attributes.custom_field.#{field}")
|
||||
end
|
||||
|
||||
def instructions(field)
|
||||
key = if model.is_a?(ProjectCustomField)
|
||||
"custom_fields.instructions.#{field}.project"
|
||||
else
|
||||
"custom_fields.instructions.#{field}.all"
|
||||
end
|
||||
|
||||
I18n.t(key)
|
||||
end
|
||||
|
||||
def show_section_field?
|
||||
model.is_a?(ProjectCustomField)
|
||||
end
|
||||
|
||||
def show_default_bool_field?
|
||||
%w[bool].include?(model.field_format)
|
||||
end
|
||||
|
||||
def show_is_required_field?
|
||||
%w[calculated_value bool].exclude?(model.field_format)
|
||||
end
|
||||
|
||||
def show_multi_value_field?
|
||||
model.multi_value_possible?
|
||||
end
|
||||
|
||||
def show_formula_field?
|
||||
%w[calculated_value].include?(model.field_format)
|
||||
end
|
||||
|
||||
def show_is_for_all_field?
|
||||
model.is_a?(WorkPackageCustomField) || model.is_a?(ProjectCustomField)
|
||||
end
|
||||
|
||||
def show_is_filter_field?
|
||||
model.is_a?(WorkPackageCustomField)
|
||||
end
|
||||
|
||||
def show_admin_only_field?
|
||||
model.is_a?(ProjectCustomField) || model.is_a?(UserCustomField)
|
||||
end
|
||||
|
||||
def show_editable_field?
|
||||
model.is_a?(UserCustomField)
|
||||
end
|
||||
|
||||
def formula_suggestions
|
||||
operators = CustomField::CalculatedValue::MATH_OPERATORS_FOR_FORMULA
|
||||
# Hide % from the suggestions as it can be used as either modulo or percentage.
|
||||
.reject { it == "%" }
|
||||
.map do |op|
|
||||
# Insert operators as plain text nodes instead of tokens, since displaying them as tokens would result
|
||||
# in too much visual clutter. We still want to offer autocompletion for them.
|
||||
{ key: op, label: op, insert_as_text: true, enabled: true }
|
||||
end
|
||||
|
||||
custom_fields = model.usable_custom_field_references_for_formula.map do |cf|
|
||||
{ key: "cf_#{cf.id}", label: cf.name, enabled: true }
|
||||
end
|
||||
|
||||
{
|
||||
custom_fields: { title: I18n.t("label_custom_field_plural"), tokens: custom_fields },
|
||||
operators: { title: I18n.t("label_mathematical_operators"), tokens: operators }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,9 +43,14 @@ module Projects
|
||||
private
|
||||
|
||||
def custom_fields
|
||||
@custom_fields ||= model
|
||||
.available_custom_fields
|
||||
.required
|
||||
@custom_fields ||= begin
|
||||
enabled_custom_fields = model.enabled_custom_field_ids.presence || ProjectCustomField.for_all.select(:id)
|
||||
|
||||
model
|
||||
.available_custom_fields
|
||||
.where(id: enabled_custom_fields)
|
||||
.required
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,9 +32,23 @@ module EnterpriseHelper
|
||||
##
|
||||
# Renders the enterprise banner component with a guard for the given feature key.
|
||||
# If the feature is not enabled, it will not render the given block.
|
||||
def with_enterprise_banner_guard(feature_key, **args)
|
||||
concat(render(EnterpriseEdition::BannerComponent.new(feature_key, **args)))
|
||||
yield if EnterpriseToken.allows_to?(feature_key)
|
||||
#
|
||||
# Parameters:
|
||||
# - feature_key: The key that identifies the specific enterprise feature.
|
||||
# - inactive_guard: A boolean flag determining whether the guard should be active
|
||||
# or bypassed. If set to `true`, the guard is bypassed and only the block is executed.
|
||||
# Defaults to `false`.
|
||||
# - **args: Additional keyword arguments to be passed to the banner component.
|
||||
#
|
||||
# Yields:
|
||||
# - Executes the provided block within the guard's context.
|
||||
def with_enterprise_banner_guard(feature_key, inactive_guard: false, **args)
|
||||
if inactive_guard
|
||||
yield
|
||||
else
|
||||
concat(render(EnterpriseEdition::BannerComponent.new(feature_key, **args)))
|
||||
yield if EnterpriseToken.allows_to?(feature_key)
|
||||
end
|
||||
end
|
||||
|
||||
def enterprise_angular_trial_inputs
|
||||
|
||||
@@ -46,6 +46,7 @@ class CustomValue < ApplicationRecord
|
||||
delegate :editable?,
|
||||
:admin_only?,
|
||||
:required?,
|
||||
:is_for_all?,
|
||||
:max_length,
|
||||
:min_length,
|
||||
:calculated_value?,
|
||||
|
||||
@@ -37,7 +37,7 @@ class ProjectCustomField < CustomField
|
||||
|
||||
acts_as_list column: :position_in_custom_field_section, scope: [:custom_field_section_id]
|
||||
|
||||
after_save :activate_required_field_in_all_projects, if: :required?
|
||||
after_save :activate_required_field_in_all_projects, if: :is_for_all?
|
||||
|
||||
validates :custom_field_section_id, presence: true
|
||||
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ class Role < ApplicationRecord
|
||||
validates :name,
|
||||
presence: true,
|
||||
length: { maximum: 256 },
|
||||
uniqueness: { case_sensitive: true }
|
||||
uniqueness: { case_sensitive: false }
|
||||
|
||||
# Turn this class into an abstract one by validating the STI column.
|
||||
validates :type,
|
||||
|
||||
+4
-1
@@ -61,7 +61,10 @@ class Type < ApplicationRecord
|
||||
|
||||
acts_as_list
|
||||
|
||||
validates :name, uniqueness: { case_sensitive: false }
|
||||
validates :name,
|
||||
presence: true,
|
||||
uniqueness: { case_sensitive: false },
|
||||
length: { maximum: 255 }
|
||||
|
||||
scopes :milestone
|
||||
|
||||
|
||||
@@ -84,11 +84,11 @@ module ProjectCustomFieldProjectMappings
|
||||
end
|
||||
|
||||
def fetch_custom_field_ids
|
||||
# only custom fields which are not set to required can be disabled
|
||||
# only custom fields which are not set "for all projects" can be disabled
|
||||
ProjectCustomField
|
||||
.visible(@user)
|
||||
.where(custom_field_section_id: @project_custom_field_section.id)
|
||||
.where(is_required: false)
|
||||
.where(is_for_all: false)
|
||||
.pluck(:id)
|
||||
end
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ module Projects::Concerns
|
||||
# although the user explicitly provided a blank value. In order to not patch `acts_as_customizable`
|
||||
# further, we simply identify these custom values and deactivate the custom field.
|
||||
|
||||
custom_field_ids = new_project.custom_values.select { |cv| cv.value.blank? && !cv.required? }.pluck(:custom_field_id)
|
||||
custom_field_ids = new_project.custom_values.select { |cv| cv.value.blank? && !cv.is_for_all? }.pluck(:custom_field_id)
|
||||
custom_field_project_mappings = new_project.project_custom_field_project_mappings
|
||||
|
||||
custom_field_project_mappings
|
||||
@@ -104,11 +104,11 @@ module Projects::Concerns
|
||||
end
|
||||
|
||||
def build_missing_project_custom_field_project_mappings(project)
|
||||
# Activate all custom fields (via mapping table) that are required or
|
||||
# have a value provided by the user, but no mapping exists.
|
||||
# Activate all custom fields (via mapping table) that have no mapping, but are either
|
||||
# intended for all projects, or have a value provided by the user.
|
||||
|
||||
custom_field_ids = project.custom_values
|
||||
.select { |cv| cv.value? || cv.required? }
|
||||
.select { |cv| cv.value? || cv.is_for_all? }
|
||||
.pluck(:custom_field_id).uniq
|
||||
activated_custom_field_ids = project.project_custom_field_project_mappings.pluck(:custom_field_id).uniq
|
||||
|
||||
|
||||
@@ -38,13 +38,11 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
)
|
||||
%>
|
||||
|
||||
<%= error_messages_for "custom_field" %>
|
||||
|
||||
<% if @custom_field.field_format_calculated_value? %>
|
||||
<%= render Admin::CustomFields::CalculatedValues::DetailsComponent.new(@custom_field) %>
|
||||
<% elsif @custom_field.hierarchical_list? %>
|
||||
<% if CustomFields::DetailsComponent.supported?(@custom_field) %>
|
||||
<%= render CustomFields::DetailsComponent.new(@custom_field) %>
|
||||
<% else %>
|
||||
<%= error_messages_for "custom_field" %>
|
||||
|
||||
<% content_controller "admin--custom-fields",
|
||||
"admin--custom-fields-format-value": @custom_field.field_format,
|
||||
"admin--custom-fields-format-config-value": OpenProject::CustomFieldFormatDependent.stimulus_config %>
|
||||
|
||||
@@ -31,13 +31,11 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
<%= render(Settings::ProjectCustomFields::NewFormHeaderComponent.new(@custom_field)) %>
|
||||
|
||||
<%= error_messages_for "custom_field" %>
|
||||
|
||||
<% if @custom_field.field_format_calculated_value? %>
|
||||
<%= render Admin::CustomFields::CalculatedValues::DetailsComponent.new(@custom_field) %>
|
||||
<% elsif @custom_field.hierarchical_list? %>
|
||||
<% if CustomFields::DetailsComponent.supported?(@custom_field) %>
|
||||
<%= render CustomFields::DetailsComponent.new(@custom_field) %>
|
||||
<% else %>
|
||||
<%= error_messages_for "custom_field" %>
|
||||
|
||||
<% content_controller "admin--custom-fields",
|
||||
"admin--custom-fields-format-value": @custom_field.field_format,
|
||||
"admin--custom-fields-format-config-value": OpenProject::CustomFieldFormatDependent.stimulus_config %>
|
||||
|
||||
@@ -37,14 +37,16 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
%>
|
||||
|
||||
<%=
|
||||
unless @custom_field.required?
|
||||
unless @custom_field.is_for_all?
|
||||
render(Primer::OpenProject::SubHeader.new(test_selector: "add-projects-sub-header")) do |component|
|
||||
component.with_action_button(scheme: :primary,
|
||||
leading_icon: :"op-include-projects",
|
||||
label: I18n.t(:label_add_projects),
|
||||
tag: :a,
|
||||
href: new_link_admin_settings_project_custom_field_path(@custom_field),
|
||||
data: { controller: "async-dialog" }) do
|
||||
component.with_action_button(
|
||||
scheme: :primary,
|
||||
leading_icon: :"op-include-projects",
|
||||
label: I18n.t(:label_add_projects),
|
||||
tag: :a,
|
||||
href: new_link_admin_settings_project_custom_field_path(@custom_field),
|
||||
data: { controller: "async-dialog" }
|
||||
) do
|
||||
I18n.t(:label_add_projects)
|
||||
end
|
||||
end
|
||||
@@ -52,10 +54,10 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
%>
|
||||
|
||||
<%=
|
||||
if @custom_field.required?
|
||||
if @custom_field.is_for_all?
|
||||
render Primer::OpenProject::FeedbackMessage.new(icon_arguments: { icon: :checklist }, border: true) do |component|
|
||||
component.with_heading(tag: :h2).with_content(I18n.t("projects.settings.project_custom_fields.is_required_blank_slate.heading"))
|
||||
component.with_description { I18n.t("projects.settings.project_custom_fields.is_required_blank_slate.description") }
|
||||
component.with_heading(tag: :h2).with_content(I18n.t("projects.settings.project_custom_fields.is_for_all_blank_slate.heading"))
|
||||
component.with_description { I18n.t("projects.settings.project_custom_fields.is_for_all_blank_slate.description") }
|
||||
end
|
||||
else
|
||||
render(
|
||||
|
||||
@@ -81,7 +81,13 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<%= f.check_box :multi_value,
|
||||
data: { action: "admin--custom-fields#checkOnlyOne" } %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.multi_select") %></p>
|
||||
<p>
|
||||
<% if @custom_field.is_a?(ProjectCustomField) %>
|
||||
<%= t("custom_fields.instructions.multi_select.project") %>
|
||||
<% else %>
|
||||
<%= t("custom_fields.instructions.multi_select.all") %>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -158,25 +164,25 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<div class="form--field">
|
||||
<%= f.check_box :is_required %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.is_required") %></p>
|
||||
<p><%= t("custom_fields.instructions.is_required.all") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field">
|
||||
<%= f.check_box :is_for_all %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.is_for_all") %></p>
|
||||
<p><%= t("custom_fields.instructions.is_for_all.all") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field">
|
||||
<%= f.check_box :is_filter %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.is_filter") %></p>
|
||||
<p><%= t("custom_fields.instructions.is_filter.all") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field" <%= format_dependent.attributes(:searchable) %>>
|
||||
<%= f.check_box :searchable %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.searchable") %></p>
|
||||
<p><%= t("custom_fields.instructions.searchable.all") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field" <%= format_dependent.attributes(:textOrientation) %>>
|
||||
@@ -186,52 +192,58 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<div class="form--field">
|
||||
<%= f.check_box :is_required %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.is_required") %></p>
|
||||
<p><%= t("custom_fields.instructions.is_required.all") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field">
|
||||
<%= f.check_box :admin_only %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.admin_only") %></p>
|
||||
<p><%= t("custom_fields.instructions.admin_only.all") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field">
|
||||
<%= f.check_box :editable %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.editable") %></p>
|
||||
<p><%= t("custom_fields.instructions.editable.all") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% when "ProjectCustomField" %>
|
||||
<div class="form--field">
|
||||
<%= f.check_box :is_required %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.is_required_for_project") %></p>
|
||||
<p><%= t("custom_fields.instructions.is_required.project") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field">
|
||||
<%= f.check_box :is_for_all %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.is_for_all.project") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field">
|
||||
<%= f.check_box :admin_only %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.admin_only") %></p>
|
||||
<p><%= t("custom_fields.instructions.admin_only.project") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field" <%= format_dependent.attributes(:searchable) %>>
|
||||
<%= f.check_box :searchable %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.searchable_for_project") %></p>
|
||||
<p><%= t("custom_fields.instructions.searchable.project") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% when "TimeEntryCustomField" %>
|
||||
<div class="form--field">
|
||||
<%= f.check_box :is_required %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.is_required") %></p>
|
||||
<p><%= t("custom_fields.instructions.is_required.all") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="form--field">
|
||||
<%= f.check_box :is_required %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t("custom_fields.instructions.is_required") %></p>
|
||||
<p><%= t("custom_fields.instructions.is_required.all") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -31,7 +31,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
<%= render(Admin::CustomFields::EditFormHeaderComponent.new(custom_field: @custom_field, selected: :edit)) %>
|
||||
|
||||
<% if @custom_field.hierarchical_list? %>
|
||||
<% if CustomFields::DetailsComponent.supported?(@custom_field) %>
|
||||
<%= render CustomFields::DetailsComponent.new(@custom_field) %>
|
||||
<% else %>
|
||||
<%= error_messages_for "custom_field" %>
|
||||
|
||||
@@ -42,7 +42,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
end
|
||||
%>
|
||||
|
||||
<% if @custom_field.hierarchical_list? %>
|
||||
<% if CustomFields::DetailsComponent.supported?(@custom_field) %>
|
||||
<%= render CustomFields::DetailsComponent.new(@custom_field) %>
|
||||
<% else %>
|
||||
<%= error_messages_for "custom_field" %>
|
||||
|
||||
@@ -42,8 +42,8 @@ Rails.application.configure do
|
||||
# Skip transaction that may be blocked
|
||||
next if data[:sql].match?(/BEGIN|COMMIT/)
|
||||
|
||||
# Skip tenant creation (load dump)
|
||||
next if data[:sql][..120].include?("Dumped by pg_dump")
|
||||
# Skip tenant creation (load dump sql which is around 300kB)
|
||||
next if data[:sql][..120].include?("Dumped by pg_dump") || data[:sql].size > 200_000
|
||||
|
||||
# Skip smaller durations
|
||||
duration = ((finish - start) * 1000).round(4)
|
||||
|
||||
+30
-16
@@ -377,18 +377,29 @@ en:
|
||||
calculated_field_not_editable: "Non-editable attribute. This value is calculated automatically."
|
||||
no_role_assigment: "No role assignment"
|
||||
instructions:
|
||||
is_required: "Mark the custom field as required. This will make it mandatory to fill in the field when creating new resources. Existing resources will not require a value when being updated."
|
||||
is_required_for_project: "Check to enable this attribute and make it required in all projects. It cannot be deactived for individual projects. Existing projects will not require a value when being updated."
|
||||
is_for_all: "Mark the custom field as available in all existing and new projects."
|
||||
multi_select: "Allows the user to assign multiple values to this custom field."
|
||||
searchable: "Include the field values when using the global search functionality."
|
||||
searchable_for_project: "Check to make this attribute available as a filter in project lists."
|
||||
editable: "Allow the field to be editable by users themselves."
|
||||
admin_only: "Check to make this attribute only visible to administrators. Users without admin rights will not be able to view or edit it."
|
||||
is_filter: >
|
||||
Allow the custom field to be used in a filter in work package views.
|
||||
Note that only with 'For all projects' selected, the custom field will show up in global views.
|
||||
formula: "Add numeric values or type / to search for an attribute or a mathematical operator."
|
||||
is_required:
|
||||
all: "Mark the custom field as required. This will make it mandatory to fill in the field when creating new resources. Existing resources will not require a value when being updated."
|
||||
project: "Check to enable this attribute and make it required in all projects. It cannot be deactivated for individual projects. Existing projects will not require a value when being updated."
|
||||
is_for_all:
|
||||
all: "Mark the custom field as available in all existing and new projects."
|
||||
project: "Mark the attribute as available in all existing and new projects."
|
||||
multi_select:
|
||||
all: "Allows the user to assign multiple values to this custom field."
|
||||
project: "Allows the user to assign multiple values to this attribute."
|
||||
searchable:
|
||||
all: "Include the field values when using the global search functionality."
|
||||
project: "Check to make this attribute available as a filter in project lists."
|
||||
editable:
|
||||
all: "Allow the field to be editable by users themselves."
|
||||
admin_only:
|
||||
all: "Check to make this custom field only visible to administrators. Users without admin rights will not be able to view or edit it."
|
||||
project: "Check to make this attribute only visible to administrators. Users without admin rights will not be able to view or edit it."
|
||||
is_filter:
|
||||
all: >
|
||||
Allow the custom field to be used in a filter in work package views.
|
||||
Note that only with 'For all projects' selected, the custom field will show up in global views.
|
||||
formula:
|
||||
project: "Add numeric values or type / to search for an attribute or a mathematical operator."
|
||||
|
||||
tab:
|
||||
no_results_title_text: There are currently no custom fields.
|
||||
@@ -625,9 +636,9 @@ en:
|
||||
label_enable_single: "Active in this project, click to disable"
|
||||
label_disable_single: "Inactive in this project, click to enable"
|
||||
remove_from_project: "Remove from project"
|
||||
is_required_blank_slate:
|
||||
heading: Required in all projects
|
||||
description: This project attribute is activated in all projects since the "Required in all projects" option is checked. It cannot be deactivated for individual projects.
|
||||
is_for_all_blank_slate:
|
||||
heading: For all projects
|
||||
description: This project attribute is enabled in all projects since the "For all projects" option is checked. It cannot be deactivated for individual projects.
|
||||
types:
|
||||
no_results_title_text: There are currently no types available.
|
||||
form:
|
||||
@@ -1363,7 +1374,7 @@ en:
|
||||
work_packages: "Work Packages"
|
||||
workspace_type: "Workspace type"
|
||||
project_custom_field:
|
||||
is_required: "Required for all projects"
|
||||
is_required: "Required"
|
||||
custom_field_section: Section
|
||||
subproject_template_assignment:
|
||||
workspace_type: "Workspace type"
|
||||
@@ -1652,6 +1663,8 @@ en:
|
||||
invalid_characters: "Only numeric values, mathematical operators and project attributes of type integer, float, calculated value and weighted list are allowed."
|
||||
not_allowed_custom_fields_referenced: "The attribute %{custom_fields} cannot be used because it leads to a circular reference; one attribute depends on the other."
|
||||
format: "%{message}"
|
||||
required:
|
||||
cannot_be_true: "cannot be set to true."
|
||||
custom_fields_project:
|
||||
attributes:
|
||||
project_ids:
|
||||
@@ -4737,6 +4750,7 @@ en:
|
||||
passwords: "Passwords"
|
||||
project_attributes:
|
||||
heading: "Project attributes"
|
||||
label_for_all_projects: "All projects"
|
||||
label_new_attribute: "Project attribute"
|
||||
label_new_section: "Section"
|
||||
label_edit_section: "Edit title"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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 Rails.root.join("db/migrate/migration_utils/utils")
|
||||
|
||||
class SetIsForAllAndUnsetRequired < ActiveRecord::Migration[8.0]
|
||||
include Migration::Utils
|
||||
|
||||
def up
|
||||
# With WP-69399, project custom fields support both required and is_for_all as separate flags.
|
||||
# Before, there was only is_required, which implied is_for_all.
|
||||
#
|
||||
# Take all project custom fields that are required and set is_for_all to true:
|
||||
ProjectCustomField
|
||||
.where(is_required: true)
|
||||
.update_all(is_for_all: true)
|
||||
|
||||
# Additionally, bool and calculated value can no longer be required.
|
||||
|
||||
CustomField
|
||||
.where(field_format: %w(bool calculated_value))
|
||||
.update_all(is_required: false)
|
||||
end
|
||||
|
||||
def down
|
||||
# Down migration can only partly reconstruct the data
|
||||
ProjectCustomField
|
||||
.update_all(is_for_all: false)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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 AddUniquenessForStatusNames < ActiveRecord::Migration[8.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
execute <<~SQL.squish
|
||||
UPDATE statuses SET name = statuses.name || ' ' || counter.rn
|
||||
FROM (SELECT id, row_number() OVER (PARTITION BY LOWER(name) ORDER BY id) AS rn FROM statuses) AS counter
|
||||
WHERE statuses.id = counter.id AND counter.rn > 1;
|
||||
SQL
|
||||
|
||||
add_index :statuses, "LOWER(name)", unique: true, algorithm: :concurrently
|
||||
end
|
||||
|
||||
def down
|
||||
remove_index :statuses, column: "LOWER(name)", algorithm: :concurrently
|
||||
end
|
||||
end
|
||||
+12
-19
@@ -27,28 +27,21 @@
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
#
|
||||
module Admin::CustomFields::CalculatedValues
|
||||
class DetailsComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
include EnterpriseHelper
|
||||
include OpPrimer::ComponentHelpers
|
||||
include OpTurbo::Streamable
|
||||
|
||||
alias_method :custom_field, :model
|
||||
class AddUniquenessForRoleNames < ActiveRecord::Migration[8.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
private
|
||||
def up
|
||||
execute <<~SQL.squish
|
||||
UPDATE roles SET name = roles.name || ' ' || counter.rn
|
||||
FROM (SELECT id, row_number() OVER (PARTITION BY LOWER(name) ORDER BY id) AS rn FROM roles) AS counter
|
||||
WHERE roles.id = counter.id AND counter.rn > 1;
|
||||
SQL
|
||||
|
||||
def form_url
|
||||
if custom_field.new_record?
|
||||
admin_settings_project_custom_fields_path
|
||||
else
|
||||
admin_settings_project_custom_field_path(custom_field)
|
||||
end
|
||||
end
|
||||
add_index :roles, "LOWER(name)", unique: true, algorithm: :concurrently
|
||||
end
|
||||
|
||||
def form_method
|
||||
custom_field.new_record? ? :post : :put
|
||||
end
|
||||
def down
|
||||
remove_index :roles, column: "LOWER(name)", algorithm: :concurrently
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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 AddUniquenessForTypeNames < ActiveRecord::Migration[8.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
execute <<~SQL.squish
|
||||
UPDATE types SET name = types.name || ' ' || counter.rn
|
||||
FROM (SELECT id, row_number() OVER (PARTITION BY LOWER(name) ORDER BY id) AS rn FROM types) AS counter
|
||||
WHERE types.id = counter.id AND counter.rn > 1;
|
||||
SQL
|
||||
|
||||
add_index :types, "LOWER(name)", unique: true, algorithm: :concurrently
|
||||
end
|
||||
|
||||
def down
|
||||
remove_index :types, column: "LOWER(name)", algorithm: :concurrently
|
||||
end
|
||||
end
|
||||
@@ -41,7 +41,7 @@ module Migration
|
||||
def in_configurable_batches(klass, default_batch_size: 1000)
|
||||
batches = ENV["OPENPROJECT_MIGRATION_BATCH_SIZE"]&.to_i || default_batch_size
|
||||
|
||||
klass.in_batches(of: batches)
|
||||
yield klass.in_batches(of: batches)
|
||||
end
|
||||
|
||||
def remove_index_if_exists(table_name, index_name)
|
||||
|
||||
@@ -44,7 +44,8 @@ module OpenProject
|
||||
}.freeze
|
||||
|
||||
def self.stimulus_config
|
||||
CONFIG.map { |target_name, (operator, formats)| [target_name, operator, formats] }.to_json
|
||||
CONFIG
|
||||
.map { |target_name, (operator, formats)| [target_name, operator, formats] }.to_json
|
||||
end
|
||||
|
||||
attr_reader :format
|
||||
|
||||
@@ -5,7 +5,7 @@ module OpenProject
|
||||
##
|
||||
# Consume a message and let it be handled
|
||||
# by all handlers
|
||||
def log(exception, context = {})
|
||||
def log(exception, context = {}) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
||||
# in case we're getting ActionController::Parameters
|
||||
context = if context.respond_to?(:to_unsafe_h)
|
||||
context.to_unsafe_h
|
||||
@@ -28,9 +28,11 @@ module OpenProject
|
||||
|
||||
# Set current contexts
|
||||
context[:level] ||= context[:exception] ? :error : :info
|
||||
context[:current_user] ||= User.current
|
||||
if the_current_user = current_user
|
||||
context[:current_user] ||= the_current_user
|
||||
end
|
||||
|
||||
registered_handlers.values.each do |handler|
|
||||
registered_handlers.each_value do |handler|
|
||||
handler.call message, context
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to delegate log to #{handler.inspect}: #{e.inspect}\nMessage: #{message.inspect}"
|
||||
@@ -103,6 +105,14 @@ module OpenProject
|
||||
extended = OpenProject::Logging.extend_payload!(payload, context)
|
||||
OpenProject::Logging.formatter.call extended
|
||||
end
|
||||
|
||||
def current_user
|
||||
User.current
|
||||
# Probably more performant to rescue "ActiveRecord::StatementInvalid: PG::UndefinedTable"
|
||||
# than to check for table existence with `User.connection.data_source_exists?(User.table_name)`
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# 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 CustomFields::DetailsComponent, type: :component do
|
||||
describe ".supported?" do
|
||||
context "with a bool cf" do
|
||||
let(:custom_field) { build_stubbed(:boolean_wp_custom_field) }
|
||||
|
||||
it "is supported" do
|
||||
expect(described_class).to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a string cf" do
|
||||
let(:custom_field) { build_stubbed(:string_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a text cf" do
|
||||
let(:custom_field) { build_stubbed(:text_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a link cf" do
|
||||
let(:custom_field) { build_stubbed(:link_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an int cf" do
|
||||
let(:custom_field) { build_stubbed(:integer_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a version cf" do
|
||||
let(:custom_field) { build_stubbed(:version_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a user cf" do
|
||||
let(:custom_field) { build_stubbed(:user_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a date cf" do
|
||||
let(:custom_field) { build_stubbed(:date_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a list cf" do
|
||||
let(:custom_field) { build_stubbed(:list_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a float cf" do
|
||||
let(:custom_field) { build_stubbed(:float_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a calculated_value cf" do
|
||||
let(:custom_field) { build_stubbed(:calculated_value_project_custom_field) }
|
||||
|
||||
it "is supported" do
|
||||
expect(described_class).to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a hierarchy cf" do
|
||||
let(:custom_field) { build_stubbed(:hierarchy_wp_custom_field) }
|
||||
|
||||
it "is supported" do
|
||||
expect(described_class).to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a weighted_item_list cf" do
|
||||
let(:custom_field) { build_stubbed(:weighted_item_list_wp_custom_field) }
|
||||
|
||||
it "is supported" do
|
||||
expect(described_class).to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,28 +29,32 @@
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
require "contracts/shared/model_contract_shared_context"
|
||||
require_relative "shared_contract_examples"
|
||||
|
||||
RSpec.describe CustomFields::CreateContract do
|
||||
include_context "ModelContract shared context"
|
||||
|
||||
let(:cf) { build_stubbed(:project_custom_field) }
|
||||
let(:contract) do
|
||||
described_class.new(cf, current_user, options: {})
|
||||
end
|
||||
|
||||
it_behaves_like "contract is valid for active admins and invalid for regular users"
|
||||
|
||||
context "for calculated value custom field" do
|
||||
let(:cf) { build_stubbed(:calculated_value_project_custom_field) }
|
||||
let(:current_user) { build_stubbed(:admin) }
|
||||
|
||||
context "without calculated_values enterprise feature" do
|
||||
it_behaves_like "contract is invalid", base: :error_enterprise_only
|
||||
it_behaves_like "custom_field contract" do
|
||||
let(:custom_field) do
|
||||
CustomField.new(name: custom_field_name,
|
||||
type: custom_field_type,
|
||||
field_format: custom_field_field_format,
|
||||
editable: custom_field_editable,
|
||||
is_filter: custom_field_is_filter,
|
||||
is_for_all: custom_field_is_for_all,
|
||||
is_required: custom_field_is_required,
|
||||
max_length: custom_field_max_length,
|
||||
min_length: custom_field_min_length,
|
||||
possible_values: custom_field_possible_values,
|
||||
regexp: custom_field_regexp,
|
||||
formula: custom_field_formula,
|
||||
searchable: custom_field_searchable,
|
||||
admin_only: custom_field_admin_only,
|
||||
default_value: custom_field_default_value,
|
||||
multi_value: custom_field_multi_value,
|
||||
content_right_to_left: custom_field_right_to_left,
|
||||
custom_field_section_id: custom_field_custom_field_section_id,
|
||||
allow_non_open_versions: custom_field_allow_non_open_versions)
|
||||
end
|
||||
|
||||
context "with calculated_values enterprise feature", with_ee: %i[calculated_values] do
|
||||
it_behaves_like "contract is valid"
|
||||
end
|
||||
subject(:contract) { described_class.new(custom_field, current_user) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# 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"
|
||||
require "contracts/shared/model_contract_shared_context"
|
||||
|
||||
RSpec.shared_examples_for "custom_field contract" do
|
||||
include_context "ModelContract shared context"
|
||||
|
||||
current_user { build_stubbed(:admin) }
|
||||
|
||||
let(:custom_field_name) { "Project name" }
|
||||
let(:custom_field_type) { "ProjectCustomField" }
|
||||
let(:custom_field_editable) { true }
|
||||
let(:custom_field_field_format) { "int" }
|
||||
let(:custom_field_is_filter) { true }
|
||||
let(:custom_field_is_for_all) { true }
|
||||
let(:custom_field_is_required) { true }
|
||||
let(:custom_field_max_length) { 0 }
|
||||
let(:custom_field_min_length) { 0 }
|
||||
let(:custom_field_possible_values) { [] }
|
||||
let(:custom_field_regexp) { nil }
|
||||
let(:custom_field_formula) { nil }
|
||||
let(:custom_field_searchable) { true }
|
||||
let(:custom_field_admin_only) { true }
|
||||
let(:custom_field_default_value) { nil }
|
||||
let(:custom_field_multi_value) { false }
|
||||
let(:custom_field_right_to_left) { false }
|
||||
let(:custom_field_custom_field_section_id) { custom_field_section.id }
|
||||
let(:custom_field_allow_non_open_versions) { nil }
|
||||
|
||||
let(:custom_field_section) { build_stubbed(:project_custom_field_section) }
|
||||
|
||||
it_behaves_like "contract is valid for active admins and invalid for regular users"
|
||||
|
||||
context "if the name is nil" do
|
||||
let(:custom_field_name) { nil }
|
||||
|
||||
it_behaves_like "contract is invalid", name: %i(blank)
|
||||
end
|
||||
|
||||
context "for a boolean field" do
|
||||
let(:custom_field_field_format) { "bool" }
|
||||
let(:custom_field_is_required) { false }
|
||||
|
||||
context "if required is true" do
|
||||
let(:custom_field_is_required) { true }
|
||||
|
||||
it_behaves_like "contract is invalid", is_required: :cannot_be_true
|
||||
end
|
||||
end
|
||||
|
||||
context "for a calculated field", with_ee: %i[calculated_values],
|
||||
with_flag: { calculated_value_project_attribute: true } do
|
||||
let(:custom_field_field_format) { "calculated_value" }
|
||||
let(:custom_field_is_required) { false }
|
||||
let(:custom_field_formula) { "1 + 1" }
|
||||
|
||||
context "without calculated_values enterprise feature", with_ee: %i[] do
|
||||
it_behaves_like "contract is invalid", base: :error_enterprise_only
|
||||
end
|
||||
|
||||
context "with calculated_values enterprise feature" do
|
||||
it_behaves_like "contract is valid"
|
||||
end
|
||||
|
||||
context "if required is true" do
|
||||
let(:custom_field_is_required) { true }
|
||||
|
||||
it_behaves_like "contract is invalid", is_required: :cannot_be_true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,35 +29,47 @@
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
require "contracts/shared/model_contract_shared_context"
|
||||
require_relative "shared_contract_examples"
|
||||
|
||||
RSpec.describe CustomFields::UpdateContract do
|
||||
include_context "ModelContract shared context"
|
||||
|
||||
let(:cf) { build_stubbed(:project_custom_field) }
|
||||
let(:contract) do
|
||||
described_class.new(cf, current_user)
|
||||
end
|
||||
|
||||
it_behaves_like "contract is valid for active admins and invalid for regular users"
|
||||
|
||||
context "for calculated value custom field" do
|
||||
let(:cf) { build_stubbed(:calculated_value_project_custom_field) }
|
||||
let(:current_user) { build_stubbed(:admin) }
|
||||
|
||||
context "without calculated_values enterprise feature" do
|
||||
it_behaves_like "contract is invalid", base: :error_enterprise_only
|
||||
it_behaves_like "custom_field contract" do
|
||||
let(:custom_field) do
|
||||
build_stubbed(:custom_field,
|
||||
name: custom_field_name,
|
||||
type: custom_field_type,
|
||||
field_format: custom_field_field_format,
|
||||
editable: custom_field_editable,
|
||||
is_filter: custom_field_is_filter,
|
||||
is_for_all: custom_field_is_for_all,
|
||||
is_required: custom_field_is_required,
|
||||
max_length: custom_field_max_length,
|
||||
min_length: custom_field_min_length,
|
||||
possible_values: custom_field_possible_values,
|
||||
regexp: custom_field_regexp,
|
||||
formula: custom_field_formula,
|
||||
searchable: custom_field_searchable,
|
||||
admin_only: custom_field_admin_only,
|
||||
default_value: custom_field_default_value,
|
||||
multi_value: custom_field_multi_value,
|
||||
content_right_to_left: custom_field_right_to_left,
|
||||
custom_field_section_id: custom_field_custom_field_section_id,
|
||||
allow_non_open_versions: custom_field_allow_non_open_versions)
|
||||
end
|
||||
|
||||
context "with calculated_values enterprise feature", with_ee: %i[calculated_values] do
|
||||
it_behaves_like "contract is valid"
|
||||
subject(:contract) { described_class.new(custom_field, current_user) }
|
||||
|
||||
context "for a calculated field", with_ee: %i[calculated_values],
|
||||
with_flag: { calculated_value_project_attribute: true } do
|
||||
let(:custom_field_field_format) { "calculated_value" }
|
||||
|
||||
let(:custom_field_formula) { "1 + 1" }
|
||||
|
||||
context "with a CustomFields::RecalculateValuesJob already existing",
|
||||
with_good_job: CustomFields::RecalculateValuesJob do
|
||||
before do
|
||||
CustomFields::RecalculateValuesJob
|
||||
.set(wait: 10.minutes) # GoodJob executes inline job without wait immediately
|
||||
.perform_later(user: current_user, custom_field_id: cf.id)
|
||||
.perform_later(user: current_user, custom_field_id: custom_field.id)
|
||||
end
|
||||
|
||||
it_behaves_like "contract is invalid", base: :previous_custom_field_recalculation_unprocessed
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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"
|
||||
require_relative "shared_context"
|
||||
|
||||
RSpec.describe "Edit project custom fields", :js do
|
||||
include_context "with seeded project custom fields"
|
||||
|
||||
let(:custom_field) { boolean_project_custom_field }
|
||||
|
||||
it_behaves_like "prevents access on insufficient permissions"
|
||||
it_behaves_like "has breadcrumb and tabs"
|
||||
it_behaves_like "shows checkboxes for configuration" do
|
||||
let(:required_supported) { false }
|
||||
end
|
||||
it_behaves_like "editing the field" do
|
||||
let(:using_primer) { true }
|
||||
end
|
||||
end
|
||||
@@ -42,173 +42,146 @@ RSpec.describe "Edit project custom field calculated value",
|
||||
project_custom_field_section: section_for_select_fields)
|
||||
end
|
||||
|
||||
let(:calculated_value) { calculated_from_int_project_custom_field }
|
||||
let(:custom_field) { calculated_from_int_project_custom_field }
|
||||
|
||||
context "with insufficient permissions" do
|
||||
it "is not accessible" do
|
||||
login_as(non_admin)
|
||||
visit edit_admin_settings_project_custom_field_path(calculated_value)
|
||||
|
||||
expect(page).to have_text("You are not authorized to access this page.")
|
||||
end
|
||||
it_behaves_like "prevents access on insufficient permissions"
|
||||
it_behaves_like "has breadcrumb and tabs"
|
||||
it_behaves_like "shows checkboxes for configuration" do
|
||||
let(:required_supported) { false }
|
||||
end
|
||||
|
||||
context "with sufficient permissions" do
|
||||
context "with calculated_values enterprise feature" do
|
||||
before do
|
||||
login_as(admin)
|
||||
visit edit_admin_settings_project_custom_field_path(calculated_value)
|
||||
visit edit_admin_settings_project_custom_field_path(custom_field)
|
||||
end
|
||||
|
||||
it "shows a correct breadcrumb menu" do
|
||||
it "allows to change basic attributes and the section of the calculated value" do
|
||||
expect(page).to have_css(".PageHeader-title", text: custom_field.name)
|
||||
|
||||
fill_in("custom_field_name", with: "Updated name", fill_options: { clear: :backspace })
|
||||
|
||||
# Calculated values cannot be required since the user cannot fill them out themselves.
|
||||
expect(page).to have_no_field("Required")
|
||||
|
||||
select(section_for_select_fields.name, from: "Section")
|
||||
find_field(id: "custom_field_formula", type: :hidden).set("1 + 1")
|
||||
|
||||
click_on "Save"
|
||||
|
||||
expect(page).to have_text("Successful update")
|
||||
|
||||
expect(page).to have_css(".PageHeader-title", text: "Updated name")
|
||||
|
||||
expect(custom_field.reload.name).to eq("Updated name")
|
||||
expect(custom_field.reload.project_custom_field_section).to eq(section_for_select_fields)
|
||||
expect(custom_field.reload.formula_string).to eq("1 + 1")
|
||||
|
||||
within ".PageHeader-breadcrumbs" do
|
||||
expect(page).to have_link("Administration")
|
||||
expect(page).to have_link("Projects")
|
||||
expect(page).to have_link("Project attributes")
|
||||
expect(page).to have_text(calculated_value.name)
|
||||
expect(page).to have_text("Updated name")
|
||||
end
|
||||
end
|
||||
|
||||
it "shows tab navigation" do
|
||||
within_test_selector("project_attribute_detail_header") do
|
||||
expect(page).to have_link("Details")
|
||||
expect(page).to have_link("Projects")
|
||||
end
|
||||
it "prevents saving a calculated value with an empty name" do
|
||||
original_name = custom_field.name
|
||||
|
||||
fill_in("custom_field_name", with: "")
|
||||
click_on "Save"
|
||||
|
||||
expect(page).to have_text("Name can't be blank")
|
||||
|
||||
expect(page).to have_no_text("Successful update")
|
||||
|
||||
expect(page).to have_css(".PageHeader-title", text: original_name)
|
||||
expect(custom_field.reload.name).to eq(original_name)
|
||||
end
|
||||
|
||||
context "without calculated_values enterprise feature", with_ee: [] do
|
||||
it do
|
||||
expect(page)
|
||||
.to have_enterprise_banner(:premium)
|
||||
.and have_no_field("custom_field_name")
|
||||
.and have_no_button("Save")
|
||||
end
|
||||
it "prevents saving a calculated value with an empty formula" do
|
||||
original_formula = custom_field.formula_string
|
||||
|
||||
find_field(id: "custom_field_formula", type: :hidden).set("")
|
||||
click_on "Save"
|
||||
|
||||
expect(page).to have_text("Formula can't be blank")
|
||||
expect(page).to have_no_text("Successful update")
|
||||
|
||||
expect(custom_field.reload.formula_string).to eq(original_formula)
|
||||
end
|
||||
|
||||
context "with calculated_values enterprise feature" do
|
||||
it "allows to change basic attributes and the section of the calculated value" do
|
||||
expect(page).to have_css(".PageHeader-title", text: calculated_value.name)
|
||||
it "allows submitting formula by pressing Enter/Return" do
|
||||
# ensure multiple spaces are handled without problems
|
||||
formula = "2 + (1 +1)"
|
||||
|
||||
fill_in("custom_field_name", with: "Updated name", fill_options: { clear: :backspace })
|
||||
select(section_for_select_fields.name, from: "custom_field_custom_field_section_id")
|
||||
find_field(id: "custom_field_formula", type: :hidden).set("1 + 1")
|
||||
pattern_input = find(:xpath, "//input[@id='custom_field_formula']/parent::div//div[@contenteditable='true']")
|
||||
|
||||
click_on "Save"
|
||||
pattern_input.set("#{formula}\n")
|
||||
|
||||
expect(page).to have_text("Successful update")
|
||||
expect(page).to have_text("Successful update")
|
||||
|
||||
expect(page).to have_css(".PageHeader-title", text: "Updated name")
|
||||
expect(custom_field.reload.formula_string).to eq(formula)
|
||||
end
|
||||
|
||||
expect(calculated_value.reload.name).to eq("Updated name")
|
||||
expect(calculated_value.reload.project_custom_field_section).to eq(section_for_select_fields)
|
||||
expect(calculated_value.reload.formula_string).to eq("1 + 1")
|
||||
context "when editing the formula" do
|
||||
using CustomFieldFormulaReferencing
|
||||
|
||||
within ".PageHeader-breadcrumbs" do
|
||||
expect(page).to have_link("Administration")
|
||||
expect(page).to have_link("Projects")
|
||||
expect(page).to have_link("Project attributes")
|
||||
expect(page).to have_text("Updated name")
|
||||
it "allows using the pattern input component" do
|
||||
expect(page).to have_css(".PageHeader-title", text: custom_field.name)
|
||||
|
||||
expect(page).to have_css("input#custom_field_formula[value='#{integer_project_custom_field} * 2']",
|
||||
visible: :hidden)
|
||||
|
||||
# Suggestions drop down is hidden
|
||||
expect(page).to have_no_css(".op-pattern-input--suggestions-dropdown .ActionListItem")
|
||||
|
||||
pattern_input = page.find(".op-pattern-input--text-field")
|
||||
pattern_input.click
|
||||
pattern_input.send_keys(" + ")
|
||||
expect(page).to have_no_css(".op-pattern-input--suggestions-dropdown .ActionListItem")
|
||||
|
||||
# Open suggestion list
|
||||
pattern_input.send_keys("/")
|
||||
within ".op-pattern-input--suggestions-dropdown" do
|
||||
expect(page).to have_css(".ActionListItem", text: float_project_custom_field.name)
|
||||
click_on(float_project_custom_field.name)
|
||||
end
|
||||
end
|
||||
|
||||
it "prevents saving a calculated value with an empty name" do
|
||||
original_name = calculated_value.name
|
||||
# Input divide operator
|
||||
pattern_input.send_keys(" / ")
|
||||
expect(page).to have_no_css(".op-pattern-input--suggestions-dropdown .ActionListItem")
|
||||
|
||||
fill_in("custom_field_name", with: "")
|
||||
click_on "Save"
|
||||
|
||||
expect(page).to have_text("Name can't be blank")
|
||||
|
||||
expect(page).to have_no_text("Successful update")
|
||||
|
||||
expect(page).to have_css(".PageHeader-title", text: original_name)
|
||||
expect(calculated_value.reload.name).to eq(original_name)
|
||||
end
|
||||
|
||||
it "prevents saving a calculated value with an empty formula" do
|
||||
original_formula = calculated_value.formula_string
|
||||
|
||||
find_field(id: "custom_field_formula", type: :hidden).set("")
|
||||
click_on "Save"
|
||||
|
||||
expect(page).to have_text("Formula can't be blank")
|
||||
expect(page).to have_no_text("Successful update")
|
||||
|
||||
expect(calculated_value.reload.formula_string).to eq(original_formula)
|
||||
end
|
||||
|
||||
it "allows submitting formula by pressing Enter/Return" do
|
||||
# ensure multiple spaces are handled without problems
|
||||
formula = "2 + (1 +1)"
|
||||
|
||||
pattern_input = find(:xpath, "//input[@id='custom_field_formula']/parent::div//div[@contenteditable='true']")
|
||||
|
||||
pattern_input.set("#{formula}\n")
|
||||
|
||||
expect(page).to have_text("Successful update")
|
||||
|
||||
expect(calculated_value.reload.formula_string).to eq(formula)
|
||||
end
|
||||
|
||||
context "when editing the formula" do
|
||||
using CustomFieldFormulaReferencing
|
||||
|
||||
it "allows using the pattern input component" do
|
||||
expect(page).to have_css(".PageHeader-title", text: calculated_value.name)
|
||||
|
||||
expect(page).to have_css("input#custom_field_formula[value='#{integer_project_custom_field} * 2']",
|
||||
visible: :hidden)
|
||||
|
||||
# Suggestions drop down is hidden
|
||||
expect(page).to have_no_css(".op-pattern-input--suggestions-dropdown .ActionListItem")
|
||||
|
||||
pattern_input = page.find(".op-pattern-input--text-field")
|
||||
pattern_input.click
|
||||
pattern_input.send_keys(" + ")
|
||||
expect(page).to have_no_css(".op-pattern-input--suggestions-dropdown .ActionListItem")
|
||||
|
||||
# Open suggestion list
|
||||
pattern_input.send_keys("/")
|
||||
within ".op-pattern-input--suggestions-dropdown" do
|
||||
expect(page).to have_css(".ActionListItem", text: float_project_custom_field.name)
|
||||
click_on(float_project_custom_field.name)
|
||||
end
|
||||
|
||||
# Input divide operator
|
||||
pattern_input.send_keys(" / ")
|
||||
expect(page).to have_no_css(".op-pattern-input--suggestions-dropdown .ActionListItem")
|
||||
|
||||
# Open suggestion list again
|
||||
pattern_input.send_keys("/")
|
||||
within ".op-pattern-input--suggestions-dropdown" do
|
||||
expect(page).to have_css(".ActionListItem", text: weighted_item_list_project_custom_field.name)
|
||||
click_on(weighted_item_list_project_custom_field.name)
|
||||
end
|
||||
|
||||
click_on("Save")
|
||||
wait_for_network_idle
|
||||
|
||||
new_formula = calculated_value.reload.formula_string
|
||||
expect(new_formula)
|
||||
.to eq(
|
||||
"#{integer_project_custom_field} * 2 + #{float_project_custom_field} / #{weighted_item_list_project_custom_field}"
|
||||
)
|
||||
# Open the suggestion list again
|
||||
pattern_input.send_keys("/")
|
||||
within ".op-pattern-input--suggestions-dropdown" do
|
||||
expect(page).to have_css(".ActionListItem", text: weighted_item_list_project_custom_field.name)
|
||||
click_on(weighted_item_list_project_custom_field.name)
|
||||
end
|
||||
|
||||
click_on("Save")
|
||||
wait_for_network_idle
|
||||
|
||||
new_formula = custom_field.reload.formula_string
|
||||
expect(new_formula)
|
||||
.to eq(
|
||||
"#{integer_project_custom_field} * 2 + #{float_project_custom_field} / #{weighted_item_list_project_custom_field}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without the feature flag", with_flag: { calculated_value_project_attribute: false } do
|
||||
it "prevents saving a calculated value" do
|
||||
expect do
|
||||
login_as(admin)
|
||||
visit new_admin_settings_project_custom_field_path(field_format: "calculated_value",
|
||||
custom_field_section_id: section_for_input_fields.id)
|
||||
fill_in("custom_field_name", with: "New calculated value")
|
||||
find_field(id: "custom_field_formula", type: :hidden).set("1 + 1")
|
||||
click_on "Save"
|
||||
context "without calculated_value enterprise feature", with_ee: [] do
|
||||
before do
|
||||
login_as(admin)
|
||||
visit edit_admin_settings_project_custom_field_path(custom_field)
|
||||
end
|
||||
|
||||
expect(page).to have_text("Format is not set to one of the allowed values.")
|
||||
end.not_to change(CustomField, :count)
|
||||
it do
|
||||
expect(page)
|
||||
.to have_enterprise_banner(:premium)
|
||||
.and have_no_field("custom_field_name")
|
||||
.and have_no_button("Save")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# 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"
|
||||
require_relative "shared_context"
|
||||
|
||||
RSpec.describe "Edit project custom fields", :js do
|
||||
include_context "with seeded project custom fields"
|
||||
|
||||
context "with insufficient permissions" do
|
||||
it "is not accessible" do
|
||||
login_as(non_admin)
|
||||
visit edit_admin_settings_project_custom_field_path(boolean_project_custom_field)
|
||||
|
||||
expect(page).to have_text("You are not authorized to access this page.")
|
||||
end
|
||||
end
|
||||
|
||||
context "with sufficient permissions" do
|
||||
before do
|
||||
login_as(admin)
|
||||
visit edit_admin_settings_project_custom_field_path(boolean_project_custom_field)
|
||||
end
|
||||
|
||||
it "shows a correct breadcrumb menu" do
|
||||
within ".PageHeader-breadcrumbs" do
|
||||
expect(page).to have_link("Administration")
|
||||
expect(page).to have_link("Projects")
|
||||
expect(page).to have_link("Project attributes")
|
||||
expect(page).to have_text(boolean_project_custom_field.name)
|
||||
end
|
||||
end
|
||||
|
||||
it "shows tab navigation" do
|
||||
within_test_selector("project_attribute_detail_header") do
|
||||
expect(page).to have_link("Details")
|
||||
expect(page).to have_link("Projects")
|
||||
end
|
||||
end
|
||||
|
||||
it "allows to change basic attributes and the section of the project custom field" do
|
||||
# TODO: reuse specs for classic custom field form in order to test for other attribute manipulations
|
||||
expect(page).to have_css(".PageHeader-title", text: boolean_project_custom_field.name)
|
||||
|
||||
fill_in("custom_field_name", with: "Updated name", fill_options: { clear: :backspace })
|
||||
select(section_for_select_fields.name, from: "custom_field_custom_field_section_id")
|
||||
|
||||
click_on("Save")
|
||||
|
||||
expect(page).to have_text("Successful update")
|
||||
|
||||
expect(page).to have_css(".PageHeader-title", text: "Updated name")
|
||||
|
||||
expect(boolean_project_custom_field.reload.name).to eq("Updated name")
|
||||
expect(boolean_project_custom_field.reload.project_custom_field_section).to eq(section_for_select_fields)
|
||||
|
||||
within ".PageHeader-breadcrumbs" do
|
||||
expect(page).to have_link("Administration")
|
||||
expect(page).to have_link("Projects")
|
||||
expect(page).to have_link("Project attributes")
|
||||
expect(page).to have_text("Updated name")
|
||||
end
|
||||
end
|
||||
|
||||
it "prevents saving a project custom field with an empty name" do
|
||||
original_name = boolean_project_custom_field.name
|
||||
|
||||
fill_in("custom_field_name", with: "")
|
||||
click_on("Save")
|
||||
|
||||
expect(page).to have_field "custom_field_name", validation_message: /Please fill (in|out) this field./
|
||||
|
||||
expect(page).to have_no_text("Successful update")
|
||||
|
||||
expect(page).to have_css(".PageHeader-title", text: original_name)
|
||||
expect(boolean_project_custom_field.reload.name).to eq(original_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -269,6 +269,12 @@ RSpec.describe "List project custom fields", :js do
|
||||
within_project_custom_field_container(boolean_project_custom_field) do
|
||||
expect(page).to have_text("1 project")
|
||||
end
|
||||
|
||||
for_all_cf = create(:project_custom_field, :integer, is_for_all: true)
|
||||
cf_index_page.visit!
|
||||
within_project_custom_field_container(for_all_cf) do
|
||||
expect(page).to have_text("All projects")
|
||||
end
|
||||
end
|
||||
|
||||
describe "deleting custom fields" do
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# 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"
|
||||
require_relative "shared_context"
|
||||
|
||||
RSpec.describe "Edit project custom fields", :js do
|
||||
include_context "with seeded project custom fields"
|
||||
|
||||
let(:custom_field) { integer_project_custom_field }
|
||||
|
||||
it_behaves_like "prevents access on insufficient permissions"
|
||||
it_behaves_like "has breadcrumb and tabs"
|
||||
it_behaves_like "shows checkboxes for configuration"
|
||||
it_behaves_like "editing the field"
|
||||
end
|
||||
@@ -143,11 +143,11 @@ RSpec.describe "Project Custom Field Mappings", :js do
|
||||
end
|
||||
end
|
||||
|
||||
context "and the project custom field is required" do
|
||||
shared_let(:project_custom_field) { create(:project_custom_field, is_required: true) }
|
||||
context "and the project custom field is for_all projects" do
|
||||
shared_let(:project_custom_field) { create(:project_custom_field, is_for_all: true) }
|
||||
|
||||
it "renders a blank slate" do
|
||||
expect(page).to have_text("Required in all projects")
|
||||
expect(page).to have_text("For all projects")
|
||||
expect(page).not_to have_test_selector("add-projects-sub-header")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -153,3 +153,134 @@ RSpec.shared_context "with seeded project custom fields" do
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples "prevents access on insufficient permissions" do
|
||||
current_user { non_admin }
|
||||
|
||||
before do
|
||||
visit edit_admin_settings_project_custom_field_path(custom_field)
|
||||
end
|
||||
|
||||
it "is not accessible" do
|
||||
expect(page).to have_text("You are not authorized to access this page.")
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples "has breadcrumb and tabs" do
|
||||
current_user { admin }
|
||||
|
||||
before do
|
||||
visit edit_admin_settings_project_custom_field_path(custom_field)
|
||||
end
|
||||
|
||||
it "shows a correct breadcrumb menu" do
|
||||
within ".PageHeader-breadcrumbs" do
|
||||
expect(page).to have_link("Administration")
|
||||
expect(page).to have_link("Projects")
|
||||
expect(page).to have_link("Project attributes")
|
||||
expect(page).to have_text(custom_field.name)
|
||||
end
|
||||
end
|
||||
|
||||
it "shows tab navigation" do
|
||||
within_test_selector("project_attribute_detail_header") do
|
||||
expect(page).to have_link("Details")
|
||||
expect(page).to have_link("Projects")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples "shows checkboxes for configuration" do
|
||||
current_user { admin }
|
||||
|
||||
let(:required_supported) do
|
||||
super()
|
||||
rescue NoMethodError
|
||||
true
|
||||
end
|
||||
|
||||
before do
|
||||
visit edit_admin_settings_project_custom_field_path(custom_field)
|
||||
end
|
||||
|
||||
it "shows checkboxes for 'Required', 'Admin-only' and 'For all projects' attributes" do
|
||||
if required_supported
|
||||
expect(page).to have_unchecked_field("Required")
|
||||
|
||||
check("Required")
|
||||
else
|
||||
expect(page).to have_no_field("Required")
|
||||
end
|
||||
|
||||
expect(page).to have_unchecked_field("Admin-only")
|
||||
check("Admin-only")
|
||||
|
||||
expect(page).to have_unchecked_field("For all projects")
|
||||
check("For all projects")
|
||||
|
||||
click_on("Save")
|
||||
|
||||
expect(page).to have_text("Successful update")
|
||||
|
||||
custom_field.reload
|
||||
expect(custom_field.is_required).to eq required_supported
|
||||
expect(custom_field.admin_only).to be_truthy
|
||||
expect(custom_field.is_for_all).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples "editing the field" do
|
||||
current_user { admin }
|
||||
|
||||
let(:using_primer) do
|
||||
super()
|
||||
rescue NoMethodError
|
||||
false
|
||||
end
|
||||
|
||||
before do
|
||||
visit edit_admin_settings_project_custom_field_path(custom_field)
|
||||
end
|
||||
|
||||
it "allows to change name and the section of the project custom field" do
|
||||
# TODO: reuse specs for classic custom field form in order to test for other attribute manipulations
|
||||
expect(page).to have_css(".PageHeader-title", text: custom_field.name)
|
||||
|
||||
fill_in("Name", with: "Updated name", fill_options: { clear: :backspace })
|
||||
select(section_for_select_fields.name, from: "custom_field_custom_field_section_id")
|
||||
|
||||
click_on("Save")
|
||||
|
||||
expect(page).to have_text("Successful update")
|
||||
|
||||
expect(page).to have_css(".PageHeader-title", text: "Updated name")
|
||||
|
||||
expect(custom_field.reload.name).to eq("Updated name")
|
||||
expect(custom_field.reload.project_custom_field_section).to eq(section_for_select_fields)
|
||||
|
||||
within ".PageHeader-breadcrumbs" do
|
||||
expect(page).to have_link("Administration")
|
||||
expect(page).to have_link("Projects")
|
||||
expect(page).to have_link("Project attributes")
|
||||
expect(page).to have_text("Updated name")
|
||||
end
|
||||
end
|
||||
|
||||
it "prevents saving a project custom field with an empty name" do
|
||||
original_name = custom_field.name
|
||||
|
||||
fill_in("Name", with: "")
|
||||
click_on("Save")
|
||||
|
||||
if using_primer
|
||||
expect(page).to have_css(".FormControl-inlineValidation", text: "Name can't be blank")
|
||||
else
|
||||
expect(page).to have_field "Name", validation_message: /Please fill (in|out) this field./
|
||||
end
|
||||
|
||||
expect(page).to have_no_text("Successful update")
|
||||
|
||||
expect(page).to have_css(".PageHeader-title", text: original_name)
|
||||
expect(custom_field.reload.name).to eq(original_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -256,10 +256,10 @@ RSpec.shared_examples_for "expected fields for the custom field's format" do |ty
|
||||
end
|
||||
|
||||
expect_page_to_have_texts(
|
||||
label_default_value, label_is_required
|
||||
label_default_value
|
||||
)
|
||||
expect_page_not_to_have_texts(
|
||||
label_min_length, label_max_length, label_regexp, label_multi_value,
|
||||
label_min_length, label_max_length, label_regexp, label_multi_value, label_is_required,
|
||||
label_allow_non_open_versions, label_possible_values, label_ee_banner_hierarchy
|
||||
)
|
||||
end
|
||||
|
||||
@@ -44,11 +44,10 @@ RSpec.describe "custom fields", :js do
|
||||
|
||||
describe "available fields" do
|
||||
it "shows all form elements" do
|
||||
expect(cf_page).to have_form_element("Name")
|
||||
expect(cf_page).to have_form_element("Default value")
|
||||
expect(cf_page).to have_form_element("Required")
|
||||
expect(cf_page).to have_form_element("For all projects")
|
||||
expect(cf_page).to have_form_element("Used as a filter")
|
||||
expect(cf_page).to have_field("Name")
|
||||
expect(cf_page).to have_field("Default value")
|
||||
expect(cf_page).to have_field("For all projects")
|
||||
expect(cf_page).to have_field("Used as a filter")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ RSpec.describe "work package custom fields of type hierarchy", :js do
|
||||
|
||||
# region Edit the details of the custom field
|
||||
|
||||
expect(page).to have_test_selector("op-custom-fields--new-hierarchy-banner")
|
||||
expect(page).to have_test_selector("op-custom-fields--top-banner")
|
||||
expect(page).to have_css(".PageHeader-title", text: hierarchy_name)
|
||||
|
||||
# Now, that was the wrong name, so I can change it to the correct one
|
||||
@@ -140,7 +140,7 @@ RSpec.describe "work package custom fields of type hierarchy", :js do
|
||||
|
||||
# And is the blue banner gone, now that I have added some items?
|
||||
hierarchy_page.switch_tab "Details"
|
||||
expect(page).not_to have_test_selector("op-custom-fields--new-hierarchy-banner")
|
||||
expect(page).not_to have_test_selector("op-custom-fields--top-banner")
|
||||
|
||||
# Finally, we delete the custom field ... I'm done with this ...
|
||||
custom_field_index_page.visit!
|
||||
|
||||
@@ -67,13 +67,25 @@ RSpec.describe "Projects copy", :js,
|
||||
end
|
||||
let!(:project_custom_field_section) { create(:project_custom_field_section, name: "Section A") }
|
||||
let!(:project_custom_field) do
|
||||
create(:text_project_custom_field, name: "Required Foo", is_required: true, project_custom_field_section:)
|
||||
create(:text_project_custom_field,
|
||||
name: "Required Foo",
|
||||
is_for_all: true,
|
||||
is_required: true,
|
||||
project_custom_field_section:)
|
||||
end
|
||||
let!(:optional_project_custom_field) do
|
||||
create(:text_project_custom_field, name: "Optional Foo", is_required: false, project_custom_field_section:)
|
||||
create(:text_project_custom_field,
|
||||
name: "Optional Foo",
|
||||
is_for_all: true,
|
||||
is_required: false,
|
||||
project_custom_field_section:)
|
||||
end
|
||||
let!(:optional_project_custom_field_with_default) do
|
||||
create(:text_project_custom_field, is_required: false, default_value: "foo", project_custom_field_section:)
|
||||
create(:text_project_custom_field,
|
||||
is_for_all: true,
|
||||
is_required: false,
|
||||
default_value: "foo",
|
||||
project_custom_field_section:)
|
||||
end
|
||||
let!(:wp_custom_field) do
|
||||
create(:text_wp_custom_field)
|
||||
@@ -168,6 +180,7 @@ RSpec.describe "Projects copy", :js,
|
||||
let!(:required_user_custom_field) do
|
||||
create(:user_project_custom_field, name: "Required User",
|
||||
is_required: true,
|
||||
is_for_all: true,
|
||||
project_custom_field_section:)
|
||||
end
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ RSpec.describe "Projects", "creation",
|
||||
create(:list_project_custom_field,
|
||||
name: "List CF",
|
||||
is_required: true,
|
||||
is_for_all: true,
|
||||
multi_value: true,
|
||||
project_custom_field_section:)
|
||||
end
|
||||
@@ -177,6 +178,7 @@ RSpec.describe "Projects", "creation",
|
||||
create(:version_project_custom_field,
|
||||
name: "Version CF",
|
||||
is_required: true,
|
||||
is_for_all: true,
|
||||
multi_value: true,
|
||||
project_custom_field_section:)
|
||||
end
|
||||
@@ -258,7 +260,14 @@ RSpec.describe "Projects", "creation",
|
||||
project_custom_field_section:)
|
||||
end
|
||||
|
||||
it "renders required custom fields for new" do
|
||||
shared_let(:required_but_inactive_custom_field) do
|
||||
create(:text_project_custom_field,
|
||||
name: "Required inactive",
|
||||
is_required: true,
|
||||
project_custom_field_section:)
|
||||
end
|
||||
|
||||
it "renders activated required custom fields for new" do
|
||||
visit new_project_path
|
||||
|
||||
expect(page).to have_heading "New project"
|
||||
@@ -275,7 +284,12 @@ RSpec.describe "Projects", "creation",
|
||||
expect(page).to have_text("3 of 3")
|
||||
expect(page).to have_field "Required Foo *"
|
||||
expect(page).to have_field "Required User *"
|
||||
|
||||
# Optional fields should not be shown
|
||||
expect(page).to have_no_field "Optional Foo"
|
||||
|
||||
# Inactive fields, even if required, should not be shown
|
||||
expect(page).to have_no_field "Required Inactive *"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -329,7 +343,7 @@ RSpec.describe "Projects", "creation",
|
||||
fill_in "Required Foo", with: "Required value"
|
||||
end
|
||||
|
||||
it "enables custom fields with provided values for this project" do
|
||||
it "enables custom fields with provided values and for_all fields for this project" do
|
||||
click_on "Complete"
|
||||
|
||||
expect_and_dismiss_flash type: :success, message: "Successful creation."
|
||||
@@ -340,7 +354,7 @@ RSpec.describe "Projects", "creation",
|
||||
|
||||
# unused custom field should not be activated
|
||||
expect(project.project_custom_field_ids).to contain_exactly(
|
||||
required_custom_field.id
|
||||
required_custom_field.id, optional_custom_field.id
|
||||
)
|
||||
end
|
||||
|
||||
@@ -349,6 +363,7 @@ RSpec.describe "Projects", "creation",
|
||||
create(:project_custom_field, name: "Foo with default value",
|
||||
field_format: "string",
|
||||
is_required: true,
|
||||
is_for_all: true,
|
||||
default_value: "Default value",
|
||||
project_custom_field_section:)
|
||||
end
|
||||
@@ -365,7 +380,7 @@ RSpec.describe "Projects", "creation",
|
||||
|
||||
# custom_field_with_default_value should be activated and contain the default value
|
||||
expect(project.project_custom_field_ids).to contain_exactly(
|
||||
custom_field_with_default_value.id, required_custom_field.id
|
||||
custom_field_with_default_value.id, required_custom_field.id, optional_custom_field.id
|
||||
)
|
||||
|
||||
expect(project.custom_value_for(custom_field_with_default_value).value).to eq("Default value")
|
||||
@@ -384,7 +399,7 @@ RSpec.describe "Projects", "creation",
|
||||
|
||||
# custom_field_with_default_value should be activated and contain the overwritten value
|
||||
expect(project.project_custom_field_ids).to contain_exactly(
|
||||
custom_field_with_default_value.id, required_custom_field.id
|
||||
custom_field_with_default_value.id, required_custom_field.id, optional_custom_field.id
|
||||
)
|
||||
|
||||
expect(project.custom_value_for(custom_field_with_default_value).value).to eq("foo")
|
||||
@@ -395,6 +410,7 @@ RSpec.describe "Projects", "creation",
|
||||
shared_let(:invisible_field) do
|
||||
create(:string_project_custom_field, name: "Text for Admins only",
|
||||
is_required: true,
|
||||
is_for_all: true,
|
||||
admin_only: true,
|
||||
project_custom_field_section:)
|
||||
end
|
||||
@@ -414,7 +430,7 @@ RSpec.describe "Projects", "creation",
|
||||
project = Project.last
|
||||
|
||||
expect(project.project_custom_field_ids).to contain_exactly(
|
||||
required_custom_field.id, invisible_field.id
|
||||
required_custom_field.id, optional_custom_field.id, invisible_field.id
|
||||
)
|
||||
|
||||
expect(project.custom_value_for(invisible_field).typed_value).to eq("foo")
|
||||
@@ -438,7 +454,7 @@ RSpec.describe "Projects", "creation",
|
||||
project = Project.last
|
||||
|
||||
expect(project.project_custom_field_ids).to contain_exactly(
|
||||
required_custom_field.id
|
||||
required_custom_field.id, optional_custom_field.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,25 +34,48 @@ RSpec.describe Projects::Settings::CustomFieldsForm,
|
||||
type: :forms,
|
||||
with_ee: %i[calculated_values],
|
||||
with_flag: { calculated_value_project_attribute: true } do
|
||||
let(:string_project_custom_field) { create(:string_project_custom_field, name: "String field", is_required: true) }
|
||||
let(:boolean_project_custom_field) { create(:boolean_project_custom_field, name: "Boolean field", is_required: true) }
|
||||
let(:text_project_custom_field) { create(:text_project_custom_field, name: "Text field", is_required: true) }
|
||||
let(:integer_project_custom_field) { create(:integer_project_custom_field, name: "Integer field", is_required: true) }
|
||||
let(:float_project_custom_field) { create(:float_project_custom_field, name: "Float field", is_required: true) }
|
||||
let(:date_project_custom_field) { create(:date_project_custom_field, name: "Date field", is_required: true) }
|
||||
let(:string_project_custom_field) do
|
||||
create(:string_project_custom_field, name: "String field", is_required: true, is_for_all: true)
|
||||
end
|
||||
let(:boolean_project_custom_field) do
|
||||
create(:boolean_project_custom_field, name: "Boolean field", is_required: true, is_for_all: true)
|
||||
end
|
||||
let(:text_project_custom_field) do
|
||||
create(:text_project_custom_field, name: "Text field", is_required: true, is_for_all: true)
|
||||
end
|
||||
let(:integer_project_custom_field) do
|
||||
create(:integer_project_custom_field, name: "Integer field", is_required: true, is_for_all: true)
|
||||
end
|
||||
let(:float_project_custom_field) do
|
||||
create(:float_project_custom_field, name: "Float field", is_required: true, is_for_all: true)
|
||||
end
|
||||
let(:date_project_custom_field) do
|
||||
create(:date_project_custom_field, name: "Date field", is_required: true, is_for_all: true)
|
||||
end
|
||||
let(:list_project_custom_field) do
|
||||
create(:list_project_custom_field, name: "List field", is_required: true, possible_values: ["eins", "zwei", "drei"])
|
||||
create(:list_project_custom_field,
|
||||
name: "List field",
|
||||
is_required: true,
|
||||
is_for_all: true,
|
||||
possible_values: %w[eins zwei drei])
|
||||
end
|
||||
let(:multi_list_project_custom_field) do
|
||||
create(:list_project_custom_field,
|
||||
name: "Multi-list field",
|
||||
is_required: true,
|
||||
is_for_all: true,
|
||||
multi_value: true,
|
||||
possible_values: ["uno", "due", "tre", "quattro"])
|
||||
possible_values: %w[uno due tre quattro])
|
||||
end
|
||||
let(:version_project_custom_field) do
|
||||
create(:version_project_custom_field, name: "Version field", is_required: true, is_for_all: true)
|
||||
end
|
||||
let(:user_project_custom_field) do
|
||||
create(:user_project_custom_field, name: "User field", is_required: true, is_for_all: true)
|
||||
end
|
||||
let(:link_project_custom_field) do
|
||||
create(:link_project_custom_field, name: "Link field", is_required: true, is_for_all: true)
|
||||
end
|
||||
let(:version_project_custom_field) { create(:version_project_custom_field, name: "Version field", is_required: true) }
|
||||
let(:user_project_custom_field) { create(:user_project_custom_field, name: "User field", is_required: true) }
|
||||
let(:link_project_custom_field) { create(:link_project_custom_field, name: "Link field", is_required: true) }
|
||||
let!(:calculated_project_custom_field) do
|
||||
create(:calculated_value_project_custom_field, name: "Calculated field", is_required: true, is_for_all: true)
|
||||
end
|
||||
@@ -135,8 +158,35 @@ RSpec.describe Projects::Settings::CustomFieldsForm,
|
||||
end
|
||||
end
|
||||
|
||||
it "does not render the required calculated field" do
|
||||
it "does not render the activated and required calculated field" do
|
||||
expect(page).to have_no_text("Calculated field")
|
||||
expect(page).to have_no_field("Calculated field", disabled: true)
|
||||
end
|
||||
|
||||
context "for a model without custom values set" do
|
||||
let(:model) { build(:project) }
|
||||
|
||||
let!(:required_inactive_field) do
|
||||
create(:string_project_custom_field, name: "Required inactive field", is_required: true, is_for_all: false)
|
||||
end
|
||||
let!(:optional_active_field) do
|
||||
create(:string_project_custom_field, name: "Optional active field", is_required: false, is_for_all: true)
|
||||
end
|
||||
let!(:required_active_field) do
|
||||
create(:string_project_custom_field, name: "Required active field", is_required: true, is_for_all: true)
|
||||
end
|
||||
|
||||
include_context "with rendered form"
|
||||
|
||||
it "only renders required, activated fields" do
|
||||
expect(page).to have_no_text("Required inactive field")
|
||||
expect(page).to have_no_field("Required inactive field", disabled: true)
|
||||
|
||||
expect(page).to have_no_text("Optional active field")
|
||||
expect(page).to have_no_field("Optional active field", disabled: true)
|
||||
|
||||
expect(page).to have_text("Required active field")
|
||||
expect(page).to have_field("Required active field", required: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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"
|
||||
require Rails.root.join("db/migrate/20251211160744_set_is_for_all_and_unset_required")
|
||||
|
||||
RSpec.describe SetIsForAllAndUnsetRequired, type: :model,
|
||||
with_ee: %i[calculated_values],
|
||||
with_flag: { calculated_value_project_attribute: true } do
|
||||
# Project custom fields to be migrated
|
||||
shared_let(:required_project_cf) { create(:project_custom_field, :integer, is_required: true, is_for_all: false) }
|
||||
shared_let(:optional_project_cf) { create(:project_custom_field, :integer, is_required: false, is_for_all: false) }
|
||||
shared_let(:required_boolean_project_cf) { create(:project_custom_field, :boolean, is_required: true, is_for_all: false) }
|
||||
|
||||
# Regular custom fields, to be ignored by the migration
|
||||
shared_let(:required_cf) { create(:custom_field, :integer, is_required: true, is_for_all: false) }
|
||||
shared_let(:optional_cf) { create(:custom_field, :integer, is_required: false, is_for_all: false) }
|
||||
|
||||
shared_let(:required_boolean_wp_cf) { create(:wp_custom_field, :boolean, is_required: true, is_for_all: false) }
|
||||
shared_let(:required_int_wp_cf) { create(:wp_custom_field, :integer, is_required: true, is_for_all: false) }
|
||||
shared_let(:required_boolean_te_cf) { create(:time_entry_custom_field, :boolean, is_required: true, is_for_all: false) }
|
||||
shared_let(:required_int_te_cf) { create(:time_entry_custom_field, :integer, is_required: true, is_for_all: false) }
|
||||
shared_let(:required_boolean_user_cf) { create(:user_custom_field, :boolean, is_required: true, is_for_all: false) }
|
||||
shared_let(:required_int_user_cf) { create(:user_custom_field, :integer, is_required: true, is_for_all: false) }
|
||||
shared_let(:required_boolean_group_cf) { create(:group_custom_field, :boolean, is_required: true, is_for_all: false) }
|
||||
shared_let(:required_int_group_cf) { create(:group_custom_field, :integer, is_required: true, is_for_all: false) }
|
||||
|
||||
# Cannot be done as a shared_let as with_ee and with_flag haven't taken hold when shared_let is run
|
||||
let!(:required_calculated_project_cf) { create(:project_custom_field, :calculated_value, is_required: true) }
|
||||
|
||||
it "updates required project custom fields as well as boolean and calculated values" do # rubocop:disable RSpec/MultipleExpectations
|
||||
ActiveRecord::Migration.suppress_messages { described_class.migrate(:up) }
|
||||
|
||||
expect(required_project_cf.reload.is_for_all).to be_truthy
|
||||
expect(optional_project_cf.reload.is_for_all).to be_falsey
|
||||
expect(required_boolean_project_cf.reload.is_for_all).to be_truthy
|
||||
expect(required_calculated_project_cf.reload.is_for_all).to be_truthy
|
||||
|
||||
# ignores non project required custom fields
|
||||
|
||||
expect(required_cf.reload.is_for_all).to be_falsey
|
||||
expect(optional_cf.reload.is_for_all).to be_falsey
|
||||
|
||||
# Keeps the existing required values except for boolean and calculated values
|
||||
# which are set to false
|
||||
expect(required_project_cf.is_required).to be_truthy
|
||||
expect(optional_project_cf.is_required).to be_falsey
|
||||
expect(required_boolean_project_cf.is_required).to be_falsey
|
||||
expect(required_calculated_project_cf.is_required).to be_falsey
|
||||
|
||||
# This happens for all boolean fields
|
||||
expect(required_boolean_wp_cf.reload.is_required).to be_falsey
|
||||
expect(required_int_wp_cf.reload.is_required).to be_truthy
|
||||
expect(required_boolean_te_cf.reload.is_required).to be_falsey
|
||||
expect(required_int_te_cf.reload.is_required).to be_truthy
|
||||
expect(required_boolean_user_cf.reload.is_required).to be_falsey
|
||||
expect(required_int_user_cf.reload.is_required).to be_truthy
|
||||
expect(required_boolean_group_cf.reload.is_required).to be_falsey
|
||||
expect(required_int_group_cf.reload.is_required).to be_truthy
|
||||
end
|
||||
end
|
||||
@@ -249,6 +249,9 @@ RSpec.describe CustomField::CalculatedValue,
|
||||
let!(:other_calculated_value) do
|
||||
create(:calculated_value_project_custom_field, formula: "2 + 2", projects: [project_without_permission])
|
||||
end
|
||||
let!(:weighted_item_list) do
|
||||
create(:project_custom_field, :weighted_item_list, projects: [project_without_permission])
|
||||
end
|
||||
|
||||
current_user { user }
|
||||
|
||||
|
||||
@@ -33,10 +33,6 @@ require "spec_helper"
|
||||
RSpec.describe GlobalRole do
|
||||
let!(:global_role) { create(:global_role, name: "globalrole", permissions: ["permissions"]) }
|
||||
|
||||
it { is_expected.to validate_presence_of :name }
|
||||
it { is_expected.to validate_uniqueness_of :name }
|
||||
it { is_expected.to validate_length_of(:name).is_at_most(256) }
|
||||
|
||||
describe "attributes" do
|
||||
subject(:role) { described_class.new }
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ require "spec_helper"
|
||||
|
||||
RSpec.describe ProjectCustomField do
|
||||
describe "activation in projects" do
|
||||
context "when creating a new required project custom field" do
|
||||
context "when creating a new 'is_for_all' project custom field" do
|
||||
let!(:project) { create(:project) }
|
||||
let!(:another_project) { create(:project) }
|
||||
|
||||
it "activates the required project custom fields in all projects" do
|
||||
project_custom_field = create(:project_custom_field, is_required: true)
|
||||
it "activates the project custom fields in all projects" do
|
||||
project_custom_field = create(:project_custom_field, is_for_all: true)
|
||||
|
||||
expect(ProjectCustomFieldProjectMapping).to exist(custom_field_id: project_custom_field.id,
|
||||
project_id: project.id)
|
||||
@@ -46,20 +46,38 @@ RSpec.describe ProjectCustomField do
|
||||
end
|
||||
end
|
||||
|
||||
context "when setting an existing project custom field to required" do
|
||||
context "when creating a new project custom field" do
|
||||
let!(:project) { create(:project) }
|
||||
let!(:another_project) { create(:project) }
|
||||
|
||||
it "activates the custom field in projects it is assigned to" do
|
||||
# Same activation rules apply for optional and required custom fields:
|
||||
optional_cf = create(:project_custom_field, projects: [project])
|
||||
required_cf = create(:project_custom_field, is_required: true, projects: [project])
|
||||
|
||||
[optional_cf, required_cf].each do |cf|
|
||||
expect(ProjectCustomFieldProjectMapping).to exist(custom_field_id: cf.id,
|
||||
project_id: project.id)
|
||||
expect(ProjectCustomFieldProjectMapping).not_to exist(custom_field_id: cf.id,
|
||||
project_id: another_project.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when setting an existing project custom field to is_for_all" do
|
||||
let!(:project_custom_field) { create(:string_project_custom_field) } # optional now
|
||||
let!(:project) do
|
||||
create(:project, custom_field_values: { "#{project_custom_field.id}": "foo" })
|
||||
end
|
||||
let!(:another_project) { create(:project) } # not using the custom field
|
||||
|
||||
it "activates the required project custom fields in all projects where it is not already activated" do
|
||||
it "activates for_all project custom fields in all projects where it is not already activated" do
|
||||
expect(ProjectCustomFieldProjectMapping).to exist(custom_field_id: project_custom_field.id,
|
||||
project_id: project.id)
|
||||
expect(ProjectCustomFieldProjectMapping).not_to exist(custom_field_id: project_custom_field.id,
|
||||
project_id: another_project.id)
|
||||
|
||||
project_custom_field.update!(is_required: true) # required now
|
||||
project_custom_field.update!(is_for_all: true) # forced active now
|
||||
|
||||
expect(ProjectCustomFieldProjectMapping).to exist(custom_field_id: project_custom_field.id,
|
||||
project_id: project.id)
|
||||
@@ -67,9 +85,9 @@ RSpec.describe ProjectCustomField do
|
||||
project_id: another_project.id)
|
||||
end
|
||||
|
||||
it "does not disabled project custom fields when set to optional" do
|
||||
project_custom_field.update!(is_required: true) # required now
|
||||
project_custom_field.update!(is_required: false) # optional again
|
||||
it "does not disable project custom fields when set to optional" do
|
||||
project_custom_field.update!(is_for_all: true) # forced active now
|
||||
project_custom_field.update!(is_for_all: false) # optional again
|
||||
|
||||
expect(ProjectCustomFieldProjectMapping).to exist(custom_field_id: project_custom_field.id,
|
||||
project_id: project.id)
|
||||
@@ -78,7 +96,7 @@ RSpec.describe ProjectCustomField do
|
||||
end
|
||||
|
||||
it "does not create duplicate mappings" do
|
||||
project_custom_field.update!(is_required: true) # required now
|
||||
project_custom_field.update!(is_for_all: true) # activated now
|
||||
|
||||
# mapping existed before, should not be duplicated
|
||||
expect(ProjectCustomFieldProjectMapping.where(project_id: project.id,
|
||||
|
||||
@@ -84,7 +84,7 @@ RSpec.describe Project, "customizable" do
|
||||
|
||||
context "with a custom field activated in different projects " \
|
||||
"and the user has view_project_attributes permission in one of the project " \
|
||||
"and with a required custom field" do
|
||||
"and with a custom field assigned to all projects" do
|
||||
let(:other_project) { create(:project) }
|
||||
let!(:project_cf) do
|
||||
# This custom field is enabled in both project and other_project to test that there is no
|
||||
@@ -96,8 +96,8 @@ RSpec.describe Project, "customizable" do
|
||||
end
|
||||
end
|
||||
|
||||
let!(:required_cf) do
|
||||
create(:string_project_custom_field, is_required: true)
|
||||
let!(:for_all_cf) do
|
||||
create(:string_project_custom_field, is_for_all: true)
|
||||
end
|
||||
|
||||
let(:user) do
|
||||
@@ -112,7 +112,7 @@ RSpec.describe Project, "customizable" do
|
||||
.to be_empty
|
||||
|
||||
expect(other_project.available_custom_fields)
|
||||
.to contain_exactly(project_cf, required_cf)
|
||||
.to contain_exactly(project_cf, for_all_cf)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -391,12 +391,14 @@ RSpec.describe Project, "customizable" do
|
||||
|
||||
let!(:required_text_custom_field) do
|
||||
create(:text_project_custom_field,
|
||||
is_for_all: true,
|
||||
is_required: true,
|
||||
project_custom_field_section: another_section)
|
||||
end
|
||||
|
||||
let!(:required_calculated_custom_field) do
|
||||
create(:calculated_value_project_custom_field,
|
||||
is_for_all: true,
|
||||
is_required: true,
|
||||
project_custom_field_section: another_section)
|
||||
end
|
||||
|
||||
@@ -35,6 +35,10 @@ RSpec.describe Role do
|
||||
let(:build_role) { build(:project_role, permissions:) }
|
||||
let(:created_role) { create(:project_role, permissions:) }
|
||||
|
||||
it { is_expected.to validate_presence_of :name }
|
||||
it { is_expected.to validate_uniqueness_of(:name).case_insensitive }
|
||||
it { is_expected.to validate_length_of(:name).is_at_most(256) }
|
||||
|
||||
describe ".create" do
|
||||
it "is prevented for type Role" do
|
||||
build_role.type = described_class.name
|
||||
|
||||
@@ -10,12 +10,6 @@ RSpec.describe WorkPackageRole do
|
||||
permissions: %w[permissions])
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to validate_presence_of :name }
|
||||
it { is_expected.to validate_uniqueness_of :name }
|
||||
it { is_expected.to validate_length_of(:name).is_at_most(256) }
|
||||
end
|
||||
|
||||
describe "#member?" do
|
||||
it "is one (even though it is builtin)" do
|
||||
expect(work_package_role).to be_member
|
||||
|
||||
@@ -119,11 +119,109 @@ RSpec.describe "API v3 Project resource create", content_type: :json do
|
||||
end
|
||||
|
||||
describe "custom fields" do
|
||||
context "with a required custom field" do
|
||||
context "with an optional custom field" do
|
||||
shared_let(:optional_custom_field) do
|
||||
create(:text_project_custom_field,
|
||||
name: "Department",
|
||||
is_for_all: true)
|
||||
end
|
||||
|
||||
shared_examples "creates a project with an empty custom value" do
|
||||
it "responds with 201" do
|
||||
expect(last_response).to have_http_status(:created)
|
||||
end
|
||||
|
||||
it "returns the newly created project" do
|
||||
expect(last_response.body)
|
||||
.to be_json_eql("Project".to_json)
|
||||
.at_path("_type")
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql("Project name".to_json)
|
||||
.at_path("name")
|
||||
end
|
||||
|
||||
it "creates a project with an empty custom field value" do
|
||||
project = Project.last
|
||||
expect(project.typed_custom_value_for(optional_custom_field))
|
||||
.to eq("")
|
||||
end
|
||||
|
||||
it "automatically activates the cf for project" do
|
||||
expect(Project.last.project_custom_fields)
|
||||
.to contain_exactly(optional_custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no custom field value is provided" do
|
||||
let(:body) do
|
||||
{
|
||||
identifier: "new_project_identifier",
|
||||
name: "Project name"
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it_behaves_like "creates a project with an empty custom value"
|
||||
end
|
||||
|
||||
context "when the custom field is provided but empty" do
|
||||
let(:body) do
|
||||
{
|
||||
identifier: "new_project_identifier",
|
||||
name: "Project name",
|
||||
optional_custom_field.attribute_name(:camel_case) => {
|
||||
raw: ""
|
||||
}
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it_behaves_like "creates a project with an empty custom value"
|
||||
end
|
||||
|
||||
context "when the custom field value is provided and valid" do
|
||||
let(:body) do
|
||||
{
|
||||
identifier: "new_project_identifier",
|
||||
name: "Project name",
|
||||
optional_custom_field.attribute_name(:camel_case) => {
|
||||
raw: "Engineering"
|
||||
}
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it "responds with 201" do
|
||||
expect(last_response).to have_http_status(:created)
|
||||
end
|
||||
|
||||
it "returns the newly created project" do
|
||||
expect(last_response.body)
|
||||
.to be_json_eql("Project".to_json)
|
||||
.at_path("_type")
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql("Project name".to_json)
|
||||
.at_path("name")
|
||||
end
|
||||
|
||||
it "creates a project with the custom field value" do
|
||||
project = Project.last
|
||||
expect(project.typed_custom_value_for(optional_custom_field))
|
||||
.to eq("Engineering")
|
||||
end
|
||||
|
||||
it "automatically activates the cf for project if the value was provided" do
|
||||
expect(Project.last.project_custom_fields)
|
||||
.to contain_exactly(optional_custom_field)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a required for_all custom field" do
|
||||
shared_let(:required_custom_field) do
|
||||
create(:text_project_custom_field,
|
||||
name: "Department",
|
||||
is_required: true)
|
||||
is_required: true,
|
||||
is_for_all: true)
|
||||
end
|
||||
|
||||
context "when no custom field value is provided" do
|
||||
|
||||
@@ -437,8 +437,8 @@ RSpec.describe "API v3 Project resource index", content_type: :json do
|
||||
create(:project_custom_field_project_mapping, project: public_wp_share_project)
|
||||
.project_custom_field
|
||||
end
|
||||
shared_let(:required_cf) do
|
||||
create(:string_project_custom_field, is_required: true)
|
||||
shared_let(:for_all_cf) do
|
||||
create(:string_project_custom_field, is_for_all: true)
|
||||
end
|
||||
|
||||
shared_let(:current_user) do
|
||||
@@ -508,22 +508,22 @@ RSpec.describe "API v3 Project resource index", content_type: :json do
|
||||
)
|
||||
end
|
||||
|
||||
it "returns the required_cf only for the other_project as a member " \
|
||||
it "returns the for_all_cf only for the other_project as a member " \
|
||||
"with view_project_attributes" do
|
||||
expect(subject).not_to have_json_path(
|
||||
"_embedded/elements/0/#{required_cf.attribute_name(:camel_case)}"
|
||||
"_embedded/elements/0/#{for_all_cf.attribute_name(:camel_case)}"
|
||||
)
|
||||
expect(subject).not_to have_json_path(
|
||||
"_embedded/elements/1/#{required_cf.attribute_name(:camel_case)}"
|
||||
"_embedded/elements/1/#{for_all_cf.attribute_name(:camel_case)}"
|
||||
)
|
||||
expect(subject).not_to have_json_path(
|
||||
"_embedded/elements/2/#{required_cf.attribute_name(:camel_case)}"
|
||||
"_embedded/elements/2/#{for_all_cf.attribute_name(:camel_case)}"
|
||||
)
|
||||
expect(subject).not_to have_json_path(
|
||||
"_embedded/elements/3/#{required_cf.attribute_name(:camel_case)}"
|
||||
"_embedded/elements/3/#{for_all_cf.attribute_name(:camel_case)}"
|
||||
)
|
||||
expect(subject).to have_json_path(
|
||||
"_embedded/elements/4/#{required_cf.attribute_name(:camel_case)}"
|
||||
"_embedded/elements/4/#{for_all_cf.attribute_name(:camel_case)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -379,6 +379,7 @@ RSpec.shared_examples_for "APIv3 workspace update form" do
|
||||
let!(:required_custom_field) do
|
||||
create(:text_project_custom_field,
|
||||
name: "Department",
|
||||
is_for_all: true,
|
||||
is_required: true)
|
||||
end
|
||||
|
||||
|
||||
@@ -90,10 +90,11 @@ RSpec.shared_examples_for "APIv3 workspace update" do
|
||||
end
|
||||
|
||||
describe "custom fields" do
|
||||
context "with a required custom field" do
|
||||
context "with a required for_all custom field" do
|
||||
let!(:required_custom_field) do
|
||||
create(:text_project_custom_field,
|
||||
name: "Department",
|
||||
is_for_all: true,
|
||||
is_required: true)
|
||||
end
|
||||
|
||||
@@ -195,6 +196,19 @@ RSpec.shared_examples_for "APIv3 workspace update" do
|
||||
expect(workspace.project_custom_fields)
|
||||
.to contain_exactly(custom_field)
|
||||
end
|
||||
|
||||
context "when the field is for_all, but not required" do
|
||||
let!(:for_all_custom_field) do
|
||||
create(:text_project_custom_field,
|
||||
name: "Department",
|
||||
is_for_all: true)
|
||||
end
|
||||
|
||||
it "automatically activates the cf for workspace even if no value was provided" do
|
||||
expect(workspace.project_custom_fields)
|
||||
.to contain_exactly(for_all_custom_field, custom_field)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with an admin only custom field" do
|
||||
@@ -277,6 +291,31 @@ RSpec.shared_examples_for "APIv3 workspace update" do
|
||||
.to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "and when the custom field is forced active (is_for_all)" do
|
||||
let(:admin_only_custom_field) do
|
||||
create(:text_project_custom_field, admin_only: true, is_for_all: true)
|
||||
end
|
||||
let(:body) do
|
||||
{
|
||||
name: "Updated workspace name"
|
||||
}
|
||||
end
|
||||
|
||||
it "responds with 200 OK" do
|
||||
expect(last_response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it "does not set the cf value" do
|
||||
expect(workspace.reload.typed_custom_value_for(admin_only_custom_field))
|
||||
.to be_nil
|
||||
end
|
||||
|
||||
it "does activate the cf for workspace" do
|
||||
expect(workspace.reload.project_custom_fields)
|
||||
.to contain_exactly(admin_only_custom_field)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,6 +52,14 @@ RSpec.describe ProjectCustomFieldProjectMappings::BulkUpdateService do
|
||||
project_custom_field_section:)
|
||||
end
|
||||
|
||||
shared_let(:visible_activated_project_custom_field) do
|
||||
create(:project_custom_field,
|
||||
name: "Visible activated field",
|
||||
admin_only: false,
|
||||
is_for_all: true,
|
||||
project_custom_field_section:)
|
||||
end
|
||||
|
||||
shared_let(:invisible_project_custom_field) do
|
||||
create(:project_custom_field,
|
||||
name: "Admin only field",
|
||||
@@ -62,23 +70,23 @@ RSpec.describe ProjectCustomFieldProjectMappings::BulkUpdateService do
|
||||
context "with admin permissions" do
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
it "bulk enables/disables all (non-required) fields of the section, including invisible ones" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
visible_required_project_custom_field
|
||||
)
|
||||
it "bulk enables/disables all (non-for_all) fields of the section, including invisible ones" do
|
||||
expect(project.project_custom_fields).to contain_exactly(visible_activated_project_custom_field)
|
||||
|
||||
expect(instance.call(action: :enable)).to be_success
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
visible_required_project_custom_field, visible_project_custom_field, invisible_project_custom_field
|
||||
)
|
||||
expected = [
|
||||
visible_activated_project_custom_field,
|
||||
visible_required_project_custom_field,
|
||||
visible_project_custom_field,
|
||||
invisible_project_custom_field
|
||||
]
|
||||
expect(project.reload.project_custom_fields).to match_array(expected)
|
||||
|
||||
expect(instance.call(action: :disable)).to be_success
|
||||
|
||||
# required fields cannot be disabled, even not by admins
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
visible_required_project_custom_field
|
||||
)
|
||||
# for_all fields cannot be disabled, even not by admins
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(visible_activated_project_custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -97,24 +105,27 @@ RSpec.describe ProjectCustomFieldProjectMappings::BulkUpdateService do
|
||||
end
|
||||
|
||||
it "bulk enables/disables all fields of the section, excluding invisible ones" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
visible_required_project_custom_field
|
||||
)
|
||||
expect(project.project_custom_fields).to contain_exactly(visible_activated_project_custom_field)
|
||||
|
||||
expect(instance.call(action: :enable)).to be_success
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
visible_required_project_custom_field, visible_project_custom_field
|
||||
)
|
||||
expected = [
|
||||
visible_activated_project_custom_field,
|
||||
visible_required_project_custom_field,
|
||||
visible_project_custom_field
|
||||
]
|
||||
expect(project.reload.project_custom_fields).to match_array(expected)
|
||||
|
||||
project.project_custom_fields << invisible_project_custom_field
|
||||
|
||||
expect(instance.call(action: :disable)).to be_success
|
||||
|
||||
# required fields cannot be disabled, invisible fields are not affected by non-admins
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
visible_required_project_custom_field, invisible_project_custom_field
|
||||
)
|
||||
# force-activated fields cannot be disabled, invisible fields are not affected by non-admins
|
||||
expected = [
|
||||
visible_activated_project_custom_field,
|
||||
invisible_project_custom_field
|
||||
]
|
||||
expect(project.reload.project_custom_fields).to match_array(expected)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -132,15 +143,11 @@ RSpec.describe ProjectCustomFieldProjectMappings::BulkUpdateService do
|
||||
end
|
||||
|
||||
it "cannot bulk enable/disable project custom fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
visible_required_project_custom_field
|
||||
)
|
||||
expect(project.project_custom_fields).to contain_exactly(visible_activated_project_custom_field)
|
||||
|
||||
expect(instance.call(action: :enable)).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
visible_required_project_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(visible_activated_project_custom_field)
|
||||
|
||||
expect(instance.call(action: :disable)).to be_failure
|
||||
end
|
||||
|
||||
@@ -46,12 +46,20 @@ RSpec.describe ProjectCustomFieldProjectMappings::ToggleService do
|
||||
|
||||
shared_let(:required_custom_field) do
|
||||
create(:project_custom_field,
|
||||
name: "Visible required field",
|
||||
name: "Required field",
|
||||
admin_only: false,
|
||||
is_required: true,
|
||||
project_custom_field_section:)
|
||||
end
|
||||
|
||||
shared_let(:forced_active_custom_field) do
|
||||
create(:project_custom_field,
|
||||
name: "Visible forced-active field",
|
||||
admin_only: false,
|
||||
is_for_all: true,
|
||||
project_custom_field_section:)
|
||||
end
|
||||
|
||||
shared_let(:invisible_custom_field) do
|
||||
create(:project_custom_field,
|
||||
name: "Admin only field",
|
||||
@@ -61,71 +69,73 @@ RSpec.describe ProjectCustomFieldProjectMappings::ToggleService do
|
||||
|
||||
let(:visible_custom_field_params) { { project_id: project.id, custom_field_id: visible_custom_field.id } }
|
||||
let(:required_custom_field_params) { { project_id: project.id, custom_field_id: required_custom_field.id } }
|
||||
let(:forced_active_custom_field_params) { { project_id: project.id, custom_field_id: forced_active_custom_field.id } }
|
||||
let(:invisible_custom_field_params) { { project_id: project.id, custom_field_id: invisible_custom_field.id } }
|
||||
|
||||
context "with admin permissions" do
|
||||
shared_let(:user) { create(:admin) }
|
||||
|
||||
it "toggles visible, non-required fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
it "toggles visible, non-is_for_all fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
2.times do
|
||||
expect(instance.call(**visible_custom_field_params, value: "1")).to be_success
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field, visible_custom_field
|
||||
)
|
||||
expected = [forced_active_custom_field, visible_custom_field]
|
||||
expect(project.reload.project_custom_fields).to match_array(expected)
|
||||
end
|
||||
|
||||
2.times do
|
||||
expect(instance.call(**visible_custom_field_params, value: "0")).to be_success
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
it "toggles invisible, non-required fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
it "toggles invisible, non-is_for_all fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
2.times do
|
||||
expect(instance.call(**invisible_custom_field_params, value: "1")).to be_success
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field, invisible_custom_field
|
||||
)
|
||||
expected = [forced_active_custom_field, invisible_custom_field]
|
||||
expect(project.reload.project_custom_fields).to match_array(expected)
|
||||
end
|
||||
|
||||
2.times do
|
||||
expect(instance.call(**invisible_custom_field_params, value: "0")).to be_success
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
it "does not toggle required fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
it "does not toggle is_for_all fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**required_custom_field_params, value: "1")).to be_failure
|
||||
expect(instance.call(**forced_active_custom_field_params, value: "1")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**required_custom_field_params, value: "0")).to be_failure
|
||||
expect(instance.call(**forced_active_custom_field_params, value: "0")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
end
|
||||
|
||||
it "does toggle required fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
2.times do
|
||||
expect(instance.call(**required_custom_field_params, value: "1")).to be_success
|
||||
|
||||
expected = [forced_active_custom_field, required_custom_field]
|
||||
expect(project.reload.project_custom_fields).to match_array(expected)
|
||||
end
|
||||
|
||||
2.times do
|
||||
expect(instance.call(**required_custom_field_params, value: "0")).to be_success
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -143,62 +153,45 @@ RSpec.describe ProjectCustomFieldProjectMappings::ToggleService do
|
||||
})
|
||||
end
|
||||
|
||||
it "toggles visible, non-required fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
it "toggles visible, non-is_for_all fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
2.times do
|
||||
expect(instance.call(**visible_custom_field_params, value: "1")).to be_success
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field, visible_custom_field
|
||||
)
|
||||
expected = [forced_active_custom_field, visible_custom_field]
|
||||
expect(project.reload.project_custom_fields).to match_array(expected)
|
||||
end
|
||||
|
||||
2.times do
|
||||
expect(instance.call(**visible_custom_field_params, value: "0")).to be_success
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
it "does not toggle invisible, non-required fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**invisible_custom_field_params, value: "1")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**invisible_custom_field_params, value: "0")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
end
|
||||
|
||||
it "does not toggle required fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
it "does not toggle is_for_all fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**required_custom_field_params, value: "1")).to be_failure
|
||||
expect(instance.call(**forced_active_custom_field_params, value: "1")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**required_custom_field_params, value: "0")).to be_failure
|
||||
expect(instance.call(**forced_active_custom_field_params, value: "0")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -215,58 +208,40 @@ RSpec.describe ProjectCustomFieldProjectMappings::ToggleService do
|
||||
})
|
||||
end
|
||||
|
||||
it "does not toggle visible, non-required fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
it "does not toggle visible, non-is_for_all fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**visible_custom_field_params, value: "1")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**visible_custom_field_params, value: "0")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
end
|
||||
|
||||
it "does not toggle invisible, non-required fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
it "does not toggle invisible, non-is_for_all fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**invisible_custom_field_params, value: "1")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**invisible_custom_field_params, value: "0")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
end
|
||||
|
||||
it "does not toggle required fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
it "does not toggle is_for_all fields" do
|
||||
expect(project.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**required_custom_field_params, value: "1")).to be_failure
|
||||
expect(instance.call(**forced_active_custom_field_params, value: "1")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
|
||||
expect(instance.call(**required_custom_field_params, value: "0")).to be_failure
|
||||
expect(instance.call(**forced_active_custom_field_params, value: "0")).to be_failure
|
||||
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(
|
||||
required_custom_field
|
||||
)
|
||||
expect(project.reload.project_custom_fields).to contain_exactly(forced_active_custom_field)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -166,16 +166,16 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
context "with required custom fields",
|
||||
context "with for_all custom fields",
|
||||
with_ee: %i[calculated_values],
|
||||
with_flag: { calculated_value_project_attribute: true } do
|
||||
let!(:calculated_custom_field) do
|
||||
create(:calculated_value_project_custom_field,
|
||||
project_custom_field_section: section)
|
||||
end
|
||||
let!(:required_calculated_custom_field) do
|
||||
let!(:for_all_calculated_custom_field) do
|
||||
create(:calculated_value_project_custom_field,
|
||||
is_required: true,
|
||||
is_for_all: true,
|
||||
project_custom_field_section: section)
|
||||
end
|
||||
|
||||
@@ -183,10 +183,10 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
{ custom_field_values: { text_custom_field.id => "foo" } }
|
||||
end
|
||||
|
||||
it "activates required calculated custom fields even if no value is provided" do
|
||||
it "activates calculated custom fields even if no value is provided" do
|
||||
subject
|
||||
expect(project.project_custom_field_project_mappings.pluck(:custom_field_id))
|
||||
.to contain_exactly(required_calculated_custom_field.id, text_custom_field.id)
|
||||
.to contain_exactly(for_all_calculated_custom_field.id, text_custom_field.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -230,7 +230,7 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
describe "calculated custom fields",
|
||||
with_ee: %i[calculated_values],
|
||||
with_flag: { calculated_value_project_attribute: true } do
|
||||
shared_let(:cf_static) { create(:integer_project_custom_field, is_required: true) }
|
||||
shared_let(:cf_static) { create(:integer_project_custom_field, is_for_all: true) }
|
||||
let(:project) { create(:project) }
|
||||
let!(:model_instance) { project }
|
||||
let(:stub_model_instance) { false }
|
||||
@@ -248,7 +248,7 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
context "when trying to explicitly set values of calculated custom fields" do
|
||||
let!(:cf_calculated) do
|
||||
create(:calculated_value_project_custom_field,
|
||||
is_required: true, formula: "1 + 1")
|
||||
is_for_all: true, formula: "1 + 1")
|
||||
end
|
||||
|
||||
let(:project_attributes) do
|
||||
@@ -269,11 +269,11 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
context "when setting value of field referenced in calculated values" do
|
||||
let!(:cf_calculated1) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_static} * 7")
|
||||
is_for_all: true, formula: "#{cf_static} * 7")
|
||||
end
|
||||
let!(:cf_calculated2) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_calculated1} * 11")
|
||||
is_for_all: true, formula: "#{cf_calculated1} * 11")
|
||||
end
|
||||
|
||||
let(:project_attributes) do
|
||||
@@ -296,11 +296,11 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
context "when not setting value of field referenced in calculated values" do
|
||||
let!(:cf_calculated1) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_static} * 7")
|
||||
is_for_all: true, formula: "#{cf_static} * 7")
|
||||
end
|
||||
let!(:cf_calculated2) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_calculated1} * 11")
|
||||
is_for_all: true, formula: "#{cf_calculated1} * 11")
|
||||
end
|
||||
|
||||
let(:project_attributes) do
|
||||
@@ -326,15 +326,15 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
let!(:cf_c) { create(:integer_project_custom_field) }
|
||||
let!(:cf_calculated1) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_a} * 7")
|
||||
is_for_all: true, formula: "#{cf_a} * 7")
|
||||
end
|
||||
let!(:cf_calculated2) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_b} * 11")
|
||||
is_for_all: true, formula: "#{cf_b} * 11")
|
||||
end
|
||||
let!(:cf_calculated3) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_c} * 13")
|
||||
is_for_all: true, formula: "#{cf_c} * 13")
|
||||
end
|
||||
|
||||
let(:project_attributes) do
|
||||
@@ -362,7 +362,7 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
context "when intermediate calculated value field is not enabled" do
|
||||
let!(:cf_calculated1) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_static} * 7")
|
||||
is_for_all: true, formula: "#{cf_static} * 7")
|
||||
end
|
||||
let!(:cf_calculated2) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
@@ -370,7 +370,7 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
end
|
||||
let!(:cf_calculated3) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_calculated2} * 13")
|
||||
is_for_all: true, formula: "#{cf_calculated2} * 13")
|
||||
end
|
||||
|
||||
let(:project_attributes) do
|
||||
@@ -400,15 +400,15 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
context "when intermediate calculated value field is for admin only" do
|
||||
let!(:cf_calculated1) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_static} * 7")
|
||||
is_for_all: true, formula: "#{cf_static} * 7")
|
||||
end
|
||||
let!(:cf_calculated2) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations, :admin_only,
|
||||
is_required: true, formula: "#{cf_calculated1} * 11")
|
||||
is_for_all: true, formula: "#{cf_calculated1} * 11")
|
||||
end
|
||||
let!(:cf_calculated3) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_calculated2} * 13")
|
||||
is_for_all: true, formula: "#{cf_calculated2} * 13")
|
||||
end
|
||||
|
||||
let(:project_attributes) do
|
||||
@@ -432,11 +432,11 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
context "when referenced value field is for admin only" do
|
||||
let!(:cf_calculated) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations,
|
||||
is_required: true, formula: "#{cf_static} * #{cf_referenced}")
|
||||
is_for_all: true, formula: "#{cf_static} * #{cf_referenced}")
|
||||
end
|
||||
|
||||
context "when referenced value is static" do
|
||||
let!(:cf_referenced) { create(:integer_project_custom_field, :admin_only, is_required: true) }
|
||||
let!(:cf_referenced) { create(:integer_project_custom_field, :admin_only, is_for_all: true) }
|
||||
|
||||
let(:project_attributes) do
|
||||
{
|
||||
@@ -466,7 +466,7 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
context "when referenced value is calculated value without references" do
|
||||
let!(:cf_referenced) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations, :admin_only,
|
||||
is_required: true, formula: "2")
|
||||
is_for_all: true, formula: "2")
|
||||
end
|
||||
|
||||
let(:project_attributes) do
|
||||
@@ -494,10 +494,10 @@ RSpec.describe Projects::CreateService, type: :model do
|
||||
end
|
||||
|
||||
context "when referenced value is calculated value with another reference" do
|
||||
let!(:cf_referenced1) { create(:integer_project_custom_field, :admin_only, is_required: true) }
|
||||
let!(:cf_referenced1) { create(:integer_project_custom_field, :admin_only, is_for_all: true) }
|
||||
let!(:cf_referenced) do
|
||||
create(:calculated_value_project_custom_field, :skip_validations, :admin_only,
|
||||
is_required: true, formula: "-1 * #{cf_referenced1}")
|
||||
is_for_all: true, formula: "-1 * #{cf_referenced1}")
|
||||
end
|
||||
|
||||
let(:project_attributes) do
|
||||
|
||||
@@ -66,7 +66,7 @@ RSpec.describe CustomFields::RecalculateValuesJob, type: :model do
|
||||
end
|
||||
|
||||
context "when custom field is calculated value" do
|
||||
context "and is not required" do
|
||||
context "and is not activated for all projects" do
|
||||
context "with formula referencing other fields" do
|
||||
let!(:static) { create(:integer_project_custom_field, projects:) }
|
||||
let!(:custom_field) do
|
||||
@@ -115,9 +115,9 @@ RSpec.describe CustomFields::RecalculateValuesJob, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
context "and is required" do
|
||||
context "and is activated for all projects" do
|
||||
context "with a static formula" do
|
||||
let!(:custom_field) { create(:calculated_value_project_custom_field, formula: "1 + 1", is_required: true) }
|
||||
let!(:custom_field) { create(:calculated_value_project_custom_field, formula: "1 + 1", is_for_all: true) }
|
||||
let(:custom_field_id) { custom_field.id }
|
||||
|
||||
it "updates calculated values on all objects" do
|
||||
@@ -142,7 +142,7 @@ RSpec.describe CustomFields::RecalculateValuesJob, type: :model do
|
||||
context "with formula referencing other fields" do
|
||||
let!(:static) { create(:integer_project_custom_field, projects: [project1, project2, project3]) }
|
||||
let!(:custom_field) do
|
||||
create(:calculated_value_project_custom_field, formula: "#{static} * 3", is_required: true)
|
||||
create(:calculated_value_project_custom_field, formula: "#{static} * 3", is_for_all: true)
|
||||
end
|
||||
let(:custom_field_id) { custom_field.id }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user