#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
# maintainer: ukanga
import os
from datetime import datetime
import copy
from django.template import Template, Context
from django.http import HttpResponse
try:
from reportlab.platypus.flowables import Flowable
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import BaseDocTemplate, PageTemplate, \
Paragraph, PageBreak, Frame, FrameBreak, NextPageTemplate, Spacer, \
Preformatted
from reportlab.platypus import Table as PDFTable, Table
from reportlab.platypus import TableStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4, landscape
from reportlab.rl_config import defaultPageSize
from reportlab.lib.units import inch
from ccdoc.utils import register_fonts
PAGE_HEIGHT = defaultPageSize[1]
PAGE_WIDTH = defaultPageSize[0]
except ImportError:
pass
register_fonts()
styles = getSampleStyleSheet()
HeaderStyle = styles["Heading1"]
HeaderStyle.fontName = 'FreeSerif'
ParaStyle = styles["Normal"]
ParaStyle.fontName = 'FreeSerif'
"""Paragraph Style"""
[docs]def p(txt):
'''Create a text Paragraph using ParaStyle'''
return pheader(txt, style=ParaStyle, sep=0.0)
PreStyle = styles["Code"]
"""Preformatted Style"""
[docs]def pre(txt):
'''Create a text Preformatted Paragraph using PreStyle'''
elements = []
s = Spacer(0.1 * inch, 0.1 * inch)
elements.append(s)
p = Preformatted(txt, PreStyle)
elements.append(p)
return elements
[docs]class PDFReport():
'''PDFReport
PDFReport Is a class that create table format reports
The Title is placed on its own page for the first page
usage::
pdfrpt = PDFRrepot()
pdfrpt.setLandscape(False)
pdfrpt.setTitle("Title")
pdfrpt.setTableData(queryset, fields, "Table Title")
pdfrpt.setFilename("filename")
pdfrpt.setNumOfColumns(2) # for two column setup
pdfrpt.render()
'''
title = u"Report"
pageinfo = ""
filename = "report"
styles = getSampleStyleSheet()
table_style = None
data = []
landscape = False
hasfooter = False
headers = []
cols = 1
PAGESIZE = A4
fontSize = 8
rowsperpage = 90
print_on_both_sides = False
firstRowHeight = 0.25
rotateTextFirstRow = False
def __init__(self):
self.headers.append("")
[docs] def setPrintOnBothSides(self, state):
"""
:param state: True or False
"""
self.print_on_both_sides = state
[docs] def setLandscape(self, state):
''' enable or disable landscape display
:param state: True or False
'''
self.landscape = state
[docs] def setRowsPerPage(self, num):
''' Sets the number of rows per page for Table data
:type num: int
'''
self.rowsperpage = int(num)
[docs] def setTitle(self, title):
'''
:param title: The Report Title
'''
if title:
self.title = title
def setPageInfo(self, pageinfo):
if pageinfo:
self.pageinfo = pageinfo
[docs] def setFilename(self, filename):
'''
:param filename: filename for the generated pdf document
'''
if filename:
self.filename = filename
[docs] def setFontSize(self, size):
'''
:param size: font-size
'''
if size:
self.fontSize = size
[docs] def setNumOfColumns(self, cols):
'''
:param cols: number of columns
'''
if cols:
self.cols = cols
[docs] def setPageBreak(self):
''' force/add a page break '''
self.data.append(PageBreak())
[docs] def setElements(self, elements):
''' Add elements like paragraphs to the overall data '''
for i in elements:
self.data.append(i)
def setTableStyle(self, ts):
self.table_style = ts
def getTableStyle(self):
if self.table_style:
return self.table_style
ts = [
('ALIGNMENT', (0, 1), (-1, -1), 'LEFT'),
('LINEBELOW', (0, 0), (-1, -0), 2, colors.black),
('GRID', (0, 0), (-1, -0), 0.25, colors.black),
('LINEBELOW', (0, 1), (-1, -1), 0.8, \
colors.lightgrey),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 2), ('ROWBACKGROUNDS', \
(0, 1), (-1, -1), \
[colors.whitesmoke, colors.white]),
('FONT', (0, 0), (-1, -1), "FreeSerif", self.fontSize)]
#last row formatting when required
if self.hasfooter is True:
ts.append(('LINEABOVE', (0, -1), (-1, -1), 1, colors.black))
ts.append(('LINEBELOW', (0, -1), (-1, -1), 2, colors.black))
ts.append(('LINEBELOW', (0, 3), (-0, -0), 2, colors.green))
ts.append(('LINEBELOW', (0, -1), (-1, -1), 0.8, \
colors.lightgrey))
ts.append(('FONT', (0, -1), (-1, -1), "FreeSerif", 7))
return ts
[docs] def setFirstRowHeight(self, height):
'''Set the height of the first row'''
self.firstRowHeight = height
def getFirstRowHeight(self):
return self.firstRowHeight
[docs] def getRowHeights(self, numOfRows):
'''retuns the row heights'''
rh = [self.getFirstRowHeight() * inch]
numOfRows -= 1
rh.extend(numOfRows * [0.25 * inch])
return rh
def rotateText(self, bl):
self.rotateTextFirstRow = bl
[docs] def setTableData(self, queryset, fields, title, colWidths=None, \
hasCounter=False):
'''set table data
:param queryset: data
:param fields: table column headings
:param title: Table Heading
'''
data = []
header = False
c = 0
pStyle = copy.copy(self.styles["Normal"])
pStyle.fontName = "FreeSerif"
pStyle.fontSize = 7
pStyle.spaceBefore = 0
pStyle.spaceAfter = 0
pStyle.leading = pStyle.fontSize + 2.8
hStyle = copy.copy(self.styles["Normal"])
hStyle.fontName = 'FreeSerif'
#hStyle.fontName = "Arial Narrow-Bold"
hStyle.fontSize = 8
hStyle.alignment = TA_CENTER
#hStyle.spaceBefore = 0
hStyle.spaceAfter = 0
#pStyle.leading = pStyle.fontSize + 2.8
#prepare the data
counter = 0
for row in queryset:
counter += 1
if not header:
if self.rotateTextFirstRow:
hStyle.borderWidth = 0
hStyle.alignment = TA_LEFT
hStyle.borderPadding = 5
value = [RotatedParagraph(Paragraph('<b>' + f["name"] + \
'</b>', hStyle), \
self.getFirstRowHeight() * inch, \
0.25 * inch) \
for f in fields]
else:
value = [pheader(f["name"], hStyle, sep=0) for f in fields]
if hasCounter:
value.insert(0, '#')
data.append(value)
header = True
ctx = Context({"object": row})
values = [pheader(Template(h["bit"]).render(ctx), pStyle, sep=0)\
for h in fields]
if hasCounter:
values.insert(0, counter)
data.append(values)
if len(data):
ts = self.getTableStyle()
table = PDFTable(data, colWidths, self.getRowHeights(len(data)), \
style=ts, splitByRow=1)
table.hAlign = "LEFT"
self.data.append(table)
else:
self.data.append(Paragraph("No Report", self.styles['Normal']))
'''The number of rows per page for two columns is about 90.
using this information you can figure how many pages the
table is going to overlap hence you place a header/subtitle
in that position for it to be printed appropriately
'''
c = float(len(queryset)) / self.rowsperpage
needsSecondPage = True
if int(c) < c:
c = int(c) + 1
needsSecondPage = False
if int(c) == 0:
c = 1
if self.print_on_both_sides is True:
if int(c) == 1 or (int(c) % 2) == 1:
#take care of headings, do not displace them
c = int(c) + 1
needsSecondPage = True
self.data.append(PageBreak())
for i in range(int(c)):
if self.print_on_both_sides is True and needsSecondPage is True \
and i == (int(c) - 1):
#empty title to allow blank page
title = ""
self.headers.append(title)
#exit((needsSecondPage, c, self.headers, title, i))
def render(self):
filename = self.filename + \
datetime.now().strftime("%Y%m%d%H%M%S") + ".pdf"
response = HttpResponse(mimetype='application/pdf')
response['Cache-Control'] = ""
response['Content-Disposition'] = "attachment; filename=%s" % filename
elements = []
self.styles['Title'].alignment = TA_LEFT
self.styles['Title'].fontName = self.styles['Heading2'].fontName = \
"FreeSerif"
self.styles["Normal"].fontName = "FreeSerif"
self.styles["Normal"].fontSize = 7
#self.styles["Normal"].fontWeight = "BOLD"
#doc = SimpleDocTemplate(filename)
#now create the title page
elements.append(Paragraph(self.title, self.styles['Title']))
if self.print_on_both_sides is True:
elements.append(PageBreak())
self.headers.insert(1, "")
#done with the title info, move to the next frame
#and queue up the later page template
elements.append(FrameBreak())
elements.append(NextPageTemplate("laterPages"))
elements.append(PageBreak())
#exit(self.headers)
for data in self.data:
elements.append(data)
if self.landscape is True:
self.PAGESIZE = landscape(A4)
doc = MultiColDocTemplate(response, self.cols, \
pagesize=self.PAGESIZE, allowSplitting=1, \
showBoundary=0)
doc.setTitle(self.title)
doc.setHeaders(self.headers)
doc.build(elements)
#response.write(open(filename).read())
#os.remove(filename)
return response
# what should appear on the first page
def myFirstPage(self, canvas, doc):
pageinfo = self.pageinfo
canvas.saveState()
'''canvas.setFont('Times-Roman',9)
canvas.drawString(inch, 0.75 * inch, "Page %d %s" %
(doc.page, pageinfo))
'''
textobject = canvas.beginText()
textobject.setTextOrigin(inch, 0.75 * inch)
textobject.setFont("FreeSerif", 9)
textobject.textLine("Page %d" % (doc.page))
textobject.setFillGray(0.4)
textobject.textLines(pageinfo)
canvas.hAlign = "CENTER"
canvas.drawText(textobject)
canvas.restoreState()
# what to do on other pages
def myLaterPages(self, canvas, doc):
pageinfo = self.pageinfo
canvas.saveState()
'''canvas.setFont('Times-Roman',9)
canvas.drawString(inch, 0.75 * inch, "Page %d %s"
% (doc.page, pageinfo))
'''
textobject = canvas.beginText()
textobject.setTextOrigin(inch, 0.75 * inch)
textobject.setFont("FreeSerif", 9)
textobject.textLine("Page %d" % (doc.page))
textobject.setFillGray(0.4)
textobject.textLines(pageinfo)
canvas.hAlign = "CENTER"
[docs]class MultiColDocTemplate(BaseDocTemplate):
'''A multi column document template'''
headers = []
title = u"Report Title Here"
def __init__(self, filename, frameCount=1, **kw):
'''@FIXME: need to remove frameCount to maintain consistency
with BaseDocTemplate constructor
and hence find a way to pass frameCount
'''
apply(BaseDocTemplate.__init__, (self, filename), kw)
self.addPageTemplates(self.firstPage())
frameWidth = (self.width / frameCount) + .85 * inch
frameHeight = self.height - .5 * inch
frames = []
for frame in range(frameCount):
leftMargin = self.leftMargin + frame * frameWidth - .85 * inch
column = Frame(leftMargin, self.bottomMargin - .95 * inch, \
frameWidth, frameHeight + 1.75 * inch, leftPadding=1, \
topPadding=1, rightPadding=1, bottomPadding=1)
frames.append(column)
template = PageTemplate(frames=frames, id="laterPages", \
onPage=self.addHeader)
self.addPageTemplates(template)
def firstPage(self):
style = getSampleStyleSheet()
#title page styles
titleStyle = style["Title"]
titleStyle.fontSize = 40
titleStyle.leading = titleStyle.fontSize * 1.1
#need to copy the object or style changes will
# apply to any incarnation of "Normal"
subTitleStyle = copy.copy(style["Normal"])
subTitleStyle.alignment = TA_CENTER
subTitleStyle.fontName = "FreeSerif"
frameHeight = self.height - .5 * inch
frameWidth = self.width
#title page frames
firstPageHeight = 3.5 * inch
firstPageBottom = frameHeight - firstPageHeight
framesFirstPage = []
titleFrame = Frame(self.leftMargin, firstPageBottom, self.width, \
firstPageHeight)
framesFirstPage.append(titleFrame)
#columns for the first page
firstPageColumn = Frame(self.leftMargin, self.bottomMargin, \
frameWidth, firstPageBottom)
framesFirstPage.append(firstPageColumn)
return PageTemplate(frames=framesFirstPage, id="firstPage")
def getTitle(self):
return u"%s" % self.title
def setTitle(self, title):
if title:
self.title = title
def getSubTitle(self, pos):
try:
''' since subtitles vary from page to page, I pick the relevant
title according to the page number '''
return u"%s" % self.headers[pos]
except:
return u""
def setHeaders(self, headers):
self.headers = headers
[docs]class RotatedText(Flowable):
'''Rotates text in a table cell.'''
def __init__(self, text, *args, **kwargs):
Flowable.__init__(self)
self.text = text
self.args = args
self.kwargs = kwargs
def draw(self):
canv = self.canv
canv.rotate(90)
canv.drawString(0, -1, self.text, *self.args, **self.kwargs)
def wrap(self, aW, aH):
canv = self.canv
return canv._leading, canv.stringWidth(self.text, *self.args, **self.kwargs)
[docs]class RotatedParagraph(Flowable):
'''Rotates a paragraph'''
def __init__(self, paragraph, aW, aH):
self.paragraph = paragraph
self.width = aW
self.height = aH
def draw(self):
canv = self.canv
canv.rotate(90)
self.paragraph.wrap(self.width, self.height)
#drawOn(canvas, x, y)
self.paragraph.drawOn(canv, -(self.width / 2) + 15, -(self.height))
[docs]class ScaledTable(Table):
'''Scales a Table'''
def __init__(self, *args, **kwargs):
Table.__init__(self, *args, **kwargs)
self.xscale = 0.8
self.yscale = 0.8
def scale(self, x, y):
self.xscale = x
self.yscale = y
def draw(self):
canv = self.canv
canv.scale(self.xscale, self.yscale)
canv.translate(1 * inch, (self._height - (self._height * self.yscale)))
Table.draw(self)