Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#45896] Generate PDF document from a work package description #15850

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
8de9de7
refactor(pdf-export): sort module for better reuse
as-op Jun 13, 2024
9370164
Draft: [#45896] Generate PDF document from a work package description
as-op Jun 13, 2024
696ca05
Merge branch 'refs/heads/dev' into feature/45896-generate-pdf-documen…
as-op Jun 27, 2024
a5ccefa
Merge branch 'refs/heads/dev' into feature/45896-generate-pdf-documen…
as-op Jun 27, 2024
b7e8145
Merge branch 'refs/heads/dev' into feature/45896-generate-pdf-documen…
as-op Jun 27, 2024
283735a
obey rubocop
as-op Jun 27, 2024
299327b
add missing styles
as-op Jun 27, 2024
df6252a
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Jul 10, 2024
42aa57d
bump md-to-pdf gem with support for ckeditor page breaks
as-op Jul 10, 2024
af97d1f
use ckeditor with page break support
as-op Jul 10, 2024
089d1d8
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Jul 17, 2024
defc175
fix support for github markdown alerts
as-op Jul 24, 2024
d8c51ca
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Aug 6, 2024
3ec3c35
apply page size
as-op Aug 6, 2024
63e0f07
Merge branch 'refs/heads/dev' into feature/45896-generate-pdf-documen…
as-op Aug 6, 2024
494d095
fix applying page size
as-op Aug 6, 2024
1bad147
obey rubocop
as-op Aug 6, 2024
97d8731
Merge branch 'refs/heads/dev' into feature/45896-generate-pdf-documen…
as-op Aug 14, 2024
dcc93fe
add icon to menu entry
as-op Aug 14, 2024
722511b
obey eslint
as-op Aug 14, 2024
e986878
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Aug 15, 2024
92d5ebc
Merge branch 'refs/heads/dev' into feature/45896-generate-pdf-documen…
as-op Aug 26, 2024
483ec8f
use ckeditor with page break support
as-op Aug 26, 2024
0f34718
Merge branch 'refs/heads/dev' into feature/45896-generate-pdf-documen…
as-op Sep 3, 2024
39d274c
resolve conflict merge
as-op Sep 3, 2024
695d8e1
resolve conflict merge
as-op Sep 3, 2024
fb8b44c
obey rubocop
as-op Sep 3, 2024
5c8b6ce
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Sep 3, 2024
ada244b
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Sep 17, 2024
67d99e6
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Sep 23, 2024
225c40c
MVP: Switch to existing fonts that we use for the other exports
as-op Sep 23, 2024
066f54b
add feature flag generate_pdf_from_work_package; hide the menu entry
as-op Sep 23, 2024
652810d
update ckeditor build: support for page breaks
as-op Sep 23, 2024
c1b2d74
obey rubocop
as-op Sep 23, 2024
739068f
update ckeditor build: support for page breaks
as-op Sep 24, 2024
b0cb856
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Sep 26, 2024
1346c14
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Sep 26, 2024
e72cdc1
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Sep 26, 2024
e1334e0
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Sep 26, 2024
d0a2fbd
solve merge conflict with dev
as-op Sep 26, 2024
50b5f35
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Nov 4, 2024
01c352e
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Nov 12, 2024
4f7504b
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Nov 12, 2024
9514cc0
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Nov 13, 2024
968fda8
pdf generator: use turbo+primer dialog
as-op Nov 13, 2024
cb6514d
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Nov 13, 2024
cc4e60b
obey rubocop
as-op Nov 13, 2024
927904b
remove dedicated export format
as-op Nov 13, 2024
f0402f5
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Nov 14, 2024
098b9f8
add footer/header text inputs; use options in generator
as-op Nov 14, 2024
3603e07
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Nov 18, 2024
bf5cc48
obey rubocop
as-op Nov 18, 2024
9ab098b
obey rubocop
as-op Nov 18, 2024
b819e27
obey rubocop
as-op Nov 18, 2024
6e48511
move text to to I18n; add generator spec
as-op Nov 18, 2024
c6f84c9
test why text/hyphen fails
as-op Nov 18, 2024
d21e404
test why text/hyphen fails, part II
as-op Nov 18, 2024
2675adc
test why text/hyphen fails, part III
as-op Nov 18, 2024
7540543
obey rubocop
as-op Nov 18, 2024
d99d2f7
add tests for generate-pdf dialog
as-op Nov 19, 2024
bae8536
fix mock functions
as-op Nov 19, 2024
10907c9
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Nov 19, 2024
16e9e86
lint(codestyle): add empty line, remove sorting
as-op Nov 20, 2024
1543be9
lint(codestyle): better alias usage
as-op Nov 20, 2024
6317f82
lint(codestyle): shorter code
as-op Nov 20, 2024
999dd53
lint(legal): add copyright notice to every file; add frozen_string_li…
as-op Nov 20, 2024
067d0f5
fix(assets): remove OpenSans font
as-op Nov 20, 2024
f644789
fix(specs): add description to commented out PDF preview in specs
as-op Nov 20, 2024
aab65f5
Merge remote-tracking branch 'origin/feature/45896-generate-pdf-docum…
as-op Nov 20, 2024
d8bbf5e
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Nov 20, 2024
f844718
fix(endpoint): use POST for form submit endpoint
as-op Nov 20, 2024
003f874
Merge remote-tracking branch 'origin/feature/45896-generate-pdf-docum…
as-op Nov 20, 2024
66289d5
Merge branch 'dev' into feature/45896-generate-pdf-document-from-a-wo…
as-op Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<%= render(Primer::Alpha::Dialog.new(
title: I18n.t("pdf_generator.dialog.title"),
id: MODAL_ID,
size: :large
)) do |dialog|
dialog.with_header(variant: :large)
dialog.with_body do
primer_form_with(
url: generate_pdf_work_package_path(work_package),
data: { turbo: false },
id: GENERATE_PDF_FORM_ID
) do |form|
flex_layout do |modal_body|
generate_selects.each_with_index do |entry, index|
modal_body.with_row(mt: index == 0 ? 0 : 3) do
render(Primer::Alpha::Select.new(
name: entry[:name],
label: entry[:label],
caption: entry[:caption],
size: :medium,
input_width: :small,
value: entry[:options].find { |e| e[:default] }[:value])
) do |component|
entry[:options].each do |entry|
component.option(label: entry[:label], value: entry[:value])
end
end
end
end
modal_body.with_row(mt: 3) do
render Primer::Alpha::TextField.new(
name: :header_text_right,
label: I18n.t("pdf_generator.dialog.header_right.label"),
caption: I18n.t("pdf_generator.dialog.header_right.caption"),
visually_hide_label: false,
value: default_header_text_right
)
end
modal_body.with_row(mt: 3) do
render Primer::Alpha::TextField.new(
name: :footer_text_center,
label: I18n.t("pdf_generator.dialog.footer_center.label"),
caption: I18n.t("pdf_generator.dialog.footer_center.caption"),
visually_hide_label: false,
value: default_footer_text_center
)
end
end
end
end
dialog.with_footer do
render(Primer::ButtonComponent.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) }
render(Primer::ButtonComponent.new(
scheme: :primary, type: :submit, form: GENERATE_PDF_FORM_ID,
data: { "close-dialog-id": MODAL_ID })) { I18n.t("pdf_generator.dialog.submit") }
end
end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# 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 "text/hyphen"

module WorkPackages
module Exports
module Generate
class ModalDialogComponent < ApplicationComponent
MODAL_ID = "op-work-package-generate-pdf-dialog"
GENERATE_PDF_FORM_ID = "op-work-packages-generate-pdf-dialog-form"
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers

attr_reader :work_package, :params

def initialize(work_package:, params:)
super

@work_package = work_package
@params = params
end

def default_header_text_right
"#{work_package.type} ##{work_package.id}"
end

def default_footer_text_center
work_package.subject
end

def generate_selects
[
{
name: "hyphenation",
label: I18n.t("pdf_generator.dialog.hyphenation.label"),
caption: I18n.t("pdf_generator.dialog.hyphenation.caption"),
options: hyphenation_options
},
{
name: "paper_size",
label: I18n.t("pdf_generator.dialog.paper_size.label"),
caption: I18n.t("pdf_generator.dialog.paper_size.caption"),
options: paper_size_options
}
]
end

def hyphenation_options
# This is a list of languages that are supported by the hyphenation library
# https://rubygems.org/gems/text-hyphen
# The labels are the language names in the language itself (NOT to be put I18n)
as-op marked this conversation as resolved.
Show resolved Hide resolved
supported_languages = [
{ label: "Català", value: "ca" },
{ label: "Dansk", value: "da" },
{ label: "Deutsch", value: "de" },
{ label: "Eesti", value: "et" },
{ label: "English (UK)", value: "en_uk" },
{ label: "English (USA)", value: "en_us" },
{ label: "Español", value: "es" },
{ label: "Euskara", value: "eu" },
{ label: "Français", value: "fr" },
{ label: "Gaeilge", value: "ga" },
{ label: "Hrvatski", value: "hr" },
{ label: "Indonesia", value: "id" },
{ label: "Interlingua", value: "ia" },
{ label: "Italiano", value: "it" },
{ label: "Magyar", value: "hu" },
{ label: "Melayu", value: "ms" },
{ label: "Nederlands", value: "nl" },
{ label: "Norsk", value: "no" },
{ label: "Polski", value: "pl" },
{ label: "Português", value: "pt" },
{ label: "Slovenčina", value: "sk" },
{ label: "Suomi", value: "fi" },
{ label: "Svenska", value: "sv" },
{ label: "Ísland", value: "is" },
{ label: "Čeština", value: "cs" },
{ label: "Монгол", value: "mn" },
{ label: "Русский", value: "ru" }
]

[{ value: "", label: "Off", default: true }].concat(supported_languages)
as-op marked this conversation as resolved.
Show resolved Hide resolved
end

def paper_size_options
[
{ label: "A4", value: "A4", default: true },
{ label: "A3", value: "A3" },
{ label: "A2", value: "A2" },
{ label: "A1", value: "A1" },
{ label: "A0", value: "A0" },
{ label: "Executive", value: "EXECUTIVE" },
{ label: "Folio", value: "FOLIO" },
{ label: "Letter", value: "LETTER" },
{ label: "Tabloid", value: "TABLOID" }
]
end
end
end
end
end
17 changes: 15 additions & 2 deletions app/controllers/work_packages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ class WorkPackagesController < ApplicationController
accept_key_auth :index, :show

before_action :authorize_on_work_package,
:project, only: :show
:project, only: %i[show generate_pdf_dialog generate_pdf]
before_action :load_and_authorize_in_optional_project,
:check_allowed_export,
:protect_from_unauthorized_export, only: %i[index export_dialog]

before_action :authorize, only: :show_conflict_flash_message
authorization_checked! :index, :show, :export_dialog
authorization_checked! :index, :show, :export_dialog, :generate_pdf_dialog, :generate_pdf

before_action :load_and_validate_query, only: :index, unless: -> { request.format.html? }
before_action :load_work_packages, only: :index, if: -> { request.format.atom? }
Expand Down Expand Up @@ -93,6 +93,19 @@ def export_dialog
respond_with_dialog WorkPackages::Exports::ModalDialogComponent.new(query: @query, project: @project, title: params[:title])
end

def generate_pdf_dialog
respond_with_dialog WorkPackages::Exports::Generate::ModalDialogComponent.new(work_package: work_package, params: params)
end

def generate_pdf
exporter = WorkPackage::PDFExport::DocumentGenerator.new(work_package, params)
export = exporter.export!
send_data(export.content, type: export.mime_type, filename: export.title)
rescue ::Exports::ExportError => e
flash[:error] = e.message
redirect_back(fallback_location: work_package_path(work_package))
end

def show_conflict_flash_message
scheme = params[:scheme]&.to_sym || :danger

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

require "mini_magick"

module WorkPackage::PDFExport::Attachments
module WorkPackage::PDFExport::Common::Attachments
def resize_image(file_path)
tmp_file = Tempfile.new(["temp_image", File.extname(file_path)])
@resized_images = [] if @resized_images.nil?
Expand All @@ -51,4 +51,27 @@ def delete_all_resized_images
@resized_images&.each(&:close!)
@resized_images = []
end

def attachment_image_local_file(attachment)
attachment.file.local_file
rescue StandardError => e
Rails.logger.error "Failed to access attachment #{attachment.id} file: #{e}"
nil # return nil as if the id was wrong and the attachment obj has not been found
end

def attachment_image_filepath(work_package, src)
# images are embedded into markup with the api-path as img.src
attachment = attachment_by_api_content_src(work_package, src)
return nil if attachment.nil? || !pdf_embeddable?(attachment.content_type)

local_file = attachment_image_local_file(attachment)
return nil if local_file.nil?

resize_image(local_file.path)
end

def attachment_by_api_content_src(work_package, src)
# find attachment by api-path
work_package.attachments.find { |a| api_url_helpers.attachment_content(a.id) == src }
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++

module WorkPackage::PDFExport::Common
module WorkPackage::PDFExport::Common::Common
as-op marked this conversation as resolved.
Show resolved Hide resolved
include Redmine::I18n
include ActionView::Helpers::TextHelper
include ActionView::Helpers::NumberHelper
Expand All @@ -36,7 +36,7 @@ module WorkPackage::PDFExport::Common
private

def get_pdf(_language)
::WorkPackage::PDFExport::View.new(current_language)
::WorkPackage::PDFExport::Common::View.new(current_language)
end

def field_value(work_package, attribute)
Expand Down Expand Up @@ -76,26 +76,11 @@ def with_vertical_margin(opts)
pdf.move_down(opts[:bottom_margin]) if opts.key?(:bottom_margin)
end

def write_optional_page_break
space_from_bottom = pdf.y - pdf.bounds.bottom
if space_from_bottom < styles.page_break_threshold
pdf.start_new_page
end
end

def get_column_value(work_package, column_name)
formatter = formatter_for(column_name, :pdf)
formatter.format(work_package)
end

def get_column_value_cell(work_package, column_name)
value = get_column_value(work_package, column_name)
return get_id_column_cell(work_package, value) if column_name == :id
return get_subject_column_cell(work_package, value) if wants_report? && column_name == :subject

escape_tags(value)
end

def get_formatted_value(value, column_name)
return "" if value.nil?

Expand All @@ -108,19 +93,6 @@ def escape_tags(value)
value.to_s.gsub("<", "&lt;").gsub(">", "&gt;")
end

def get_id_column_cell(work_package, value)
href = url_helpers.work_package_url(work_package)
make_link_href_cell(href, value)
end

def get_subject_column_cell(work_package, value)
make_link_anchor(work_package.id, escape_tags(value))
end

def make_link_href_cell(href, caption)
"<color rgb='#{styles.link_color}'><link href='#{href}'>#{caption}</link></color>"
end

def make_link_anchor(anchor, caption)
"<link anchor=\"#{anchor}\">#{caption}</link>"
end
Expand Down Expand Up @@ -306,6 +278,10 @@ def title_datetime
DateTime.now.strftime("%Y-%m-%d_%H-%M")
end

def footer_date
format_time(Time.zone.now)
end

def current_page_nr
pdf.page_number + @page_count - (with_cover? ? 1 : 0)
end
Expand Down
51 changes: 51 additions & 0 deletions app/models/work_package/pdf_export/common/logo.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 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::PDFExport::Common::Logo
as-op marked this conversation as resolved.
Show resolved Hide resolved
def logo_image
image_obj, image_info = pdf.build_image_object(logo_image_filename)
[image_obj, image_info]
end

def logo_image_filename
custom_logo_image_filename || Rails.root.join("app/assets/images/logo_openproject.png")
end

def custom_logo_image_filename
return unless CustomStyle.current.present? &&
CustomStyle.current.export_logo.present? && CustomStyle.current.export_logo.local_file.present?

image_file = CustomStyle.current.export_logo.local_file.path
content_type = OpenProject::ContentTypeDetector.new(image_file).detect
return unless pdf_embeddable?(content_type)

image_file
end
end
Loading
Loading