Skip to content

Commit

Permalink
Merge pull request #15850 from opf/feature/45896-generate-pdf-documen…
Browse files Browse the repository at this point in the history
…t-from-a-work-package-description

[#45896] Generate PDF document from a work package description
  • Loading branch information
as-op authored Nov 20, 2024
2 parents 3c22b56 + 66289d5 commit 2fbaf7d
Show file tree
Hide file tree
Showing 44 changed files with 1,251 additions and 135 deletions.
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)
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)
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
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
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

0 comments on commit 2fbaf7d

Please sign in to comment.