parent
d86aded425
commit
eb4d7a19d9
@ -0,0 +1,10 @@ |
||||
class Projects::Export < Export |
||||
acts_as_attachable view_permission: :view_project, |
||||
add_permission: :view_project, |
||||
delete_permission: :view_project, |
||||
only_user_allowed: true |
||||
|
||||
def ready? |
||||
attachments.any? |
||||
end |
||||
end |
@ -1,35 +0,0 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
module Projects::Exports |
||||
register = ::Exports::Register |
||||
|
||||
register.list(Project, Projects::Exports::CSV) |
||||
end |
@ -0,0 +1,117 @@ |
||||
require 'active_storage/filename' |
||||
|
||||
module Exports |
||||
class ExportJob < ::ApplicationJob |
||||
def perform(export:, user:, mime_type:, query:, **options) |
||||
self.export = export |
||||
self.current_user = user |
||||
self.mime_type = mime_type |
||||
self.query = query |
||||
self.options = options |
||||
|
||||
User.execute_as(user) do |
||||
prepare! |
||||
export! |
||||
schedule_cleanup |
||||
rescue StandardError => e |
||||
Rails.logger.error "Failed to run export job for #{user}: #{e.message}" |
||||
raise e |
||||
end |
||||
end |
||||
|
||||
def status_reference |
||||
arguments.first[:export] |
||||
end |
||||
|
||||
def updates_own_status? |
||||
true |
||||
end |
||||
|
||||
protected |
||||
|
||||
class_attribute :model |
||||
|
||||
attr_accessor :export, :current_user, :mime_type, :query, :options |
||||
|
||||
def prepare! |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
def export! |
||||
exporter_instance.export! do |result| |
||||
handle_export_result(export, result) |
||||
end |
||||
end |
||||
|
||||
def exporter_instance |
||||
::Exports::Register |
||||
.list_exporter(model, mime_type) |
||||
.new(query, options) |
||||
end |
||||
|
||||
def handle_export_result(export, result) |
||||
case result.content |
||||
when File |
||||
store_attachment(export, result.content) |
||||
when Tempfile |
||||
store_from_tempfile(export, result) |
||||
else |
||||
store_from_string(export, result) |
||||
end |
||||
end |
||||
|
||||
def store_from_tempfile(export, export_result) |
||||
renamed_file_path = target_file_name(export_result) |
||||
File.rename(export_result.content.path, renamed_file_path) |
||||
file = File.open(renamed_file_path) |
||||
store_attachment(export, file) |
||||
file.close |
||||
end |
||||
|
||||
## |
||||
# Create a target file name, replacing any invalid characters |
||||
def target_file_name(export_result) |
||||
target_name = ActiveStorage::Filename.new(export_result.title).sanitized |
||||
File.join(File.dirname(export_result.content.path), target_name) |
||||
end |
||||
|
||||
def schedule_cleanup |
||||
CleanupOutdatedJob.perform_after_grace |
||||
end |
||||
|
||||
def store_from_string(export, export_result) |
||||
with_tempfile(export_result.title, export_result.content) do |file| |
||||
store_attachment(export, file) |
||||
end |
||||
end |
||||
|
||||
def with_tempfile(title, content) |
||||
name_parts = [title[0..title.rindex('.') - 1], title[title.rindex('.')..-1]] |
||||
|
||||
Tempfile.create(name_parts, encoding: content.encoding) do |file| |
||||
file.write content |
||||
|
||||
yield file |
||||
end |
||||
end |
||||
|
||||
def store_attachment(container, file) |
||||
call = Attachments::CreateService |
||||
.bypass_whitelist(user: User.current) |
||||
.call(container: container, file: file, filename: File.basename(file), description: '') |
||||
|
||||
call.on_success do |
||||
download_url = ::API::V3::Utilities::PathHelper::ApiV3Path.attachment_content(call.result.id) |
||||
|
||||
upsert_status status: :success, |
||||
message: I18n.t('export.succeeded'), |
||||
payload: download_payload(download_url) |
||||
end |
||||
|
||||
call.on_failure do |
||||
upsert_status status: :failure, |
||||
message: I18n.t('export.failed', message: call.message) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,13 @@ |
||||
require 'active_storage/filename' |
||||
|
||||
module Projects |
||||
class ExportJob < ::Exports::ExportJob |
||||
self.model = Project |
||||
|
||||
private |
||||
|
||||
def prepare! |
||||
self.query = Marshal.load(query) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,27 @@ |
||||
require 'active_storage/filename' |
||||
|
||||
module WorkPackages |
||||
class ExportJob < ::Exports::ExportJob |
||||
self.model = WorkPackage |
||||
|
||||
def title |
||||
I18n.t('export.your_work_packages_export') |
||||
end |
||||
|
||||
private |
||||
|
||||
def prepare! |
||||
self.query = set_query_props(query || Query.new, options[:query_attributes]) |
||||
end |
||||
|
||||
def set_query_props(query, query_attributes) |
||||
filters = query_attributes.delete('filters') |
||||
filters = Queries::WorkPackages::FilterSerializer.load(filters) |
||||
|
||||
query.tap do |q| |
||||
q.attributes = query_attributes |
||||
q.filters = filters |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,116 +0,0 @@ |
||||
require 'active_storage/filename' |
||||
|
||||
module WorkPackages |
||||
module Exports |
||||
class ExportJob < ::ApplicationJob |
||||
def perform(export:, user:, mime_type:, query:, query_attributes:, options:) |
||||
User.execute_as user do |
||||
query = set_query_props(query || Query.new, query_attributes) |
||||
export_work_packages(export, mime_type, query, options) |
||||
|
||||
schedule_cleanup |
||||
end |
||||
end |
||||
|
||||
def status_reference |
||||
arguments.first[:export] |
||||
end |
||||
|
||||
def updates_own_status? |
||||
true |
||||
end |
||||
|
||||
protected |
||||
|
||||
def title |
||||
I18n.t('export.your_work_packages_export') |
||||
end |
||||
|
||||
private |
||||
|
||||
def export_work_packages(export, mime_type, query, options) |
||||
::Exports::Register |
||||
.list_exporter(WorkPackage, mime_type) |
||||
.new(query, options) |
||||
.export! do |result| |
||||
handle_export_result(export, result) |
||||
end |
||||
end |
||||
|
||||
def handle_export_result(export, result) |
||||
case result.content |
||||
when File |
||||
store_attachment(export, result.content) |
||||
when Tempfile |
||||
store_from_tempfile(export, result) |
||||
else |
||||
store_from_string(export, result) |
||||
end |
||||
end |
||||
|
||||
def store_from_tempfile(export, export_result) |
||||
renamed_file_path = target_file_name(export_result) |
||||
File.rename(export_result.content.path, renamed_file_path) |
||||
file = File.open(renamed_file_path) |
||||
store_attachment(export, file) |
||||
file.close |
||||
end |
||||
|
||||
## |
||||
# Create a target file name, replacing any invalid characters |
||||
def target_file_name(export_result) |
||||
target_name = ActiveStorage::Filename.new(export_result.title).sanitized |
||||
File.join(File.dirname(export_result.content.path), target_name) |
||||
end |
||||
|
||||
def schedule_cleanup |
||||
::WorkPackages::Exports::CleanupOutdatedJob.perform_after_grace |
||||
end |
||||
|
||||
def set_query_props(query, query_attributes) |
||||
filters = query_attributes.delete('filters') |
||||
filters = Queries::WorkPackages::FilterSerializer.load(filters) |
||||
|
||||
query.tap do |q| |
||||
q.attributes = query_attributes |
||||
q.filters = filters |
||||
end |
||||
end |
||||
|
||||
def store_from_string(export, export_result) |
||||
with_tempfile(export_result.title, export_result.content) do |file| |
||||
store_attachment(export, file) |
||||
end |
||||
end |
||||
|
||||
def with_tempfile(title, content) |
||||
name_parts = [title[0..title.rindex('.') - 1], title[title.rindex('.')..-1]] |
||||
|
||||
Tempfile.create(name_parts, encoding: content.encoding) do |file| |
||||
file.write content |
||||
|
||||
yield file |
||||
end |
||||
end |
||||
|
||||
def store_attachment(container, file) |
||||
call = Attachments::CreateService |
||||
.bypass_whitelist(user: User.current) |
||||
.call(container: container, file: file, filename: File.basename(file), description: '') |
||||
|
||||
call.on_success do |
||||
download_url = ::API::V3::Utilities::PathHelper::ApiV3Path.attachment_content(call.result.id) |
||||
|
||||
upsert_status status: :success, |
||||
message: I18n.t('export.succeeded'), |
||||
payload: download_payload(download_url) |
||||
end |
||||
|
||||
call.on_failure do |
||||
upsert_status status: :failure, |
||||
message: I18n.t('export.failed', message: call.message) |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,15 @@ |
||||
OpenProject::Application.configure do |application| |
||||
application.config.to_prepare do |
||||
::Exports::Register.register do |
||||
list WorkPackage, WorkPackage::Exports::CSV |
||||
list WorkPackage, ::WorkPackage::PDFExport::WorkPackageListToPdf |
||||
|
||||
single WorkPackage, ::WorkPackage::PDFExport::WorkPackageToPdf |
||||
|
||||
formatter WorkPackage, WorkPackage::Exports::Formatters::Costs |
||||
formatter WorkPackage, WorkPackage::Exports::Formatters::EstimatedHours |
||||
|
||||
list Project, Projects::Exports::CSV |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,85 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require 'features/work_packages/work_packages_page' |
||||
|
||||
describe 'project export', type: :feature, js: true do |
||||
shared_let(:project1) { FactoryBot.create :project } |
||||
shared_let(:project2) { FactoryBot.create :project } |
||||
shared_let(:admin) { FactoryBot.create :admin } |
||||
|
||||
let(:index_page) { ::Pages::Projects::Index.new } |
||||
|
||||
let(:current_user) { admin } |
||||
|
||||
before do |
||||
@download_list = DownloadList.new |
||||
|
||||
login_as(current_user) |
||||
|
||||
index_page.visit! |
||||
end |
||||
|
||||
after do |
||||
DownloadList.clear |
||||
end |
||||
|
||||
subject { @download_list.refresh_from(page).latest_downloaded_content } |
||||
|
||||
def export!(expect_success = true) |
||||
index_page.click_more_menu_item 'Export' |
||||
click_on export_type |
||||
|
||||
# Expect to get a response regarding queuing |
||||
expect(page).to have_content I18n.t('js.job_status.generic_messages.in_queue'), |
||||
wait: 10 |
||||
|
||||
begin |
||||
perform_enqueued_jobs |
||||
rescue StandardError |
||||
# nothing |
||||
end |
||||
|
||||
if expect_success |
||||
expect(page).to have_text("The export has completed successfully") |
||||
end |
||||
end |
||||
|
||||
describe 'CSV export' do |
||||
let(:export_type) { 'CSV' } |
||||
|
||||
it 'exports the visible projects' do |
||||
expect(page).to have_selector('td.name', text: project1.name) |
||||
|
||||
export! |
||||
|
||||
expect(subject).to have_text(project1.name) |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue