diff --git a/app/components/admin/custom_fields/calculated_values/details_component.html.erb b/app/components/admin/custom_fields/calculated_values/details_component.html.erb
deleted file mode 100644
index f2c7bc22106..00000000000
--- a/app/components/admin/custom_fields/calculated_values/details_component.html.erb
+++ /dev/null
@@ -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
-%>
diff --git a/app/components/custom_fields/details_component.html.erb b/app/components/custom_fields/details_component.html.erb
index 88ba8c9aaeb..4c6560c5171 100644
--- a/app/components/custom_fields/details_component.html.erb
+++ b/app/components/custom_fields/details_component.html.erb
@@ -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
diff --git a/app/components/custom_fields/details_component.rb b/app/components/custom_fields/details_component.rb
index 17229d0fa32..a81ad052f37 100644
--- a/app/components/custom_fields/details_component.rb
+++ b/app/components/custom_fields/details_component.rb
@@ -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
diff --git a/app/components/projects/settings/creation_wizard/project_custom_field_sections/custom_field_row_component.html.erb b/app/components/projects/settings/creation_wizard/project_custom_field_sections/custom_field_row_component.html.erb
index be471cc6591..3c010df85ae 100644
--- a/app/components/projects/settings/creation_wizard/project_custom_field_sections/custom_field_row_component.html.erb
+++ b/app/components/projects/settings/creation_wizard/project_custom_field_sections/custom_field_row_component.html.erb
@@ -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
%>
-
diff --git a/app/components/projects/settings/creation_wizard/project_custom_field_sections/custom_field_row_component.rb b/app/components/projects/settings/creation_wizard/project_custom_field_sections/custom_field_row_component.rb
index e0f7e494b37..80aeb6c5276 100644
--- a/app/components/projects/settings/creation_wizard/project_custom_field_sections/custom_field_row_component.rb
+++ b/app/components/projects/settings/creation_wizard/project_custom_field_sections/custom_field_row_component.rb
@@ -57,6 +57,10 @@ module Projects
end
end
+ def toggle_enabled?
+ !@project_custom_field.required?
+ end
+
def toggle_data_attributes
{
"turbo-method": :post,
diff --git a/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.html.erb b/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.html.erb
index f56ec9422b9..3e04f6d3cf1 100644
--- a/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.html.erb
+++ b/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.html.erb
@@ -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
diff --git a/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.rb b/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.rb
index 1fbb2c874aa..1f5891239c7 100644
--- a/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.rb
+++ b/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.rb
@@ -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
diff --git a/app/components/settings/project_custom_field_sections/custom_field_row_component.html.erb b/app/components/settings/project_custom_field_sections/custom_field_row_component.html.erb
index 6a9fb982a3b..d50e07d8703 100644
--- a/app/components/settings/project_custom_field_sections/custom_field_row_component.html.erb
+++ b/app/components/settings/project_custom_field_sections/custom_field_row_component.html.erb
@@ -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?
diff --git a/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.rb b/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.rb
index a474ef4c119..6a48812643f 100644
--- a/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.rb
+++ b/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.rb
@@ -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
diff --git a/app/contracts/custom_fields/base_contract.rb b/app/contracts/custom_fields/base_contract.rb
index e3b027ef34f..d959804701b 100644
--- a/app/contracts/custom_fields/base_contract.rb
+++ b/app/contracts/custom_fields/base_contract.rb
@@ -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
diff --git a/app/contracts/project_custom_field_project_mappings/base_contract.rb b/app/contracts/project_custom_field_project_mappings/base_contract.rb
index 0d506e33dc3..580a3855532 100644
--- a/app/contracts/project_custom_field_project_mappings/base_contract.rb
+++ b/app/contracts/project_custom_field_project_mappings/base_contract.rb
@@ -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? ||
diff --git a/app/contracts/work_package_types/create_contract.rb b/app/contracts/work_package_types/create_contract.rb
index 288e2a9660e..1662b6810fb 100644
--- a/app/contracts/work_package_types/create_contract.rb
+++ b/app/contracts/work_package_types/create_contract.rb
@@ -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
diff --git a/app/forms/custom_fields/calculated_values/details_form.rb b/app/forms/custom_fields/calculated_values/details_form.rb
deleted file mode 100644
index 7d172c8df02..00000000000
--- a/app/forms/custom_fields/calculated_values/details_form.rb
+++ /dev/null
@@ -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
diff --git a/app/forms/custom_fields/details_form.rb b/app/forms/custom_fields/details_form.rb
index 30fe8c7e9f0..44d17c05556 100644
--- a/app/forms/custom_fields/details_form.rb
+++ b/app/forms/custom_fields/details_form.rb
@@ -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
diff --git a/app/forms/projects/settings/custom_fields_form.rb b/app/forms/projects/settings/custom_fields_form.rb
index 2fffeb5e3e5..0bacd9b81dc 100644
--- a/app/forms/projects/settings/custom_fields_form.rb
+++ b/app/forms/projects/settings/custom_fields_form.rb
@@ -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
diff --git a/app/helpers/enterprise_helper.rb b/app/helpers/enterprise_helper.rb
index 0b2d64b2f51..24a7b0a8522 100644
--- a/app/helpers/enterprise_helper.rb
+++ b/app/helpers/enterprise_helper.rb
@@ -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
diff --git a/app/models/custom_value.rb b/app/models/custom_value.rb
index b9e1e4294d4..cb88fbede6a 100644
--- a/app/models/custom_value.rb
+++ b/app/models/custom_value.rb
@@ -46,6 +46,7 @@ class CustomValue < ApplicationRecord
delegate :editable?,
:admin_only?,
:required?,
+ :is_for_all?,
:max_length,
:min_length,
:calculated_value?,
diff --git a/app/models/project_custom_field.rb b/app/models/project_custom_field.rb
index 1d24ffb10a8..18aa643262f 100644
--- a/app/models/project_custom_field.rb
+++ b/app/models/project_custom_field.rb
@@ -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
diff --git a/app/models/role.rb b/app/models/role.rb
index a6abb6286e6..6e2eaadf0b3 100644
--- a/app/models/role.rb
+++ b/app/models/role.rb
@@ -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,
diff --git a/app/models/type.rb b/app/models/type.rb
index a3b26c86b54..d7d5e097974 100644
--- a/app/models/type.rb
+++ b/app/models/type.rb
@@ -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
diff --git a/app/services/project_custom_field_project_mappings/bulk_update_service.rb b/app/services/project_custom_field_project_mappings/bulk_update_service.rb
index 009d9a401f1..ef5b866cba4 100644
--- a/app/services/project_custom_field_project_mappings/bulk_update_service.rb
+++ b/app/services/project_custom_field_project_mappings/bulk_update_service.rb
@@ -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
diff --git a/app/services/projects/concerns/new_project_service.rb b/app/services/projects/concerns/new_project_service.rb
index 077f59abe12..bad9e78e9b6 100644
--- a/app/services/projects/concerns/new_project_service.rb
+++ b/app/services/projects/concerns/new_project_service.rb
@@ -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
diff --git a/app/views/admin/settings/project_custom_fields/edit.html.erb b/app/views/admin/settings/project_custom_fields/edit.html.erb
index 9c5092917a8..644f966d3a0 100644
--- a/app/views/admin/settings/project_custom_fields/edit.html.erb
+++ b/app/views/admin/settings/project_custom_fields/edit.html.erb
@@ -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 %>
diff --git a/app/views/admin/settings/project_custom_fields/new.html.erb b/app/views/admin/settings/project_custom_fields/new.html.erb
index 15b15441c4b..d36d93794f2 100644
--- a/app/views/admin/settings/project_custom_fields/new.html.erb
+++ b/app/views/admin/settings/project_custom_fields/new.html.erb
@@ -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 %>
diff --git a/app/views/admin/settings/project_custom_fields/project_mappings.html.erb b/app/views/admin/settings/project_custom_fields/project_mappings.html.erb
index 2c0091f80f6..ec678d8b692 100644
--- a/app/views/admin/settings/project_custom_fields/project_mappings.html.erb
+++ b/app/views/admin/settings/project_custom_fields/project_mappings.html.erb
@@ -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(
diff --git a/app/views/custom_fields/_form.html.erb b/app/views/custom_fields/_form.html.erb
index e25119494da..cddcda38427 100644
--- a/app/views/custom_fields/_form.html.erb
+++ b/app/views/custom_fields/_form.html.erb
@@ -81,7 +81,13 @@ See COPYRIGHT and LICENSE files for more details.
<%= f.check_box :multi_value,
data: { action: "admin--custom-fields#checkOnlyOne" } %>
>
@@ -186,52 +192,58 @@ See COPYRIGHT and LICENSE files for more details.
<% when "ProjectCustomField" %>
+
<% when "TimeEntryCustomField" %>
<% else %>
<% end %>
diff --git a/app/views/custom_fields/edit.html.erb b/app/views/custom_fields/edit.html.erb
index 0814385d202..937e22cd26d 100644
--- a/app/views/custom_fields/edit.html.erb
+++ b/app/views/custom_fields/edit.html.erb
@@ -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" %>
diff --git a/app/views/custom_fields/new.html.erb b/app/views/custom_fields/new.html.erb
index 3769b68cc7b..feb38394b5c 100644
--- a/app/views/custom_fields/new.html.erb
+++ b/app/views/custom_fields/new.html.erb
@@ -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" %>
diff --git a/config/initializers/log_slow_sql_queries.rb b/config/initializers/log_slow_sql_queries.rb
index 6bd3f31882e..324c9cd9a2f 100644
--- a/config/initializers/log_slow_sql_queries.rb
+++ b/config/initializers/log_slow_sql_queries.rb
@@ -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)
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 11bd317a4f7..4473d695f57 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -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"
diff --git a/db/migrate/20251211160744_set_is_for_all_and_unset_required.rb b/db/migrate/20251211160744_set_is_for_all_and_unset_required.rb
new file mode 100644
index 00000000000..0d991a3f270
--- /dev/null
+++ b/db/migrate/20251211160744_set_is_for_all_and_unset_required.rb
@@ -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
diff --git a/db/migrate/20251218100700_add_uniqueness_for_status_names.rb b/db/migrate/20251218100700_add_uniqueness_for_status_names.rb
new file mode 100644
index 00000000000..3e3b72d4a31
--- /dev/null
+++ b/db/migrate/20251218100700_add_uniqueness_for_status_names.rb
@@ -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
diff --git a/app/components/admin/custom_fields/calculated_values/details_component.rb b/db/migrate/20251218100721_add_uniqueness_for_role_names.rb
similarity index 69%
rename from app/components/admin/custom_fields/calculated_values/details_component.rb
rename to db/migrate/20251218100721_add_uniqueness_for_role_names.rb
index f2c70c9bba5..044022404db 100644
--- a/app/components/admin/custom_fields/calculated_values/details_component.rb
+++ b/db/migrate/20251218100721_add_uniqueness_for_role_names.rb
@@ -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
diff --git a/db/migrate/20251218100741_add_uniqueness_for_type_names.rb b/db/migrate/20251218100741_add_uniqueness_for_type_names.rb
new file mode 100644
index 00000000000..445df2aca36
--- /dev/null
+++ b/db/migrate/20251218100741_add_uniqueness_for_type_names.rb
@@ -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
diff --git a/db/migrate/migration_utils/utils.rb b/db/migrate/migration_utils/utils.rb
index 62c3a4337f3..9fe0d69be2c 100644
--- a/db/migrate/migration_utils/utils.rb
+++ b/db/migrate/migration_utils/utils.rb
@@ -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)
diff --git a/lib/open_project/custom_field_format_dependent.rb b/lib/open_project/custom_field_format_dependent.rb
index a07c6d0828d..9a39e7de624 100644
--- a/lib/open_project/custom_field_format_dependent.rb
+++ b/lib/open_project/custom_field_format_dependent.rb
@@ -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
diff --git a/lib/open_project/logging/log_delegator.rb b/lib/open_project/logging/log_delegator.rb
index c1aac281b32..364bfd539a8 100644
--- a/lib/open_project/logging/log_delegator.rb
+++ b/lib/open_project/logging/log_delegator.rb
@@ -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
diff --git a/spec/components/custom_fields/details_component_spec.rb b/spec/components/custom_fields/details_component_spec.rb
new file mode 100644
index 00000000000..021cc262dfe
--- /dev/null
+++ b/spec/components/custom_fields/details_component_spec.rb
@@ -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
diff --git a/spec/contracts/custom_fields/create_contract_spec.rb b/spec/contracts/custom_fields/create_contract_spec.rb
index a412d7ab23b..dde1c8147dd 100644
--- a/spec/contracts/custom_fields/create_contract_spec.rb
+++ b/spec/contracts/custom_fields/create_contract_spec.rb
@@ -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
diff --git a/spec/contracts/custom_fields/shared_contract_examples.rb b/spec/contracts/custom_fields/shared_contract_examples.rb
new file mode 100644
index 00000000000..8be282bf0ed
--- /dev/null
+++ b/spec/contracts/custom_fields/shared_contract_examples.rb
@@ -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
diff --git a/spec/contracts/custom_fields/update_contract_spec.rb b/spec/contracts/custom_fields/update_contract_spec.rb
index 0acb09b3e79..f0640fc3b34 100644
--- a/spec/contracts/custom_fields/update_contract_spec.rb
+++ b/spec/contracts/custom_fields/update_contract_spec.rb
@@ -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
diff --git a/spec/features/admin/custom_fields/projects/boolean_spec.rb b/spec/features/admin/custom_fields/projects/boolean_spec.rb
new file mode 100644
index 00000000000..971c8ef482b
--- /dev/null
+++ b/spec/features/admin/custom_fields/projects/boolean_spec.rb
@@ -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
diff --git a/spec/features/admin/custom_fields/projects/calculated_value_spec.rb b/spec/features/admin/custom_fields/projects/calculated_value_spec.rb
index a89c6a0ee83..61cc532108c 100644
--- a/spec/features/admin/custom_fields/projects/calculated_value_spec.rb
+++ b/spec/features/admin/custom_fields/projects/calculated_value_spec.rb
@@ -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
diff --git a/spec/features/admin/custom_fields/projects/edit_spec.rb b/spec/features/admin/custom_fields/projects/edit_spec.rb
deleted file mode 100644
index f1fee9d9bc2..00000000000
--- a/spec/features/admin/custom_fields/projects/edit_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/features/admin/custom_fields/projects/index_spec.rb b/spec/features/admin/custom_fields/projects/index_spec.rb
index 12e5ee1e3f8..da661d28c8a 100644
--- a/spec/features/admin/custom_fields/projects/index_spec.rb
+++ b/spec/features/admin/custom_fields/projects/index_spec.rb
@@ -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
diff --git a/spec/features/admin/custom_fields/projects/integer_spec.rb b/spec/features/admin/custom_fields/projects/integer_spec.rb
new file mode 100644
index 00000000000..5f737795c75
--- /dev/null
+++ b/spec/features/admin/custom_fields/projects/integer_spec.rb
@@ -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
diff --git a/spec/features/admin/custom_fields/projects/project_mappings_spec.rb b/spec/features/admin/custom_fields/projects/project_mappings_spec.rb
index 988e72a462f..cc0756d377c 100644
--- a/spec/features/admin/custom_fields/projects/project_mappings_spec.rb
+++ b/spec/features/admin/custom_fields/projects/project_mappings_spec.rb
@@ -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
diff --git a/spec/features/admin/custom_fields/projects/shared_context.rb b/spec/features/admin/custom_fields/projects/shared_context.rb
index 88dc429e621..e4033565791 100644
--- a/spec/features/admin/custom_fields/projects/shared_context.rb
+++ b/spec/features/admin/custom_fields/projects/shared_context.rb
@@ -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
diff --git a/spec/features/admin/custom_fields/shared_custom_field_expectations.rb b/spec/features/admin/custom_fields/shared_custom_field_expectations.rb
index ef6caf19975..8628576adad 100644
--- a/spec/features/admin/custom_fields/shared_custom_field_expectations.rb
+++ b/spec/features/admin/custom_fields/shared_custom_field_expectations.rb
@@ -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
diff --git a/spec/features/admin/custom_fields/work_packages/boolean_spec.rb b/spec/features/admin/custom_fields/work_packages/boolean_spec.rb
index 6fd514ab840..b0aab4f1a33 100644
--- a/spec/features/admin/custom_fields/work_packages/boolean_spec.rb
+++ b/spec/features/admin/custom_fields/work_packages/boolean_spec.rb
@@ -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
diff --git a/spec/features/admin/custom_fields/work_packages/hierarchy_spec.rb b/spec/features/admin/custom_fields/work_packages/hierarchy_spec.rb
index 69f85dbe380..24904b71eeb 100644
--- a/spec/features/admin/custom_fields/work_packages/hierarchy_spec.rb
+++ b/spec/features/admin/custom_fields/work_packages/hierarchy_spec.rb
@@ -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!
diff --git a/spec/features/projects/copy_spec.rb b/spec/features/projects/copy_spec.rb
index 2e0e5c46127..21f7d067321 100644
--- a/spec/features/projects/copy_spec.rb
+++ b/spec/features/projects/copy_spec.rb
@@ -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
diff --git a/spec/features/projects/create_spec.rb b/spec/features/projects/create_spec.rb
index 9a4d1431648..1ef2eacd56d 100644
--- a/spec/features/projects/create_spec.rb
+++ b/spec/features/projects/create_spec.rb
@@ -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
diff --git a/spec/forms/projects/settings/custom_fields_form_spec.rb b/spec/forms/projects/settings/custom_fields_form_spec.rb
index 16489afaffa..dae7f3bf554 100644
--- a/spec/forms/projects/settings/custom_fields_form_spec.rb
+++ b/spec/forms/projects/settings/custom_fields_form_spec.rb
@@ -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
diff --git a/spec/migrations/set_is_for_all_and_unset_required_spec.rb b/spec/migrations/set_is_for_all_and_unset_required_spec.rb
new file mode 100644
index 00000000000..61b874fe19d
--- /dev/null
+++ b/spec/migrations/set_is_for_all_and_unset_required_spec.rb
@@ -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
diff --git a/spec/models/custom_field/calculated_value_spec.rb b/spec/models/custom_field/calculated_value_spec.rb
index 0644a7b4a5e..d8b8ff133c4 100644
--- a/spec/models/custom_field/calculated_value_spec.rb
+++ b/spec/models/custom_field/calculated_value_spec.rb
@@ -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 }
diff --git a/spec/models/global_role_spec.rb b/spec/models/global_role_spec.rb
index 06337879ff9..ba745764cd9 100644
--- a/spec/models/global_role_spec.rb
+++ b/spec/models/global_role_spec.rb
@@ -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 }
diff --git a/spec/models/project_custom_field_spec.rb b/spec/models/project_custom_field_spec.rb
index 268356f1925..314d25e6add 100644
--- a/spec/models/project_custom_field_spec.rb
+++ b/spec/models/project_custom_field_spec.rb
@@ -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,
diff --git a/spec/models/projects/customizable_spec.rb b/spec/models/projects/customizable_spec.rb
index dbb6ce11934..a83a690f2ce 100644
--- a/spec/models/projects/customizable_spec.rb
+++ b/spec/models/projects/customizable_spec.rb
@@ -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
diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb
index 1412c5eb5fd..be0dc6d23b0 100644
--- a/spec/models/role_spec.rb
+++ b/spec/models/role_spec.rb
@@ -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
diff --git a/spec/models/work_package_role_spec.rb b/spec/models/work_package_role_spec.rb
index f6451bde9d9..d8daaae1a2f 100644
--- a/spec/models/work_package_role_spec.rb
+++ b/spec/models/work_package_role_spec.rb
@@ -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
diff --git a/spec/requests/api/v3/projects/create_resource_spec.rb b/spec/requests/api/v3/projects/create_resource_spec.rb
index c24bb2f2fe9..e588cec7dba 100644
--- a/spec/requests/api/v3/projects/create_resource_spec.rb
+++ b/spec/requests/api/v3/projects/create_resource_spec.rb
@@ -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
diff --git a/spec/requests/api/v3/projects/index_resource_spec.rb b/spec/requests/api/v3/projects/index_resource_spec.rb
index fdb65d53d95..7cc03f90f67 100644
--- a/spec/requests/api/v3/projects/index_resource_spec.rb
+++ b/spec/requests/api/v3/projects/index_resource_spec.rb
@@ -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
diff --git a/spec/requests/api/v3/workspaces/update_form_resource_examples.rb b/spec/requests/api/v3/workspaces/update_form_resource_examples.rb
index 03dc1099a40..47eef2dbc76 100644
--- a/spec/requests/api/v3/workspaces/update_form_resource_examples.rb
+++ b/spec/requests/api/v3/workspaces/update_form_resource_examples.rb
@@ -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
diff --git a/spec/requests/api/v3/workspaces/update_resource_examples.rb b/spec/requests/api/v3/workspaces/update_resource_examples.rb
index 435a220808f..f2d9a99c9f4 100644
--- a/spec/requests/api/v3/workspaces/update_resource_examples.rb
+++ b/spec/requests/api/v3/workspaces/update_resource_examples.rb
@@ -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
diff --git a/spec/services/project_custom_field_project_mappings/bulk_update_service_spec.rb b/spec/services/project_custom_field_project_mappings/bulk_update_service_spec.rb
index e905976c158..a4b3774ec11 100644
--- a/spec/services/project_custom_field_project_mappings/bulk_update_service_spec.rb
+++ b/spec/services/project_custom_field_project_mappings/bulk_update_service_spec.rb
@@ -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
diff --git a/spec/services/project_custom_field_project_mappings/toggle_service_spec.rb b/spec/services/project_custom_field_project_mappings/toggle_service_spec.rb
index 79bed3f0c7a..cfe3745fda5 100644
--- a/spec/services/project_custom_field_project_mappings/toggle_service_spec.rb
+++ b/spec/services/project_custom_field_project_mappings/toggle_service_spec.rb
@@ -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
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index ea3bcc570d5..f4527f71015 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -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
diff --git a/spec/workers/custom_fields/recalculate_values_job_spec.rb b/spec/workers/custom_fields/recalculate_values_job_spec.rb
index d477448380b..d9672d6baa6 100644
--- a/spec/workers/custom_fields/recalculate_values_job_spec.rb
+++ b/spec/workers/custom_fields/recalculate_values_job_spec.rb
@@ -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 }