diff --git a/app/views/members/_member_form.html.erb b/app/views/members/_member_form.html.erb index 0578df202b..7b837399ed 100644 --- a/app/views/members/_member_form.html.erb +++ b/app/views/members/_member_form.html.erb @@ -29,10 +29,9 @@ See docs/COPYRIGHT.rdoc for more details. <%= javascript_include_tag "members_select_boxes.js" %> -<%= labelled_tabular_form_for(:member, url: {controller: 'members', action: 'create', project_id: project}, +<%= labelled_tabular_form_for(:member, url: main_app.project_members_path, method: :post, - html: {id: "members_add_form", class: "form -vertical -bordered -medium-compressed"}) do |f| %> - + html: { id: "members_add_form", class: "form -vertical -bordered -medium-compressed" }) do |f| %> <% csp_onclick('hideAddMemberForm()', '.hide-add-member-form-link') %>
diff --git a/modules/bcf/app/controllers/bcf/issues_controller.rb b/modules/bcf/app/controllers/bcf/issues_controller.rb index 49a0e4c31e..0e10681b37 100644 --- a/modules/bcf/app/controllers/bcf/issues_controller.rb +++ b/modules/bcf/app/controllers/bcf/issues_controller.rb @@ -29,10 +29,24 @@ module ::Bcf @bcf_file = params[:bcf_file] begin + @roles = Role.find_all_givable + @listing = @importer.get_extractor_list! @bcf_file.path - @issues = ::Bcf::Issue.with_markup - .includes(work_package: %i[status priority assigned_to]) - .where(uuid: @listing.map { |e| e[:uuid] }) + if @listing.blank? + 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] }) + + + all_people = @listing.map { |entry| entry[:people] }.flatten.uniq + all_mails = @listing.map { |entry| entry[:mail_addresses] }.flatten.uniq + + @known_users = User.where(mail: all_mails).includes(:memberships) + @unknown_mails = all_mails.map(&:downcase) - @known_users.map(&:mail).map(&:downcase) + @members = @known_users.select { |user| user.projects.map(&:id).include? @project.id } + @non_members = @known_users - @members + @invalid_people = all_people - all_mails rescue StandardError => e flash[:error] = I18n.t('bcf.bcf_xml.import_failed', error: e.message) redirect_to action: :index diff --git a/modules/bcf/config/locales/en.yml b/modules/bcf/config/locales/en.yml index c9ddfab020..1cd59167b2 100644 --- a/modules/bcf/config/locales/en.yml +++ b/modules/bcf/config/locales/en.yml @@ -4,6 +4,8 @@ en: label_bcf: 'BCF' issues: "Issues" experimental_badge: "Experimental" + exceptions: + file_invalid: "BCF file invalid" x_bcf_issues: zero: 'No BCF issues' diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb index e55a82d880..4e33938476 100644 --- a/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb @@ -15,7 +15,7 @@ module OpenProject::Bcf::BcfXml # but do not perform the import def get_extractor_list!(file) Zip::File.open(file) do |zip| - yield_topic_entries(zip) + yield_markup_bcf_files(zip) .map do |entry| to_listing(MarkupExtractor.new(entry)) end @@ -44,11 +44,13 @@ module OpenProject::Bcf::BcfXml Hash[keys.map { |k| [k, extractor.public_send(k)] }].tap do |attributes| attributes[:viewpoint_count] = extractor.viewpoints.count attributes[:comments_count] = extractor.comments.count + attributes[:people] = extractor.people + attributes[:mail_addresses] = extractor.mail_addresses end end def synchronize_topics(zip) - yield_topic_entries(zip) + yield_markup_bcf_files(zip) .map do |entry| issue = IssueReader.new(project, zip, entry, current_user: current_user).extract! issue.save @@ -57,9 +59,9 @@ module OpenProject::Bcf::BcfXml end ## - # Yields topic entries and their uuid from the ZIP files + # Yields topic bcf files (that contain topic entries and their uuid) from the ZIP files # while skipping all other entries - def yield_topic_entries(zip) + def yield_markup_bcf_files(zip) zip.select { |entry| entry.name.end_with?('markup.bcf') } end end diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb index ffc4e2bd11..176b5b71f4 100644 --- a/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb @@ -94,7 +94,7 @@ module OpenProject::Bcf::BcfXml # Extend comments with new or updated values from XML def build_comments extractor.comments.each do |data| - next if issue.comments.has_uuid?(data[:uuid]) + next if issue.comments.has_uuid?(data[:uuid]) # Comment has already been imported once. comment = issue.comments.build data.slice(:uuid) # Cannot link to a journal when no work package diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb index eab2c26bcf..6ce2ec3dee 100644 --- a/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb @@ -220,7 +220,7 @@ module OpenProject::Bcf::BcfXml end ## - # Create a single topic node + # Create a single comment node def comment_node(xml, uuid, journal) xml.Comment "Guid" => uuid do xml.Date to_bcf_datetime(journal.created_at) diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/markup_extractor.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/markup_extractor.rb index 8270c96fb2..3aa83897f0 100644 --- a/modules/bcf/lib/open_project/bcf/bcf_xml/markup_extractor.rb +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/markup_extractor.rb @@ -57,7 +57,7 @@ module OpenProject::Bcf::BcfXml uuid: node['Guid'], viewpoint: node.xpath('Viewpoint/text()').to_s, snapshot: node.xpath('Snapshot/text()').to_s - } + }.with_indifferent_access end end @@ -68,10 +68,22 @@ module OpenProject::Bcf::BcfXml date: node.xpath('Date/text()').to_s, author: node.xpath('Author/text()').to_s, comment: node.xpath('Comment/text()').to_s - } + }.with_indifferent_access end end + def mail_addresses + people.filter do |person| + # person value is an email address + person =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i + end + .uniq + end + + def people + ([assignee, author] + comments.map { |comment| comment[:author] }).filter(&:present?).uniq + end + private def extract_non_empty(path, prefix: '/Markup/Topic/'.freeze, attribute: false) diff --git a/modules/bcf/spec/api/v3/work_packages/work_package_representer_spec.rb b/modules/bcf/spec/api/v3/work_packages/work_package_representer_spec.rb index 176133d6f8..424250fa57 100644 --- a/modules/bcf/spec/api/v3/work_packages/work_package_representer_spec.rb +++ b/modules/bcf/spec/api/v3/work_packages/work_package_representer_spec.rb @@ -49,10 +49,10 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do Structural IT Development 2015-06-21T12:00:00Z - dangl@iabi.eu + mike@example.com 2015-06-21T14:22:47Z - dangl@iabi.eu - linhard@iabi.eu + mike@example.com + andy@example.com This is a topic with all informations present. JsonElement.json @@ -70,29 +70,29 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do 2015-08-31T12:40:17Z - dangl@iabi.eu + mike@example.com This is an unmodified topic at the uppermost hierarchical level. All times in the XML are marked as UTC times. 2015-08-31T14:00:01Z - dangl@iabi.eu + mike@example.com 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. 2015-08-31T13:07:11Z - dangl@iabi.eu + mike@example.com This comment again is in the highest hierarchy level. It references a viewpoint. 2015-08-31T15:42:58Z - dangl@iabi.eu + mike@example.com This comment contained some spllng errs. Hopefully, the modifier did catch them all. 2015-08-31T16:07:11Z - dangl@iabi.eu + mike@example.com Viewpoint_8dc86298-9737-40b4-a448-98a9e953293a.bcfv @@ -140,5 +140,15 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do .including('id') .at_path('bcf/viewpoints/') end + + it "contains topic labels" do + is_expected.to be_json_eql([ + { + labels: ["Structural", "IT Development"] + } + ].to_json) + .including('id') + .at_path('bcf/topic/') + end end end diff --git a/modules/bcf/spec/bcf/bcf_xml/issue_writer_spec.rb b/modules/bcf/spec/bcf/bcf_xml/issue_writer_spec.rb index e650089609..78f93a0b5f 100644 --- a/modules/bcf/spec/bcf/bcf_xml/issue_writer_spec.rb +++ b/modules/bcf/spec/bcf/bcf_xml/issue_writer_spec.rb @@ -39,10 +39,10 @@ describe ::OpenProject::Bcf::BcfXml::IssueWriter do Structural IT Development 2015-06-21T12:00:00Z - dangl@iabi.eu + mike@example.com 2015-06-21T14:22:47Z - dangl@iabi.eu - linhard@iabi.eu + mike@example.com + andy@example.com This is a topic with all informations present. JsonElement.json @@ -60,29 +60,29 @@ describe ::OpenProject::Bcf::BcfXml::IssueWriter do 2015-08-31T12:40:17Z - dangl@iabi.eu + mike@example.com This is an unmodified topic at the uppermost hierarchical level. All times in the XML are marked as UTC times. 2015-08-31T14:00:01Z - dangl@iabi.eu + mike@example.com 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. 2015-08-31T13:07:11Z - dangl@iabi.eu + mike@example.com This comment again is in the highest hierarchy level. It references a viewpoint. 2015-08-31T15:42:58Z - dangl@iabi.eu + mike@example.com This comment contained some spllng errs. Hopefully, the modifier did catch them all. 2015-08-31T16:07:11Z - dangl@iabi.eu + mike@example.com Viewpoint_8dc86298-9737-40b4-a448-98a9e953293a.bcfv diff --git a/modules/bcf/spec/bcf/bcf_xml/markup_extractor_spec.rb b/modules/bcf/spec/bcf/bcf_xml/markup_extractor_spec.rb new file mode 100644 index 0000000000..0c475abe46 --- /dev/null +++ b/modules/bcf/spec/bcf/bcf_xml/markup_extractor_spec.rb @@ -0,0 +1,102 @@ +#-- copyright +# OpenProject Costs Plugin +# +# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# version 3. +# +# 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. +#++ + +require 'spec_helper' + +describe ::OpenProject::Bcf::BcfXml::MarkupExtractor do + let(:filename) { 'MaximumInformation.bcf' } + let(:file) do + Rack::Test::UploadedFile.new( + File.join(Rails.root, "modules/bcf/spec/fixtures/files/#{filename}"), + 'application/octet-stream') + end + let(:entries) do + Zip::File.open(file) do |zip| + zip.select { |entry| entry.name.end_with?('markup.bcf') } + end + end + + subject { described_class.new entries.second } + + it '#initialize' do + expect(subject).to be_a described_class + expect(subject.markup).to be_a String + expect(subject.doc).to be_a Nokogiri::XML::Document + end + + it '#uuid' do + expect(subject.uuid).to be_eql '63E78882-7C6A-4BF7-8982-FC478AFB9C97' + end + + it '#title' do + expect(subject.title).to be_eql 'Maximum Content' + end + + it '#priority' do + expect(subject.priority).to be_eql 'High' + end + + it '#status' do + expect(subject.status).to be_eql 'Open' + end + + it '#description' do + expect(subject.description).to be_eql 'This is a topic with all informations present.' + end + + it '#author' do + expect(subject.author).to be_eql 'mike@example.com' + end + + it '#assignee' do + expect(subject.assignee).to be_eql 'andy@example.com' + end + + it '#due_date' do + expect(subject.due_date).to be_nil + end + + it '#viewpoints' do + expect(subject.viewpoints.size).to eql 3 + expect(subject.viewpoints.first[:uuid]).to eql '8dc86298-9737-40b4-a448-98a9e953293a' + expect(subject.viewpoints.first[:viewpoint]).to eql 'Viewpoint_8dc86298-9737-40b4-a448-98a9e953293a.bcfv' + expect(subject.viewpoints.first[:snapshot]).to eql 'Snapshot_8dc86298-9737-40b4-a448-98a9e953293a.png' + end + + it '#comments' do + expect(subject.comments.size).to eql 4 + expect(subject.comments.first[:uuid]).to eql '780FAE52-C432-42BE-ADEA-FF3E7A8CD8E1' + expect(subject.comments.first[:date]).to eql '2015-08-31T12:40:17Z' + expect(subject.comments.first[:author]).to eql 'mike@example.com' + expect(subject.comments.first[:comment]).to eql 'This is an unmodified topic at the uppermost hierarchical level. +All times in the XML are marked as UTC times.' + end + + it '#people' do + expect(subject.people.size).to eql 2 + expect(subject.people.first).to eql 'andy@example.com' + expect(subject.people.second).to eql 'mike@example.com' + end + + it '#mail_addresses' do + expect(subject.mail_addresses.size).to eql 2 + expect(subject.mail_addresses.first).to eql 'andy@example.com' + expect(subject.mail_addresses.second).to eql 'mike@example.com' + end +end diff --git a/modules/bcf/spec/controllers/issues_controller_spec.rb b/modules/bcf/spec/controllers/issues_controller_spec.rb index e69de29bb2..714849b718 100644 --- a/modules/bcf/spec/controllers/issues_controller_spec.rb +++ b/modules/bcf/spec/controllers/issues_controller_spec.rb @@ -0,0 +1,88 @@ +#-- 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::IssuesController, type: :controller do + let(:manage_bcf_role) { FactoryBot.create(:role, permissions: %i[manage_bcf view_linked_issues view_work_packages]) } + let(:collaborator_role) {FactoryBot.create(:role, permissions: %i[view_linked_issues view_work_packages])} + let(:bcf_manager) { FactoryBot.create(:user) } + let(:collaborator) { FactoryBot.create(:user) } + + let(:non_member) { FactoryBot.create(:user) } + let(:project) { FactoryBot.create(:project, + identifier: 'bim_project' + ) } + let(:member) { + FactoryBot.create(:member, + project: project, + user: collaborator, + roles: [collaborator_role]) + } + let(:bcf_manager_member) { + FactoryBot.create(:member, + project: project, + user: bcf_manager, + roles: [manage_bcf_role]) + } + before do + bcf_manager_member + member + allow(User).to receive(:current).and_return(bcf_manager) + end + + describe '#prepare_import' do + let(:params) { { project_id: project.identifier.to_s, + bcf_file: file} } + + let(:action) do + post :prepare_import, params: params + end + + context 'with valid BCF file' do + let(:filename) { 'MaximumInformation.bcf' } + let(:file) { Rack::Test::UploadedFile.new( + File.join(Rails.root, "modules/bcf/spec/fixtures/files/#{filename}"), + 'application/octet-stream') } + + it 'should be successful' do + expect { action }.to change { Attachment.count }.by(1) + expect(response).to be_successful + end + end + + context 'with invalid BCF file' do + let(:file) { FileHelpers.mock_uploaded_file } + + it 'should redirect back to where we started from' do + expect { action }.to change { Attachment.count }.by(1) + expect(response).to redirect_to '/projects/bim_project/issues' + end + end + end +end diff --git a/modules/bcf/spec/fixtures/files/MaximumInformation.bcf b/modules/bcf/spec/fixtures/files/MaximumInformation.bcf index d79df69448..d1fdbc9106 100644 Binary files a/modules/bcf/spec/fixtures/files/MaximumInformation.bcf and b/modules/bcf/spec/fixtures/files/MaximumInformation.bcf differ