Skip to content

Commit

Permalink
Merge pull request #13735 from opf/feature/49977-pdf-export-single-wo…
Browse files Browse the repository at this point in the history
…rk-package-include-all-attributes-and-fields-according-to-the-work-package-type-form-configuration

[#49977] PDF export (single work package): Include all attributes and fields according to the work package type form configuration
  • Loading branch information
as-op authored Nov 6, 2023
2 parents 546c969 + 8b5f3d9 commit 808cb5b
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 WorkPackage::Exports
module Formatters
class DerivedRemainingHours < ::Exports::Formatters::Default
def self.apply?(name, _export_format)
%i[derived_remaining_time derived_remaining_hours].include?(name.to_sym)
end

def format(work_package, **)
formatted_derived_hours(work_package)
end

private

def formatted_hours(value)
value.nil? ? nil : "#{value} #{I18n.t('export.units.hours')}"
end

def formatted_derived_hours(work_package)
if (derived_estimated_value = work_package.derived_estimated_hours)
formatted_hours(derived_estimated_value)
end
end
end
end
end
49 changes: 49 additions & 0 deletions app/models/work_package/exports/formatters/spent_units.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 WorkPackage::Exports
module Formatters
class SpentUnits < ::Exports::Formatters::Default
def self.apply?(name, _export_format)
%i[costs_by_type spent_units].include?(name.to_sym)
end

def format(work_package, **)
cost_helper = ::Costs::AttributesHelper.new(work_package, User.current)
values = cost_helper.summarized_cost_entries.map do |kvp|
cost_type = kvp[0]
volume = kvp[1]
type_unit = volume.to_d == 1.0.to_d ? cost_type.unit : cost_type.unit_plural
"#{volume} #{type_unit}"
end
return nil if values.empty?

values.join(', ')
end
end
end
end
68 changes: 41 additions & 27 deletions app/models/work_package/pdf_export/work_package_detail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,49 +103,63 @@ def attribute_table_rows(work_package)

def attribute_data_list(work_package)
list = if respond_to?(:column_objects)
attributes_list_by_columns
attributes_data_by_columns
else
attributes_list_by_wp(work_package)
attributes_data_by_wp(work_package)
end
list
.map { |entry| entry.merge({ value: entry[:value] || get_column_value_cell(work_package, entry[:name]) }) }
.select { |attribute_data| can_show_attribute?(attribute_data) }
.map { |entry| entry.merge({ value: get_column_value_cell(work_package, entry[:name]) }) }
end

def can_show_attribute?(attribute_data)
attribute_data[:value].present?
end

def attributes_list_by_columns
def attributes_data_by_columns
column_objects
.reject { |column| column.name == :subject }
.map do |column|
{ label: column.caption || '', name: column.name }
end
end

def attributes_list_by_wp(work_package)
list = ::Query.available_columns(work_package.project)
.reject { |column| %i[subject project].include?(column.name) }
.map do |column|
{ label: column.caption || '', name: column.name }
end
spent_units = costs_attribute_spent_units(work_package)
list << spent_units unless spent_units.nil?
list
def attributes_data_by_wp(work_package)
column_entries(%i[id type status])
.concat(form_configuration_columns(work_package))
end

def form_configuration_columns(work_package)
work_package
.type.attribute_groups
.filter { |group| group.is_a?(Type::AttributeGroup) }
.map do |group|
group.attributes.map do |form_key|
form_key_to_column_entries(form_key.to_sym, work_package)
end
end.flatten
end

def costs_attribute_spent_units(work_package)
cost_helper = ::Costs::AttributesHelper.new(work_package, User.current)
values = cost_helper.summarized_cost_entries.map do |kvp|
cost_type = kvp[0]
volume = kvp[1]
type_unit = volume.to_d == 1.0.to_d ? cost_type.unit : cost_type.unit_plural
"#{volume} #{type_unit}"
def form_key_to_column_entries(form_key, work_package)
if CustomField.custom_field_attribute? form_key
id = form_key.to_s.sub('custom_field_', '').to_i
cf = CustomField.find_by(id:)
return [] if cf.nil? || cf.formattable?

return [{ label: cf.name || form_key, name: form_key }]
end

if form_key == :date
column_entries(%i[start_date due_date duration])
elsif form_key == :bcf_thumbnail
[]
else
column_name = ::API::Utilities::PropertyNameConverter.to_ar_name(form_key, context: work_package)
[column_entry(column_name)]
end
return nil if values.empty?
end

def column_entries(column_names)
column_names.map { |key| column_entry(key) }
end

{ label: I18n.t('activerecord.attributes.work_package.spent_units'), name: :spent_units, value: values.join(', ') }
def column_entry(column_name)
{ label: WorkPackage.human_attribute_name(column_name), name: column_name }
end

def build_columns_table_cells(attribute_data)
Expand Down
2 changes: 2 additions & 0 deletions config/initializers/export_formats.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
single WorkPackage, WorkPackage::PDFExport::WorkPackageToPdf

formatter WorkPackage, WorkPackage::Exports::Formatters::EstimatedHours
formatter WorkPackage, WorkPackage::Exports::Formatters::DerivedRemainingHours
formatter WorkPackage, WorkPackage::Exports::Formatters::SpentUnits
formatter WorkPackage, WorkPackage::Exports::Formatters::Hours
formatter WorkPackage, WorkPackage::Exports::Formatters::Days
formatter WorkPackage, WorkPackage::Exports::Formatters::Currency
Expand Down
43 changes: 30 additions & 13 deletions spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,17 @@
RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do
include Redmine::I18n
include PDFExportSpecUtils
let(:type) { create(:type_bug) }
let(:project) { create(:project, name: 'Foo Bla. Report No. 4/2021 with/for Case 42', types: [type]) }
let(:type) do
create(:type_bug, custom_fields: [long_text_custom_field]).tap do |t|
t.attribute_groups.first.attributes.push(long_text_custom_field.attribute_name)
end
end
let(:project) do
create(:project,
name: 'Foo Bla. Report No. 4/2021 with/for Case 42',
types: [type],
work_package_custom_fields: [long_text_custom_field])
end
let(:user) do
create(:user,
member_with_permissions: { project => %w[view_work_packages export_work_packages] })
Expand All @@ -42,6 +51,7 @@
let(:image_path) { Rails.root.join("spec/fixtures/files/image.png") }
let(:image_attachment) { Attachment.new author: user, file: File.open(image_path) }
let(:attachments) { [image_attachment] }
let(:long_text_custom_field) { create(:issue_custom_field, :text, name: 'LongText') }
let(:work_package) do
description = <<~DESCRIPTION
**Lorem** _ipsum_ ~~dolor~~ `sit` [amet](https://example.com/), consetetur sadipscing elitr.
Expand All @@ -62,16 +72,20 @@
type:,
subject: 'Work package 1',
story_points: 1,
description:).tap do |wp|
description:,
custom_values: { long_text_custom_field.id => 'foo' }).tap do |wp|
allow(wp)
.to receive(:attachments)
.and_return attachments
end
end
let(:options) { {} }
let(:exporter) do
described_class.new(work_package, options)
end
let(:export) do
login_as(user)
described_class.new(work_package, options)
exporter
end
let(:export_pdf) do
Timecop.freeze(export_time) do
Expand All @@ -95,23 +109,26 @@ def get_column_value(column_name)

describe 'with a request for a PDF' do
it 'contains correct data' do
details = Query.available_columns(work_package.project)
.reject { |column| %i[subject project].include?(column.name) }
.flat_map do |column|
value = get_column_value(column.name)
value.blank? ? [] : [column.caption.upcase, value]
details = exporter.send(:attributes_data_by_wp, work_package)
.flat_map do |item|
value = get_column_value(item[:name])
result = [item[:label].upcase]
result << value if value.present?
result
end

expect(pdf[:strings]).to eq([
# Joining the results for comparison since word wrapping leads to a different array for the same content
expect(pdf[:strings].join(' ')).to eq([
"#{type.name} ##{work_package.id} - #{work_package.subject}",
*details,
label_title(:description),
'Lorem', ' ', 'ipsum', ' ', 'dolor', ' ', 'sit', ' ',
'amet', ', consetetur sadipscing elitr.', ' ', '@OpenProject Admin',
'Image Caption',
'Foo',
'1', export_time_formatted, project.name
])
'1', export_time_formatted, project.name,
'LongText', 'foo',
'2', export_time_formatted, project.name
].join(' '))
expect(pdf[:images].length).to eq(2)
end
end
Expand Down

0 comments on commit 808cb5b

Please sign in to comment.