Merge pull request #10703 from opf/implementation/42552-api-add-live-file-link-collections-to-storages

[#42552] API: Add live file link collections to storages
pull/10742/head
Eric Schubert 2 years ago committed by GitHub
commit f7a88f7cd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      docs/api/apiv3/components/schemas/file_link_read_model.yml
  2. 19
      docs/api/apiv3/components/schemas/storage_model.yml
  3. 3
      docs/api/apiv3/paths/storage.yml
  4. 15
      docs/api/apiv3/paths/work_package_file_links.yml
  5. 12
      lib/api/decorators/unpaginated_collection.rb
  6. 11
      lib/api/v3/utilities/endpoints/index.rb
  7. 49
      modules/storages/app/models/queries/storages/file_links/filter/storage_filter.rb
  8. 11
      modules/storages/lib/api/v3/file_links/file_link_representer.rb
  9. 15
      modules/storages/lib/api/v3/storages/storage_representer.rb
  10. 4
      modules/storages/lib/open_project/storages/engine.rb
  11. 8
      modules/storages/spec/lib/api/v3/file_links/file_link_representer_rendering_spec.rb
  12. 15
      modules/storages/spec/lib/api/v3/storages/storages_representer_rendering_spec.rb
  13. 42
      modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb

@ -38,8 +38,10 @@ properties:
type: object
required:
- self
- storage
- container
- creator
- permission
- originOpen
- staticOriginOpen
- originOpenLocation
@ -81,6 +83,16 @@ properties:
- description: |-
The uri to delete the file link.
**Resource**: N/A
permission:
allOf:
- $ref: './link.yml'
- description: |-
The urn of the user specific file link permission on its storage. Can be one of:
- urn:openproject-org:api:v3:file-links:permission:View
- urn:openproject-org:api:v3:file-links:permission:NotAllowed
**Resource**: N/A
originOpen:
allOf:
@ -175,6 +187,9 @@ example:
title: Obi-Wan Kenobi
delete:
href: /api/v3/work_package/17/file_links/1337
permission:
href: urn:openproject-org:api:v3:file-links:permission:View
title: View
originOpen:
href: https://nextcloud.deathstar.rocks/index.php/f/5503?openfile=1
staticOriginOpen:

@ -5,7 +5,7 @@ required:
- id
- _type
- name
- storageType
- _links
properties:
id:
type: integer
@ -30,7 +30,8 @@ properties:
required:
- self
- type
- href
- origin
- connectionState
properties:
self:
allOf:
@ -52,6 +53,17 @@ properties:
- description: |-
Web uri of the storage instance
**Resource**: N/A
connectionState:
allOf:
- $ref: "./link.yml"
- description: |-
The urn of the storage connection state. Can be one of:
- urn:openproject-org:api:v3:storages:connection:Connected
- urn:openproject-org:api:v3:storages:connection:FailedAuthentication
- urn:openproject-org:api:v3:storages:connection:Error
**Resource**: N/A
example:
id: 1337
@ -68,3 +80,6 @@ example:
title: Nextcloud
origin:
href: https://nextcloud.deathstar.rocks/
connectionState:
href: urn:openproject-org:api:v3:storages:connection:FailedAuthentication
title: Failed Authentication

@ -6,7 +6,8 @@ get:
tags:
- File links
description: |-
Gets a storage resource.
Gets a storage resource. As a side effect, a live connection to the storages origin is established to retrieve
connection state data.
parameters:
- name: id
description: Storage id

@ -118,6 +118,9 @@ get:
- File links
description: |-
Gets all file links of a work package.
As a side effect, for every file link a request is sent to the storage's origin to fetch live data and patch
the file link's data before returning, as well as retrieving permissions of the user on this origin file.
parameters:
- name: id
description: Work package id
@ -126,6 +129,18 @@ get:
schema:
type: integer
example: 1337
- name: filters
in: query
description: |-
JSON specifying filter conditions.
Accepts the same format as returned by the [queries](https://www.openproject.org/docs/api/endpoints/queries/)
endpoint. The following filters are supported:
- storage
required: false
example: '[{"storage":{"operator":"=","values":["42"]}}]'
schema:
type: string
responses:
'200':
description: OK

@ -29,8 +29,8 @@
module API
module Decorators
class UnpaginatedCollection < ::API::Decorators::Collection
def initialize(models, self_link:, current_user:)
super(models, model_count(models), self_link: self_link, current_user: current_user)
def initialize(models, self_link:, current_user:, query: {})
super(models, model_count(models), self_link: make_self_link(self_link, query), current_user:)
end
def model_count(models)
@ -43,6 +43,14 @@ module API
models
end.count
end
private
def make_self_link(self_link_base, query)
return self_link_base if query.empty?
"#{self_link_base}?#{query.to_query}"
end
end
end
end

@ -36,7 +36,7 @@ module API
scope: nil,
render_representer: nil,
self_path: api_name.underscore.pluralize)
super(model: model, api_name: api_name, scope: scope, render_representer: render_representer)
super(model:, api_name:, scope:, render_representer:)
self.self_path = self_path
end
@ -82,7 +82,7 @@ module API
if paginated_representer?
render_paginated_success(results, query, params, self_path)
else
render_unpaginated_success(results, self_path)
render_unpaginated_success(results, query, self_path)
end
end
@ -99,10 +99,13 @@ module API
current_user: User.current)
end
def render_unpaginated_success(results, self_path)
def render_unpaginated_success(results, query, self_path)
unpaginated_params = calculate_default_params(query).except(:offset, :pageSize)
render_representer
.new(results,
self_link: self_path,
query: unpaginated_params,
current_user: User.current)
end
@ -117,7 +120,7 @@ module API
return unless query.group_by
query.group_values.map do |group, count|
::API::Decorators::AggregationGroup.new(group, count, query: query, current_user: User.current)
::API::Decorators::AggregationGroup.new(group, count, query:, current_user: User.current)
end
end

@ -0,0 +1,49 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-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 Queries::Storages::FileLinks::Filter
class StorageFilter < ::Queries::Filters::Base
self.model = ::Storages::FileLink
def human_name
::Storages::FileLink.human_attribute_name(name)
end
def type
:list
end
def allowed_values
::Storages::Storage.pluck(:id).map { |id| [id, id.to_s] }
end
def where
operator_strategy.sql_for_field(values, ::Storages::FileLink.table_name, 'storage_id')
end
end
end

@ -29,6 +29,9 @@
module API
module V3
module FileLinks
URN_PERMISSION_VIEW = "#{::API::V3::URN_PREFIX}file-links:permission:View".freeze
URN_PERMISSION_NOT_ALLOWED = "#{::API::V3::URN_PREFIX}file-links:permission:NotAllowed".freeze
class FileLinkRepresenter < ::API::Decorators::Single
include API::Decorators::LinkedResource
include API::Decorators::DateProperty
@ -66,6 +69,14 @@ module API
}
end
link :permission do
# TODO: replace with service to check real permission state
{
href: URN_PERMISSION_VIEW,
title: 'View'
}
end
link :originOpen do
{
href: storage_url_open(represented)

@ -33,6 +33,11 @@
module API
module V3
module Storages
URN_TYPE_NEXTCLOUD = "#{::API::V3::URN_PREFIX}storages:Nextcloud".freeze
URN_CONNECTION_CONNECTED = "#{::API::V3::URN_PREFIX}storages:connection:Connected".freeze
URN_CONNECTION_AUTH_FAILED = "#{::API::V3::URN_PREFIX}storages:connection:FailedAuthentication".freeze
URN_CONNECTION_ERROR = "#{::API::V3::URN_PREFIX}storages:connection:Error".freeze
class StorageRepresenter < ::API::Decorators::Single
# LinkedResource module defines helper methods to describe attributes
include API::Decorators::LinkedResource
@ -51,7 +56,7 @@ module API
link :type do
{
href: "#{::API::V3::URN_PREFIX}storages:nextcloud",
href: URN_TYPE_NEXTCLOUD,
title: 'Nextcloud'
}
end
@ -62,6 +67,14 @@ module API
}
end
link :connectionState do
# TODO: replace with service to check real connection state
{
href: URN_CONNECTION_CONNECTED,
title: 'Connected'
}
end
def _type
'Storage'
end

@ -99,6 +99,10 @@ module OpenProject::Storages
filter filter
exclude filter
end
::Queries::Register.register(::Queries::Storages::FileLinks::FileLinkQuery) do
filter ::Queries::Storages::FileLinks::Filter::StorageFilter
end
end
end

@ -83,6 +83,14 @@ describe ::API::V3::FileLinks::FileLinkRepresenter, 'rendering' do
end
end
describe 'permission' do
it_behaves_like 'has a titled link' do
let(:link) { 'permission' }
let(:href) { 'urn:openproject-org:api:v3:file-links:permission:View' }
let(:title) { 'View' }
end
end
describe 'originOpen' do
it_behaves_like 'has an untitled link' do
let(:link) { 'originOpen' }

@ -43,6 +43,21 @@ describe ::API::V3::Storages::StorageRepresenter, 'rendering' do
let(:title) { storage.name }
end
end
describe 'origin' do
it_behaves_like 'has an untitled link' do
let(:link) { 'origin' }
let(:href) { storage.host }
end
end
describe 'connectionState' do
it_behaves_like 'has a titled link' do
let(:link) { 'connectionState' }
let(:href) { 'urn:openproject-org:api:v3:storages:connection:Connected' }
let(:title) { 'Connected' }
end
end
end
describe 'properties' do

@ -39,19 +39,20 @@ describe 'API v3 file links resource', :enable_storages, type: :request do
create(:user, member_in_project: project, member_with_permissions: permissions)
end
let(:work_package) { create(:work_package, author: current_user, project: project) }
let(:another_work_package) { create(:work_package, author: current_user, project: project) }
let(:work_package) { create(:work_package, author: current_user, project:) }
let(:another_work_package) { create(:work_package, author: current_user, project:) }
let(:storage) { create(:storage, creator: current_user) }
let(:another_storage) { create(:storage, creator: current_user) }
let!(:project_storage) { create(:project_storage, project: project, storage: storage) }
let!(:project_storage) { create(:project_storage, project:, storage:) }
let!(:another_project_storage) { nil }
let(:file_link) do
create(:file_link, creator: current_user, container: work_package, storage: storage)
create(:file_link, creator: current_user, container: work_package, storage:)
end
let(:file_link_of_other_work_package) do
create(:file_link, creator: current_user, container: another_work_package, storage: storage)
create(:file_link, creator: current_user, container: another_work_package, storage:)
end
# If a storage mapping between a project and a storage is removed, the file link still persist. This can occur on
# moving a work package to another project, too, if target project does not yet have the storage mapping.
@ -95,9 +96,36 @@ describe 'API v3 file links resource', :enable_storages, type: :request do
end
end
context 'when storages module is inactive', :disable_storages do
context 'if storages feature is inactive', :disable_storages do
it_behaves_like 'not found'
end
describe 'with filter by storage' do
let!(:another_project_storage) { create(:project_storage, project:, storage: another_storage) }
let(:path) { "#{api_v3_paths.file_links(work_package.id)}?filters=#{CGI.escape(filters.to_json)}" }
let(:filters) do
[
{ storage: { operator: '=', values: [storage_id] } }
]
end
context 'if filtered by one storage' do
let(:storage_id) { storage.id }
it_behaves_like 'API V3 collection response', 1, 1, 'FileLink', 'Collection' do
let(:elements) { [file_link] }
end
end
context 'if filtered by another storage' do
let(:storage_id) { another_storage.id }
it_behaves_like 'API V3 collection response', 1, 1, 'FileLink', 'Collection' do
# has the now linked storage's file links
let(:elements) { [file_link_of_unlinked_storage] }
end
end
end
end
describe 'POST /api/v3/work_packages/:work_package_id/file_links' do
@ -188,7 +216,7 @@ describe 'API v3 file links resource', :enable_storages, type: :request do
origin_name: 'original name',
creator: current_user,
container: work_package,
storage: storage)
storage:)
end
let(:already_existing_file_link_payload) do
build(:file_link_element,

Loading…
Cancel
Save