From 10994f8af980f583a8c575bdc665b96908d97f46 Mon Sep 17 00:00:00 2001
From: as-op
Date: Mon, 7 Jul 2025 17:01:14 +0200
Subject: [PATCH 01/24] [#65401] Project phase field shows in pdf report even
when it doesn't show on wp
https://community.openproject.org/work_packages/65401
---
app/models/work_package/exports/attributes.rb | 38 +++++++++++++++++++
.../work_package/exports/macros/attributes.rb | 3 ++
.../pdf_export/export/wp/attributes.rb | 6 +++
3 files changed, 47 insertions(+)
create mode 100644 app/models/work_package/exports/attributes.rb
diff --git a/app/models/work_package/exports/attributes.rb b/app/models/work_package/exports/attributes.rb
new file mode 100644
index 00000000000..08497bfe4d9
--- /dev/null
+++ b/app/models/work_package/exports/attributes.rb
@@ -0,0 +1,38 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 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 Attributes
+ def user_allowed_in_view_attribute?(obj, attribute_name)
+ if attribute_name.to_sym == :project_phase && obj.is_a?(WorkPackage)
+ User.current.allowed_in_project?(:view_project_phases, obj.project)
+ else
+ true
+ end
+ end
+ end
+end
diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb
index 71f7947b3d4..2d4b0d4d67c 100644
--- a/app/models/work_package/exports/macros/attributes.rb
+++ b/app/models/work_package/exports/macros/attributes.rb
@@ -45,6 +45,7 @@ module WorkPackage::Exports
# projectValue:1234:active # Outputs project with id 1234 value for "active"
# projectValue:my-project-identifier:active # Outputs project with identifier my-project-identifier value for "active"
class Attributes < OpenProject::TextFormatting::Matchers::RegexMatcher
+ extend WorkPackage::Exports::Attributes
DISABLED_PROJECT_RICH_TEXT_FIELDS = %i[description status_explanation status_description].freeze
DISABLED_WORK_PACKAGE_RICH_TEXT_FIELDS = %i[description].freeze
@@ -154,6 +155,8 @@ module WorkPackage::Exports
else
"cf_#{cf.id}"
end
+
+ return "" if cf.nil? && !user_allowed_in_view_attribute?(obj, ar_name)
return msg_macro_error_rich_text if disabled_rich_text_fields.include?(ar_name.to_sym)
format_attribute_value(ar_name, obj.class, obj)
diff --git a/app/models/work_package/pdf_export/export/wp/attributes.rb b/app/models/work_package/pdf_export/export/wp/attributes.rb
index bb41a95233f..445b4eb7961 100644
--- a/app/models/work_package/pdf_export/export/wp/attributes.rb
+++ b/app/models/work_package/pdf_export/export/wp/attributes.rb
@@ -29,6 +29,8 @@
#++
module WorkPackage::PDFExport::Export::Wp::Attributes
+ include WorkPackage::Exports::Attributes
+
def write_attributes!(work_package)
work_package
.type.attribute_groups
@@ -138,6 +140,10 @@ module WorkPackage::PDFExport::Export::Wp::Attributes
current_part = { type: :attribute, list: [] }
parts = [current_part]
group.attributes.each do |form_key|
+ if !CustomField.custom_field_attribute?(form_key) && !user_allowed_in_view_attribute?(work_package, form_key)
+ next
+ end
+
if allowed_long_text_custom_field?(form_key, work_package)
cf = form_key_to_custom_field(form_key)
if current_part[:type] == :long_text
From 06174472868371a6cfcb8eafe0d94fa490f21903 Mon Sep 17 00:00:00 2001
From: as-op
Date: Tue, 8 Jul 2025 11:26:18 +0200
Subject: [PATCH 02/24] pdf export: hide project phase attribute if there is no
project phase active in the project
---
app/models/work_package/exports/attributes.rb | 9 +++++++--
app/models/work_package/exports/macros/attributes.rb | 2 +-
.../work_package/pdf_export/export/wp/attributes.rb | 2 +-
3 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/app/models/work_package/exports/attributes.rb b/app/models/work_package/exports/attributes.rb
index 08497bfe4d9..48f442063d2 100644
--- a/app/models/work_package/exports/attributes.rb
+++ b/app/models/work_package/exports/attributes.rb
@@ -27,9 +27,14 @@
#++
module WorkPackage::Exports
module Attributes
- def user_allowed_in_view_attribute?(obj, attribute_name)
+ def user_allowed_view_wp_project_phase?(work_package)
+ User.current.allowed_in_project?(:view_project_phases, work_package.project) &&
+ work_package.project.phases.active.any?
+ end
+
+ def user_allowed_view_attribute?(obj, attribute_name)
if attribute_name.to_sym == :project_phase && obj.is_a?(WorkPackage)
- User.current.allowed_in_project?(:view_project_phases, obj.project)
+ user_allowed_view_wp_project_phase?(obj)
else
true
end
diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb
index 2d4b0d4d67c..f68eaccdcfa 100644
--- a/app/models/work_package/exports/macros/attributes.rb
+++ b/app/models/work_package/exports/macros/attributes.rb
@@ -156,7 +156,7 @@ module WorkPackage::Exports
"cf_#{cf.id}"
end
- return "" if cf.nil? && !user_allowed_in_view_attribute?(obj, ar_name)
+ return "" if cf.nil? && !user_allowed_view_attribute?(obj, ar_name)
return msg_macro_error_rich_text if disabled_rich_text_fields.include?(ar_name.to_sym)
format_attribute_value(ar_name, obj.class, obj)
diff --git a/app/models/work_package/pdf_export/export/wp/attributes.rb b/app/models/work_package/pdf_export/export/wp/attributes.rb
index 445b4eb7961..86d0f4887b4 100644
--- a/app/models/work_package/pdf_export/export/wp/attributes.rb
+++ b/app/models/work_package/pdf_export/export/wp/attributes.rb
@@ -140,7 +140,7 @@ module WorkPackage::PDFExport::Export::Wp::Attributes
current_part = { type: :attribute, list: [] }
parts = [current_part]
group.attributes.each do |form_key|
- if !CustomField.custom_field_attribute?(form_key) && !user_allowed_in_view_attribute?(work_package, form_key)
+ if !CustomField.custom_field_attribute?(form_key) && !user_allowed_view_attribute?(work_package, form_key)
next
end
From 1569d4f8b0341364a13a73e6878652d630aa8267 Mon Sep 17 00:00:00 2001
From: as-op
Date: Tue, 8 Jul 2025 12:18:25 +0200
Subject: [PATCH 03/24] do not throw errors in export for
I18n::MissingTranslationData
---
app/models/work_package/exports/attributes.rb | 3 +++
app/models/work_package/exports/macros/attributes.rb | 4 ++++
2 files changed, 7 insertions(+)
diff --git a/app/models/work_package/exports/attributes.rb b/app/models/work_package/exports/attributes.rb
index 48f442063d2..cd3d0c5004e 100644
--- a/app/models/work_package/exports/attributes.rb
+++ b/app/models/work_package/exports/attributes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -25,6 +27,7 @@
#
# See COPYRIGHT and LICENSE files for more details.
#++
+
module WorkPackage::Exports
module Attributes
def user_allowed_view_wp_project_phase?(work_package)
diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb
index f68eaccdcfa..eb032a68565 100644
--- a/app/models/work_package/exports/macros/attributes.rb
+++ b/app/models/work_package/exports/macros/attributes.rb
@@ -105,6 +105,10 @@ module WorkPackage::Exports
model.human_attribute_name(
::API::Utilities::PropertyNameConverter.to_ar_name(attribute.to_sym, context: model.new)
)
+ rescue I18n::MissingTranslationData
+ # If the translation is missing, we return the attribute name as a fallback
+ # # This can happen if the attribute is a custom field, the attribute is not translated or does not exist
+ attribute.to_s
end
def self.resolve_work_package_match(id, type, attribute, user)
From 5f9b6c57139ee6f6cfade07504300bd3a5d5942f Mon Sep 17 00:00:00 2001
From: as-op
Date: Tue, 8 Jul 2025 14:56:59 +0200
Subject: [PATCH 04/24] add specs for workPackageValue, workPackageLabel,
projectLabel, projectValue macros
---
.../pdf_export/common/macro_spec.rb | 412 ++++++++++++++++--
1 file changed, 382 insertions(+), 30 deletions(-)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index 9d32edcad91..2f78102c248 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -31,14 +31,52 @@
require "spec_helper"
RSpec.describe WorkPackage::PDFExport::Common::Macro do
- let(:work_package) do
- create(:work_package, id: 185, subject: "Work package 1")
+ shared_let(:type_task) { create(:type_task) }
+ shared_let(:custom_field) do
+ create(
+ :work_package_custom_field,
+ name: "Custom Field 1",
+ field_format: "string",
+ types: [type_task]
+ )
+ end
+ shared_let(:project_custom_field_section) { create(:project_custom_field_section) }
+ shared_let(:project_custom_field) do
+ create(:string_project_custom_field, name: "Project Custom Field 1", project_custom_field_section:)
+ end
+ shared_let(:project) do
+ create(
+ :project,
+ status_code: "on_track",
+ work_package_custom_fields: [custom_field],
+ project_custom_fields: [project_custom_field],
+ custom_field_values: { project_custom_field.id => "Project custom value 1" },
+ &:save!
+ )
+ end
+ shared_let(:work_package) do
+ create(
+ :work_package,
+ id: 185,
+ subject: "Work package 1",
+ type: type_task,
+ status: create(:status, name: "In Progress"), project: project,
+ custom_field_values: { custom_field.id => "Custom value 1" },
+ &:save!
+ )
+ end
+ shared_let(:formatter) { Class.new { extend WorkPackage::PDFExport::Common::Macro } }
+ shared_let(:user) do
+ create(:user, member_with_permissions: { project => %i[view_work_packages view_project_attributes view_project] })
+ end
+ let!(:markdown) { "" }
+
+ before do
+ User.current = user
end
- let(:markdown) { "" }
- let(:formatter) { Class.new { extend WorkPackage::PDFExport::Common::Macro } }
subject(:formatted) do
- formatter.apply_markdown_field_macros(markdown, { work_package: work_package, user: User.current })
+ formatter.apply_markdown_field_macros(markdown, { work_package: work_package, project: project, user: })
end
describe "empty text" do
@@ -47,48 +85,362 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
- describe "wp mention tag" do
- let(:markdown) { '#185' }
+ describe "wp mention macro" do
+ describe "with tag" do
+ let(:markdown) { '#185' }
- it "ignores the tag" do
- expect(formatted).to eq("\\#185\n")
+ it "ignores the tag" do
+ expect(formatted).to eq("\\#185\n")
+ end
+ end
+
+ describe "with plain" do
+ let(:markdown) { "#185" }
+
+ it "contains correct data" do
+ expect(formatted).to eq("#185\n")
+ end
+ end
+
+ describe "with markdown formating bold" do
+ let(:markdown) { "\n**#185**\n" }
+
+ it "contains correct data" do
+ expect(formatted).to eq("**#185**\n")
+ end
+ end
+
+ describe "with markdown formating strikethrough" do
+ let(:markdown) { "~~#185~~" }
+
+ it "contains correct data" do
+ expect(formatted).to eq("~~#185~~\n")
+ end
+ end
+
+ describe "with strikethrough in table" do
+ let(:markdown) { "" }
+
+ it "contains correct data" do
+ expect(formatted).to eq("\n")
+ end
end
end
- describe "wp mention plain" do
- let(:markdown) { "#185" }
+ describe "workPackageValue macro" do
+ describe "with current work package attribute" do
+ let(:markdown) { "workPackageValue:subject" }
- it "contains correct data" do
- expect(formatted).to eq("#185\n")
+ it "outputs the attribute value" do
+ expect(formatted).to eq("Work package 1\n")
+ end
+ end
+
+ describe "with specific work package ID and attribute" do
+ let(:markdown) { "workPackageValue:185:subject" }
+
+ it "outputs the attribute value for the specified work package" do
+ expect(formatted).to eq("Work package 1\n")
+ end
+ end
+
+ describe "with non-existent work package ID" do
+ let(:markdown) { "workPackageValue:999:subject" }
+
+ it "outputs an error message" do
+ expect(formatted).to include("Macro error, resource not found")
+ end
+ end
+
+ describe "with status attribute" do
+ let(:markdown) { "workPackageValue:status" }
+
+ it "outputs the status name" do
+ expect(formatted).to eq("In Progress\n")
+ end
+ end
+
+ describe "with custom field by name" do
+ let(:markdown) { "workPackageValue:\"Custom Field 1\"" }
+
+ it "outputs the custom field value" do
+ expect(formatted).to eq("Custom value 1\n")
+ end
+ end
+
+ describe "with specific work package ID and custom field" do
+ let(:markdown) { "workPackageValue:185:\"Custom Field 1\"" }
+
+ it "outputs the custom field value for the specified work package" do
+ expect(formatted).to eq("Custom value 1\n")
+ end
+ end
+
+ describe "with non-existent attribute" do
+ let(:markdown) { "workPackageValue:nonexistent_attribute" }
+
+ it "outputs an empty value" do
+ expect(formatted).to eq(" \n")
+ end
+ end
+
+ describe "with markdown formatting" do
+ let(:markdown) { "**workPackageValue:subject**" }
+
+ it "preserves the markdown formatting" do
+ expect(formatted).to eq("**Work package 1**\n")
+ end
+ end
+
+ describe "in a table" do
+ let(:markdown) { "" }
+
+ it "processes the macro inside HTML" do
+ expect(formatted).to eq("\n")
+ end
end
end
- describe "wp mention with markdown formating bold" do
- let(:markdown) { "\n**#185**\n" }
+ describe "workPackageLabel macro" do
+ describe "with current work package attribute" do
+ let(:markdown) { "workPackageLabel:subject" }
- it "contains correct data" do
- expect(formatted).to eq("**#185**\n")
+ it "outputs the attribute label" do
+ expect(formatted).to eq("Subject\n")
+ end
+ end
+
+ describe "with specific work package ID and attribute" do
+ let(:markdown) { "workPackageLabel:185:subject" }
+
+ it "outputs the attribute label for the specified work package" do
+ expect(formatted).to eq("Subject\n")
+ end
+ end
+
+ describe "with non-existent work package ID" do
+ let(:markdown) { "workPackageLabel:999:subject" }
+
+ it "outputs a humanized form" do
+ expect(formatted).to include("Subject")
+ end
+ end
+
+ describe "with status attribute" do
+ let(:markdown) { "workPackageLabel:status" }
+
+ it "outputs the status label" do
+ expect(formatted).to eq("Status\n")
+ end
+ end
+
+ describe "with custom field by name" do
+ let(:markdown) { "workPackageLabel:\"Custom Field 1\"" }
+
+ it "outputs the custom field name" do
+ expect(formatted).to include("Custom Field 1")
+ end
+ end
+
+ describe "with specific work package ID and custom field" do
+ let(:markdown) { "workPackageLabel:185:\"Custom Field 1\"" }
+
+ it "outputs the custom field name for the specified work package" do
+ expect(formatted).to include("Custom Field 1")
+ end
+ end
+
+ describe "with non-existent attribute" do
+ let(:markdown) { "workPackageLabel:nonexistent_attribute" }
+
+ it "outputs the humanized attribute name" do
+ expect(formatted).to include("nonexistent_attribute")
+ end
+ end
+
+ describe "with markdown formatting" do
+ let(:markdown) { "**workPackageLabel:subject**" }
+
+ it "preserves the markdown formatting" do
+ expect(formatted).to include("**Subject**")
+ end
+ end
+
+ describe "in a table" do
+ let(:markdown) { "" }
+
+ it "processes the macro inside HTML" do
+ expect(formatted).to eq("\n")
+ end
end
end
- describe "wp mention with markdown formating strikethrough" do
- let(:markdown) { "~~#185~~" }
+ describe "projectValue macro" do
+ describe "with current project attribute" do
+ let(:markdown) { "projectValue:name" }
- it "contains correct data" do
- expect(formatted).to eq("~~#185~~\n")
+ it "outputs the attribute value" do
+ expect(formatted).to eq("#{project.name}\n")
+ end
+ end
+
+ describe "with specific project ID and attribute" do
+ let(:markdown) { "projectValue:#{project.id}:name" }
+
+ it "outputs the attribute value for the specified project" do
+ expect(formatted).to eq("#{project.name}\n")
+ end
+ end
+
+ describe "with specific project identifier and attribute" do
+ let(:markdown) { "projectValue:\"#{project.identifier}\":name" }
+
+ it "outputs the attribute value for the specified project" do
+ expect(formatted).to eq("#{project.name}\n")
+ end
+ end
+
+ describe "with non-existent project ID" do
+ let(:markdown) { "projectValue:999:name" }
+
+ it "outputs an error message" do
+ expect(formatted).to include("Macro error, resource not found")
+ end
+ end
+
+ describe "with status attribute" do
+ let(:markdown) { "projectValue:status_code" }
+
+ it "outputs the status code" do
+ expect(formatted).to include(project.status_code)
+ end
+ end
+
+ describe "with custom field by name" do
+ let(:markdown) { "projectValue:\"Project Custom Field 1\"" }
+
+ it "outputs the custom field value" do
+ expect(formatted).to eq("Project custom value 1\n")
+ end
+ end
+
+ describe "with specific project ID and custom field" do
+ let(:markdown) { "projectValue:#{project.id}:\"Project Custom Field 1\"" }
+
+ it "outputs the custom field value for the specified project" do
+ expect(formatted).to eq("Project custom value 1\n")
+ end
+ end
+
+ describe "with non-existent attribute" do
+ let(:markdown) { "projectValue:nonexistent_attribute" }
+
+ it "outputs an empty value" do
+ expect(formatted).to eq(" \n")
+ end
+ end
+
+ describe "with markdown formatting" do
+ let(:markdown) { "**projectValue:name**" }
+
+ it "preserves the markdown formatting" do
+ expect(formatted).to eq("**#{project.name}**\n")
+ end
+ end
+
+ describe "in a table" do
+ let(:markdown) { "" }
+
+ it "processes the macro inside HTML" do
+ expect(formatted).to eq("\n")
+ end
end
end
- describe "wp mention with strikethrough in table" do
- let(:markdown) { "" }
+ describe "projectLabel macro" do
+ describe "with current project attribute" do
+ let(:markdown) { "projectLabel:name" }
- it "contains correct data" do
- expect(formatted).to eq("\n")
+ it "outputs the attribute label" do
+ expect(formatted).to eq("Name\n")
+ end
+ end
+
+ describe "with specific project ID and attribute" do
+ let(:markdown) { "projectLabel:#{project.id}:name" }
+
+ it "outputs the attribute label for the specified project" do
+ expect(formatted).to eq("Name\n")
+ end
+ end
+
+ describe "with specific project identifier and attribute" do
+ let(:markdown) { "projectLabel:\"#{project.identifier}\":name" }
+
+ it "outputs the attribute label for the specified project" do
+ expect(formatted).to eq("Name\n")
+ end
+ end
+
+ describe "with non-existent project ID" do
+ let(:markdown) { "projectLabel:999:name" }
+
+ it "outputs the attribute label" do
+ expect(formatted).to eq("Name\n")
+ end
+ end
+
+ describe "with status attribute" do
+ let(:markdown) { "projectLabel:status_code" }
+
+ it "outputs the status label" do
+ expect(formatted).to eq("Project status\n")
+ end
+ end
+
+ describe "with custom field by name" do
+ let(:markdown) { "projectLabel:\"Project Custom Field 1\"" }
+
+ it "outputs the custom field name" do
+ expect(formatted).to eq("Project Custom Field 1\n")
+ end
+ end
+
+ describe "with specific project ID and custom field" do
+ let(:markdown) { "projectLabel:#{project.id}:\"Project Custom Field 1\"" }
+
+ it "outputs the custom field name for the specified project" do
+ expect(formatted).to include("Project Custom Field 1")
+ end
+ end
+
+ describe "with non-existent attribute" do
+ let(:markdown) { "projectLabel:nonexistent_attribute" }
+
+ it "outputs the humanized attribute name" do
+ expect(formatted).to include("nonexistent_attribute")
+ end
+ end
+
+ describe "with markdown formatting" do
+ let(:markdown) { "**projectLabel:name**" }
+
+ it "preserves the markdown formatting" do
+ expect(formatted).to eq("**Name**\n")
+ end
+ end
+
+ describe "in a table" do
+ let(:markdown) { "" }
+
+ it "processes the macro inside HTML" do
+ expect(formatted).to eq("\n")
+ end
end
end
end
From 77226bed4269c414b1b8bc35a3a0c4f4cbbc357d Mon Sep 17 00:00:00 2001
From: as-op
Date: Tue, 8 Jul 2025 15:21:13 +0200
Subject: [PATCH 05/24] add specs for workPackageValue:project_phase
---
.../pdf_export/common/macro_spec.rb | 135 ++++++++++++------
1 file changed, 90 insertions(+), 45 deletions(-)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index 2f78102c248..a96e151730d 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -66,17 +66,25 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
)
end
shared_let(:formatter) { Class.new { extend WorkPackage::PDFExport::Common::Macro } }
- shared_let(:user) do
- create(:user, member_with_permissions: { project => %i[view_work_packages view_project_attributes view_project] })
+ let(:additional_permissions) { [] }
+ let(:user) do
+ create(
+ :user,
+ member_with_permissions: {
+ project => %i[view_work_packages view_project_attributes view_project] + additional_permissions
+ }
+ )
end
- let!(:markdown) { "" }
+ let(:markdown) { "" }
before do
User.current = user
end
subject(:formatted) do
- formatter.apply_markdown_field_macros(markdown, { work_package: work_package, project: project, user: })
+ formatter
+ .apply_markdown_field_macros(markdown, { work_package: work_package, project: project, user: })
+ .sub("\n", "")
end
describe "empty text" do
@@ -91,7 +99,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
it "ignores the tag" do
expect(formatted).to eq("\\#185\n")
+ "data-type=\"work_package\" data-text=\"#185\">\\#185")
end
end
@@ -100,7 +108,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
it "contains correct data" do
expect(formatted).to eq("#185\n")
+ "data-type=\"work_package\" data-text=\"#185\">#185")
end
end
@@ -109,7 +117,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
it "contains correct data" do
expect(formatted).to eq("**#185**\n")
+ "data-type=\"work_package\" data-text=\"#185\">#185**")
end
end
@@ -118,7 +126,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
it "contains correct data" do
expect(formatted).to eq("~~#185~~\n")
+ "data-type=\"work_package\" data-text=\"#185\">#185~~")
end
end
@@ -127,7 +135,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
it "contains correct data" do
expect(formatted).to eq("\n")
+ "data-type=\"work_package\" data-text=\"##185\">##185
")
end
end
end
@@ -137,7 +145,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageValue:subject" }
it "outputs the attribute value" do
- expect(formatted).to eq("Work package 1\n")
+ expect(formatted).to eq("Work package 1")
end
end
@@ -145,7 +153,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageValue:185:subject" }
it "outputs the attribute value for the specified work package" do
- expect(formatted).to eq("Work package 1\n")
+ expect(formatted).to eq("Work package 1")
end
end
@@ -161,7 +169,44 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageValue:status" }
it "outputs the status name" do
- expect(formatted).to eq("In Progress\n")
+ expect(formatted).to eq("In Progress")
+ end
+ end
+
+ describe "with project_phase attribute" do
+ let(:project_phase_active) { true }
+ let(:phase_definition) { create(:project_phase_definition, name: "Test Phase") }
+ let!(:project_phase) do
+ create(:project_phase, project: project, definition: phase_definition, active: project_phase_active)
+ end
+ let(:markdown) { "workPackageValue:project_phase" }
+
+ before do
+ work_package.update!(project_phase_definition_id: phase_definition.id)
+ end
+
+ describe "without the permission" do
+ it "outputs nothing" do
+ expect(formatted).to eq("")
+ end
+ end
+
+ describe "with the permission" do
+ let(:additional_permissions) { [:view_project_phases] }
+
+ describe "with active phase" do
+ it "outputs the project phase name" do
+ expect(formatted).to eq("Test Phase")
+ end
+ end
+
+ describe "without active phase" do
+ let(:project_phase_active) { false }
+
+ it "outputs the project phase name" do
+ expect(formatted).to eq("")
+ end
+ end
end
end
@@ -169,7 +214,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageValue:\"Custom Field 1\"" }
it "outputs the custom field value" do
- expect(formatted).to eq("Custom value 1\n")
+ expect(formatted).to eq("Custom value 1")
end
end
@@ -177,7 +222,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageValue:185:\"Custom Field 1\"" }
it "outputs the custom field value for the specified work package" do
- expect(formatted).to eq("Custom value 1\n")
+ expect(formatted).to eq("Custom value 1")
end
end
@@ -185,7 +230,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageValue:nonexistent_attribute" }
it "outputs an empty value" do
- expect(formatted).to eq(" \n")
+ expect(formatted).to eq(" ")
end
end
@@ -193,7 +238,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "**workPackageValue:subject**" }
it "preserves the markdown formatting" do
- expect(formatted).to eq("**Work package 1**\n")
+ expect(formatted).to eq("**Work package 1**")
end
end
@@ -201,7 +246,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "" }
it "processes the macro inside HTML" do
- expect(formatted).to eq("\n")
+ expect(formatted).to eq("")
end
end
end
@@ -211,7 +256,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageLabel:subject" }
it "outputs the attribute label" do
- expect(formatted).to eq("Subject\n")
+ expect(formatted).to eq("Subject")
end
end
@@ -219,7 +264,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageLabel:185:subject" }
it "outputs the attribute label for the specified work package" do
- expect(formatted).to eq("Subject\n")
+ expect(formatted).to eq("Subject")
end
end
@@ -227,7 +272,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageLabel:999:subject" }
it "outputs a humanized form" do
- expect(formatted).to include("Subject")
+ expect(formatted).to eq("Subject")
end
end
@@ -235,7 +280,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageLabel:status" }
it "outputs the status label" do
- expect(formatted).to eq("Status\n")
+ expect(formatted).to eq("Status")
end
end
@@ -243,7 +288,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageLabel:\"Custom Field 1\"" }
it "outputs the custom field name" do
- expect(formatted).to include("Custom Field 1")
+ expect(formatted).to eq("Custom Field 1")
end
end
@@ -251,7 +296,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageLabel:185:\"Custom Field 1\"" }
it "outputs the custom field name for the specified work package" do
- expect(formatted).to include("Custom Field 1")
+ expect(formatted).to eq("Custom Field 1")
end
end
@@ -259,7 +304,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageLabel:nonexistent_attribute" }
it "outputs the humanized attribute name" do
- expect(formatted).to include("nonexistent_attribute")
+ expect(formatted).to eq("nonexistent_attribute")
end
end
@@ -267,7 +312,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "**workPackageLabel:subject**" }
it "preserves the markdown formatting" do
- expect(formatted).to include("**Subject**")
+ expect(formatted).to eq("**Subject**")
end
end
@@ -275,7 +320,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "" }
it "processes the macro inside HTML" do
- expect(formatted).to eq("\n")
+ expect(formatted).to eq("")
end
end
end
@@ -285,7 +330,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectValue:name" }
it "outputs the attribute value" do
- expect(formatted).to eq("#{project.name}\n")
+ expect(formatted).to eq(project.name)
end
end
@@ -293,7 +338,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectValue:#{project.id}:name" }
it "outputs the attribute value for the specified project" do
- expect(formatted).to eq("#{project.name}\n")
+ expect(formatted).to eq(project.name)
end
end
@@ -301,7 +346,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectValue:\"#{project.identifier}\":name" }
it "outputs the attribute value for the specified project" do
- expect(formatted).to eq("#{project.name}\n")
+ expect(formatted).to eq(project.name)
end
end
@@ -317,7 +362,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectValue:status_code" }
it "outputs the status code" do
- expect(formatted).to include(project.status_code)
+ expect(formatted).to eq(project.status_code)
end
end
@@ -325,7 +370,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectValue:\"Project Custom Field 1\"" }
it "outputs the custom field value" do
- expect(formatted).to eq("Project custom value 1\n")
+ expect(formatted).to eq("Project custom value 1")
end
end
@@ -333,7 +378,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectValue:#{project.id}:\"Project Custom Field 1\"" }
it "outputs the custom field value for the specified project" do
- expect(formatted).to eq("Project custom value 1\n")
+ expect(formatted).to eq("Project custom value 1")
end
end
@@ -341,7 +386,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectValue:nonexistent_attribute" }
it "outputs an empty value" do
- expect(formatted).to eq(" \n")
+ expect(formatted).to eq(" ")
end
end
@@ -349,7 +394,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "**projectValue:name**" }
it "preserves the markdown formatting" do
- expect(formatted).to eq("**#{project.name}**\n")
+ expect(formatted).to eq("**#{project.name}**")
end
end
@@ -357,7 +402,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "" }
it "processes the macro inside HTML" do
- expect(formatted).to eq("\n")
+ expect(formatted).to eq("")
end
end
end
@@ -367,7 +412,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:name" }
it "outputs the attribute label" do
- expect(formatted).to eq("Name\n")
+ expect(formatted).to eq("Name")
end
end
@@ -375,7 +420,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:#{project.id}:name" }
it "outputs the attribute label for the specified project" do
- expect(formatted).to eq("Name\n")
+ expect(formatted).to eq("Name")
end
end
@@ -383,7 +428,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:\"#{project.identifier}\":name" }
it "outputs the attribute label for the specified project" do
- expect(formatted).to eq("Name\n")
+ expect(formatted).to eq("Name")
end
end
@@ -391,7 +436,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:999:name" }
it "outputs the attribute label" do
- expect(formatted).to eq("Name\n")
+ expect(formatted).to eq("Name")
end
end
@@ -399,7 +444,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:status_code" }
it "outputs the status label" do
- expect(formatted).to eq("Project status\n")
+ expect(formatted).to eq("Project status")
end
end
@@ -407,7 +452,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:\"Project Custom Field 1\"" }
it "outputs the custom field name" do
- expect(formatted).to eq("Project Custom Field 1\n")
+ expect(formatted).to eq("Project Custom Field 1")
end
end
@@ -415,7 +460,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:#{project.id}:\"Project Custom Field 1\"" }
it "outputs the custom field name for the specified project" do
- expect(formatted).to include("Project Custom Field 1")
+ expect(formatted).to eq("Project Custom Field 1")
end
end
@@ -423,7 +468,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:nonexistent_attribute" }
it "outputs the humanized attribute name" do
- expect(formatted).to include("nonexistent_attribute")
+ expect(formatted).to eq("nonexistent_attribute")
end
end
@@ -431,7 +476,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "**projectLabel:name**" }
it "preserves the markdown formatting" do
- expect(formatted).to eq("**Name**\n")
+ expect(formatted).to eq("**Name**")
end
end
@@ -439,7 +484,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "" }
it "processes the macro inside HTML" do
- expect(formatted).to eq("\n")
+ expect(formatted).to eq("")
end
end
end
From a1ab38967158aa89744df2a011ef7138f4564fb0 Mon Sep 17 00:00:00 2001
From: as-op
Date: Tue, 8 Jul 2025 15:37:31 +0200
Subject: [PATCH 06/24] obey rubocop
---
.../work_package/exports/macros/attributes.rb | 31 +++++++++++++------
.../pdf_export/export/wp/attributes.rb | 8 +++--
2 files changed, 26 insertions(+), 13 deletions(-)
diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb
index eb032a68565..b3d5c59df71 100644
--- a/app/models/work_package/exports/macros/attributes.rb
+++ b/app/models/work_package/exports/macros/attributes.rb
@@ -150,20 +150,31 @@ module WorkPackage::Exports
end
def self.resolve_value(obj, attribute, disabled_rich_text_fields)
- cf = obj.available_custom_fields.find { |pcf| pcf.name == attribute }
+ custom_field = find_custom_field(obj, attribute)
+ return msg_macro_error_rich_text if custom_field&.formattable?
- return msg_macro_error_rich_text if cf&.formattable?
+ attribute_name = convert_to_attribute_name(custom_field, attribute, obj)
+ return "" unless can_view_attribute?(custom_field, obj, attribute_name)
+ return msg_macro_error_rich_text if disabled_rich_text_fields.include?(attribute_name.to_sym)
- ar_name = if cf.nil?
- ::API::Utilities::PropertyNameConverter.to_ar_name(attribute.to_sym, context: obj)
- else
- "cf_#{cf.id}"
- end
+ format_attribute_value(attribute_name, obj.class, obj)
+ end
- return "" if cf.nil? && !user_allowed_view_attribute?(obj, ar_name)
- return msg_macro_error_rich_text if disabled_rich_text_fields.include?(ar_name.to_sym)
- format_attribute_value(ar_name, obj.class, obj)
+ def self.can_view_attribute?(custom_field, obj, attribute_name)
+ !(custom_field.nil? && !user_allowed_view_attribute?(obj, attribute_name))
+ end
+
+ def self.convert_to_attribute_name(custom_field, attribute, obj)
+ if custom_field.nil?
+ ::API::Utilities::PropertyNameConverter.to_ar_name(attribute.to_sym, context: obj)
+ else
+ "cf_#{custom_field.id}"
+ end
+ end
+
+ def self.find_custom_field(obj, attribute)
+ obj.available_custom_fields.find { |pcf| pcf.name == attribute }
end
def self.format_attribute_value(ar_name, model, obj)
diff --git a/app/models/work_package/pdf_export/export/wp/attributes.rb b/app/models/work_package/pdf_export/export/wp/attributes.rb
index 86d0f4887b4..1253d5e6ba9 100644
--- a/app/models/work_package/pdf_export/export/wp/attributes.rb
+++ b/app/models/work_package/pdf_export/export/wp/attributes.rb
@@ -140,9 +140,7 @@ module WorkPackage::PDFExport::Export::Wp::Attributes
current_part = { type: :attribute, list: [] }
parts = [current_part]
group.attributes.each do |form_key|
- if !CustomField.custom_field_attribute?(form_key) && !user_allowed_view_attribute?(work_package, form_key)
- next
- end
+ next if skip_attribute?(form_key, work_package)
if allowed_long_text_custom_field?(form_key, work_package)
cf = form_key_to_custom_field(form_key)
@@ -162,6 +160,10 @@ module WorkPackage::PDFExport::Export::Wp::Attributes
parts
end
+ def skip_attribute?(form_key, work_package)
+ !CustomField.custom_field_attribute?(form_key) && !user_allowed_view_attribute?(work_package, form_key)
+ end
+
def allowed_long_text_custom_field?(form_key, work_package)
return false unless CustomField.custom_field_attribute? form_key
From 5c40ca18f7cd739185a4f9e5169429202e195522 Mon Sep 17 00:00:00 2001
From: as-op
Date: Tue, 8 Jul 2025 15:48:09 +0200
Subject: [PATCH 07/24] adjust spec setup
---
.../work_packages/pdf_export/work_package_to_pdf_spec.rb | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb
index 29f211f14b7..2d1b675a313 100644
--- a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb
+++ b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb
@@ -71,6 +71,10 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do
# cf_disabled_in_project.id not included == disabled
work_package_custom_field_ids: [cf_long_text.id, cf_empty_long_text.id, cf_global_bool.id, cf_link.id])
end
+ let(:phase_definition) { create(:project_phase_definition, name: "Test Phase") }
+ let!(:project_phase) do
+ create(:project_phase, project: project, definition: phase_definition, active: true)
+ end
let(:forbidden_project) do
create(:project,
name: "Forbidden project",
@@ -88,7 +92,9 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do
end
let(:user) do
create(:user,
- member_with_permissions: { project => %w[view_work_packages export_work_packages view_project_attributes] })
+ member_with_permissions: {
+ project => %w[view_work_packages export_work_packages view_project_attributes view_project_phases]
+ })
end
let(:another_user) do
create(:user, firstname: "Secret User")
From 3bf61873ca8f8561b2d08c84da09a4a2cd260acb Mon Sep 17 00:00:00 2001
From: as-op
Date: Tue, 8 Jul 2025 15:51:04 +0200
Subject: [PATCH 08/24] obey rubocop
---
app/models/work_package/exports/macros/attributes.rb | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb
index b3d5c59df71..022aae0f9ce 100644
--- a/app/models/work_package/exports/macros/attributes.rb
+++ b/app/models/work_package/exports/macros/attributes.rb
@@ -160,7 +160,6 @@ module WorkPackage::Exports
format_attribute_value(attribute_name, obj.class, obj)
end
-
def self.can_view_attribute?(custom_field, obj, attribute_name)
!(custom_field.nil? && !user_allowed_view_attribute?(obj, attribute_name))
end
From 8ca51ff77b6e6877d0b76689088b21d9c7c5f880 Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 09:27:42 +0200
Subject: [PATCH 09/24] use consistent naming
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195313118
---
app/models/work_package/pdf_export/export/wp/attributes.rb | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/models/work_package/pdf_export/export/wp/attributes.rb b/app/models/work_package/pdf_export/export/wp/attributes.rb
index 1253d5e6ba9..dd4e0a8f51e 100644
--- a/app/models/work_package/pdf_export/export/wp/attributes.rb
+++ b/app/models/work_package/pdf_export/export/wp/attributes.rb
@@ -140,7 +140,7 @@ module WorkPackage::PDFExport::Export::Wp::Attributes
current_part = { type: :attribute, list: [] }
parts = [current_part]
group.attributes.each do |form_key|
- next if skip_attribute?(form_key, work_package)
+ next unless show_attribute?(form_key, work_package)
if allowed_long_text_custom_field?(form_key, work_package)
cf = form_key_to_custom_field(form_key)
@@ -160,8 +160,8 @@ module WorkPackage::PDFExport::Export::Wp::Attributes
parts
end
- def skip_attribute?(form_key, work_package)
- !CustomField.custom_field_attribute?(form_key) && !user_allowed_view_attribute?(work_package, form_key)
+ def show_attribute?(form_key, work_package)
+ CustomField.custom_field_attribute?(form_key) || user_allowed_view_attribute?(work_package, form_key)
end
def allowed_long_text_custom_field?(form_key, work_package)
From 893165c135ff7cd22062a623511efc2c23c7e3dc Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 09:29:44 +0200
Subject: [PATCH 10/24] better naming
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195349876
---
app/models/work_package/exports/attributes.rb | 6 +++---
app/models/work_package/exports/macros/attributes.rb | 2 +-
app/models/work_package/pdf_export/export/wp/attributes.rb | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/models/work_package/exports/attributes.rb b/app/models/work_package/exports/attributes.rb
index cd3d0c5004e..8f81fc1fb5d 100644
--- a/app/models/work_package/exports/attributes.rb
+++ b/app/models/work_package/exports/attributes.rb
@@ -30,14 +30,14 @@
module WorkPackage::Exports
module Attributes
- def user_allowed_view_wp_project_phase?(work_package)
+ def allowed_to_view_wp_project_phase?(work_package)
User.current.allowed_in_project?(:view_project_phases, work_package.project) &&
work_package.project.phases.active.any?
end
- def user_allowed_view_attribute?(obj, attribute_name)
+ def allowed_to_view_attribute?(obj, attribute_name)
if attribute_name.to_sym == :project_phase && obj.is_a?(WorkPackage)
- user_allowed_view_wp_project_phase?(obj)
+ allowed_to_view_wp_project_phase?(obj)
else
true
end
diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb
index 022aae0f9ce..32be596fdd3 100644
--- a/app/models/work_package/exports/macros/attributes.rb
+++ b/app/models/work_package/exports/macros/attributes.rb
@@ -161,7 +161,7 @@ module WorkPackage::Exports
end
def self.can_view_attribute?(custom_field, obj, attribute_name)
- !(custom_field.nil? && !user_allowed_view_attribute?(obj, attribute_name))
+ !(custom_field.nil? && !allowed_to_view_attribute?(obj, attribute_name))
end
def self.convert_to_attribute_name(custom_field, attribute, obj)
diff --git a/app/models/work_package/pdf_export/export/wp/attributes.rb b/app/models/work_package/pdf_export/export/wp/attributes.rb
index dd4e0a8f51e..3a3240570f5 100644
--- a/app/models/work_package/pdf_export/export/wp/attributes.rb
+++ b/app/models/work_package/pdf_export/export/wp/attributes.rb
@@ -161,7 +161,7 @@ module WorkPackage::PDFExport::Export::Wp::Attributes
end
def show_attribute?(form_key, work_package)
- CustomField.custom_field_attribute?(form_key) || user_allowed_view_attribute?(work_package, form_key)
+ CustomField.custom_field_attribute?(form_key) || allowed_to_view_attribute?(work_package, form_key)
end
def allowed_long_text_custom_field?(form_key, work_package)
From 0fe4d90c7c85144c66f16946edc003c60b5e1315 Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 09:31:13 +0200
Subject: [PATCH 11/24] better naming & parameter
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195351784
---
app/models/work_package/exports/attributes.rb | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/app/models/work_package/exports/attributes.rb b/app/models/work_package/exports/attributes.rb
index 8f81fc1fb5d..dd1791f5b01 100644
--- a/app/models/work_package/exports/attributes.rb
+++ b/app/models/work_package/exports/attributes.rb
@@ -30,14 +30,13 @@
module WorkPackage::Exports
module Attributes
- def allowed_to_view_wp_project_phase?(work_package)
- User.current.allowed_in_project?(:view_project_phases, work_package.project) &&
- work_package.project.phases.active.any?
+ def allowed_to_view_project_phases?(project)
+ User.current.allowed_in_project?(:view_project_phases, project) && project.phases.active.any?
end
def allowed_to_view_attribute?(obj, attribute_name)
if attribute_name.to_sym == :project_phase && obj.is_a?(WorkPackage)
- allowed_to_view_wp_project_phase?(obj)
+ allowed_to_view_project_phases?(obj.project)
else
true
end
From c1c7dc127f8e6ca4b26dd43ddbf8c113f6d28b55 Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 09:49:48 +0200
Subject: [PATCH 12/24] remove rescue I18n::MissingTranslationData as it's
thrown in dev & test; but disable it for the tests it would be thrown
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195367016
---
.../work_package/exports/macros/attributes.rb | 4 ---
.../pdf_export/common/macro_spec.rb | 32 +++++++++++++++----
2 files changed, 26 insertions(+), 10 deletions(-)
diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb
index 32be596fdd3..8c73907f0f4 100644
--- a/app/models/work_package/exports/macros/attributes.rb
+++ b/app/models/work_package/exports/macros/attributes.rb
@@ -105,10 +105,6 @@ module WorkPackage::Exports
model.human_attribute_name(
::API::Utilities::PropertyNameConverter.to_ar_name(attribute.to_sym, context: model.new)
)
- rescue I18n::MissingTranslationData
- # If the translation is missing, we return the attribute name as a fallback
- # # This can happen if the attribute is a custom field, the attribute is not translated or does not exist
- attribute.to_s
end
def self.resolve_work_package_match(id, type, attribute, user)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index a96e151730d..a6b1c7543b2 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -252,6 +252,16 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "workPackageLabel macro" do
+ let!(:original_setting) { ActiveModel::Translation.raise_on_missing_translations }
+
+ before do
+ ActiveModel::Translation.raise_on_missing_translations = false
+ end
+
+ after do
+ ActiveModel::Translation.raise_on_missing_translations = original_setting
+ end
+
describe "with current work package attribute" do
let(:markdown) { "workPackageLabel:subject" }
@@ -288,7 +298,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageLabel:\"Custom Field 1\"" }
it "outputs the custom field name" do
- expect(formatted).to eq("Custom Field 1")
+ expect(formatted).to eq("Custom field 1")
end
end
@@ -296,7 +306,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageLabel:185:\"Custom Field 1\"" }
it "outputs the custom field name for the specified work package" do
- expect(formatted).to eq("Custom Field 1")
+ expect(formatted).to eq("Custom field 1")
end
end
@@ -304,7 +314,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "workPackageLabel:nonexistent_attribute" }
it "outputs the humanized attribute name" do
- expect(formatted).to eq("nonexistent_attribute")
+ expect(formatted).to eq("Nonexistent attribute")
end
end
@@ -408,6 +418,16 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "projectLabel macro" do
+ let!(:original_setting) { ActiveModel::Translation.raise_on_missing_translations }
+
+ before do
+ ActiveModel::Translation.raise_on_missing_translations = false
+ end
+
+ after do
+ ActiveModel::Translation.raise_on_missing_translations = original_setting
+ end
+
describe "with current project attribute" do
let(:markdown) { "projectLabel:name" }
@@ -452,7 +472,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:\"Project Custom Field 1\"" }
it "outputs the custom field name" do
- expect(formatted).to eq("Project Custom Field 1")
+ expect(formatted).to eq("Project custom field 1")
end
end
@@ -460,7 +480,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:#{project.id}:\"Project Custom Field 1\"" }
it "outputs the custom field name for the specified project" do
- expect(formatted).to eq("Project Custom Field 1")
+ expect(formatted).to eq("Project custom field 1")
end
end
@@ -468,7 +488,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:markdown) { "projectLabel:nonexistent_attribute" }
it "outputs the humanized attribute name" do
- expect(formatted).to eq("nonexistent_attribute")
+ expect(formatted).to eq("Nonexistent attribute")
end
end
From 9831be871999e24ef6fc88e5a5234c311f8d428d Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 09:53:58 +0200
Subject: [PATCH 13/24] refactor call
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195515426
---
.../work_package/exports/macros/attributes.rb | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb
index 8c73907f0f4..b9636f46d5d 100644
--- a/app/models/work_package/exports/macros/attributes.rb
+++ b/app/models/work_package/exports/macros/attributes.rb
@@ -102,11 +102,22 @@ module WorkPackage::Exports
end
def self.resolve_label(model, attribute)
- model.human_attribute_name(
- ::API::Utilities::PropertyNameConverter.to_ar_name(attribute.to_sym, context: model.new)
- )
+ model.human_attribute_name(to_ar_name(attribute, model.new))
end
+ def self.to_ar_name(attribute, context)
+ ::API::Utilities::PropertyNameConverter.to_ar_name(attribute.to_sym, context:)
+ end
+
+ ##
+ # Resolves a work package or project match based on the type and id.
+ # Returns the formatted value or an error message if not found.
+ #
+ # @param id [String] The ID of the work package or project.
+ # @param type [String] The type of the match (label or value).
+ # @param attribute [String] The attribute to resolve.
+ # @param user [User] The user context for visibility checks.
+
def self.resolve_work_package_match(id, type, attribute, user)
return resolve_label_work_package(attribute) if type == "label"
return msg_macro_error(I18n.t("export.macro.model_not_found", model: type)) unless type == "value"
@@ -162,7 +173,7 @@ module WorkPackage::Exports
def self.convert_to_attribute_name(custom_field, attribute, obj)
if custom_field.nil?
- ::API::Utilities::PropertyNameConverter.to_ar_name(attribute.to_sym, context: obj)
+ to_ar_name(attribute, obj)
else
"cf_#{custom_field.id}"
end
From 6e2d0e6d5e0f06128fb5c3591af671ec6a4f2b96 Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 10:22:23 +0200
Subject: [PATCH 14/24] do not use static work package id, but add
interpolation in all usages
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195667995
---
.../pdf_export/common/macro_spec.rb | 51 +++++++++++--------
1 file changed, 30 insertions(+), 21 deletions(-)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index a6b1c7543b2..eead1725ffb 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -57,7 +57,6 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
shared_let(:work_package) do
create(
:work_package,
- id: 185,
subject: "Work package 1",
type: type_task,
status: create(:status, name: "In Progress"), project: project,
@@ -94,48 +93,58 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "wp mention macro" do
+ let(:expected_tag) {
+ "##{
+ work_package.id
+ }"
+ }
describe "with tag" do
- let(:markdown) { '#185' }
+ let(:markdown) { expected_tag }
- it "ignores the tag" do
- expect(formatted).to eq("\\#185")
+ it "loops the tag through" do
+ expect(formatted).to eq(
+ "\\##{work_package.id}" # note: escaped backslash in the tag text for correct markdown rendering
+ )
end
end
describe "with plain" do
- let(:markdown) { "#185" }
+ let(:markdown) { "##{work_package.id}" }
it "contains correct data" do
- expect(formatted).to eq("#185")
+ expect(formatted).to eq(expected_tag)
end
end
describe "with markdown formating bold" do
- let(:markdown) { "\n**#185**\n" }
+ let(:markdown) { "\n**##{work_package.id}**\n" }
it "contains correct data" do
- expect(formatted).to eq("**#185**")
+ expect(formatted).to eq("**#{expected_tag}**")
end
end
describe "with markdown formating strikethrough" do
- let(:markdown) { "~~#185~~" }
+ let(:markdown) { "~~##{work_package.id}~~" }
it "contains correct data" do
- expect(formatted).to eq("~~#185~~")
+ expect(formatted).to eq("~~#{expected_tag}~~")
end
end
describe "with strikethrough in table" do
- let(:markdown) { "" }
+ let(:markdown) { "" }
it "contains correct data" do
- expect(formatted).to eq("")
+ expect(formatted).to eq("")
end
end
end
@@ -150,7 +159,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "with specific work package ID and attribute" do
- let(:markdown) { "workPackageValue:185:subject" }
+ let(:markdown) { "workPackageValue:#{work_package.id}:subject" }
it "outputs the attribute value for the specified work package" do
expect(formatted).to eq("Work package 1")
@@ -219,7 +228,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "with specific work package ID and custom field" do
- let(:markdown) { "workPackageValue:185:\"Custom Field 1\"" }
+ let(:markdown) { "workPackageValue:#{work_package.id}:\"Custom Field 1\"" }
it "outputs the custom field value for the specified work package" do
expect(formatted).to eq("Custom value 1")
@@ -271,7 +280,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "with specific work package ID and attribute" do
- let(:markdown) { "workPackageLabel:185:subject" }
+ let(:markdown) { "workPackageLabel:#{work_package.id}:subject" }
it "outputs the attribute label for the specified work package" do
expect(formatted).to eq("Subject")
@@ -303,7 +312,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "with specific work package ID and custom field" do
- let(:markdown) { "workPackageLabel:185:\"Custom Field 1\"" }
+ let(:markdown) { "workPackageLabel:#{work_package.id}:\"Custom Field 1\"" }
it "outputs the custom field name for the specified work package" do
expect(formatted).to eq("Custom field 1")
From 0b1b5f3168ab275b9fd48d921b76703f70e961be Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 10:24:38 +0200
Subject: [PATCH 15/24] more compact boolean complexity
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195518040
---
app/models/work_package/exports/macros/attributes.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb
index b9636f46d5d..d955a730e9b 100644
--- a/app/models/work_package/exports/macros/attributes.rb
+++ b/app/models/work_package/exports/macros/attributes.rb
@@ -168,7 +168,7 @@ module WorkPackage::Exports
end
def self.can_view_attribute?(custom_field, obj, attribute_name)
- !(custom_field.nil? && !allowed_to_view_attribute?(obj, attribute_name))
+ custom_field || allowed_to_view_attribute?(obj, attribute_name)
end
def self.convert_to_attribute_name(custom_field, attribute, obj)
From 38e924b0f3780d4637744ea8cd6d89a6644497f0 Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 10:27:01 +0200
Subject: [PATCH 16/24] do not return an empty string
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195520084
---
app/models/work_package/exports/macros/attributes.rb | 2 +-
spec/models/work_package/pdf_export/common/macro_spec.rb | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb
index d955a730e9b..37bbc2fecda 100644
--- a/app/models/work_package/exports/macros/attributes.rb
+++ b/app/models/work_package/exports/macros/attributes.rb
@@ -161,7 +161,7 @@ module WorkPackage::Exports
return msg_macro_error_rich_text if custom_field&.formattable?
attribute_name = convert_to_attribute_name(custom_field, attribute, obj)
- return "" unless can_view_attribute?(custom_field, obj, attribute_name)
+ return " " unless can_view_attribute?(custom_field, obj, attribute_name)
return msg_macro_error_rich_text if disabled_rich_text_fields.include?(attribute_name.to_sym)
format_attribute_value(attribute_name, obj.class, obj)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index eead1725ffb..636a0850944 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -195,8 +195,8 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "without the permission" do
- it "outputs nothing" do
- expect(formatted).to eq("")
+ it "outputs a single space" do
+ expect(formatted).to eq(" ")
end
end
@@ -213,7 +213,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
let(:project_phase_active) { false }
it "outputs the project phase name" do
- expect(formatted).to eq("")
+ expect(formatted).to eq(" ")
end
end
end
From 2d414f6d387e08db966f00d54a29c946ee205ae2 Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 10:30:39 +0200
Subject: [PATCH 17/24] use the automatically built project_phase_definition
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195529232
---
spec/models/work_package/pdf_export/common/macro_spec.rb | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index 636a0850944..12d7773c7d0 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -184,14 +184,13 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
describe "with project_phase attribute" do
let(:project_phase_active) { true }
- let(:phase_definition) { create(:project_phase_definition, name: "Test Phase") }
let!(:project_phase) do
- create(:project_phase, project: project, definition: phase_definition, active: project_phase_active)
+ create(:project_phase, project: project, active: project_phase_active)
end
let(:markdown) { "workPackageValue:project_phase" }
before do
- work_package.update!(project_phase_definition_id: phase_definition.id)
+ work_package.update!(project_phase_definition_id: project_phase.definition.id)
end
describe "without the permission" do
@@ -205,7 +204,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
describe "with active phase" do
it "outputs the project phase name" do
- expect(formatted).to eq("Test Phase")
+ expect(formatted).to eq(project_phase.name)
end
end
From 691b36b4cfd58dfd8e56e8e1c90fb6644fd9cc88 Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 10:32:38 +0200
Subject: [PATCH 18/24] remove not needed save! on work_package and project
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195602347 and https://github.com/opf/openproject/pull/19449#discussion_r2195602900
---
spec/models/work_package/pdf_export/common/macro_spec.rb | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index 12d7773c7d0..6f0adae19a1 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -50,8 +50,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
status_code: "on_track",
work_package_custom_fields: [custom_field],
project_custom_fields: [project_custom_field],
- custom_field_values: { project_custom_field.id => "Project custom value 1" },
- &:save!
+ custom_field_values: { project_custom_field.id => "Project custom value 1" }
)
end
shared_let(:work_package) do
@@ -60,8 +59,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
subject: "Work package 1",
type: type_task,
status: create(:status, name: "In Progress"), project: project,
- custom_field_values: { custom_field.id => "Custom value 1" },
- &:save!
+ custom_field_values: { custom_field.id => "Custom value 1" }
)
end
shared_let(:formatter) { Class.new { extend WorkPackage::PDFExport::Common::Macro } }
From 81efe18f33001302d82d8b6c55fa808ecda766f9 Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 10:37:35 +0200
Subject: [PATCH 19/24] use single quotes to avoid escaping double quotes
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195649689
---
.../pdf_export/common/macro_spec.rb | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index 6f0adae19a1..b70e0c67f6c 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -91,7 +91,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "wp mention macro" do
- let(:expected_tag) {
+ let(:expected_tag) do
"##{
work_package.id
}"
- }
+ end
+
describe "with tag" do
let(:markdown) { expected_tag }
it "loops the tag through" do
+ # note: escaped backslash in the tag text for correct markdown rendering
expect(formatted).to eq(
"\\##{work_package.id}" # note: escaped backslash in the tag text for correct markdown rendering
- )
+ }\">\\##{work_package.id}"
+ )
end
end
@@ -217,7 +219,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "with custom field by name" do
- let(:markdown) { "workPackageValue:\"Custom Field 1\"" }
+ let(:markdown) { 'workPackageValue:"Custom Field 1"' }
it "outputs the custom field value" do
expect(formatted).to eq("Custom value 1")
@@ -301,7 +303,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "with custom field by name" do
- let(:markdown) { "workPackageLabel:\"Custom Field 1\"" }
+ let(:markdown) { 'workPackageLabel:"Custom Field 1"' }
it "outputs the custom field name" do
expect(formatted).to eq("Custom field 1")
@@ -383,7 +385,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "with custom field by name" do
- let(:markdown) { "projectValue:\"Project Custom Field 1\"" }
+ let(:markdown) { 'projectValue:"Project Custom Field 1"' }
it "outputs the custom field value" do
expect(formatted).to eq("Project custom value 1")
@@ -475,7 +477,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "with custom field by name" do
- let(:markdown) { "projectLabel:\"Project Custom Field 1\"" }
+ let(:markdown) { 'projectLabel:"Project Custom Field 1"' }
it "outputs the custom field name" do
expect(formatted).to eq("Project custom field 1")
From 3ff02731bdd6dd26f10dec72606aa34ce758c0ca Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 10:38:56 +0200
Subject: [PATCH 20/24] remove unnecessary new lines in the test
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195651639
---
spec/models/work_package/pdf_export/common/macro_spec.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index b70e0c67f6c..427c7fad596 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -125,7 +125,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
describe "with markdown formating bold" do
- let(:markdown) { "\n**##{work_package.id}**\n" }
+ let(:markdown) { "**##{work_package.id}**" }
it "contains correct data" do
expect(formatted).to eq("**#{expected_tag}**")
From d75e6a50b6d8a91986635674462661573f52550c Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 11:37:07 +0200
Subject: [PATCH 21/24] add test for unsupported formatted Custom Field
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195651639
---
.../pdf_export/common/macro_spec.rb | 32 +++++++++++++++++--
1 file changed, 30 insertions(+), 2 deletions(-)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index 427c7fad596..58f95d56c73 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -40,6 +40,15 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
types: [type_task]
)
end
+ shared_let(:formatted_custom_field) do
+ create(
+ :work_package_custom_field,
+ name: "Custom Formatted Field",
+ field_format: "text",
+ is_for_all: true,
+ types: [type_task]
+ )
+ end
shared_let(:project_custom_field_section) { create(:project_custom_field_section) }
shared_let(:project_custom_field) do
create(:string_project_custom_field, name: "Project Custom Field 1", project_custom_field_section:)
@@ -48,7 +57,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
create(
:project,
status_code: "on_track",
- work_package_custom_fields: [custom_field],
+ work_package_custom_fields: [custom_field, formatted_custom_field],
project_custom_fields: [project_custom_field],
custom_field_values: { project_custom_field.id => "Project custom value 1" }
)
@@ -59,7 +68,10 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
subject: "Work package 1",
type: type_task,
status: create(:status, name: "In Progress"), project: project,
- custom_field_values: { custom_field.id => "Custom value 1" }
+ custom_field_values: {
+ custom_field.id => "Custom value 1",
+ formatted_custom_field.id => "**Formatted** _text_ content"
+ }
)
end
shared_let(:formatter) { Class.new { extend WorkPackage::PDFExport::Common::Macro } }
@@ -234,6 +246,22 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
+ describe "with formatted custom field" do
+ let(:markdown) { 'workPackageValue:"Custom Formatted Field"' }
+
+ it "outputs an error message for rich text" do
+ expect(formatted).to include(I18n.t("export.macro.rich_text_unsupported"))
+ end
+ end
+
+ describe "with specific work package ID and formatted custom field" do
+ let(:markdown) { "workPackageValue:#{work_package.id}:\"Custom Formatted Field\"" }
+
+ it "outputs an error message for rich text" do
+ expect(formatted).to include(I18n.t("export.macro.rich_text_unsupported"))
+ end
+ end
+
describe "with non-existent attribute" do
let(:markdown) { "workPackageValue:nonexistent_attribute" }
From 78d97f161d81d741e196c242b37fad1b880aac15 Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 11:47:59 +0200
Subject: [PATCH 22/24] add tests with another work package referenced by ID
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195656739
---
.../pdf_export/common/macro_spec.rb | 50 ++++++++++++++++---
1 file changed, 43 insertions(+), 7 deletions(-)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index 58f95d56c73..9f6834cd654 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -67,13 +67,25 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
:work_package,
subject: "Work package 1",
type: type_task,
- status: create(:status, name: "In Progress"), project: project,
+ status: create(:status, name: "In Progress"),
+ project: project,
custom_field_values: {
custom_field.id => "Custom value 1",
formatted_custom_field.id => "**Formatted** _text_ content"
}
)
end
+ shared_let(:other_work_package) do
+ create(
+ :work_package,
+ subject: "Work package 2",
+ project: project,
+ type: type_task,
+ custom_field_values: {
+ custom_field.id => "Custom value 2"
+ }
+ )
+ end
shared_let(:formatter) { Class.new { extend WorkPackage::PDFExport::Common::Macro } }
let(:additional_permissions) { [] }
let(:user) do
@@ -119,12 +131,12 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
it "loops the tag through" do
# note: escaped backslash in the tag text for correct markdown rendering
expect(formatted).to eq(
- "\\##{work_package.id}"
- )
+ "\\##{work_package.id}"
+ )
end
end
@@ -178,6 +190,14 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
+ describe "withh another work package ID and attribute" do
+ let(:markdown) { "workPackageValue:#{other_work_package.id}:subject" }
+
+ it "outputs the attribute value for the specified work package" do
+ expect(formatted).to eq("Work package 2")
+ end
+ end
+
describe "with non-existent work package ID" do
let(:markdown) { "workPackageValue:999:subject" }
@@ -246,6 +266,14 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
+ describe "with another work package ID and custom field" do
+ let(:markdown) { "workPackageValue:#{other_work_package.id}:\"Custom Field 1\"" }
+
+ it "outputs the custom field value for the specified work package" do
+ expect(formatted).to eq("Custom value 2")
+ end
+ end
+
describe "with formatted custom field" do
let(:markdown) { 'workPackageValue:"Custom Formatted Field"' }
@@ -270,6 +298,14 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
+ describe "with another work package id and a non-existent attribute" do
+ let(:markdown) { "workPackageValue:#{other_work_package.id}:nonexistent_attribute" }
+
+ it "outputs an empty value" do
+ expect(formatted).to eq(" ")
+ end
+ end
+
describe "with markdown formatting" do
let(:markdown) { "**workPackageValue:subject**" }
From 5a9a0f95e82ca13bfe72923dc8847e3eeb402700 Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 11:55:24 +0200
Subject: [PATCH 23/24] add tests with another project referenced by ID
resolves https://github.com/opf/openproject/pull/19449#discussion_r2195669032
---
.../pdf_export/common/macro_spec.rb | 42 +++++++++++++++++--
1 file changed, 38 insertions(+), 4 deletions(-)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index 9f6834cd654..e32a5387cb8 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -62,6 +62,15 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
custom_field_values: { project_custom_field.id => "Project custom value 1" }
)
end
+ shared_let(:other_project) do
+ create(
+ :project,
+ name: "Other Project",
+ work_package_custom_fields: [custom_field, formatted_custom_field],
+ project_custom_fields: [project_custom_field],
+ custom_field_values: { project_custom_field.id => "Project custom value 2" }
+ )
+ end
shared_let(:work_package) do
create(
:work_package,
@@ -79,7 +88,7 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
create(
:work_package,
subject: "Work package 2",
- project: project,
+ project: other_project,
type: type_task,
custom_field_values: {
custom_field.id => "Custom value 2"
@@ -92,7 +101,8 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
create(
:user,
member_with_permissions: {
- project => %i[view_work_packages view_project_attributes view_project] + additional_permissions
+ project => %i[view_work_packages view_project_attributes view_project] + additional_permissions,
+ other_project => %i[view_work_packages view_project_attributes view_project] + additional_permissions
}
)
end
@@ -131,12 +141,12 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
it "loops the tag through" do
# note: escaped backslash in the tag text for correct markdown rendering
expect(formatted).to eq(
- "\\##{work_package.id}"
- )
+ )
end
end
@@ -424,6 +434,14 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
+ describe "with other project ID and attribute" do
+ let(:markdown) { "projectValue:#{other_project.id}:name" }
+
+ it "outputs the attribute value for the specified project" do
+ expect(formatted).to eq(other_project.name)
+ end
+ end
+
describe "with specific project identifier and attribute" do
let(:markdown) { "projectValue:\"#{project.identifier}\":name" }
@@ -432,6 +450,14 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
+ describe "with other project identifier and attribute" do
+ let(:markdown) { "projectValue:\"#{other_project.identifier}\":name" }
+
+ it "outputs the attribute value for the specified project" do
+ expect(formatted).to eq(other_project.name)
+ end
+ end
+
describe "with non-existent project ID" do
let(:markdown) { "projectValue:999:name" }
@@ -464,6 +490,14 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
+ describe "with other project ID and custom field" do
+ let(:markdown) { "projectValue:#{other_project.id}:\"Project Custom Field 1\"" }
+
+ it "outputs the custom field value for the specified project" do
+ expect(formatted).to eq("Project custom value 2")
+ end
+ end
+
describe "with non-existent attribute" do
let(:markdown) { "projectValue:nonexistent_attribute" }
From 2c6ff99dd8100f84b3b052b3bf83c88d2e36d5ec Mon Sep 17 00:00:00 2001
From: as-op
Date: Thu, 10 Jul 2025 12:05:04 +0200
Subject: [PATCH 24/24] add tests with restricted project/work_package
referenced by ID
---
.../pdf_export/common/macro_spec.rb | 68 +++++++++++++++++++
1 file changed, 68 insertions(+)
diff --git a/spec/models/work_package/pdf_export/common/macro_spec.rb b/spec/models/work_package/pdf_export/common/macro_spec.rb
index e32a5387cb8..45b3a7c1936 100644
--- a/spec/models/work_package/pdf_export/common/macro_spec.rb
+++ b/spec/models/work_package/pdf_export/common/macro_spec.rb
@@ -95,6 +95,26 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
}
)
end
+ shared_let(:restricted_other_project) do
+ create(
+ :project,
+ name: "Other Project",
+ work_package_custom_fields: [custom_field, formatted_custom_field],
+ project_custom_fields: [project_custom_field],
+ custom_field_values: { project_custom_field.id => "Project custom value 3" }
+ )
+ end
+ shared_let(:restricted_work_package) do
+ create(
+ :work_package,
+ subject: "Work package 3",
+ project: restricted_other_project,
+ type: type_task,
+ custom_field_values: {
+ custom_field.id => "Custom value 3"
+ }
+ )
+ end
shared_let(:formatter) { Class.new { extend WorkPackage::PDFExport::Common::Macro } }
let(:additional_permissions) { [] }
let(:user) do
@@ -208,6 +228,30 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
+ describe "with restricted work package ID and attribute" do
+ let(:markdown) { "workPackageValue:#{restricted_work_package.id}:subject" }
+
+ it "outputs an error message" do
+ expect(formatted).to include("Macro error, resource not found")
+ end
+ end
+
+ describe "with restricted work package ID and custom field" do
+ let(:markdown) { "workPackageValue:#{restricted_work_package.id}:\"Custom Field 1\"" }
+
+ it "outputs an error message" do
+ expect(formatted).to include("Macro error, resource not found")
+ end
+ end
+
+ describe "with restricted work package ID and formatted custom field" do
+ let(:markdown) { "workPackageValue:#{restricted_work_package.id}:\"Custom Formatted Field\"" }
+
+ it "outputs an error message" do
+ expect(formatted).to include("Macro error, resource not found")
+ end
+ end
+
describe "with non-existent work package ID" do
let(:markdown) { "workPackageValue:999:subject" }
@@ -466,6 +510,30 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
+ describe "with restricted project ID" do
+ let(:markdown) { "projectValue:#{restricted_other_project.id}:name" }
+
+ it "outputs an error message" do
+ expect(formatted).to include("Macro error, resource not found")
+ end
+ end
+
+ describe "with restricted project identifier" do
+ let(:markdown) { "projectValue:\"#{restricted_other_project.identifier}\":name" }
+
+ it "outputs an error message" do
+ expect(formatted).to include("Macro error, resource not found")
+ end
+ end
+
+ describe "with restricted project ID and custom field" do
+ let(:markdown) { "projectValue:#{restricted_other_project.id}:\"Project Custom Field 1\"" }
+
+ it "outputs an error message" do
+ expect(formatted).to include("Macro error, resource not found")
+ end
+ end
+
describe "with status attribute" do
let(:markdown) { "projectValue:status_code" }