From b1952a50e3f870de62987f90f9b55fa7197cad28 Mon Sep 17 00:00:00 2001 From: Vincent Robert Date: Tue, 27 Feb 2024 16:35:15 +0100 Subject: [PATCH 1/4] Update CI config and add latest Redmine version --- .github/workflows/4_2_11.yml | 12 ++++++------ .github/workflows/{5_1_0.yml => 5_1_1.yml} | 17 +++++++++-------- .github/workflows/master.yml | 14 +++++++------- README.md | 4 ++-- 4 files changed, 24 insertions(+), 23 deletions(-) rename .github/workflows/{5_1_0.yml => 5_1_1.yml} (90%) diff --git a/.github/workflows/4_2_11.yml b/.github/workflows/4_2_11.yml index e46d452..ab352be 100644 --- a/.github/workflows/4_2_11.yml +++ b/.github/workflows/4_2_11.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout Redmine - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: redmine/redmine ref: ${{ env.REDMINE_VERSION }} @@ -48,7 +48,7 @@ jobs: - name: Install package dependencies run: > - sudo apt-get install --yes --quiet + sudo apt-get update && sudo apt-get install --yes --quiet build-essential cmake libicu-dev @@ -81,7 +81,7 @@ jobs: run: gem install bundler -v '~> 1.0' - name: Checkout dependencies - Base RSpec plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: jbbarth/redmine_base_rspec path: redmine/plugins/redmine_base_rspec @@ -114,19 +114,19 @@ jobs: bundle exec rails test:scm:setup:subversion - name: Checkout dependencies - Base Deface plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: jbbarth/redmine_base_deface path: redmine/plugins/redmine_base_deface - name: Checkout dependencies - Base StimulusJS plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: nanego/redmine_base_stimulusjs path: redmine/plugins/redmine_base_stimulusjs - name: Checkout plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: redmine/plugins/${{ env.PLUGIN_NAME }} diff --git a/.github/workflows/5_1_0.yml b/.github/workflows/5_1_1.yml similarity index 90% rename from .github/workflows/5_1_0.yml rename to .github/workflows/5_1_1.yml index 8bb175f..2aac546 100644 --- a/.github/workflows/5_1_0.yml +++ b/.github/workflows/5_1_1.yml @@ -1,8 +1,8 @@ -name: Tests 5.1.0 +name: Tests 5.1.1 env: PLUGIN_NAME: redmine_tiny_features - REDMINE_VERSION: 5.1.0 + REDMINE_VERSION: 5.1.1 RAILS_ENV: test on: @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout Redmine - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: redmine/redmine ref: ${{ env.REDMINE_VERSION }} @@ -48,7 +48,7 @@ jobs: - name: Install package dependencies run: > - sudo apt-get install --yes --quiet + sudo apt-get update && sudo apt-get install --yes --quiet build-essential cmake libicu-dev @@ -81,7 +81,7 @@ jobs: run: gem install bundler -v '~> 1.0' - name: Checkout dependencies - Base RSpec plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: jbbarth/redmine_base_rspec path: redmine/plugins/redmine_base_rspec @@ -89,6 +89,7 @@ jobs: - name: Prepare Redmine source working-directory: redmine run: | + rm -f test/integration/routing/plugins_test.rb # Fix routing tests # TODO Remove this line when https://www.redmine.org/issues/38707 is fixed sed -i '/rubocop/d' Gemfile rm -f .rubocop* cp plugins/redmine_base_rspec/spec/support/database-${{ matrix.db }}.yml config/database.yml @@ -114,19 +115,19 @@ jobs: bundle exec rails test:scm:setup:subversion - name: Checkout dependencies - Base Deface plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: jbbarth/redmine_base_deface path: redmine/plugins/redmine_base_deface - name: Checkout dependencies - Base StimulusJS plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: nanego/redmine_base_stimulusjs path: redmine/plugins/redmine_base_stimulusjs - name: Checkout plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: redmine/plugins/${{ env.PLUGIN_NAME }} diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index deb8bf8..13263ae 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - ruby: ['3.1'] + ruby: ['3.2'] db: ['postgres'] fail-fast: false @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout Redmine - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: redmine/redmine ref: ${{ env.REDMINE_VERSION }} @@ -48,7 +48,7 @@ jobs: - name: Install package dependencies run: > - sudo apt-get install --yes --quiet + sudo apt-get update && sudo apt-get install --yes --quiet build-essential cmake libicu-dev @@ -81,7 +81,7 @@ jobs: run: gem install bundler -v '~> 1.0' - name: Checkout dependencies - Base RSpec plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: jbbarth/redmine_base_rspec path: redmine/plugins/redmine_base_rspec @@ -114,19 +114,19 @@ jobs: bundle exec rails test:scm:setup:subversion - name: Checkout dependencies - Base Deface plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: jbbarth/redmine_base_deface path: redmine/plugins/redmine_base_deface - name: Checkout dependencies - Base StimulusJS plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: nanego/redmine_base_stimulusjs path: redmine/plugins/redmine_base_stimulusjs - name: Checkout plugin - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: redmine/plugins/${{ env.PLUGIN_NAME }} diff --git a/README.md b/README.md index 34d8680..2d7b5fb 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ Here is a complete list of the features: | Plugin branch | Redmine Version | Test Status | |---------------|-----------------|-------------------| | master | 4.2.11 | [![4.2.11][1]][5] | -| master | 5.1.0 | [![5.1.0][2]][5] | +| master | 5.1.1 | [![5.1.1][2]][5] | | master | master | [![master][3]][5] | [1]: https://github.com/nanego/redmine_tiny_features/actions/workflows/4_2_11.yml/badge.svg -[2]: https://github.com/nanego/redmine_tiny_features/actions/workflows/5_1_0.yml/badge.svg +[2]: https://github.com/nanego/redmine_tiny_features/actions/workflows/5_1_1.yml/badge.svg [3]: https://github.com/nanego/redmine_tiny_features/actions/workflows/master.yml/badge.svg [5]: https://github.com/nanego/redmine_tiny_features/actions From 64ceaafc7706cfbd4ab4d06b15799bbef42a7542 Mon Sep 17 00:00:00 2001 From: Vincent Robert Date: Fri, 5 Apr 2024 10:01:47 +0200 Subject: [PATCH 2/4] Update CI config: add latest Redmine version --- .github/workflows/{5_1_1.yml => 5_1_2.yml} | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename .github/workflows/{5_1_1.yml => 5_1_2.yml} (99%) diff --git a/.github/workflows/5_1_1.yml b/.github/workflows/5_1_2.yml similarity index 99% rename from .github/workflows/5_1_1.yml rename to .github/workflows/5_1_2.yml index 2aac546..62e5c2e 100644 --- a/.github/workflows/5_1_1.yml +++ b/.github/workflows/5_1_2.yml @@ -1,8 +1,8 @@ -name: Tests 5.1.1 +name: Tests 5.1.2 env: PLUGIN_NAME: redmine_tiny_features - REDMINE_VERSION: 5.1.1 + REDMINE_VERSION: 5.1.2 RAILS_ENV: test on: diff --git a/README.md b/README.md index 2d7b5fb..ae1a0a7 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ Here is a complete list of the features: | Plugin branch | Redmine Version | Test Status | |---------------|-----------------|-------------------| | master | 4.2.11 | [![4.2.11][1]][5] | -| master | 5.1.1 | [![5.1.1][2]][5] | +| master | 5.1.2 | [![5.1.2][2]][5] | | master | master | [![master][3]][5] | [1]: https://github.com/nanego/redmine_tiny_features/actions/workflows/4_2_11.yml/badge.svg -[2]: https://github.com/nanego/redmine_tiny_features/actions/workflows/5_1_1.yml/badge.svg +[2]: https://github.com/nanego/redmine_tiny_features/actions/workflows/5_1_2.yml/badge.svg [3]: https://github.com/nanego/redmine_tiny_features/actions/workflows/master.yml/badge.svg [5]: https://github.com/nanego/redmine_tiny_features/actions From 44272c8b326222262eb2609cc5510574719ddb03 Mon Sep 17 00:00:00 2001 From: Yalaeddin Date: Mon, 4 Mar 2024 11:23:16 +0100 Subject: [PATCH 3/4] [Exports PDF des demandes] Liens vers les fichiers --- Gemfile | 1 + lib/redmine_tiny_features/hooks.rb | 1 + .../issues_pdf_helper_patch.rb | 253 ++++++++++++++++++ spec/helpers/issues_pdf_helper_patch_spec.rb | 24 ++ 4 files changed, 279 insertions(+) create mode 100644 Gemfile create mode 100644 lib/redmine_tiny_features/issues_pdf_helper_patch.rb create mode 100644 spec/helpers/issues_pdf_helper_patch_spec.rb diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..e1727bc --- /dev/null +++ b/Gemfile @@ -0,0 +1 @@ +gem 'pdf-reader' \ No newline at end of file diff --git a/lib/redmine_tiny_features/hooks.rb b/lib/redmine_tiny_features/hooks.rb index c2934c1..36f7aaf 100644 --- a/lib/redmine_tiny_features/hooks.rb +++ b/lib/redmine_tiny_features/hooks.rb @@ -42,6 +42,7 @@ def after_plugins_loaded(_context = {}) require_relative 'user_patch' require_relative 'users_helper_patch' require_relative 'user_preference_patch' + require_relative 'issues_pdf_helper_patch' end end end diff --git a/lib/redmine_tiny_features/issues_pdf_helper_patch.rb b/lib/redmine_tiny_features/issues_pdf_helper_patch.rb new file mode 100644 index 0000000..59b67ad --- /dev/null +++ b/lib/redmine_tiny_features/issues_pdf_helper_patch.rb @@ -0,0 +1,253 @@ +# frozen_string_literal: true +include Redmine::Export::PDF + +module RedmineTinyFeatures + module Helpers + module IssuesPdfHelperPatch + # Returns a PDF string of a single issue + def issue_to_pdf(issue, assoc={}) + pdf = ITCPDF.new(current_language) + pdf.set_title("#{issue.project} - #{issue.tracker} ##{issue.id}") + pdf.alias_nb_pages + pdf.footer_date = format_date(User.current.today) + pdf.add_page + pdf.SetFontStyle('B', 11) + buf = "#{issue.project} - #{issue.tracker} ##{issue.id}" + pdf.RDMMultiCell(190, 5, buf) + pdf.SetFontStyle('', 8) + base_x = pdf.get_x + i = 1 + issue.ancestors.visible.each do |ancestor| + pdf.set_x(base_x + i) + buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}" + pdf.RDMMultiCell(190 - i, 5, buf) + i += 1 if i < 35 + end + pdf.SetFontStyle('B', 11) + pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s) + pdf.SetFontStyle('', 8) + pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}") + pdf.ln + + left = [] + left << [l(:field_status), issue.status] + left << [l(:field_priority), issue.priority] + left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id') + left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id') + left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id') + + right = [] + right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date') + right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date') + right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio') + right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours') + right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project) + + rows = left.size > right.size ? left.size : right.size + left << nil while left.size < rows + right << nil while right.size < rows + + custom_field_values = issue.visible_custom_field_values.reject {|value| value.custom_field.full_width_layout?} + half = (custom_field_values.size / 2.0).ceil + custom_field_values.each_with_index do |custom_value, i| + (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)] + end + + if pdf.get_rtl + border_first_top = 'RT' + border_last_top = 'LT' + border_first = 'R' + border_last = 'L' + else + border_first_top = 'LT' + border_last_top = 'RT' + border_first = 'L' + border_last = 'R' + end + + rows = left.size > right.size ? left.size : right.size + rows.times do |i| + heights = [] + pdf.SetFontStyle('B', 9) + item = left[i] + heights << pdf.get_string_height(35, item ? "#{item.first}:" : "") + item = right[i] + heights << pdf.get_string_height(35, item ? "#{item.first}:" : "") + pdf.SetFontStyle('', 9) + item = left[i] + heights << pdf.get_string_height(60, item ? item.last.to_s : "") + item = right[i] + heights << pdf.get_string_height(60, item ? item.last.to_s : "") + height = heights.max + + item = left[i] + pdf.SetFontStyle('B', 9) + pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", + (i == 0 ? border_first_top : border_first), '', 0, 0) + pdf.SetFontStyle('', 9) + pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", + (i == 0 ? border_last_top : border_last), '', 0, 0) + + item = right[i] + pdf.SetFontStyle('B', 9) + pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", + (i == 0 ? border_first_top : border_first), '', 0, 0) + pdf.SetFontStyle('', 9) + pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", + (i == 0 ? border_last_top : border_last), '', 0, 2) + + pdf.set_x(base_x) + end + + pdf.SetFontStyle('B', 9) + pdf.RDMCell(35 + 155, 5, l(:field_description), "LRT", 1) + pdf.SetFontStyle('', 9) + + # Set resize image scale + pdf.set_image_scale(1.6) + text = + textilizable( + issue, :description, + :only_path => false, + :edit_section_links => false, + :headings => false, + :inline_attachments => false + ) + pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB") + + custom_field_values = issue.visible_custom_field_values.select {|value| value.custom_field.full_width_layout?} + custom_field_values.each do |value| + text = show_value(value, false) + next if text.blank? + + pdf.SetFontStyle('B', 9) + pdf.RDMCell(35+155, 5, value.custom_field.name, "LRT", 1) + pdf.SetFontStyle('', 9) + pdf.RDMwriteHTMLCell(35+155, 5, '', '', text, issue.attachments, "LRB") + end + + unless issue.leaf? + truncate_length = (!is_cjk? ? 90 : 65) + pdf.SetFontStyle('B', 9) + pdf.RDMCell(35+155, 5, l(:label_subtask_plural) + ":", "LTR") + pdf.ln + issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| + buf = "#{child.tracker} # #{child.id}: #{child.subject}". + truncate(truncate_length) + level = 10 if level >= 10 + pdf.SetFontStyle('', 8) + pdf.RDMCell(35 + 135, 5, (level >=1 ? " " * level : "") + buf, border_first) + pdf.SetFontStyle('B', 8) + pdf.RDMCell(20, 5, child.status.to_s, border_last) + pdf.ln + end + end + + relations = issue.relations.select {|r| r.other_issue(issue).visible?} + unless relations.empty? + truncate_length = (!is_cjk? ? 80 : 60) + pdf.SetFontStyle('B', 9) + pdf.RDMCell(35 + 155, 5, l(:label_related_issues) + ":", "LTR") + pdf.ln + relations.each do |relation| + buf = relation.to_s(issue) do |other| + text = "" + if Setting.cross_project_issue_relations? + text += "#{relation.other_issue(issue).project} - " + end + text += "#{other.tracker} ##{other.id}: #{other.subject}" + text + end + buf = buf.truncate(truncate_length) + pdf.SetFontStyle('', 8) + pdf.RDMCell(35+155-60, 5, buf, border_first) + pdf.SetFontStyle('B', 8) + pdf.RDMCell(20, 5, relation.other_issue(issue).status.to_s, "") + pdf.RDMCell(20, 5, format_date(relation.other_issue(issue).start_date), "") + pdf.RDMCell(20, 5, format_date(relation.other_issue(issue).due_date), border_last) + pdf.ln + end + end + pdf.RDMCell(190, 5, "", "T") + pdf.ln + + if issue.changesets.any? && + User.current.allowed_to?(:view_changesets, issue.project) + pdf.SetFontStyle('B', 9) + pdf.RDMCell(190, 5, l(:label_associated_revisions), "B") + pdf.ln + issue.changesets.each do |changeset| + pdf.SetFontStyle('B', 8) + csstr = "#{l(:label_revision)} #{changeset.format_identifier} - " + csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s + pdf.RDMCell(190, 5, csstr) + pdf.ln + unless changeset.comments.blank? + pdf.SetFontStyle('', 8) + pdf.RDMwriteHTMLCell( + 190, 5, '', '', + changeset.comments.to_s, issue.attachments, "" + ) + end + pdf.ln + end + end + + if assoc[:journals].present? + pdf.SetFontStyle('B', 9) + pdf.RDMCell(190, 5, l(:label_history), "B") + pdf.ln + assoc[:journals].each do |journal| + pdf.SetFontStyle('B', 8) + title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}" + title += " (#{l(:field_private_notes)})" if journal.private_notes? + pdf.RDMCell(190, 5, title) + pdf.ln + pdf.SetFontStyle('I', 8) + details_to_strings(journal.visible_details, true).each do |string| + pdf.RDMMultiCell(190, 5, "- " + string) + end + if journal.notes? + pdf.ln unless journal.details.empty? + pdf.SetFontStyle('', 8) + text = + textilizable( + journal, :notes, + :only_path => false, + :edit_section_links => false, + :headings => false, + :inline_attachments => false + ) + pdf.RDMwriteFormattedCell(190, 5, '', '', text, issue.attachments, "") + end + pdf.ln + end + end + + if issue.attachments.any? + pdf.SetFontStyle('B', 9) + pdf.RDMCell(190, 5, l(:label_attachment_plural), "B") + pdf.ln + issue.attachments.each do |attachment| + pdf.SetFontStyle('', 8) + # start patch *** create links to attachments + url = send(:attachment_url, attachment, {}) + link = link_to attachment.filename, url, {} + link_Y = pdf.GetY + link_X = pdf.GetX + pdf.writeHTMLCell(80, 5, link_X, link_Y, link) + # end patch **** + pdf.RDMCell(20, 5, number_to_human_size(attachment.filesize), 0, 0, "R") + pdf.RDMCell(25, 5, format_date(attachment.created_on), 0, 0, "R") + pdf.RDMCell(65, 5, attachment.author.name, 0, 0, "R") + pdf.ln + end + end + pdf.output + end + end + end +end + +Redmine::Export::PDF::IssuesPdfHelper.prepend RedmineTinyFeatures::Helpers::IssuesPdfHelperPatch +ActionView::Base.prepend Redmine::Export::PDF::IssuesPdfHelper \ No newline at end of file diff --git a/spec/helpers/issues_pdf_helper_patch_spec.rb b/spec/helpers/issues_pdf_helper_patch_spec.rb new file mode 100644 index 0000000..d40b640 --- /dev/null +++ b/spec/helpers/issues_pdf_helper_patch_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'pdf/reader' +require "spec_helper" + +describe "IssuesPdfHelperPatch", type: :helper do + include Redmine::Export::PDF::IssuesPdfHelper + include ApplicationHelper + fixtures :issues, :attachments + + describe "issue export pdf" do + it "Should create attachment links" do + issue = Issue.find(2) + result = issue_to_pdf(issue) + pdf_reader = PDF::Reader.new(StringIO.new(result)) + + assert pdf_reader.pages.any? { |page| page.text.include?(issue.subject.to_s) }, "The expected subject is not present in the PDF" + + issue.attachments.each do |attachment| + assert_equal(true, result.include?("(http://test.host/attachments/#{attachment.id})")) + end + end + end +end From dc45123a7883691515fd316402bffbdad1c8c713 Mon Sep 17 00:00:00 2001 From: Vincent Robert Date: Fri, 5 Apr 2024 11:33:19 +0200 Subject: [PATCH 4/4] Add links to attached files in generated PDFs --- Gemfile | 4 +- README.md | 1 + .../issues_pdf_helper_patch.rb | 442 +++++++++--------- spec/helpers/issues_pdf_helper_patch_spec.rb | 24 +- 4 files changed, 247 insertions(+), 224 deletions(-) diff --git a/Gemfile b/Gemfile index e1727bc..0b07e6a 100644 --- a/Gemfile +++ b/Gemfile @@ -1 +1,3 @@ -gem 'pdf-reader' \ No newline at end of file +group :test do + gem 'pdf-reader' +end diff --git a/README.md b/README.md index ae1a0a7..4752e73 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Here is a complete list of the features: * Add a user parameter to also **display pagination links at the top of issues results** * Include the 'notes' field in workflows, providing the capability to **require notes** when updating an issue * Issues filter: **sort group-by options alphabetically** +* PDF exports: add **links to attached files in generated PDF**" ## Test status diff --git a/lib/redmine_tiny_features/issues_pdf_helper_patch.rb b/lib/redmine_tiny_features/issues_pdf_helper_patch.rb index 59b67ad..d1af426 100644 --- a/lib/redmine_tiny_features/issues_pdf_helper_patch.rb +++ b/lib/redmine_tiny_features/issues_pdf_helper_patch.rb @@ -1,253 +1,259 @@ # frozen_string_literal: true + include Redmine::Export::PDF +include CustomFieldsHelper module RedmineTinyFeatures - module Helpers - module IssuesPdfHelperPatch - # Returns a PDF string of a single issue - def issue_to_pdf(issue, assoc={}) - pdf = ITCPDF.new(current_language) - pdf.set_title("#{issue.project} - #{issue.tracker} ##{issue.id}") - pdf.alias_nb_pages - pdf.footer_date = format_date(User.current.today) - pdf.add_page - pdf.SetFontStyle('B', 11) - buf = "#{issue.project} - #{issue.tracker} ##{issue.id}" - pdf.RDMMultiCell(190, 5, buf) - pdf.SetFontStyle('', 8) - base_x = pdf.get_x - i = 1 - issue.ancestors.visible.each do |ancestor| - pdf.set_x(base_x + i) - buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}" - pdf.RDMMultiCell(190 - i, 5, buf) - i += 1 if i < 35 - end - pdf.SetFontStyle('B', 11) - pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s) - pdf.SetFontStyle('', 8) - pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}") - pdf.ln + module IssuesPdfHelperPatch - left = [] - left << [l(:field_status), issue.status] - left << [l(:field_priority), issue.priority] - left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id') - left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id') - left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id') - - right = [] - right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date') - right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date') - right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio') - right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours') - right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project) - - rows = left.size > right.size ? left.size : right.size - left << nil while left.size < rows - right << nil while right.size < rows - - custom_field_values = issue.visible_custom_field_values.reject {|value| value.custom_field.full_width_layout?} - half = (custom_field_values.size / 2.0).ceil - custom_field_values.each_with_index do |custom_value, i| - (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)] - end + # Returns a PDF string of a single issue + def issue_to_pdf(issue, assoc = {}) + pdf = ITCPDF.new(current_language) + pdf.set_title("#{issue.project} - #{issue.tracker} ##{issue.id}") + pdf.alias_nb_pages + pdf.footer_date = format_date(User.current.today) + pdf.add_page + pdf.SetFontStyle('B', 11) + buf = "#{issue.project} - #{issue.tracker} ##{issue.id}" + pdf.RDMMultiCell(190, 5, buf) + pdf.SetFontStyle('', 8) + base_x = pdf.get_x + i = 1 + issue.ancestors.visible.each do |ancestor| + pdf.set_x(base_x + i) + buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}" + pdf.RDMMultiCell(190 - i, 5, buf) + i += 1 if i < 35 + end + pdf.SetFontStyle('B', 11) + pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s) + pdf.SetFontStyle('', 8) + pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}") + pdf.ln - if pdf.get_rtl - border_first_top = 'RT' - border_last_top = 'LT' - border_first = 'R' - border_last = 'L' - else - border_first_top = 'LT' - border_last_top = 'RT' - border_first = 'L' - border_last = 'R' - end + left = [] + left << [l(:field_status), issue.status] + left << [l(:field_priority), issue.priority] + left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id') + left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id') + left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id') - rows = left.size > right.size ? left.size : right.size - rows.times do |i| - heights = [] - pdf.SetFontStyle('B', 9) - item = left[i] - heights << pdf.get_string_height(35, item ? "#{item.first}:" : "") - item = right[i] - heights << pdf.get_string_height(35, item ? "#{item.first}:" : "") - pdf.SetFontStyle('', 9) - item = left[i] - heights << pdf.get_string_height(60, item ? item.last.to_s : "") - item = right[i] - heights << pdf.get_string_height(60, item ? item.last.to_s : "") - height = heights.max - - item = left[i] - pdf.SetFontStyle('B', 9) - pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", - (i == 0 ? border_first_top : border_first), '', 0, 0) - pdf.SetFontStyle('', 9) - pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", - (i == 0 ? border_last_top : border_last), '', 0, 0) - - item = right[i] - pdf.SetFontStyle('B', 9) - pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", - (i == 0 ? border_first_top : border_first), '', 0, 0) - pdf.SetFontStyle('', 9) - pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", - (i == 0 ? border_last_top : border_last), '', 0, 2) - - pdf.set_x(base_x) - end + right = [] + right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date') + right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date') + right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio') + right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours') + right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project) + + rows = [left.size, right.size].max + left << nil while left.size < rows + right << nil while right.size < rows + + custom_field_values = issue.visible_custom_field_values.reject { |value| value.custom_field.full_width_layout? } + half = (custom_field_values.size / 2.0).ceil + custom_field_values.each_with_index do |custom_value, i| + (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)] + end + + if pdf.get_rtl + border_first_top = 'RT' + border_last_top = 'LT' + border_first = 'R' + border_last = 'L' + else + border_first_top = 'LT' + border_last_top = 'RT' + border_first = 'L' + border_last = 'R' + end + + rows = [left.size, right.size].max + rows.times do |i| + heights = [] + pdf.SetFontStyle('B', 9) + item = left[i] + heights << pdf.get_string_height(35, item ? "#{item.first}:" : "") + item = right[i] + heights << pdf.get_string_height(35, item ? "#{item.first}:" : "") + pdf.SetFontStyle('', 9) + item = left[i] + heights << pdf.get_string_height(60, item ? item.last.to_s : "") + item = right[i] + heights << pdf.get_string_height(60, item ? item.last.to_s : "") + height = heights.max + item = left[i] pdf.SetFontStyle('B', 9) - pdf.RDMCell(35 + 155, 5, l(:field_description), "LRT", 1) + pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", + (i == 0 ? border_first_top : border_first), '', 0, 0) pdf.SetFontStyle('', 9) + pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", + (i == 0 ? border_last_top : border_last), '', 0, 0) - # Set resize image scale - pdf.set_image_scale(1.6) - text = - textilizable( - issue, :description, - :only_path => false, - :edit_section_links => false, - :headings => false, - :inline_attachments => false - ) - pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB") - - custom_field_values = issue.visible_custom_field_values.select {|value| value.custom_field.full_width_layout?} - custom_field_values.each do |value| - text = show_value(value, false) - next if text.blank? - - pdf.SetFontStyle('B', 9) - pdf.RDMCell(35+155, 5, value.custom_field.name, "LRT", 1) - pdf.SetFontStyle('', 9) - pdf.RDMwriteHTMLCell(35+155, 5, '', '', text, issue.attachments, "LRB") + item = right[i] + pdf.SetFontStyle('B', 9) + pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", + (i == 0 ? border_first_top : border_first), '', 0, 0) + pdf.SetFontStyle('', 9) + pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", + (i == 0 ? border_last_top : border_last), '', 0, 2) + + pdf.set_x(base_x) + end + + pdf.SetFontStyle('B', 9) + pdf.RDMCell(35 + 155, 5, l(:field_description), "LRT", 1) + pdf.SetFontStyle('', 9) + + # Set resize image scale + pdf.set_image_scale(1.6) + text = pdf_format_text(issue, :description) + pdf.RDMwriteFormattedCell(35 + 155, 5, '', '', text, issue.attachments, "LRB") + + custom_field_values = issue.visible_custom_field_values.select { |value| value.custom_field.full_width_layout? } + custom_field_values.each do |value| + is_html = value.custom_field.full_text_formatting? + text = show_value(value, is_html) + next if text.blank? + + pdf.SetFontStyle('B', 9) + pdf.RDMCell(35 + 155, 5, value.custom_field.name, "LRT", 1) + pdf.SetFontStyle('', 9) + if is_html + pdf.RDMwriteFormattedCell(35 + 155, 5, '', '', text, issue.attachments, "LRB") + else + pdf.RDMwriteHTMLCell(35 + 155, 5, '', '', text, issue.attachments, "LRB") end + end - unless issue.leaf? - truncate_length = (!is_cjk? ? 90 : 65) - pdf.SetFontStyle('B', 9) - pdf.RDMCell(35+155, 5, l(:label_subtask_plural) + ":", "LTR") + unless issue.leaf? + truncate_length = (!is_cjk? ? 90 : 65) + pdf.SetFontStyle('B', 9) + pdf.RDMCell(35 + 155, 5, l(:label_subtask_plural) + ":", "LTR") + pdf.ln + issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| + buf = "#{child.tracker} # #{child.id}: #{child.subject}". + truncate(truncate_length) + level = 10 if level >= 10 + pdf.SetFontStyle('', 8) + pdf.RDMCell(35 + 135, 5, (level >= 1 ? " " * level : "") + buf, border_first) + pdf.SetFontStyle('B', 8) + pdf.RDMCell(20, 5, child.status.to_s, border_last) pdf.ln - issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| - buf = "#{child.tracker} # #{child.id}: #{child.subject}". - truncate(truncate_length) - level = 10 if level >= 10 - pdf.SetFontStyle('', 8) - pdf.RDMCell(35 + 135, 5, (level >=1 ? " " * level : "") + buf, border_first) - pdf.SetFontStyle('B', 8) - pdf.RDMCell(20, 5, child.status.to_s, border_last) - pdf.ln - end end + end - relations = issue.relations.select {|r| r.other_issue(issue).visible?} - unless relations.empty? - truncate_length = (!is_cjk? ? 80 : 60) - pdf.SetFontStyle('B', 9) - pdf.RDMCell(35 + 155, 5, l(:label_related_issues) + ":", "LTR") - pdf.ln - relations.each do |relation| - buf = relation.to_s(issue) do |other| - text = "" - if Setting.cross_project_issue_relations? - text += "#{relation.other_issue(issue).project} - " - end - text += "#{other.tracker} ##{other.id}: #{other.subject}" - text + relations = issue.relations.select { |r| r.other_issue(issue).visible? } + unless relations.empty? + truncate_length = (!is_cjk? ? 80 : 60) + pdf.SetFontStyle('B', 9) + pdf.RDMCell(35 + 155, 5, l(:label_related_issues) + ":", "LTR") + pdf.ln + relations.each do |relation| + buf = relation.to_s(issue) do |other| + text = "" + if Setting.cross_project_issue_relations? + text += "#{relation.other_issue(issue).project} - " end - buf = buf.truncate(truncate_length) - pdf.SetFontStyle('', 8) - pdf.RDMCell(35+155-60, 5, buf, border_first) - pdf.SetFontStyle('B', 8) - pdf.RDMCell(20, 5, relation.other_issue(issue).status.to_s, "") - pdf.RDMCell(20, 5, format_date(relation.other_issue(issue).start_date), "") - pdf.RDMCell(20, 5, format_date(relation.other_issue(issue).due_date), border_last) - pdf.ln + text += "#{other.tracker} ##{other.id}: #{other.subject}" + text end + buf = buf.truncate(truncate_length) + pdf.SetFontStyle('', 8) + pdf.RDMCell(35 + 155 - 60, 5, buf, border_first) + pdf.SetFontStyle('B', 8) + pdf.RDMCell(20, 5, relation.other_issue(issue).status.to_s, "") + pdf.RDMCell(20, 5, format_date(relation.other_issue(issue).start_date), "") + pdf.RDMCell(20, 5, format_date(relation.other_issue(issue).due_date), border_last) + pdf.ln end - pdf.RDMCell(190, 5, "", "T") - pdf.ln + end + pdf.RDMCell(190, 5, "", "T") + pdf.ln - if issue.changesets.any? && - User.current.allowed_to?(:view_changesets, issue.project) - pdf.SetFontStyle('B', 9) - pdf.RDMCell(190, 5, l(:label_associated_revisions), "B") + if issue.changesets.any? && + User.current.allowed_to?(:view_changesets, issue.project) + pdf.SetFontStyle('B', 9) + pdf.RDMCell(190, 5, l(:label_associated_revisions), "B") + pdf.ln + issue.changesets.each do |changeset| + pdf.SetFontStyle('B', 8) + csstr = "#{l(:label_revision)} #{changeset.format_identifier} - " + csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s + pdf.RDMCell(190, 5, csstr) pdf.ln - issue.changesets.each do |changeset| - pdf.SetFontStyle('B', 8) - csstr = "#{l(:label_revision)} #{changeset.format_identifier} - " - csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s - pdf.RDMCell(190, 5, csstr) - pdf.ln - unless changeset.comments.blank? - pdf.SetFontStyle('', 8) - pdf.RDMwriteHTMLCell( - 190, 5, '', '', - changeset.comments.to_s, issue.attachments, "" - ) - end - pdf.ln + unless changeset.comments.blank? + pdf.SetFontStyle('', 8) + pdf.RDMwriteHTMLCell( + 190, 5, '', '', + changeset.comments.to_s, issue.attachments, "" + ) end + pdf.ln end + end - if assoc[:journals].present? - pdf.SetFontStyle('B', 9) - pdf.RDMCell(190, 5, l(:label_history), "B") + if assoc[:journals].present? + pdf.SetFontStyle('B', 9) + pdf.RDMCell(190, 5, l(:label_history), "B") + pdf.ln + assoc[:journals].each do |journal| + pdf.SetFontStyle('B', 8) + title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}" + title += " (#{l(:field_private_notes)})" if journal.private_notes? + pdf.RDMCell(190, 5, title) pdf.ln - assoc[:journals].each do |journal| - pdf.SetFontStyle('B', 8) - title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}" - title += " (#{l(:field_private_notes)})" if journal.private_notes? - pdf.RDMCell(190, 5, title) - pdf.ln - pdf.SetFontStyle('I', 8) - details_to_strings(journal.visible_details, true).each do |string| - pdf.RDMMultiCell(190, 5, "- " + string) - end - if journal.notes? - pdf.ln unless journal.details.empty? - pdf.SetFontStyle('', 8) - text = - textilizable( - journal, :notes, - :only_path => false, - :edit_section_links => false, - :headings => false, - :inline_attachments => false - ) - pdf.RDMwriteFormattedCell(190, 5, '', '', text, issue.attachments, "") - end - pdf.ln + pdf.SetFontStyle('I', 8) + details_to_strings(journal.visible_details, true).each do |string| + pdf.RDMMultiCell(190, 5, "- " + string) + end + if journal.notes? + pdf.ln unless journal.details.empty? + pdf.SetFontStyle('', 8) + text = pdf_format_text(journal, :notes) + pdf.RDMwriteFormattedCell(190, 5, '', '', text, issue.attachments, "") end + pdf.ln end + end - if issue.attachments.any? - pdf.SetFontStyle('B', 9) - pdf.RDMCell(190, 5, l(:label_attachment_plural), "B") + if issue.attachments.any? + pdf.SetFontStyle('B', 9) + pdf.RDMCell(190, 5, l(:label_attachment_plural), "B") + pdf.ln + issue.attachments.each do |attachment| + pdf.SetFontStyle('', 8) + + ###### + # start patch *** create links to attachments + url = send(:attachment_url, attachment, {}) + link = link_to attachment.filename, url, {} + link_Y = pdf.GetY + link_X = pdf.GetX + pdf.writeHTMLCell(80, 5, link_X, link_Y, link) + # end patch **** + ############ + + pdf.RDMCell(20, 5, number_to_human_size(attachment.filesize), 0, 0, "R") + pdf.RDMCell(25, 5, format_date(attachment.created_on), 0, 0, "R") + pdf.RDMCell(65, 5, attachment.author.name, 0, 0, "R") pdf.ln - issue.attachments.each do |attachment| - pdf.SetFontStyle('', 8) - # start patch *** create links to attachments - url = send(:attachment_url, attachment, {}) - link = link_to attachment.filename, url, {} - link_Y = pdf.GetY - link_X = pdf.GetX - pdf.writeHTMLCell(80, 5, link_X, link_Y, link) - # end patch **** - pdf.RDMCell(20, 5, number_to_human_size(attachment.filesize), 0, 0, "R") - pdf.RDMCell(25, 5, format_date(attachment.created_on), 0, 0, "R") - pdf.RDMCell(65, 5, attachment.author.name, 0, 0, "R") - pdf.ln - end end - pdf.output end + pdf.output end + + def pdf_format_text(object, attribute) + textilizable(object, attribute, + :only_path => false, + :edit_section_links => false, + :headings => false, + :inline_attachments => false + ) + end + end end -Redmine::Export::PDF::IssuesPdfHelper.prepend RedmineTinyFeatures::Helpers::IssuesPdfHelperPatch -ActionView::Base.prepend Redmine::Export::PDF::IssuesPdfHelper \ No newline at end of file +Redmine::Export::PDF::IssuesPdfHelper.prepend RedmineTinyFeatures::IssuesPdfHelperPatch +ActionView::Base.prepend Redmine::Export::PDF::IssuesPdfHelper diff --git a/spec/helpers/issues_pdf_helper_patch_spec.rb b/spec/helpers/issues_pdf_helper_patch_spec.rb index d40b640..c31fae6 100644 --- a/spec/helpers/issues_pdf_helper_patch_spec.rb +++ b/spec/helpers/issues_pdf_helper_patch_spec.rb @@ -9,16 +9,30 @@ fixtures :issues, :attachments describe "issue export pdf" do - it "Should create attachment links" do + + it "includes a link for each attached file" do issue = Issue.find(2) result = issue_to_pdf(issue) pdf_reader = PDF::Reader.new(StringIO.new(result)) - assert pdf_reader.pages.any? { |page| page.text.include?(issue.subject.to_s) }, "The expected subject is not present in the PDF" + expect(pdf_reader.pages.any? { |page| page.text.include?(issue.subject.to_s) }).to be_truthy, "The expected subject is not present in the PDF" + + expect(issue.attachments.size).to eq 2 + expect(result).to include "URI (http://test.host/attachments/4)" + expect(result).to include "URI (http://test.host/attachments/10)" + end + + def assert_checksum(expected, filename) + filepath = Rails.root.join(filename) + checksum = Digest::MD5.hexdigest(File.read(filepath)) + assert checksum.in?(Array(expected)), "Bad checksum for file: #{filename}, local version should be reviewed: checksum=#{checksum}, expected=#{Array(expected).join(" or ")}" + end - issue.attachments.each do |attachment| - assert_equal(true, result.include?("(http://test.host/attachments/#{attachment.id})")) - end + it "checks core helper checksums Redmine::Export::PDF::IssuesPdfHelper" do + # the issue_to_pdf method is completely overridden, so it should be updated if the core method is modified + # Redmine 4.2.11 & 5.1.2 are validated + assert_checksum %w"c9a65d240988113acc0d3ab1ff0521b9 83ce301735da9b092f59e2865dea7349", "lib/redmine/export/pdf/issues_pdf_helper.rb" end + end end