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) { "

##185

" } + + it "contains correct data" do + expect(formatted).to eq("

##185

\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) { "
workPackageValue:subject
" } + + it "processes the macro inside HTML" do + expect(formatted).to eq("
Work package 1
\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) { "
workPackageLabel:subject
" } + + it "processes the macro inside HTML" do + expect(formatted).to eq("
Subject
\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) { "
projectValue:name
" } + + it "processes the macro inside HTML" do + expect(formatted).to eq("
#{project.name}
\n") + end end end - describe "wp mention with strikethrough in table" do - let(:markdown) { "

##185

" } + describe "projectLabel macro" do + describe "with current project attribute" do + let(:markdown) { "projectLabel:name" } - it "contains correct data" do - expect(formatted).to eq("

##185

\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) { "
projectLabel:name
" } + + it "processes the macro inside HTML" do + expect(formatted).to eq("
Name
\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("

##185

\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) { "
workPackageValue:subject
" } it "processes the macro inside HTML" do - expect(formatted).to eq("
Work package 1
\n") + expect(formatted).to eq("
Work package 1
") 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) { "
workPackageLabel:subject
" } it "processes the macro inside HTML" do - expect(formatted).to eq("
Subject
\n") + expect(formatted).to eq("
Subject
") 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) { "
projectValue:name
" } it "processes the macro inside HTML" do - expect(formatted).to eq("
#{project.name}
\n") + expect(formatted).to eq("
#{project.name}
") 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) { "
projectLabel:name
" } it "processes the macro inside HTML" do - expect(formatted).to eq("
Name
\n") + expect(formatted).to eq("
Name
") 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) { "

##185

" } + let(:markdown) { "

##{work_package.id}

" } it "contains correct data" do - expect(formatted).to eq("

##185

") + expect(formatted).to eq("

#{expected_tag}

") 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" }