Source code for libreport.pdfreport

#!/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'


[docs]def pheader(txt, style=HeaderStyle, klass=Paragraph, sep=0.3): '''Creates a reportlab PDF element and adds it to the global Elements list :param style: can be a HeaderStyle, a ParaStyle or a custom style (default HeaderStyle) :param klass: the reportlab Class to be called, default Paragraph :param sep: space separator height ''' elements = [] s = Spacer(0.2 * inch, sep * inch) elements.append(s) para = klass(txt, style) elements.append(para) return elements
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 enableFooter(self, state): ''' enable formatter for the last row of the table e.g for summaries have bold border lines :type state: True or False ''' self.hasfooter = state
[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")
[docs] def addHeader(self, canvas, document): ''' display the heading of the page or document ''' canvas.saveState() title = self.getSubTitle(document.page - 1) fontsize = 12 fontname = 'FreeSerif' headerBottom = document.bottomMargin + document.height + \ document.topMargin / 2 bottomLine = headerBottom - fontsize / 4 topLine = headerBottom + fontsize lineLength = document.width + document.leftMargin canvas.setFont(fontname, fontsize) canvas.drawString(document.leftMargin, headerBottom, title) canvas.restoreState()
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)