kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
412 lines
14 KiB
412 lines
14 KiB
#-- encoding: UTF-8
|
|
|
|
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
|
#++
|
|
|
|
module API
|
|
module V3
|
|
module Utilities
|
|
class CustomFieldInjector
|
|
TYPE_MAP = {
|
|
'string' => 'String',
|
|
'empty' => 'String',
|
|
'text' => 'Formattable',
|
|
'int' => 'Integer',
|
|
'float' => 'Float',
|
|
'date' => 'Date',
|
|
'bool' => 'Boolean',
|
|
'user' => 'User',
|
|
'version' => 'Version',
|
|
'list' => 'CustomOption'
|
|
}.freeze
|
|
|
|
LINK_FORMATS = %w(list user version).freeze
|
|
|
|
NAMESPACE_MAP = {
|
|
'user' => ['users', 'groups', 'placeholder_users'],
|
|
'version' => 'versions',
|
|
'list' => 'custom_options'
|
|
}.freeze
|
|
|
|
REPRESENTER_MAP = {
|
|
'user' => '::API::V3::Principals::PrincipalRepresenterFactory',
|
|
'version' => '::API::V3::Versions::VersionRepresenter',
|
|
'list' => '::API::V3::CustomOptions::CustomOptionRepresenter'
|
|
}.freeze
|
|
|
|
class << self
|
|
def create_value_representer(custom_fields, representer)
|
|
new_representer_class_with(representer) do |injector|
|
|
custom_fields.each do |custom_field|
|
|
injector.inject_value(custom_field)
|
|
end
|
|
end
|
|
end
|
|
|
|
def create_schema_representer(custom_fields, representer)
|
|
new_representer_class_with(representer) do |injector|
|
|
custom_fields.each do |custom_field|
|
|
injector.inject_schema(custom_field)
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def linked_field?(custom_field)
|
|
LINK_FORMATS.include?(custom_field.field_format)
|
|
end
|
|
|
|
def property_field?(custom_field)
|
|
!linked_field?(custom_field)
|
|
end
|
|
|
|
def new_representer_class_with(representer)
|
|
injector = new(representer)
|
|
|
|
yield injector
|
|
|
|
injector.modified_representer_class
|
|
end
|
|
end
|
|
|
|
def initialize(representer_class)
|
|
@class = Class.new(representer_class) do
|
|
include API::Decorators::LinkedResource
|
|
end
|
|
end
|
|
|
|
def modified_representer_class
|
|
@class
|
|
end
|
|
|
|
def inject_schema(custom_field)
|
|
case custom_field.field_format
|
|
when 'version'
|
|
inject_version_schema(custom_field)
|
|
when 'user'
|
|
inject_user_schema(custom_field)
|
|
when 'list'
|
|
inject_list_schema(custom_field)
|
|
else
|
|
inject_basic_schema(custom_field)
|
|
end
|
|
end
|
|
|
|
def inject_value(custom_field)
|
|
case custom_field.field_format
|
|
when *LINK_FORMATS
|
|
inject_link_value(custom_field)
|
|
else
|
|
inject_property_value(custom_field)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def property_name(id)
|
|
"customField#{id}".to_sym
|
|
end
|
|
|
|
def inject_version_schema(custom_field)
|
|
@class.schema_with_allowed_collection property_name(custom_field.id),
|
|
type: resource_type(custom_field),
|
|
name_source: ->(*) { custom_field.name },
|
|
values_callback: ->(*) {
|
|
represented
|
|
.assignable_custom_field_values(custom_field)
|
|
},
|
|
writable: true,
|
|
value_representer: Versions::VersionRepresenter,
|
|
link_factory: ->(version) {
|
|
{
|
|
href: api_v3_paths.version(version.id),
|
|
title: version.name
|
|
}
|
|
},
|
|
required: custom_field.is_required
|
|
end
|
|
|
|
def inject_user_schema(custom_field)
|
|
@class.schema_with_allowed_link property_name(custom_field.id),
|
|
type: resource_type(custom_field),
|
|
writable: true,
|
|
name_source: ->(*) { custom_field.name },
|
|
required: custom_field.is_required,
|
|
href_callback: allowed_users_href_callback
|
|
end
|
|
|
|
def inject_list_schema(custom_field)
|
|
@class.schema_with_allowed_collection(
|
|
property_name(custom_field.id),
|
|
type: resource_type(custom_field),
|
|
name_source: ->(*) { custom_field.name },
|
|
values_callback: list_schemas_values_callback(custom_field),
|
|
value_representer: CustomOptions::CustomOptionRepresenter,
|
|
writable: true,
|
|
link_factory: list_schemas_link_callback,
|
|
required: custom_field.is_required
|
|
)
|
|
end
|
|
|
|
def inject_basic_schema(custom_field, writable: true)
|
|
@class.schema property_name(custom_field.id),
|
|
type: resource_type(custom_field),
|
|
name_source: ->(*) { custom_field.name },
|
|
required: custom_field.is_required,
|
|
has_default: custom_field.default_value.present?,
|
|
writable: writable,
|
|
min_length: cf_min_length(custom_field),
|
|
max_length: cf_max_length(custom_field),
|
|
regular_expression: cf_regexp(custom_field),
|
|
options: cf_options(custom_field)
|
|
end
|
|
|
|
def inject_link_value(custom_field)
|
|
name = property_name(custom_field.id)
|
|
expected_namespace = NAMESPACE_MAP[custom_field.field_format]
|
|
|
|
link = LinkValueGetter.link_for custom_field
|
|
setter = link_value_setter_for(custom_field, name, expected_namespace)
|
|
getter = embedded_link_value_getter(custom_field)
|
|
|
|
method = if custom_field.multi_value?
|
|
:resources
|
|
else
|
|
:resource
|
|
end
|
|
|
|
@class.send(method,
|
|
property_name(custom_field.id),
|
|
link: link,
|
|
setter: setter,
|
|
getter: getter)
|
|
end
|
|
|
|
def link_value_setter_for(custom_field, property, expected_namespace)
|
|
->(fragment:, represented:, **) {
|
|
values = Array([fragment].flatten).flat_map do |link|
|
|
href = link['href']
|
|
value =
|
|
if href
|
|
::API::Utilities::ResourceLinkParser.parse_id(
|
|
href,
|
|
property: property,
|
|
expected_version: '3',
|
|
expected_namespace: expected_namespace
|
|
)
|
|
end
|
|
|
|
[value].compact
|
|
end
|
|
|
|
represented.send(:"custom_field_#{custom_field.id}=", values)
|
|
}
|
|
end
|
|
|
|
def embedded_link_value_getter(custom_field)
|
|
representer_class = derive_representer_class(custom_field)
|
|
|
|
proc do
|
|
# Do not embed list or multi values as their links contain all the
|
|
# information needed (title and href) already.
|
|
next if !represented.available_custom_fields.include?(custom_field) ||
|
|
custom_field.list? ||
|
|
custom_field.multi_value?
|
|
|
|
value = represented.send custom_field.accessor_name
|
|
|
|
next unless value
|
|
|
|
representer_class
|
|
.create(value, current_user: current_user)
|
|
end
|
|
end
|
|
|
|
def inject_property_value(custom_field)
|
|
@class.property "custom_field_#{custom_field.id}".to_sym,
|
|
as: property_name(custom_field.id),
|
|
getter: property_value_getter_for(custom_field),
|
|
setter: property_value_setter_for(custom_field),
|
|
render_nil: true
|
|
end
|
|
|
|
def property_value_getter_for(custom_field)
|
|
->(*) {
|
|
next unless available_custom_fields.include?(custom_field)
|
|
|
|
value = send custom_field.accessor_name
|
|
|
|
if custom_field.field_format == 'text'
|
|
::API::Decorators::Formattable.new(value, object: self)
|
|
else
|
|
value
|
|
end
|
|
}
|
|
end
|
|
|
|
def property_value_setter_for(custom_field)
|
|
->(fragment:, **) {
|
|
value = if fragment && custom_field.field_format == 'text'
|
|
fragment['raw']
|
|
else
|
|
fragment
|
|
end
|
|
send(:"custom_field_#{custom_field.id}=", value)
|
|
}
|
|
end
|
|
|
|
def allowed_users_href_callback
|
|
static_filters = allowed_users_static_filters
|
|
instance_filters = method(:allowed_users_instance_filter)
|
|
|
|
->(*) {
|
|
# Careful to not alter the static_filters object here.
|
|
# It is made available in the closure (which is class level) and would thus
|
|
# keep the appended filters between requests.
|
|
filters = static_filters + instance_filters.call(represented)
|
|
|
|
api_v3_paths.path_for(:principals, filters: filters, page_size: 0)
|
|
}
|
|
end
|
|
|
|
def cf_min_length(custom_field)
|
|
custom_field.min_length if custom_field.min_length.positive?
|
|
end
|
|
|
|
def cf_max_length(custom_field)
|
|
custom_field.max_length if custom_field.max_length.positive?
|
|
end
|
|
|
|
def cf_regexp(custom_field)
|
|
custom_field.regexp unless custom_field.regexp.blank?
|
|
end
|
|
|
|
def cf_options(custom_field)
|
|
{
|
|
rtl: ("true" if custom_field.content_right_to_left)
|
|
}
|
|
end
|
|
|
|
def list_schemas_values_callback(custom_field)
|
|
->(*) { represented.assignable_custom_field_values(custom_field) }
|
|
end
|
|
|
|
def list_schemas_link_callback
|
|
->(value) do
|
|
{
|
|
href: api_v3_paths.custom_option(value.id),
|
|
title: value.to_s
|
|
}
|
|
end
|
|
end
|
|
|
|
def derive_representer_class(custom_field)
|
|
REPRESENTER_MAP[custom_field.field_format]
|
|
.constantize
|
|
end
|
|
|
|
def resource_type(custom_field)
|
|
type = TYPE_MAP[custom_field.field_format]
|
|
|
|
if custom_field.multi_value?
|
|
"[]#{type}"
|
|
else
|
|
type
|
|
end
|
|
end
|
|
|
|
def allowed_users_static_filters
|
|
[
|
|
{ status: { operator: '!',
|
|
values: [Principal.statuses[:locked].to_s] } },
|
|
{ type: { operator: '=',
|
|
values: %w[User Group PlaceholderUser] } }
|
|
]
|
|
end
|
|
|
|
def allowed_users_instance_filter(represented)
|
|
project_id_value =
|
|
if represented.respond_to?(:model) && represented.model.is_a?(Project)
|
|
represented.id
|
|
else
|
|
represented.project_id.to_s
|
|
end
|
|
|
|
if project_id_value.present?
|
|
[{ member: { operator: '=', values: [project_id_value.to_s] } }]
|
|
else
|
|
[{ member: { operator: '*', values: [] } }]
|
|
end
|
|
end
|
|
|
|
module RepresenterClass
|
|
def custom_field_injector(config)
|
|
@custom_field_injector_config = config.reverse_merge custom_field_injector_config
|
|
end
|
|
|
|
def custom_field_injector_config
|
|
@custom_field_injector_config ||= { type: :value_representer,
|
|
injector_class: ::API::V3::Utilities::CustomFieldInjector }
|
|
end
|
|
|
|
def create_class(represented, current_user)
|
|
custom_fields = if current_user.admin?
|
|
represented.available_custom_fields
|
|
else
|
|
represented.available_custom_fields.select(&:visible?)
|
|
end
|
|
|
|
custom_field_class(custom_fields)
|
|
end
|
|
|
|
def create(represented, **args)
|
|
create_class(represented, args[:current_user])
|
|
.new(represented, **args)
|
|
end
|
|
|
|
def custom_field_class(custom_fields)
|
|
custom_field_sha = OpenProject::Cache::CacheKey.expand(custom_fields.sort_by(&:id))
|
|
|
|
cached_custom_field_classes[custom_field_sha] ||= begin
|
|
injector_class = custom_field_injector_config[:injector_class]
|
|
|
|
method_name = :"create_#{custom_field_injector_config[:type]}"
|
|
|
|
injector_class.send(method_name, custom_fields, self)
|
|
end
|
|
end
|
|
|
|
def cached_custom_field_classes
|
|
@cached_custom_field_classes ||= {}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|