Merge pull request #2153 from opf/feature/16971_work_package_forms_endpoint

16971 work package forms endpoint
pull/2173/head
ulferts 10 years ago
commit 4c1bd38aa8
  1. 2
      app/assets/javascripts/angular/services/work-package-service.js
  2. 4
      lib/api/errors/error_base.rb
  3. 42
      lib/api/v3/render/render_api.rb
  4. 6
      lib/api/v3/statuses/statuses_api.rb
  5. 52
      lib/api/v3/utilities/resource_link_parser.rb
  6. 58
      lib/api/v3/work_packages/form/form_api.rb
  7. 111
      lib/api/v3/work_packages/form/form_representer.rb
  8. 59
      lib/api/v3/work_packages/form/work_package_attribute_links_representer.rb
  9. 104
      lib/api/v3/work_packages/form/work_package_payload_representer.rb
  10. 94
      lib/api/v3/work_packages/form/work_package_schema_representer.rb
  11. 49
      lib/api/v3/work_packages/link_to_object_extractor.rb
  12. 22
      lib/api/v3/work_packages/work_package_contract.rb
  13. 8
      lib/api/v3/work_packages/work_package_representer.rb
  14. 27
      lib/api/v3/work_packages/work_packages_api.rb
  15. 129
      spec/lib/api/v3/work_packages/form/form_representer_spec.rb
  16. 67
      spec/lib/api/v3/work_packages/form/work_package_payload_representer_spec.rb
  17. 126
      spec/lib/api/v3/work_packages/form/work_package_schema_representer_spec.rb
  18. 37
      spec/lib/api/v3/work_packages/work_package_representer_spec.rb
  19. 70
      spec/requests/api/v3/work_package_resource_spec.rb
  20. 283
      spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb

@ -134,7 +134,7 @@ module.exports = function($http, PathHelper, WorkPackagesHelper, HALAPIResource,
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8"
}, force: true};
return workPackage.links.update.fetch(options).then(function(workPackage) {
return workPackage.links.updateImmediately.fetch(options).then(function(workPackage) {
return workPackage;
})
},

@ -33,9 +33,11 @@ module API
attr_reader :code, :message, :details, :errors
def self.create(errors)
[:error_unauthorized, :error_conflict, :error_readonly].each do |key|
[:error_not_found, :error_unauthorized, :error_conflict, :error_readonly].each do |key|
if errors.has_key?(key)
case key
when :error_not_found
return ::API::Errors::NotFound.new(errors[key].join(' '))
when :error_unauthorized
return ::API::Errors::Unauthorized
when :error_conflict

@ -31,7 +31,6 @@ module API
module V3
module Render
class RenderAPI < Grape::API
include OpenProject::TextFormatting
format :txt
resources :render do
@ -43,43 +42,32 @@ module API
end
def context_object
try_context_object
rescue ::ActiveRecord::RecordNotFound
fail API::Errors::InvalidRenderContext.new('Context does not exist!')
end
def try_context_object
if params[:context]
context_object = nil
namespace, id = parse_context
context = parse_context
case namespace
case context[:ns]
when 'work_packages'
context_object = WorkPackage.visible(current_user).find_by_id(id)
end
unless context_object
fail API::Errors::InvalidRenderContext.new('Context does not exist!')
WorkPackage.visible(current_user).find(context[:id])
end
end
end
def parse_context
contexts = API::V3::Root.routes.map do |route|
route_options = route.instance_variable_get(:@options)
match = route_options[:compiled].match(params[:context])
context = ::API::V3::Utilities::ResourceLinkParser.parse(params[:context])
if match
{
ns: /\/(?<ns>\w+)\//.match(route_options[:namespace])[:ns],
id: match[:id]
}
end
end
contexts.compact!.uniq! { |c| c[:ns] }
fail API::Errors::InvalidRenderContext.new('No context found.') if contexts.empty?
unless SUPPORTED_CONTEXT_NAMESPACES.include? contexts[0][:ns]
if context.nil?
fail API::Errors::InvalidRenderContext.new('No context found.')
elsif !SUPPORTED_CONTEXT_NAMESPACES.include? context[:ns]
fail API::Errors::InvalidRenderContext.new('Unsupported context found.')
else
context
end
[contexts[0][:ns], contexts[0][:id]]
end
def render(type)

@ -39,6 +39,12 @@ module API
get do
StatusCollectionRepresenter.new(@statuses)
end
namespace ':id' do
get do
fail NotImplementedError
end
end
end
end
end

@ -0,0 +1,52 @@
#-- 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 ResourceLinkParser
def self.parse(resource_link)
API::V3::Root.routes.each do |route|
route_options = route.instance_variable_get(:@options)
match = route_options[:compiled].match(resource_link)
if match
return {
ns: /\/(?<ns>\w+)\//.match(route_options[:namespace])[:ns],
id: match[:id]
}
end
end
nil
end
end
end
end
end

@ -0,0 +1,58 @@
#-- 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 WorkPackages
module Form
class FormAPI < Grape::API
helpers do
def process_form_request
write_work_package_attributes
write_request_valid?
error = ::API::Errors::ErrorBase.create(@representer.represented.errors)
if error.is_a? ::API::Errors::Validation
status 200
FormRepresenter.new(@representer.represented, current_user: current_user)
else
fail error
end
end
end
post '/form' do
process_form_request
end
end
end
end
end
end

@ -0,0 +1,111 @@
#-- 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.
#++
require 'roar/decorator'
require 'roar/json/hal'
module API
module V3
module WorkPackages
module Form
class FormRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include API::Utilities::UrlHelper
include OpenProject::TextFormatting
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
def initialize(model, options = {})
@current_user = options[:current_user]
super(model)
end
property :_type, exec_context: :decorator, writeable: false
link :self do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/form",
}
end
link :validate do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/form",
method: :post
}
end
link :previewMarkup do
{
href: "#{root_path}api/v3/render/textile?"\
"#{root_path}api/v3/work_packages/#{represented.id}",
method: :post
}
end
link :commit do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}",
method: :patch
} if @current_user.allowed_to?(:edit_work_packages, represented.project) &&
represented.valid?
end
property :payload,
embedded: true,
decorator: Form::WorkPackagePayloadRepresenter,
getter: -> (*) { self }
property :schema,
embedded: true,
exec_context: :decorator,
getter: -> (*) {
Form::WorkPackageSchemaRepresenter.new(represented,
current_user: @current_user)
}
property :validation_errors, embedded: true, exec_context: :decorator
def _type
'Form'
end
def validation_errors
errors = represented.errors
errors.keys.each_with_object({}) do |key, hash|
error = ::API::Errors::Validation.new(errors.full_message(key, errors[key]))
hash[key] = ::API::V3::Errors::ErrorRepresenter.new(error)
end
end
end
end
end
end
end

@ -0,0 +1,59 @@
#-- 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.
#++
require 'roar/decorator'
require 'roar/json/hal'
module API
module V3
module WorkPackages
module Form
class WorkPackageAttributeLinksRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include API::Utilities::UrlHelper
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
property :status,
exec_context: :decorator,
getter: -> (*) {
{ href: "#{root_path}api/v3/statuses/#{represented.status.id}" }
},
setter: -> (value, *) {
resource = ::API::V3::Utilities::ResourceLinkParser.parse value['href']
represented.status_id = resource[:id] if resource[:ns] == 'statuses'
},
if: -> (*) { represented.status }
end
end
end
end
end

@ -0,0 +1,104 @@
#-- 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.
#++
require 'roar/decorator'
require 'roar/json/hal'
module API
module V3
module WorkPackages
module Form
class WorkPackagePayloadRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
def initialize(represented, options={})
if options[:enforce_lock_version_validation]
# enforces availibility validation of lock_version
represented.lock_version = nil
end
super(represented)
end
property :_type, exec_context: :decorator, writeable: false
property :linked_resources,
as: :_links,
exec_context: :decorator,
getter: -> (*) {
work_package_attribute_links_representer represented
},
setter: -> (value, *) {
representer = work_package_attribute_links_representer represented
representer.from_json(value.to_json)
}
property :lock_version
property :subject, render_nil: true
property :raw_description,
getter: -> (*) { description },
setter: -> (value, *) { self.description = value },
render_nil: true
property :parent_id, writeable: true
property :project_id, getter: -> (*) { project.id }
property :start_date,
getter: -> (*) {
start_date.to_datetime.utc.iso8601 unless start_date.nil?
},
render_nil: true
property :due_date,
getter: -> (*) {
due_date.to_datetime.utc.iso8601 unless due_date.nil?
},
render_nil: true
property :version_id,
getter: -> (*) { fixed_version.try(:id) },
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
def _type
'WorkPackage'
end
private
def work_package_attribute_links_representer(represented)
::API::V3::WorkPackages::Form::WorkPackageAttributeLinksRepresenter.new represented
end
end
end
end
end
end

@ -0,0 +1,94 @@
#-- 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.
#++
require 'roar/decorator'
require 'roar/json/hal'
module API
module V3
module WorkPackages
module Form
class WorkPackageSchemaRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include API::Utilities::UrlHelper
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
def initialize(model, options = {})
@current_user = options[:current_user]
super(model)
end
property :_type,
getter: -> (*) { { type: 'MetaType', required: true, writable: false } },
writeable: false
property :lock_version,
getter: -> (*) { { type: 'Integer', required: true, writable: false } },
writeable: false
property :subject,
getter: -> (*) { { type: 'String' } },
writeable: false
property :status,
exec_context: :decorator,
getter: -> (*) { represented.new_statuses_allowed_to(@current_user) } do
include Roar::JSON::HAL
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
property :links_to_allowed_statuses,
as: :_links,
getter: -> (*) { self } do
include API::Utilities::UrlHelper
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
property :allowed_values, exec_context: :decorator
def allowed_values
represented.map do |status|
{ href: "#{root_path}api/v3/statuses/#{status.id}", title: status.name }
end
end
end
property :type, getter: -> (*) { 'Status' }
collection :allowed_values,
embedded: true,
class: ::Status,
decorator: ::API::V3::Statuses::StatusRepresenter,
getter: -> (*) { self }
end
end
end
end
end
end

@ -0,0 +1,49 @@
#-- 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 WorkPackages
module LinkToObjectExtractor
def self.parse_links(links)
links.keys.each_with_object({}) do |attribute, h|
resource = ::API::V3::Utilities::ResourceLinkParser.parse links[attribute]['href']
if resource
case resource[:ns]
when 'statuses'
h[:status_id] = resource[:id]
end
end
end
end
end
end
end
end

@ -34,7 +34,13 @@ module API
module V3
module WorkPackages
class WorkPackageContract < Reform::Contract
WRITEABLE_ATTRIBUTES = ['lock_version', 'subject', 'parent_id', 'description'].freeze
WRITEABLE_ATTRIBUTES = [
'lock_version',
'subject',
'parent_id',
'description',
'status_id'
].freeze
def initialize(object, user)
super(object)
@ -43,9 +49,10 @@ module API
@can = WorkPackagePolicy.new(user)
end
validate :user_allowed_to_access
validate :user_allowed_to_edit
validate :user_allowed_to_edit_parent
validate :lock_version_set
validate :lock_version_valid
validate :readonly_attributes_unchanged
extend Reform::Form::ActiveModel::ModelValidations
@ -53,6 +60,13 @@ module API
private
def user_allowed_to_access
unless ::WorkPackage.visible(@user).exists?(model)
message = "Couldn't find WorkPackage with id=#{model.id}"
errors.add :error_not_found, message
end
end
def user_allowed_to_edit
errors.add :error_unauthorized, '' unless @can.allowed?(model, :edit)
end
@ -63,8 +77,8 @@ module API
end
end
def lock_version_set
errors.add :error_conflict, '' if model.lock_version.nil?
def lock_version_valid
errors.add :error_conflict, '' if model.lock_version.nil? || model.lock_version_changed?
end
def readonly_attributes_unchanged

@ -58,6 +58,14 @@ module API
end
link :update do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}/form",
method: :post,
title: "Update #{represented.subject}"
} if current_user_allowed_to(:edit_work_packages)
end
link :updateImmediately do
{
href: "#{root_path}api/v3/work_packages/#{represented.id}",
method: :patch,

@ -40,15 +40,20 @@ module API
helpers do
attr_reader :work_package
def decorate_work_package(work_package)
@representer = ::API::V3::WorkPackages::WorkPackageRepresenter.new(work_package, { current_user: current_user }, :activities, :users)
def write_work_package_attributes
if request_body
payload = ::API::V3::WorkPackages::Form::WorkPackagePayloadRepresenter
.new(@work_package, enforce_lock_version_validation: true)
payload.from_json(request_body.to_json)
end
end
def patch_request_body
env['api.request.input']
def request_body
env['api.request.body']
end
def patch_request_valid?
def write_request_valid?
contract = WorkPackageContract.new(@representer.represented, current_user)
# Although the contract triggers the ActiveModel validations on
@ -68,7 +73,8 @@ module API
before do
@work_package = WorkPackage.find(params[:id])
decorate_work_package(@work_package)
@representer = ::API::V3::WorkPackages::WorkPackageRepresenter
.new(work_package, { current_user: current_user }, :activities, :users)
end
get do
@ -77,9 +83,7 @@ module API
end
patch do
@representer.represented.lock_version = nil # enforces availibility validation of lock_version
@representer.from_json(patch_request_body)
write_work_package_attributes
send_notifications = !(params.has_key?(:notify) && params[:notify] == 'false')
update_service = UpdateWorkPackageService.new(current_user,
@ -87,8 +91,8 @@ module API
nil,
send_notifications)
if patch_request_valid? && update_service.save
decorate_work_package(@work_package.reload)
if write_request_valid? && update_service.save
@representer.represented.reload
@representer
else
fail ::API::Errors::ErrorBase.create(@representer.represented.errors)
@ -149,6 +153,7 @@ module API
mount ::API::V3::WorkPackages::WatchersAPI
mount ::API::V3::WorkPackages::StatusesAPI
mount ::API::V3::Relations::RelationsAPI
mount ::API::V3::WorkPackages::Form::FormAPI
end

@ -0,0 +1,129 @@
#-- 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::WorkPackages::Form::FormRepresenter do
let(:work_package) {
FactoryGirl.build(:work_package,
id: 42,
created_at: DateTime.now,
updated_at: DateTime.now)
}
let(:current_user) {
FactoryGirl.build(:user, member_in_project: work_package.project)
}
let(:representer) { described_class.new(work_package, current_user: current_user) }
context 'generation' do
subject(:generated) { representer.to_json }
it { is_expected.to be_json_eql('Form'.to_json).at_path('_type') }
describe '_links' do
it { is_expected.to have_json_path('_links') }
it { is_expected.to have_json_path('_links/self/href') }
describe 'validate' do
it { is_expected.to have_json_path('_links/validate/href') }
it { is_expected.to be_json_eql(:post.to_json).at_path('_links/validate/method') }
end
describe 'preview markup' do
it { is_expected.to have_json_path('_links/previewMarkup/href') }
it { is_expected.to be_json_eql(:post.to_json).at_path('_links/previewMarkup/method') }
it 'contains link to work package' do
body = parse_json(subject)
preview_markup_wp_link = body['_links']['previewMarkup']['href'].split('?')[1]
wp_self_link = body['_links']['commit']['href']
expect(preview_markup_wp_link).to eq(wp_self_link)
end
end
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) }
it { is_expected.not_to have_json_path('_links/commit/href') }
end
context 'user with insufficient permissions' do
let(:role) { FactoryGirl.create(:role, permissions: []) }
let(:current_user) {
FactoryGirl.build(:user,
member_in_project: work_package.project,
member_through_role: role)
}
before { allow(work_package).to receive(:valid?).and_return(true) }
it { is_expected.not_to have_json_path('_links/commit/href') }
end
end
end
describe 'validation errors' do
context 'w/o errors' do
it { is_expected.to be_json_eql({}.to_json).at_path('_embedded/validationErrors') }
end
context 'with errors' do
let(:subject_error_message) { 'Subject can\'t be blank!' }
let(:status_error_message) { 'Status can\'t be blank!' }
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 } }
before do
allow(work_package.errors).to receive(:keys).and_return(errors.keys)
allow(work_package.errors).to receive(:full_message).with(:subject, [])
.and_return(subject_error_message)
allow(work_package.errors).to receive(:full_message).with(:status, [])
.and_return(status_error_message)
end
it { is_expected.to be_json_eql(errors.to_json).at_path('_embedded/validationErrors') }
end
end
end
end

@ -0,0 +1,67 @@
#-- 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::WorkPackages::Form::WorkPackagePayloadRepresenter do
let(:work_package) {
FactoryGirl.build(:work_package,
created_at: DateTime.now,
updated_at: DateTime.now)
}
let(:representer) { described_class.new(work_package) }
before { allow(work_package).to receive(:lock_version).and_return(1) }
context 'generation' do
subject(:generated) { representer.to_json }
it { is_expected.to include_json('WorkPackage'.to_json).at_path('_type') }
describe 'work_package' do
it { is_expected.to have_json_path('subject') }
it { is_expected.to have_json_path('rawDescription') }
describe 'lock version' do
it { is_expected.to have_json_path('lockVersion') }
it { is_expected.to have_json_type(Integer).at_path('lockVersion') }
it { is_expected.to be_json_eql(work_package.lock_version.to_json).at_path('lockVersion') }
end
end
describe '_links' do
it { is_expected.to have_json_type(Object).at_path('_links') }
it 'should link status' do
expect(subject).to have_json_path('_links/status/href')
end
end
end
end

@ -0,0 +1,126 @@
#-- 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::WorkPackages::Form::WorkPackageSchemaRepresenter do
let(:work_package) { FactoryGirl.build(:work_package) }
let(:current_user) {
FactoryGirl.build(:user, member_in_project: work_package.project)
}
let(:representer) { described_class.new(work_package, current_user: current_user) }
context 'generation' do
subject(:generated) { representer.to_json }
describe 'schema' do
shared_examples_for 'schema property' do |path, type, required, writable|
it { is_expected.to have_json_path(path) }
it { is_expected.to be_json_eql(type.to_json).at_path("#{path}/type") }
it 'has valid required value' do
required_path = "#{path}/required"
if required.nil?
is_expected.not_to have_json_path(required_path)
else
is_expected.to be_json_eql(required.to_json).at_path(required_path)
end
end
it 'has valid writable value' do
writable_path = "#{path}/writable"
if writable.nil?
is_expected.not_to have_json_path(writable_path)
else
is_expected.to be_json_eql(writable.to_json).at_path(writable_path)
end
end
end
describe '_type' do
it_behaves_like 'schema property', '_type', 'MetaType', true, false
end
describe 'lock version' do
it_behaves_like 'schema property', 'lockVersion', 'Integer', true, false
end
describe 'subject' do
it_behaves_like 'schema property', 'subject', 'String'
end
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 'contains valid links to statuses' do
status_links = statuses.map do |status|
{ href: "/api/v3/statuses/#{status.id}", title: status.name }
end
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|
{ _type: 'Status', id: status.id, name: status.name }
end
is_expected.to be_json_eql(embedded_statuses.to_json)
.at_path('status/_embedded/allowedValues')
end
end
context 'w/o allowed statuses' do
before { allow(work_package).to receive(:new_statuses_allowed_to).and_return([]) }
it_behaves_like 'contains statuses' do
let(:statuses) { [] }
end
end
context 'with allowed statuses' do
let(:statuses) { FactoryGirl.build_list(:status, 3) }
before { allow(work_package).to receive(:new_statuses_allowed_to).and_return(statuses) }
it_behaves_like 'contains statuses'
end
end
end
end
end

@ -45,7 +45,17 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
}
let(:category) { FactoryGirl.build(:category) }
let(:project) { work_package.project }
let(:permissions) { %i(view_work_packages view_work_package_watchers add_work_package_watchers delete_work_package_watchers manage_work_package_relations add_work_package_notes) }
let(:permissions) {
[
:view_work_packages,
:view_work_package_watchers,
:edit_work_packages,
:add_work_package_watchers,
:delete_work_package_watchers,
:manage_work_package_relations,
:add_work_package_notes
]
}
let(:role) { FactoryGirl.create :role, permissions: permissions }
before(:each) do
@ -124,6 +134,29 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
expect(subject).to have_json_path('_links/self/title')
end
describe 'update links' do
describe 'update by form' do
it { expect(subject).to have_json_path('_links/update/href') }
it {
expect(subject).to be_json_eql("/api/v3/work_packages/#{work_package.id}/form".to_json)
.at_path('_links/update/href')
}
it { expect(subject).to be_json_eql('post'.to_json).at_path('_links/update/method') }
end
describe 'immediate update' do
it { expect(subject).to have_json_path('_links/updateImmediately/href') }
it {
expect(subject).to be_json_eql("/api/v3/work_packages/#{work_package.id}".to_json)
.at_path('_links/updateImmediately/href')
}
it {
expect(subject).to be_json_eql('patch'.to_json)
.at_path('_links/updateImmediately/method')
}
end
end
describe 'version' do
context 'no version set' do
it { is_expected.to_not have_json_path('versionViewable') }
@ -304,7 +337,7 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
end
end
describe 'delete' do
describe 'log_time' do
it_behaves_like 'action link' do
let(:action) { 'log_time' }
let(:permission) { :log_time }

@ -210,17 +210,37 @@ h4. things we like
end
context 'user without needed permissions' do
let(:current_user) { FactoryGirl.create :user }
let(:params) { valid_params }
context 'no permission to see the work package' do
let(:work_package) { FactoryGirl.create(:work_package, id: 42) }
let(:current_user) { FactoryGirl.create :user }
let(:params) { valid_params }
include_context 'patch request'
include_context 'patch request'
it_behaves_like 'unauthorized access'
it_behaves_like 'not found', 42, 'WorkPackage'
end
context 'no permission to edit the work package' do
let(:role) { FactoryGirl.create(:role, permissions: [:view_work_packages]) }
let(:current_user) {
FactoryGirl.create(:user,
member_in_project: work_package.project,
member_through_role: role)
}
let(:params) { valid_params }
include_context 'patch request'
it_behaves_like 'unauthorized access'
end
end
context 'user with needed permissions' do
shared_examples_for 'lock version updated' do
it { expect(subject.body).to be_json_eql(work_package.reload.lock_version).at_path('lockVersion') }
it {
expect(subject.body).to be_json_eql(work_package.reload.lock_version)
.at_path('lockVersion')
}
end
describe 'notification' do
@ -346,6 +366,44 @@ h4. things we like
end
end
context 'status' do
let(:target_status) { FactoryGirl.create(:status) }
let(:status_link) { "/api/v3/statuses/#{target_status.id}" }
let(:status_parameter) { { _links: { status: { href: status_link } } } }
let(:params) { valid_params.merge(status_parameter) }
before { allow(User).to receive(:current).and_return current_user }
context 'valid status' do
let!(:workflow) {
FactoryGirl.create(:workflow,
type_id: work_package.type.id,
old_status: work_package.status,
new_status: target_status,
role: current_user.memberships[0].roles[0])
}
include_context 'patch request'
it { expect(response.status).to eq(200) }
it 'should respond with updated work package status' do
expect(subject.body).to be_json_eql(target_status.name.to_json)
.at_path('status')
end
it_behaves_like 'lock version updated'
end
context 'invalid status' do
include_context 'patch request'
it_behaves_like 'constraint violation',
'Status no valid transition exists from old to new '\
'status for the current user roles.'
end
end
describe 'update with read-only attributes' do
describe 'single read-only violation' do
context 'start date' do
@ -469,7 +527,7 @@ h4. things we like
it_behaves_like 'update conflict'
end
context 'state object' do
context 'stale object' do
let(:params) { valid_params.merge(subject: 'Updated subject') }
before do

@ -0,0 +1,283 @@
#-- 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'
require 'rack/test'
describe 'API v3 Work package form resource', type: :request do
include Rack::Test::Methods
include Capybara::RSpecMatchers
let(:project) { FactoryGirl.create(:project, is_public: false) }
let(:work_package) { FactoryGirl.create(:work_package, project: project) }
let(:authorized_user) { FactoryGirl.create(:user, member_in_project: project) }
let(:unauthorized_user) { FactoryGirl.create(:user) }
describe '#post' do
let(:post_path) { "/api/v3/work_packages/#{work_package.id}/form" }
let(:valid_params) do
{
_type: 'WorkPackage',
lockVersion: work_package.lock_version
}
end
subject(:response) { last_response }
shared_context 'post request' do
before(:each) do
allow(User).to receive(:current).and_return current_user
post post_path, params.to_json, 'CONTENT_TYPE' => 'application/json'
end
end
context 'user without needed permissions' do
let(:work_package) { FactoryGirl.create(:work_package, id: 42, project: project) }
let(:params) { {} }
include_context 'post request' do
let(:current_user) { unauthorized_user }
end
it_behaves_like 'not found', 42, 'WorkPackage'
end
context 'user with needed permissions' do
let(:params) {}
let(:current_user) { authorized_user }
context 'non-existing work package' do
let(:post_path) { '/api/v3/work_packages/eeek/form' }
include_context 'post request'
it_behaves_like 'not found', 'eeek', 'WorkPackage'
end
context 'existing work package' do
shared_examples_for 'valid payload' do
it { expect(response.status).to eq(200) }
it { expect(subject.body).to have_json_path('_embedded/payload') }
it { expect(subject.body).to have_json_path('_embedded/payload/lockVersion') }
it { expect(subject.body).to have_json_path('_embedded/payload/subject') }
it { expect(subject.body).to have_json_path('_embedded/payload/rawDescription') }
end
shared_examples_for 'valid payload with initial values' do
it {
expect(subject.body).to be_json_eql(work_package.lock_version.to_json)
.at_path('_embedded/payload/lockVersion')
}
it {
expect(subject.body).to be_json_eql(work_package.subject.to_json)
.at_path('_embedded/payload/subject')
}
it {
expect(subject.body).to be_json_eql(work_package.description.to_json)
.at_path('_embedded/payload/rawDescription')
}
end
shared_examples_for 'having no errors' do
it {
expect(subject.body).to be_json_eql({}.to_json)
.at_path('_embedded/validationErrors')
}
end
shared_examples_for 'having an error' do |property|
it { expect(subject.body).to have_json_path("_embedded/validationErrors/#{property}") }
describe 'error body' do
let(:error_id) { 'urn:openproject-org:api:v3:errors:PropertyConstraintViolation' }
let(:error_body) {
parse_json(subject.body)['_embedded']['validationErrors'][property]
}
it { expect(error_body['errorIdentifier']).to eq(error_id) }
end
end
describe 'body' do
context 'empty' do
include_context 'post request'
it_behaves_like 'valid payload'
it_behaves_like 'valid payload with initial values'
it_behaves_like 'having no errors'
end
context 'filled' do
let(:valid_params) do
{
_type: 'WorkPackage',
lockVersion: work_package.lock_version
}
end
describe 'no change' do
let(:params) { valid_params }
include_context 'post request'
it_behaves_like 'valid payload'
it_behaves_like 'valid payload with initial values'
it_behaves_like 'having no errors'
end
describe 'lock version' do
context 'missing lock version' do
let(:params) { valid_params.except(:lockVersion) }
include_context 'post request'
it_behaves_like 'update conflict'
end
context 'stale object' do
let(:params) { valid_params.merge(subject: 'Updated subject') }
before do
params
work_package.subject = 'I am the first!'
work_package.save!
expect(valid_params[:lockVersion]).not_to eq(work_package.lock_version)
end
include_context 'post request'
it { expect(response.status).to eq(409) }
it_behaves_like 'update conflict'
end
end
describe 'subject' do
include_context 'post request'
context 'valid subject' do
let(:params) { valid_params.merge(subject: 'Updated subject') }
it_behaves_like 'valid payload'
it_behaves_like 'having no errors'
it 'should respond with updated work package subject' do
expect(subject.body).to be_json_eql('Updated subject'.to_json)
.at_path('_embedded/payload/subject')
end
end
context 'invalid subject' do
let(:params) { valid_params.merge(subject: nil) }
it_behaves_like 'valid payload'
it_behaves_like 'having an error', 'subject'
it 'should respond with updated work package subject' do
expect(subject.body).to be_json_eql(nil.to_json)
.at_path('_embedded/payload/subject')
end
end
end
describe 'description' do
let(:path) { '_embedded/payload/rawDescription' }
let(:description) { '*Some text* _describing_ *something*...' }
let(:params) { valid_params.merge(rawDescription: description) }
include_context 'post request'
it_behaves_like 'valid payload'
it_behaves_like 'having no errors'
it 'should respond with updated work package description' do
expect(subject.body).to be_json_eql(description.to_json).at_path(path)
end
end
describe 'status' do
let(:path) { '_embedded/payload/_links/status/href' }
let(:target_status) { FactoryGirl.create(:status) }
let(:status_link) { "/api/v3/statuses/#{target_status.id}" }
let(:status_parameter) { { _links: { status: { href: status_link } } } }
let(:params) { valid_params.merge(status_parameter) }
context 'valid status' do
let!(:workflow) {
FactoryGirl.create(:workflow,
type_id: work_package.type.id,
old_status: work_package.status,
new_status: target_status,
role: current_user.memberships[0].roles[0])
}
include_context 'post request'
it_behaves_like 'valid payload'
it_behaves_like 'having no 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 'invalid status' do
include_context 'post request'
it_behaves_like 'valid payload'
it_behaves_like 'having an error', 'status_id'
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
end
end
end
end
end
end
end
Loading…
Cancel
Save