Implement Markup rendering for PDFs, and use that for order terms
authorTero Marttila <terom@fixme.fi>
Fri, 07 Jan 2011 01:23:24 +0200
changeset 31 e1b63e4d10f4
parent 30 97d5d37333d2
child 32 10c48a6843ad
Implement Markup rendering for PDFs, and use that for order terms
README
svv/controllers.py
svv/orders.py
svv/pdf.py
--- a/README	Fri Jan 07 01:22:52 2011 +0200
+++ b/README	Fri Jan 07 01:23:24 2011 +0200
@@ -7,4 +7,5 @@
     * python-werkzeug (tested 0.6)
     * python-sqlalchemy (tested 0.5)
     * python-reportlab (tested 2.4)
+    * python-markdown (tested 2.0.3 - may have compatibility issues)
 
--- a/svv/controllers.py	Fri Jan 07 01:22:52 2011 +0200
+++ b/svv/controllers.py	Fri Jan 07 01:23:24 2011 +0200
@@ -194,6 +194,9 @@
         """
             Generate the document, and return it as a .pdf file, with the filename generated from the document's title.
         """
+
+        # XXX: proper support
+        self.process(**url_values)
         
         pdf_file = self.generate(**url_values)
 
--- a/svv/orders.py	Fri Jan 07 01:22:52 2011 +0200
+++ b/svv/orders.py	Fri Jan 07 01:23:24 2011 +0200
@@ -6,7 +6,7 @@
 from svv.controllers import PageHandler, DocumentHandler
 from svv.html import tags
 from svv import database as db
-from svv import pdf
+from svv import pdf, markup
 
 import datetime
 import logging
@@ -1220,24 +1220,27 @@
 
         return form
 
-    def load_order (self, id):
+    def process (self, id):
         """
             Return OrderModel object for given ID
         """
 
-        # XXX: really need that OrderModel object :)
-        form = OrderForm(self.app)
-        form.load(id)
+        # db session
+        self.session = self.app.session()
+        
+        # order object
+        self.order = self.session.query(Order).options(db.eagerload(Order.customer), db.eagerload(Order.contact)).get(id)
 
-        return form
+        # form params
+        self.params = self.load_params(self.POST)
     
     def generate_document (self, id) :
+        """
+            Return PDF document to generate
+        """
 
-        # retrieve from db
-        order = self.load_order(id)
-
-        # params set by form
-        params = self.load_params(self.POST)
+        order = self.order
+        params = self.params
 
         title = "Teekkarispeksi Ry - Vuokrasopimus"
         author = "Teekkarispeksi Ry"
@@ -1247,7 +1250,7 @@
                 ("", ""),
                 ("", ""),
                 ("", ""),
-                ("Vuokrasopimus", [order.customer_name, order.event_name, 'dd.mm.yy hh:mm-hh:mm']),
+                ("Vuokrasopimus", [order.customer.name, order.event_name, 'dd.mm.yy hh:mm-hh:mm']),
             ),
             footer_columns  = (
                 ("Teekkarispeksi Ry", ("www.teekkarispeksi.fi", )),
@@ -1262,30 +1265,60 @@
         )
 
         # stylesheet
-        styles = pdf.Styles
+        styles = pdf.Styles()
 
         from reportlab.platypus import Paragraph as p
+        
 
-        # contract terms
-        list_seq = pdf.ListItem.seq
-        terms = pdf.ListItem("Sopimusehdot", styles.h2, None, list_seq(), [
-            pdf.ListItem("Osapuolet", styles.list_h2, None, list_seq(), [
-                pdf.ListItem(None, None, "Teekkarispeksi ry (Y-tunnus 1888541-7), jäljempänä “Vuokranantaja”."),
-                pdf.ListItem(None, None, order.customer_name + u", jäljempänä “Vuokraaja”. 1.1 ja 1.2 jäljempänä yhdessä “osapuolet”.")
-            ]),
-            pdf.ListItem("Kaluston lainaaminen", styles.list_h2, None, list_seq(), [
-                pdf.ListItem("Yleistä", styles.list_h3, "Tässä sopimuksessa sovitaan toiminnasta Vuokranantajan lainatessa tanssimattoja Vuokraajalle"),
-                pdf.ListItem("Vuokranantajan velvollisuudet", styles.list_h3, "Vuokranantaja sitoutuu toimittamaan sovittuna ajankohtana Vuokraajalle erikseen sovittava (liite) määrä tanssimattoja."),
-                pdf.ListItem("Blaa Blaa", styles.list_h3, "Etc."),
-            ]),
-            pdf.ListItem("Tätä sopimusta ja sitä koskevia erimielisyyksiä koskevat määräykset", styles.list_h2, None, list_seq(), [
-                pdf.ListItem("Sopimuksen voimassaolo", styles.list_h3, "Sopimus on osapuolia sitova sen jälkeen, kun osapuolet ovat sen allekirjoittaneet."),
-                pdf.ListItem("Muutosten tekeminen", styles.list_h3, "Muutokset sopimukseen on tehtävä kirjallisesti molempien osapuolten kesken."),
-                pdf.ListItem("Blaa Blaa", styles.list_h3, "Etc."),
-            ]),
-        ])
+        text = u"""\
+# Vuokrasopimus
+### Teekkarispeksi ry AV-tekniikka
 
+## Sopimusehdot
 
+1.  ## Osapuolet
+    1.  Teekkarispeksi ry (Y-tunnus 1888541-7), jäljempänä “Vuokranantaja”.
+    2.  {order.customer.name}, jäljempänä “Vuokraaja”. 1.1 ja 1.2 jäljempänä yhdessä “osapuolet”.
+
+2.  ## Kaluston lainaaminen
+    1.  ### Yleistä
+        Tässä sopimuksessa sovitaan toiminnasta Vuokranantajan lainatessa tanssimattoja Vuokraajalle.
+
+    2.  ### Vuokranantajan velvollisuudet
+        Vuokranantaja sitoutuu toimittamaan sovittuna ajankohtana Vuokraajalle erikseen sovittava (liite) määrä tanssimattoja.
+
+    3.  ### Blaa Blaa
+        Etc.
+
+3. ## Tätä sopimusta ja sitä koskevia erimielisyyksiä koskevat määräykset
+    1.  ### Sopimuksen voimassaolo
+        Sopimus on osapuolia sitova sen jälkeen, kun osapuolet ovat sen allekirjoittaneet.
+
+    2.  ### Muutosten tekeminen
+        Muutokset sopimukseen on tehtävä kirjallisesti molempien osapuolten kesken.
+
+    3.  ### Blaa Blaa
+
+        Etc.
+
+## Nouto
+    Aika: _______________       Paikka: _______________
+
+## Palautus
+    Aika: _______________       Paikka: _______________
+
+        """
+
+        # format
+        text = text.format(
+            order       = order,
+        )
+        
+        # parse to doc tree
+        root = markup.Markup().parse(text)
+
+        # format to flowables
+        text_elements = list(pdf.Markup(styles).render(root))
 
         sig_prefill = {}
 
@@ -1295,16 +1328,9 @@
         if params.prefill_ourname :
             sig_prefill[('Vuokranantaja', 'Nimen selvennys')] = params.prefill_ourname
 
-        elements = [
-                p("Vuokrasopimus", styles.h1),
-                p("Teekkarispeksi ry AV-tekniikka", styles.h3),
-        ] + list(terms.render_pdf()) + [
-                p("Nouto", styles.h2),
-                p("\t\tAika: _______________\tPaikka: _______________", styles.text),
-                p("Palautus", styles.h2),
-                p("\t\tAika: _______________\tPaikka: _______________", styles.text),
-                
+        elements = text_elements + [
                 pdf.SignatureBlock(("Vuokranantaja", "Vuokraaja"), ("%(column)s", "Nimen selvennys", "Aika ja paikka"), sig_prefill),
+
         ]
 
         # ok
--- a/svv/pdf.py	Fri Jan 07 01:22:52 2011 +0200
+++ b/svv/pdf.py	Fri Jan 07 01:23:24 2011 +0200
@@ -23,7 +23,15 @@
 
     samplestyles = styles.getSampleStyleSheet()
 
+    # default
+    default = styles.ParagraphStyle('Text', samplestyles['Normal'],
+
+    )
+    
     # normal text
+    text = p = default
+
+    # headers
     h1 = styles.ParagraphStyle('Heading1', samplestyles['h1'],
             fontName        = 'Times-Bold',
             fontSize        = 22,
@@ -46,9 +54,6 @@
             spaceAfter      = 0,
     )
 
-    text = styles.ParagraphStyle('Text', samplestyles['Normal'],
-
-    )
 
     # list indent level
     list_indent = inch / 4
@@ -82,86 +87,55 @@
     )
     
     # infot
-    list_text = styles.ParagraphStyle('ListText', samplestyles['Normal'],
+    list_p = styles.ParagraphStyle('ListText', samplestyles['Normal'],
             bulletIndent    = 0,
             leftIndent      = list_indent,
     )
-
-class ListItem (object) :
-    """
-        Indented/nested list
-    """
+    list_text = list_p
 
+    # lookup styles by rule
+    # searched in order, first match against end of node_path wins
+    RULES = (
+        ('li p',        list_p),
+        ('li h1',       list_h1),
+        ('li h2',       list_h2),
+        ('li h3',       list_h3),
+
+        ('p',           p),
+        ('h1',          h1),
+        ('h2',          h2),
+        ('h3',          h3),
+
+    )
+    
     @classmethod
-    def seq (cls) :
+    def match (cls, name=None) :
         """
-            List numbering.
-
-            Fixed as numeric only for now
+            Return appropriate style.
         """
         
-        for idx in itertools.count(1) :
-            yield "%d." % (idx, )
+        if name :
+            return getattr(cls, name, cls.default)
 
+        else :
+            return cls.default
 
-    def __init__ (self, title, title_style, text, subseq=None, sublist=None, 
-            text_style=Styles.list_text, indent=Styles.list_indent) :
+    def lookup (self, node_path) :
         """
-                title       - title to display as first line
-                title_style - paragraph style for title line
-                text        - multi-line texto to display on first or previous lines
-                subseq      - sequence of bullet-texts to use for items in sublist
-                sublist     - sequence of sub-nodes
-
-                text_style  - paragraph style for text lines
-                indent      - indentation for text and sub-lists
+            Return a suitable ParagraphStyle to use for a node
+                
+                node_path   - tag-path to node (list of tags as ancestry of node)
         """
 
-        self.title = title
-        self.title_style = title_style
-        self.text = text
-
-        self.subseq = subseq
-        self.sublist = sublist
-
-        self.text_style = text_style
-        self.indent = indent
-
-    def render_pdf (self, bullet=None) :
-        """
-            Yield a series of PDF flowables for this list node and sub-nodes, useable for pdf.DocTemplateBase.build()
-
-                bullet      - bullet text to use for this item's paragraph
-        """
-
-        # first line, with possible bullet
-        if self.title :
-            yield rlpp.Paragraph(self.title, self.title_style, bullet)
+        # XXX: find a better hack
+        nodepath = ' '.join(node_path)
 
-        elif self.text :
-            yield rlpp.Paragraph(self.text, self.text_style, bullet)
-
-        # indented text after title
-        if self.title and self.text :
-            yield rlpp.Paragraph(self.text, self.text_style)
-
-        if self.sublist :
-            # following lines, indented
-            yield rlpp.Indenter(self.indent)
+        for rule, style in self.RULES :
+            if nodepath.endswith(rule) :
+                return style
 
-            # sub-items
-            for item in self.sublist :
-                # get (optional) bullet for item
-                bullet = next(self.subseq, None) if self.subseq else None
-
-                # render item as series of elements
-                for element in item.render_pdf(bullet) :
-                    yield element
-
-            # de-dent
-            yield rlpp.Indenter(-self.indent)
-
-    
+        # default
+        return self.default
 
 class SignatureBlock (rlpp.Flowable) :
     """
@@ -453,3 +427,171 @@
         # binary data out
         return buf.getvalue()
 
+class Markup (object) :
+    """
+        Generate Paragraphs from Markup docs.
+    """
+    
+    def __init__ (self, styles) :
+        """
+            Initialize render state.
+                
+                styles          - Style instance to use
+
+            This class is stateful and single-use.
+
+        """
+        
+        # elment stack
+        self.stack = []
+
+        # styles
+        self.styles = styles
+
+    def lookup_style (self, elem=None) :
+        """
+            Lookup style for given element, which must be at the top of the stack.
+        """
+        
+        # path to element
+        path = self.stack
+
+        if elem is not None and path[-1] is not elem :
+            # XXX: sub-elem?
+            path = path + [elem]
+
+        # lookup
+        return self.styles.lookup([elem.tag for elem in path])
+
+    ## Basic text
+    def render_p (self, elem, bullet=None) :
+        """
+            Normal paragraph.
+
+            XXX: inline markup?
+        """
+
+        style = self.lookup_style(elem)
+
+        yield rlpp.Paragraph(elem.text, style, bullet)
+    
+    def _render_hx (self, elem, bullet=None) :
+        """
+            Render given h1/h2/h3 element
+        """
+        
+        # get style for <hX> tag
+        style = self.lookup_style(elem)
+        
+        # styled text
+        yield rlpp.Paragraph(elem.text, style, bullet)
+
+    render_h1 = render_h2 = render_h3 = _render_hx
+    
+    ## Lists
+    def list_ol_seq (self) :
+        """
+            Numeric ordered list numbering.
+        """
+        
+        for idx in itertools.count(1) :
+            yield "%d." % (idx, )
+    
+    def list_ul_seq (self) :
+        """
+            Bulleted unordered list numbering.
+        """
+
+        while True :
+            yield "&bull;"
+
+    def _render_list (self, elem, seq, bullet=None) :
+        """
+            Render <ul>/<ol> containing <li>s
+        """
+
+        indent = self.styles.list_indent
+
+        # following lines, indented
+        yield rlpp.Indenter(indent)
+
+        # children
+        for sub in elem :
+            # get (optional) bullet for item
+            bullet = next(seq, None) if seq else None
+
+            # sub-render
+            for ff in self.render(sub, bullet) :
+                yield ff
+
+        # de-dent
+        yield rlpp.Indenter(-indent)
+
+    def render_ul (self, elem, bullet=None) : return self._render_list(elem, self.list_ul_seq(), bullet)
+    def render_ol (self, elem, bullet=None) : return self._render_list(elem, self.list_ol_seq(), bullet)
+ 
+    def render_li (self, elem, bullet) :
+        """
+            Render <li>, with given bullet on first line of text
+        """
+
+        children = list(elem)
+        
+        # render bullet on first item
+        if elem.text :
+            # style for text pseudo-element
+            style = self.lookup_style(elem.makeelement('text', {}))
+
+            # direct text, no sub-p or such
+            yield rlpp.Paragraph(elem.text, style, bullet)
+        
+        elif children :
+            sub = children.pop(0)
+
+            # render first sub with bullet
+            for ff in self.render(sub, bullet) :
+                yield ff
+
+        # render remaining elements
+        for sub in children :
+            for ff in self.render(sub) :
+                yield ff
+ 
+    ## Root element
+    def render_root (self, elem, bullet) :
+        """
+            Render the given root element's children, ignoring the root element itself.
+        """
+
+        for sub in elem :
+            for ff in self.render(sub) :
+                yield ff
+           
+    ## Top-level element render dispatch
+    def render (self, elem, bullet=None) :
+        """
+            Yield a series of Flowables for the given element and its children.
+
+                elem            - Element to (recursively) render
+                bullet          - optional bullet text to use for this element
+        """
+
+        log.debug("%r, bullet=%r", elem, bullet)
+
+        try :
+            # lookup
+            func = getattr(self, 'render_%s' % elem.tag)
+
+        except AttributeError, ex :
+            raise Exception("Unhandled tag %r" % elem)
+
+        # enter element
+        self.stack.append(elem)
+
+        # dispatch
+        for ff in func(elem, bullet) :
+            yield ff
+
+        # exit element
+        assert self.stack.pop(-1) is elem
+