Merge pull request #7610 from opf/bim/bcf-xml-api

BCF-XML-API V1
pull/7614/head
Wieland Lindenthal 5 years ago committed by GitHub
commit a5fa3b3487
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      modules/bcf/app/assets/stylesheets/bcf/bcf.sass
  2. 14
      modules/bcf/app/controllers/bcf/issues_controller.rb
  3. 4
      modules/bcf/app/models/bcf/issue.rb
  4. 1
      modules/bcf/app/views/bcf/issues/_render_issues.html.erb
  5. 34
      modules/bcf/lib/api/bcf_xml/v1/bcf_xml_api.rb
  6. 11
      modules/bcf/lib/open_project/bcf/bcf_xml/exporter.rb
  7. 1
      modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb
  8. 21
      modules/bcf/lib/open_project/bcf/engine.rb
  9. 2
      modules/bcf/spec/controllers/issues_controller_spec.rb
  10. 4
      modules/bcf/spec/factories/bcf_viewpoint_factory.rb
  11. 159
      modules/bcf/spec/requests/api/bcf_xml/v1/bcf_xml_api_spec.rb

@ -7,6 +7,8 @@
flex: 0 0 250px flex: 0 0 250px
margin: 10px margin: 10px
padding: 10px padding: 10px
&.-failed
border: 1px solid #EAEAEA
img img
width: 100% width: 100%

@ -46,28 +46,24 @@ module ::Bcf
menu_item :work_packages menu_item :work_packages
def upload; end
def index def index
@issues = ::Bcf::Issue.in_project(@project) redirect_to action: :upload
.with_markup
.includes(:comments, :work_package, viewpoints: :attachments)
.page(page_param)
.per_page(per_page_param)
end end
def upload; end
def prepare_import def prepare_import
render_next render_next
rescue StandardError => e rescue StandardError => e
flash[:error] = I18n.t('bcf.bcf_xml.import_failed', error: e.message) flash[:error] = I18n.t('bcf.bcf_xml.import_failed', error: e.message)
redirect_to action: :index redirect_to action: :upload
end end
def configure_import def configure_import
render_next render_next
rescue StandardError => e rescue StandardError => e
flash[:error] = I18n.t('bcf.bcf_xml.import_failed', error: e.message) flash[:error] = I18n.t('bcf.bcf_xml.import_failed', error: e.message)
redirect_to action: :index redirect_to action: :upload
end end
def perform_import def perform_import

@ -10,10 +10,6 @@ module Bcf
after_update :invalidate_markup_cache after_update :invalidate_markup_cache
class << self class << self
def in_project(project)
where(project_id: project.try(:id) || project)
end
def with_markup def with_markup
select '*', select '*',
extract_first_node(title_path, 'title'), extract_first_node(title_path, 'title'),

@ -3,6 +3,7 @@
<% issues.each do |issue| %> <% issues.each do |issue| %>
<% status_id = issue.work_package&.status_id %> <% status_id = issue.work_package&.status_id %>
<% highlighting_class = status_id.present? ? "__hl_background_status_#{status_id}" : '' %> <% highlighting_class = status_id.present? ? "__hl_background_status_#{status_id}" : '' %>
<% highlighting_class << ' -failed' if issue.errors.present? %>
<div class="<%= highlighting_class %>"> <div class="<%= highlighting_class %>">
<p> <p>
<strong><%= issue.title %></strong> <strong><%= issue.title %></strong>

@ -30,9 +30,11 @@
#++ #++
module API module API
module V3 module BcfXml
module BcfXml module V1
class BcfXmlAPI < ::API::OpenProjectAPI class BcfXmlAPI < ::API::OpenProjectAPI
prefix :bcf_xml_api
resources :projects do resources :projects do
route_param :id do route_param :id do
namespace 'bcf_xml' do namespace 'bcf_xml' do
@ -54,10 +56,34 @@ module API
def import_options def import_options
params[:import_options].presence || {} params[:import_options].presence || {}
end end
def find_project
Project.find(params[:id])
end
end
get do
project = find_project
authorize_any(%i[view_linked_issues view_work_packages], projects: [project]) do
raise ::API::Errors::NotFound.new
end
authorize(:view_linked_issues, context: project) do
raise API::Errors::NotFound.new
end
query = Query.new_default(name: '_', project: project)
updated_query = ::API::V3::UpdateQueryFromV3ParamsService.new(query, User.current).call(params)
exporter = ::OpenProject::Bcf::BcfXml::Exporter.new(updated_query.result)
header['Content-Disposition'] = "attachment; filename=\"#{exporter.bcf_filename}\""
env['api.format'] = :binary
exporter.list_from_api.read
end end
post do post do
project = Project.find(params[:id]) project = find_project
authorize(:manage_bcf, context: project) do authorize(:manage_bcf, context: project) do
raise API::Errors::NotFound.new raise API::Errors::NotFound.new
@ -68,7 +94,7 @@ module API
importer = ::OpenProject::Bcf::BcfXml::Importer.new(file, importer = ::OpenProject::Bcf::BcfXml::Importer.new(file,
project, project,
current_user: User.current) current_user: User.current)
importer.import! import_options importer.import!(import_options)
rescue StandardError => e rescue StandardError => e
raise API::Errors::InternalError.new e.message raise API::Errors::InternalError.new e.message
ensure ensure

@ -29,6 +29,17 @@ module OpenProject::Bcf::BcfXml
raise e raise e
end end
def list_from_api
Dir.mktmpdir do |dir|
files = create_bcf! dir
zip_folder dir, files
end
rescue StandardError => e
Rails.logger.error "Failed to export work package list #{e} #{e.message}"
raise e
end
def success(zip) def success(zip)
WorkPackage::Exporter::Success WorkPackage::Exporter::Success
.new format: :xls, .new format: :xls,

@ -142,6 +142,7 @@ module OpenProject::Bcf::BcfXml
issue.work_package = call.result issue.work_package = call.result
create_wp_comment(user, I18n.t('bcf.bcf_xml.import_update_comment')) if is_update create_wp_comment(user, I18n.t('bcf.bcf_xml.import_update_comment')) if is_update
else else
issue.errors.merge!(call.errors)
Rails.logger.error "Failed to synchronize BCF #{issue.uuid} with work package: #{call.errors.full_messages.join('; ')}" Rails.logger.error "Failed to synchronize BCF #{issue.uuid} with work package: #{call.errors.full_messages.join('; ')}"
end end
end end

@ -45,10 +45,11 @@ module OpenProject::Bcf
project_module :bcf do project_module :bcf do
permission :view_linked_issues, permission :view_linked_issues,
'bcf/issues': %i[index] { 'bcf/issues': %i[index] },
dependencies: %i[view_work_packages]
permission :manage_bcf, permission :manage_bcf,
'bcf/issues': %i[index upload prepare_import configure_import perform_import] { 'bcf/issues': %i[index upload prepare_import configure_import perform_import] },
dependencies: %i[view_linked_issues view_work_packages add_work_packages edit_work_packages]
end end
OpenProject::AccessControl.permission(:view_work_packages).actions << 'bcf/issues/redirect_to_bcf_issues_list' OpenProject::AccessControl.permission(:view_work_packages).actions << 'bcf/issues/redirect_to_bcf_issues_list'
@ -134,11 +135,6 @@ module OpenProject::Bcf
"#{project(project_id)}/bcf_xml" "#{project(project_id)}/bcf_xml"
end end
add_api_endpoint 'API::V3::Projects::ProjectsAPI' do
content_type :binary, 'application/octet-stream'
mount ::API::V3::BcfXml::BcfXmlAPI
end
initializer 'bcf.register_hooks' do initializer 'bcf.register_hooks' do
# don't use require_dependency to not reload hooks in development mode # don't use require_dependency to not reload hooks in development mode
require 'open_project/xls_export/hooks/work_package_hook.rb' require 'open_project/xls_export/hooks/work_package_hook.rb'
@ -146,6 +142,7 @@ module OpenProject::Bcf
initializer 'bcf.register_mimetypes' do initializer 'bcf.register_mimetypes' do
Mime::Type.register "application/octet-stream", :bcf unless Mime::Type.lookup_by_extension(:bcf) Mime::Type.register "application/octet-stream", :bcf unless Mime::Type.lookup_by_extension(:bcf)
Mime::Type.register "application/octet-stream", :bcfzip unless Mime::Type.lookup_by_extension(:bcfzip)
end end
config.to_prepare do config.to_prepare do
@ -154,6 +151,14 @@ module OpenProject::Bcf
::Queries::Register.filter ::Query, OpenProject::Bcf::BcfIssueAssociatedFilter ::Queries::Register.filter ::Query, OpenProject::Bcf::BcfIssueAssociatedFilter
::Queries::Register.column ::Query, OpenProject::Bcf::QueryBcfThumbnailColumn ::Queries::Register.column ::Query, OpenProject::Bcf::QueryBcfThumbnailColumn
::API::Root.class_eval do
content_type :binary, 'application/octet-stream'
default_format :binary
version 'v1', using: :path do
mount ::API::BcfXml::V1::BcfXmlAPI
end
end
end end
end end
end end

@ -120,7 +120,7 @@ describe ::Bcf::IssuesController, type: :controller do
it 'should redirect back to where we started from' do it 'should redirect back to where we started from' do
expect { action }.to change { Attachment.count }.by(1) expect { action }.to change { Attachment.count }.by(1)
expect(response).to redirect_to '/projects/bim_project/issues' expect(response).to redirect_to '/projects/bim_project/issues/upload'
end end
end end
end end

@ -35,6 +35,10 @@
FactoryBot.define do FactoryBot.define do
factory :bcf_viewpoint, class: ::Bcf::Viewpoint do factory :bcf_viewpoint, class: ::Bcf::Viewpoint do
new_uuid = SecureRandom.uuid
uuid { new_uuid }
viewpoint_name { "Viewpoint_#{new_uuid}.bcfv" }
after(:create) do |viewpoint| after(:create) do |viewpoint|
create(:bcf_viewpoint_attachment, container: viewpoint) create(:bcf_viewpoint_attachment, container: viewpoint)
end end

@ -0,0 +1,159 @@
#-- 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'BCF XML API v1 bcf_xml resource', type: :request do
include Rack::Test::Methods
let(:current_user) do
FactoryBot.create(:user, member_in_project: project, member_through_role: role, firstname: "BIMjamin")
end
let(:status) { FactoryBot.create(:status, name: 'New', is_default: true) }
let(:priority) { FactoryBot.create(:issue_priority, name: "Mega high", is_default: true)}
let(:work_package) { FactoryBot.create(:work_package, status: status, priority: priority) }
let(:project) { work_package.project }
let(:bcf_issue) { FactoryBot.create(:bcf_issue_with_comment, work_package: work_package) }
let(:role) { FactoryBot.create(:role, permissions: permissions) }
let(:permissions) { %i(view_work_packages view_linked_issues) }
let(:filename) { 'MaximumInformation.bcf' }
let(:bcf_xml_file) do
Rack::Test::UploadedFile.new(
File.join(Rails.root, "modules/bcf/spec/fixtures/files/#{filename}"),
'application/octet-stream'
)
end
subject(:response) { last_response }
before do
login_as(current_user)
OpenProject::Cache.clear
end
describe 'GET /bcf_xml_api/v1/projects/<project>/bcf_xml' do
let(:path) { "/bcf_xml_api/v1/projects/#{project.identifier}/bcf_xml" }
context 'without params' do
before do
bcf_issue
get path
end
it 'responds 200 OK' do
expect(subject.status).to eq(200)
end
it 'responds with correct Content-Type' do
expect(subject.content_type)
.to eql("application/octet-stream")
end
it 'responds with correct Content-Disposition' do
expect(subject.header["Content-Disposition"])
.to match(/attachment; filename="OpenProject_Work_packages_\d\d\d\d-\d\d-\d\d.bcfzip"/)
end
it 'responds with a correct .bcfzip file in the body ' do
expect(zip_has_file?(subject.body, 'bcf.version')).to be_truthy
expect(zip_has_file?(subject.body, "#{bcf_issue.uuid}/markup.bcf")).to be_truthy
end
context "without :view_linked_issues permission" do
let(:permissions) { %i[view_work_packages] }
it "returns a status 404" do
expect(subject.status).to eql(404)
end
end
end
context 'with params filter on work package subject' do
let(:escaped_query_params) do
CGI.escape("[{\"subject\":{\"operator\":\"!~\",\"values\":[\"#{work_package.subject}\"]}}]")
end
let(:path) do
"/bcf_xml_api/v1/projects/#{project.identifier}/bcf_xml?filters=#{escaped_query_params}"
end
before do
bcf_issue
get path
end
it 'excludes the work package from the .bcfzip file' do
expect(zip_has_file?(subject.body, "#{bcf_issue.uuid}/markup.bcf")).to be_falsey
end
end
end
describe 'POST /bcf_xml_api/v1/projects/<project>/bcf_xml' do
let(:permissions) { %i(view_work_packages add_work_packages edit_work_packages manage_bcf view_linked_issues) }
let(:path) { "/bcf_xml_api/v1/projects/#{project.identifier}/bcf_xml" }
let(:params) do
{
bcf_xml_file: bcf_xml_file
}
end
before do
expect(project.work_packages.count).to eql(1)
post path, params, 'CONTENT_TYPE' => 'multipart/form-data'
end
context 'without import conflicts' do
it "creates two new work packages" do
expect(subject.status).to eql(201)
expect(project.work_packages.count).to eql(3)
end
end
context "without :manage_bcf permission" do
let(:permissions) do
%i[view_work_packages add_work_packages edit_work_packages view_linked_issues]
end
it "returns a status 404" do
expect(subject.status).to eql(404)
expect(project.work_packages.count).to eql(1)
end
end
end
def zip_has_file?(zip_string, filename)
has_file = false
Zip::File.open_buffer(zip_string) do |zip_file|
has_file = zip_file.find { |entry| entry.name == filename }.present?
end
has_file
end
end
Loading…
Cancel
Save