diff --git a/app/models/work_package/exports/attributes.rb b/app/models/work_package/exports/attributes.rb
new file mode 100644
index 00000000000..dd1791f5b01
--- /dev/null
+++ b/app/models/work_package/exports/attributes.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+#-- 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 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_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..37bbc2fecda 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
@@ -101,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"
@@ -145,18 +157,30 @@ 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
- return msg_macro_error_rich_text if disabled_rich_text_fields.include?(ar_name.to_sym)
+ format_attribute_value(attribute_name, obj.class, obj)
+ end
- format_attribute_value(ar_name, obj.class, obj)
+ def self.can_view_attribute?(custom_field, obj, attribute_name)
+ custom_field || allowed_to_view_attribute?(obj, attribute_name)
+ end
+
+ def self.convert_to_attribute_name(custom_field, attribute, obj)
+ if custom_field.nil?
+ to_ar_name(attribute, 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 bb41a95233f..3a3240570f5 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,8 @@ module WorkPackage::PDFExport::Export::Wp::Attributes
current_part = { type: :attribute, list: [] }
parts = [current_part]
group.attributes.each do |form_key|
+ 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)
if current_part[:type] == :long_text
@@ -156,6 +160,10 @@ module WorkPackage::PDFExport::Export::Wp::Attributes
parts
end
+ def show_attribute?(form_key, work_package)
+ 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)
return false unless CustomField.custom_field_attribute? form_key
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..45b3a7c1936 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,111 @@
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(: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:)
+ end
+ shared_let(:project) do
+ create(
+ :project,
+ status_code: "on_track",
+ 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" }
+ )
+ 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,
+ subject: "Work package 1",
+ type: type_task,
+ 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: other_project,
+ type: type_task,
+ custom_field_values: {
+ custom_field.id => "Custom value 2"
+ }
+ )
+ 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
+ create(
+ :user,
+ member_with_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
let(:markdown) { "" }
- let(:formatter) { Class.new { extend WorkPackage::PDFExport::Common::Macro } }
+
+ before do
+ User.current = user
+ end
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: })
+ .sub("\n", "")
end
describe "empty text" do
@@ -47,48 +144,542 @@ RSpec.describe WorkPackage::PDFExport::Common::Macro do
end
end
- describe "wp mention tag" do
- let(:markdown) { '#185' }
+ describe "wp mention macro" do
+ let(:expected_tag) do
+ "##{
+ work_package.id
+ }"
+ end
- it "ignores the tag" do
- expect(formatted).to eq("\\#185\n")
+ 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}"
+ )
+ end
+ end
+
+ describe "with plain" do
+ let(:markdown) { "##{work_package.id}" }
+
+ it "contains correct data" do
+ expect(formatted).to eq(expected_tag)
+ end
+ end
+
+ describe "with markdown formating bold" do
+ let(:markdown) { "**##{work_package.id}**" }
+
+ it "contains correct data" do
+ expect(formatted).to eq("**#{expected_tag}**")
+ end
+ end
+
+ describe "with markdown formating strikethrough" do
+ let(:markdown) { "~~##{work_package.id}~~" }
+
+ it "contains correct data" do
+ expect(formatted).to eq("~~#{expected_tag}~~")
+ end
+ end
+
+ describe "with strikethrough in table" do
+ let(:markdown) { "
" }
+
+ it "contains correct data" do
+ expect(formatted).to eq("")
+ 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")
+ end
+ end
+
+ describe "with specific work package ID and attribute" do
+ 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")
+ 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 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" }
+
+ 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")
+ end
+ end
+
+ describe "with project_phase attribute" do
+ let(:project_phase_active) { true }
+ let!(:project_phase) do
+ create(:project_phase, project: project, active: project_phase_active)
+ end
+ let(:markdown) { "workPackageValue:project_phase" }
+
+ before do
+ work_package.update!(project_phase_definition_id: project_phase.definition.id)
+ end
+
+ describe "without the permission" do
+ it "outputs a single space" 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(project_phase.name)
+ 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
+
+ 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")
+ end
+ end
+
+ describe "with specific work package ID and custom field" do
+ 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")
+ 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"' }
+
+ 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" }
+
+ it "outputs an empty value" do
+ expect(formatted).to eq(" ")
+ 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**" }
+
+ it "preserves the markdown formatting" do
+ expect(formatted).to eq("**Work package 1**")
+ end
+ end
+
+ describe "in a table" do
+ let(:markdown) { "" }
+
+ it "processes the macro inside HTML" do
+ expect(formatted).to eq("")
+ end
end
end
- describe "wp mention with markdown formating bold" do
- let(:markdown) { "\n**#185**\n" }
+ describe "workPackageLabel macro" do
+ let!(:original_setting) { ActiveModel::Translation.raise_on_missing_translations }
- it "contains correct data" do
- expect(formatted).to eq("**#185**\n")
+ 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" }
+
+ it "outputs the attribute label" do
+ expect(formatted).to eq("Subject")
+ end
+ end
+
+ describe "with specific work package ID and attribute" do
+ let(:markdown) { "workPackageLabel:#{work_package.id}:subject" }
+
+ it "outputs the attribute label for the specified work package" do
+ expect(formatted).to eq("Subject")
+ end
+ end
+
+ describe "with non-existent work package ID" do
+ let(:markdown) { "workPackageLabel:999:subject" }
+
+ it "outputs a humanized form" do
+ expect(formatted).to eq("Subject")
+ end
+ end
+
+ describe "with status attribute" do
+ let(:markdown) { "workPackageLabel:status" }
+
+ it "outputs the status label" do
+ expect(formatted).to eq("Status")
+ 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 eq("Custom field 1")
+ end
+ end
+
+ describe "with specific work package ID and custom field" do
+ 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")
+ end
+ end
+
+ describe "with non-existent attribute" do
+ let(:markdown) { "workPackageLabel:nonexistent_attribute" }
+
+ it "outputs the humanized attribute name" do
+ expect(formatted).to eq("Nonexistent attribute")
+ end
+ end
+
+ describe "with markdown formatting" do
+ let(:markdown) { "**workPackageLabel:subject**" }
+
+ it "preserves the markdown formatting" do
+ expect(formatted).to eq("**Subject**")
+ end
+ end
+
+ describe "in a table" do
+ let(:markdown) { "" }
+
+ it "processes the macro inside HTML" do
+ expect(formatted).to eq("")
+ 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)
+ 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)
+ 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" }
+
+ it "outputs the attribute value for the specified project" do
+ expect(formatted).to eq(project.name)
+ 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" }
+
+ it "outputs an error message" do
+ expect(formatted).to include("Macro error, resource not found")
+ 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" }
+
+ it "outputs the status code" do
+ expect(formatted).to eq(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")
+ 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")
+ 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" }
+
+ it "outputs an empty value" do
+ expect(formatted).to eq(" ")
+ end
+ end
+
+ describe "with markdown formatting" do
+ let(:markdown) { "**projectValue:name**" }
+
+ it "preserves the markdown formatting" do
+ expect(formatted).to eq("**#{project.name}**")
+ end
+ end
+
+ describe "in a table" do
+ let(:markdown) { "" }
+
+ it "processes the macro inside HTML" do
+ expect(formatted).to eq("")
+ end
end
end
- describe "wp mention with strikethrough in table" do
- let(:markdown) { "" }
+ describe "projectLabel macro" do
+ let!(:original_setting) { ActiveModel::Translation.raise_on_missing_translations }
- it "contains correct data" do
- expect(formatted).to eq("\n")
+ 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" }
+
+ it "outputs the attribute label" do
+ expect(formatted).to eq("Name")
+ 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")
+ 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")
+ 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")
+ end
+ end
+
+ describe "with status attribute" do
+ let(:markdown) { "projectLabel:status_code" }
+
+ it "outputs the status label" do
+ expect(formatted).to eq("Project status")
+ 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")
+ 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 eq("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 eq("Nonexistent attribute")
+ end
+ end
+
+ describe "with markdown formatting" do
+ let(:markdown) { "**projectLabel:name**" }
+
+ it "preserves the markdown formatting" do
+ expect(formatted).to eq("**Name**")
+ end
+ end
+
+ describe "in a table" do
+ let(:markdown) { "" }
+
+ it "processes the macro inside HTML" do
+ expect(formatted).to eq("")
+ end
end
end
end
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")