Merge branch 'release/17.0' into dev

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