replace string object by custom option for list custom fields

pull/5353/head
Jens Ulferts 8 years ago
parent f85032559d
commit 8a08856748
No known key found for this signature in database
GPG Key ID: 3CAA4B1182CF5308
  1. 6
      app/models/custom_option.rb
  2. 27
      app/models/queries/work_packages/filter/custom_field_filter.rb
  3. 68
      app/models/query/results.rb
  4. 1
      docs/api/apiv3-documentation.apib
  5. 51
      docs/api/apiv3/endpoints/custom-options.apib
  6. 0
      docs/api/apiv3/endpoints/principals.apib
  7. 0
      docs/api/apiv3/endpoints/roles.apib
  8. 3
      frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.ts
  9. 2
      frontend/app/components/wp-display/field-types/wp-display-multiple-lines-string-objects-field.directive.html
  10. 4
      frontend/app/components/wp-display/field-types/wp-display-multiple-lines-string-objects-field.module.ts
  11. 10
      frontend/app/components/wp-display/field-types/wp-display-resources-field-multiline.directive.html
  12. 0
      frontend/app/components/wp-display/field-types/wp-display-resources-field.directive.html
  13. 4
      frontend/app/components/wp-display/field-types/wp-display-resources-field.module.ts
  14. 40
      frontend/app/components/wp-display/field-types/wp-display-string-object-field.module.ts
  15. 11
      frontend/app/components/wp-display/wp-display-field/wp-display-field.config.ts
  16. 1
      frontend/app/components/wp-display/wp-display-field/wp-display-field.module.ts
  17. 11
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-field.config.ts
  18. 37
      lib/api/decorators/aggregation_group.rb
  19. 48
      lib/api/v3/custom_options/custom_option_representer.rb
  20. 67
      lib/api/v3/custom_options/custom_options_api.rb
  21. 13
      lib/api/v3/queries/schemas/custom_option_filter_dependency_representer.rb
  22. 2
      lib/api/v3/queries/schemas/filter_dependency_representer_factory.rb
  23. 1
      lib/api/v3/root.rb
  24. 24
      lib/api/v3/utilities/custom_field_injector.rb
  25. 14
      lib/api/v3/utilities/custom_field_injector/link_value_getter.rb
  26. 17
      lib/api/v3/utilities/path_helper.rb
  27. 2
      lib/api/v3/work_packages/schema/specific_work_package_schema.rb
  28. 33
      spec/factories/custom_option_factory.rb
  29. 5
      spec/features/work_packages/details/inplace_editor/custom_field_spec.rb
  30. 64
      spec/lib/api/v3/custom_options/custom_option_representer_spec.rb
  31. 8
      spec/lib/api/v3/queries/schemas/custom_option_filter_dependency_representer_spec.rb
  32. 2
      spec/lib/api/v3/queries/schemas/filter_dependency_representer_factory_spec.rb
  33. 17
      spec/lib/api/v3/utilities/custom_field_injector_spec.rb
  34. 6
      spec/lib/api/v3/utilities/path_helper_spec.rb
  35. 2
      spec/lib/api/v3/work_packages/schema/specific_work_package_schema_spec.rb
  36. 17
      spec/models/queries/work_packages/filter/custom_field_filter_spec.rb
  37. 119
      spec/requests/api/v3/custom_options/custom_options_resource_spec.rb
  38. 18
      spec/requests/api/v3/work_package_resource_spec.rb
  39. 7
      spec_legacy/unit/query_spec.rb

@ -36,4 +36,10 @@ class CustomOption < ActiveRecord::Base
belongs_to :custom_field
validates :value, presence: true, length: { maximum: 255 }
def to_s
value
end
alias :name :to_s
end

@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@ -119,7 +120,7 @@ class Queries::WorkPackages::Filter::CustomFieldFilter <
when 'version'
Version.find(values)
when 'list'
value_objects_for_list
custom_field.custom_options.find(values)
else
super
end
@ -182,16 +183,6 @@ class Queries::WorkPackages::Filter::CustomFieldFilter <
.map(&:id).include? custom_field.id
end
def value_objects_for_list
objects = allowed_values.select do |value|
values.include? value.last.to_s
end
objects.map do |value|
Queries::StringObject.new(value.last, value.first)
end
end
def strategies
strategies = Queries::Filters::STRATEGIES.dup
strategies[:list_optional] = Queries::Filters::Strategies::CfListOptional
@ -200,17 +191,3 @@ class Queries::WorkPackages::Filter::CustomFieldFilter <
strategies
end
end
#
# This object is only used to transport the values tothe query filter instance
# representer which expects a class and deduces the path from the classes' name.
#
class Queries::StringObject
attr_accessor :id,
:name
def initialize(id, name)
self.id = [name, id]
self.name = name
end
end

@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@ -56,22 +57,9 @@ class ::Query::Results
@work_package_count_by_group ||= begin
r = nil
if query.grouped?
begin
# Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
r = WorkPackage
.group(query.group_by_statement)
.visible
.includes(:status, :project)
.where(query.statement)
.references(:statuses, :projects)
.count
rescue ActiveRecord::RecordNotFound
r = { nil => work_package_count }
end
c = query.group_by_column
if c.is_a?(QueryCustomFieldColumn)
r = r.keys.inject({}) { |h, k| h[c.custom_field.cast_value(k)] = r[k]; h }
end
r = groups_grouped_by_column(query)
r = transform_group_keys(query, r)
end
r
end
@ -164,4 +152,52 @@ class ::Query::Results
def custom_field_column?(name)
name.to_s =~ /\Acf_\d+\z/
end
def groups_grouped_by_column(query)
# Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
WorkPackage
.group(query.group_by_statement)
.visible
.includes(:status, :project)
.where(query.statement)
.references(:statuses, :projects)
.count
rescue ActiveRecord::RecordNotFound
{ nil => work_package_count }
end
def transform_group_keys(query, groups)
column = query.group_by_column
if column.is_a?(QueryCustomFieldColumn) && column.custom_field.list?
transform_list_group_by_keys(column.custom_field, groups)
elsif column.is_a?(QueryCustomFieldColumn)
transform_custom_field_keys(column.custom_field, groups)
else
groups
end
end
def transform_list_group_by_keys(custom_field, groups)
options = custom_options_for_keys(custom_field, groups)
groups.transform_keys do |key|
if custom_field.multi_value?
key.split('.').map do |subkey|
options[subkey].first
end
else
options[key] ? options[key].first : nil
end
end
end
def custom_options_for_keys(custom_field, groups)
keys = groups.keys.map { |k| k.split('.') }
custom_field.custom_options.find(keys.flatten).group_by { |o| o.id.to_s }
end
def transform_custom_field_keys(custom_field, groups)
groups.transform_keys { |key| custom_field.cast_value(key) }
end
end

@ -10,6 +10,7 @@ FORMAT: 1A
<!-- include(apiv3/endpoints/attachments.apib) -->
<!-- include(apiv3/endpoints/categories.apib) -->
<!-- include(apiv3/endpoints/configuration.apib) -->
<!-- include(apiv3/endpoints/custom-options.apib) -->
<!-- include(apiv3/endpoints/forms.apib) -->
<!-- include(apiv3/endpoints/principals.apib) -->
<!-- include(apiv3/endpoints/priorities.apib) -->

@ -0,0 +1,51 @@
# Group Custom Objects
## Linked Properties
| Link | Description | Type | Constraints | Supported operations |
|:-------------:|-------------------------- | ------------- | ----------- | -------------------- |
| self | This custom object | CustomObject | not null | READ |
## Local Properties
| Property | Description | Type | Constraints | Supported operations |
|:----------------:| ---------------------------------------------- | -------- | ----------- | -------------------- |
| id | The identifier | Integer | | READ |
| value | The value defined for this custom object | String | | READ |
Custom objects are options of list custom fields.
## Custom Object [/api/v3/custom_objects/{id}]
+ Model
+ Body
{
"_links": {
"self": { "href": "/api/v3/custom_objects/1" }
},
"_type": "CustomObject",
"value": "Foo"
}
## View Custom Object [GET]
+ Parameters
+ id (required, integer, `1`) ... The custom object's identifier
+ Response 200 (application/hal+json)
[Custom Object][]
+ Response 404 (application/hal+json)
Returned if the custom object does not exist or the client does not have sufficient permissions to see it.
**Required permission:** view work package in any project the custom object's custom field is active in.
+ Body
{
"_type": "Error",
"errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound",
"message": "The specified resource does not exist."
}

@ -109,7 +109,8 @@ export class WorkPackageDisplayAttributeController {
protected updateAttribute(wp:WorkPackageResourceInterface) {
this.workPackage = wp;
if (this.schema[this.attribute] && (this.schema[this.attribute].type === '[]StringObject' || this.schema[this.attribute].type === '[]User')) {
if (this.schema[this.attribute] && (this.schema[this.attribute].type === '[]CustomOption' ||
this.schema[this.attribute].type === '[]User')) {
this.field = new MultipleLinesStringObjectsDisplayField(this.workPackage, this.attribute, this.schema[this.attribute])
} else {
this.field = this.wpDisplayField.getField(this.workPackage, this.attribute, this.schema[this.attribute]) as DisplayField;

@ -6,4 +6,4 @@
<div title="I18n.t('js.work_packages.no_value')" ng-if="vm.field.value.length == 0" class="custom-option -empty">
-
</div>
</span>
</span>

@ -26,8 +26,8 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {StringObjectsDisplayField} from "./wp-display-string-objects-field.module";
import {ResourcesDisplayField} from "./wp-display-resources-field.module";
export class MultipleLinesStringObjectsDisplayField extends StringObjectsDisplayField {
export class MultipleLinesStringObjectsDisplayField extends ResourcesDisplayField {
public template: string = '/components/wp-display/field-types/wp-display-multiple-lines-string-objects-field.directive.html';
}

@ -0,0 +1,10 @@
<span>
<div ng-repeat="value in vm.field.value" title="{{ value }}" class="custom-option -multiple-lines">
{{ value }}
</div>
<div title="I18n.t('js.work_packages.no_value')" ng-if="vm.field.value.length == 0" class="custom-option -empty">
-
</div>
</span>

@ -28,8 +28,8 @@
import {DisplayField} from "../wp-display-field/wp-display-field.module";
export class StringObjectsDisplayField extends DisplayField {
public template: string = '/components/wp-display/field-types/wp-display-string-objects-field.directive.html';
export class ResourcesDisplayField extends DisplayField {
public template: string = '/components/wp-display/field-types/wp-display-resources-field.directive.html';
public get value() {
if (this.schema) {

@ -1,40 +0,0 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// 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 doc/COPYRIGHT.rdoc for more details.
// ++
import {DisplayField} from "../wp-display-field/wp-display-field.module";
export class StringObjectDisplayField extends DisplayField {
public get value() {
if(this.schema) {
return this.resource[this.name] && (this.resource[this.name].value || this.resource[this.name].name);
}
else {
return null;
}
}
}

@ -29,8 +29,7 @@
import {WorkPackageDisplayFieldService} from './wp-display-field.service';
import {TextDisplayField} from '../field-types/wp-display-text-field.module';
import {ResourceDisplayField} from '../field-types/wp-display-resource-field.module';
import {StringObjectDisplayField} from '../field-types/wp-display-string-object-field.module';
import {StringObjectsDisplayField} from '../field-types/wp-display-string-objects-field.module';
import {ResourcesDisplayField} from '../field-types/wp-display-resources-field.module';
import {FormattableDisplayField} from '../field-types/wp-display-formattable-field.module';
import {DurationDisplayField} from '../field-types/wp-display-duration-field.module';
import {DateDisplayField} from '../field-types/wp-display-date-field.module';
@ -55,10 +54,10 @@ openprojectModule
'Status',
'Priority',
'Version',
'Category'])
.addFieldType(StringObjectDisplayField, 'string_object', ['StringObject'])
.addFieldType(StringObjectsDisplayField, 'string_objects', ['[]StringObject'])
.addFieldType(StringObjectsDisplayField, 'users', ['[]User'])
'Category',
'CustomOption'])
.addFieldType(ResourcesDisplayField, 'resources', ['[]CustomOption',
'[]User'])
.addFieldType(FormattableDisplayField, 'formattable', ['Formattable'])
.addFieldType(DurationDisplayField, 'duration', ['Duration'])
.addFieldType(DateDisplayField, 'date', ['Date'])

@ -36,6 +36,7 @@ export class DisplayField extends Field {
public static $injector: ng.auto.IInjectorService;
public template:string|null = null;
public I18n: op.I18n;
public mode:string|null = null;
public get value() {
if (this.schema) {

@ -39,13 +39,6 @@ import {DateEditField} from "../field-types/wp-edit-date-field.module";
import {WikiTextareaEditField} from "../field-types/wp-edit-wiki-textarea-field.module";
import {openprojectModule} from "../../../angular-modules";
//TODO: Implement
class DateRangeEditField extends EditField {
}
//TODO: See file wp-field.service.js:getInplaceEditStrategy for more eventual classes
openprojectModule
.run((wpEditField:WorkPackageEditFieldService) => {
wpEditField.defaultType = 'text';
@ -59,10 +52,10 @@ openprojectModule
'User',
'Version',
'Category',
'StringObject',
'CustomOption',
'Project'])
.addFieldType(MultiSelectEditField, 'multi-select', [
'[]StringObject',
'[]CustomOption',
'[]User'
])
.addFieldType(FloatEditField, 'float', ['Float'])

@ -35,7 +35,7 @@ module API
@sums = sums
@query = query
group_key = set_links!(query, group_key) || group_key
group_key = set_links!(group_key) || group_key
@link = ::API::V3::Utilities::ResourceLinkGenerator.make_link(group_key)
@ -92,35 +92,20 @@ module API
:query
##
# Initializes the links collection for this group if the query is being grouped by
# a multi value custom field. In that case an updated group_key is returned too.
# Initializes the links collection for this group if the group has multiple keys
#
# @return [String] A new group key for the multi value custom field.
def set_links!(query, group_key)
if multi_value_custom_field? query
options = link_options query, group_key
if options
@links = options.map do |opt|
{
href: ::API::V3::Utilities::ResourceLinkGenerator.make_link(opt.id.to_s),
title: opt.value
}
end
options.map(&:value).join(", ")
def set_links!(group_key)
if group_key.is_a?(Array)
@links = group_key.map do |opt|
{
href: ::API::V3::Utilities::ResourceLinkGenerator.make_link(opt),
title: opt.to_s
}
end
end
end
def multi_value_custom_field?(query)
column = query.group_by_column
column.is_a?(QueryCustomFieldColumn) && column.custom_field.multi_value?
end
def link_options(query, group_key)
query.group_by_column.custom_field.custom_options.where(id: group_key.to_s.split("."))
group_key.map(&:name).sort.join(", ")
end
end
def value

@ -0,0 +1,48 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# 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-2017 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 doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module CustomOptions
class CustomOptionRepresenter < ::API::Decorators::Single
self_link
# TODO: add link to custom field once api for custom fields exists
def _type
'CustomOption'
end
property :id
property :value
end
end
end
end

@ -0,0 +1,67 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# 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-2017 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 doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module CustomOptions
class CustomOptionsAPI < ::API::OpenProjectAPI
resources :custom_options do
namespace ':id' do
params do
requires :id, type: Integer
end
helpers do
def authorize_view_in_activated_project(custom_option)
allowed = Project
.allowed_to(current_user, :view_work_packages)
.joins(:work_package_custom_fields)
.where(custom_fields: { id: custom_option.custom_field_id })
.exists?
unless allowed
raise API::Errors::NotFound
end
end
end
get do
co = CustomOption.find(params[:id])
authorize_view_in_activated_project(co)
CustomOptionRepresenter.new(co, current_user: current_user)
end
end
end
end
end
end
end

@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@ -31,7 +32,7 @@ module API
module V3
module Queries
module Schemas
class StringObjectFilterDependencyRepresenter <
class CustomOptionFilterDependencyRepresenter <
FilterDependencyRepresenter
schema_with_allowed_collection :values,
@ -41,13 +42,13 @@ module API
required: true,
visibility: false,
values_callback: ->(*) {
represented.allowed_values
represented.custom_field.custom_options
},
value_representer: StringObjects::StringObjectRepresenter,
value_representer: CustomOptions::CustomOptionRepresenter,
link_factory: ->(value) {
{
href: api_v3_paths.string_object(value),
title: value
href: api_v3_paths.custom_option(value),
title: value.to_s
}
},
show_if: ->(*) {
@ -57,7 +58,7 @@ module API
private
def type
'[]StringObject'
'[]CustomOption'
end
end
end

@ -84,7 +84,7 @@ module API
case format
when 'list'
'API::V3::Queries::Schemas::StringObjectFilterDependencyRepresenter'
'API::V3::Queries::Schemas::CustomOptionFilterDependencyRepresenter'
when 'bool'
'API::V3::Queries::Schemas::BooleanFilterDependencyRepresenter'
when 'user', 'version', 'float'

@ -38,6 +38,7 @@ module API
mount ::API::V3::Attachments::AttachmentsAPI
mount ::API::V3::Categories::CategoriesAPI
mount ::API::V3::Configuration::ConfigurationAPI
mount ::API::V3::CustomOptions::CustomOptionsAPI
mount ::API::V3::Principals::PrincipalsAPI
mount ::API::V3::Priorities::PrioritiesAPI
mount ::API::V3::Projects::ProjectsAPI

@ -40,7 +40,7 @@ module API
'bool' => 'Boolean',
'user' => 'User',
'version' => 'Version',
'list' => 'StringObject'
'list' => 'CustomOption'
}.freeze
LINK_FORMATS = ['list', 'user', 'version'].freeze
@ -48,19 +48,19 @@ module API
PATH_METHOD_MAP = {
'user' => :user,
'version' => :version,
'list' => :string_object
'list' => :custom_option
}.freeze
NAMESPACE_MAP = {
'user' => 'users',
'version' => 'versions',
'list' => 'string_objects'
'list' => 'custom_options'
}.freeze
REPRESENTER_MAP = {
'user' => Users::UserRepresenter,
'version' => Versions::VersionRepresenter,
'list' => StringObjects::StringObjectRepresenter
'list' => CustomOptions::CustomOptionRepresenter
}.freeze
class << self
@ -182,14 +182,14 @@ module API
@class.schema_with_allowed_collection property_name(custom_field.id),
type: 'Version',
name_source: -> (*) { custom_field.name },
values_callback: -> (*) {
name_source: ->(*) { custom_field.name },
values_callback: ->(*) {
customized
.assignable_custom_field_values(custom_field)
},
writable: true,
value_representer: Versions::VersionRepresenter,
link_factory: -> (version) {
link_factory: ->(version) {
{
href: api_v3_paths.version(version.id),
title: version.name
@ -216,16 +216,14 @@ module API
end
def inject_list_schema(custom_field, customized)
representer = StringObjects::StringObjectRepresenter
type = custom_field.multi_value ? "[]StringObject" : "StringObject"
representer = CustomOptions::CustomOptionRepresenter
type = custom_field.multi_value ? "[]CustomOption" : "CustomOption"
name_source = ->(*) { custom_field.name }
values_callback = ->(*) { customized.assignable_custom_field_values(custom_field) }
link_factory = ->(value) do
# allow both single values and tuples for
# custom titles
{
href: api_v3_paths.string_object(value),
title: Array(value).first
href: api_v3_paths.custom_option(value.id),
title: value.to_s
}
end

@ -59,11 +59,10 @@ module API
Array(represented.custom_value_for(custom_field)).flat_map do |custom_value|
if custom_value && custom_value.value.present?
title = link_value_title(custom_value)
params = link_value_params(title, custom_field, custom_value)
[{
title: title,
href: api_v3_paths.send(path_method, params)
href: api_v3_paths.send(path_method, custom_value.value)
}]
else
[]
@ -79,17 +78,8 @@ module API
end
end
def link_value_params(title, custom_field, custom_value)
if custom_field.list?
# list custom_fields values use string objects which support and need titles
[title, custom_value.value]
else
custom_value.value
end
end
##
# While multi value custom fields are expected to simpl return an empty array
# While multi value custom fields are expected to simply return an empty array
# if they have no value a normal single value custom field is expected by
# the frontend to return a single element with a null href and title.
def single_empty_value

@ -107,6 +107,10 @@ module API
"#{work_packages_by_project(project_id)}/form"
end
def self.custom_option(id)
"#{root}/custom_options/#{id}"
end
def self.my_preferences
"#{root}/my_preferences"
end
@ -262,18 +266,9 @@ module API
"#{statuses}/#{id}"
end
##
# Accepts either a single value or a [value, title] tuple (array)
# and returns an URL to a string object for it.
def self.string_object(value)
val, title = Array(value).reverse.map { |v| ::ERB::Util::url_encode(v) }
path = "#{root}/string_objects?value=#{val}"
if title
"#{path}&title=#{title}"
else
path
end
val = ::ERB::Util::url_encode(value)
"#{root}/string_objects?value=#{val}"
end
def self.types

@ -66,7 +66,7 @@ module API
def assignable_custom_field_values(custom_field)
case custom_field.field_format
when 'list'
custom_field.custom_options.map { |co| [co.value, co.id] }
custom_field.possible_values
when 'version'
assignable_values(:version, nil)
end

@ -0,0 +1,33 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# 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-2017 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 doc/COPYRIGHT.rdoc for more details.
#++
FactoryGirl.define do
factory :custom_option do
sequence(:value) { |n| "Custom Option #{n}" }
end
end

@ -86,7 +86,6 @@ describe 'custom field inplace editor', js: true do
message: I18n.t('js.notice_successful_update'),
field: field1
field2.activate!
expect_update 'Y',
message: I18n.t('js.notice_successful_update'),
@ -96,11 +95,11 @@ describe 'custom field inplace editor', js: true do
customField2: 'Y'
field1.activate!
field1.expect_value("/api/v3/string_objects?value=#{custom_value('bar')}&title=bar")
field1.expect_value("/api/v3/custom_options/#{custom_value('bar')}")
field1.cancel_by_escape
field2.activate!
field2.expect_value("/api/v3/string_objects?value=#{custom_value('Y')}&title=Y")
field2.expect_value("/api/v3/custom_options/#{custom_value('Y')}")
expect_update 'X',
message: I18n.t('js.notice_successful_update'),
field: field2

@ -0,0 +1,64 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# 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-2017 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 doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::CustomOptions::CustomOptionRepresenter do
include ::API::V3::Utilities::PathHelper
let(:custom_option) { FactoryGirl.build_stubbed(:custom_option, custom_field: custom_field) }
let(:custom_field) { FactoryGirl.build_stubbed(:list_wp_custom_field) }
let(:user) { FactoryGirl.build_stubbed(:user) }
let(:representer) do
described_class.new(custom_option, current_user: user)
end
subject { representer.to_json }
describe 'generation' do
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.custom_option custom_option.id }
let(:title) { custom_option.to_s }
end
end
it 'has the type "CustomOption"' do
is_expected.to be_json_eql('CustomOption'.to_json).at_path('_type')
end
it 'has an id' do
is_expected.to be_json_eql(custom_option.id.to_json).at_path('id')
end
it 'has a value' do
is_expected.to be_json_eql(custom_option.to_s.to_json).at_path('value')
end
end
end

@ -28,7 +28,7 @@
require 'spec_helper'
describe ::API::V3::Queries::Schemas::StringObjectFilterDependencyRepresenter do
describe ::API::V3::Queries::Schemas::CustomOptionFilterDependencyRepresenter do
include ::API::V3::Utilities::PathHelper
let(:project) { FactoryGirl.build_stubbed(:project) }
@ -52,10 +52,10 @@ describe ::API::V3::Queries::Schemas::StringObjectFilterDependencyRepresenter do
context 'properties' do
describe 'values' do
let(:path) { 'values' }
let(:type) { '[]StringObject' }
let(:type) { '[]CustomOption' }
let(:hrefs) do
filter.allowed_values.each_with_object([]) do |value, array|
array << api_v3_paths.string_object(value)
custom_field.custom_options.map do |value|
api_v3_paths.custom_option(value)
end
end

@ -105,7 +105,7 @@ describe ::API::V3::Queries::Schemas::FilterDependencyRepresenterFactory do
let(:custom_field) { FactoryGirl.build_stubbed(:list_wp_custom_field) }
it 'is the string object dependency' do
is_expected.to be_a(::API::V3::Queries::Schemas::StringObjectFilterDependencyRepresenter)
is_expected.to be_a(::API::V3::Queries::Schemas::CustomOptionFilterDependencyRepresenter)
end
end

@ -155,7 +155,7 @@ describe ::API::V3::Utilities::CustomFieldInjector do
allow(schema)
.to receive(:assignable_custom_field_values)
.with(custom_field)
.and_return(custom_field.possible_values.map { |co| [co.value, co.id] })
.and_return(custom_field.possible_values)
end
let(:custom_field) do
@ -170,7 +170,7 @@ describe ::API::V3::Utilities::CustomFieldInjector do
it_behaves_like 'has basic schema properties' do
let(:path) { cf_path }
let(:type) { 'StringObject' }
let(:type) { 'CustomOption' }
let(:name) { custom_field.name }
let(:required) { true }
let(:writable) { true }
@ -180,7 +180,7 @@ describe ::API::V3::Utilities::CustomFieldInjector do
let(:path) { cf_path }
let(:hrefs) do
custom_field.possible_values.map do |value|
api_v3_paths.string_object([value.value, value.id])
api_v3_paths.custom_option(value.id)
end
end
end
@ -297,18 +297,21 @@ describe ::API::V3::Utilities::CustomFieldInjector do
end
context 'list custom field' do
let(:value) { 'Foobar' }
let(:raw_value) { value }
let(:value) { FactoryGirl.build_stubbed(:custom_option) }
let(:typed_value) { value.value }
let(:raw_value) { value.id.to_s }
let(:field_format) { 'list' }
it_behaves_like 'has a titled link' do
let(:link) { cf_path }
let(:href) { "/api/v3/string_objects?value=#{raw_value}&title=#{value}" }
let(:title) { value }
let(:href) { api_v3_paths.custom_option(value.id) }
let(:title) { value.value }
end
context 'value is nil' do
let(:value) { nil }
let(:raw_value) { '' }
let(:typed_value) { '' }
it_behaves_like 'has an empty link' do
let(:link) { cf_path }

@ -140,6 +140,12 @@ describe ::API::V3::Utilities::PathHelper do
it_behaves_like 'api v3 path', '/configuration'
end
describe '#custom_option' do
subject { helper.custom_option 42 }
it_behaves_like 'api v3 path', '/custom_options/42'
end
describe '#create_work_package_form' do
subject { helper.create_work_package_form }

@ -263,7 +263,7 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
it "is a list custom fields' possible values" do
expect(subject.assignable_custom_field_values(list_cf))
.to eql list_cf.possible_values.map { |co| [co.value, co.id] }
.to eql list_cf.possible_values
end
it "is a version custom fields' project values" do

@ -412,20 +412,9 @@ describe Queries::WorkPackages::Filter::CustomFieldFilter, type: :model do
end
it 'returns an array with custom classes' do
expect(instance.value_objects.length)
.to eql(2)
expect(instance.value_objects[0].id)
.to match_array([custom_field.custom_options.first.value,
custom_field.custom_options.first.id])
expect(instance.value_objects[0].name)
.to eql(custom_field.custom_options.first.value)
expect(instance.value_objects[1].id)
.to match_array([custom_field.custom_options.last.value,
custom_field.custom_options.last.id])
expect(instance.value_objects[1].name)
.to eql(custom_field.custom_options.last.value)
expect(instance.value_objects)
.to match_array([custom_field.custom_options.last,
custom_field.custom_options.first])
end
end
end

@ -0,0 +1,119 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# 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-2017 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 doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'API v3 Custom Options resource' do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
let(:user) do
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
end
let(:project) { FactoryGirl.create(:project) }
let(:role) { FactoryGirl.create(:role, permissions: permissions) }
let(:permissions) { [:view_work_packages] }
let(:custom_field) do
cf = FactoryGirl.create(:list_wp_custom_field)
project.work_package_custom_fields << cf
cf
end
let(:custom_option) do
FactoryGirl.create(:custom_option,
custom_field: custom_field)
end
subject(:response) { last_response }
describe 'GET api/v3/custom_options/:id' do
let(:path) { api_v3_paths.custom_option custom_option.id }
before do
allow(User)
.to receive(:current)
.and_return(user)
get path
end
context 'when being allowed' do
it 'is successful' do
expect(subject.status)
.to eql(200)
end
it 'returns the custom option' do
expect(response.body)
.to be_json_eql('CustomOption'.to_json)
.at_path('_type')
expect(response.body)
.to be_json_eql(custom_option.id.to_json)
.at_path('id')
expect(response.body)
.to be_json_eql(custom_option.value.to_json)
.at_path('value')
end
end
context 'when lacking permission' do
let(:permissions) { [] }
it 'is 404' do
expect(subject.status)
.to eql(404)
end
end
context 'when custom option not in project' do
let(:custom_field) do
# not added to project
FactoryGirl.create(:list_wp_custom_field)
end
it 'is 404' do
expect(subject.status)
.to eql(404)
end
end
context 'when not existing' do
let(:path) { api_v3_paths.custom_option 0 }
it 'is 404' do
expect(subject.status)
.to eql(404)
end
end
end
end

@ -833,21 +833,19 @@ describe 'API v3 Work package resource', type: :request do
end
context 'list custom field' do
let(:custom_field) {
FactoryGirl.create(:work_package_custom_field,
field_format: 'list',
is_required: false,
possible_values: [target_value])
}
let(:target_value) { 'Low No. of specialc#aracters!' }
let(:custom_field) do
FactoryGirl.create(:list_wp_custom_field)
end
let(:target_value) { custom_field.possible_values.last }
let(:value_link) do
api_v3_paths.string_object [target_value, custom_field.custom_options.first.id]
api_v3_paths.custom_option target_value.id
end
let(:value_parameter) {
let(:value_parameter) do
{ _links: { custom_field.accessor_name.camelize(:lower) => { href: value_link } } }
}
end
let(:params) { valid_params.merge(value_parameter) }
before do

@ -366,9 +366,12 @@ describe Query, type: :model do
q = Query.new(name: '_', group_by: 'cf_1')
count_by_group = q.results.work_package_count_by_group
assert_kind_of Hash, count_by_group
assert_equal %w(NilClass String), count_by_group.keys.map { |k| k.class.name }.uniq.sort
expect(count_by_group.keys.map { |k| k.class.name }.uniq)
.to match_array(%w(CustomOption NilClass))
assert_equal %w(Fixnum), count_by_group.values.map { |k| k.class.name }.uniq
assert count_by_group.has_key?('1')
puts count_by_group
expect(count_by_group.any? { |k, v| k.is_a?(CustomOption) && k.id == 1 && v == 1 })
.to be_truthy
end
it 'should issue count by date custom field group' do

Loading…
Cancel
Save