queries create and create form api endpoints

pull/5233/head
Markus Kahl 8 years ago
parent 4c32d17ede
commit 9e79d02d8f
  1. 54
      app/contracts/queries/base_contract.rb
  2. 42
      app/contracts/queries/create_contract.rb
  3. 9
      app/models/queries/not_existing_filter.rb
  4. 33
      app/models/query.rb
  5. 59
      app/services/queries/create_query_service.rb
  6. 7
      config/locales/en.yml
  7. 47
      docs/api/apiv3/endpoints/queries.apib
  8. 2
      lib/api/utilities/resource_link_parser.rb
  9. 62
      lib/api/v3/queries/create_form_api.rb
  10. 68
      lib/api/v3/queries/create_form_representer.rb
  11. 79
      lib/api/v3/queries/create_query.rb
  12. 3
      lib/api/v3/queries/form_representer.rb
  13. 6
      lib/api/v3/queries/queries_api.rb
  14. 98
      lib/api/v3/queries/query_payload_representer.rb
  15. 50
      lib/api/v3/queries/query_representer.rb
  16. 186
      lib/api/v3/queries/query_serialization.rb
  17. 2
      lib/api/v3/work_packages/work_packages_shared_helpers.rb
  18. 6
      lib/open_project/patches/hash.rb
  19. 283
      spec/requests/api/v3/queries/create_form_api_spec.rb
  20. 173
      spec/requests/api/v3/queries/create_query_spec.rb

@ -0,0 +1,54 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'model_contract'
module Queries
class BaseContract < ::ModelContract
attribute :name
attribute :project_id
attribute :is_public # => public
attribute :display_sums # => sums
attribute :column_names # => columns
attribute :filters
attribute :sort_criteria # => sortBy
attribute :group_by # => groupBy
attr_reader :user
def initialize(query, user)
super query
@user = user
end
end
end

@ -0,0 +1,42 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'queries/base_contract'
module Queries
class CreateContract < BaseContract
validate :user_allowed_to_make_public
def user_allowed_to_make_public
if is_public && !user.allowed_to?(:manage_public_queries, model.project)
errors.add :public, :error_unauthorized
end
end
end
end

@ -52,4 +52,13 @@ class Queries::NotExistingFilter < Queries::BaseFilter
# deactivating superclass validation
def validate_inclusion_of_operator; end
def to_hash
{
non_existent_filter: {
operator: operator,
values: values
}
}
end
end

@ -46,6 +46,9 @@ class Query < ActiveRecord::Base
validates_length_of :name, maximum: 255
validate :validate_work_package_filters
validate :validate_columns
validate :validate_sort_criteria
validate :validate_group_by
scope :visible, ->(to:) do
# User can see public queries and his own queries
@ -190,9 +193,7 @@ class Query < ActiveRecord::Base
unless filter.valid?
messages = filter
.errors
.messages
.values
.flatten
.full_messages
.join(" #{I18n.t('support.array.sentence_connector')} ")
attribute_name = filter.human_name
@ -208,6 +209,32 @@ class Query < ActiveRecord::Base
end
end
def validate_columns
available_names = available_columns.map(&:name).map(&:to_s)
column_names.each do |name|
unless available_names.include? name.to_s
errors.add :column_names, I18n.t(:error_invalid_query_column, value: name)
end
end
end
def validate_sort_criteria
available_criteria = sortable_columns.map(&:name).map(&:to_s)
sort_criteria.each do |name, _dir|
unless available_criteria.include? name.to_s
errors.add :sort_criteria, I18n.t(:error_invalid_sort_criterion, value: name)
end
end
end
def validate_group_by
unless group_by.nil? || groupable_columns.map(&:name).map(&:to_s).include?(group_by.to_s)
errors.add :group_by, I18n.t(:error_invalid_group_by, value: group_by)
end
end
def editable_by?(user)
return false unless user
# Admin can edit them all and regular users can edit their private queries

@ -0,0 +1,59 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class CreateQueryService
include Concerns::Contracted
attr_reader :user
self.contract = Queries::CreateContract
def initialize(user:)
@user = user
end
def call(query)
create query
end
private
def create(query)
initialize_contract! query
result, errors = validate_and_save query
query.update user: user
ServiceResult.new success: result, errors: errors, result: query
end
def initialize_contract!(query)
self.contract = self.class.contract.new query, user
end
end

@ -251,6 +251,10 @@ en:
activemodel:
errors:
models:
"queries/create_contract":
attributes:
public:
error_unauthorized: "- The user has no permission to create public queries."
"relations/base_contract":
attributes:
to:
@ -850,6 +854,9 @@ en:
error_cookie_missing: 'The OpenProject cookie is missing. Please ensure that cookies are enabled, as this application will not properly function without.'
error_custom_option_not_found: "Option does not exist."
error_failed_to_delete_entry: 'Failed to delete this entry.'
error_invalid_group_by: "Can't group by: %{value}"
error_invalid_query_column: "Invalid query column: %{value}"
error_invalid_sort_criterion: "Can't sort by column: %{value}"
error_pdf_export_too_many_columns: "Too many columns selected for the PDF export. Please reduce the number of columns."
error_token_authenticity: 'Unable to verify Cross-Site Request Forgery token.'
error_work_package_done_ratios_not_updated: "Work package done ratios not updated."

@ -1231,6 +1231,53 @@ Returns a collection of queries. The collection can be filtered via query parame
"message": "You are not allowed to see the queries."
}
## Create query [POST]
When calling this endpoint the client provides a single object, containing at least the properties and links that are required, in the body.
The required fields of a Query can be found in its schema, which is embedded in the respective form.
Note that it is only allowed to provide properties or links supporting the write operation.
+ Response 201 (application/hal+json)
[Query][]
+ Response 400 (application/hal+json)
Occurs when the client did not send a valid JSON object in the request body.
+ Body
{
"_type": "Error",
"errorIdentifier": "urn:openproject-org:api:v3:errors:InvalidRequestBody",
"message": "The request body was not a single JSON object."
}
+ Response 404 (application/hal+json)
Returned when the client tries to use (link) unknown users, projects or operators in the query to be created.
+ Body
{
"_type": "Error",
"errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound",
"message": "User 42 not found"
}
## Query Create Form [/api/v3/queries/form]
This endpoint returns a form to allow a guided creation of a new query.
The returned form will be pre-filled with default values for every property, if available.
For more details and all possible responses see the general specification of [Forms](#forms).
## Query Create Form [POST]
+ Response 200 (application/hal+json)
[Example Form][]
## Schema For Global Queries [/api/v3/queries/schema]
+ Model

@ -34,7 +34,7 @@ module API
# http://tools.ietf.org/html/rfc3986#section-3.3
SEGMENT_CHARACTER = '(\w|[-~!$&\'\(\)*+\.,:;=@]|%[0-9A-Fa-f]{2})'.freeze
RESOURCE_REGEX =
"/api/v(?<version>\\d)/(?<namespace>\\w+)/(?<id>#{SEGMENT_CHARACTER}+)\\z".freeze
"/api/v(?<version>\\d)/(?<namespace>[\\w\/]+)/(?<id>#{SEGMENT_CHARACTER}+)\\z".freeze
SO_REGEX = "/api/v(?<version>\\d)/string_objects/?\\?value=(?<id>\\w*).*\\z".freeze
class << self

@ -0,0 +1,62 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'api/v3/queries/query_representer'
require 'queries/create_query_service'
module API
module V3
module Queries
class CreateFormAPI < ::API::OpenProjectAPI
resource :form do
helpers ::API::V3::Queries::CreateQuery
post do
representer = ::API::V3::Queries::QueryRepresenter.create Query.new_default, current_user: current_user
query = representer.from_hash Hash(request_body)
contract = ::Queries::CreateContract.new query, current_user
contract.validate
query.user = current_user
api_errors = ::API::Errors::ErrorBase.create_errors(contract.errors)
# errors for invalid data (e.g. validation errors) are handled inside the form
if api_errors.all? { |error| error.code == 422 }
status 200
CreateFormRepresenter.new query, current_user: current_user, errors: api_errors
else
fail ::API::Errors::MultipleErrors.create_if_many(api_errors)
end
end
end
end
end
end
end

@ -0,0 +1,68 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
class CreateFormRepresenter < FormRepresenter
link :self do
{
href: api_v3_paths.query_form,
method: :post
}
end
link :validate do
{
href: api_v3_paths.query_form,
method: :post
}
end
link :commit do
if allow_commit?
{
href: api_v3_paths.queries,
method: :post
}
end
end
private
def allow_commit?
represented.name.present? && (
(!represented.is_public && current_user.allowed_to?(:save_queries, represented.project)) ||
(represented.is_public && current_user.allowed_to?(:manage_public_queries, represented.project))
) && @errors.empty?
end
end
end
end
end

@ -0,0 +1,79 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'api/v3/queries/query_representer'
require 'queries/create_query_service'
module API
module V3
module Queries
module CreateQuery
def create_query(request_body, current_user)
rep = representer.new Query.new, current_user: current_user
query = rep.from_hash request_body
call = ::CreateQueryService.new(user: current_user).call query
if call.success?
representer.new call.result, current_user: current_user, embed_links: true
else
fail ::API::Errors::ErrorBase.create_and_merge_errors(call.errors)
end
end
def representer
::API::V3::Queries::QueryRepresenter
end
end
def create_query_form(
query,
current_user:,
contract_class: ::Queries::CreateContract,
form_class: ::API::V3::Queries::CreateFormRepresenter,
action: :update
)
write_work_package_attributes(work_package, request_body, reset_lock_version: true)
contract = contract_class.new(query, current_user)
contract.validate
api_errors = ::API::Errors::ErrorBase.create_errors(contract.errors)
# errors for invalid data (e.g. validation errors) are handled inside the form
if only_validation_errors(api_errors)
status 200
form_class.new(query,
current_user: current_user,
errors: api_errors,
action: action)
else
fail ::API::Errors::MultipleErrors.create_if_many(api_errors)
end
end
end
end
end

@ -32,8 +32,7 @@ module API
module Queries
class FormRepresenter < ::API::Decorators::Form
def payload_representer
# TODO: Flesh out
{}
QueryPayloadRepresenter.new(represented, current_user: current_user)
end
def schema_representer

@ -41,8 +41,10 @@ module API
mount API::V3::Queries::Operators::QueryOperatorsAPI
mount API::V3::Queries::Schemas::QuerySchemaAPI
mount API::V3::Queries::Schemas::QueryFilterInstanceSchemaAPI
mount API::V3::Queries::CreateFormAPI
helpers ::API::V3::Queries::Helpers::QueryRepresenterResponse
helpers ::API::V3::Queries::CreateQuery
helpers do
def authorize_by_policy(action, &block)
@ -99,6 +101,10 @@ module API
end
end
post do
create_query request_body, current_user
end
params do
requires :id, desc: 'Query id'
end

@ -0,0 +1,98 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'roar/decorator'
require 'roar/json/hal'
module API
module V3
module Queries
class QueryPayloadRepresenter < ::API::Decorators::Single
prepend QuerySerialization
links :columns do
represented.columns.map do |column|
{ href: api_v3_paths.query_column(convert_attribute(column.name)) }
end
end
link :groupBy do
column = represented.group_by_column
if column
{ href: api_v3_paths.query_group_by(convert_attribute(column.name)) }
else
{ href: nil }
end
end
links :sortBy do
represented.sort_criteria.map do |column, dir|
name = ::API::Utilities::PropertyNameConverter.from_ar_name column
{ href: api_v3_paths.query_sort_by(name, dir) }
end
end
linked_property :project
property :name
property :filters,
exec_context: :decorator,
getter: ->(*) { trimmed_filters filters }
property :display_sums, as: :sums
property :is_public, as: :public
private
##
# Uses the a normal query's filter representation and removes the bits
# we don't want for a payload.
def trimmed_filters(filters)
filters.map(&:to_hash).map { |v| trim_links v }
end
def trim_links(value)
if value.is_a? ::Hash
value.except("_type", "name", "title", "schema").map_values { |v| trim_links v }
elsif value.is_a? Array
value.map { |v| trim_links v }
else
value
end
end
def convert_attribute(attribute)
::API::Utilities::PropertyNameConverter.from_ar_name(attribute)
end
end
end
end
end

@ -36,6 +36,8 @@ module API
class QueryRepresenter < ::API::Decorators::Single
self_link
prepend QuerySerialization
attr_accessor :results,
:params
@ -93,7 +95,7 @@ module API
end
links :sortBy do
map_with_sort_by_as_decorated do |sort_by|
map_with_sort_by_as_decorated(represented.sort_criteria) do |sort_by|
{
href: api_v3_paths.query_sort_by(sort_by.converted_name, sort_by.direction_name),
title: sort_by.name
@ -113,11 +115,8 @@ module API
end
link :update do
href = if represented.project
api_v3_paths.query_project_form(represented.project.identifier)
else
api_v3_paths.query_form
end
href = api_v3_paths.query_form
{
href: href,
method: :post
@ -129,24 +128,12 @@ module API
property :id
property :name
property :filters,
exec_context: :decorator,
getter: ->(*) {
represented.filters.map do |filter|
::API::V3::Queries::Filters::QueryFilterInstanceRepresenter.new(filter)
end
}
property :public, getter: -> (*) { is_public }
property :filters, exec_context: :decorator
property :is_public, as: :public
property :sort_by,
exec_context: :decorator,
getter: ->(*) {
return unless represented.sort_criteria
map_with_sort_by_as_decorated do |sort_by|
::API::V3::Queries::SortBys::QuerySortByRepresenter.new(sort_by)
end
},
embedded: true,
if: ->(*) {
embed_links
@ -159,11 +146,6 @@ module API
property :columns,
exec_context: :decorator,
getter: ->(*) {
represented.columns.map do |column|
::API::V3::Queries::Columns::QueryColumnRepresenter.new(column)
end
},
embedded: true,
if: ->(*) {
embed_links
@ -171,13 +153,6 @@ module API
property :group_by,
exec_context: :decorator,
getter: ->(*) {
return unless represented.grouped?
column = represented.group_by_column
::API::V3::Queries::GroupBys::QueryGroupByRepresenter.new(column)
},
embedded: true,
if: ->(*) {
embed_links
@ -201,15 +176,6 @@ module API
::API::Utilities::PropertyNameConverter.from_ar_name(attribute)
end
def map_with_sort_by_as_decorated
represented.sort_criteria.map do |attribute, order|
decorated = ::API::V3::Queries::SortBys::SortByDecorator.new(attribute,
order)
yield decorated
end
end
def _type
'Query'
end

@ -0,0 +1,186 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module QuerySerialization
##
# Overriding this to initialize properties whose values depend on the "_links" attribute.
def from_hash(hash)
query = super
initialize_links! query, hash
query
end
def columns
represented.columns.map do |column|
::API::V3::Queries::Columns::QueryColumnRepresenter.new(column)
end
end
def filters
represented.filters.map do |filter|
::API::V3::Queries::Filters::QueryFilterInstanceRepresenter.new(filter)
end
end
def filters=(filters_hash)
represented.filters = []
filters_hash.each do |filter_attributes|
name = get_filter_name filter_attributes
operator = get_filter_operator filter_attributes
if name && operator
represented.add_filter name, operator, get_filter_values(filter_attributes)
else
raise API::Errors::InvalidRequestBody, "Could not read filter from: #{filter_attributes}"
end
end
end
def sort_by
return unless represented.sort_criteria
map_with_sort_by_as_decorated(represented.sort_criteria) do |sort_by|
::API::V3::Queries::SortBys::QuerySortByRepresenter.new(sort_by)
end
end
def group_by
return unless represented.grouped?
column = represented.group_by_column
::API::V3::Queries::GroupBys::QueryGroupByRepresenter.new(column)
end
module_function
def get_filter_name(filter_attributes)
href = filter_attributes.dig("_links", "filter", "href")
id = id_from_href "queries/filters", href
::API::Utilities::QueryFiltersNameConverter.to_ar_name id, refer_to_ids: true if id
end
def get_filter_operator(filter_attributes)
op_href = filter_attributes.dig("_links", "operator", "href")
id_from_href "queries/operators", op_href
end
def get_filter_values(filter_attributes)
filter_attributes["values"] ||
Array(filter_attributes.dig("_links", "values"))
.map { |value| id_from_href nil, value["href"] }
.compact
end
def initialize_links!(query, attributes)
query.project_id = get_project_id attributes
query.group_by = get_group_by attributes
query.column_names = get_columns attributes
if sort_criteria = get_sort_criteria(attributes)
query.sort_criteria = sort_criteria
end
end
def get_user_id(query_attributes)
href = query_attributes.dig("_links", "user", "href")
id_from_href "users", href
end
def get_project_id(query_attributes)
href = query_attributes.dig("_links", "project", "href")
id_from_href "projects", href
end
def get_sort_criteria(query_attributes)
criteria = Array(query_attributes.dig("_links", "sortBy")).map do |sort_by|
if id = id_from_href("queries/sort_bys", sort_by.href)
column, direction = id.split("-") # e.g. ["start_date", "desc"]
if column && direction
column = ::API::Utilities::PropertyNameConverter.to_ar_name(column, context: WorkPackage.new)
direction = nil unless ["asc", "desc"].include? direction
[column, direction]
end
end
end
criteria.compact.presence
end
def get_group_by(query_attributes)
href = query_attributes.dig "_links", "groupBy", "href"
attr = id_from_href "queries/group_bys", href
::API::Utilities::PropertyNameConverter.to_ar_name(attr, context: WorkPackage.new) if attr
end
def get_columns(query_attributes)
columns = Array(query_attributes.dig("_links", "columns")).map do |column|
name = id_from_href "queries/columns", column.href
::API::Utilities::PropertyNameConverter.to_ar_name(name, context: WorkPackage.new) if name
end
columns.map(&:to_sym).compact.presence
end
def id_from_href(expected_namespace, href)
return nil if href.blank?
::API::Utilities::ResourceLinkParser.parse_id(
href,
property: (expected_namespace && expected_namespace.split("/").last) || "filter_value",
expected_version: "3",
expected_namespace: expected_namespace
)
end
def map_with_sort_by_as_decorated(sort_criteria)
sort_criteria.map do |attribute, order|
decorated = ::API::V3::Queries::SortBys::SortByDecorator.new(attribute, order)
yield decorated
end
end
end
end
end
end

@ -37,7 +37,7 @@ module API
def merge_hash_into_work_package!(hash, work_package)
payload = ::API::V3::WorkPackages::WorkPackagePayloadRepresenter.create(work_package)
payload.from_hash(hash)
payload.from_hash(Hash(hash))
end
def write_work_package_attributes(work_package, request_body, reset_lock_version: false)

@ -45,6 +45,12 @@ module OpenProject
def dig(*keys)
keys.inject(self) { |hash, key| hash && (hash.is_a?(Hash) || nil) && hash[key] }
end
def map_values(&_block)
entries = map { |key, value| [key, (yield value)] }
::Hash[entries]
end
end
end
end

@ -0,0 +1,283 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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 "POST /api/v3/queries/form", type: :request do
include API::V3::Utilities::PathHelper
let(:path) { api_v3_paths.query_form }
let(:user) { FactoryGirl.create(:admin) }
let!(:project) { FactoryGirl.create(:project_with_types) }
let(:parameters) { {} }
let(:override_params) { {} }
let(:form) { JSON.parse response.body }
before do
login_as(user)
post path,
params: parameters.merge(override_params).to_json,
headers: { 'CONTENT_TYPE' => 'application/json' }
end
it 'should return 200(OK)' do
expect(response.status).to eq(200)
end
it 'should be of type form' do
expect(form["_type"]).to eq "Form"
end
it 'has the available_projects link for creation in the schema' do
expect(form.dig("_embedded", "schema", "project", "_links", "allowedValues", "href"))
.to eq "/api/v3/queries/available_projects"
end
describe 'with empty parameters' do
it 'has 1 validation error' do
expect(form.dig("_embedded", "validationErrors").size).to eq 1
end
it 'has a validation error on name' do
expect(form.dig("_embedded", "validationErrors", "name", "message")).to eq "Name can't be blank."
end
it 'has no commit link' do
expect(form.dig("_links", "commit")).to be_nil
end
end
describe 'with all minimum parameters' do
let(:parameters) do
{
name: "Some Query"
}
end
it 'has 0 validation errors' do
expect(form.dig("_embedded", "validationErrors")).to be_empty
end
it 'has the given name set' do
expect(form.dig("_embedded", "payload", "name")).to eq parameters[:name]
end
end
describe 'with all parameters given' do
let(:status) { FactoryGirl.create :status }
let(:parameters) do
{
name: "Some Query",
public: true,
sums: true,
filters: [
{
name: "Status",
_links: {
filter: {
href: "/api/v3/queries/filters/status"
},
operator: {
"href": "/api/v3/queries/operators/="
},
values: [
{
href: "/api/v3/statuses/#{status.id}",
}
]
}
}
],
_links: {
project: {
href: "/api/v3/projects/#{project.id}"
},
columns: [
{
href: "/api/v3/queries/columns/id"
},
{
href: "/api/v3/queries/columns/subject"
}
],
sortBy: [
{
href: "/api/v3/queries/sort_bys/id-desc"
},
{
href: "/api/v3/queries/sort_bys/assignee-asc"
}
],
groupBy: {
href: "/api/v3/queries/group_bys/assignee"
}
}
}
end
it 'has 0 validation errors' do
expect(form.dig("_embedded", "validationErrors")).to be_empty
end
it 'has a commit link' do
expect(form.dig("_links", "commit")).to be_present
end
it 'has the given name set' do
expect(form.dig("_embedded", "payload", "name")).to eq parameters[:name]
end
it 'has the project set' do
project_link = {
"href" => "/api/v3/projects/#{project.id}",
"title" => project.name
}
expect(form.dig("_embedded", "payload", "_links", "project")).to eq project_link
end
it 'is set to public' do
expect(form.dig("_embedded", "payload", "public")).to eq true
end
it 'has the filters set' do
filters = [
{
"_links" => {
"filter" => { "href" => "/api/v3/queries/filters/status" },
"operator" => { "href" => "/api/v3/queries/operators/=" },
"values" => [
{ "href" => "/api/v3/statuses/#{status.id}" }
]
}
}
]
expect(form.dig("_embedded", "payload", "filters")).to eq filters
end
it 'has the columns set' do
columns = [
{ "href" => "/api/v3/queries/columns/id" },
{ "href" => "/api/v3/queries/columns/subject" }
]
expect(form.dig("_embedded", "payload", "_links", "columns")).to eq columns
end
it 'has the groupBy set' do
group_by = { "href" => "/api/v3/queries/group_bys/assignee" }
expect(form.dig("_embedded", "payload", "_links", "groupBy")).to eq group_by
end
it 'has the columns set' do
sort_by = [
{ "href" => "/api/v3/queries/sort_bys/id-desc" },
{ "href" => "/api/v3/queries/sort_bys/assignee-asc" }
]
expect(form.dig("_embedded", "payload", "_links", "sortBy")).to eq sort_by
end
context "with an unknown filter" do
let(:override_params) do
filter = parameters[:filters][0]
filter[:_links][:filter][:href] = "/api/v3/queries/filters/statuz"
{ filters: [filter] }
end
it "returns a validation error" do
expect(form.dig("_embedded", "validationErrors", "base", "message")).to eq "Statuz does not exist."
end
end
context "with an unknown column" do
let(:override_params) do
column = { href: "/api/v3/queries/columns/wurst" }
links = parameters[:_links]
links[:columns] = links[:columns] + [column]
{ _links: links }
end
it "returns a validation error" do
expect(form.dig("_embedded", "validationErrors", "columnNames", "message"))
.to eq "Invalid query column: wurst"
end
end
context "with an invalid groupBy column" do
let(:override_params) do
column = { href: "/api/v3/queries/group_bys/foobar" }
links = parameters[:_links]
links[:groupBy] = column
{ _links: links }
end
it "returns a validation error" do
expect(form.dig("_embedded", "validationErrors", "groupBy", "message"))
.to eq "Can't group by: foobar"
end
end
context "with an invalid sort criterion" do
let(:override_params) do
sort_criterion = { href: "/api/v3/queries/sort_bys/spentTime-desc" }
links = parameters[:_links]
links[:sortBy] = links[:sortBy] + [sort_criterion]
{ _links: links }
end
it "returns a validation error" do
expect(form.dig("_embedded", "validationErrors", "sortCriteria", "message"))
.to eq "Can't sort by column: spent_hours"
end
end
context "with an unauthorized user trying to set the query public" do
let(:user) { FactoryGirl.create :user }
it "should reject the request" do
expect(form.dig("_embedded", "validationErrors", "public", "message"))
.to eq "Public - The user has no permission to create public queries."
end
end
end
end

@ -0,0 +1,173 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe "POST /api/v3/queries", type: :request do
let(:user) { FactoryGirl.create :admin }
let(:status) { FactoryGirl.create :status }
let(:project) { FactoryGirl.create :project }
let(:params) do
{
name: "Dummy Query",
filters: [
{
name: "Status",
_links: {
filter: {
href: "/api/v3/queries/filters/status"
},
operator: {
"href": "/api/v3/queries/operators/="
},
schema: {
"href": "/api/v3/queries/filter_instance_schemas/status"
},
values: [
{
href: "/api/v3/statuses/#{status.id}",
}
]
}
}
],
_links: {
project: {
href: "/api/v3/projects/#{project.id}"
},
columns: [
{
href: "/api/v3/queries/columns/id"
},
{
href: "/api/v3/queries/columns/subject"
},
{
href: "/api/v3/queries/columns/status"
},
{
href: "/api/v3/queries/columns/assignee"
}
],
sortBy: [
{
href: "/api/v3/queries/sort_bys/id-desc"
},
{
href: "/api/v3/queries/sort_bys/assignee-asc"
}
],
groupBy: {
href: "/api/v3/queries/group_bys/assignee"
}
}
}
end
before do
login_as user
end
describe "creating a query" do
before do
post "/api/v3/queries",
params: params.to_json,
headers: { "Content-Type": "application/json" }
end
it 'should return 201 (created)' do
expect(response.status).to eq(201)
end
it 'should render the created query' do
json = JSON.parse(response.body)
expect(json["_type"]).to eq "Query"
expect(json["name"]).to eq "Dummy Query"
end
it 'should create the query correctly' do
query = Query.first
expect(query.group_by_column.name).to eq :assigned_to
expect(query.sort_criteria).to eq [["id", "desc"], ["assigned_to", "asc"]]
expect(query.columns.map(&:name)).to eq [:id, :subject, :status, :assigned_to]
expect(query.user).to eq user
expect(query.project).to eq project
end
end
context "with invalid parameters" do
def post!
post "/api/v3/queries",
params: params.to_json,
headers: { "Content-Type": "application/json" }
end
def json
JSON.parse response.body
end
it "yields a 404 error given an unknown user" do
params[:_links][:user][:href] = "/api/v3/users/#{user.id}352"
post!
expect(response.status).to eq 404
expect(json["message"]).to eq "User #{user.id}352 not found"
end
it "yields a 404 error given an unknown project" do
params[:_links][:project][:href] = "/api/v3/projects/#{project.id}42"
post!
expect(response.status).to eq 404
expect(json["message"]).to eq "Project #{project.id}42 not found"
end
it "yields a 422 error given an unknown operator" do
params[:filters][0][:_links][:operator][:href] = "/api/v3/queries/operators/wut"
post!
expect(response.status).to eq 422
expect(json["message"]).to eq "Status Operator is not included in the list"
end
it "yields a 422 error given an unknown filter" do
params[:filters][0][:_links][:filter][:href] = "/api/v3/queries/filters/statuz"
post!
expect(response.status).to eq 422
expect(json["message"]).to eq "Statuz does not exist."
end
end
end
Loading…
Cancel
Save