mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
[#65994] PDF export of project list
https://community.openproject.org/work_packages/65994
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
<% helpers.supported_export_formats.each do |key| %>
|
||||
<li class="op-export-options--option">
|
||||
<%= link_to projects_path(format: key, **helpers.projects_query_params.except(:page, :per_page)),
|
||||
"data-controller": "job-dialog",
|
||||
"data-job-dialog-close-dialog-id-value": MODAL_ID,
|
||||
class: "op-export-options--option-link" do %>
|
||||
<%= helpers.op_icon("icon-big icon-export-#{key}") %>
|
||||
<span class="op-export-options--option-label"><%= t("export.format.#{key}") %></span>
|
||||
|
||||
@@ -128,7 +128,7 @@ module Exports::PDF::Common::Common
|
||||
previous_color = pdf.stroke_color
|
||||
previous_line_width = pdf.line_width
|
||||
@pdf.stroke do
|
||||
pdf.stroke_color = color
|
||||
pdf.stroke_color = color if color
|
||||
pdf.line_width = height
|
||||
pdf.horizontal_line left, right, at: top
|
||||
end
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
@@ -31,7 +29,7 @@ module Projects::Exports
|
||||
module Formatters
|
||||
class Active < ::Exports::Formatters::Default
|
||||
def self.apply?(attribute, export_format)
|
||||
export_format == :pdf && %i[active].include?(attribute.to_sym)
|
||||
export_format == :pdf && attribute.to_sym == :active
|
||||
end
|
||||
|
||||
##
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# 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 Projects::Exports
|
||||
module Formatters
|
||||
class Favored < ::Exports::Formatters::Default
|
||||
def self.apply?(attribute, export_format)
|
||||
export_format == :pdf && attribute.to_sym == :favored
|
||||
end
|
||||
|
||||
##
|
||||
# Takes a project and returns yes/no depending on the favored attribute
|
||||
def format(project, **)
|
||||
project.favored_by?(User.current) ? I18n.t(:general_text_Yes) : I18n.t(:general_text_No)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -31,7 +31,7 @@ module Projects::Exports
|
||||
module Formatters
|
||||
class Public < ::Exports::Formatters::Default
|
||||
def self.apply?(attribute, export_format)
|
||||
export_format == :pdf && %i[public].include?(attribute.to_sym)
|
||||
export_format == :pdf && attribute.to_sym == :public
|
||||
end
|
||||
|
||||
##
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# 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 Projects::Exports
|
||||
module Formatters
|
||||
class RequiredDiskSpace < ::Exports::Formatters::Default
|
||||
def self.apply?(attribute, export_format)
|
||||
export_format == :pdf && attribute.to_sym == :required_disk_space
|
||||
end
|
||||
|
||||
##
|
||||
# Takes a project and returns the formatted value if the required disk space is greater than 0.
|
||||
def format(project, **)
|
||||
return "" unless project.required_disk_space.to_i > 0
|
||||
|
||||
number_to_human_size(project.required_disk_space, precision: 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,141 @@
|
||||
# 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 Projects::Exports
|
||||
class PDF < QueryExporter
|
||||
include Exports::PDF::Common::Common
|
||||
include Exports::PDF::Common::Logo
|
||||
include Exports::PDF::Common::Attachments
|
||||
include Exports::PDF::Common::Markdown
|
||||
include Exports::PDF::Components::Page
|
||||
include Exports::PDF::Components::WpTable
|
||||
include Exports::PDF::Components::Cover
|
||||
include Projects::Exports::PDFExport::TableOfContent
|
||||
include Projects::Exports::PDFExport::Report
|
||||
include Projects::Exports::PDFExport::InfoMap
|
||||
include Projects::Exports::PDFExport::Styles
|
||||
|
||||
attr_accessor :pdf
|
||||
|
||||
def initialize(object, options = {})
|
||||
super
|
||||
setup_page!
|
||||
end
|
||||
|
||||
def setup_page!
|
||||
self.pdf = get_pdf
|
||||
configure_page_size!(:portrait)
|
||||
pdf.title = heading
|
||||
end
|
||||
|
||||
def export!
|
||||
file = render_pdf(all_projects)
|
||||
success(file)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to generate PDF export: #{e.message}:\n#{e.backtrace.join("\n")}"
|
||||
error(I18n.t(:error_pdf_failed_to_export, error: e.message))
|
||||
end
|
||||
|
||||
def title
|
||||
build_pdf_filename([heading].join("_"))
|
||||
end
|
||||
|
||||
def heading
|
||||
query.name || I18n.t(:label_project_plural)
|
||||
end
|
||||
|
||||
def footer_title
|
||||
heading
|
||||
end
|
||||
|
||||
def cover_page_heading
|
||||
heading
|
||||
end
|
||||
|
||||
def cover_page_dates
|
||||
nil
|
||||
end
|
||||
|
||||
def cover_page_subheading
|
||||
User.current&.name
|
||||
end
|
||||
|
||||
def cover_page_title
|
||||
@cover_page_title ||= Setting.app_title
|
||||
end
|
||||
|
||||
def render_pdf(projects, filename: "pdf_export")
|
||||
@page_count = 0
|
||||
@id_project_meta_map, flat_list = build_meta_infos_map(projects)
|
||||
file = render_projects_report_pdf(flat_list, filename)
|
||||
if wants_total_page_nrs?
|
||||
@total_page_nr = @page_count
|
||||
@page_count = 0
|
||||
setup_page! # clear current pdf
|
||||
file = render_projects_report_pdf(flat_list, filename)
|
||||
end
|
||||
file
|
||||
end
|
||||
|
||||
def wants_total_page_nrs?
|
||||
true
|
||||
end
|
||||
|
||||
def with_cover?
|
||||
true
|
||||
end
|
||||
|
||||
def render_projects_report_pdf(flat_list, filename)
|
||||
render_projects_report(flat_list)
|
||||
file = Tempfile.new(filename)
|
||||
pdf.render_file(file.path)
|
||||
@page_count += pdf.page_count
|
||||
delete_all_resized_images
|
||||
file.close
|
||||
file
|
||||
end
|
||||
|
||||
def write_after_pages!
|
||||
write_headers!
|
||||
write_footers!
|
||||
end
|
||||
|
||||
def render_projects_report(flat_list)
|
||||
write_cover_page! if with_cover?
|
||||
if flat_list.size > 1
|
||||
render_toc(flat_list, @id_project_meta_map)
|
||||
elsif !flat_list.empty?
|
||||
@id_project_meta_map[flat_list[0].id][:level_path] = []
|
||||
end
|
||||
render_report(flat_list, @id_project_meta_map)
|
||||
write_after_pages!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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 Projects::Exports::PDFExport
|
||||
module InfoMap
|
||||
def build_flat_meta_infos_map(projects)
|
||||
infos_map = {}
|
||||
projects.each_with_index do |project, index|
|
||||
infos_map[project.id] = { level_path: [index + 1], level: 0, children: [], project: }
|
||||
end
|
||||
[infos_map, projects.to_a]
|
||||
end
|
||||
|
||||
def init_meta_infos_map_nodes(projects)
|
||||
infos_map = {}
|
||||
projects.each do |project|
|
||||
infos_map[project.id] = { level_path: [], level: 0, children: [], project: }
|
||||
end
|
||||
infos_map
|
||||
end
|
||||
|
||||
def link_meta_infos_map_nodes(infos_map, projects)
|
||||
projects.reject { |wp| wp.parent_id.nil? }.each do |project|
|
||||
parent = infos_map[project.parent_id]
|
||||
infos_map[project.id][:parent] = parent
|
||||
parent[:children].push(infos_map[project.id]) if parent
|
||||
end
|
||||
infos_map
|
||||
end
|
||||
|
||||
def build_meta_infos_map(projects)
|
||||
# build a quick access map for the hierarchy tree
|
||||
infos_map = init_meta_infos_map_nodes projects
|
||||
# connect parent and children (only wp available in the query)
|
||||
infos_map = link_meta_infos_map_nodes infos_map, projects
|
||||
# recursive travers creating level index path e.g. [1, 2, 1] from root nodes
|
||||
root_nodes = infos_map.values.select { |node| node[:parent].nil? }
|
||||
flat_list = []
|
||||
fill_meta_infos_map_nodes({ children: root_nodes }, [], flat_list)
|
||||
[infos_map, flat_list]
|
||||
end
|
||||
|
||||
def fill_meta_infos_map_nodes(node, level_path, flat_list)
|
||||
node[:level_path] = level_path
|
||||
flat_list.push(node[:project]) unless node[:project].nil?
|
||||
index = 1
|
||||
node[:children].each do |sub|
|
||||
fill_meta_infos_map_nodes(sub, level_path + [index], flat_list)
|
||||
index += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,309 @@
|
||||
# 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 Projects::Exports::PDFExport
|
||||
module Report
|
||||
def render_report(projects, info_map)
|
||||
projects.each do |project|
|
||||
write_optional_page_break
|
||||
write_project_detail(project, info_map[project.id])
|
||||
end
|
||||
end
|
||||
|
||||
def write_project_detail(project, info_map_entry)
|
||||
info_map_entry[:page_number] = current_page_nr
|
||||
with_margin(styles.project_margins) do
|
||||
write_project_title(project, info_map_entry[:level_path])
|
||||
write_project_detail_content(project)
|
||||
write_project_work_packages(project)
|
||||
end
|
||||
end
|
||||
|
||||
def write_project_title(project, level_path)
|
||||
text_style = styles.project_title
|
||||
with_margin(styles.project_title_margins) do
|
||||
link_target_at_current_y(project.id)
|
||||
level_string_width = write_project_level(level_path, text_style)
|
||||
pdf.indent(level_string_width) do
|
||||
pdf.formatted_text([text_style.merge({ text: project.name, link: url_helpers.project_url(project) })])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write_project_level(level_path, text_style)
|
||||
return 0 if level_path.empty?
|
||||
|
||||
level_string = "#{level_path.join('.')}. "
|
||||
level_string_width = measure_text_width(level_string, text_style)
|
||||
pdf.float { pdf.formatted_text([text_style.merge({ text: level_string })]) }
|
||||
level_string_width
|
||||
end
|
||||
|
||||
def selects
|
||||
@selects = query
|
||||
.selects
|
||||
.reject { |s| s.is_a?(Queries::Selects::NotExistingSelect) }
|
||||
end
|
||||
|
||||
def write_project_detail_content(project)
|
||||
return if selects.empty?
|
||||
|
||||
entries = []
|
||||
selects.each do |select|
|
||||
entries = process_select(project, select, entries)
|
||||
end
|
||||
write_table_entries(entries) unless entries.empty?
|
||||
end
|
||||
|
||||
def process_select(project, select, entries)
|
||||
if custom_field_select?(select)
|
||||
process_custom_field_select(project, select, entries)
|
||||
elsif project_phase_select?(select)
|
||||
process_project_phase_select(project, select, entries)
|
||||
elsif can_view_attribute?(project, select.attribute)
|
||||
process_attribute_select(project, select, entries)
|
||||
else
|
||||
entries
|
||||
end
|
||||
end
|
||||
|
||||
def process_custom_field_select(project, select, entries)
|
||||
return entries unless custom_field_active_in_project?(project, select.custom_field)
|
||||
|
||||
if select.custom_field.formattable?
|
||||
write_table_entries(entries) unless entries.empty?
|
||||
write_formattable_custom_field(project, select.custom_field)
|
||||
[]
|
||||
else
|
||||
entry = table_entry(project, "cf_#{select.custom_field.id}", select.caption)
|
||||
entries.push(entry) if entry
|
||||
entries
|
||||
end
|
||||
end
|
||||
|
||||
def process_project_phase_select(project, select, entries)
|
||||
entry = user_can_view_project_phases?(project) ? table_entry_project_phase(project, select) : nil
|
||||
entries.push(entry) if entry
|
||||
entries
|
||||
end
|
||||
|
||||
def process_attribute_select(project, select, entries)
|
||||
if attribute_formattable?(select.attribute)
|
||||
write_table_entries(entries) unless entries.empty?
|
||||
write_formattable_attribute(project, select.attribute, select.caption)
|
||||
[]
|
||||
else
|
||||
entry = table_entry(project, select.attribute, select.caption)
|
||||
entries.push(entry) if entry
|
||||
entries
|
||||
end
|
||||
end
|
||||
|
||||
def project_phase_select?(select)
|
||||
select.is_a?(::Queries::Projects::Selects::ProjectPhase)
|
||||
end
|
||||
|
||||
def table_entry_project_phase(project, select)
|
||||
phase = project.phases.active.find_by(definition: select.project_phase_definition)
|
||||
return nil if phase.nil?
|
||||
|
||||
[
|
||||
{ content: select.caption }.merge(styles.project_attributes_table_label_cell),
|
||||
format_phase_value(phase)
|
||||
]
|
||||
end
|
||||
|
||||
def format_phase_value(phase)
|
||||
start = if phase.start_date.present?
|
||||
format_date(phase.start_date)
|
||||
else
|
||||
I18n.t("js.label_no_start_date")
|
||||
end
|
||||
|
||||
finish = if phase.finish_date.present?
|
||||
format_date(phase.finish_date)
|
||||
else
|
||||
I18n.t("js.label_no_due_date")
|
||||
end
|
||||
|
||||
"#{start} - #{finish}"
|
||||
end
|
||||
|
||||
def table_entry(project, value_name, caption)
|
||||
value = format_attribute(project, value_name, :pdf)
|
||||
return nil if hide_empty_attributes? && value.blank?
|
||||
|
||||
value = make_link_href_cell(url_helpers.project_url(project), value) if value_name == :id
|
||||
[
|
||||
{ content: caption }.merge(styles.project_attributes_table_label_cell),
|
||||
value || ""
|
||||
]
|
||||
end
|
||||
|
||||
def can_view_attribute?(_project, attribute)
|
||||
return false if attribute.nil? || %i[name favored].include?(attribute)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def user_can_view_project_phases?(project)
|
||||
User.current.allowed_in_project?(:view_project_phases, project) && project.phases.active.any?
|
||||
end
|
||||
|
||||
def attribute_formattable?(attribute)
|
||||
%i[description status_explanation].include? attribute
|
||||
end
|
||||
|
||||
def custom_field_select?(select)
|
||||
select.is_a?(::Queries::Projects::Selects::CustomField)
|
||||
end
|
||||
|
||||
def custom_field_active_in_project?(project, custom_field)
|
||||
custom_field.is_for_all? ||
|
||||
project.project_custom_field_project_mappings.exists?(custom_field_id: custom_field.id)
|
||||
end
|
||||
|
||||
def write_formattable_attribute(project, attribute, caption)
|
||||
write_project_markdown project.try(attribute), caption
|
||||
end
|
||||
|
||||
def write_formattable_custom_field(project, custom_field)
|
||||
custom_field_value = project.custom_value_for(custom_field.id)
|
||||
write_project_markdown custom_field_value.value, custom_field.name
|
||||
end
|
||||
|
||||
def write_project_markdown(value, caption)
|
||||
return if hide_empty_attributes? && value.blank?
|
||||
|
||||
write_markdown_label(caption)
|
||||
value = Prawn::Text::NBSP if value.blank?
|
||||
with_margin(styles.project_markdown_margins) do
|
||||
write_markdown!(value, styles.project_markdown_styling_yml)
|
||||
end
|
||||
end
|
||||
|
||||
def write_markdown_label(caption)
|
||||
with_margin(styles.project_markdown_label_margins) do
|
||||
pdf.formatted_text([styles.project_markdown_label.merge({ text: caption })])
|
||||
end
|
||||
end
|
||||
|
||||
def write_table_entries(row_entries)
|
||||
return if row_entries.empty?
|
||||
|
||||
rows = if attributes_table_4_column?
|
||||
0.step(row_entries.length - 1, 2).map do |i|
|
||||
row_entries[i] + (row_entries[i + 1] || ["", ""])
|
||||
end
|
||||
else
|
||||
row_entries
|
||||
end
|
||||
|
||||
pdf.table(
|
||||
rows,
|
||||
column_widths: attributes_table_column_widths,
|
||||
cell_style: styles.project_attributes_table_cell.merge({ inline_format: true })
|
||||
)
|
||||
end
|
||||
|
||||
def attributes_table_4_column?
|
||||
true
|
||||
end
|
||||
|
||||
def hide_empty_attributes?
|
||||
true
|
||||
end
|
||||
|
||||
def attributes_table_column_widths
|
||||
widths = if attributes_table_4_column?
|
||||
# label | value | label | value
|
||||
[1.5, 2.0, 1.5, 2.0]
|
||||
else
|
||||
# label | value
|
||||
[1.0, 3.0]
|
||||
end
|
||||
ratio = pdf.bounds.width / widths.sum
|
||||
widths.map { |w| w * ratio }
|
||||
end
|
||||
|
||||
def write_project_work_packages(project)
|
||||
return if work_package_tables.empty?
|
||||
|
||||
with_margin(styles.wp_tables_margins) do
|
||||
write_project_work_packages_tables(project)
|
||||
end
|
||||
end
|
||||
|
||||
def write_project_work_packages_tables(project)
|
||||
work_package_tables.each do |table|
|
||||
type_id = table[:type_id]
|
||||
column_names = table[:column_names]
|
||||
sort_criteria = table[:sort_criteria] || []
|
||||
caption = table[:caption]
|
||||
|
||||
query = build_work_packages_query(project, type_id, column_names, sort_criteria)
|
||||
write_project_work_packages_table(query, caption) if query
|
||||
end
|
||||
end
|
||||
|
||||
def work_package_tables
|
||||
[]
|
||||
end
|
||||
|
||||
def build_work_packages_query(project, type_id, column_names, sort_criteria = [])
|
||||
return nil unless type_id
|
||||
|
||||
query = Query.new(project:)
|
||||
query.filters.clear
|
||||
query.include_subprojects = false
|
||||
query.column_names = column_names
|
||||
query.sort_criteria = sort_criteria
|
||||
query.add_filter("type_id", "=", [type_id])
|
||||
query
|
||||
end
|
||||
|
||||
def get_column_value_cell(work_package, column_name)
|
||||
value = get_value_cell_by_column(work_package, column_name, false)
|
||||
value = make_link_href(url_helpers.work_package_url(work_package), value) if column_name == :subject
|
||||
value
|
||||
end
|
||||
|
||||
def write_project_work_packages_table(query, caption)
|
||||
return unless query
|
||||
|
||||
work_packages = query.results.work_packages
|
||||
return unless work_packages.any?
|
||||
|
||||
write_optional_page_break
|
||||
write_markdown_label(caption)
|
||||
write_work_packages_table!(work_packages, query)
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,321 @@
|
||||
page:
|
||||
page_size: "A4"
|
||||
margin_top: 80
|
||||
margin_bottom: 60
|
||||
margin_left: 40
|
||||
margin_right: 40
|
||||
page_break_threshold: 200
|
||||
link_color: "175A8E"
|
||||
|
||||
page_header:
|
||||
offset: 20
|
||||
size: 8
|
||||
|
||||
page_footer:
|
||||
offset: -30
|
||||
size: 8
|
||||
spacing: 6
|
||||
|
||||
page_logo:
|
||||
height: 20
|
||||
align: "right"
|
||||
offset: 10
|
||||
|
||||
page_heading:
|
||||
size: 14
|
||||
styles: [ "bold" ]
|
||||
margin_bottom: 10
|
||||
|
||||
cover:
|
||||
header:
|
||||
logo_height: 25
|
||||
border:
|
||||
color: 'd3dee3'
|
||||
height: 1
|
||||
offset: 6
|
||||
footer:
|
||||
size: 10
|
||||
color: '414d5f'
|
||||
offset: 30
|
||||
hero:
|
||||
padding_right: 144
|
||||
padding_top: 120
|
||||
title:
|
||||
max_height: 30
|
||||
spacing: 10
|
||||
font: 'SpaceMono'
|
||||
color: '414d5f'
|
||||
size: 10
|
||||
heading:
|
||||
spacing: 14
|
||||
color: '414d5f'
|
||||
styles:
|
||||
- bold
|
||||
size: 16
|
||||
dates:
|
||||
spacing: 4
|
||||
max_height: 16
|
||||
color: '414d5f'
|
||||
size: 10
|
||||
styles:
|
||||
- bold
|
||||
subheading:
|
||||
max_height: 30
|
||||
color: '414d5f'
|
||||
size: 10
|
||||
|
||||
toc:
|
||||
title:
|
||||
size: 16
|
||||
color: "000000"
|
||||
margin_bottom: 4
|
||||
subject_indent: 4
|
||||
indent_mode: "stairs"
|
||||
margin_top: 10
|
||||
margin_bottom: 20
|
||||
item:
|
||||
size: 10
|
||||
color: "000000"
|
||||
margin_bottom: 12
|
||||
item_level_1:
|
||||
size: 11
|
||||
styles: [ "bold" ]
|
||||
margin_top: 6
|
||||
margin_bottom: 6
|
||||
item_level_2:
|
||||
size: 11
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
item_level_3:
|
||||
size: 10
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
item_level_4:
|
||||
size: 10
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
item_level_5:
|
||||
size: 10
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
|
||||
project:
|
||||
margin_bottom: 20
|
||||
title:
|
||||
size: 12
|
||||
color: "000000"
|
||||
margin_bottom: 8
|
||||
styles: [ "bold" ]
|
||||
markdown_label:
|
||||
size: 10
|
||||
color: "000000"
|
||||
styles: [ "bold" ]
|
||||
margin_bottom: 3
|
||||
markdown_margins:
|
||||
margin_bottom: 8
|
||||
markdown:
|
||||
font:
|
||||
size: 10
|
||||
leading: 3
|
||||
header:
|
||||
size: 8
|
||||
styles: [ "bold" ]
|
||||
padding_top: 4
|
||||
header_1:
|
||||
size: 10
|
||||
header_2:
|
||||
size: 10
|
||||
header_3:
|
||||
size: 9
|
||||
paragraph:
|
||||
align: "left"
|
||||
unordered_list:
|
||||
spacing: 1
|
||||
unordered_list_point:
|
||||
spacing: 4
|
||||
ordered_list:
|
||||
spacing: 1
|
||||
ordered_list_point:
|
||||
spacing: 4
|
||||
spanning: true
|
||||
list_style_type: decimal
|
||||
ordered_list_point_2:
|
||||
list_style_type: lower-latin
|
||||
ordered_list_point_3:
|
||||
list_style_type: lower-roman
|
||||
ordered_list_point_4:
|
||||
list_style_type: upper-latin
|
||||
ordered_list_point_5:
|
||||
list_style_type: upper-roman
|
||||
task_list:
|
||||
spacing: 1
|
||||
task_list_point:
|
||||
spacing: 4
|
||||
checked: "☑"
|
||||
unchecked: "☐"
|
||||
link:
|
||||
color: "175A8E"
|
||||
styles: [ ]
|
||||
code:
|
||||
color: "880000"
|
||||
size: 9
|
||||
font: "SpaceMono"
|
||||
blockquote:
|
||||
background_color: "f4f9ff"
|
||||
size: 10
|
||||
styles: [ "italic" ]
|
||||
color: "0f3b66"
|
||||
border_color: "b8d6f4"
|
||||
border_width: 1
|
||||
padding: 4
|
||||
padding_left: 6
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
no_border_left: false
|
||||
no_border_right: true
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
image:
|
||||
align: "center"
|
||||
margin_bottom: 4
|
||||
caption:
|
||||
size: 8
|
||||
align: "center"
|
||||
codeblock:
|
||||
background_color: "F5F5F5"
|
||||
color: "880000"
|
||||
padding: 10
|
||||
size: 8
|
||||
margin_top: 10
|
||||
margin_bottom: 10
|
||||
font: "SpaceMono"
|
||||
table:
|
||||
auto_width: true
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
header:
|
||||
size: 9
|
||||
styles: [ "bold" ]
|
||||
background_color: "F0F0F0"
|
||||
cell:
|
||||
size: 8
|
||||
border_width: 0.25
|
||||
padding: 5
|
||||
html_table:
|
||||
auto_width: true
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
header:
|
||||
size: 9
|
||||
styles: [ "bold" ]
|
||||
border_width: 0.25
|
||||
border_color: '000000'
|
||||
no_border: true
|
||||
cell:
|
||||
size: 8
|
||||
padding: 5
|
||||
border_width: 0.25
|
||||
border_color: '000000'
|
||||
no_border: true
|
||||
valign: middle
|
||||
alerts:
|
||||
NOTE:
|
||||
border_color: '0969da'
|
||||
alert_color: '0969da'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
TIP:
|
||||
border_color: '1a7f37'
|
||||
alert_color: '1a7f37'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
IMPORTANT:
|
||||
border_color: '8250df'
|
||||
alert_color: '8250df'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
WARNING:
|
||||
border_color: 'bf8700'
|
||||
alert_color: 'bf8700'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
CAUTION:
|
||||
border_color: 'd1242f'
|
||||
alert_color: 'd1242f'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
padding: '4mm'
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
attributes_group:
|
||||
size: 11
|
||||
styles: [ "bold" ]
|
||||
margin_top: 0
|
||||
margin_bottom: 4
|
||||
hr:
|
||||
color: "1F2328"
|
||||
height: 0.25
|
||||
attributes_table:
|
||||
cell:
|
||||
size: 10
|
||||
color: "000000"
|
||||
padding_left: 0
|
||||
padding_right: 5
|
||||
padding_top: 0
|
||||
padding_bottom: 8
|
||||
border_color: "4B4B4B"
|
||||
border_width: 0.25
|
||||
no_border: true
|
||||
cell_label:
|
||||
styles: [ "bold" ]
|
||||
size: 10
|
||||
|
||||
wp_table:
|
||||
margins:
|
||||
margin_top: 8
|
||||
group_heading:
|
||||
size: 11
|
||||
styles: [ "bold" ]
|
||||
margin_bottom: 10
|
||||
table:
|
||||
margin_bottom: 5
|
||||
cell:
|
||||
size: 9
|
||||
color: "000000"
|
||||
padding_left: 0
|
||||
padding_right: 5
|
||||
padding_top: 0
|
||||
padding_bottom: 5
|
||||
border_width: 0.25
|
||||
no_border: true
|
||||
cell_header:
|
||||
size: 9
|
||||
styles: [ "bold" ]
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# 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 Projects::Exports::PDFExport::Styles
|
||||
class PDFStyles
|
||||
include MarkdownToPDF::Common
|
||||
include MarkdownToPDF::StyleHelper
|
||||
include Exports::PDF::Common::Styles
|
||||
include Exports::PDF::Components::PageStyles
|
||||
include Exports::PDF::Components::CoverStyles
|
||||
include Exports::PDF::Components::WpTableStyles
|
||||
include WorkPackage::PDFExport::Common::AttributesTableStyles
|
||||
|
||||
def project_title
|
||||
resolve_font(@styles.dig(:project, :title))
|
||||
end
|
||||
|
||||
def project_title_margins
|
||||
resolve_margin(@styles.dig(:project, :title))
|
||||
end
|
||||
|
||||
def project_margins
|
||||
resolve_margin(@styles[:project])
|
||||
end
|
||||
|
||||
def toc_title
|
||||
resolve_font(@styles.dig(:toc, :title))
|
||||
end
|
||||
|
||||
def toc_title_margins
|
||||
resolve_margin(@styles.dig(:toc, :title))
|
||||
end
|
||||
|
||||
def toc_max_depth
|
||||
@styles.dig(:toc, :max_depth) || 4
|
||||
end
|
||||
|
||||
def toc_margins
|
||||
resolve_margin(@styles[:toc])
|
||||
end
|
||||
|
||||
def toc_indent_mode
|
||||
@styles.dig(:toc, :indent_mode)
|
||||
end
|
||||
|
||||
def toc_item(level)
|
||||
resolve_font(@styles.dig(:toc, :item)).merge(
|
||||
resolve_font(@styles.dig(:toc, :"item_level_#{level}"))
|
||||
)
|
||||
end
|
||||
|
||||
def toc_item_subject_indent
|
||||
resolve_pt(@styles.dig(:toc, :subject_indent), 4)
|
||||
end
|
||||
|
||||
def toc_item_margins(level)
|
||||
resolve_margin(@styles.dig(:toc, :item)).merge(
|
||||
resolve_margin(@styles.dig(:toc, :"item_level_#{level}"))
|
||||
)
|
||||
end
|
||||
|
||||
def project_markdown_label
|
||||
resolve_font(@styles.dig(:project, :markdown_label))
|
||||
end
|
||||
|
||||
def project_markdown_label_margins
|
||||
resolve_margin(@styles.dig(:project, :markdown_label))
|
||||
end
|
||||
|
||||
def project_markdown_margins
|
||||
resolve_margin(@styles.dig(:project, :markdown_margins))
|
||||
end
|
||||
|
||||
def project_markdown_styling_yml
|
||||
resolve_markdown_styling(@styles.dig(:project, :markdown) || {})
|
||||
end
|
||||
|
||||
def project_attributes_table_margins
|
||||
resolve_margin(@styles.dig(:project, :attributes_table))
|
||||
end
|
||||
|
||||
def project_attributes_table_cell
|
||||
resolve_table_cell(@styles.dig(:project, :attributes_table, :cell))
|
||||
end
|
||||
|
||||
def project_attributes_table_label
|
||||
resolve_font(@styles.dig(:project, :attributes_table, :cell_label))
|
||||
end
|
||||
|
||||
def project_attributes_table_label_cell
|
||||
project_attributes_table_cell.merge(
|
||||
resolve_table_cell(@styles.dig(:project, :attributes_table, :cell_label)) || {}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def styles
|
||||
@styles ||= PDFStyles.new(styles_asset_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def styles_asset_path
|
||||
File.dirname(File.expand_path(__FILE__))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,179 @@
|
||||
# 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 Projects::Exports::PDFExport
|
||||
module TableOfContent
|
||||
def render_toc(projects, info_map)
|
||||
with_margin(styles.toc_title_margins) do
|
||||
pdf.formatted_text([{ text: I18n.t("label_table_of_contents") }.merge(styles.toc_title)])
|
||||
end
|
||||
toc_list = build_toc_data_list(projects, info_map)
|
||||
with_margin(styles.toc_margins) do
|
||||
write_toc! toc_list
|
||||
end
|
||||
start_new_page_if_needed
|
||||
end
|
||||
|
||||
def build_toc_data_list(projects, info_map)
|
||||
projects.map do |project|
|
||||
build_toc_data_list_entry(project, info_map)
|
||||
end
|
||||
end
|
||||
|
||||
def build_toc_data_list_entry(project, info_map)
|
||||
project_info = info_map[project.id]
|
||||
level_data = calculate_level_data(project_info[:level_path])
|
||||
page_nr_string = (project_info[:page_number] || "000").to_s
|
||||
build_toc_entry(project.id, project.name, level_data, page_nr_string)
|
||||
end
|
||||
|
||||
def calculate_level_data(level_path)
|
||||
level = [level_path.length, styles.toc_max_depth].min
|
||||
level_style = styles.toc_item(level)
|
||||
level_string = "#{level_path.join('.')}. "
|
||||
{
|
||||
level:,
|
||||
level_style:,
|
||||
level_string:,
|
||||
level_string_width: measure_part_width(level_string, level_style)
|
||||
}
|
||||
end
|
||||
|
||||
def build_toc_entry(id, title, level_data, page_nr_string)
|
||||
{
|
||||
id:,
|
||||
level_string: level_data[:level_string],
|
||||
level_string_width: level_data[:level_string_width],
|
||||
title:,
|
||||
page_nr_string:,
|
||||
page_nr_string_width: measure_part_width(page_nr_string, level_data[:level_style]),
|
||||
level: level_data[:level]
|
||||
}
|
||||
end
|
||||
|
||||
def measure_part_width(part, part_style)
|
||||
measure_text_width(part, part_style) + styles.toc_item_subject_indent
|
||||
end
|
||||
|
||||
def write_toc!(toc_list)
|
||||
levels_indent_list = toc_indent_list(toc_list)
|
||||
toc_list.each do |toc_item|
|
||||
with_margin(styles.toc_item_margins(toc_item[:level])) do
|
||||
write_toc_item!(toc_item, levels_indent_list)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def toc_indent_list(toc_list)
|
||||
levels = toc_list.pluck(:level).uniq.sort
|
||||
level_max_widths = levels.map do |level|
|
||||
toc_list.select { |item| item[:level] == level }.pluck(:level_string_width).max
|
||||
end
|
||||
mode = (styles.toc_indent_mode || :flat).to_sym
|
||||
case mode
|
||||
when :stairs
|
||||
toc_indent_list_stairs(levels, level_max_widths)
|
||||
when :third_level
|
||||
toc_indent_list_third_level(levels, level_max_widths)
|
||||
else
|
||||
toc_indent_list_flat(levels, level_max_widths)
|
||||
end
|
||||
end
|
||||
|
||||
def toc_indent_list_flat(levels, level_max_widths)
|
||||
levels_max_width = level_max_widths.max
|
||||
levels.map do |_|
|
||||
{ level_indent: 0, subject_index: levels_max_width }
|
||||
end
|
||||
end
|
||||
|
||||
def toc_indent_list_third_level(levels, level_max_widths)
|
||||
indent_list = []
|
||||
first_section = level_max_widths[0..1].max || 0
|
||||
second_section = level_max_widths[2..].max || 0
|
||||
levels.each do |level|
|
||||
if level < 3
|
||||
indent_list.push({ level_indent: 0, subject_index: first_section })
|
||||
else
|
||||
indent_list.push({ level_indent: first_section, subject_index: first_section + second_section })
|
||||
end
|
||||
end
|
||||
indent_list
|
||||
end
|
||||
|
||||
def toc_indent_list_stairs(levels, level_max_widths)
|
||||
indent_list = []
|
||||
levels.each do |level|
|
||||
level_indent = level <= 1 ? 0 : indent_list.last[:subject_index]
|
||||
subject_index = level_indent + level_max_widths[level - 1]
|
||||
indent_list.push({ level_indent:, subject_index: })
|
||||
end
|
||||
indent_list
|
||||
end
|
||||
|
||||
def build_toc_item_styles(toc_item)
|
||||
toc_item_style = styles.toc_item(toc_item[:level])
|
||||
part_style = toc_item_style.clone
|
||||
font_styles = part_style.delete(:styles) || []
|
||||
part_style[:style] = font_styles[0] unless font_styles.empty?
|
||||
[part_style, toc_item_style]
|
||||
end
|
||||
|
||||
def write_toc_item!(toc_item, levels_indent_list)
|
||||
y_start_position = pdf.y
|
||||
part_style, toc_item_style = build_toc_item_styles(toc_item)
|
||||
indent = levels_indent_list[toc_item[:level] - 1]
|
||||
|
||||
write_toc_part_float(indent[:level_indent], toc_item[:level_string], part_style)
|
||||
write_toc_part_float(0, toc_item[:page_nr_string], part_style.merge({ align: :right }))
|
||||
write_toc_item_subject(toc_item, indent[:subject_index], toc_item_style)
|
||||
write_toc_item_link(toc_item, y_start_position)
|
||||
end
|
||||
|
||||
def write_toc_item_subject(toc_item, indent, subject_style)
|
||||
pdf.indent(indent, toc_item[:page_nr_string_width]) do
|
||||
pdf.formatted_text([subject_style.merge({ text: toc_item[:title] })])
|
||||
end
|
||||
end
|
||||
|
||||
def write_toc_part_float(indent, part, part_style)
|
||||
pdf.float do
|
||||
pdf.indent(indent) do
|
||||
pdf.text(part, part_style)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write_toc_item_link(toc_item, y_start_position)
|
||||
rect = [pdf.bounds.absolute_right, pdf.y, pdf.bounds.absolute_left, y_start_position]
|
||||
pdf.link_annotation(rect, Border: [0, 0, 0], Dest: toc_item[:id].to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -42,13 +42,17 @@ module Projects::Exports
|
||||
end
|
||||
|
||||
def projects
|
||||
@projects ||= query
|
||||
@projects ||= all_projects
|
||||
.page(page)
|
||||
.per_page(Setting.work_packages_projects_export_limit.to_i)
|
||||
end
|
||||
|
||||
def all_projects
|
||||
query
|
||||
.results
|
||||
.with_required_storage
|
||||
.with_latest_activity
|
||||
.includes(:custom_values)
|
||||
.page(page)
|
||||
.per_page(Setting.work_packages_projects_export_limit.to_i)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -36,6 +36,7 @@ module Queries
|
||||
class_methods do
|
||||
def from_hash(hash) # rubocop:disable Metrics/AbcSize
|
||||
new(user: hash[:user]).tap do |query|
|
||||
query.name = hash[:name] if hash[:name].present?
|
||||
query.add_filters hash[:filters] if hash[:filters].present?
|
||||
query.add_orders hash[:orders] if hash[:orders].present?
|
||||
query.group hash[:group_by] if hash[:group_by].present?
|
||||
@@ -50,7 +51,8 @@ module Queries
|
||||
orders: orders.map { |o| [o.attribute, o.direction] },
|
||||
group_by: respond_to?(:group_by) ? group_by : nil,
|
||||
selects: selects.map(&:attribute),
|
||||
user:
|
||||
user:,
|
||||
name:
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ module Projects
|
||||
class ExportJob < ::Exports::ExportJob
|
||||
self.model = Project
|
||||
|
||||
def title
|
||||
I18n.t("export.your_projects_export")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare!
|
||||
|
||||
@@ -50,12 +50,15 @@ Rails.application.configure do |application|
|
||||
formatter WorkPackage, WorkPackage::Exports::Formatters::SpentUnits
|
||||
|
||||
list Project, Projects::Exports::CSV
|
||||
list Project, Projects::Exports::PDF
|
||||
formatter Project, Exports::Formatters::CustomField
|
||||
formatter Project, Exports::Formatters::CustomFieldPdf
|
||||
formatter Project, Projects::Exports::Formatters::Status
|
||||
formatter Project, Projects::Exports::Formatters::Description
|
||||
formatter Project, Projects::Exports::Formatters::Public
|
||||
formatter Project, Projects::Exports::Formatters::Active
|
||||
formatter Project, Projects::Exports::Formatters::Favored
|
||||
formatter Project, Projects::Exports::Formatters::RequiredDiskSpace
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2487,6 +2487,7 @@ en:
|
||||
label: "Include descriptions"
|
||||
caption: "This option will add a description column in raw format."
|
||||
your_work_packages_export: "Work packages are being exported"
|
||||
your_projects_export: "Projects are being exported"
|
||||
succeeded: "Export completed"
|
||||
failed: "An error has occurred while trying to export the work packages: %{message}"
|
||||
errors:
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* -- 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.
|
||||
* ++
|
||||
*/
|
||||
|
||||
import { ApplicationController } from 'stimulus-use';
|
||||
import { renderStreamMessage } from '@hotwired/turbo';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { TurboHelpers } from 'core-turbo/helpers';
|
||||
|
||||
export default class AsyncJobDialogController extends ApplicationController {
|
||||
static values = {
|
||||
closeDialogId: String,
|
||||
};
|
||||
|
||||
declare closeDialogIdValue:string;
|
||||
|
||||
connect() {
|
||||
this.element.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
TurboHelpers.showProgressBar();
|
||||
this.closePreviousDialog();
|
||||
this.requestJob().then((job_id) => {
|
||||
if (job_id) {
|
||||
return this.showJobModal(job_id);
|
||||
}
|
||||
return this.handleError('No job ID returned from server.');
|
||||
})
|
||||
.catch((error:HttpErrorResponse|string) => this.handleError(error))
|
||||
.finally(() => {
|
||||
TurboHelpers.hideProgressBar();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closePreviousDialog() {
|
||||
if (!this.closeDialogIdValue) {
|
||||
return; // No dialog ID specified, nothing to close
|
||||
}
|
||||
const dialog = document.getElementById(this.closeDialogIdValue) as HTMLDialogElement;
|
||||
if (dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
async requestJob():Promise<string> {
|
||||
const response = await fetch(this.href, {
|
||||
method: this.method,
|
||||
headers: { Accept: 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
const result = await response.json() as { job_id:string };
|
||||
if (!result.job_id) {
|
||||
throw new Error('Invalid response from server');
|
||||
}
|
||||
return result.job_id;
|
||||
}
|
||||
|
||||
async showJobModal(job_id:string) {
|
||||
const response = await fetch(`/job_statuses/${job_id}/dialog`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'text/vnd.turbo-stream.html' },
|
||||
});
|
||||
if (response.ok) {
|
||||
renderStreamMessage(await response.text());
|
||||
} else {
|
||||
throw new Error(response.statusText || 'Invalid response from server');
|
||||
}
|
||||
}
|
||||
|
||||
async handleError(error:HttpErrorResponse|string):Promise<void> {
|
||||
void window.OpenProject.getPluginContext().then((pluginContext) => {
|
||||
pluginContext.services.notifications.addError(error);
|
||||
});
|
||||
}
|
||||
|
||||
get href() {
|
||||
return (this.element as HTMLLinkElement).href;
|
||||
}
|
||||
|
||||
get method() {
|
||||
return (this.element as HTMLLinkElement).dataset.jobHrefMethod || 'GET';
|
||||
}
|
||||
}
|
||||
@@ -7,3 +7,5 @@ styles:
|
||||
path: "modules/reporting/app/workers/cost_query/pdf"
|
||||
- name: "meeting"
|
||||
path: "modules/meeting/app/workers/meetings/pdf"
|
||||
- name: "project-report"
|
||||
path: "app/models/projects/exports/pdf_export"
|
||||
|
||||
Reference in New Issue
Block a user