Merge pull request #2239 from opf/feature/16972_add_assignee_responsible_to_wp_form

16972 add assignee responsible to wp form
pull/2092/merge
Alex Coles 10 years ago
commit 6ead8dddcb
  1. 16
      config/initializers/grape.rb
  2. 32
      doc/apiv3-documentation.apib
  3. 4
      frontend/tests/integration/mocks/work-package.json
  4. 4
      frontend/tests/integration/mocks/work-packages/819.json
  5. 4
      frontend/tests/integration/mocks/work-packages/820.json
  6. 4
      frontend/tests/integration/mocks/work-packages/821.json
  7. 2
      lib/api/decorators/collection.rb
  8. 26
      lib/api/errors/form/invalid_resource_link.rb
  9. 19
      lib/api/v3/activities/activity_representer.rb
  10. 17
      lib/api/v3/attachments/attachment_representer.rb
  11. 4
      lib/api/v3/categories/categories_api.rb
  12. 2
      lib/api/v3/priorities/priorities_api.rb
  13. 48
      lib/api/v3/projects/available_assignees_api.rb
  14. 48
      lib/api/v3/projects/available_responsibles_api.rb
  15. 8
      lib/api/v3/projects/project_representer.rb
  16. 3
      lib/api/v3/projects/projects_api.rb
  17. 7
      lib/api/v3/queries/query_representer.rb
  18. 12
      lib/api/v3/root_representer.rb
  19. 4
      lib/api/v3/statuses/status_representer.rb
  20. 2
      lib/api/v3/statuses/statuses_api.rb
  21. 9
      lib/api/v3/users/user_representer.rb
  22. 162
      lib/api/v3/utilities/path_helper.rb
  23. 2
      lib/api/v3/versions/versions_api.rb
  24. 34
      lib/api/v3/work_packages/form/form_representer.rb
  25. 57
      lib/api/v3/work_packages/form/work_package_attribute_links_representer.rb
  26. 26
      lib/api/v3/work_packages/form/work_package_payload_representer.rb
  27. 60
      lib/api/v3/work_packages/form/work_package_schema_representer.rb
  28. 10
      lib/api/v3/work_packages/relation_representer.rb
  29. 42
      lib/api/v3/work_packages/work_package_contract.rb
  30. 40
      lib/api/v3/work_packages/work_package_representer.rb
  31. 45
      lib/api/v3/work_packages/work_packages_api.rb
  32. 2
      spec/lib/api/v3/categories/category_collection_representer_spec.rb
  33. 2
      spec/lib/api/v3/priorities/priority_collection_representer_spec.rb
  34. 6
      spec/lib/api/v3/root_representer_spec.rb
  35. 2
      spec/lib/api/v3/statuses/status_collection_representer_spec.rb
  36. 2
      spec/lib/api/v3/users/user_collection_representer_spec.rb
  37. 255
      spec/lib/api/v3/utilities/path_helper_spec.rb
  38. 2
      spec/lib/api/v3/versions/version_collection_representer_spec.rb
  39. 19
      spec/lib/api/v3/work_packages/form/form_representer_spec.rb
  40. 34
      spec/lib/api/v3/work_packages/form/work_package_payload_representer_spec.rb
  41. 44
      spec/lib/api/v3/work_packages/form/work_package_schema_representer_spec.rb
  42. 20
      spec/requests/api/v3/projects/available_assignees_api_spec.rb
  43. 18
      spec/requests/api/v3/projects/available_responsibles_api_spec.rb
  44. 93
      spec/requests/api/v3/work_package_resource_spec.rb
  45. 158
      spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb

@ -1,4 +1,3 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
@ -27,17 +26,8 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'reform'
require 'reform/form/coercion'
module API
module V3
module Priorities
class PriorityModel < Reform::Form
include Coercion
property :name, type: String
end
end
module Grape
class Endpoint
include ::API::V3::Utilities::PathHelper
end
end

@ -1972,10 +1972,6 @@ Note that due to sharing this might be more than the versions *defined* by that
"version": {
"href": "/api/v3/versions/1"
},
"availableStatuses": {
"href": "/api/v3/work_packages/1528/available_statuses",
"title": "Available Statuses"
},
"availableWatchers": {
"href": "/api/v3/work_packages/1528/available_watchers",
"title": "Available Watchers"
@ -2637,6 +2633,20 @@ Gets a list of users that can be assigned to work packages in the given project.
[Available Assignees][]
+ Response 403 (application/hal+json)
Returned if the client does not have sufficient permissions.
**Required permission:** view work packages
+ Body
{
"_type": "Error",
"errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
"message": "You are not allowed to see the assignable users for this project."
}
## Available Responsibles [/api/v3/projects/{project_id}/work_packages/available_responsibles]
+ Model
@ -2718,3 +2728,17 @@ Gets a list of users that can be assigned as the responsible of a work package i
+ Response 200 (application/hal+json)
[Available Responsibles][]
+ Response 403 (application/hal+json)
Returned if the client does not have sufficient permissions.
**Required permission:** view work packages
+ Body
{
"_type": "Error",
"errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
"message": "You are not allowed to see the users available as responsible for this project."
}

@ -34,10 +34,6 @@
"href": "/api/v3/users/34",
"title": "Reinhold Weber - Joe3757"
},
"availableStatuses": {
"href": "/api/v3/work_packages/819/available_statuses",
"title": "Available Statuses"
},
"availableWatchers": {
"href": "/api/v3/work_packages/819/available_watchers",
"title": "Available Watchers"

@ -39,10 +39,6 @@
"href": "/api/v3/users/5",
"title": "Freida Fay - Christine6938"
},
"availableStatuses": {
"href": "/api/v3/work_packages/819/available_statuses",
"title": "Available Statuses"
},
"availableWatchers": {
"href": "/api/v3/work_packages/819/available_watchers",
"title": "Available Watchers"

@ -34,10 +34,6 @@
"href": "/api/v3/users/5",
"title": "Freida Fay - Christine6938"
},
"availableStatuses": {
"href": "/api/v3/work_packages/820/available_statuses",
"title": "Available Statuses"
},
"availableWatchers": {
"href": "/api/v3/work_packages/820/available_watchers",
"title": "Available Watchers"

@ -39,10 +39,6 @@
"href": "/api/v3/users/5",
"title": "Freida Fay - Christine6938"
},
"availableStatuses": {
"href": "/api/v3/work_packages/821/available_statuses",
"title": "Available Statuses"
},
"availableWatchers": {
"href": "/api/v3/work_packages/821/available_watchers",
"title": "Available Watchers"

@ -48,7 +48,7 @@ module API
as_strategy = API::Utilities::CamelCasingStrategy.new
link :self do
{ href: "#{root_path}api/v3/#{@self_link}" }
{ href: @self_link }
end
property :_type, getter: -> (*) { 'Collection' }

@ -27,24 +27,18 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'reform'
require 'reform/form/coercion'
module API
module V3
module Queries
class QueryModel < Reform::Form
include Coercion
module Errors
module Form
class InvalidResourceLink < StandardError
def initialize(property, expected_resource, actual_resource = :unknown)
expected = expected_resource.to_s.singularize.capitalize
actual = actual_resource.to_s.singularize.capitalize
message = "For property #{property} a resource of type #{expected}" \
" is expected but got a resource of type #{actual}"
property :name, type: String
property :project_id, type: Integer
property :user_id, type: Integer
property :filters, type: String
property :is_public, type: String
property :column_names, type: String
property :sort_criteria, type: String
property :group_by, type: String
property :display_sums, type: String
super(message)
end
end
end
end

@ -36,7 +36,7 @@ module API
class ActivityRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include API::Utilities::UrlHelper
include API::V3::Utilities::PathHelper
include OpenProject::TextFormatting
self.as_strategy = API::Utilities::CamelCasingStrategy.new
@ -50,20 +50,29 @@ module API
property :_type, exec_context: :decorator
link :self do
{ href: "#{root_path}api/v3/activities/#{represented.id}", title: "#{represented.id}" }
{
href: api_v3_paths.activity(represented.id),
title: "#{represented.id}"
}
end
link :workPackage do
{ href: "#{root_path}api/v3/work_packages/#{represented.journable.id}", title: "#{represented.journable.subject}" }
{
href: api_v3_paths.work_package(represented.journable.id),
title: "#{represented.journable.subject}"
}
end
link :user do
{ href: "#{root_path}api/v3/users/#{represented.user.id}", title: "#{represented.user.name} - #{represented.user.login}" }
{
href: api_v3_paths.user(represented.user.id),
title: "#{represented.user.name} - #{represented.user.login}"
}
end
link :update do
{
href: "#{root_path}api/v3/activities/#{represented.id}",
href: api_v3_paths.activity(represented.id),
method: :patch,
title: "#{represented.id}"
} if current_user_allowed_to_edit?

@ -36,24 +36,33 @@ module API
class AttachmentRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include OpenProject::StaticRouting::UrlHelpers
include API::V3::Utilities::PathHelper
self.as_strategy = API::Utilities::CamelCasingStrategy.new
property :_type, exec_context: :decorator
link :self do
{ href: "#{root_path}api/v3/attachments/#{represented.id}", title: "#{represented.filename}" }
{
href: api_v3_paths.attachment(represented.id),
title: "#{represented.filename}"
}
end
link :work_package do
work_package = represented.container
{ href: "#{root_path}api/v3/work_packages/#{work_package.id}", title: "#{work_package.subject}" } unless work_package.nil?
{
href: api_v3_paths.work_package(work_package.id),
title: "#{work_package.subject}"
} unless work_package.nil?
end
link :author do
author = represented.author
{ href: "#{root_path}api/v3/users/#{author.id}", title: "#{author.name} - #{author.login}" } unless author.nil?
{
href: api_v3_paths.user(author.id),
title: "#{author.name} - #{author.login}"
} unless author.nil?
end
property :id, render_nil: true

@ -37,9 +37,11 @@ module API
end
get do
self_link = api_v3_paths.categories(@project.identifier)
CategoryCollectionRepresenter.new(@categories,
@categories.count,
"projects/#{@project.identifier}/categories")
self_link)
end
end
end

@ -39,7 +39,7 @@ module API
get do
PriorityCollectionRepresenter.new(@priorities,
@priorities.count,
'priorities')
api_v3_paths.priorities)
end
end
end

@ -0,0 +1,48 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Projects
class AvailableAssigneesAPI < Grape::API
resource :available_assignees do
get do
authorize(:view_project, context: @project)
available_assignees = @project.possible_assignees
total = available_assignees.count
self_link = api_v3_paths.available_assignees(@project.id)
::API::V3::Users::UserCollectionRepresenter.new(available_assignees,
total,
self_link)
end
end
end
end
end
end

@ -0,0 +1,48 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Projects
class AvailableResponsiblesAPI < Grape::API
resource :available_responsibles do
get do
authorize(:view_project, context: @project)
available_responsibles = @project.possible_responsibles
total = available_responsibles.count
self_link = api_v3_paths.available_responsibles(@project.id)
::API::V3::Users::UserCollectionRepresenter.new(available_responsibles,
total,
self_link)
end
end
end
end
end
end

@ -36,7 +36,7 @@ module API
class ProjectRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include OpenProject::StaticRouting::UrlHelpers
include API::V3::Utilities::PathHelper
self.as_strategy = API::Utilities::CamelCasingStrategy.new
@ -44,17 +44,17 @@ module API
link :self do
{
href: "#{root_path}api/v3/projects/#{represented.id}",
href: api_v3_paths.project(represented.id),
title: "#{represented.name}"
}
end
link 'categories' do
"#{root_path}api/v3/projects/#{represented.id}/categories"
{ href: api_v3_paths.categories(represented.id) }
end
link 'versions' do
"#{root_path}api/v3/projects/#{represented.id}/versions"
{ href: api_v3_paths.versions(represented.id) }
end
property :id, render_nil: true

@ -45,10 +45,11 @@ module API
ProjectRepresenter.new(@project)
end
mount API::V3::Projects::AvailableAssigneesAPI
mount API::V3::Projects::AvailableResponsiblesAPI
mount API::V3::Categories::CategoriesAPI
mount API::V3::Versions::VersionsAPI
end
end
end
end

@ -36,14 +36,17 @@ module API
class QueryRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include OpenProject::StaticRouting::UrlHelpers
include API::V3::Utilities::PathHelper
self.as_strategy = API::Utilities::CamelCasingStrategy.new
property :_type, exec_context: :decorator
link :self do
{ href: "#{root_path}api/v3/queries/#{represented.id}", title: "#{represented.name}" }
{
href: api_v3_paths.query(represented.id),
title: "#{represented.name}"
}
end
property :id, render_nil: true

@ -35,23 +35,27 @@ module API
class RootRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include OpenProject::StaticRouting::UrlHelpers
include API::V3::Utilities::PathHelper
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
link 'priorities' do
"#{root_path}api/v3/priorities"
{
href: api_v3_paths.priorities
}
end
link 'project' do
{
href: "#{root_path}api/v3/project/{project_id}",
href: api_v3_paths.project('{project_id}'),
templated: true
}
end
link 'statuses' do
"#{root_path}api/v3/statuses"
{
href: api_v3_paths.statuses
}
end
end
end

@ -36,7 +36,7 @@ module API
class StatusRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include OpenProject::StaticRouting::UrlHelpers
include API::V3::Utilities::PathHelper
self.as_strategy = API::Utilities::CamelCasingStrategy.new
@ -44,7 +44,7 @@ module API
link :self do
{
href: "#{root_path}api/v3/statuses/#{represented.id}",
href: api_v3_paths.status(represented.id),
title: "#{represented.name}"
}
end

@ -39,7 +39,7 @@ module API
get do
StatusCollectionRepresenter.new(@statuses,
@statuses.count,
'statuses')
api_v3_paths.statuses)
end
namespace ':id' do

@ -36,7 +36,7 @@ module API
class UserRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include OpenProject::StaticRouting::UrlHelpers
include API::V3::Utilities::PathHelper
include AvatarHelper
self.as_strategy = API::Utilities::CamelCasingStrategy.new
@ -52,12 +52,15 @@ module API
property :_type, exec_context: :decorator
link :self do
{ href: "#{root_path}api/v3/users/#{represented.id}", title: "#{represented.name} - #{represented.login}" }
{
href: api_v3_paths.user(represented.id),
title: "#{represented.name} - #{represented.login}"
}
end
link :removeWatcher do
{
href: "#{root_path}api/v3/work_packages/#{@work_package.id}/watchers/#{represented.id}",
href: api_v3_paths.watcher(represented.id, @work_package.id),
method: :delete,
title: 'Remove watcher'
} if @work_package && current_user_allowed_to(:delete_work_package_watchers, @work_package)

@ -0,0 +1,162 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Utilities
module PathHelper
include API::Utilities::UrlHelper
class ApiV3Path
def self.root
"#{root_path}api/v3"
end
def self.activity(id)
"#{root}/activities/#{id}"
end
def self.attachment(id)
"#{root}/attachments/#{id}"
end
def self.available_assignees(project_id)
"#{project(project_id)}/available_assignees"
end
def self.available_responsibles(project_id)
"#{project(project_id)}/available_responsibles"
end
def self.available_watchers(work_package_id)
"#{work_package(work_package_id)}/watchers"
end
def self.categories(project_id)
"#{project(project_id)}/categories"
end
def self.preview_textile(link)
preview_markup(:textile, link)
end
def self.priorities
"#{root}/priorities"
end
def self.projects
"#{root}/projects"
end
def self.project(id)
"#{projects}/#{id}"
end
def self.query(id)
"#{root}/queries/#{id}"
end
def self.relation(id)
"#{root}/relations/#{id}"
end
def self.statuses
"#{root}/statuses"
end
def self.status(id)
"#{statuses}/#{id}"
end
def self.users
"#{root}/users"
end
def self.user(id)
"#{users}/#{id}"
end
def self.versions(project_id)
"#{project(project_id)}/versions"
end
def self.watcher(id, work_package_id)
"#{work_package(work_package_id)}/watchers/#{id}"
end
def self.work_packages
"#{root}/work_packages"
end
def self.work_package(id)
"#{work_packages}/#{id}"
end
def self.work_package_activities(id)
"#{work_package(id)}/activities"
end
def self.work_package_relations(id)
"#{work_package(id)}/relations"
end
def self.work_package_relation(id, work_package_id)
"#{work_package_relations(work_package_id)}/#{id}"
end
def self.work_package_form(id)
"#{work_package(id)}/form"
end
def self.work_package_watchers(id)
"#{work_package(id)}/watchers"
end
def self.root_path
@@root_path ||= Class.new.tap do |c|
c.extend(::API::V3::Utilities::PathHelper)
end.root_path
end
def self.preview_markup(method, link)
path = "#{root}/render/#{method}"
path += "?#{link}" unless link.nil?
path
end
end
def api_v3_paths
ApiV3Path
end
end
end
end
end

@ -39,7 +39,7 @@ module API
get do
VersionCollectionRepresenter.new(@versions,
@versions.count,
"projects/#{@project.identifier}/versions")
api_v3_paths.versions(@project.identifier))
end
end
end

@ -37,7 +37,7 @@ module API
class FormRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include API::Utilities::UrlHelper
include API::V3::Utilities::PathHelper
include OpenProject::TextFormatting
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
@ -52,31 +52,32 @@ module API
link :self do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/form",
href: api_v3_paths.work_package_form(represented.id),
}
end
link :validate do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/form",
href: api_v3_paths.work_package_form(represented.id),
method: :post
}
end
link :previewMarkup do
{
href: "#{root_path}api/v3/render/textile?"\
"#{root_path}api/v3/work_packages/#{represented.id}",
href: api_v3_paths.preview_textile(api_v3_paths.work_package(represented.id)),
method: :post
}
end
link :commit do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}",
href: api_v3_paths.work_package(represented.id),
method: :patch
} if @current_user.allowed_to?(:edit_work_packages, represented.project) &&
represented.valid?
# Calling valid? on represented empties the list of errors
# also removing errors from other sources (like contracts).
represented.errors.empty?
end
property :payload,
@ -98,9 +99,26 @@ module API
def validation_errors
errors = represented.errors
properties = errors.keys
properties.each do |p|
match = /(?<property>\w+)_id/.match(p)
if match
key = match[:property].to_sym
error = Array(errors[key]) + errors[p]
errors.set(key, error)
errors.delete(p)
end
end
errors.keys.each_with_object({}) do |key, hash|
error = ::API::Errors::Validation.new(errors.full_message(key, errors[key]))
messages = errors[key].each_with_object([]) do |m, l|
l << errors.full_message(key, m)
end
error = ::API::Errors::Validation.new(messages)
hash[key] = ::API::V3::Errors::ErrorRepresenter.new(error)
end
end

@ -37,21 +37,68 @@ module API
class WorkPackageAttributeLinksRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include API::Utilities::UrlHelper
include API::V3::Utilities::PathHelper
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
property :status,
exec_context: :decorator,
getter: -> (*) {
{ href: "#{root_path}api/v3/statuses/#{represented.status.id}" }
{ href: api_v3_paths.status(represented.status_id) }
},
setter: -> (value, *) {
resource = ::API::V3::Utilities::ResourceLinkParser.parse value['href']
resource = parse_resource(:status, :statuses, value['href'])
represented.status_id = resource[:id] if resource[:ns] == 'statuses'
represented.status_id = resource[:id] if resource
}
property :assignee,
exec_context: :decorator,
getter: -> (*) {
id = represented.assigned_to_id
{ href: (api_v3_paths.user(id) if id) }
},
setter: -> (value, *) {
user_id = parse_user_resource(:assignee, value['href'])
represented.assigned_to_id = user_id
}
property :responsible,
exec_context: :decorator,
getter: -> (*) {
id = represented.responsible_id
{ href: (api_v3_paths.user(id) if id) }
},
if: -> (*) { represented.status }
setter: -> (value, *) {
user_id = parse_user_resource(:responsible, value['href'])
represented.responsible_id = user_id
}
private
def parse_resource(property, ns, href)
return nil unless href
resource = ::API::V3::Utilities::ResourceLinkParser.parse href
if resource.nil? || resource[:ns] != ns.to_s
actual_ns = resource ? resource[:ns] : nil
fail ::API::Errors::Form::InvalidResourceLink.new(property, ns, actual_ns)
end
resource
end
def parse_user_resource(property, href)
resource = parse_resource(property, :users, href)
resource ? resource[:id] : nil
end
end
end
end

@ -70,23 +70,23 @@ module API
render_nil: true
property :parent_id, writeable: true
property :project_id, getter: -> (*) { project.id }
property :project_id,
getter: -> (*) { nil },
render_nil: false
property :start_date,
getter: -> (*) {
start_date.to_datetime.utc.iso8601 unless start_date.nil?
},
render_nil: true
getter: -> (*) { nil },
render_nil: false
property :due_date,
getter: -> (*) {
due_date.to_datetime.utc.iso8601 unless due_date.nil?
},
render_nil: true
getter: -> (*) { nil },
render_nil: false
property :version_id,
getter: -> (*) { fixed_version.try(:id) },
getter: -> (*) { nil },
setter: -> (value, *) { self.fixed_version_id = value },
render_nil: true
property :created_at, getter: -> (*) { created_at.utc.iso8601 }, render_nil: true
property :updated_at, getter: -> (*) { updated_at.utc.iso8601 }, render_nil: true
render_nil: false
property :created_at,
getter: -> (*) { nil }, render_nil: false
property :updated_at,
getter: -> (*) { nil }, render_nil: false
def _type
'WorkPackage'

@ -37,7 +37,7 @@ module API
class WorkPackageSchemaRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include API::Utilities::UrlHelper
include API::V3::Utilities::PathHelper
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
@ -58,7 +58,15 @@ module API
writeable: false
property :status,
exec_context: :decorator,
getter: -> (*) { represented.new_statuses_allowed_to(@current_user) } do
getter: -> (*) {
status_origin = represented
if represented.persisted? && represented.status_id_changed?
status_origin = represented.class.find(represented.id)
end
status_origin.new_statuses_allowed_to(@current_user)
} do
include Roar::JSON::HAL
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
@ -66,7 +74,7 @@ module API
property :links_to_allowed_statuses,
as: :_links,
getter: -> (*) { self } do
include API::Utilities::UrlHelper
include API::V3::Utilities::PathHelper
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
@ -74,7 +82,7 @@ module API
def allowed_values
represented.map do |status|
{ href: "#{root_path}api/v3/statuses/#{status.id}", title: status.name }
{ href: api_v3_paths.status(status.id), title: status.name }
end
end
end
@ -87,6 +95,50 @@ module API
decorator: ::API::V3::Statuses::StatusRepresenter,
getter: -> (*) { self }
end
property :assignee, getter: -> (*) { self } do
include Roar::JSON::HAL
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
property :links_to_available_assignees,
as: :_links,
getter: -> (*) { self } do
include API::V3::Utilities::PathHelper
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
property :allowed_values,
getter: -> (*) {
{ href: api_v3_paths.available_assignees(represented.project.id) }
},
exec_context: :decorator
end
property :type, getter: -> (*) { 'User' }
end
property :responsible, getter: -> (*) { self } do
include Roar::JSON::HAL
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
property :links_to_available_responsibles,
as: :_links,
getter: -> (*) { self } do
include API::V3::Utilities::PathHelper
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
property :allowed_values,
getter: -> (*) {
{ href: api_v3_paths.available_responsibles(represented.project.id) }
},
exec_context: :decorator
end
property :type, getter: -> (*) { 'User' }
end
end
end
end

@ -36,7 +36,7 @@ module API
class RelationRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include OpenProject::StaticRouting::UrlHelpers
include API::V3::Utilities::PathHelper
self.as_strategy = API::Utilities::CamelCasingStrategy.new
@ -51,20 +51,20 @@ module API
property :_type, exec_context: :decorator
link :self do
{ href: "#{root_path}api/v3/relations/#{represented.id}" }
{ href: api_v3_paths.relation(represented.id) }
end
link :relatedFrom do
{ href: "#{root_path}api/v3/work_packages/#{represented.from_id}" }
{ href: api_v3_paths.work_package(represented.from_id) }
end
link :relatedTo do
{ href: "#{root_path}api/v3/work_packages/#{represented.to_id}" }
{ href: api_v3_paths.work_package(represented.to_id) }
end
link :remove do
{
href: "#{root_path}api/v3/work_packages/#{represented.from.id}/relations/#{represented.id}",
href: api_v3_paths.work_package_relation(represented.id, represented.from.id),
method: :delete,
title: 'Remove relation'
} if current_user_allowed_to(:manage_work_package_relations)

@ -39,7 +39,9 @@ module API
'subject',
'parent_id',
'description',
'status_id'
'status_id',
'assigned_to_id',
'responsible_id'
].freeze
def initialize(object, user)
@ -54,6 +56,8 @@ module API
validate :user_allowed_to_edit_parent
validate :lock_version_valid
validate :readonly_attributes_unchanged
validate :assignee_visible
validate :responsible_visible
extend Reform::Form::ActiveModel::ModelValidations
copy_validations_from WorkPackage
@ -62,7 +66,7 @@ module API
def user_allowed_to_access
unless ::WorkPackage.visible(@user).exists?(model)
message = "Couldn't find WorkPackage with id=#{model.id}"
message = not_found_error_message('WorkPackage', model.id)
errors.add :error_not_found, message
end
end
@ -77,6 +81,10 @@ module API
end
end
def parent_changed?
model.changed.include? 'parent_id'
end
def lock_version_valid
errors.add :error_conflict, '' if model.lock_version.nil? || model.lock_version_changed?
end
@ -87,26 +95,32 @@ module API
errors.add :error_readonly, changed_attributes unless changed_attributes.empty?
end
def milestone_constraint
errors.add :parent_id, :cannot_be_milestone if model.parent && model.parent.is_milestone?
def assignee_visible
people_visible :assignee, 'assigned_to_id', model.project.possible_assignees
end
def user_allowed_to_access_parent
if parent_changed? && !parent_visible?
errors.add(:parent_id, error_message('parent_id.does_not_exist'))
end
def responsible_visible
people_visible :responsible, 'responsible_id', model.project.possible_responsibles
end
def parent_changed?
model.changed.include? 'parent_id'
def people_visible(attribute, id_attribute, list)
id = model[id_attribute]
return if id.nil? || !model.changed.include?(id_attribute)
unless user_visible?(id, list)
errors.add attribute, I18n.t('activerecord.errors.messages.inclusion', locale: :en)
end
end
def parent_visible?
!model.parent_id || ::WorkPackage.visible(@user).exists?(model.parent_id)
def user_visible?(user_id, list)
user = User.find_by_id(user_id)
!user.nil? && list.include?(user)
end
def error_message(path)
I18n.t("activerecord.errors.models.work_package.attributes.#{path}")
def not_found_error_message(object_type, id)
"Couldn't find #{object_type} with id=#{id}"
end
end
end

@ -36,7 +36,7 @@ module API
class WorkPackageRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include API::Utilities::UrlHelper
include API::V3::Utilities::PathHelper
include OpenProject::TextFormatting
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
@ -52,14 +52,14 @@ module API
link :self do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}",
href: api_v3_paths.work_package(represented.id),
title: "#{represented.subject}"
}
end
link :update do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/form",
href: api_v3_paths.work_package_form(represented.id),
method: :post,
title: "Update #{represented.subject}"
} if current_user_allowed_to(:edit_work_packages)
@ -67,7 +67,7 @@ module API
link :updateImmediately do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}",
href: api_v3_paths.work_package(represented.id),
method: :patch,
title: "Update #{represented.subject}"
} if current_user_allowed_to(:edit_work_packages)
@ -107,43 +107,35 @@ module API
link :author do
{
href: "#{root_path}api/v3/users/#{represented.author.id}",
href: api_v3_paths.user(represented.author.id),
title: "#{represented.author.name} - #{represented.author.login}"
} unless represented.author.nil?
end
link :responsible do
{
href: "#{root_path}api/v3/users/#{represented.responsible.id}",
href: api_v3_paths.user(represented.responsible.id),
title: "#{represented.responsible.name} - #{represented.responsible.login}"
} unless represented.responsible.nil?
end
link :assignee do
{
href: "#{root_path}api/v3/users/#{represented.assigned_to.id}",
href: api_v3_paths.user(represented.assigned_to.id),
title: "#{represented.assigned_to.name} - #{represented.assigned_to.login}"
} unless represented.assigned_to.nil?
end
link :availableStatuses do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/available_statuses",
title: 'Available Statuses'
} if @current_user.allowed_to?({ controller: :work_packages, action: :update },
represented.project)
end
link :availableWatchers do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/available_watchers",
href: api_v3_paths.available_watchers(represented.id),
title: 'Available Watchers'
}
end
link :watchChanges do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/watchers",
href: api_v3_paths.work_package_watchers(represented.id),
method: :post,
data: { user_id: @current_user.id },
title: 'Watch work package'
@ -154,7 +146,7 @@ module API
link :unwatchChanges do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/watchers/#{@current_user.id}",
href: "#{api_v3_paths.work_package_watchers(represented.id)}/#{@current_user.id}",
method: :delete,
title: 'Unwatch work package'
} if current_user_allowed_to(:view_work_packages) &&
@ -163,7 +155,7 @@ module API
link :addWatcher do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/watchers{?user_id}",
href: "#{api_v3_paths.work_package_watchers(represented.id)}{?user_id}",
method: :post,
title: 'Add watcher',
templated: true
@ -172,7 +164,7 @@ module API
link :addRelation do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/relations",
href: api_v3_paths.work_package_relations(represented.id),
method: :post,
title: 'Add relation'
} if current_user_allowed_to(:manage_work_package_relations)
@ -188,7 +180,7 @@ module API
link :changeParent do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}",
href: api_v3_paths.work_package(represented.id),
method: :patch,
title: "Change parent of #{represented.subject}"
} if current_user_allowed_to(:manage_subtasks)
@ -196,7 +188,7 @@ module API
link :addComment do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/activities",
href: api_v3_paths.work_package_activities(represented.id),
method: :post,
title: 'Add comment'
} if current_user_allowed_to(:add_work_package_notes)
@ -204,7 +196,7 @@ module API
link :parent do
{
href: "#{root_path}api/v3/work_packages/#{represented.parent.id}",
href: api_v3_paths.work_package(represented.parent.id),
title: represented.parent.subject
} unless represented.parent.nil? || !represented.parent.visible?
end
@ -219,7 +211,7 @@ module API
link :version do
{
href: version_path(represented.fixed_version),
href: api_v3_paths.versions(represented.fixed_version),
type: 'text/html',
title: "#{represented.fixed_version.to_s_for_project(represented.project)}"
} if represented.fixed_version && @current_user.allowed_to?({ controller: 'versions', action: 'show' }, represented.fixed_version.project, global: false)

@ -45,7 +45,11 @@ module API
payload = ::API::V3::WorkPackages::Form::WorkPackagePayloadRepresenter
.new(@work_package, enforce_lock_version_validation: true)
payload.from_json(request_body.to_json)
begin
payload.from_json(request_body.to_json)
rescue ::API::Errors::Form::InvalidResourceLink => e
fail ::API::Errors::Validation.new(e.message)
end
end
end
@ -56,10 +60,9 @@ module API
def write_request_valid?
contract = WorkPackageContract.new(@representer.represented, current_user)
# Although the contract triggers the ActiveModel validations on
# the work package, it does not merge the contract errors with
# the model errors. Thus, we need to do it manually.
unless contract.validate
# We need to merge the contract errors with the model errors in
# order to have them available at one place.
unless contract.validate & @representer.represented.valid?
contract.errors.keys.each do |key|
contract.errors[key].each do |message|
@representer.represented.errors.add(key, message)
@ -126,38 +129,6 @@ module API
end
resource :available_assignees do
get do
authorize(:add_work_packages, context: @work_package.project) \
|| authorize(:edit_work_packages, context: @work_package.project)
available_assignees = @work_package.assignable_assignees
total = available_assignees.count
self_link = "work_packages/#{@work_package.id}/available_assignees"
::API::V3::Users::UserCollectionRepresenter.new(available_assignees,
total,
self_link)
end
end
resource :available_responsibles do
get do
authorize(:add_work_packages, context: @work_package.project) \
|| authorize(:edit_work_packages, context: @work_package.project)
available_responsibles = @work_package.assignable_responsibles
total = available_responsibles.count
self_link = "work_packages/#{@work_package.id}/available_responsibles"
::API::V3::Users::UserCollectionRepresenter.new(available_responsibles,
total,
self_link)
end
end
mount ::API::V3::WorkPackages::WatchersAPI
mount ::API::V3::Relations::RelationsAPI
mount ::API::V3::WorkPackages::Form::FormAPI

@ -30,7 +30,7 @@ require 'spec_helper'
describe ::API::V3::Categories::CategoryCollectionRepresenter do
let(:categories) { FactoryGirl.build_list(:category, 3) }
let(:representer) { described_class.new(categories, 42, 'projects/1/categories') }
let(:representer) { described_class.new(categories, 42, '/api/v3/projects/1/categories') }
context 'generation' do
subject(:collection) { representer.to_json }

@ -30,7 +30,7 @@ require 'spec_helper'
describe ::API::V3::Priorities::PriorityCollectionRepresenter do
let(:priorities) { FactoryGirl.build_list(:priority, 3) }
let(:representer) { described_class.new(priorities, 42, 'priorities') }
let(:representer) { described_class.new(priorities, 42, '/api/v3/priorities') }
context 'generation' do
subject(:collection) { representer.to_json }

@ -46,6 +46,12 @@ describe ::API::V3::RootRepresenter do
it { should have_json_path('_links/project') }
it { should have_json_path('_links/project/href') }
it { should have_json_path('_links/project/templated') }
it {
should be_json_eql('/api/v3/projects/{project_id}'.to_json)
.at_path('_links/project/href')
}
it { should be_json_eql(true.to_json).at_path('_links/project/templated') }
end
describe 'statuses' do

@ -30,7 +30,7 @@ require 'spec_helper'
require 'lib/api/v3/statuses/shared/status_collection_representer'
describe ::API::V3::Statuses::StatusCollectionRepresenter do
include_context 'status collection representer', 'statuses'
include_context 'status collection representer', '/api/v3/statuses'
context 'generation' do
subject(:collection) { representer.to_json }

@ -35,7 +35,7 @@ describe ::API::V3::Users::UserCollectionRepresenter do
created_on: Time.now,
updated_on: Time.now)
}
let(:representer) { described_class.new(users, 42, 'work_package/1/watchers') }
let(:representer) { described_class.new(users, 42, '/api/v3/work_package/1/watchers') }
context 'generation' do
subject(:collection) { representer.to_json }

@ -0,0 +1,255 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::Utilities::PathHelper do
let(:helper) { Class.new.tap { |c| c.extend(::API::V3::Utilities::PathHelper) }.api_v3_paths }
shared_examples_for 'api v3 path' do
it { is_expected.to match(/^\/api\/v3/) }
end
describe '#root' do
subject { helper.root }
it_behaves_like 'api v3 path'
end
describe '#activity' do
subject { helper.activity 1 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/activities\/1/) }
end
describe '#attachment' do
subject { helper.attachment 1 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/attachments\/1/) }
end
describe '#available_assignees' do
subject { helper.available_assignees 42 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/projects\/42\/available_assignees/) }
end
describe '#available_responsibles' do
subject { helper.available_responsibles 42 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/projects\/42\/available_responsibles/) }
end
describe '#available_watchers' do
subject { helper.available_watchers 42 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/work_packages\/42\/watchers/) }
end
describe '#categories' do
subject { helper.categories 42 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/projects\/42\/categories/) }
end
describe '#preview_textile' do
subject { helper.preview_textile '/api/v3/work_packages/42' }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/render\/textile/) }
it { is_expected.to match(/\?\/api\/v3\/work_packages\/42$/) }
end
describe '#priorities' do
subject { helper.priorities }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/priorities/) }
end
describe 'projects paths' do
describe '#projects' do
subject { helper.projects }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/projects/) }
end
describe '#project' do
subject { helper.project 1 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/projects\/1/) }
end
end
describe '#query' do
subject { helper.query 1 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/queries\/1/) }
end
describe 'relations paths' do
describe '#relation' do
subject { helper.relation 1 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/relations/) }
end
describe '#relation' do
subject { helper.relation 1 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/relations\/1/) }
end
end
describe 'statuses paths' do
describe '#statuses' do
subject { helper.statuses }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/statuses/) }
end
describe '#status' do
subject { helper.status 1 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/statuses\/1/) }
end
end
describe '#user' do
subject { helper.user 1 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/users\/1/) }
end
describe '#versions' do
subject { helper.versions 42 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/projects\/42\/versions/) }
end
describe 'work packages paths' do
shared_examples_for 'api v3 work packages path' do
it { is_expected.to match(/^\/api\/v3\/work_packages/) }
end
describe '#work_packages' do
subject { helper.work_packages }
it_behaves_like 'api v3 work packages path'
end
describe '#work_package' do
subject { helper.work_package 1 }
it_behaves_like 'api v3 work packages path'
it { is_expected.to match(/^\/api\/v3\/work_packages\/1/) }
end
describe '#work_package_activities' do
subject { helper.work_package_activities 42 }
it_behaves_like 'api v3 work packages path'
it { is_expected.to match(/^\/api\/v3\/work_packages\/42\/activities/) }
end
describe '#work_package_relations' do
subject { helper.work_package_relations 42 }
it_behaves_like 'api v3 work packages path'
it { is_expected.to match(/^\/api\/v3\/work_packages\/42\/relations/) }
end
describe '#work_package_relation' do
subject { helper.work_package_relation 1, 42 }
it_behaves_like 'api v3 work packages path'
it { is_expected.to match(/^\/api\/v3\/work_packages\/42\/relations\/1/) }
end
describe '#work_package_form' do
subject { helper.work_package_form 1 }
it_behaves_like 'api v3 work packages path'
it { is_expected.to match(/^\/api\/v3\/work_packages\/1\/form/) }
end
describe '#work_package_watchers' do
subject { helper.work_package_watchers 1 }
it_behaves_like 'api v3 work packages path'
it { is_expected.to match(/^\/api\/v3\/work_packages\/1\/watchers/) }
end
describe '#watcher' do
subject { helper.watcher 1, 42 }
it_behaves_like 'api v3 work packages path'
it { is_expected.to match(/^\/api\/v3\/work_packages\/42\/watchers\/1/) }
end
end
end

@ -29,7 +29,7 @@
require 'spec_helper'
describe ::API::V3::Versions::VersionCollectionRepresenter do
let(:self_link) { 'projects/1/versions' }
let(:self_link) { '/api/v3/projects/1/versions' }
let(:versions) { FactoryGirl.build_list(:version, 3) }
let(:representer) { described_class.new(versions, 42, self_link) }

@ -72,15 +72,13 @@ describe ::API::V3::WorkPackages::Form::FormRepresenter do
describe 'commit' do
context 'valid work package' do
before { allow(work_package).to receive(:valid?).and_return(true) }
it { is_expected.to have_json_path('_links/commit/href') }
it { is_expected.to be_json_eql(:patch.to_json).at_path('_links/commit/method') }
end
context 'invalid work package' do
before { allow(work_package).to receive(:valid?).and_return(false) }
before { allow(work_package.errors).to receive(:empty?).and_return(false) }
it { is_expected.not_to have_json_path('_links/commit/href') }
end
@ -93,7 +91,7 @@ describe ::API::V3::WorkPackages::Form::FormRepresenter do
member_through_role: role)
}
before { allow(work_package).to receive(:valid?).and_return(true) }
before { allow(work_package.errors).to receive(:empty?).and_return(true) }
it { is_expected.not_to have_json_path('_links/commit/href') }
end
@ -108,21 +106,24 @@ describe ::API::V3::WorkPackages::Form::FormRepresenter do
context 'with errors' do
let(:subject_error_message) { 'Subject can\'t be blank!' }
let(:status_error_message) { 'Status can\'t be blank!' }
let(:errors) {
{ subject: [subject_error_message], status: [status_error_message] }
}
let(:subject_error) { ::API::Errors::Validation.new(subject_error_message) }
let(:status_error) { ::API::Errors::Validation.new(status_error_message) }
let(:api_subject_error) { ::API::V3::Errors::ErrorRepresenter.new(subject_error) }
let(:api_status_error) { ::API::V3::Errors::ErrorRepresenter.new(status_error) }
let(:errors) { { subject: api_subject_error, status: api_status_error } }
let(:api_errors) { { subject: api_subject_error, status: api_status_error } }
before do
allow(work_package.errors).to receive(:keys).and_return(errors.keys)
allow(work_package.errors).to receive(:full_message).with(:subject, [])
allow(work_package).to receive(:errors).and_return(errors)
allow(work_package.errors).to receive(:full_message).with(:subject, anything)
.and_return(subject_error_message)
allow(work_package.errors).to receive(:full_message).with(:status, [])
allow(work_package.errors).to receive(:full_message).with(:status, anything)
.and_return(status_error_message)
end
it { is_expected.to be_json_eql(errors.to_json).at_path('_embedded/validationErrors') }
it { is_expected.to be_json_eql(api_errors.to_json).at_path('_embedded/validationErrors') }
end
end
end

@ -57,10 +57,38 @@ describe ::API::V3::WorkPackages::Form::WorkPackagePayloadRepresenter do
end
describe '_links' do
it { is_expected.to have_json_type(Object).at_path('_links') }
it { is_expected.to have_json_path('_links') }
it 'should link status' do
expect(subject).to have_json_path('_links/status/href')
shared_examples_for 'linked property' do |property_name, href|
let(:path) { "_links/#{property_name}/href" }
it { expect(subject).to have_json_path(path) }
it { expect(subject).to be_json_eql(href.to_json).at_path(path) }
end
describe 'status' do
let(:status) { FactoryGirl.build(:status, id: 42) }
before { work_package.status = status }
it_behaves_like 'linked property', 'status', '/api/v3/statuses/42'
end
describe 'assignee and responsible' do
let(:user) { FactoryGirl.build(:user, id: 42) }
describe 'assignee' do
before { work_package.assigned_to = user }
it_behaves_like 'linked property', 'assignee', '/api/v3/users/42'
end
describe 'responsible' do
before { work_package.responsible = user }
it_behaves_like 'linked property', 'responsible', '/api/v3/users/42'
end
end
end
end

@ -65,6 +65,22 @@ describe ::API::V3::WorkPackages::Form::WorkPackageSchemaRepresenter do
end
end
shared_examples_for 'linked property' do |property_name, type|
it { is_expected.to have_json_path(property_name) }
it { is_expected.to be_json_eql(type.to_json).at_path("#{property_name}/type") }
it { is_expected.to have_json_path("#{property_name}/_links") }
it { is_expected.to have_json_path("#{property_name}/_links/allowedValues") }
end
shared_examples_for 'linked with href' do |property_name|
let(:path) { "#{property_name}/_links/allowedValues/href" }
it { is_expected.to be_json_eql(href).at_path(path) }
end
describe '_type' do
it_behaves_like 'schema property', '_type', 'MetaType', true, false
end
@ -79,11 +95,7 @@ describe ::API::V3::WorkPackages::Form::WorkPackageSchemaRepresenter do
describe 'status' do
shared_examples_for 'contains statuses' do
it { is_expected.to have_json_path('status') }
it { is_expected.to have_json_path('status/_links') }
it { is_expected.to have_json_path('status/_links/allowedValues') }
it_behaves_like 'linked property', 'status', 'Status'
it 'contains valid links to statuses' do
status_links = statuses.map do |status|
@ -93,8 +105,6 @@ describe ::API::V3::WorkPackages::Form::WorkPackageSchemaRepresenter do
is_expected.to be_json_eql(status_links.to_json).at_path('status/_links/allowedValues')
end
it { is_expected.to be_json_eql('Status'.to_json).at_path('status/type') }
it 'embeds statuses' do
embedded_statuses = statuses.map do |status|
{
@ -135,6 +145,26 @@ describe ::API::V3::WorkPackages::Form::WorkPackageSchemaRepresenter do
it_behaves_like 'contains statuses'
end
end
describe 'responsible and assignee' do
let(:base_href) { "/api/v3/projects/#{work_package.project.id}" }
describe 'assignee' do
it_behaves_like 'linked property', 'assignee', 'User'
it_behaves_like 'linked with href', 'assignee' do
let(:href) { "#{base_href}/available_assignees".to_json }
end
end
describe 'responsible' do
it_behaves_like 'linked property', 'responsible', 'User'
it_behaves_like 'linked with href', 'responsible' do
let(:href) { "#{base_href}/available_responsibles".to_json }
end
end
end
end
end
end

@ -29,16 +29,16 @@
require 'spec_helper'
require 'rack/test'
describe API::V3::WorkPackages::WorkPackagesAPI, type: :request do
describe API::V3::Projects::ProjectsAPI, type: :request do
let(:admin) { FactoryGirl.create(:admin) }
describe 'available assignees' do
let(:work_package) { FactoryGirl.build_stubbed(:work_package) }
let(:project) { FactoryGirl.build_stubbed(:project) }
before { allow(WorkPackage).to receive(:find).and_return(work_package) }
before { allow(Project).to receive(:find).and_return(project) }
shared_context 'request available assignees' do
before { get "/api/v3/work_packages/#{work_package.id}/available_assignees" }
before { get "/api/v3/projects/#{project.id}/available_assignees" }
end
it_behaves_like 'safeguarded API' do
@ -60,7 +60,7 @@ describe API::V3::WorkPackages::WorkPackagesAPI, type: :request do
context 'single user' do
before do
allow(work_package.project).to receive(:possible_assignees).and_return([user])
allow(project).to receive(:possible_assignees).and_return([user])
allow(user).to receive(:created_on).and_return(user.created_at)
allow(user).to receive(:updated_on).and_return(user.created_at)
@ -71,7 +71,7 @@ describe API::V3::WorkPackages::WorkPackagesAPI, type: :request do
context 'multiple users' do
before do
allow(work_package.project).to receive(:possible_assignees).and_return([user, user2])
allow(project).to receive(:possible_assignees).and_return([user, user2])
allow(user).to receive(:created_on).and_return(user.created_at)
allow(user).to receive(:updated_on).and_return(user.created_at)
@ -86,14 +86,14 @@ describe API::V3::WorkPackages::WorkPackagesAPI, type: :request do
describe 'groups' do
let(:group) { FactoryGirl.create(:group) }
let(:work_package) { FactoryGirl.create(:work_package) }
let(:project) { FactoryGirl.create(:project) }
before { allow(WorkPackage).to receive(:find).and_return(work_package) }
before { allow(Project).to receive(:find).and_return(project) }
context 'with work_package_group_assignment' do
before do
allow(Setting).to receive(:work_package_group_assignment?).and_return(true)
work_package.project.add_member! group, FactoryGirl.create(:role)
project.add_member! group, FactoryGirl.create(:role)
end
it_behaves_like 'returns available assignees', 1, 1
@ -102,7 +102,7 @@ describe API::V3::WorkPackages::WorkPackagesAPI, type: :request do
context 'without work_package_group_assignment' do
before do
allow(Setting).to receive(:work_package_group_assignment?).and_return(false)
work_package.project.add_member! group, FactoryGirl.create(:role)
project.add_member! group, FactoryGirl.create(:role)
end
it_behaves_like 'returns available assignees', 0, 0

@ -29,16 +29,16 @@
require 'spec_helper'
require 'rack/test'
describe API::V3::WorkPackages::WorkPackagesAPI do
describe API::V3::Projects::ProjectsAPI do
let(:admin) { FactoryGirl.create(:admin) }
describe 'available responsibles' do
let(:work_package) { FactoryGirl.build_stubbed(:work_package) }
let(:project) { FactoryGirl.build_stubbed(:project) }
before { allow(WorkPackage).to receive(:find).and_return(work_package) }
before { allow(Project).to receive(:find).and_return(project) }
shared_context 'request available responsibles' do
before { get "/api/v3/work_packages/#{work_package.id}/available_responsibles" }
before { get "/api/v3/projects/#{project.id}/available_responsibles" }
end
it_behaves_like 'safeguarded API' do
@ -60,7 +60,7 @@ describe API::V3::WorkPackages::WorkPackagesAPI do
context 'single user' do
before do
allow(work_package.project).to receive(:possible_responsibles).and_return([user])
allow(project).to receive(:possible_responsibles).and_return([user])
allow(user).to receive(:created_on).and_return(user.created_at)
allow(user).to receive(:updated_on).and_return(user.created_at)
@ -71,7 +71,7 @@ describe API::V3::WorkPackages::WorkPackagesAPI do
context 'multiple users' do
before do
allow(work_package.project).to receive(:possible_responsibles).and_return([user, user2])
allow(project).to receive(:possible_responsibles).and_return([user, user2])
allow(user).to receive(:created_on).and_return(user.created_at)
allow(user).to receive(:updated_on).and_return(user.created_at)
@ -86,14 +86,14 @@ describe API::V3::WorkPackages::WorkPackagesAPI do
describe 'groups' do
let(:group) { FactoryGirl.create(:group) }
let(:work_package) { FactoryGirl.create(:work_package) }
let(:project) { FactoryGirl.create(:project) }
before { allow(WorkPackage).to receive(:find).and_return(work_package) }
before { allow(Project).to receive(:find).and_return(project) }
context 'with work_package_group_assignment' do
before do
allow(Setting).to receive(:work_package_group_assignment?).and_return(true)
work_package.project.add_member! group, FactoryGirl.create(:role)
project.add_member! group, FactoryGirl.create(:role)
end
it_behaves_like 'returns available responsibles', 0, 0

@ -205,7 +205,7 @@ h4. things we like
shared_context 'patch request' do
before(:each) do
allow(User).to receive(:current).and_return current_user
patch patch_path, params.to_json, 'CONTENT_TYPE' => 'application/json'
patch patch_path, params.to_json, 'CONTENT_TYPE' => 'application/json'
end
end
@ -402,6 +402,97 @@ h4. things we like
'Status no valid transition exists from old to new '\
'status for the current user roles.'
end
context 'wrong resource' do
let(:status_link) { "/api/v3/users/#{current_user.id}" }
include_context 'patch request'
it_behaves_like 'constraint violation',
'For property status a resource of type Status' \
' is expected but got a resource of type User.'
end
end
context 'assignee and responsible' do
let(:user) { FactoryGirl.create(:user, member_in_project: project) }
let(:params) { valid_params.merge(user_parameter) }
let(:work_package) {
FactoryGirl.create(:work_package,
project: project,
assigned_to: current_user,
responsible: current_user)
}
before { allow(User).to receive(:current).and_return current_user }
shared_examples_for 'handling people' do |property|
let(:user_parameter) { { _links: { property => { href: user_href } } } }
describe 'nil' do
let(:user_href) { nil }
include_context 'patch request'
it { expect(response.status).to eq(200) }
it { expect(response.body).not_to have_json_path("_links/#{property}") }
it_behaves_like 'lock version updated'
end
describe 'valid' do
let(:user_href) { "/api/v3/users/#{user.id}" }
include_context 'patch request'
it { expect(response.status).to eq(200) }
it {
expect(response.body).to be_json_eql("#{user.name} - #{user.login}".to_json)
.at_path("_links/#{property}/title")
}
it_behaves_like 'lock version updated'
end
describe 'invalid' do
include_context 'patch request'
context 'user doesn\'t exist' do
let(:user_href) { '/api/v3/users/909090' }
it_behaves_like 'constraint violation',
"#{property.capitalize} is not included in the list"
end
context 'user is not visible' do
let(:invalid_user) { FactoryGirl.create(:user, id: 42) }
let(:user_href) { '/api/v3/users/42' }
it_behaves_like 'constraint violation',
"#{property.capitalize} is not included in the list"
end
context 'wrong resource' do
let(:user_href) { "/api/v3/statuses/#{work_package.status.id}" }
include_context 'patch request'
it_behaves_like 'constraint violation',
"For property #{property} a resource of type User" \
' is expected but got a resource of type Status.'
end
end
end
context 'assingee' do
it_behaves_like 'handling people', 'assignee'
end
context 'responsible' do
it_behaves_like 'handling people', 'responsible'
end
end
describe 'update with read-only attributes' do

@ -261,19 +261,167 @@ describe 'API v3 Work package form resource', type: :request do
it 'should respond with updated work package status' do
expect(subject.body).to be_json_eql(status_link.to_json).at_path(path)
end
it 'should still show the original allowed statuses' do
expect(subject.body).to be_json_eql(status_link.to_json)
.at_path('_embedded/schema/status/_links/allowedValues/1/href')
end
end
context 'invalid status' do
include_context 'post request'
context 'no transition' do
include_context 'post request'
it_behaves_like 'valid payload'
it_behaves_like 'valid payload'
it_behaves_like 'having an error', 'status_id'
it_behaves_like 'having an error', 'status'
it 'should respond with updated work package status' do
expect(subject.body).to be_json_eql(status_link.to_json).at_path(path)
it 'should respond with updated work package status' do
expect(subject.body).to be_json_eql(status_link.to_json).at_path(path)
end
end
context 'status does not exist' do
let(:error_id) {
'urn:openproject-org:api:v3:errors:MultipleErrors'.to_json
}
let(:status_link) { '/api/v3/statuses/-1' }
include_context 'post request'
it_behaves_like 'valid payload'
it {
expect(subject.body).to be_json_eql(error_id)
.at_path('_embedded/validationErrors/status/errorIdentifier')
}
it {
expect(subject.body).to have_json_size(2)
.at_path('_embedded/validationErrors/status/_embedded/errors')
}
it 'should respond with updated work package status' do
expect(subject.body).to be_json_eql(status_link.to_json).at_path(path)
end
end
context 'wrong resource' do
let(:status_link) { "/api/v3/users/#{authorized_user.id}" }
include_context 'post request'
it_behaves_like 'constraint violation',
'For property status a resource of type Status' \
' is expected but got a resource of type User.'
end
end
end
describe 'assignee and responsible' do
shared_examples_for 'handling people' do |property|
let(:path) { "_embedded/payload/_links/#{property}/href" }
let(:visible_user) {
FactoryGirl.create(:user,
member_in_project: project)
}
let(:user_parameter) { { _links: { property => { href: user_link } } } }
let(:params) { valid_params.merge(user_parameter) }
context "valid #{property}" do
shared_examples_for 'valid user assignment' do
include_context 'post request'
it_behaves_like 'valid payload'
it_behaves_like 'having no errors'
it "should respond with updated work package #{property}" do
expect(subject.body).to be_json_eql(user_link.to_json).at_path(path)
end
end
context 'empty user' do
let(:user_link) { nil }
it_behaves_like 'valid user assignment'
end
context 'existing user' do
let(:user_link) { "/api/v3/users/#{visible_user.id}" }
it_behaves_like 'valid user assignment'
end
end
context "invalid #{property}" do
context 'non-existing user' do
let(:user_link) { '/api/v3/users/42' }
include_context 'post request'
it_behaves_like 'valid payload'
it_behaves_like 'having an error', property
it "should respond with updated work package #{property}" do
expect(subject.body).to be_json_eql(user_link.to_json).at_path(path)
end
end
context 'wrong resource' do
let(:user_link) { "/api/v3/statuses/#{work_package.status.id}" }
include_context 'post request'
it_behaves_like 'constraint violation',
"For property #{property} a resource of type User" \
' is expected but got a resource of type Status.'
end
end
end
it_behaves_like 'handling people', 'assignee'
it_behaves_like 'handling people', 'responsible'
end
describe 'multiple errors' do
let(:user_link) { '/api/v3/users/42' }
let(:status_link) { '/api/v3/statuses/-1' }
let(:links) {
{
_links: {
status: { href: status_link },
assignee: { href: user_link },
responsible: { href: user_link }
}
}
}
let(:params) { valid_params.merge(subject: nil).merge(links) }
include_context 'post request'
it_behaves_like 'valid payload'
it {
expect(subject.body).to have_json_size(4).at_path('_embedded/validationErrors')
}
it { expect(subject.body).to have_json_path('_embedded/validationErrors/subject') }
it { expect(subject.body).to have_json_path('_embedded/validationErrors/status') }
it {
expect(subject.body).to have_json_size(2)
.at_path('_embedded/validationErrors/status/_embedded/errors')
}
it { expect(subject.body).to have_json_path('_embedded/validationErrors/assignee') }
it {
expect(subject.body).to have_json_path('_embedded/validationErrors/responsible')
}
end
end
end

Loading…
Cancel
Save