Merge pull request #10136 from opf/feature/40931-signaling-included-properties-of-the-projects-api

Feature/40931 signaling included properties of the projects api
pull/10274/head
Oliver Günther 3 years ago committed by GitHub
commit 8431743fb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      app/models/user.rb
  2. 37
      app/services/api/v3/work_package_collection_from_query_service.rb
  3. 5
      db/migrate/20220223095355_projects_lft_rgt_index.rb
  4. 1
      docs/api/apiv3/openapi-spec.yml
  5. 8
      docs/api/apiv3/paths/groups.yml
  6. 8
      docs/api/apiv3/paths/principals.yml
  7. 8
      docs/api/apiv3/paths/project_work_packages.yml
  8. 8
      docs/api/apiv3/paths/projects.yml
  9. 8
      docs/api/apiv3/paths/users.yml
  10. 8
      docs/api/apiv3/paths/work_packages.yml
  11. 3
      docs/api/apiv3/tags/basic_objects.yml
  12. 83
      docs/api/apiv3/tags/signaling.yml
  13. 2
      lib/api/decorators/offset_paginated_collection.rb
  14. 162
      lib/api/decorators/sql/hal.rb
  15. 54
      lib/api/decorators/sql_collection_representer.rb
  16. 2
      lib/api/utilities/endpoints/index.rb
  17. 25
      lib/api/utilities/url_props_parsing_helper.rb
  18. 16
      lib/api/v3/capabilities/capability_sql_representer.rb
  19. 37
      lib/api/v3/groups/group_sql_collection_representer.rb
  20. 48
      lib/api/v3/groups/group_sql_representer.rb
  21. 2
      lib/api/v3/groups/groups_api.rb
  22. 2
      lib/api/v3/memberships/memberships_api.rb
  23. 37
      lib/api/v3/placeholder_users/placeholder_user_sql_collection_representer.rb
  24. 48
      lib/api/v3/placeholder_users/placeholder_user_sql_representer.rb
  25. 2
      lib/api/v3/placeholder_users/placeholder_users_api.rb
  26. 37
      lib/api/v3/principals/principal_collection_representer.rb
  27. 37
      lib/api/v3/principals/principal_sql_collection_representer.rb
  28. 70
      lib/api/v3/principals/principal_sql_representer.rb
  29. 24
      lib/api/v3/principals/principals_api.rb
  30. 2
      lib/api/v3/projects/available_assignees_api.rb
  31. 2
      lib/api/v3/projects/available_responsibles_api.rb
  32. 37
      lib/api/v3/projects/project_sql_collection_representer.rb
  33. 133
      lib/api/v3/projects/project_sql_representer.rb
  34. 12
      lib/api/v3/projects/projects_api.rb
  35. 8
      lib/api/v3/users/unpaginated_user_collection_representer.rb
  36. 2
      lib/api/v3/users/user_collection_representer.rb
  37. 37
      lib/api/v3/users/user_sql_collection_representer.rb
  38. 83
      lib/api/v3/users/user_sql_representer.rb
  39. 32
      lib/api/v3/users/users_api.rb
  40. 69
      lib/api/v3/utilities/endpoints/sql_fallbacked_index.rb
  41. 8
      lib/api/v3/utilities/endpoints/sql_index.rb
  42. 5
      lib/api/v3/utilities/endpoints/sql_show.rb
  43. 7
      lib/api/v3/utilities/path_helper.rb
  44. 27
      lib/api/v3/utilities/sql_representer_walker.rb
  45. 6
      lib/api/v3/utilities/sql_walker_results.rb
  46. 10
      lib/api/v3/work_packages/watchers_api.rb
  47. 37
      lib/api/v3/work_packages/work_package_sql_collection_representer.rb
  48. 47
      lib/api/v3/work_packages/work_package_sql_representer.rb
  49. 2
      modules/costs/lib/api/v3/time_entries/time_entries_api.rb
  50. 2
      modules/documents/lib/api/v3/documents/documents_api.rb
  51. 2
      modules/grids/app/controllers/api/v3/grids/grids_api.rb
  52. 4
      spec/lib/api/utilities/url_props_parsing_helper_spec.rb
  53. 5
      spec/lib/api/v3/actions/action_sql_respresenter_rendering_spec.rb
  54. 11
      spec/lib/api/v3/capabilities/capability_sql_representer_rendering_spec.rb
  55. 74
      spec/lib/api/v3/groups/group_sql_representer_rendering_spec.rb
  56. 74
      spec/lib/api/v3/placeholder_users/placeholder_user_sql_representer_rendering_spec.rb
  57. 141
      spec/lib/api/v3/principals/principal_sql_representer_rendering_spec.rb
  58. 145
      spec/lib/api/v3/projects/project_sql_collection_representer_rendering_spec.rb
  59. 200
      spec/lib/api/v3/projects/project_sql_representer_rendering_spec.rb
  60. 35
      spec/lib/api/v3/users/unpaginated_user_collection_representer_spec.rb
  61. 33
      spec/lib/api/v3/users/user_collection_representer_spec.rb
  62. 189
      spec/lib/api/v3/users/user_sql_representer_rendering_spec.rb
  63. 74
      spec/lib/api/v3/work_packages/work_package_sql_representer_rendering_spec.rb
  64. 46
      spec/requests/api/v3/capability_resource_spec.rb
  65. 61
      spec/requests/api/v3/groups/group_resource_spec.rb
  66. 55
      spec/requests/api/v3/placeholder_users/index_resource_spec.rb
  67. 69
      spec/requests/api/v3/principals/principals_resource_spec.rb
  68. 58
      spec/requests/api/v3/projects/index_resource_spec.rb
  69. 52
      spec/requests/api/v3/user/user_resource_spec.rb

@ -105,10 +105,10 @@ class User < Principal
attr_accessor :password, :password_confirmation, :last_before_login_on
validates :login,
:firstname,
:lastname,
:mail,
presence: { unless: Proc.new { |user| user.builtin? } }
:firstname,
:lastname,
:mail,
presence: { unless: Proc.new { |user| user.builtin? } }
validates :login, uniqueness: { if: Proc.new { |user| !user.login.blank? }, case_sensitive: false }
validates :mail, uniqueness: { allow_blank: true, case_sensitive: false }

@ -30,7 +30,7 @@ module API
module V3
class WorkPackageCollectionFromQueryService
include Utilities::PathHelper
include ::API::Utilities::PageSizeHelper
include ::API::Utilities::UrlPropsParsingHelper
def initialize(query, user, scope: nil)
self.query = query
@ -88,6 +88,8 @@ module API
params[:offset] = to_i_or_nil(params[:offset])
params[:pageSize] = pageSizeParam(params)
end
params[:select] = nested_from_csv(provided_params['select'])
end
end
@ -147,18 +149,27 @@ module API
def collection_representer(work_packages, params:, project:, groups:, sums:)
resulting_params = calculate_resulting_params(params)
::API::V3::WorkPackages::WorkPackageCollectionRepresenter.new(
work_packages,
self_link: self_link(project),
project: project,
query: resulting_params,
page: resulting_params[:offset],
per_page: resulting_params[:pageSize],
groups: groups,
total_sums: sums,
embed_schemas: true,
current_user: current_user
)
if resulting_params[:select]
::API::V3::Utilities::SqlRepresenterWalker
.new(work_packages,
current_user: current_user,
self_path: self_link(project),
url_query: resulting_params)
.walk(::API::V3::WorkPackages::WorkPackageSqlCollectionRepresenter)
else
::API::V3::WorkPackages::WorkPackageCollectionRepresenter.new(
work_packages,
self_link: self_link(project),
project: project,
query: resulting_params,
page: resulting_params[:offset],
per_page: resulting_params[:pageSize],
groups: groups,
total_sums: sums,
embed_schemas: true,
current_user: current_user
)
end
end
def to_i_or_nil(value)

@ -0,0 +1,5 @@
class ProjectsLftRgtIndex < ActiveRecord::Migration[6.1]
def change
add_index :projects, %i[lft rgt]
end
end

@ -770,6 +770,7 @@ tags:
- "$ref": "./tags/collections.yml"
- "$ref": "./tags/filters.yml"
- "$ref": "./tags/forms.yml"
- "$ref": "./tags/signaling.yml"
- "$ref": "./tags/actions_and_capabilities.yml"
- "$ref": "./tags/activities.yml"
- "$ref": "./tags/attachments.yml"

@ -18,6 +18,14 @@ get:
schema:
default: '[["id", "asc"]]'
type: string
- description: |-
Comma separated list of properties to include.
example: 'total,elements/name,elements/self,self'
in: query
name: select
required: false
schema:
type: string
responses:
'200':
content:

@ -22,6 +22,14 @@ get:
required: false
schema:
type: string
- description: |-
Comma separated list of properties to include.
example: 'total,elements/name,elements/self,self'
in: query
name: select
required: false
schema:
type: string
responses:
'200':
content:

@ -60,6 +60,14 @@ get:
schema:
default: 'false'
type: boolean
- description: |-
Comma separated list of properties to include.
example: 'total,elements/subject,elements/id,self'
in: query
name: select
required: false
schema:
type: string
responses:
'200':
content:

@ -57,6 +57,14 @@ get:
required: false
schema:
type: string
- description: |-
Comma separated list of properties to include.
example: 'total,elements/identifier,elements/name'
in: query
name: select
required: false
schema:
type: string
responses:
'200':
content:

@ -46,6 +46,14 @@ get:
required: false
schema:
type: string
- description: |-
Comma separated list of properties to include.
example: 'total,elements/name,elements/self,self'
in: query
name: select
required: false
schema:
type: string
responses:
'200':
content:

@ -111,6 +111,14 @@ get:
schema:
default: 'false'
type: boolean
- description: |-
Comma separated list of properties to include.
example: 'total,elements/subject,elements/id,self'
in: query
name: select
required: false
schema:
type: string
responses:
'200':
content:

@ -147,6 +147,9 @@ description: |-
* `urn:openproject-org:api:v3:errors:InvalidUserStatusTransition` (**HTTP 400**)
The client used an invalid transition in the attempt to change the status of a user account.
* `urn:openproject-org:api:v3:errors:InvalidSignal` (**HTTP 400**)
The client specified a select not available on the resource, e.g because the property/link does not exist on it.
* `urn:openproject-org:api:v3:errors:Unauthenticated` (**HTTP 401**)
The client has to authenticate to access the requested resource.

@ -0,0 +1,83 @@
---
description: |-
Some endpoints, especially those returning `Collection` resources, support signaling desired properties. By signaling, the client
can convey to the server the properties to include in a response.
Currently only `select` is supported which allows to specify the subset of properties a client is interested in. The benefit of using `select`
is reduced response time. Other signaling, especially expanding the embedded resources to include as well over multiple layers of embedding
are in consideration to be implemented (probably named `embed`) but as of now, they are not supported. Please also see
[the specification for OData that inspired this feature](https://www.odata.org/documentation/odata-version-2-0/uri-conventions/).
For example, a resource `/api/v3/bogus` that without signaling returns:
```
{
"_type": "Collection"
"count": 20,
"total": 554,
"_embedded": {
"elements": [
{
"id": 1,
"name": "Some name"
},
{
"id": 9,
"name": "Another name"
}
]
},
"_links": {
"self": {
"href": "/api/v3/bogus",
"title": "A bogus collection"
},
"bar": {
"href": "/api/v3/bar",
"title": "Foobar"
}
}
}
```
can via signaling `/api/v3/bogus?select=total,elements/name,bar` be instructed to return:
```
{
"total": 554,
"_embedded": {
"elements": [
{
"name": "Some name"
},
{
"name": "Another name"
}
]
},
"_links": {
"bar": {
"href": "/api/v3/bar",
"title": "Foobar"
}
}
}
```
The `select` query property is a comma separated list of the properties to include, e.g. `select=total,elements/name,bar`.
The API also accepts alternative styles of writing like `select=["total","elements/name","bar"]`. Each individual item in the list
is the path inside the resource. So while `total` refers to the property on the top level, `elements/name` refers to the property `name` within
the collection of `elements`. The full path has to be provided for every property, e.g. `select=elements/name,elements/id`.
The order of the list has no impact on the selection. There is also a wildcard `*` which will result in every property on that level to be selected.
To select every property in the example above, the client would have to signal `select=*,elements/*`.
Please note that the nesting into `_embedded` and `_links` is not included in the query prop `select` as
links in the context of HAL can be considered properties of the resource just the same as unnested properties and forcing
clients to write the full nesting would not increase clarity.
Link properties are considered to be a single value that cannot be split up further. Every property within a link will be returned
if the link is signaled to be selected.
The `select` signaling flag has been introduced for performance reasons. Not every end point supports it and those that do oftentimes only
allow a subset of their resource's properties to be selected. End points supporting the `select` query prop are documented accordingly.
name: Signaling

@ -31,7 +31,7 @@
module API
module Decorators
class OffsetPaginatedCollection < ::API::Decorators::Collection
include ::API::Utilities::PageSizeHelper
include ::API::Utilities::UrlPropsParsingHelper
def self.per_page_default(relation)
relation.base_class.per_page

@ -57,7 +57,20 @@ module API
options[:column]
end
"'#{name}', #{representation}"
if options[:render_if]
<<-SQL.squish
'#{name}',
CASE WHEN #{options[:render_if].call(walker_results)} THEN
#{representation}
ELSE
NULL
END
SQL
else
<<-SQL.squish
'#{name}', #{representation}
SQL
end
end.join(', ')
end
@ -80,48 +93,59 @@ module API
scope
end
def link(name, column: nil, path: nil, title: nil, href: nil, join: nil, render_if: nil, **additional_properties)
def link(name,
column: nil,
path: nil,
title: nil,
href: nil,
join: nil,
render_if: nil,
sql: nil,
**additional_properties)
links[name] = { column: column,
path: path,
title: title,
join: join,
href: href,
render_if: render_if,
sql: sql,
additional_properties: additional_properties }
end
def links_selects(select, walker_result)
selected_links(select)
.map do |name, link|
path_name = link[:path] ? link[:path][:api] : name
title = link[:title] ? link[:title].call : "#{name}.name"
column = link[:column] ? link[:column].call : name
href = link[:href] ? link[:href].call(walker_result) : "format('#{api_v3_paths.send(path_name, '%s')}', #{column})"
link_attributes = ["'href'", href]
if title
link_attributes += ["'title'", title]
end
(link[:additional_properties] || {}).each do |key, value|
link_attributes += ["'#{key}'", value]
end
if link[:render_if]
<<-SQL
'#{name}',
CASE WHEN #{link[:render_if].call(walker_result)} THEN
json_build_object(#{link_attributes.join(', ')})
ELSE
NULL
END
if link[:sql]
<<-SQL.squish
'#{name}', #{link[:sql].call}
SQL
else
<<-SQL
'#{name}', json_build_object(#{link_attributes.join(', ')})
SQL
title = link[:title] ? link[:title].call : "#{name}.name"
link_attributes = ["'href'", link_href(link, name, walker_result)]
if title
link_attributes += ["'title'", title]
end
(link[:additional_properties] || {}).each do |key, value|
link_attributes += ["'#{key}'", value]
end
if link[:render_if]
<<-SQL.squish
'#{name}',
CASE WHEN #{link[:render_if].call(walker_result)} THEN
json_build_object(#{link_attributes.join(', ')})
ELSE
NULL
END
SQL
else
<<-SQL.squish
'#{name}', json_build_object(#{link_attributes.join(', ')})
SQL
end
end
end
.join(', ')
@ -143,19 +167,20 @@ module API
link[:column]
end
<<-SQL
next unless representation
<<-SQL.squish
'#{name}', #{representation}
SQL
end
.flatten
.join(', ')
end
def select_sql(select, walker_result)
<<~SELECT
json_strip_nulls(json_build_object(
#{[properties_sql(select, walker_result),
select_links(select, walker_result),
select_embedded(select, walker_result)].compact.join(', ')}
#{json_object_string(select, walker_result)}
))
SELECT
end
@ -166,7 +191,7 @@ module API
def to_sql(walker_result)
ctes = walker_result.ctes.map do |key, sql|
<<~SQL
<<~SQL.squish
#{key} AS (
#{sql}
)
@ -175,7 +200,7 @@ module API
ctes_sql = ctes.any? ? "WITH #{ctes.join(', ')}" : ""
<<~SQL
<<~SQL.squish
#{ctes_sql}
SELECT
@ -185,30 +210,37 @@ module API
SQL
end
protected
def json_object_string(select, walker_result)
[properties_sql(select, walker_result),
select_links(select, walker_result),
select_embedded(select, walker_result)]
.compact_blank
.join(', ')
end
# All properties and links that the client can correctly signal to have selected.
def valid_selects
links.keys + properties.keys + [:*]
end
private
def select_embedded(select, walker_result)
embedded = embedded_selects(select, walker_result)
if embedded.present?
<<~SQL
'_embedded', json_strip_nulls(json_build_object(
#{embedded_selects(select, walker_result)}
))
SQL
namespaced_json_object('_embedded') do
embedded_selects(select, walker_result)
end
end
def select_links(select, walker_result)
<<~SELECT
'_links', json_strip_nulls(json_build_object(
#{links_selects(select, walker_result)}
))
SELECT
namespaced_json_object('_links') do
links_selects(select, walker_result)
end
end
def select_from(walker_result)
"(#{walker_result.scope.to_sql}) element"
"(#{walker_result.projection_scope.to_sql}) element"
end
def selected_links(select)
@ -239,10 +271,36 @@ module API
end
def ensure_valid_selects(requested)
supported = links.keys + properties.keys + [:*]
invalid = requested - supported
invalid = requested - valid_selects
raise API::Errors::InvalidSignal.new(invalid, valid_selects, :select) if invalid.any?
end
def namespaced_json_object(namespace)
json_object = yield
return if json_object.blank?
<<~SELECT
'#{namespace}', json_strip_nulls(json_build_object(
#{json_object}
))
SELECT
end
def link_href(link, name, walker_result)
path_name = link[:path] ? link[:path][:api] : name
column = link[:column] ? link[:column].call : name
link[:href] ? link[:href].call(walker_result) : "format('#{api_v3_paths.send(path_name, '%s')}', #{column})"
end
def sql_offset(walker_result)
(walker_result.offset - 1) * walker_result.page_size
end
raise API::Errors::InvalidSignal.new(invalid, supported, :select) if invalid.any?
def sql_limit(walker_result)
walker_result.page_size
end
end
end

@ -34,16 +34,18 @@ module API
class << self
def ctes(walker_result)
{
all_elements: walker_result.scope.to_sql,
page_elements: "SELECT * FROM all_elements LIMIT #{sql_limit(walker_result)} OFFSET #{sql_offset(walker_result)}",
total: "SELECT COUNT(*) from all_elements"
projection: walker_result.projection_scope.limit(sql_limit(walker_result)).offset(sql_offset(walker_result)).to_sql,
# Currently there does not appear to be a way to correctly transform the filter_scope into sql
# (e.g. by calling #to_sql) so that the extra call to the database is avoided. The main problem
# is that LEFT JOINS added via includes are not part of the SQL generated by #to_sql.
total: "SELECT #{walker_result.filter_scope.count}"
}
end
private
def select_from(walker_result)
"page_elements"
"projection"
end
def full_self_path(walker_results, overrides = {})
@ -51,15 +53,33 @@ module API
end
def href_query(walker_results, overrides)
walker_results.url_query.merge(overrides).to_query
end
query_params = walker_results.url_query.merge(overrides)
if (select_params = query_params.delete(:select))
query_params[:select] = select_href_params(select_params).flatten.join(',')
end
def sql_offset(walker_result)
(walker_result.offset - 1) * walker_result.page_size
# Embedding is not supported yet but parts of the functionality is already in place.
query_params.delete(:embed)
query_params.to_query
end
def sql_limit(walker_result)
walker_result.page_size
# Turns the nested values for select and embed into the external representation
# of a comma separated string. E.g.
# { '*' => {}, 'elements' => { '*' => {} } }
# is turned into
# '*,elements/*'
def select_href_params(params)
params.map do |key, value|
if value.empty?
key
else
select_href_params(value).map do |sub_value|
"#{key}/#{sub_value}"
end
end
end
end
end
@ -80,31 +100,33 @@ module API
link :self,
href: ->(walker_result) { "'#{full_self_path(walker_result)}'" },
title: -> { nil }
title: -> {}
link :jumpTo,
href: ->(walker_result) { "'#{full_self_path(walker_result, offset: '{offset}')}'" },
title: -> { nil },
title: -> {},
templated: true
link :changeSize,
href: ->(walker_result) { "'#{full_self_path(walker_result, pageSize: '{size}')}'" },
title: -> { nil },
title: -> {},
templated: true
link :previousByOffset,
href: ->(walker_result) { "'#{full_self_path(walker_result, offset: walker_result.offset - 1)}'" },
render_if: ->(walker_result) { walker_result.offset > 1 },
title: -> { nil }
title: -> {}
link :nextByOffset,
href: ->(walker_result) { "'#{full_self_path(walker_result, offset: walker_result.offset + 1)}'" },
render_if: ->(walker_result) { "#{walker_result.offset * walker_result.page_size} < (SELECT * FROM total)" },
title: -> { nil }
title: -> {}
embedded :elements,
representation: ->(walker_result) do
"json_agg(#{walker_result.replace_map['elements']})"
replacement = walker_result.replace_map['elements']
replacement ? "json_agg(#{replacement})" : nil
end
end
end

@ -30,7 +30,7 @@ module API
module Utilities
module Endpoints
class Index
include ::API::Utilities::PageSizeHelper
include ::API::Utilities::UrlPropsParsingHelper
def initialize(model:,
api_name: model.name.demodulize,

@ -30,7 +30,7 @@
module API
module Utilities
module PageSizeHelper
module UrlPropsParsingHelper
##
# Determine set page_size from string
def resolve_page_size(string)
@ -68,6 +68,29 @@ module API
def to_i_or_nil(string)
string ? string.to_i : nil
end
# Parses a comma separated list of values and turns it into
# a nested hash. e.g.:
# = "a,b/c/d,e,b/f"
# is turned into:
# = { "a" => {}, "e" => {}, "b" => { "c" => { "d" => {} }, "f" => {} } }
# The order of the values does not matter.
# It also accepts an array of individual strings, e.g.:
# = ["a","b/c/d","e","b/f"]
def nested_from_csv(value)
return unless value
value
.delete_prefix('[')
.delete_suffix(']')
.split(',')
.map { |path| nested_hash(path.strip.tr("\"'", '').split('/')) }
.inject({}) { |hash, nested| hash.deep_merge(nested) }
end
def nested_hash(path)
{ path[0] => path.length > 1 ? nested_hash(path[1..]) : {} }
end
end
end
end

@ -39,7 +39,7 @@ module API
property :id,
representation: ->(*) {
<<~SQL
<<~SQL.squish
CASE
WHEN context_id IS NULL THEN action || '/g-' || principal_id
ELSE action || '/p' || context_id || '-' || principal_id
@ -50,22 +50,22 @@ module API
link :self,
path: { api: :capability, params: %w(action) },
column: -> {
<<~SQL
<<~SQL.squish
CASE
WHEN context_id IS NULL THEN action || '/g-' || principal_id
ELSE action || '/p' || context_id || '-' || principal_id
END
SQL
},
title: -> { nil }
title: -> {}
link :action,
path: { api: :action, params: %w(action) },
title: -> { nil }
title: -> {}
link :context,
href: ->(*) {
<<~SQL
<<~SQL.squish
CASE
WHEN context_id IS NULL THEN '#{api_v3_paths.capabilities_contexts_global}'
ELSE format('#{api_v3_paths.project('%s')}', context_id)
@ -73,7 +73,7 @@ module API
SQL
},
title: ->(*) {
<<~SQL
<<~SQL.squish
CASE
WHEN context_id IS NULL THEN '#{I18n.t('activerecord.errors.models.capability.context.global')}'
ELSE context_name
@ -86,7 +86,7 @@ module API
link :principal,
href: ->(*) {
<<~SQL
<<~SQL.squish
CASE principal_type
WHEN 'Group' THEN format('#{api_v3_paths.group('%s')}', principal_id)
WHEN 'PlaceholderUser' THEN format('#{api_v3_paths.placeholder_user('%s')}', principal_id)
@ -101,7 +101,7 @@ module API
" || ' ' || "
end
<<~SQL
<<~SQL.squish
CASE principal_type
WHEN 'Group' THEN lastname
WHEN 'PlaceholderUser' THEN lastname

@ -0,0 +1,37 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module Groups
class GroupSqlCollectionRepresenter < API::Decorators::SqlCollectionRepresenter
self.embed_map = {
elements: GroupSqlRepresenter
}.with_indifferent_access
end
end
end
end

@ -0,0 +1,48 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module Groups
class GroupSqlRepresenter
include API::Decorators::Sql::Hal
link :self,
path: { api: :group, params: %w(id) },
column: -> { :id },
title: -> { 'lastname' }
property :_type,
representation: ->(*) { "'Group'" }
property :id
property :name,
column: :lastname
end
end
end
end

@ -35,7 +35,7 @@ module API
authorize_any %i[view_members manage_members], global: true
end
get &::API::V3::Utilities::Endpoints::Index
get &::API::V3::Utilities::Endpoints::SqlFallbackedIndex
.new(model: Group)
.mount
post &::API::V3::Utilities::Endpoints::Create

@ -30,7 +30,7 @@ module API
module V3
module Memberships
class MembershipsAPI < ::API::OpenProjectAPI
helpers ::API::Utilities::PageSizeHelper
helpers ::API::Utilities::UrlPropsParsingHelper
resources :memberships do
get &::API::V3::Utilities::Endpoints::Index

@ -0,0 +1,37 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module PlaceholderUsers
class PlaceholderUserSqlCollectionRepresenter < API::Decorators::SqlCollectionRepresenter
self.embed_map = {
elements: PlaceholderUserSqlRepresenter
}.with_indifferent_access
end
end
end
end

@ -0,0 +1,48 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module PlaceholderUsers
class PlaceholderUserSqlRepresenter
include API::Decorators::Sql::Hal
link :self,
path: { api: :placeholder_user, params: %w(id) },
column: -> { :id },
title: -> { 'lastname' }
property :_type,
representation: ->(*) { "'PlaceholderUser'" }
property :id
property :name,
column: :lastname
end
end
end
end

@ -31,7 +31,7 @@ module API
module PlaceholderUsers
class PlaceholderUsersAPI < ::API::OpenProjectAPI
resources :placeholder_users do
get &::API::V3::Utilities::Endpoints::Index
get &::API::V3::Utilities::Endpoints::SqlFallbackedIndex
.new(model: PlaceholderUser, scope: -> { PlaceholderUser.visible(current_user) })
.mount

@ -0,0 +1,37 @@
# OpenProject is an open source project management software.
# Copyright (C) 2022 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.
#-- encoding: UTF-8
# Exists to satisfy the expectations of the default end points.
module API
module V3
module Principals
class PrincipalCollectionRepresenter < ::API::V3::Users::UserCollectionRepresenter
end
end
end
end

@ -0,0 +1,37 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module Principals
class PrincipalSqlCollectionRepresenter < API::Decorators::SqlCollectionRepresenter
self.embed_map = {
elements: PrincipalSqlRepresenter
}.with_indifferent_access
end
end
end
end

@ -0,0 +1,70 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module Principals
# This representer is able to render all the concrete classes of Principal: User, Group and PlaceholderUser.
class PrincipalSqlRepresenter
include API::Decorators::Sql::Hal
class << self
def select_sql(select, walker_result)
<<~SELECT
json_strip_nulls(
CASE
WHEN type = 'Group' THEN json_build_object(#{group_select_sql(select, walker_result)})
WHEN type = 'PlaceholderUser' THEN json_build_object(#{placeholder_user_select_sql(select, walker_result)})
WHEN type = 'User' THEN json_build_object(#{user_select_sql(select, walker_result)})
END
)
SELECT
end
private
def group_select_sql(select, walker_result)
API::V3::Groups::GroupSqlRepresenter.json_object_string(select, walker_result)
end
def placeholder_user_select_sql(select, walker_result)
API::V3::PlaceholderUsers::PlaceholderUserSqlRepresenter.json_object_string(select, walker_result)
end
def user_select_sql(select, walker_result)
API::V3::Users::UserSqlRepresenter.json_object_string(select, walker_result)
end
def valid_selects
API::V3::Groups::GroupSqlRepresenter.valid_selects &
API::V3::PlaceholderUsers::PlaceholderUserSqlRepresenter.valid_selects &
API::V3::Users::UserSqlRepresenter.valid_selects
end
end
end
end
end
end

@ -30,27 +30,11 @@ module API
module V3
module Principals
class PrincipalsAPI < ::API::OpenProjectAPI
helpers ::API::Utilities::PageSizeHelper
resource :principals do
get do
query = ParamsToQueryService.new(Principal, current_user).call(params)
if query.valid?
principals = query
.results
.where(id: Principal.visible(current_user))
.includes(:preference)
::API::V3::Users::PaginatedUserCollectionRepresenter.new(principals,
self_link: api_v3_paths.principals,
page: to_i_or_nil(params[:offset]),
per_page: resolve_page_size(params[:pageSize]),
current_user: current_user)
else
raise ::API::Errors::InvalidQuery.new(query.errors.full_messages)
end
end
get &::API::V3::Utilities::Endpoints::SqlFallbackedIndex
.new(model: Principal,
scope: -> { Principal.visible(current_user).includes(:preference) })
.mount
end
end
end

@ -41,7 +41,7 @@ module API
scope: -> {
Principal.possible_assignee(@project).includes(:preference)
},
render_representer: Users::UserCollectionRepresenter)
render_representer: Users::UnpaginatedUserCollectionRepresenter)
.mount
end
end

@ -41,7 +41,7 @@ module API
scope: -> {
Principal.possible_assignee(@project).includes(:preference)
},
render_representer: Users::UserCollectionRepresenter)
render_representer: Users::UnpaginatedUserCollectionRepresenter)
.mount
end
end

@ -0,0 +1,37 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module Projects
class ProjectSqlCollectionRepresenter < API::Decorators::SqlCollectionRepresenter
self.embed_map = {
elements: ProjectSqlRepresenter
}.with_indifferent_access
end
end
end
end

@ -0,0 +1,133 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 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-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 COPYRIGHT and LICENSE files for more details.
#++
module API
module V3
module Projects
class ProjectSqlRepresenter
include API::Decorators::Sql::Hal
class << self
def ctes(walker_result)
{
ancestors: ancestors_sql(walker_result)
}
end
protected
def ancestors_sql(walker_result)
<<-SQL.squish
SELECT id, CASE WHEN count(link) = 0 THEN '[]' ELSE json_agg(link) END ancestors
FROM
(
SELECT
origin.id,
#{ancestor_projection} link
FROM projects origin
LEFT OUTER JOIN projects ancestors
ON ancestors.lft < origin.lft AND ancestors.rgt > origin.rgt
#{visibility_join}
WHERE origin.id IN (#{origin_subselect(walker_result).select(:id).to_sql})
ORDER by origin.id, ancestors.lft
) ancestors
GROUP BY id
SQL
end
def origin_subselect(walker_result)
if walker_result.page_size
walker_result.filter_scope.limit(sql_limit(walker_result)).offset(sql_offset(walker_result))
else
walker_result.filter_scope
end
end
def ancestor_projection
if User.current.admin?
<<-SQL.squish
CASE
WHEN ancestors.id IS NOT NULL
THEN json_build_object('href', format('/api/v3/projects/%s', ancestors.id), 'title', ancestors.name)
ELSE NULL
END
SQL
else
<<-SQL.squish
CASE
WHEN ancestors.id IS NOT NULL AND visible_ancestors.id IS NOT NULL
THEN json_build_object('href', format('/api/v3/projects/%s', ancestors.id), 'title', ancestors.name)
WHEN ancestors.id IS NOT NULL AND visible_ancestors.id IS NULL
THEN json_build_object('href', '#{API::V3::URN_UNDISCLOSED}', 'title', '#{I18n.t(:'api_v3.undisclosed.ancestor')}')
ELSE NULL
END
SQL
end
end
def visibility_join
if User.current.admin?
''
else
<<-SQL.squish
LEFT OUTER JOIN (#{Project.visible.to_sql}) visible_ancestors
ON visible_ancestors.id = ancestors.id
SQL
end
end
end
link :self,
path: { api: :project, params: %w(id) },
column: -> { :id },
title: -> { :name }
link :ancestors,
sql: -> { 'ancestors' },
join: {
table: :ancestors,
condition: 'ancestors.id = projects.id',
select: 'ancestors'
}
property :_type,
representation: ->(*) { "'Project'" }
property :id
property :name
property :identifier
property :active
property :public
end
end
end
end

@ -41,12 +41,12 @@ module API
end
resources :projects do
get &::API::V3::Utilities::Endpoints::Index.new(model: Project,
scope: -> {
visible_project_scope
.includes(ProjectRepresenter.to_eager_load)
})
.mount
get &::API::V3::Utilities::Endpoints::SqlFallbackedIndex.new(model: Project,
scope: -> {
visible_project_scope
.includes(ProjectRepresenter.to_eager_load)
})
.mount
post &::API::V3::Utilities::Endpoints::Create.new(model: Project)
.mount

@ -28,16 +28,10 @@
# See COPYRIGHT and LICENSE files for more details.
#++
# TODO: this is to be removed or rather the UserCollectionRepresenter is
# to be turned into an OffsetPaginatedCollection representer.
# It is not possible to do that right now as we do not have a
# solution for an accessible autocompleter drop down widget. We therefore
# have to fetch all users when we want to present them inside of a drop down.
module API
module V3
module Users
class PaginatedUserCollectionRepresenter < ::API::Decorators::OffsetPaginatedCollection
class UnpaginatedUserCollectionRepresenter < ::API::Decorators::UnpaginatedCollection
include API::V3::Principals::NotBuiltinElements
end
end

@ -31,7 +31,7 @@
module API
module V3
module Users
class UserCollectionRepresenter < ::API::Decorators::UnpaginatedCollection
class UserCollectionRepresenter < ::API::Decorators::OffsetPaginatedCollection
include API::V3::Principals::NotBuiltinElements
end
end

@ -0,0 +1,37 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module Users
class UserSqlCollectionRepresenter < API::Decorators::SqlCollectionRepresenter
self.embed_map = {
elements: UserSqlRepresenter
}.with_indifferent_access
end
end
end
end

@ -0,0 +1,83 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module Users
class UserSqlRepresenter
include API::Decorators::Sql::Hal
class << self
def user_name_projection(*)
case Setting.user_format
when :firstname_lastname
'concat(firstname, \' \', lastname)'
when :firstname
'firstname'
when :lastname_firstname
'concat(lastname, \' \', firstname)'
when :lastname_coma_firstname
'concat(lastname, \', \', firstname)'
when :lastname_n_firstname
'concat_ws(lastname, \'\', firstname)'
when :username
'login'
else
raise ArgumentError, "Invalid user format"
end
end
def render_if_manage_user_or_self(*)
if User.current.allowed_to_globally?(:manage_user)
'TRUE'
else
"id = #{User.current.id}"
end
end
end
link :self,
path: { api: :user, params: %w(id) },
column: -> { :id },
title: method(:user_name_projection)
property :_type,
representation: ->(*) { "'User'" }
property :id
property :name,
representation: method(:user_name_projection)
property :firstname,
render_if: method(:render_if_manage_user_or_self)
property :lastname,
render_if: method(:render_if_manage_user_or_self)
end
end
end
end

@ -26,15 +26,10 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require 'api/v3/users/user_representer'
require 'api/v3/users/paginated_user_collection_representer'
module API
module V3
module Users
class UsersAPI < ::API::OpenProjectAPI
helpers ::API::Utilities::PageSizeHelper
helpers do
def user_transition(allowed)
if allowed
@ -47,30 +42,21 @@ module API
fail ::API::Errors::InvalidUserStatusTransition
end
end
def authorize_user_cru_allowed
authorize_by_with_raise(current_user.allowed_to_globally?(:manage_user))
end
end
resources :users do
post &::API::V3::Utilities::Endpoints::Create.new(model: User).mount
get do
authorize_user_cru_allowed
query = ParamsToQueryService.new(User, current_user).call(params)
if query.valid?
users = query.results.includes(:preference)
PaginatedUserCollectionRepresenter.new(users,
self_link: api_v3_paths.users,
page: to_i_or_nil(params[:offset]),
per_page: resolve_page_size(params[:pageSize]),
current_user: current_user)
else
raise ::API::Errors::InvalidQuery.new(query.errors.full_messages)
# The namespace only exists to add the after_validation callback
namespace '' do
after_validation do
authorize_by_with_raise(current_user.allowed_to_globally?(:manage_user))
end
get &::API::V3::Utilities::Endpoints::SqlFallbackedIndex
.new(model: User,
scope: -> { User.user.includes(:preference) })
.mount
end
mount ::API::V3::Users::Schemas::UserSchemaAPI

@ -0,0 +1,69 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module Utilities
module Endpoints
class SqlFallbackedIndex < SqlIndex
private
def render_paginated_success(results, query, params, self_path)
resulting_params = calculate_resulting_params(query, params)
if resulting_params[:select]
::API::V3::Utilities::SqlRepresenterWalker
.new(results,
current_user: User.current,
self_path: self_path,
url_query: resulting_params)
.walk(deduce_render_representer)
else
deduce_fallback_render_representer
.new(results,
self_link: self_path,
query: resulting_params,
page: resulting_params[:offset],
per_page: resulting_params[:pageSize],
groups: calculate_groups(query),
current_user: User.current)
end
end
def deduce_fallback_render_representer
"::API::V3::#{deduce_api_namespace}::#{api_name}CollectionRepresenter".constantize
end
def calculate_resulting_params(query, provided_params)
super.tap do |params|
params.delete(:select) unless provided_params.has_key?(:select)
end
end
end
end
end
end
end

@ -38,8 +38,6 @@ module API
::API::V3::Utilities::SqlRepresenterWalker
.new(results,
embed: { 'elements' => {} },
select: { '*' => {}, 'elements' => { '*' => {} } },
current_user: User.current,
self_path: self_path,
url_query: resulting_params)
@ -53,6 +51,12 @@ module API
def deduce_render_representer
"::API::V3::#{deduce_api_namespace}::#{api_name}SqlCollectionRepresenter".constantize
end
def calculate_resulting_params(query, provided_params)
super.tap do |params|
params[:select] = nested_from_csv(provided_params['select']) || { '*' => {}, 'elements' => { '*' => {} } }
end
end
end
end
end

@ -60,9 +60,8 @@ module API
def render(scope)
::API::V3::Utilities::SqlRepresenterWalker
.new(scope.limit(1),
embed: {},
select: { '*' => {} },
current_user: User.current)
current_user: User.current,
url_query: { select: { '*' => {} } })
.walk(render_representer)
end

@ -497,14 +497,15 @@ module API
"#{project(project_id)}/work_packages"
end
def self.path_for(path, filters: nil, sort_by: nil, group_by: nil, page_size: nil, offset: nil)
def self.path_for(path, filters: nil, sort_by: nil, group_by: nil, page_size: nil, offset: nil, select: nil)
query_params = {
filters: filters&.to_json,
sortBy: sort_by&.to_json,
groupBy: group_by,
pageSize: page_size,
offset: offset
}.reject { |_, v| v.blank? }
offset: offset,
select: select
}.compact_blank
if query_params.any?
"#{send(path)}?#{query_params.to_query}"

@ -32,20 +32,17 @@ module API
module V3
module Utilities
class SqlRepresenterWalker
include API::Utilities::PageSizeHelper
include API::Utilities::UrlPropsParsingHelper
def initialize(scope,
current_user:,
url_query: {},
embed: {},
select: {},
self_path: nil)
self.scope = scope
self.current_user = current_user
self.embed = embed
self.select = select
self.self_path = self_path
self.url_query = url_query
# Hard wiring the properties to embed is a work around until signaling the properties to embed is implemented
self.url_query = url_query.merge(embed: { 'elements' => {} })
end
def walk(start)
@ -60,7 +57,7 @@ module API
end
embedded_depth_first([], start) do |_, stack, current_representer|
result.scope = current_representer.joins(select_for(stack), result.scope)
result.projection_scope = current_representer.joins(select_for(stack), result.projection_scope)
end
embedded_depth_first([], start) do |_, _, current_representer|
@ -80,12 +77,18 @@ module API
attr_accessor :scope,
:current_user,
:embed,
:select,
:sql,
:url_query,
:self_path
def embed
url_query[:embed]
end
def select
url_query[:select]
end
def embedded_depth_first(stack, current_representer, &block)
up_map = {}
@ -96,15 +99,15 @@ module API
up_map[key] = embedded_depth_first(stack.dup << key, representer, &block)
end
yield up_map, stack, current_representer
yield up_map, stack, current_representer if select_for(stack)
end
def select_for(stack)
stack.any? ? select.dig(*stack) : select
end
def embed_for(stack)
stack.any? ? embed.dig(*stack) : embed
def embed_for(stacker)
stacker.any? ? embed.dig(*stacker) : embed
end
end
end

@ -33,14 +33,16 @@ module API
module Utilities
class SqlWalkerResults
def initialize(scope, url_query:, self_path: nil, replace_map: {})
self.scope = scope
self.filter_scope = scope.dup
self.projection_scope = scope.dup.distinct(false).reselect("#{scope.model.table_name}.*")
self.ctes = {}
self.self_path = self_path
self.url_query = url_query
self.replace_map = replace_map
end
attr_accessor :scope,
attr_accessor :filter_scope,
:projection_scope,
:sql,
:selects,
:ctes,

@ -32,7 +32,7 @@ module API
module V3
module WorkPackages
class WatchersAPI < ::API::OpenProjectAPI
helpers ::API::Utilities::PageSizeHelper
helpers ::API::Utilities::UrlPropsParsingHelper
get '/available_watchers' do
authorize(:add_work_package_watchers, context: @work_package.project)
@ -42,7 +42,7 @@ module API
if query.valid?
users = query.results.merge(@work_package.addable_watcher_users).includes(:preference)
::API::V3::Users::PaginatedUserCollectionRepresenter.new(
::API::V3::Users::UserCollectionRepresenter.new(
users,
self_link: api_v3_paths.users,
page: to_i_or_nil(params[:offset]),
@ -59,9 +59,9 @@ module API
def watchers_collection
watchers = @work_package.watcher_users.merge(Principal.not_locked)
self_link = api_v3_paths.work_package_watchers(@work_package.id)
Users::UserCollectionRepresenter.new(watchers,
self_link: self_link,
current_user: current_user)
Users::UnpaginatedUserCollectionRepresenter.new(watchers,
self_link: self_link,
current_user: current_user)
end
end

@ -0,0 +1,37 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module WorkPackages
class WorkPackageSqlCollectionRepresenter < API::Decorators::SqlCollectionRepresenter
self.embed_map = {
elements: WorkPackageSqlRepresenter
}.with_indifferent_access
end
end
end
end

@ -0,0 +1,47 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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 API
module V3
module WorkPackages
class WorkPackageSqlRepresenter
include API::Decorators::Sql::Hal
link :self,
path: { api: :work_package, params: %w(id) },
column: -> { :id },
title: -> { 'subject' }
property :_type,
representation: ->(*) { "'WorkPackage'" }
property :id
property :subject
end
end
end
end

@ -30,7 +30,7 @@ module API
module V3
module TimeEntries
class TimeEntriesAPI < ::API::OpenProjectAPI
helpers ::API::Utilities::PageSizeHelper
helpers ::API::Utilities::UrlPropsParsingHelper
resources :time_entries do
get &::API::V3::Utilities::Endpoints::Index.new(model: TimeEntry).mount

@ -30,7 +30,7 @@ module API
module V3
module Documents
class DocumentsAPI < ::API::OpenProjectAPI
helpers ::API::Utilities::PageSizeHelper
helpers ::API::Utilities::UrlPropsParsingHelper
resources :documents do
get do

@ -32,7 +32,7 @@ module API
class GridsAPI < ::API::OpenProjectAPI
resources :grids do
helpers do
include API::Utilities::PageSizeHelper
include API::Utilities::UrlPropsParsingHelper
end
get do

@ -28,10 +28,10 @@
require 'spec_helper'
describe ::API::Utilities::PageSizeHelper do
describe ::API::Utilities::UrlPropsParsingHelper do
let(:clazz) do
Class.new do
include ::API::Utilities::PageSizeHelper
include ::API::Utilities::UrlPropsParsingHelper
end
end
let(:subject) { clazz.new }

@ -46,9 +46,8 @@ describe ::API::V3::Actions::ActionSqlRepresenter, 'rendering' do
subject(:json) do
::API::V3::Utilities::SqlRepresenterWalker
.new(scope,
embed: {},
select: { 'id' => {}, '_type' => {}, 'self' => {} },
current_user: current_user)
current_user: current_user,
url_query: { select: { 'id' => {}, '_type' => {}, 'self' => {} } })
.walk(API::V3::Actions::ActionSqlRepresenter)
.to_json
end

@ -58,11 +58,12 @@ describe ::API::V3::Capabilities::CapabilitySqlRepresenter, 'rendering' do
subject(:json) do
::API::V3::Utilities::SqlRepresenterWalker
.new(scope,
embed: {},
select: { 'id' => {}, '_type' => {}, 'self' => {}, 'action' => {}, 'context' => {}, 'principal' => {} },
current_user: current_user)
.walk(API::V3::Capabilities::CapabilitySqlRepresenter)
.new(
scope,
current_user: current_user,
url_query: { select: { 'id' => {}, '_type' => {}, 'self' => {}, 'action' => {}, 'context' => {}, 'principal' => {} } }
)
.walk(described_class)
.to_json
end

@ -0,0 +1,74 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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'
describe ::API::V3::Groups::GroupSqlRepresenter, 'rendering' do
include ::API::V3::Utilities::PathHelper
subject(:json) do
::API::V3::Utilities::SqlRepresenterWalker
.new(scope,
current_user: current_user,
url_query: { select: select })
.walk(described_class)
.to_json
end
let(:scope) do
Group
.where(id: group.id)
end
let(:group) { create(:group) }
let(:select) { { '*' => {} } }
current_user do
create(:user)
end
context 'when rendering all supported properties' do
let(:expected) do
{
_type: "Group",
id: group.id,
name: group.name,
_links: {
self: {
href: api_v3_paths.group(group.id),
title: group.name
}
}
}
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected.to_json)
end
end
end

@ -0,0 +1,74 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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'
describe ::API::V3::PlaceholderUsers::PlaceholderUserSqlRepresenter, 'rendering' do
include ::API::V3::Utilities::PathHelper
subject(:json) do
::API::V3::Utilities::SqlRepresenterWalker
.new(scope,
current_user: current_user,
url_query: { select: select })
.walk(described_class)
.to_json
end
let(:scope) do
PlaceholderUser
.where(id: placeholder_user.id)
end
let(:placeholder_user) { create(:placeholder_user) }
let(:select) { { '*' => {} } }
current_user do
create(:user)
end
context 'when rendering all supported properties' do
let(:expected) do
{
_type: "PlaceholderUser",
id: placeholder_user.id,
name: placeholder_user.name,
_links: {
self: {
href: api_v3_paths.placeholder_user(placeholder_user.id),
title: placeholder_user.name
}
}
}
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected.to_json)
end
end
end

@ -0,0 +1,141 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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'
describe ::API::V3::Principals::PrincipalSqlRepresenter, 'rendering' do
include ::API::V3::Utilities::PathHelper
subject(:json) do
::API::V3::Utilities::SqlRepresenterWalker
.new(scope,
current_user: current_user,
url_query: { select: select })
.walk(described_class)
.to_json
end
let(:scope) do
Principal
.where(id: rendered_principal.id)
end
let(:group) { create(:group) }
let(:placeholder_user) { create(:placeholder_user) }
let(:select) { { '*' => {} } }
current_user do
create(:user)
end
context 'when rendering all supported properties for a group' do
let(:rendered_principal) { group }
let(:expected) do
{
_type: "Group",
id: group.id,
name: group.name,
_links: {
self: {
href: api_v3_paths.group(group.id),
title: group.name
}
}
}
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected.to_json)
end
end
context 'when rendering all supported properties for a placeholder user' do
let(:rendered_principal) { placeholder_user }
let(:expected) do
{
_type: "PlaceholderUser",
id: placeholder_user.id,
name: placeholder_user.name,
_links: {
self: {
href: api_v3_paths.placeholder_user(placeholder_user.id),
title: placeholder_user.name
}
}
}
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected.to_json)
end
end
context 'when rendering all supported properties for a user' do
let(:rendered_principal) { current_user }
let(:expected) do
{
_type: "User",
id: current_user.id,
name: current_user.name,
firstname: current_user.firstname,
lastname: current_user.lastname,
_links: {
self: {
href: api_v3_paths.user(current_user.id),
title: current_user.name
}
}
}
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected.to_json)
end
end
context 'when rendering only the name property for a user' do
let(:rendered_principal) { current_user }
let(:select) { { 'name' => {} } }
let(:expected) do
{
name: current_user.name
}
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected.to_json)
end
end
end

@ -0,0 +1,145 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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'
describe ::API::V3::Projects::ProjectSqlCollectionRepresenter, 'rendering' do
include ::API::V3::Utilities::PathHelper
subject(:json) do
::API::V3::Utilities::SqlRepresenterWalker
.new(scope,
current_user: current_user,
self_path: 'some_path',
url_query: { offset: 1, pageSize: 5, select: select })
.walk(described_class)
.to_json
end
let(:scope) do
Project
.where(id: project.id)
end
let(:project) do
create(:project)
end
let(:role) { create(:role) }
let(:select) do
{ '*' => {}, 'elements' => { '*' => {} } }
end
current_user do
create(:user,
member_in_project: project,
member_through_role: role)
end
context 'when rendering everything' do
let(:expected) do
{
_type: "Collection",
pageSize: 5,
total: 1,
count: 1,
offset: 1,
_embedded: {
elements: [
{
id: project.id,
_type: "Project",
name: project.name,
identifier: project.identifier,
active: true,
public: false,
_links: {
ancestors: [],
self: {
href: api_v3_paths.project(project.id),
title: project.name
}
}
}
]
},
_links: {
self: {
href: "some_path?offset=1&pageSize=5&select=%2A%2Celements%2F%2A"
},
changeSize: {
href: "some_path?offset=1&pageSize=%7Bsize%7D&select=%2A%2Celements%2F%2A",
templated: true
},
jumpTo: {
href: "some_path?offset=%7Boffset%7D&pageSize=5&select=%2A%2Celements%2F%2A",
templated: true
}
}
}.to_json
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected)
end
end
context 'when rendering only collection attributes' do
let(:select) do
{ '*' => {} }
end
let(:expected) do
{
_type: "Collection",
pageSize: 5,
total: 1,
count: 1,
offset: 1,
_links: {
self: {
href: "some_path?offset=1&pageSize=5&select=%2A"
},
changeSize: {
href: "some_path?offset=1&pageSize=%7Bsize%7D&select=%2A",
templated: true
},
jumpTo: {
href: "some_path?offset=%7Boffset%7D&pageSize=5&select=%2A",
templated: true
}
}
}.to_json
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected)
end
end
end

@ -0,0 +1,200 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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'
describe ::API::V3::Projects::ProjectSqlRepresenter, 'rendering' do
include ::API::V3::Utilities::PathHelper
subject(:json) do
::API::V3::Utilities::SqlRepresenterWalker
.new(scope,
current_user: current_user,
url_query: { select: select })
.walk(described_class)
.to_json
end
let(:scope) do
Project
.where(id: project.id)
end
let(:project) do
create(:project)
end
let(:role) { create(:role) }
let(:select) { { '*' => {} } }
current_user do
create(:user,
member_in_project: project,
member_through_role: role)
end
context 'when rendering all supported properties' do
it 'renders as expected' do
expect(json)
.to be_json_eql(
{
id: project.id,
_type: "Project",
name: project.name,
identifier: project.identifier,
active: true,
public: false,
_links: {
ancestors: [],
self: {
href: api_v3_paths.project(project.id),
title: project.name
}
}
}.to_json
)
end
end
context 'with an ancestor' do
let!(:parent) do
create(:project, members: { current_user => role }).tap do |parent|
project.parent = parent
project.save
end
end
let!(:grandparent) do
create(:project, members: { current_user => role }).tap do |grandparent|
parent.parent = grandparent
parent.save
end
end
let(:select) { { 'ancestors' => {} } }
it 'renders as expected' do
expect(json)
.to be_json_eql(
{
_links: {
ancestors: [
{
href: api_v3_paths.project(grandparent.id),
title: grandparent.name
},
{
href: api_v3_paths.project(parent.id),
title: parent.name
}
]
}
}.to_json
)
end
end
context 'with an ancestor the user does not have permission to see' do
let!(:parent) do
create(:project).tap do |parent|
project.parent = parent
project.save
end
end
let!(:grandparent) do
create(:project, members: { current_user => role }).tap do |grandparent|
parent.parent = grandparent
parent.save
end
end
let(:select) { { 'ancestors' => {} } }
it 'renders as expected' do
expect(json)
.to be_json_eql(
{
_links: {
ancestors: [
{
href: api_v3_paths.project(grandparent.id),
title: grandparent.name
},
{
href: API::V3::URN_UNDISCLOSED,
title: I18n.t(:'api_v3.undisclosed.ancestor')
}
]
}
}.to_json
)
end
end
context 'with an archived ancestor but with the user being admin' do
let!(:parent) do
create(:project, active: false).tap do |parent|
project.parent = parent
project.save
end
end
let!(:grandparent) do
create(:project).tap do |grandparent|
parent.parent = grandparent
parent.save
end
end
let(:select) { { 'ancestors' => {} } }
current_user do
create(:admin)
end
it 'renders as expected' do
expect(json)
.to be_json_eql(
{
_links: {
ancestors: [
{
href: api_v3_paths.project(grandparent.id),
title: grandparent.name
},
{
href: api_v3_paths.project(parent.id),
title: parent.name
}
]
}
}.to_json
)
end
end
end

@ -28,45 +28,20 @@
require 'spec_helper'
describe ::API::V3::Users::PaginatedUserCollectionRepresenter do
let(:self_base_link) { '/api/v3/users' }
let(:collection_inner_type) { 'User' }
let(:total) { 3 }
let(:page) { 1 }
let(:page_size) { 2 }
let(:actual_count) { 3 }
describe ::API::V3::Users::UnpaginatedUserCollectionRepresenter do
let(:users) do
users = build_stubbed_list(:user,
actual_count)
allow(users)
.to receive(:per_page)
.with(page_size)
.and_return(users)
allow(users)
.to receive(:page)
.with(page)
.and_return(users)
allow(users)
.to receive(:count)
.and_return(total)
users
build_stubbed_list(:user,
3)
end
let(:representer) do
described_class.new(users,
self_link: '/api/v3/users',
per_page: page_size,
page: page,
self_link: '/api/v3/work_package/1/watchers',
current_user: users.first)
end
context 'generation' do
subject(:collection) { representer.to_json }
it_behaves_like 'offset-paginated APIv3 collection'
it_behaves_like 'unpaginated APIv3 collection', 3, 'work_package/1/watchers', 'User'
end
end

@ -29,19 +29,44 @@
require 'spec_helper'
describe ::API::V3::Users::UserCollectionRepresenter do
let(:self_base_link) { '/api/v3/users' }
let(:collection_inner_type) { 'User' }
let(:total) { 3 }
let(:page) { 1 }
let(:page_size) { 2 }
let(:actual_count) { 3 }
let(:users) do
build_stubbed_list(:user,
3)
users = build_stubbed_list(:user,
actual_count)
allow(users)
.to receive(:per_page)
.with(page_size)
.and_return(users)
allow(users)
.to receive(:page)
.with(page)
.and_return(users)
allow(users)
.to receive(:count)
.and_return(total)
users
end
let(:representer) do
described_class.new(users,
self_link: '/api/v3/work_package/1/watchers',
self_link: '/api/v3/users',
per_page: page_size,
page: page,
current_user: users.first)
end
context 'generation' do
subject(:collection) { representer.to_json }
it_behaves_like 'unpaginated APIv3 collection', 3, 'work_package/1/watchers', 'User'
it_behaves_like 'offset-paginated APIv3 collection'
end
end

@ -0,0 +1,189 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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'
describe ::API::V3::Users::UserSqlRepresenter, 'rendering' do
include ::API::V3::Utilities::PathHelper
subject(:json) do
::API::V3::Utilities::SqlRepresenterWalker
.new(scope,
current_user: current_user,
url_query: { select: select })
.walk(described_class)
.to_json
end
let(:scope) do
User
.where(id: rendered_user.id)
end
let(:rendered_user) { current_user }
let(:select) { { '*' => {} } }
current_user do
create(:user)
end
context 'when rendering all supported properties' do
let(:expected) do
{
_type: "User",
id: current_user.id,
name: current_user.name,
firstname: current_user.firstname,
lastname: current_user.lastname,
_links: {
self: {
href: api_v3_paths.user(current_user.id),
title: current_user.name
}
}
}
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected.to_json)
end
end
describe 'name property' do
shared_examples_for 'name property depending on user format setting' do
let(:select) { { 'name' => {} } }
let(:expected) do
{
name: current_user.name
}
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected.to_json)
end
end
context 'when user_format is set to firstname', with_settings: { user_format: :firstname } do
it_behaves_like 'name property depending on user format setting'
end
context 'when user_format is set to lastname_firstname', with_settings: { user_format: :lastname_firstname } do
it_behaves_like 'name property depending on user format setting'
end
context 'when user_format is set to lastname_coma_firstname', with_settings: { user_format: :lastname_coma_firstname } do
it_behaves_like 'name property depending on user format setting'
end
context 'when user_format is set to lastname_n_firstname', with_settings: { user_format: :lastname_n_firstname } do
it_behaves_like 'name property depending on user format setting'
end
context 'when user_format is set to username', with_settings: { user_format: :username } do
it_behaves_like 'name property depending on user format setting'
end
end
describe 'firstname property' do
let(:select) { { 'firstname' => {} } }
context 'when the user is the current user' do
it 'renders the firstname' do
expect(json)
.to be_json_eql(
{
firstname: rendered_user.firstname
}.to_json
)
end
end
context 'when the user is a user not having manage_user permission' do
let(:rendered_user) { create(:user) }
it 'hides the firstname' do
expect(json)
.to be_json_eql({}.to_json)
end
end
context 'when the user is a user having manage_user permission' do
let(:current_user) { create(:user, global_permissions: [:manage_user]) }
let(:rendered_user) { create(:user) }
it 'renders the firstname' do
expect(json)
.to be_json_eql(
{
firstname: rendered_user.firstname
}.to_json
)
end
end
end
describe 'lastname property' do
let(:select) { { 'lastname' => {} } }
context 'when the user is the current user' do
it 'renders the lastname' do
expect(json)
.to be_json_eql(
{
lastname: rendered_user.lastname
}.to_json
)
end
end
context 'when the user is a user not having manage_user permission' do
let(:rendered_user) { create(:user) }
it 'hides the lastname' do
expect(json)
.to be_json_eql({}.to_json)
end
end
context 'when the user is a user having manage_user permission' do
let(:current_user) { create(:user, global_permissions: [:manage_user]) }
let(:rendered_user) { create(:user) }
it 'renders the lastname' do
expect(json)
.to be_json_eql(
{
lastname: rendered_user.lastname
}.to_json
)
end
end
end
end

@ -0,0 +1,74 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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'
describe ::API::V3::WorkPackages::WorkPackageSqlRepresenter, 'rendering' do
include ::API::V3::Utilities::PathHelper
subject(:json) do
::API::V3::Utilities::SqlRepresenterWalker
.new(scope,
current_user: current_user,
url_query: { select: select })
.walk(described_class)
.to_json
end
let(:scope) do
WorkPackage
.where(id: rendered_work_package.id)
end
let(:rendered_work_package) { create(:work_package) }
let(:select) { { '*' => {} } }
current_user do
create(:user)
end
context 'when rendering all supported properties' do
let(:expected) do
{
_type: "WorkPackage",
id: rendered_work_package.id,
subject: rendered_work_package.subject,
_links: {
self: {
href: api_v3_paths.work_package(rendered_work_package.id),
title: rendered_work_package.subject
}
}
}
end
it 'renders as expected' do
expect(json)
.to be_json_eql(expected.to_json)
end
end
end

@ -131,7 +131,14 @@ describe 'API v3 capabilities resource', type: :request, content_type: :json do
'values' => [other_user.id.to_s]
} }]
end
let(:path) { "#{api_v3_paths.path_for(:capabilities, filters: filters, sort_by: [%i(id asc)])}&pageSize=2&offset=2" }
let(:path) do
api_v3_paths.path_for(:capabilities,
filters: filters,
sort_by: [%i(id asc)],
select: '*,elements/*',
page_size: 2,
offset: 2)
end
it 'returns a slice of the visible memberships' do
expect(subject.body)
@ -244,7 +251,7 @@ describe 'API v3 capabilities resource', type: :request, content_type: :json do
end
end
context 'invalid filter' do
context 'with an invalid filter' do
let(:filters) do
[{ 'bogus' => {
'operator' => '=',
@ -320,6 +327,39 @@ describe 'API v3 capabilities resource', type: :request, content_type: :json do
end
end
context 'when signaling to only include a subset of properties' do
let(:current_user_permissions) { %i[manage_members] }
let(:path) { api_v3_paths.path_for(:capabilities, filters: filters, sort_by: [%i(id asc)], select: 'elements/id') }
let(:filters) do
[{ 'principalId' => {
'operator' => '=',
'values' => [current_user.id.to_s]
} }]
end
it 'contains only the filtered capabilities in the response' do
expected = {
_embedded: {
elements: [
{
id: "memberships/create/p#{project.id}-#{current_user.id}"
},
{
id: "memberships/destroy/p#{project.id}-#{current_user.id}"
},
{
id: "memberships/update/p#{project.id}-#{current_user.id}"
}
]
}
}
expect(subject.body)
.to be_json_eql(expected.to_json)
end
end
context 'without permissions' do
current_user do
create(:user)
@ -382,7 +422,7 @@ describe 'API v3 capabilities resource', type: :request, content_type: :json do
it 'returns 200 OK' do
expect(subject.status)
.to eql(200)
.to be(200)
end
it 'returns the capability' do

@ -406,32 +406,53 @@ describe 'API v3 Group resource', type: :request, content_type: :json do
get get_path
end
context 'having the necessary permission' do
it 'responds with 200 OK' do
expect(subject.status)
.to eq(200)
end
it 'responds with a collection of groups' do
expect(subject.body)
.to be_json_eql('Collection'.to_json)
.at_path('_type')
it_behaves_like 'API V3 collection response', 2, 2, 'Group' do
let(:elements) { [other_group, group] }
end
expect(subject.body)
.to be_json_eql('2')
.at_path('total')
context 'when signaling' do
let(:get_path) { api_v3_paths.path_for :groups, select: 'total,count,elements/*' }
expect(subject.body)
.to be_json_eql(other_group.id.to_json)
.at_path('_embedded/elements/0/id')
let(:expected) do
{
total: 2,
count: 2,
_embedded: {
elements: [
{
_type: 'Group',
id: other_group.id,
name: other_group.name,
_links: {
self: {
href: api_v3_paths.group(other_group.id),
title: other_group.name
}
}
},
{
_type: 'Group',
id: group.id,
name: group.name,
_links: {
self: {
href: api_v3_paths.group(group.id),
title: group.name
}
}
}
]
}
}
end
expect(subject.body)
.to be_json_eql(group.id.to_json)
.at_path('_embedded/elements/1/id')
it 'is the reduced set of properties of the embedded elements' do
expect(last_response.body)
.to be_json_eql(expected.to_json)
end
end
context 'not having the necessary permission' do
context 'when not having the necessary permission' do
let(:permissions) { [] }
it_behaves_like 'unauthorized access'

@ -32,6 +32,7 @@ require 'rack/test'
describe ::API::V3::PlaceholderUsers::PlaceholderUsersAPI,
'index',
content_type: :json,
type: :request do
include API::V3::Utilities::PathHelper
@ -40,7 +41,6 @@ describe ::API::V3::PlaceholderUsers::PlaceholderUsersAPI,
shared_let(:placeholder2) { create :placeholder_user, name: 'bar' }
let(:send_request) do
header "Content-Type", "application/json"
get api_v3_paths.placeholder_users
end
@ -52,26 +52,71 @@ describe ::API::V3::PlaceholderUsers::PlaceholderUsersAPI,
send_request
end
describe 'admin user' do
context 'for an admin user' do
let(:user) { build(:admin) }
it_behaves_like 'API V3 collection response', 2, 2, 'PlaceholderUser'
end
describe 'user with manage_placeholder_user permission' do
context 'when signaling for the desired properties' do
let(:user) { build(:admin) }
let(:send_request) do
get api_v3_paths.path_for :placeholder_users, select: 'total,count,elements/*'
end
let(:expected) do
{
total: 2,
count: 2,
_embedded: {
elements: [
{
_type: 'PlaceholderUser',
id: placeholder2.id,
name: placeholder2.name,
_links: {
self: {
href: api_v3_paths.placeholder_user(placeholder2.id),
title: placeholder2.name
}
}
},
{
_type: 'PlaceholderUser',
id: placeholder1.id,
name: placeholder1.name,
_links: {
self: {
href: api_v3_paths.placeholder_user(placeholder1.id),
title: placeholder1.name
}
}
}
]
}
}
end
it 'is the reduced set of properties of the embedded elements' do
expect(last_response.body)
.to be_json_eql(expected.to_json)
end
end
context 'for a user with manage_placeholder_user permission' do
let(:user) { create(:user, global_permission: %i[manage_placeholder_user]) }
it_behaves_like 'API V3 collection response', 2, 2, 'PlaceholderUser'
end
describe 'user with manage_members permission' do
context 'for a user with manage_members permission' do
let(:project) { create(:project) }
let(:user) { create(:user, member_in_project: project, member_with_permissions: %i[manage_members]) }
it_behaves_like 'API V3 collection response', 2, 2, 'PlaceholderUser'
end
describe 'unauthorized user' do
context 'for an unauthorized user' do
let(:user) { build(:user) }
it_behaves_like 'API V3 collection response', 0, 0, 'PlaceholderUser'

@ -37,10 +37,11 @@ describe 'API v3 Principals resource', type: :request do
subject(:response) { last_response }
let(:path) do
api_v3_paths.path_for :principals, filters: filter, sort_by: order
api_v3_paths.path_for :principals, filters: filter, sort_by: order, select: select
end
let(:order) { { name: :desc } }
let(:filter) { nil }
let(:select) { nil }
let(:project) { create(:project) }
let(:other_project) { create(:project) }
let(:non_member_project) { create(:project) }
@ -166,5 +167,71 @@ describe 'API v3 Principals resource', type: :request do
let(:elements) { [current_user] }
end
end
context 'when signaling' do
let(:select) { 'total,count,elements/*' }
let(:expected) do
{
total: 4,
count: 4,
_embedded: {
elements: [
{
_type: 'PlaceholderUser',
id: placeholder_user.id,
name: placeholder_user.name,
_links: {
self: {
href: api_v3_paths.placeholder_user(placeholder_user.id),
title: placeholder_user.name
}
}
},
{
_type: 'Group',
id: group.id,
name: group.name,
_links: {
self: {
href: api_v3_paths.group(group.id),
title: group.name
}
}
},
{
_type: "User",
id: other_user.id,
name: other_user.name,
_links: {
self: {
href: api_v3_paths.user(other_user.id),
title: other_user.name
}
}
},
{
_type: "User",
id: user.id,
name: user.name,
firstname: user.firstname,
lastname: user.lastname,
_links: {
self: {
href: api_v3_paths.user(user.id),
title: user.name
}
}
}
]
}
}
end
it 'is the reduced set of properties of the embedded elements' do
expect(last_response.body)
.to be_json_eql(expected.to_json)
end
end
end
end

@ -42,6 +42,12 @@ describe 'API v3 Project resource index', type: :request, content_type: :json do
let(:other_project) do
create(:project, public: false)
end
let(:parent_project) do
create(:project, public: false, members: { current_user => role }).tap do |parent|
project.parent = parent
project.save
end
end
let(:role) { create(:role) }
let(:filters) { [] }
let(:get_path) do
@ -58,11 +64,6 @@ describe 'API v3 Project resource index', type: :request, content_type: :json do
get get_path
end
it 'succeeds' do
expect(response.status)
.to be(200)
end
it_behaves_like 'API V3 collection response', 1, 1, 'Project'
context 'with a pageSize and offset' do
@ -88,14 +89,6 @@ describe 'API v3 Project resource index', type: :request, content_type: :json do
context 'when filtering for project by ancestor' do
let(:projects) { [project, other_project, parent_project] }
let(:parent_project) do
parent_project = create(:project, public: false, members: { current_user => role })
project.update_attribute(:parent_id, parent_project.id)
parent_project
end
let(:filters) do
[{ ancestor: { operator: '=', values: [parent_project.id.to_s] } }]
end
@ -231,4 +224,43 @@ describe 'API v3 Project resource index', type: :request, content_type: :json do
end
end
end
context 'when signaling the properties to include' do
let(:projects) { [project, parent_project] }
let(:select) { 'elements/id,elements/name,elements/ancestors,total' }
let(:get_path) do
api_v3_paths.path_for :projects, select: select
end
let(:expected) do
{
total: 2,
_embedded: {
elements: [
{
id: parent_project.id,
name: parent_project.name,
_links: {
ancestors: []
}
},
{
id: project.id,
name: project.name,
_links: {
ancestors: [
href: api_v3_paths.project(parent_project.id),
title: parent_project.name
]
}
}
]
}
}
end
it 'is the reduced set of properties of the embedded elements' do
expect(last_response.body)
.to be_json_eql(expected.to_json)
end
end
end

@ -58,7 +58,7 @@ describe 'API v3 User resource',
end
shared_examples 'flow with permitted user' do
it 'should respond with 200' do
it 'responds with 200' do
expect(subject.status).to eq(200)
end
@ -78,12 +78,13 @@ describe 'API v3 User resource',
it 'has the users index path for link self href' do
expect(subject.body)
.to be_json_eql((api_v3_paths.users + '?offset=1&pageSize=30').to_json)
.to be_json_eql("#{api_v3_paths.users}?filters=%5B%5D" \
"\u0026offset=1\u0026pageSize=20\u0026sortBy=%5B%5B%22id%22%2C%22asc%22%5D%5D".to_json)
.at_path('_links/self/href')
end
context 'if pageSize = 1 and offset = 2' do
let(:get_path) { api_v3_paths.users + '?pageSize=1&offset=2' }
let(:get_path) { api_v3_paths.path_for(:users, page_size: 1, offset: 2) }
it 'contains the current user in the response' do
expect(subject.body)
@ -92,7 +93,7 @@ describe 'API v3 User resource',
end
end
context 'on filtering for name' do
context 'when filtering by name' do
let(:get_path) do
filter = [{ 'name' => {
'operator' => '~',
@ -115,13 +116,13 @@ describe 'API v3 User resource',
end
end
context 'on sorting' do
context 'when sorting' do
let(:users_by_name_order) do
User.human.ordered_by_name(desc: true)
end
let(:get_path) do
sort = [['name', 'desc']]
sort = [%w[name desc]]
"#{api_v3_paths.users}?#{{ sortBy: sort.to_json }.to_query}"
end
@ -139,7 +140,7 @@ describe 'API v3 User resource',
end
end
context 'on an invalid filter' do
context 'with an invalid filter' do
let(:get_path) do
filter = [{ 'name' => {
'operator' => 'a',
@ -150,30 +151,57 @@ describe 'API v3 User resource',
end
it 'returns an error' do
expect(subject.status).to eql(400)
expect(subject.status).to be(400)
end
end
context 'when signaling desired properties' do
let(:get_path) do
api_v3_paths.path_for :users,
sort_by: [%w[name desc]],
page_size: 1,
select: 'total,elements/name'
end
let(:expected) do
{
total: 2,
_embedded: {
elements: [
{
name: current_user.name
}
]
}
}
end
it 'returns an error' do
expect(subject.body)
.to be_json_eql(expected.to_json)
end
end
end
context 'admin' do
context 'for an admin' do
let(:current_user) { admin }
it_behaves_like 'flow with permitted user'
end
context 'user with global manage_user permission' do
context 'for a user with global manage_user permission' do
let(:current_user) { user_with_global_manage_user }
it_behaves_like 'flow with permitted user'
end
context 'locked admin' do
context 'for a locked admin' do
let(:current_user) { locked_admin }
it_behaves_like 'unauthorized access'
end
context 'other user' do
context 'for another user' do
it_behaves_like 'unauthorized access'
end
end

Loading…
Cancel
Save