Merge pull request #10676 from opf/implementation/42362-api-add-storages-links-to-project-resource

[#42362] API: Add storages links to project resource
implementation/42379-add-endpoint-to-update-cache-with-live-data
Andreas Pfohl 3 years ago committed by GitHub
commit ebf654c5a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 241
      docs/api/apiv3/components/schemas/project_model.yml
  2. 10
      docs/api/apiv3/components/schemas/work_packages_model.yml
  3. 63
      docs/api/apiv3/paths/work_packages.yml
  4. 12
      lib/api/v3/projects/project_representer.rb
  5. 122
      spec/lib/api/v3/projects/project_representer_rendering_spec.rb

@ -2,6 +2,10 @@
---
type: object
properties:
_type:
type: string
enum:
- Project
id:
type: integer
description: Projects' id
@ -15,165 +19,162 @@ properties:
description: Indicates whether the project is currently active or already archived
statusExplanation:
allOf:
- "$ref": "./formattable.yml"
- description: A text detailing and explaining why the project has the reported
status
- $ref: './formattable.yml'
- description: A text detailing and explaining why the project has the reported status
public:
type: boolean
description: Indicates whether the project is accessible for everybody
description:
allOf:
- "$ref": "./formattable.yml"
- {}
$ref: './formattable.yml'
createdAt:
type: string
format: date-time
description: Time of creation
readOnly: true
updatedAt:
type: string
format: date-time
description: Time of the most recent change to the project
readOnly: true
_links:
type: object
required:
- self
- categories
- types
- versions
- memberships
- workPackages
- self
- categories
- types
- versions
- memberships
- workPackages
properties:
update:
allOf:
- "$ref": "./link.yml"
- description: |-
Form endpoint that aids in updating this project
# Conditions
**Permission**: edit project
readOnly: true
- $ref: './link.yml'
- description: |-
Form endpoint that aids in updating this project
# Conditions
**Permission**: edit project
updateImmediately:
allOf:
- "$ref": "./link.yml"
- description: |-
Directly update this project
# Conditions
**Permission**: edit project
readOnly: true
- $ref: './link.yml'
- description: |-
Directly update this project
# Conditions
**Permission**: edit project
delete:
allOf:
- "$ref": "./link.yml"
- description: |-
Delete this project
# Conditions
**Permission**: admin
readOnly: true
- $ref: './link.yml'
- description: |-
Delete this project
# Conditions
**Permission**: admin
createWorkPackage:
allOf:
- "$ref": "./link.yml"
- description: |-
Form endpoint that aids in preparing and creating a work package
# Conditions
**Permission**: add work packages
readOnly: true
- $ref: './link.yml'
- description: |-
Form endpoint that aids in preparing and creating a work package
# Conditions
**Permission**: add work packages
createWorkPackageImmediately:
allOf:
- "$ref": "./link.yml"
- description: |-
Directly creates a work package in the project
# Conditions
**Permission**: add work packages
readOnly: true
- $ref: './link.yml'
- description: |-
Directly creates a work package in the project
# Conditions
**Permission**: add work packages
self:
allOf:
- "$ref": "./link.yml"
- description: |-
This project
**Resource**: Project
readOnly: true
- $ref: './link.yml'
- description: |-
This project
**Resource**: Project
categories:
allOf:
- "$ref": "./link.yml"
- description: |-
Categories available in this project
**Resource**: Collection
readOnly: true
- $ref: './link.yml'
- description: |-
Categories available in this project
**Resource**: Collection
types:
allOf:
- "$ref": "./link.yml"
- description: |-
Types available in this project
**Resource**: Collection
# Conditions
**Permission**: view work packages or manage types
readOnly: true
- $ref: './link.yml'
- description: |-
Types available in this project
**Resource**: Collection
# Conditions
**Permission**: view work packages or manage types
versions:
allOf:
- "$ref": "./link.yml"
- description: |-
Versions available in this project
**Resource**: Collection
# Conditions
**Permission**: view work packages or manage versions
readOnly: true
- $ref: './link.yml'
- description: |-
Versions available in this project
**Resource**: Collection
# Conditions
**Permission**: view work packages or manage versions
memberships:
allOf:
- "$ref": "./link.yml"
- description: |-
Memberships in the project
**Resource**: Collection
# Conditions
**Permission**: view members
readOnly: true
- $ref: './link.yml'
- description: |-
Memberships in the project
**Resource**: Collection
# Conditions
**Permission**: view members
workPackages:
allOf:
- "$ref": "./link.yml"
- description: |-
Work Packages of this project
**Resource**: Collection
readOnly: true
- $ref: './link.yml'
- description: |-
Work Packages of this project
**Resource**: Collection
parent:
allOf:
- "$ref": "./link.yml"
- description: |-
Parent project of the project
**Resource**: Project
# Conditions
**Permission** edit project
- $ref: './link.yml'
- description: |-
Parent project of the project
**Resource**: Project
# Conditions
**Permission** edit project
status:
allOf:
- "$ref": "./link.yml"
- description: |-
Denotes the status of the project, so whether the project is on track, at risk or is having trouble.
**Resource**: ProjectStatus
# Conditions
**Permission** edit project
- $ref: './link.yml'
- description: |-
Denotes the status of the project, so whether the project is on track, at risk or is having trouble.
**Resource**: ProjectStatus
# Conditions
**Permission** edit project
storages:
type: array
items:
allOf:
- $ref: './link.yml'
- description: |-
The link to a storage that is active for this project.
**Resource**: Storage
# Conditions
**Permission**: view_file_links

@ -1,7 +1,7 @@
# Schema: Work_PackagesModel
---
allOf:
- "$ref": "./collection_model.yml"
- $ref: './collection_model.yml'
- type: object
required:
- _links
@ -14,7 +14,7 @@ allOf:
properties:
self:
allOf:
- "$ref": "./link.yml"
- $ref: './link.yml'
- description: |-
The work package collection
@ -33,7 +33,7 @@ allOf:
example:
_links:
self:
href: "/api/v3/work_packages"
href: '/api/v3/work_packages'
total: 2
count: 2
_type: Collection
@ -43,11 +43,11 @@ example:
_type: WorkPackage
_links:
self:
href: "/api/v3/work_packages/1"
href: '/api/v3/work_packages/1'
id: 1
- _hint: Work package resource shortened for brevity
_type: WorkPackage
_links:
self:
href: "/api/v3/work_packages/2"
href: '/api/v3/work_packages/2'
id: 2

@ -1,6 +1,11 @@
# /api/v3/work_packages
---
get:
summary: List work packages
operationId: list_work_packages
tags:
- Work Packages
description: Returns a collection of work packages.
parameters:
- description: Page number inside the requested collection.
example: '25'
@ -123,69 +128,35 @@ get:
'200':
content:
application/hal+json:
examples:
response:
value:
_embedded:
elements:
- _links:
self:
href: "/api/v3/work_packages/1"
_type: WorkPackage
id: 1
subject: Skipped other properties for brevity
- _links:
self:
href: "/api/v3/work_packages/2"
_type: WorkPackage
id: 2
subject: Skipped other properties for brevity
_links:
self:
href: "/api/v3/work_packages"
_type: Collection
count: 2
total: 2
schema:
"$ref": "../components/schemas/work_packages_model.yml"
$ref: '../components/schemas/work_packages_model.yml'
description: OK
headers: { }
'400':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:InvalidQuery
message: Operator can't be blank.
$ref: '../components/schemas/error_response.yml'
example:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:InvalidQuery
message: Operator can't be blank.
description: |-
Returned if the client sends a query parameter, that the server knows, but does not understand.
E.g. by providing a syntactically incorrect `filters` parameter.
headers: { }
'403':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission
message: You are not allowed to see work packages.
$ref: '../components/schemas/error_response.yml'
example:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission
message: You are not allowed to see work packages.
description: |-
Returned if the client does not have sufficient permissions.
**Required permission:** view work packages (globally or in any project)
headers: { }
tags:
- Work Packages
description: ''
operationId: List_work_packages
summary: List work packages
post:
parameters:
- description: |-

@ -88,6 +88,18 @@ module API
{ href: api_v3_paths.work_packages_by_project(represented.id) }
end
links :storages,
cache_if: -> {
current_user_allowed_to(:view_file_links, context: represented)
} do
represented.storages.map do |storage|
{
href: api_v3_paths.storage(storage.id),
title: storage.name
}
end
end
link :categories do
{ href: api_v3_paths.categories_by_project(represented.id) }
end

@ -40,16 +40,16 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
status: status).tap do |p|
allow(p)
.to receive(:available_custom_fields)
.and_return([int_custom_field, version_custom_field])
.and_return([int_custom_field, version_custom_field])
allow(p)
.to receive(:"custom_field_#{int_custom_field.id}")
.and_return(int_custom_value.value)
.and_return(int_custom_value.value)
allow(p)
.to receive(:custom_value_for)
.with(version_custom_field)
.and_return(version_custom_value)
.with(version_custom_field)
.and_return(version_custom_value)
allow(p)
.to receive(:ancestors_from_root)
@ -91,10 +91,10 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
CustomValue.new(custom_field: version_custom_field,
value: version.id,
customized: nil).tap do |cv|
allow(cv)
.to receive(:typed_value)
.and_return(version)
end
allow(cv)
.to receive(:typed_value)
.and_return(version)
end
end
let(:permissions) { %i[add_work_packages view_members] }
@ -130,10 +130,8 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
let(:value) { project.description }
end
context 'statusExplanation' do
it_behaves_like 'formattable property', 'statusExplanation' do
let(:value) { status.explanation }
end
it_behaves_like 'formattable property', 'statusExplanation' do
let(:value) { status.explanation }
end
it_behaves_like 'has UTC ISO 8601 date and time' do
@ -146,25 +144,23 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
let(:json_path) { 'updatedAt' }
end
context 'int custom field' do
describe 'int custom field' do
context 'if the user is admin' do
before do
allow(user)
.to receive(:admin?)
.and_return(true)
.and_return(true)
end
it "has a property for the int custom field" do
is_expected
.to be_json_eql(int_custom_value.value.to_json)
.at_path("customField#{int_custom_field.id}")
expect(subject).to be_json_eql(int_custom_value.value.to_json)
.at_path("customField#{int_custom_field.id}")
end
end
context 'if the user is no admin' do
it "has no property for the int custom field" do
is_expected
.not_to have_json_path("customField#{int_custom_field.id}")
expect(subject).not_to have_json_path("customField#{int_custom_field.id}")
end
end
end
@ -176,25 +172,25 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
it 'links to self' do
expect(subject).to have_json_path('_links/self/href')
end
it 'has a title for link to self' do
expect(subject).to have_json_path('_links/self/title')
end
describe 'create work packages' do
context 'user allowed to create work packages' do
context 'if user is allowed to create work packages' do
it 'has the correct path for a create form' do
is_expected
.to be_json_eql(api_v3_paths.create_project_work_package_form(project.id).to_json)
.at_path('_links/createWorkPackage/href')
expect(subject).to be_json_eql(api_v3_paths.create_project_work_package_form(project.id).to_json)
.at_path('_links/createWorkPackage/href')
end
it 'has the correct path to create a work package' do
is_expected.to be_json_eql(api_v3_paths.work_packages_by_project(project.id).to_json)
.at_path('_links/createWorkPackageImmediately/href')
expect(subject).to be_json_eql(api_v3_paths.work_packages_by_project(project.id).to_json)
.at_path('_links/createWorkPackageImmediately/href')
end
end
context 'user not allowed to create work packages' do
context 'if user is not allowed to create work packages' do
let(:permissions) { [] }
it_behaves_like 'has no link' do
@ -249,7 +245,6 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
end
end
# rubocop:disable RSpec/MultipleMemoizedHelpers
describe 'ancestors' do
let(:link) { 'ancestors' }
let(:grandparent_project) do
@ -346,7 +341,6 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
it_behaves_like 'has an empty link collection'
end
end
# rubocop:enable RSpec/MultipleMemoizedHelpers
describe 'status' do
it_behaves_like 'has a titled link' do
@ -367,8 +361,8 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
describe 'categories' do
it 'has the correct link to its categories' do
is_expected.to be_json_eql(api_v3_paths.categories_by_project(project.id).to_json)
.at_path('_links/categories/href')
expect(subject).to be_json_eql(api_v3_paths.categories_by_project(project.id).to_json)
.at_path('_links/categories/href')
end
end
@ -405,13 +399,13 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
let(:permissions) { [:view_work_packages] }
it 'links to the types active in the project' do
is_expected.to be_json_eql(api_v3_paths.types_by_project(project.id).to_json)
.at_path('_links/types/href')
expect(subject).to be_json_eql(api_v3_paths.types_by_project(project.id).to_json)
.at_path('_links/types/href')
end
it 'links to the work packages in the project' do
is_expected.to be_json_eql(api_v3_paths.work_packages_by_project(project.id).to_json)
.at_path('_links/workPackages/href')
expect(subject).to be_json_eql(api_v3_paths.work_packages_by_project(project.id).to_json)
.at_path('_links/workPackages/href')
end
end
@ -419,8 +413,8 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
let(:permissions) { [:manage_types] }
it 'links to the types active in the project' do
is_expected.to be_json_eql(api_v3_paths.types_by_project(project.id).to_json)
.at_path('_links/types/href')
expect(subject).to be_json_eql(api_v3_paths.types_by_project(project.id).to_json)
.at_path('_links/types/href')
end
end
@ -428,11 +422,11 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
let(:permission) { [] }
it 'has no types link' do
is_expected.to_not have_json_path('_links/types/href')
expect(subject).not_to have_json_path('_links/types/href')
end
it 'has no work packages link' do
is_expected.to_not have_json_path('_links/workPackages/href')
expect(subject).not_to have_json_path('_links/workPackages/href')
end
end
end
@ -452,20 +446,48 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
end
end
describe 'storages' do
let(:storage) { build_stubbed(:storage) }
let(:permissions) { %i[view_file_links] }
before do
allow(project).to receive(:storages).and_return([storage])
end
it_behaves_like 'has a link collection' do
let(:link) { 'storages' }
let(:hrefs) do
[
{
href: api_v3_paths.storage(storage.id),
title: storage.name
}
]
end
end
context 'if user has no permission to view file links' do
let(:permissions) { [] }
it_behaves_like 'has no link' do
let(:link) { 'storages' }
end
end
end
describe 'link custom field' do
context 'if the user is admin and the field is invisible' do
before do
allow(user)
.to receive(:admin?)
.and_return(true)
.and_return(true)
version_custom_field.visible = false
end
it 'links custom fields' do
is_expected
.to be_json_eql(api_v3_paths.version(version.id).to_json)
.at_path("_links/customField#{version_custom_field.id}/href")
expect(subject).to be_json_eql(api_v3_paths.version(version.id).to_json)
.at_path("_links/customField#{version_custom_field.id}/href")
end
end
@ -475,16 +497,14 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
end
it "has no property for the int custom field" do
is_expected
.not_to have_json_path("links/customField#{version_custom_field.id}")
expect(subject).not_to have_json_path("links/customField#{version_custom_field.id}")
end
end
context 'if the user is no admin and the field is visible' do
it 'links custom fields' do
is_expected
.to be_json_eql(api_v3_paths.version(version.id).to_json)
.at_path("_links/customField#{version_custom_field.id}/href")
expect(subject).to be_json_eql(api_v3_paths.version(version.id).to_json)
.at_path("_links/customField#{version_custom_field.id}/href")
end
end
end
@ -532,7 +552,7 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
before do
allow(user)
.to receive(:admin?)
.and_return(true)
.and_return(true)
end
it_behaves_like 'has an untitled link' do
@ -590,7 +610,7 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
it 'is based on the representer\'s cache_key' do
allow(OpenProject::Cache)
.to receive(:fetch)
.and_call_original
.and_call_original
representer.to_json
@ -615,14 +635,14 @@ describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do
end
it 'changes when the project is updated' do
project.updated_at = Time.now + 20.seconds
project.updated_at = 20.seconds.from_now
expect(representer.json_cache_key)
.not_to eql former_cache_key
end
it 'changes when the project status is updated' do
project.status.updated_at = Time.now + 20.seconds
project.status.updated_at = 20.seconds.from_now
expect(representer.json_cache_key)
.not_to eql former_cache_key

Loading…
Cancel
Save