Files
Redmine/lib/redmine/export/pdf/issues_pdf_helper.rb
Jean-Philippe Lang ed50d42210 Replace Date.today with User.current.today (#22320).
Depending on the offset between a user's configured timezone and the server
timezone, Date.today may be more or less often wrong from the user's
perspective, leading to things like issues marked as overdue too early or too
late, or yesterday / tomorrow being displayed / selected where 'today' is
intended.

A test case illustrating the problem with Issue#overdue? is included

Patch by Jens Kraemer.

git-svn-id: http://svn.redmine.org/redmine/trunk@15379 e93f8b46-1217-0410-a6f0-8f06a7374b81
2016-05-07 10:42:22 +00:00

532 lines
21 KiB
Ruby

# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2016 Jean-Philippe Lang
#
# 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.
module Redmine
module Export
module PDF
module IssuesPdfHelper
# 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
while left.size < rows
left << nil
end
while right.size < rows
right << nil
end
half = (issue.visible_custom_field_values.size / 2.0).ceil
issue.visible_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")
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) {|other|
text = ""
if Setting.cross_project_issue_relations?
text += "#{relation.other_issue(issue).project} - "
end
text += "#{other.tracker} ##{other.id}: #{other.subject}"
text
}
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
for changeset in issue.changesets
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
for attachment in issue.attachments
pdf.SetFontStyle('',8)
pdf.RDMCell(80,5, attachment.filename)
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
# Returns a PDF string of a list of issues
def issues_to_pdf(issues, project, query)
pdf = ITCPDF.new(current_language, "L")
title = query.new_record? ? l(:label_issue_plural) : query.name
title = "#{project} - #{title}" if project
pdf.set_title(title)
pdf.alias_nb_pages
pdf.footer_date = format_date(User.current.today)
pdf.set_auto_page_break(false)
pdf.add_page("L")
# Landscape A4 = 210 x 297 mm
page_height = pdf.get_page_height # 210
page_width = pdf.get_page_width # 297
left_margin = pdf.get_original_margins['left'] # 10
right_margin = pdf.get_original_margins['right'] # 10
bottom_margin = pdf.get_footer_margin
row_height = 4
# column widths
table_width = page_width - right_margin - left_margin
col_width = []
unless query.inline_columns.empty?
col_width = calc_col_width(issues, query, table_width, pdf)
table_width = col_width.inject(0, :+)
end
# use full width if the description is displayed
if table_width > 0 && query.has_column?(:description)
col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width}
table_width = col_width.inject(0, :+)
end
# title
pdf.SetFontStyle('B',11)
pdf.RDMCell(190, 8, title)
pdf.ln
# totals
totals = query.totals.map {|column, total| "#{column.caption}: #{total}"}
if totals.present?
pdf.SetFontStyle('B',10)
pdf.RDMCell(table_width, 6, totals.join(" "), 0, 1, 'R')
end
totals_by_group = query.totals_by_group
render_table_header(pdf, query, col_width, row_height, table_width)
previous_group = false
issue_list(issues) do |issue, level|
if query.grouped? &&
(group = query.group_by_column.value(issue)) != previous_group
pdf.SetFontStyle('B',10)
group_label = group.blank? ? 'None' : group.to_s.dup
group_label << " (#{query.issue_count_by_group[group]})"
pdf.bookmark group_label, 0, -1
pdf.RDMCell(table_width, row_height * 2, group_label, 'LR', 1, 'L')
pdf.SetFontStyle('',8)
totals = totals_by_group.map {|column, total| "#{column.caption}: #{total[group]}"}.join(" ")
if totals.present?
pdf.RDMCell(table_width, row_height, totals, 'LR', 1, 'L')
end
previous_group = group
end
# fetch row values
col_values = fetch_row_values(issue, query, level)
# make new page if it doesn't fit on the current one
base_y = pdf.get_y
max_height = get_issues_to_pdf_write_cells(pdf, col_values, col_width)
space_left = page_height - base_y - bottom_margin
if max_height > space_left
pdf.add_page("L")
render_table_header(pdf, query, col_width, row_height, table_width)
base_y = pdf.get_y
end
# write the cells on page
issues_to_pdf_write_cells(pdf, col_values, col_width, max_height)
pdf.set_y(base_y + max_height)
if query.has_column?(:description) && issue.description?
pdf.set_x(10)
pdf.set_auto_page_break(true, bottom_margin)
pdf.RDMwriteHTMLCell(0, 5, 10, '', issue.description.to_s, issue.attachments, "LRBT")
pdf.set_auto_page_break(false)
end
end
if issues.size == Setting.issues_export_limit.to_i
pdf.SetFontStyle('B',10)
pdf.RDMCell(0, row_height, '...')
end
pdf.output
end
def is_cjk?
case current_language.to_s.downcase
when 'ja', 'zh-tw', 'zh', 'ko'
true
else
false
end
end
# fetch row values
def fetch_row_values(issue, query, level)
query.inline_columns.collect do |column|
s = if column.is_a?(QueryCustomFieldColumn)
cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
show_value(cv, false)
else
value = issue.send(column.name)
if column.name == :subject
value = " " * level + value
end
if value.is_a?(Date)
format_date(value)
elsif value.is_a?(Time)
format_time(value)
else
value
end
end
s.to_s
end
end
# calculate columns width
def calc_col_width(issues, query, table_width, pdf)
# calculate statistics
# by captions
pdf.SetFontStyle('B',8)
margins = pdf.get_margins
col_padding = margins['cell']
col_width_min = query.inline_columns.map {|v| pdf.get_string_width(v.caption) + col_padding}
col_width_max = Array.new(col_width_min)
col_width_avg = Array.new(col_width_min)
col_min = pdf.get_string_width('OO') + col_padding * 2
if table_width > col_min * col_width_avg.length
table_width -= col_min * col_width_avg.length
else
col_min = pdf.get_string_width('O') + col_padding * 2
if table_width > col_min * col_width_avg.length
table_width -= col_min * col_width_avg.length
else
ratio = table_width / col_width_avg.inject(0, :+)
return col_width = col_width_avg.map {|w| w * ratio}
end
end
word_width_max = query.inline_columns.map {|c|
n = 10
c.caption.split.each {|w|
x = pdf.get_string_width(w) + col_padding
n = x if n < x
}
n
}
# by properties of issues
pdf.SetFontStyle('',8)
k = 1
issue_list(issues) {|issue, level|
k += 1
values = fetch_row_values(issue, query, level)
values.each_with_index {|v,i|
n = pdf.get_string_width(v) + col_padding * 2
col_width_max[i] = n if col_width_max[i] < n
col_width_min[i] = n if col_width_min[i] > n
col_width_avg[i] += n
v.split.each {|w|
x = pdf.get_string_width(w) + col_padding
word_width_max[i] = x if word_width_max[i] < x
}
}
}
col_width_avg.map! {|x| x / k}
# calculate columns width
ratio = table_width / col_width_avg.inject(0, :+)
col_width = col_width_avg.map {|w| w * ratio}
# correct max word width if too many columns
ratio = table_width / word_width_max.inject(0, :+)
word_width_max.map! {|v| v * ratio} if ratio < 1
# correct and lock width of some columns
done = 1
col_fix = []
col_width.each_with_index do |w,i|
if w > col_width_max[i]
col_width[i] = col_width_max[i]
col_fix[i] = 1
done = 0
elsif w < word_width_max[i]
col_width[i] = word_width_max[i]
col_fix[i] = 1
done = 0
else
col_fix[i] = 0
end
end
# iterate while need to correct and lock coluns width
while done == 0
# calculate free & locked columns width
done = 1
ratio = table_width / col_width.inject(0, :+)
# correct columns width
col_width.each_with_index do |w,i|
if col_fix[i] == 0
col_width[i] = w * ratio
# check if column width less then max word width
if col_width[i] < word_width_max[i]
col_width[i] = word_width_max[i]
col_fix[i] = 1
done = 0
elsif col_width[i] > col_width_max[i]
col_width[i] = col_width_max[i]
col_fix[i] = 1
done = 0
end
end
end
end
ratio = table_width / col_width.inject(0, :+)
col_width.map! {|v| v * ratio + col_min}
col_width
end
def render_table_header(pdf, query, col_width, row_height, table_width)
# headers
pdf.SetFontStyle('B',8)
pdf.set_fill_color(230, 230, 230)
base_x = pdf.get_x
base_y = pdf.get_y
max_height = get_issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, true)
# write the cells on page
issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, max_height, true)
pdf.set_xy(base_x, base_y + max_height)
# rows
pdf.SetFontStyle('',8)
pdf.set_fill_color(255, 255, 255)
end
# returns the maximum height of MultiCells
def get_issues_to_pdf_write_cells(pdf, col_values, col_widths, head=false)
heights = []
col_values.each_with_index do |column, i|
heights << pdf.get_string_height(col_widths[i], head ? column.caption : column)
end
return heights.max
end
# Renders MultiCells and returns the maximum height used
def issues_to_pdf_write_cells(pdf, col_values, col_widths, row_height, head=false)
col_values.each_with_index do |column, i|
pdf.RDMMultiCell(col_widths[i], row_height, head ? column.caption : column.strip, 1, '', 1, 0)
end
end
# Draw lines to close the row (MultiCell border drawing in not uniform)
#
# parameter "col_id_width" is not used. it is kept for compatibility.
def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y,
col_id_width, col_widths, rtl=false)
col_x = top_x
pdf.line(col_x, top_y, col_x, lower_y) # id right border
col_widths.each do |width|
if rtl
col_x -= width
else
col_x += width
end
pdf.line(col_x, top_y, col_x, lower_y) # columns right border
end
pdf.line(top_x, top_y, top_x, lower_y) # left border
pdf.line(top_x, lower_y, col_x, lower_y) # bottom border
end
end
end
end
end