diff --git a/app/models/exports/pdf/common/common.rb b/app/models/exports/pdf/common/common.rb
index 5d17e9639708..fed4754959ee 100644
--- a/app/models/exports/pdf/common/common.rb
+++ b/app/models/exports/pdf/common/common.rb
@@ -428,9 +428,9 @@ def make_link_href_cell(href, caption)
"#{make_link_href(href, caption)}"
end
- def get_id_column_cell(work_package, value)
+ def get_id_column_cell(work_package)
href = url_helpers.work_package_url(work_package)
- make_link_href_cell(href, value)
+ make_link_href_cell(href, work_package.display_id)
end
def get_subject_column_cell(work_package, value)
@@ -469,7 +469,7 @@ def get_cf_link_cell(custom_url)
def get_value_cell_by_column(work_package, column_name, format_subject)
value = get_column_value(work_package, column_name)
return get_cf_link_cell(value) if value.is_a?(::Exports::Formatters::LinkFormatter)
- return get_id_column_cell(work_package, value) if column_name == :id
+ return get_id_column_cell(work_package) if column_name == :id
return get_subject_column_cell(work_package, value) if format_subject && column_name == :subject
escape_tags(value)
diff --git a/app/models/exports/pdf/common/markdown.rb b/app/models/exports/pdf/common/markdown.rb
index 6121610f74d0..197ef474e114 100644
--- a/app/models/exports/pdf/common/markdown.rb
+++ b/app/models/exports/pdf/common/markdown.rb
@@ -35,6 +35,7 @@ class MD2PDFExport
include MarkdownToPDF::Core
include MarkdownToPDF::Parser
include Exports::PDF::Common::Common
+ include Exports::PDF::Common::WorkPackageMentions
def initialize(styling_yml, pdf, hyphenation_language)
@styles = MarkdownToPDF::Styles.new(styling_yml)
@@ -94,45 +95,14 @@ def handle_wp_mention_html_tag(tag, node, opts)
wp_mention_macro(tag.attr("data-text") || "", tag.attr("data-id") || "", opts)
end
- def expand_wp_mention(work_package, content)
- detail_level = content.count("#")
- return content if detail_level == 1
-
- # ##1234: {Type} #{ID}: {Subject}
- content = "#{work_package.type} ##{work_package.id}: #{work_package.subject}"
- return content if detail_level == 2
-
- # ###1234: {Status} {Type} #{ID}: {Subject} ({Start Date} - {End Date})
- "#{work_package.status.name} #{content}#{work_package_dates(work_package)}"
- end
-
def wp_mention_macro(content, id, opts)
- id = id[/\d+/]
return [text_hash(content, opts)] if id.blank?
- work_package = WorkPackage.find_by(id: id)
+ work_package = WorkPackage.find_by_display_id(id)
return [text_hash(content, opts)] unless work_package&.visible?
content = expand_wp_mention(work_package, content)
- [text_hash(content, opts.merge({ link: url_helpers.work_package_url(id) }))]
- end
-
- def work_package_dates(work_package)
- return "" if work_package.start_date.blank? && work_package.due_date.blank?
-
- if work_package.due_date.present? && work_package.start_date == work_package.due_date
- return " (#{format_date(work_package.due_date)})"
- end
-
- work_package_date_range(work_package)
- end
-
- def work_package_date_range(work_package)
- content = [
- work_package.start_date.present? ? format_date(work_package.start_date) : I18n.t("label_no_start_date"),
- work_package.due_date.present? ? format_date(work_package.due_date) : I18n.t("label_no_due_date")
- ].join(" - ")
- " (#{content})"
+ [text_hash(content, opts.merge({ link: url_helpers.work_package_url(work_package) }))]
end
def handle_mention_html_tag(tag, node, opts)
diff --git a/app/models/exports/pdf/common/work_package_mentions.rb b/app/models/exports/pdf/common/work_package_mentions.rb
new file mode 100644
index 000000000000..06db3a0a7b1c
--- /dev/null
+++ b/app/models/exports/pdf/common/work_package_mentions.rb
@@ -0,0 +1,63 @@
+# 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 Exports::PDF::Common::WorkPackageMentions
+ include Redmine::I18n
+
+ def expand_wp_mention(work_package, content)
+ detail_level = content.count("#")
+ return work_package.formatted_id if detail_level == 1
+
+ # ##: {Type} {formatted_id}: {Subject}
+ content = "#{work_package.type} #{work_package.formatted_id}: #{work_package.subject}"
+ return content if detail_level == 2
+
+ # ###: {Status} {Type} {formatted_id}: {Subject} ({Start Date} - {End Date})
+ "#{work_package.status.name} #{content}#{work_package_dates(work_package)}"
+ end
+
+ def work_package_dates(work_package)
+ return "" if work_package.start_date.blank? && work_package.due_date.blank?
+
+ if work_package.due_date.present? && work_package.start_date == work_package.due_date
+ return " (#{format_date(work_package.due_date)})"
+ end
+
+ work_package_date_range(work_package)
+ end
+
+ def work_package_date_range(work_package)
+ content = [
+ work_package.start_date.present? ? format_date(work_package.start_date) : I18n.t("label_no_start_date"),
+ work_package.due_date.present? ? format_date(work_package.due_date) : I18n.t("label_no_due_date")
+ ].join(" - ")
+ " (#{content})"
+ end
+end
diff --git a/app/models/exports/pdf/components/gantt/gantt_builder.rb b/app/models/exports/pdf/components/gantt/gantt_builder.rb
index 1d676164a648..aec81b947f08 100644
--- a/app/models/exports/pdf/components/gantt/gantt_builder.rb
+++ b/app/models/exports/pdf/components/gantt/gantt_builder.rb
@@ -742,7 +742,7 @@ def build_row_text_lines_wp_info(entry, left, right, top)
# @param [WorkPackage] work_package
# @return [String]
def work_package_info_line(work_package)
- "#{work_package.type} ##{work_package.id} • #{work_package.status} • #{work_package_info_line_date work_package}"
+ "#{work_package.type} #{work_package.formatted_id} • #{work_package.status} • #{work_package_info_line_date work_package}"
end
def work_package_info_line_date(work_package)
diff --git a/app/models/work_package/pdf_export/document_generator.rb b/app/models/work_package/pdf_export/document_generator.rb
index 24ff567ad66a..e9bf70d064e8 100644
--- a/app/models/work_package/pdf_export/document_generator.rb
+++ b/app/models/work_package/pdf_export/document_generator.rb
@@ -83,7 +83,7 @@ def footer_title
def title
# ____.pdf
build_pdf_filename([work_package.project, work_package.type,
- "##{work_package.id}", work_package.subject].join("_"))
+ work_package.display_id, work_package.subject].join("_"))
end
def with_images?
diff --git a/app/models/work_package/pdf_export/generator/generator.rb b/app/models/work_package/pdf_export/generator/generator.rb
index aaf20aa824d7..f199aa220316 100644
--- a/app/models/work_package/pdf_export/generator/generator.rb
+++ b/app/models/work_package/pdf_export/generator/generator.rb
@@ -36,6 +36,7 @@ class MD2PDFGenerator
include MarkdownToPDF::Core
include MarkdownToPDF::Parser
include MarkdownToPDF::StyleSchema
+ include Exports::PDF::Common::WorkPackageMentions
def initialize(styling_yml)
symbol_yml = symbolize(styling_yml)
@@ -94,17 +95,40 @@ def hyphenate(text)
@hyphens.hyphenate(text)
end
+ def handle_wp_mention_html_tag(tag, node, opts)
+ # #185
+ # ##185
+ # ###185
+ next_node = node&.next
+ if next_node && next_node.type == :text && next_node.respond_to?(:string_content)
+ next_node.string_content = ""
+ end
+ render_wp_mention(tag.attr("data-text") || "", tag.attr("data-id") || "", opts)
+ end
+
+ def render_wp_mention(content, id, opts)
+ return [text_hash(content, opts)] if id.blank?
+
+ work_package = WorkPackage.find_by_display_id(id)
+ return [text_hash(content, opts)] unless work_package&.visible?
+
+ [text_hash(expand_wp_mention(work_package, content), opts)]
+ end
+
def handle_mention_html_tag(tag, node, opts)
- if tag.text.blank?
- #
+ if tag.attr("data-type") == "work_package"
+ handle_wp_mention_html_tag(tag, node, opts)
+ elsif tag.text.blank?
#
text = tag.attr("data-text")
if text.present? && !node.next.respond_to?(:string_content) && node.next.string_content != text
return [text_hash(text, opts)]
end
+ []
+ else
+ # @Some User
+ []
end
- # @Some User
- []
end
def handle_unknown_inline_html_tag(tag, node, opts)
diff --git a/app/models/work_package/pdf_export/work_package_to_pdf.rb b/app/models/work_package/pdf_export/work_package_to_pdf.rb
index 541dcb1203a5..ebb0759a769a 100644
--- a/app/models/work_package/pdf_export/work_package_to_pdf.rb
+++ b/app/models/work_package/pdf_export/work_package_to_pdf.rb
@@ -108,7 +108,7 @@ def wp_title_formatted_text(work_package, style)
end
def heading
- "#{work_package.type} ##{work_package.id} - #{work_package.subject}"
+ "#{work_package.type} #{work_package.formatted_id} - #{work_package.subject}"
end
def footer_title
@@ -118,7 +118,7 @@ def footer_title
def title
# ____.pdf
build_pdf_filename([work_package.project, work_package.type,
- "##{work_package.id}", work_package.subject].join("_"))
+ work_package.display_id, work_package.subject].join("_"))
end
def write_description!(work_package)
diff --git a/spec/models/exports/pdf/common/markdown/md2_pdf_export_spec.rb b/spec/models/exports/pdf/common/markdown/md2_pdf_export_spec.rb
new file mode 100644
index 000000000000..813c3b584b48
--- /dev/null
+++ b/spec/models/exports/pdf/common/markdown/md2_pdf_export_spec.rb
@@ -0,0 +1,108 @@
+# 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.
+#++
+
+require "spec_helper"
+
+RSpec.describe Exports::PDF::Common::Markdown::MD2PDFExport do
+ let(:exporter) { described_class.allocate }
+ let(:type) { build_stubbed(:type, name: "Bug") }
+ let(:status) { build_stubbed(:status, name: "In Progress") }
+ let(:work_package) do
+ build_stubbed(:work_package, type:, status:, subject: "Fix login")
+ end
+
+ describe "#wp_mention_macro" do
+ let(:admin) { create(:admin) }
+ let(:wp) { create(:work_package) }
+
+ before do
+ User.current = admin
+ end
+
+ context "in classic mode",
+ with_flag: { semantic_work_package_ids: false } do
+ it "generates a link URL with the numeric id" do
+ result = exporter.wp_mention_macro("##{wp.id}", wp.id.to_s, {})
+ expect(result.first[:link]).to include(wp.id.to_s)
+ end
+ end
+
+ context "in semantic mode",
+ with_flag: { semantic_work_package_ids: true },
+ with_settings: { work_packages_identifier: "semantic" } do
+ it "generates a link URL with the semantic identifier" do
+ result = exporter.wp_mention_macro("##{wp.id}", wp.id.to_s, {})
+ expect(result.first[:link]).to include(wp.identifier)
+ end
+ end
+ end
+
+ describe "#expand_wp_mention" do
+ context "in classic mode", with_settings: { work_packages_identifier: "classic" } do
+ it "returns formatted_id for level 1" do
+ expect(exporter.expand_wp_mention(work_package, "##{work_package.id}"))
+ .to eq("##{work_package.id}")
+ end
+
+ it "uses formatted_id in level 2 expansion" do
+ result = exporter.expand_wp_mention(work_package, "###{work_package.id}")
+ expect(result).to eq("Bug ##{work_package.id}: Fix login")
+ end
+
+ it "uses formatted_id in level 3 expansion" do
+ allow(exporter).to receive(:work_package_dates).with(work_package).and_return("")
+ result = exporter.expand_wp_mention(work_package, "####{work_package.id}")
+ expect(result).to eq("In Progress Bug ##{work_package.id}: Fix login")
+ end
+ end
+
+ context "in semantic mode",
+ with_flag: { semantic_work_package_ids: true },
+ with_settings: { work_packages_identifier: "semantic" } do
+ before { work_package.identifier = "PROJ-42" }
+
+ it "returns semantic formatted_id for level 1" do
+ expect(exporter.expand_wp_mention(work_package, "##{work_package.id}"))
+ .to eq("PROJ-42")
+ end
+
+ it "uses semantic formatted_id in level 2 expansion" do
+ result = exporter.expand_wp_mention(work_package, "###{work_package.id}")
+ expect(result).to eq("Bug PROJ-42: Fix login")
+ end
+
+ it "uses semantic formatted_id in level 3 expansion" do
+ allow(exporter).to receive(:work_package_dates).with(work_package).and_return("")
+ result = exporter.expand_wp_mention(work_package, "####{work_package.id}")
+ expect(result).to eq("In Progress Bug PROJ-42: Fix login")
+ end
+ end
+ end
+end
diff --git a/spec/models/work_package/pdf_export/document_generator_spec.rb b/spec/models/work_package/pdf_export/document_generator_spec.rb
index 99d6d586f595..72fb8d91af4a 100644
--- a/spec/models/work_package/pdf_export/document_generator_spec.rb
+++ b/spec/models/work_package/pdf_export/document_generator_spec.rb
@@ -7,7 +7,7 @@
include Redmine::I18n
include PDFExportSpecUtils
- let(:project) { create(:project) }
+ let(:project) { create(:project, name: "PDF Project") }
let(:user) { create(:admin) }
let(:description) do
"This is a test description with an macro: workPackageValue:assignee"
@@ -20,7 +20,7 @@
subject: "Document Generator Specs",
type:)
end
- let(:type) { create(:type) }
+ let(:type) { create(:type, name: "Feature") }
let(:options) do
{}
end
@@ -52,6 +52,25 @@
PDF::Inspector::Text.analyze(content).strings
end
+ describe "#title" do
+ context "in classic mode",
+ with_flag: { semantic_work_package_ids: false } do
+ it "uses the numeric id in the filename" do
+ expected_title = "PDF_Project_Feature_#{work_package.id}_Document_Generator_Specs_2023-06-30_23-59.pdf"
+ expect(export_pdf.title).to eql(expected_title)
+ end
+ end
+
+ context "in semantic mode",
+ with_flag: { semantic_work_package_ids: true },
+ with_settings: { work_packages_identifier: "semantic" } do
+ it "uses the semantic identifier in the filename" do
+ expected_title = "PDF_Project_Feature_#{work_package.identifier}_Document_Generator_Specs_2023-06-30_23-59.pdf"
+ expect(export_pdf.title).to eql(expected_title)
+ end
+ end
+ end
+
describe "with a request for a PDF" do
it "contains correct data" do
expected_result = [
@@ -65,6 +84,15 @@
expect(result.join(" ")).to eq(expected_result.join(" "))
end
+ describe "with a work package mention in the description" do
+ let(:other_work_package) { create(:work_package, project:, type:) }
+ let(:description) { "##{other_work_package.id}" }
+
+ it "renders the mention using formatted_id" do
+ expect(pdf).to include(other_work_package.formatted_id)
+ end
+ end
+
describe "with a request for a PDF with hyphenation and no header/footer text" do
let(:options) do
{
diff --git a/spec/models/work_packages/pdf_export/work_package_list_to_pdf_gantt_spec.rb b/spec/models/work_packages/pdf_export/work_package_list_to_pdf_gantt_spec.rb
index 01b34feb97f0..456a94402b21 100644
--- a/spec/models/work_packages/pdf_export/work_package_list_to_pdf_gantt_spec.rb
+++ b/spec/models/work_packages/pdf_export/work_package_list_to_pdf_gantt_spec.rb
@@ -177,7 +177,8 @@ def wp_title_dates(work_package)
end
def wp_title_column(work_package)
- "#{work_package.type} ##{work_package.id} • #{work_package.status} • #{wp_title_dates work_package} #{work_package.subject}"
+ "#{work_package.type} #{work_package.formatted_id} • #{work_package.status} • " \
+ "#{wp_title_dates work_package} #{work_package.subject}"
end
subject(:pdf) do
@@ -225,6 +226,24 @@ def include_calls?(calls_to_find, all_calls)
end
end
+ describe "work package id formatting" do
+ context "in classic mode",
+ with_flag: { semantic_work_package_ids: false } do
+ it "uses the numeric id and not the semantic identifier in the gantt chart" do
+ expect(pdf[:strings]).to include("##{work_package_task.id}")
+ end
+ end
+
+ context "in semantic mode",
+ with_flag: { semantic_work_package_ids: true },
+ with_settings: { work_packages_identifier: "semantic" } do
+ it "uses the semantic identifier and not the numeric id in the gantt chart" do
+ expect(pdf[:strings]).to include(work_package_task.identifier)
+ expect(pdf[:strings]).not_to include("##{work_package_task.id}")
+ end
+ end
+ end
+
describe "with a request for a PDF gantt split on multiple horizontal pages" do
let(:work_package_milestone_due) do
Date.new(2024, 5, 8)
diff --git a/spec/models/work_packages/pdf_export/work_package_list_to_pdf_spec.rb b/spec/models/work_packages/pdf_export/work_package_list_to_pdf_spec.rb
index 9d09273d68e9..361e9d8167f7 100644
--- a/spec/models/work_packages/pdf_export/work_package_list_to_pdf_spec.rb
+++ b/spec/models/work_packages/pdf_export/work_package_list_to_pdf_spec.rb
@@ -132,7 +132,7 @@ def work_packages_sum
def work_package_columns(work_package)
[
- work_package.id.to_s,
+ work_package.display_id.to_s,
work_package.subject,
work_package.status.name,
work_package.story_points.to_s,
@@ -148,7 +148,7 @@ def work_package_done_ratio(work_package)
def work_package_details(work_package, index, ltfs = [])
result = [
"#{index}.", work_package.subject,
- column_title(:id), work_package.id.to_s,
+ column_title(:id), work_package.display_id.to_s,
column_title(:status), work_package.status.name,
column_title(:story_points), work_package.story_points.to_s,
column_title(:done_ratio), work_package_done_ratio(work_package),
@@ -250,12 +250,12 @@ def pdf_strings_without_footers(nr_of_pages)
project_phase_with_gates.name,
*column_titles - ["Project phase"],
- work_package_parent.id.to_s,
+ work_package_parent.display_id.to_s,
work_package_parent.subject,
project_phase.name,
*column_titles - ["Project phase"],
- work_package_child.id.to_s,
+ work_package_child.display_id.to_s,
work_package_child.subject
]
strings = pdf_strings_without_footers(1)
@@ -453,7 +453,7 @@ def pdf_strings_without_footers(nr_of_pages)
def relation_table_row(work_package)
[
- work_package.id.to_s,
+ work_package.display_id.to_s,
work_package.type.name,
work_package.subject,
work_package.status.name
@@ -463,7 +463,7 @@ def relation_table_row(work_package)
def detail_attributes(work_package, index)
[
"#{index}.", work_package.subject,
- column_title(:id), work_package.id.to_s,
+ column_title(:id), work_package.display_id.to_s,
column_title(:status), work_package.status.name
]
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 ce1184952704..3e57cfa4c908 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
@@ -235,7 +235,7 @@
end
let(:expected_details) do
[
- "#{type.name} ##{work_package.id} - #{work_package.subject}",
+ "#{type.name} #{work_package.formatted_id} - #{work_package.subject}",
" ", exporter.prawn_badge_text_stuffing(work_package.status.name.downcase), # badge & padding
"People",
"Assignee", user.name,