Merge pull request #7893 from opf/bim/feature/bcf_api_topics_create

Bim/feature/bcf api topics create

[ci skip]
pull/7897/head
Oliver Günther 5 years ago committed by GitHub
commit 3fc6c302dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      app/contracts/work_packages/base_contract.rb
  2. 2
      app/contracts/work_packages/create_contract.rb
  3. 31
      app/models/priority/inexistent_priority.rb
  4. 31
      app/models/status/inexistent_status.rb
  5. 32
      app/models/type/inexistent_type.rb
  6. 31
      app/models/user/inexistent_user.rb
  7. 6
      app/services/work_packages/create_service.rb
  8. 2
      app/services/work_packages/set_attributes_service.rb
  9. 3
      config/locales/en.yml
  10. 2
      lib/api/errors/error_base.rb
  11. 4
      lib/api/errors/unwritable_property.rb
  12. 12
      lib/api/utilities/endpoints/modify.rb
  13. 43
      modules/bcf/app/contracts/bcf/issues/base_contract.rb
  14. 39
      modules/bcf/app/contracts/bcf/issues/create_contract.rb
  15. 50
      modules/bcf/app/controllers/bcf/api/v2_1/endpoints/create.rb
  16. 46
      modules/bcf/app/controllers/bcf/api/v2_1/endpoints/modify_mixin.rb
  17. 15
      modules/bcf/app/controllers/bcf/api/v2_1/endpoints/update.rb
  18. 19
      modules/bcf/app/controllers/bcf/api/v2_1/topics_api.rb
  19. 6
      modules/bcf/app/controllers/bcf/issues_controller.rb
  20. 126
      modules/bcf/app/models/bcf/issue.rb
  21. 64
      modules/bcf/app/representers/bcf/api/v2_1/errors/error_mapper.rb
  22. 4
      modules/bcf/app/representers/bcf/api/v2_1/errors/error_representer.rb
  23. 113
      modules/bcf/app/representers/bcf/api/v2_1/topics/single_representer.rb
  24. 51
      modules/bcf/app/services/bcf/issues/create_service.rb
  25. 34
      modules/bcf/app/services/bcf/issues/set_attributes_service.rb
  26. 162
      modules/bcf/app/services/bcf/issues/transform_attributes_service.rb
  27. 9
      modules/bcf/db/migrate/20191119144123_add_issue_columns.rb
  28. 3
      modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb
  29. 186
      modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb
  30. 2
      modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb
  31. 85
      modules/bcf/spec/api/v3/work_packages/work_package_representer_spec.rb
  32. 2
      modules/bcf/spec/bcf/bcf_xml/importer_spec.rb
  33. 13
      modules/bcf/spec/bcf/bcf_xml/issue_reader_spec.rb
  34. 6
      modules/bcf/spec/bcf/bcf_xml/issue_writer_spec.rb
  35. 45
      modules/bcf/spec/contracts/bcf/issues/create_contract_spec.rb
  36. 129
      modules/bcf/spec/contracts/bcf/issues/shared_contract_examples.rb
  37. 28
      modules/bcf/spec/factories/bcf_issue_factory.rb
  38. 27
      modules/bcf/spec/models/bcf/issue_spec.rb
  39. 53
      modules/bcf/spec/representers/bcf/api/v2_1/topics/single_representer_rendering_spec.rb
  40. 2
      modules/bcf/spec/requests/api/bcf/v2_1/projects_api_spec.rb
  41. 3
      modules/bcf/spec/requests/api/bcf/v2_1/shared_responses.rb
  42. 312
      modules/bcf/spec/requests/api/bcf/v2_1/topics_api_spec.rb
  43. 173
      modules/bcf/spec/services/bcf/issues/create_service_spec.rb
  44. 16
      modules/boards/spec/requests/api/v3/grids/grids_resource_spec.rb
  45. 2
      modules/boards/spec/requests/api/v3/grids/grids_update_form_resource_spec.rb
  46. 16
      modules/my_page/spec/requests/api/v3/grids/grids_resource_spec.rb
  47. 2
      modules/my_page/spec/requests/api/v3/grids/grids_update_form_resource_spec.rb
  48. 56
      spec/contracts/work_packages/base_contract_spec.rb
  49. 13
      spec/models/mail_handler_spec.rb
  50. 22
      spec/requests/api/v3/membership_resources_spec.rb
  51. 2
      spec/requests/api/v3/relations/relations_api_spec.rb
  52. 4
      spec/requests/api/v3/support/response_examples.rb
  53. 9
      spec/requests/api/v3/version_resource_spec.rb
  54. 4
      spec/requests/api/v3/work_package_resource_spec.rb
  55. 4
      spec/services/projects/set_attributes_service_spec.rb
  56. 4
      spec/support/matchers/be_html_eql.rb

@ -103,7 +103,9 @@ module WorkPackages
message: :greater_than_or_equal_to_start_date,
allow_blank: true },
unless: Proc.new { |wp| wp.start_date.blank? }
validate :validate_enabled_type
validate :validate_type_exists
validate :validate_milestone_constraint
validate :validate_parent_not_milestone
@ -112,13 +114,17 @@ module WorkPackages
validate :validate_parent_in_same_project
validate :validate_parent_not_subtask
validate :validate_status_exists
validate :validate_status_transition
validate :validate_active_priority
validate :validate_priority_exists
validate :validate_category
validate :validate_estimated_hours
validate :validate_assigned_to_exists
def initialize(work_package, user, options: {})
super
@ -149,11 +155,19 @@ module WorkPackages
def validate_enabled_type
# Checks that the issue can not be added/moved to a disabled type
if model.project && (model.type_id_changed? || model.project_id_changed?)
if type_context_changed?
errors.add :type_id, :inclusion unless model.project.types.include?(model.type)
end
end
def validate_assigned_to_exists
errors.add :assigned_to, :does_not_exist if model.assigned_to&.is_a?(User::InexistentUser)
end
def validate_type_exists
errors.add :type, :does_not_exist if type_inexistent?
end
def validate_milestone_constraint
if model.is_milestone? && model.due_date && model.start_date && model.start_date != model.due_date
errors.add :due_date, :not_start_date
@ -186,6 +200,10 @@ module WorkPackages
end
end
def validate_status_exists
errors.add :status, :does_not_exist if model.status&.is_a?(Status::InexistentStatus)
end
def validate_status_transition
if status_changed? && status_exists? && !(model.type_id_changed? || status_transition_exists?)
errors.add :status_id, :status_transition_invalid
@ -198,6 +216,10 @@ module WorkPackages
end
end
def validate_priority_exists
errors.add :priority, :does_not_exist if model.priority&.is_a?(Priority::InexistentPriority)
end
def validate_category
if inexistent_category?
errors.add :category, :does_not_exist
@ -287,5 +309,13 @@ module WorkPackages
query
end
end
def type_context_changed?
model.project && !type_inexistent? && (model.type_id_changed? || model.project_id_changed?)
end
def type_inexistent?
model.type.is_a?(Type::InexistentType)
end
end
end

@ -32,8 +32,6 @@ require 'work_packages/base_contract'
module WorkPackages
class CreateContract < BaseContract
# TODO: Think about whether this can be removed
# as it is unwriteable. So why bother checking for the correct author
attribute :author_id,
writeable: false do
errors.add :author_id, :invalid if model.author != user

@ -0,0 +1,31 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
class Priority::InexistentPriority < IssuePriority; end

@ -0,0 +1,31 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
class Status::InexistentStatus < Status; end

@ -0,0 +1,32 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
class Type::InexistentType < Type
end

@ -0,0 +1,31 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
class User::InexistentUser < User; end

@ -74,7 +74,7 @@ class WorkPackages::CreateService
end
def set_attributes(attributes, wp)
WorkPackages::SetAttributesService
attributes_service_class
.new(user: user,
model: wp,
contract_class: contract_class)
@ -96,4 +96,8 @@ class WorkPackages::CreateService
result
end
def attributes_service_class
::WorkPackages::SetAttributesService
end
end

@ -177,7 +177,7 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes
end
def reassign_status(available_statuses)
return if available_statuses.include? work_package.status
return if available_statuses.include?(work_package.status) || work_package.status.is_a?(Status::InexistentStatus)
new_status = available_statuses.detect(&:is_default) || available_statuses.first
work_package.status = new_status if new_status.present?

@ -497,7 +497,7 @@ en:
could_not_be_copied: "could not be (fully) copied."
does_not_exist: "does not exist."
error_unauthorized: "may not be accessed."
error_readonly: "is not writable."
error_readonly: "was attempted to be written but is not writable."
empty: "can't be empty."
even: "must be even."
exclusion: "is reserved."
@ -2652,7 +2652,6 @@ en:
estimated_hours: "Estimated hours cannot be set on parent work packages."
invalid_user_assigned_to_work_package: "The chosen user is not allowed to be '%{property}' for this work package."
start_date: "Start date cannot be set on parent work packages."
writing_read_only_attributes: "You must not write a read-only attribute."
resources:
schema: 'Schema'

@ -89,7 +89,7 @@ module API
api_attribute_name = ::API::Utilities::PropertyNameConverter.from_ar_name(attribute)
errors.symbols_and_messages_for(attribute).each do |symbol, full_message, _|
api_errors << if symbol == :error_readonly
::API::Errors::UnwritableProperty.new(api_attribute_name)
::API::Errors::UnwritableProperty.new(api_attribute_name, full_message)
else
::API::Errors::Validation.new(api_attribute_name, full_message)
end

@ -33,8 +33,8 @@ module API
identifier 'urn:openproject-org:api:v3:errors:PropertyIsReadOnly'
code 422
def initialize(property)
super I18n.t('api_v3.errors.writing_read_only_attributes')
def initialize(property, message)
super message
@property = property
@details = { attribute: property }

@ -43,12 +43,16 @@ module API
end
def present_error(call)
errors = call.errors
errors = merge_dependent_errors call if errors.empty?
api_errors = [::API::Errors::ErrorBase.create_and_merge_errors(postprocess_errors(call))]
api_errors = [::API::Errors::ErrorBase.create_and_merge_errors(errors)]
fail(::API::Errors::MultipleErrors
.create_if_many(api_errors))
end
fail ::API::Errors::MultipleErrors.create_if_many(api_errors)
def postprocess_errors(call)
errors = call.errors
errors = merge_dependent_errors call if errors.empty?
errors
end
def merge_dependent_errors(call)

@ -0,0 +1,43 @@
#-- 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 Bcf::Issues
class BaseContract < ::ModelContract
attribute :uuid
attribute :work_package
attribute :index
def validate
validate_user_allowed_to_manage
super
end
end
end

@ -0,0 +1,39 @@
#-- 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 Bcf::Issues
class CreateContract < BaseContract
private
def validate_user_allowed_to_manage
unless model.project && user.allowed_to?(:manage_bcf, model.project)
errors.add :base, :error_unauthorized
end
end
end
end

@ -0,0 +1,50 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
module Bcf::API::V2_1::Endpoints
class Create < API::Utilities::Endpoints::Create
include ModifyMixin
def present_success(_current_user, call)
render_representer
.new(call.result)
end
def postprocess_errors(call)
Bcf::API::V2_1::Errors::ErrorMapper.map(super)
end
private
def deduce_process_service
"::Bcf::#{deduce_backend_namespace}::#{update_or_create}Service".constantize
end
end
end

@ -0,0 +1,46 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
module Bcf::API::V2_1::Endpoints
module ModifyMixin
private
def deduce_parse_service
Bcf::API::V2_1::ParseResourceParamsService
end
def deduce_in_and_out_representer
"::Bcf::API::V2_1::#{deduce_api_namespace}::SingleRepresenter".constantize
end
alias_method :deduce_parse_representer, :deduce_in_and_out_representer
alias_method :deduce_render_representer, :deduce_in_and_out_representer
end
end

@ -30,22 +30,11 @@
module Bcf::API::V2_1::Endpoints
class Update < API::Utilities::Endpoints::Update
include ModifyMixin
def present_success(_current_user, call)
render_representer
.new(call.result)
end
private
def deduce_parse_service
Bcf::API::V2_1::ParseResourceParamsService
end
def deduce_in_and_out_representer
"::Bcf::API::V2_1::#{deduce_api_namespace}::SingleRepresenter".constantize
end
alias_method :deduce_parse_representer, :deduce_in_and_out_representer
alias_method :deduce_render_representer, :deduce_in_and_out_representer
end
end

@ -47,6 +47,25 @@ module Bcf::API::V2_1
scope: -> { topics })
.mount
post &::Bcf::API::V2_1::Endpoints::Create
.new(model: Bcf::Issue,
api_name: 'Topics',
params_modifier: ->(attributes) {
attributes[:project_id] = @project.id
wp_attributes = Bcf::Issues::TransformAttributesService
.new
.call(attributes)
.result
attributes
.slice(:stage,
:index,
:labels)
.merge(wp_attributes)
})
.mount
route_param :uuid, regexp: /\A[a-f0-9\-]+\z/ do
after_validation do
@issue = topics.find_by_uuid!(params[:uuid])

@ -141,9 +141,9 @@ module ::Bcf
raise(StandardError.new(I18n.t('bcf.exceptions.file_invalid')))
end
@issues = ::Bcf::Issue.with_markup
.includes(work_package: %i[status priority assigned_to])
.where(uuid: @listing.map { |e| e[:uuid] }, project: @project)
@issues = ::Bcf::Issue
.includes(work_package: %i[status priority assigned_to])
.where(uuid: @listing.map { |e| e[:uuid] }, project: @project)
render 'bcf/issues/diff_on_work_packages'
end

@ -9,120 +9,14 @@ module Bcf
after_update :invalidate_markup_cache
class << self
def with_markup
select '*',
extract_first_node(title_path, 'title'),
extract_first_node(description_path, 'description'),
extract_first_node(priority_text_path, 'priority_text'),
extract_first_node(status_text_path, 'status_text'),
extract_first_node(type_text_path, 'type_text'),
extract_first_node(assignee_text_path, 'assignee_text'),
extract_first_node(due_date_text_path, 'due_date_text'),
extract_first_node(creation_date_text_path, 'creation_date_text'),
extract_first_node(creation_author_text_path, 'creation_author_text'),
extract_first_node(modified_date_text_path, 'modified_date_text'),
extract_first_node(modified_author_text_path, 'modified_author_text'),
extract_first_node(index_text_path, 'index_text'),
extract_first_node(stage_text_path, 'stage_text'),
extract_nodes(labels_path, 'labels')
end
validates :work_package, presence: true
class << self
def of_project(project)
includes(:work_package)
.references(:work_packages)
.merge(WorkPackage.for_projects(project))
end
protected
def title_path
'/Markup/Topic/Title/text()'
end
def description_path
'/Markup/Topic/Description/text()'
end
def priority_text_path
'/Markup/Topic/Priority/text()'
end
def status_text_path
'/Markup/Topic/@TopicStatus'
end
def type_text_path
'/Markup/Topic/@TopicType'
end
def assignee_text_path
'/Markup/Topic/AssignedTo/text()'
end
def due_date_text_path
'/Markup/Topic/DueDate/text()'
end
def stage_text_path
'/Markup/Topic/Stage/text()'
end
def creation_date_text_path
'/Markup/Topic/CreationDate/text()'
end
def creation_author_text_path
'/Markup/Topic/CreationAuthor/text()'
end
def modified_date_text_path
'/Markup/Topic/ModifiedDate/text()'
end
def modified_author_text_path
'/Markup/Topic/ModifiedAuthor/text()'
end
def index_text_path
'/Markup/Topic/Index/text()'
end
def labels_path
'/Markup/Topic/Labels/text()'
end
private
def extract_first_node(path, as)
"(xpath('#{path}', markup))[1] AS #{as}"
end
def extract_nodes(path, as)
"(xpath('#{path}', markup)) AS #{as}"
end
end
%i[title
description
priority_text
status_text
type_text
assignee_text
due_date_text
creation_date_text
creation_author_text
modified_date_text
modified_author_text
stage_text
index_text].each do |name|
define_method name do
from_attributes_or_doc name
end
end
def labels
from_attributes_or_doc :labels, multiple: true
end
def markup_doc
@ -132,21 +26,5 @@ module Bcf
def invalidate_markup_cache
@markup_doc = nil
end
private
def from_attributes_or_doc(key, multiple: false)
if attributes.keys.include? key.to_s
self[key]
else
path = markup_doc.xpath(self.class.send("#{key}_path"))
if multiple
path.map(&:to_s)
else
path.first.to_s.presence
end
end
end
end
end

@ -0,0 +1,64 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
module Bcf::API::V2_1::Errors
class ErrorMapper
extend ActiveModel::Naming
extend ActiveModel::Translation
def read_attribute_for_validation(_attr)
nil
end
def self.lookup_ancestors
[::Bcf::Issue]
end
def self.map(original_errors)
mapped_errors = ActiveModel::Errors.new(new)
original_errors.send(:error_symbols).each do |key, errors|
errors.map(&:first).each do |error|
mapped_errors.add(error_key_mapper(key), error)
end
end
mapped_errors
end
def self.i18n_scope
:activerecord
end
def self.error_key_mapper(key)
{ subject: :title }[key] || key
end
end
end

@ -31,7 +31,9 @@
module Bcf::API::V2_1::Errors
class ErrorRepresenter < BaseRepresenter
property :message,
getter: ->(*) { message },
getter: ->(*) {
[message].concat(Array(errors).map(&:message)).compact.join(' ')
},
render_nil: true
end
end

@ -35,45 +35,114 @@ module Bcf::API::V2_1
property :uuid,
as: :guid
property :type_text,
as: :topic_type
property :type,
as: :topic_type,
getter: ->(*) {
work_package
.type
.name
}
property :status_text,
as: :topic_status
property :status,
as: :topic_status,
getter: ->(*) {
work_package
.status
.name
}
property :priority,
as: :priority,
getter: ->(*) {
work_package
.priority
.name
}
property :reference_links,
getter: ->(decorator:, **) {
[decorator.api_v3_paths.work_package(work_package.id)]
}
property :title
property :title,
getter: ->(*) {
work_package.subject
}
property :index_text,
as: :index
property :index
property :labels
property :creation_date_text,
as: :creation_date
property :creation_date,
getter: ->(decorator:, **) {
decorator
.formatted_date_time(:created_at)
}
property :creation_author_text,
as: :creation_author
property :creation_author,
getter: ->(*) {
work_package
.author
.mail
}
property :modified_date_text,
as: :modified_date
property :modified_date,
getter: ->(decorator:, **) {
decorator
.formatted_date_time(:updated_at)
}
property :modified_author_text,
as: :modified_author
property :modified_author,
getter: ->(*) {
work_package
.journals
.max_by(&:version)
.user
.mail
}
property :assignee,
as: :assigned_to,
getter: ->(decorator:, **) {
decorator
.assigned_to
&.mail
}
property :stage
property :assignee_text,
as: :assigned_to
property :description,
getter: ->(*) {
work_package.description
}
property :due_date,
getter: ->(decorator:, **) {
decorator.datetime_formatter.format_date(work_package.due_date, allow_nil: true)
},
setter: ->(fragment:, decorator:, **) {
date = decorator
.datetime_formatter
.parse_date(fragment,
due_date,
allow_nil: true)
self.due_date = date
}
property :stage_text,
as: :stage
def datetime_formatter
::API::V3::Utilities::DateTimeFormatter
end
property :description
def formatted_date_time(method)
datetime_formatter
.format_datetime(represented.work_package.send(method), allow_nil: true)
end
property :due_date_text,
as: :due_date
def assigned_to
represented
.work_package
.assigned_to
end
end
end

@ -0,0 +1,51 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2019 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 Bcf::Issues
class CreateService < ::BaseServices::Create
private
def before_perform(params)
wp_call = WorkPackages::CreateService
.new(user: user)
.call(params)
if wp_call.success?
issue_params = {
work_package: wp_call.result
}.merge(params.slice(:stage, :labels, :index))
super(issue_params)
else
wp_call
end
end
end
end

@ -0,0 +1,34 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2019 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 Bcf::Issues
class SetAttributesService < ::BaseServices::SetAttributes
end
end

@ -0,0 +1,162 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2019 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 Bcf::Issues
class TransformAttributesService
def call(attributes)
ServiceResult.new success: true,
result: work_package_attributes(attributes)
end
private
##
# BCF issues might have empty titles. OP needs one.
def title(attributes)
if attributes[:title]
attributes[:title]
elsif attributes[:import_options]
'(Imported BCF issue contained no title)'
end
end
def author(project, attributes)
find_user_in_project(project, attributes[:author]) || User.system
end
def assignee(project, attributes)
assignee = find_user_in_project(project, attributes[:assignee])
return assignee if assignee.present?
missing_assignee(attributes[:assignee], attributes[:import_options] || {})
end
##
# Try to find the given user by mail in the project
def find_user_in_project(project, mail)
project.users.find_by(mail: mail)
end
def type(project, attributes)
type_name = attributes[:type]
type = project.types.find_by(name: type_name)
return type if type.present?
missing_type(project, type_name, attributes[:import_options] || {})
end
##
# Handle unknown statuses during import
def status(attributes)
status_name = attributes[:status]
status = ::Status.find_by(name: status_name)
return status if status.present?
missing_status(status_name, attributes[:import_options] || {})
end
##
# Handle unknown priorities during import
def priority(attributes)
priority_name = attributes[:priority]
priority = ::IssuePriority.find_by(name: priority_name)
return priority if priority.present?
missing_priority(priority_name, attributes[:import_options] || {})
end
##
# Get mapped and raw attributes from MarkupExtractor
# and return all values that are non-nil
def work_package_attributes(attributes)
project = Project.find(attributes[:project_id])
{
# Fixed attributes we know
project: project,
type: type(project, attributes),
# Native attributes from the extractor
subject: title(attributes),
description: attributes[:description],
due_date: attributes[:due_date],
start_date: attributes[:start_date],
# Mapped attributes
assigned_to: assignee(project, attributes),
status: status(attributes),
priority: priority(attributes)
}.compact
end
def missing_status(status_name, import_options)
if import_options[:unknown_statuses_action] == 'use_default'
::Status.default
elsif import_options[:unknown_statuses_action] == 'chose' &&
import_options[:unknown_statuses_chose_ids].any?
::Status.find_by(id: import_options[:unknown_statuses_chose_ids].first)
elsif status_name
Status::InexistentStatus.new
end
end
def missing_priority(priority_name, import_options)
if import_options[:unknown_priorities_action] == 'use_default'
# NOP The 'use_default' case gets already covered by OP.
elsif import_options[:unknown_priorities_action] == 'chose' &&
import_options[:unknown_priorities_chose_ids].any?
::IssuePriority.find_by(id: import_options[:unknown_priorities_chose_ids].first)
elsif priority_name
Priority::InexistentPriority.new
end
end
def missing_type(project, type_name, import_options)
if import_options[:unknown_types_action] == 'use_default'
project.types.default&.first
elsif import_options[:unknown_types_action] == 'chose' &&
import_options[:unknown_types_chose_ids].any?
project.types.find_by(id: import_options[:unknown_types_chose_ids].first)
elsif type_name
Type::InexistentType.new
end
end
def missing_assignee(assignee_name, import_options)
if import_options[:invalid_people_action] != 'anonymize' && assignee_name
User::InexistentUser.new
end
end
end
end

@ -0,0 +1,9 @@
class AddIssueColumns < ActiveRecord::Migration[6.0]
def change
change_table :bcf_issues do |i|
i.string :stage
i.integer :index
i.text :labels, array: true, default: []
end
end
end

@ -155,8 +155,7 @@ module OpenProject::Bcf::BcfXml
zip,
entry,
current_user: current_user,
import_options: import_options,
aggregations: aggregations).extract!
import_options: import_options).extract!
if issue.errors.blank?
issue.save

@ -6,10 +6,10 @@ require_relative 'file_entry'
module OpenProject::Bcf::BcfXml
class IssueReader
attr_reader :zip, :entry, :issue, :extractor, :project, :user, :import_options, :aggregations
attr_reader :zip, :entry, :issue, :extractor, :project, :user, :import_options
attr_accessor :wp_last_updated_at, :is_update
def initialize(project, zip, entry, current_user:, import_options:, aggregations:)
def initialize(project, zip, entry, current_user:, import_options:)
@zip = zip
@entry = entry
@project = project
@ -17,23 +17,13 @@ module OpenProject::Bcf::BcfXml
@issue = find_or_initialize_issue
@extractor = MarkupExtractor.new(entry)
@import_options = import_options
@aggregations = aggregations
@doc = nil
@wp_last_updated_at = nil
@is_update = false
end
def extract!
@doc = extractor.doc
markup = extractor.doc.to_xml(indent: 2)
treat_empty_titles
treat_unknown_types
treat_unknown_statuses
treat_unknown_priorities
extractor.doc = @doc
markup = @doc.to_xml(indent: 2)
issue.markup = markup
extractor.markup = markup
@ -51,82 +41,6 @@ module OpenProject::Bcf::BcfXml
private
##
# BCF issues might have empty titles. OP needs one.
def treat_empty_titles
title_node = @doc.xpath('/Markup/Topic/Title').first
return if title_node&.content&.present?
title_node.content = "(Imported BCF issue contained no title)"
end
##
# Handle unknown types during import
def treat_unknown_types
if aggregations.unknown_types.present?
if import_options[:unknown_types_action] == 'use_default'
replace_type_with(::Type.default.first&.name)
elsif import_options[:unknown_types_action] == 'chose' && import_options[:unknown_types_chose_ids].any?
replace_type_with(::Type.find_by(id: import_options[:unknown_types_chose_ids].first)&.name)
else
raise StandardError.new 'Unknown topic type found in import. Use an existing type name.'
end
end
end
def replace_type_with(new_type_name)
raise StandardError.new "New type name can't be blank." unless new_type_name.present?
@doc.xpath('/Markup/Topic').first.set_attribute('TopicType', new_type_name)
end
##
# Handle unknown statuses during import
def treat_unknown_statuses
if aggregations.unknown_statuses.present?
if import_options[:unknown_statuses_action] == 'use_default'
replace_status_with(::Status.default&.name)
elsif import_options[:unknown_statuses_action] == 'chose' && import_options[:unknown_statuses_chose_ids].any?
replace_status_with(::Status.find_by(id: import_options[:unknown_statuses_chose_ids].first)&.name)
else
raise StandardError.new 'Unknown topic status found in import. Use an existing status name.'
end
end
end
def replace_status_with(new_status_name)
raise StandardError.new "New status name can't be blank." unless new_status_name.present?
@doc.xpath('/Markup/Topic').first.set_attribute('TopicStatus', new_status_name)
end
##
# Handle unknown priorities during import
def treat_unknown_priorities
if aggregations.unknown_priorities.present?
if import_options[:unknown_priorities_action] == 'use_default'
# NOP The 'use_default' case gets already covered by OP.
elsif import_options[:unknown_priorities_action] == 'chose' && import_options[:unknown_priorities_chose_ids].any?
replace_priorities_with(::IssuePriority.find_by(id: import_options[:unknown_priorities_chose_ids].first)&.name)
else
raise StandardError.new 'Unknown topic priority found in import. Use an existing priority name.'
end
end
end
def replace_priorities_with(new_priority_name)
raise StandardError.new "New priority name can't be blank." unless new_priority_name.present?
priority_node = @doc.xpath('/Markup/Topic/Priority').first
if priority_node
priority_node.content = new_priority_name
else
# Valid BCF XML Topics must have a Title node. So we can add the Priority node just behind it and thus,
# maintain the schema's sequence compliance.
@doc.at('/Markup/Topic/Title').after("<Priority>#{new_priority_name}</Priority>")
end
end
def synchronize_with_work_package
self.is_update = issue.work_package.present?
self.wp_last_updated_at = issue.work_package&.updated_at
@ -152,13 +66,9 @@ module OpenProject::Bcf::BcfXml
end
def create_work_package
call = WorkPackages::CreateService.new(user: user).call(work_package_attributes
.merge(send_notifications: false)
.symbolize_keys)
call = WorkPackages::CreateService.new(user: user).call(work_package_attributes)
if call.success?
force_overwrite(call.result)
end
force_overwrite(call.result) if call.success?
call
end
@ -167,62 +77,42 @@ module OpenProject::Bcf::BcfXml
find_user_in_project(extractor.author) || User.system
end
def assignee
find_user_in_project(extractor.assignee)
end
def type
type_name = extractor.type
type = ::Type.find_by(name: type_name)
return type if type.present?
return ::Type.default&.first if import_options[:unknown_types_action] == 'default'
if import_options[:unknown_types_action] == 'chose' &&
import_options[:unknown_types_chose_ids].any?
return ::Type.find_by(id: import_options[:unknown_types_chose_ids].first)
else
ServiceResult.new success: false,
errors: issue.errors,
result: issue
end
end
def start_date
extractor.creation_date.to_date unless is_update
end
def update_work_package
if import_is_newer?
WorkPackages::UpdateService
.new(user: user, model: issue.work_package)
.call(work_package_attributes.merge(send_notifications: false).symbolize_keys)
.call(work_package_attributes)
else
import_is_outdated(issue)
end
end
##
# Get mapped and raw attributes from MarkupExtractor
# and return all values that are non-nil
###
## Get mapped and raw attributes from MarkupExtractor
## and return all values that are non-nil
def work_package_attributes
{
# Fixed attributes we know
project: project,
type: type,
# Native attributes from the extractor
subject: extractor.title,
description: extractor.description,
due_date: extractor.due_date,
start_date: start_date,
# Mapped attributes
assigned_to: assignee,
status_id: statuses.fetch(extractor.status, statuses[:default]),
priority_id: priorities.fetch(extractor.priority, priorities[:default])
}.compact
attributes = ::Bcf::Issues::TransformAttributesService
.new
.call(extractor_attributes.merge(import_options: import_options))
.result
.merge(send_notifications: false)
.symbolize_keys
attributes[:start_date] = extractor.creation_date.to_date unless is_update
attributes
end
def extractor_attributes
attributes = {
project_id: project.id
}
%i(type title description due_date assignee status priority).each do |key|
attributes[key] = extractor.send(key)
end
attributes
end
##
@ -400,22 +290,10 @@ module OpenProject::Bcf::BcfXml
def update_journal_attributes(bcf_comment, comment_data)
bcf_comment.journal.update(notes: comment_data[:comment],
created_at: comment_data[:modified_date])
created_at: comment_data[:modified_date])
bcf_comment.journal.save
end
##
# Keep a hash map of current status ids for faster lookup
def statuses
@statuses ||= Hash[Status.pluck(:name, :id)].merge(default: Status.default.id)
end
##
# Keep a hash map of current status ids for faster lookup
def priorities
@priorities ||= Hash[IssuePriority.pluck(:name, :id)].merge(default: IssuePriority.default.try(:id))
end
def import_is_outdated(issue)
issue.errors.add :base,
:conflict,

@ -250,7 +250,7 @@ module OpenProject::Bcf::BcfXml
def find_or_initialize_issue
::Bcf::Issue.find_or_initialize_by(work_package: work_package)
end
def to_bcf_datetime(date_time)
date_time.utc.iso8601
end

@ -31,91 +31,12 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
member_in_project: project,
member_through_role: role)
end
let(:markup) do
<<-MARKUP
<Markup xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Header>
<File IfcProject="0M6o7Znnv7hxsbWgeu7oQq" IfcSpatialStructureElement="23B$bNeGHFQuMYJzvUX0FD" isExternal="false">
<Filename>IfcPile_01.ifc</Filename>
<Date>2014-10-27T16:27:27Z</Date>
<Reference>../IfcPile_01.ifc</Reference>
</File>
</Header>
<Topic Guid="63E78882-7C6A-4BF7-8982-FC478AFB9C97" TopicType="Structural" TopicStatus="Open">
<ReferenceLink>https://bim--it.net</ReferenceLink>
<Title>Maximum Content</Title>
<Priority>High</Priority>
<Index>0</Index>
<Labels>Structural</Labels>
<Labels>IT Development</Labels>
<CreationDate>2015-06-21T12:00:00Z</CreationDate>
<CreationAuthor>mike@example.com</CreationAuthor>
<ModifiedDate>2015-06-21T14:22:47Z</ModifiedDate>
<ModifiedAuthor>mike@example.com</ModifiedAuthor>
<AssignedTo>andy@example.com</AssignedTo>
<Description>This is a topic with all information present.</Description>
<BimSnippet SnippetType="JSON">
<Reference>JsonElement.json</Reference>
<ReferenceSchema>http://json-schema.org</ReferenceSchema>
</BimSnippet>
<DocumentReference isExternal="true">
<ReferencedDocument>https://github.com/BuildingSMART/BCF-XML</ReferencedDocument>
<Description>GitHub BCF Specification</Description>
</DocumentReference>
<DocumentReference>
<ReferencedDocument>../markup.xsd</ReferencedDocument>
<Description>Markup.xsd Schema</Description>
</DocumentReference>
<RelatedTopic Guid="5019D939-62A4-45D9-B205-FAB602C98FE8" />
</Topic>
<Comment Guid="780FAE52-C432-42BE-ADEA-FF3E7A8CD8E1">
<Date>2015-08-31T12:40:17Z</Date>
<Author>mike@example.com</Author>
<Comment>This is an unmodified topic at the uppermost hierarchical level.
All times in the XML are marked as UTC times.</Comment>
</Comment>
<Comment Guid="897E4909-BDF3-4CC7-A283-6506CAFF93DD">
<Date>2015-08-31T14:00:01Z</Date>
<Author>mike@example.com</Author>
<Comment>This comment was a reply to the first comment in BCF v2.0. This is a no longer supported functionality and therefore is to be treated as a regular comment in v2.1.</Comment>
</Comment>
<Comment Guid="39C4B780-1B48-44E5-9802-D359007AA44E">
<Date>2015-08-31T13:07:11Z</Date>
<Author>mike@example.com</Author>
<Comment>This comment again is in the highest hierarchy level.
It references a viewpoint.</Comment>
<Viewpoint Guid="8dc86298-9737-40b4-a448-98a9e953293a" />
</Comment>
<Comment Guid="BD17158C-4267-4433-98C1-904F9B41CA50">
<Date>2015-08-31T15:42:58Z</Date>
<Author>mike@example.com</Author>
<Comment>This comment contained some spllng errs.
Hopefully, the modifier did catch them all.</Comment>
<ModifiedDate>2015-08-31T16:07:11Z</ModifiedDate>
<ModifiedAuthor>mike@example.com</ModifiedAuthor>
</Comment>
<Viewpoints Guid="8dc86298-9737-40b4-a448-98a9e953293a">
<Viewpoint>Viewpoint_8dc86298-9737-40b4-a448-98a9e953293a.bcfv</Viewpoint>
<Snapshot>Snapshot_8dc86298-9737-40b4-a448-98a9e953293a.png</Snapshot>
</Viewpoints>
<Viewpoints Guid="21dd4807-e9af-439e-a980-04d913a6b1ce">
<Viewpoint>Viewpoint_21dd4807-e9af-439e-a980-04d913a6b1ce.bcfv</Viewpoint>
<Snapshot>Snapshot_21dd4807-e9af-439e-a980-04d913a6b1ce.png</Snapshot>
</Viewpoints>
<Viewpoints Guid="81daa431-bf01-4a49-80a2-1ab07c177717">
<Viewpoint>Viewpoint_81daa431-bf01-4a49-80a2-1ab07c177717.bcfv</Viewpoint>
<Snapshot>Snapshot_81daa431-bf01-4a49-80a2-1ab07c177717.png</Snapshot>
</Viewpoints>
</Markup>
MARKUP
end
let(:bcf_issue) do
FactoryBot.create(:bcf_issue_with_comment, markup: markup)
let!(:bcf_issue) do
FactoryBot.create(:bcf_issue_with_comment, work_package: work_package)
end
let(:work_package) do
FactoryBot.create(:work_package,
project_id: project.id,
bcf_issue: bcf_issue)
project_id: project.id)
end
let(:representer) do
described_class.new(work_package,

@ -63,7 +63,7 @@ describe ::OpenProject::Bcf::BcfXml::Importer do
workflow
priority
bcf_manager_member
allow(User).to receive(:current).and_return(bcf_manager)
login_as(bcf_manager)
end
describe '#to_listing' do

@ -88,15 +88,13 @@ describe ::OpenProject::Bcf::BcfXml::IssueReader do
end
let(:entry_stream) { StringIO.new(markup) }
let(:import_options) { OpenProject::Bcf::BcfXml::Importer::DEFAULT_IMPORT_OPTIONS }
let(:aggregations) { OpenProject::Bcf::BcfXml::Aggregations.new([], project) }
subject do
described_class.new(project,
nil,
entry,
current_user: bcf_manager,
import_options: import_options,
aggregations: aggregations)
import_options: import_options)
end
before do
@ -134,12 +132,6 @@ describe ::OpenProject::Bcf::BcfXml::IssueReader do
end
context 'with no import options provided' do
let(:aggregations) do
Struct
.new(:unknown_statuses, :unknown_types, :unknown_priorities)
.new([nil], [nil], [nil])
end
let(:bcf_issue) { subject.extract! }
it 'sets a status' do
@ -160,7 +152,8 @@ describe ::OpenProject::Bcf::BcfXml::IssueReader do
context 'on updating import' do
context '#update_comment' do
let!(:bcf_issue) { FactoryBot.create :bcf_issue_with_comment }
let(:work_package) { FactoryBot.create(:work_package) }
let!(:bcf_issue) { FactoryBot.create :bcf_issue_with_comment, work_package: work_package }
before do
allow(subject).to receive(:issue).and_return(bcf_issue)

@ -84,16 +84,16 @@ describe ::OpenProject::Bcf::BcfXml::IssueWriter do
end
let(:bcf_issue) do
FactoryBot.create(:bcf_issue_with_comment,
work_package: work_package,
markup: markup)
end
let(:priority) { FactoryBot.create :priority_low }
let(:current_user) { FactoryBot.create(:user) }
let(:due_date) { DateTime.now }
let(:type) { FactoryBot.create :type, name: 'Issue'}
let(:type) { FactoryBot.create :type, name: 'Issue' }
let(:work_package) do
FactoryBot.create(:work_package,
project_id: project.id,
bcf_issue: bcf_issue,
priority: priority,
author: current_user,
assigned_to: current_user,
@ -104,7 +104,7 @@ describe ::OpenProject::Bcf::BcfXml::IssueWriter do
before do
allow(User).to receive(:current).and_return current_user
work_package.bcf_issue.comments.first.journal.update_attribute('journable_id', work_package.id)
bcf_issue.comments.first.journal.update_attribute('journable_id', work_package.id)
FactoryBot.create(:work_package_journal, notes: "Some note created in OP.", journable_id: work_package.id)
work_package.reload
end

@ -0,0 +1,45 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require_relative './shared_contract_examples'
describe Bcf::Issues::CreateContract do
it_behaves_like 'issues contract' do
let(:issue) do
Bcf::Issue.new(uuid: issue_uuid,
work_package: issue_work_package,
stage: issue_stage,
index: issue_index,
labels: issue_labels)
end
let(:permissions) { [:manage_bcf] }
subject(:contract) { described_class.new(issue, current_user) }
end
end

@ -0,0 +1,129 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
shared_examples_for 'issues contract' do
let(:current_user) do
FactoryBot.build_stubbed(:user)
end
let!(:allowed_to) do
allow(current_user)
.to receive(:allowed_to?) do |permission, permission_project|
permissions.include?(permission) && project == permission_project
end
end
let(:issue_uuid) { 'issue uuid' }
let(:project) { FactoryBot.build_stubbed(:project) }
let(:issue_work_package) { FactoryBot.build_stubbed(:stubbed_work_package, project: project) }
let(:issue_work_package_id) do
id = 5
allow(WorkPackage)
.to receive(:find)
.with(id)
.and_return(issue_work_package)
id
end
let(:issue_stage) { nil }
let(:issue_labels) { [] }
let(:issue_index) { 8 }
before do
allow(issue)
.to receive(:project)
.and_return(project)
end
def expect_valid(valid, symbols = {})
expect(contract.validate).to eq(valid)
symbols.each do |key, arr|
expect(contract.errors.symbols_for(key)).to match_array arr
end
end
shared_examples 'is valid' do
it 'is valid' do
expect_valid(true)
end
end
it_behaves_like 'is valid'
context 'if the uuid is nil' do
let(:issue_uuid) { nil }
it_behaves_like 'is valid' # as the uuid will be set
end
context 'if the work_package_id is nil' do
let(:issue_work_package) { nil }
it 'is invalid' do
expect_valid(false, work_package: %i(blank))
end
end
context 'if the user lacks permission' do
let(:permissions) { [] }
it 'is invalid' do
expect_valid(false, base: %i(error_unauthorized))
end
end
context 'if the stage is nil' do
let(:issue_stage) { nil }
it_behaves_like 'is valid'
end
context 'if the stage is written' do
let(:issue_stage) { 'some stage' }
it 'is invalid' do
expect_valid(false, stage: %i(error_readonly))
end
end
context 'if labels is written' do
let(:issue_labels) { %w(some labels) }
it 'is invalid' do
expect_valid(false, labels: %i(error_readonly))
end
end
context 'if index is nil' do
let(:issue_index) { nil }
it_behaves_like 'is valid'
end
end

@ -1,20 +1,15 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject Backlogs Plugin
#
# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
# Copyright (C)2010-2011 friflaj
# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
# Copyright (C)2009-2010 Mark Maglana
# Copyright (C)2009 Joe Heck, Nate Lowrie
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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.
# 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 Backlogs is a derivative work based on ChiliProject Backlogs.
# The copyright follows:
# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
# 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
@ -30,7 +25,7 @@
# 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.
# See docs/COPYRIGHT.rdoc for more details.
#++
FactoryBot.define do
@ -114,6 +109,9 @@ FactoryBot.define do
</Markup>
MARKUP
end
stage { 'Construction stop' }
labels { ['Structural', 'IT Development', 'Huge'] }
sequence(:index) { |n| n }
factory :bcf_issue_with_comment do
after(:create) do |issue|

@ -33,31 +33,6 @@ describe ::Bcf::Issue, type: :model do
let(:work_package) { FactoryBot.create :work_package, type: type }
let(:issue) { FactoryBot.create :bcf_issue, work_package: work_package }
shared_examples_for 'provides attributes' do
it "provides attributes" do
expect(subject.title).to be_eql 'Maximum Content'
expect(subject.description).to be_eql 'This is a topic with all information present.'
expect(subject.priority_text).to be_eql 'High'
expect(subject.status_text).to be_eql 'Open'
expect(subject.type_text).to be_eql 'Structural'
expect(subject.assignee_text).to be_eql 'andy@example.com'
expect(subject.index_text).to be_eql '0'
expect(subject.labels).to contain_exactly 'Structural', 'IT Development'
expect(subject.due_date_text).to be_nil
expect(subject.creation_date_text).to eql "2015-06-21T12:00:00Z"
expect(subject.creation_author_text).to eql "mike@example.com"
expect(subject.modified_date_text).to eql "2015-06-21T14:22:47Z"
expect(subject.modified_author_text).to eql "michelle@example.com"
expect(subject.stage_text).to eql "Construction start"
end
end
context '#self.with_markup' do
subject { ::Bcf::Issue.with_markup.find_by id: issue.id }
it_behaves_like 'provides attributes'
end
context '#markup_doc' do
subject { issue }
@ -76,8 +51,6 @@ describe ::Bcf::Issue, type: :model do
subject.save
expect(subject.markup_doc).to_not be_eql(first_fetched_doc)
end
it_behaves_like 'provides attributes'
end
describe '.of_project' do

@ -33,7 +33,27 @@ require_relative '../shared_examples'
describe Bcf::API::V2_1::Topics::SingleRepresenter, 'rendering' do
include API::V3::Utilities::PathHelper
let(:work_package) { FactoryBot.build_stubbed(:stubbed_work_package, type: FactoryBot.build_stubbed(:type)) }
let(:assignee) { FactoryBot.build_stubbed(:user) }
let(:creator) { FactoryBot.build_stubbed(:user) }
let(:modifier) { FactoryBot.build_stubbed(:user) }
let(:first_journal) { FactoryBot.build_stubbed(:journal, version: 1, user: creator) }
let(:last_journal) { FactoryBot.build_stubbed(:journal, version: 2, user: modifier) }
let(:journals) { [first_journal, last_journal] }
let(:type) { FactoryBot.build_stubbed(:type) }
let(:status) { FactoryBot.build_stubbed(:status) }
let(:priority) { FactoryBot.build_stubbed(:priority) }
let(:work_package) do
FactoryBot.build_stubbed(:stubbed_work_package,
assigned_to: assignee,
due_date: Date.today,
status: status,
priority: priority,
type: type).tap do |wp|
allow(wp)
.to receive(:journals)
.and_return(journals)
end
end
let(:issue) { FactoryBot.build_stubbed(:bcf_issue, work_package: work_package) }
let(:instance) { described_class.new(issue) }
@ -50,18 +70,25 @@ describe Bcf::API::V2_1::Topics::SingleRepresenter, 'rendering' do
context 'topic_type' do
it_behaves_like 'attribute' do
let(:value) { issue.type_text }
let(:value) { type.name }
let(:path) { 'topic_type' }
end
end
context 'topic_status' do
it_behaves_like 'attribute' do
let(:value) { issue.status_text }
let(:value) { status.name }
let(:path) { 'topic_status' }
end
end
context 'priority' do
it_behaves_like 'attribute' do
let(:value) { priority.name }
let(:path) { 'priority' }
end
end
context 'reference_links' do
it_behaves_like 'attribute' do
let(:value) { [api_v3_paths.work_package(work_package.id)] }
@ -71,14 +98,14 @@ describe Bcf::API::V2_1::Topics::SingleRepresenter, 'rendering' do
context 'title' do
it_behaves_like 'attribute' do
let(:value) { issue.title }
let(:value) { work_package.subject }
let(:path) { 'title' }
end
end
context 'index' do
it_behaves_like 'attribute' do
let(:value) { issue.index_text }
let(:value) { issue.index }
let(:path) { 'index' }
end
end
@ -92,56 +119,56 @@ describe Bcf::API::V2_1::Topics::SingleRepresenter, 'rendering' do
context 'creation_date' do
it_behaves_like 'attribute' do
let(:value) { issue.creation_date_text }
let(:value) { work_package.created_at.iso8601 }
let(:path) { 'creation_date' }
end
end
context 'creation_author' do
it_behaves_like 'attribute' do
let(:value) { issue.creation_author_text }
let(:value) { work_package.author.mail }
let(:path) { 'creation_author' }
end
end
context 'modified_date' do
it_behaves_like 'attribute' do
let(:value) { issue.modified_date_text }
let(:value) { work_package.updated_at.iso8601 }
let(:path) { 'modified_date' }
end
end
context 'modified_author' do
it_behaves_like 'attribute' do
let(:value) { issue.modified_author_text }
let(:value) { modifier.mail }
let(:path) { 'modified_author' }
end
end
context 'description' do
it_behaves_like 'attribute' do
let(:value) { issue.description }
let(:value) { work_package.description }
let(:path) { 'description' }
end
end
context 'due_date' do
it_behaves_like 'attribute' do
let(:value) { issue.due_date_text }
let(:value) { work_package.due_date.iso8601 }
let(:path) { 'due_date' }
end
end
context 'assigned_to' do
it_behaves_like 'attribute' do
let(:value) { issue.assignee_text }
let(:value) { work_package.assigned_to.mail }
let(:path) { 'assigned_to' }
end
end
context 'stage' do
it_behaves_like 'attribute' do
let(:value) { issue.stage_text }
let(:value) { issue.stage }
let(:path) { 'stage' }
end
end

@ -119,7 +119,7 @@ describe 'BCF 2.1 projects resource', type: :request, content_type: :json do
end
it_behaves_like 'bcf api unprocessable response' do
let(:message) { 'You must not write a read-only attribute.' }
let(:message) { 'ID was attempted to be written but is not writable.' }
end
end
end

@ -28,7 +28,8 @@
shared_examples_for 'bcf api successful response' do
it 'responds correctly with the expected body', :aggregate_failures do
expect(subject.status).to eq 200
expect(subject.status)
.to eql(defined?(expected_status) ? expected_status : 200)
expect(subject.body).to be_json_eql(expected_body.to_json)
expect(subject.headers['Content-Type']).to eql 'application/json; charset=utf-8'
end

@ -38,22 +38,32 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma
let(:view_only_user) do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: [:view_linked_issues])
member_with_permissions: %i[view_linked_issues view_work_packages])
end
let(:only_member_user) do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: [])
end
let(:edit_member_user) do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: %i[manage_bcf add_work_packages view_linked_issues])
end
let(:non_member_user) do
FactoryBot.create(:user)
end
let(:project) do
FactoryBot.create(:project,
enabled_module_names: [:bcf])
enabled_module_names: %i[bcf work_package_tracking])
end
let(:assignee) { FactoryBot.create(:user) }
let(:work_package) do
FactoryBot.create(:work_package,
assigned_to: assignee,
project: project)
end
let(:work_package) { FactoryBot.create(:work_package, project: project) }
let(:bcf_issue) { FactoryBot.create(:bcf_issue, work_package: work_package) }
subject(:response) { last_response }
@ -72,26 +82,24 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma
let(:expected_body) do
[
{
"assigned_to": "andy@example.com",
"creation_author": "mike@example.com",
"creation_date": "2015-06-21T12:00:00Z",
"description": "This is a topic with all information present.",
"assigned_to": assignee.mail,
"creation_author": work_package.author.mail,
"creation_date": work_package.created_at.iso8601,
"description": work_package.description,
"due_date": nil,
guid: bcf_issue.uuid,
"index": "0",
"labels": [
"Structural",
"IT Development"
],
"modified_author": "michelle@example.com",
"modified_date": "2015-06-21T14:22:47Z",
"guid": bcf_issue.uuid,
"index": bcf_issue.index,
"labels": bcf_issue.labels,
"priority": work_package.priority.name,
"modified_author": current_user.mail,
"modified_date": work_package.updated_at.iso8601,
"reference_links": [
api_v3_paths.work_package(work_package.id)
],
"stage": "Construction start",
"title": "Maximum Content",
"topic_status": "Open",
"topic_type": "Structural"
"stage": bcf_issue.stage,
"title": work_package.subject,
"topic_status": work_package.status.name,
"topic_type": work_package.type.name
}
]
end
@ -123,26 +131,24 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma
it_behaves_like 'bcf api successful response' do
let(:expected_body) do
{
"assigned_to": "andy@example.com",
"creation_author": "mike@example.com",
"creation_date": "2015-06-21T12:00:00Z",
"description": "This is a topic with all information present.",
"assigned_to": assignee.mail,
"creation_author": work_package.author.mail,
"creation_date": work_package.created_at.iso8601,
"description": work_package.description,
"due_date": nil,
guid: bcf_issue.uuid,
"index": "0",
"labels": [
"Structural",
"IT Development"
],
"modified_author": "michelle@example.com",
"modified_date": "2015-06-21T14:22:47Z",
"guid": bcf_issue.uuid,
"index": bcf_issue.index,
"labels": bcf_issue.labels,
"priority": work_package.priority.name,
"modified_author": current_user.mail,
"modified_date": work_package.updated_at.iso8601,
"reference_links": [
api_v3_paths.work_package(work_package.id)
],
"stage": "Construction start",
"title": "Maximum Content",
"topic_status": "Open",
"topic_type": "Structural"
"stage": bcf_issue.stage,
"title": work_package.subject,
"topic_status": work_package.status.name,
"topic_type": work_package.type.name
}
end
end
@ -165,4 +171,240 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma
it_behaves_like 'bcf api not allowed response'
end
end
describe 'POST /api/bcf/2.1/projects/:project_id/topics' do
let(:path) { "/api/bcf/2.1/projects/#{project.id}/topics" }
let(:current_user) { edit_member_user }
let(:type) do
FactoryBot.create(:type).tap do |t|
project.types << t
end
end
let(:status) do
FactoryBot.create(:status)
end
let!(:default_status) do
FactoryBot.create(:default_status)
end
let!(:default_type) do
FactoryBot.create(:type, is_default: true)
end
let!(:standard_type) do
FactoryBot.create(:type_standard)
end
let!(:priority) do
FactoryBot.create(:priority)
end
let!(:default_priority) do
FactoryBot.create(:default_priority)
end
let(:description) { 'some description' }
let(:stage) { nil }
let(:labels) { [] }
let(:index) { 5 }
let(:params) do
{
topic_type: type.name,
topic_status: status.name,
priority: priority.name,
title: 'BCF topic 101',
labels: labels,
stage: stage,
index: index,
due_date: Date.today.iso8601,
assigned_to: view_only_user.mail,
description: description
}
end
before do
login_as(current_user)
post path, params.to_json
end
it_behaves_like 'bcf api successful response' do
let(:expected_status) { 201 }
let(:expected_body) do
issue = Bcf::Issue.last
work_package = WorkPackage.last
{
guid: issue&.uuid,
topic_type: type.name,
topic_status: status.name,
priority: priority.name,
title: 'BCF topic 101',
labels: labels,
index: index,
reference_links: [
api_v3_paths.work_package(work_package&.id)
],
assigned_to: view_only_user.mail,
due_date: Date.today.iso8601,
stage: stage,
creation_author: edit_member_user.mail,
creation_date: work_package&.created_at&.iso8601,
modified_author: edit_member_user.mail,
modified_date: work_package&.updated_at&.iso8601,
description: description
}
end
end
context 'with minimal parameters' do
let(:params) do
{
title: 'BCF topic 101'
}
end
it_behaves_like 'bcf api successful response' do
let(:expected_status) { 201 }
let(:expected_body) do
issue = Bcf::Issue.last
work_package = WorkPackage.last
{
guid: issue&.uuid,
topic_type: standard_type.name,
topic_status: default_status.name,
priority: default_priority.name,
title: 'BCF topic 101',
labels: [],
index: nil,
reference_links: [
api_v3_paths.work_package(work_package&.id)
],
assigned_to: nil,
due_date: nil,
stage: nil,
creation_author: edit_member_user.mail,
creation_date: work_package&.created_at&.iso8601,
modified_author: edit_member_user.mail,
modified_date: work_package&.updated_at&.iso8601,
description: nil
}
end
end
end
context 'without a title' do
let(:params) do
{
}
end
it_behaves_like 'bcf api unprocessable response' do
let(:message) do
"Title can't be blank."
end
end
end
context 'with an inexistent status' do
let(:params) do
{
title: 'Some title',
topic_status: 'Some non existing status'
}
end
it_behaves_like 'bcf api unprocessable response' do
let(:message) do
"Status does not exist."
end
end
end
context 'with an inexistent priority' do
let(:params) do
{
title: 'Some title',
priority: 'Some non existing priority'
}
end
it_behaves_like 'bcf api unprocessable response' do
let(:message) do
"Priority does not exist."
end
end
end
context 'with an inexistent type' do
let(:params) do
{
title: 'Some title',
topic_type: 'Some non existing type'
}
end
it_behaves_like 'bcf api unprocessable response' do
let(:message) do
"Type does not exist."
end
end
end
context 'with an inexistent assigned_to' do
let(:params) do
{
title: 'Some title',
assigned_to: 'Some non existing assignee'
}
end
it_behaves_like 'bcf api unprocessable response' do
let(:message) do
"Assignee does not exist."
end
end
end
context 'with two inexistent related resources' do
let(:params) do
{
title: 'Some title',
assigned_to: 'Some non existing assignee',
topic_type: 'Some non existing type'
}
end
it_behaves_like 'bcf api unprocessable response' do
let(:message) do
"Multiple field constraints have been violated. Type does not exist. Assignee does not exist."
end
end
end
context 'with a label' do
let(:params) do
{
title: 'Some title',
labels: ['some label']
}
end
it_behaves_like 'bcf api unprocessable response' do
let(:message) do
"Labels was attempted to be written but is not writable."
end
end
end
context 'with a stage' do
let(:params) do
{
title: 'Some title',
stage: 'some stage'
}
end
it_behaves_like 'bcf api unprocessable response' do
let(:message) do
"Stage was attempted to be written but is not writable."
end
end
end
end
end

@ -0,0 +1,173 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe Bcf::Issues::CreateService, type: :model do
let(:user) { FactoryBot.build_stubbed(:user) }
let(:contract_class) do
double('contract_class', '<=': true)
end
let(:issue_valid) { true }
let(:instance) do
described_class.new(user: user,
contract_class: contract_class)
end
let(:call_attributes) { { subject: 'Some name' } }
let(:set_attributes_success) do
true
end
let(:set_attributes_errors) do
double('set_attributes_errors')
end
let(:set_attributes_result) do
ServiceResult.new result: created_issue,
success: set_attributes_success,
errors: set_attributes_errors
end
let!(:created_work_package) do
FactoryBot.build_stubbed(:work_package)
end
let(:wp_create_errors) do
double('wp_create_errors')
end
let(:wp_create_result) do
ServiceResult.new result: created_work_package,
success: true,
errors: wp_create_errors
end
let!(:wp_create_service) do
wp_service = double('wp create service')
allow(WorkPackages::CreateService)
.to receive(:new)
.with(user: user)
.and_return(wp_service)
allow(wp_service)
.to receive(:call)
.and_return(wp_create_result)
wp_service
end
let!(:created_issue) do
issue = FactoryBot.build_stubbed(:bcf_issue)
allow(Bcf::Issue)
.to receive(:new)
.and_return(issue)
allow(issue)
.to receive(:save)
.and_return(issue_valid)
issue
end
let!(:set_attributes_service) do
service = double('set_attributes_service_instance')
allow(Bcf::Issues::SetAttributesService)
.to receive(:new)
.with(user: user,
model: created_issue,
contract_class: contract_class)
.and_return(service)
allow(service)
.to receive(:call)
.and_return(set_attributes_result)
end
describe '#call' do
subject { instance.call(call_attributes) }
it 'is successful' do
expect(subject.success?).to be_truthy
end
it 'returns the result of the SetAttributesService' do
expect(subject)
.to eql set_attributes_result
end
it 'persists the issue' do
expect(created_issue)
.to receive(:save)
.and_return(issue_valid)
subject
end
it 'creates a issue' do
expect(subject.result)
.to eql created_issue
end
context 'if the SetAttributeService is unsuccessful' do
let(:set_attributes_success) { false }
it 'is unsuccessful' do
expect(subject.success?).to be_falsey
end
it 'returns the result of the SetAttributesService' do
expect(subject)
.to eql set_attributes_result
end
it 'does not persist the changes' do
expect(created_issue)
.to_not receive(:save)
subject
end
it "exposes the contract's errors" do
subject
expect(subject.errors).to eql set_attributes_errors
end
end
context 'when the issue is invalid' do
let(:issue_valid) { false }
it 'is unsuccessful' do
expect(subject.success?).to be_falsey
end
it "exposes the issue's errors" do
subject
expect(subject.errors).to eql created_issue.errors
end
end
end
end

@ -288,21 +288,7 @@ describe 'API v3 Grids resource for Board Grids', type: :request, content_type:
}.with_indifferent_access
end
it 'responds with 422 and mentions the error' do
expect(subject.status).to eq 422
expect(subject.body)
.to be_json_eql('Error'.to_json)
.at_path('_type')
expect(subject.body)
.to be_json_eql("You must not write a read-only attribute.".to_json)
.at_path('message')
expect(subject.body)
.to be_json_eql("scope".to_json)
.at_path('_embedded/details/attribute')
end
it_behaves_like 'read-only violation', 'scope', Boards::Grid
end
context 'with the grid not existing' do

@ -121,7 +121,7 @@ describe "PATCH /api/v3/grids/:id/form for Board Grids", type: :request, content
it 'has a validation error on scope as the value is not writeable' do
expect(subject.body)
.to be_json_eql("You must not write a read-only attribute.".to_json)
.to be_json_eql("Scope was attempted to be written but is not writable.".to_json)
.at_path('_embedded/validationErrors/scope/message')
end
end

@ -277,21 +277,7 @@ describe 'API v3 Grids resource', type: :request, content_type: :json do
}.with_indifferent_access
end
it 'responds with 422 and mentions the error' do
expect(subject.status).to eq 422
expect(subject.body)
.to be_json_eql('Error'.to_json)
.at_path('_type')
expect(subject.body)
.to be_json_eql("You must not write a read-only attribute.".to_json)
.at_path('message')
expect(subject.body)
.to be_json_eql("scope".to_json)
.at_path('_embedded/details/attribute')
end
it_behaves_like 'read-only violation', 'scope', Grids::Grid
end
context 'with the page not existing' do

@ -132,7 +132,7 @@ describe "PATCH /api/v3/grids/:id/form", type: :request, content_type: :json do
it 'has a validation error on scope as the value is not writeable' do
expect(subject.body)
.to be_json_eql("You must not write a read-only attribute.".to_json)
.to be_json_eql("Scope was attempted to be written but is not writable.".to_json)
.at_path('_embedded/validationErrors/scope/message')
end
end

@ -174,7 +174,7 @@ describe WorkPackages::BaseContract do
before do
allow(work_package)
.to receive(:status_id_change)
.and_return [1,2]
.and_return [1, 2]
end
it 'is writable' do
@ -182,6 +182,19 @@ describe WorkPackages::BaseContract do
end
end
end
context 'is an inexistent status' do
before do
work_package.status = Status::InexistentStatus.new
end
it 'is invalid' do
contract.validate
expect(subject.errors.symbols_for(:status))
.to match_array [:does_not_exist]
end
end
end
describe 'estimated hours' do
@ -521,6 +534,34 @@ describe WorkPackages::BaseContract do
end
end
end
context 'inexistent type' do
before do
work_package.type = Type::InexistentType.new
contract.validate
end
it 'is invalid' do
expect(contract.errors.symbols_for(:type))
.to match_array [:does_not_exist]
end
end
end
context 'assigned_to' do
context 'inexistent user' do
before do
work_package.assigned_to = User::InexistentUser.new
contract.validate
end
it 'is invalid' do
expect(contract.errors.symbols_for(:assigned_to))
.to match_array [:does_not_exist]
end
end
end
describe 'category' do
@ -630,6 +671,19 @@ describe WorkPackages::BaseContract do
.to be_empty
end
end
context 'inexistent priority' do
before do
work_package.priority = Priority::InexistentPriority.new
contract.validate
end
it 'is invalid' do
expect(contract.errors.symbols_for(:priority))
.to match_array [:does_not_exist]
end
end
end
describe 'status' do

@ -276,18 +276,19 @@ describe MailHandler, type: :model do
end
it 'rejects if unknown_user=accept and permission check is present' do
expected =
'MailHandler: work_package could not be created by Anonymous due to ' \
'#["may not be accessed.", "Type is not writable.", "Project is not writable.", ' \
'"Subject is not writable.", "Description is not writable."]'
'#["may not be accessed.", "Type was attempted to be written but is not writable.", ' \
'"Project was attempted to be written but is not writable.", ' \
'"Subject was attempted to be written but is not writable.", ' \
'"Description was attempted to be written but is not writable."]'
expect(Rails.logger)
.to(receive(:error))
.to receive(:error)
.with(expected)
result = submit_email 'ticket_by_unknown_user.eml',
issue: {project: project.identifier},
issue: { project: project.identifier },
unknown_user: 'accept'
expect(result).to eq false
@ -295,7 +296,7 @@ describe MailHandler, type: :model do
it 'accepts if unknown_user=accept and no_permission_check' do
work_package = submit_email 'ticket_by_unknown_user.eml',
issue: {project: project.identifier},
issue: { project: project.identifier },
unknown_user: 'accept',
no_permission_check: 1

@ -576,14 +576,7 @@ describe 'API v3 memberhips resource', type: :request, content_type: :json do
}.to_json
end
it 'returns 422' do
expect(last_response.status)
.to eql(422)
expect(last_response.body)
.to be_json_eql("You must not write a read-only attribute.".to_json)
.at_path('message')
end
it_behaves_like 'read-only violation', 'project', Member
end
context 'if attempting to switch the principal' do
@ -602,18 +595,7 @@ describe 'API v3 memberhips resource', type: :request, content_type: :json do
}.to_json
end
it 'returns 422' do
expect(last_response.status)
.to eql(422)
expect(last_response.body)
.to be_json_eql("You must not write a read-only attribute.".to_json)
.at_path('message')
expect(last_response.body)
.to be_json_eql("user".to_json)
.at_path('_embedded/details/attribute')
end
it_behaves_like 'read-only violation', 'user', Member
end
context 'if lacking the manage permissions' do

@ -302,7 +302,7 @@ describe 'API v3 Relation resource', type: :request, content_type: :json do
it "should let the user know the attribute is read-only" do
msg = JSON.parse(last_response.body)["message"]
expect(msg).to include 'read-only'
expect(msg).to include "Work package an existing relation's `from` link is immutable"
end
end
end

@ -150,7 +150,7 @@ shared_examples_for 'format error' do |message|
message
end
shared_examples_for 'read-only violation' do |attribute|
shared_examples_for 'read-only violation' do |attribute, model, attribute_message = nil|
describe 'details' do
subject { JSON.parse(last_response.body)['_embedded']['details'] }
@ -160,7 +160,7 @@ shared_examples_for 'read-only violation' do |attribute|
it_behaves_like 'error response',
422,
'PropertyIsReadOnly',
I18n.t('api_v3.errors.writing_read_only_attributes')
"#{attribute_message || model.human_attribute_name(attribute)} #{I18n.t('activerecord.errors.messages.error_readonly')}"
end
shared_examples_for 'multiple errors' do |code, _message|

@ -236,14 +236,7 @@ describe 'API v3 Version resource', content_type: :json do
}.to_json
end
it 'returns 422' do
expect(last_response.status)
.to eql(422)
expect(last_response.body)
.to be_json_eql("You must not write a read-only attribute.".to_json)
.at_path('message')
end
it_behaves_like 'read-only violation', 'project', Version
end
context 'if lacking the manage permissions' do

@ -856,13 +856,13 @@ describe 'API v3 Work package resource',
context 'created_at' do
let(:params) { valid_params.merge(createdAt: tomorrow) }
it_behaves_like 'read-only violation', 'createdAt'
it_behaves_like 'read-only violation', 'createdAt', WorkPackage, 'Created on'
end
context 'updated_at' do
let(:params) { valid_params.merge(updatedAt: tomorrow) }
it_behaves_like 'read-only violation', 'updatedAt'
it_behaves_like 'read-only violation', 'updatedAt', WorkPackage, 'Updated on'
end
end
end

@ -69,11 +69,11 @@ describe Projects::SetAttributesService, type: :model do
before do
allow(project)
.to receive(:valid?)
.and_return(project_valid)
.and_return(project_valid)
expect(contract_instance)
.to receive(:validate)
.and_return(contract_valid)
.and_return(contract_valid)
end
subject { instance.call(call_attributes) }

@ -56,11 +56,11 @@
@path = path + ' > *'
end
should_message = -> (actual) do
should_message = ->(actual) do
['expected:', expected.to_s, 'got:', actual.to_s].join("\n")
end
should_not_message = -> (actual) do
should_not_message = ->(actual) do
['expected:', actual.to_s, 'not to be equivalent to:', expected.to_s].join("\n")
end

Loading…
Cancel
Save