Implement OrderContractDocument
authorTero Marttila <terom@fixme.fi>
Thu, 23 Dec 2010 19:56:44 +0200
changeset 15 e098ee83b363
parent 14 5b2cc88412f7
child 16 46b7e358a5f4
Implement OrderContractDocument
static/js/forms.js
svv/controllers.py
svv/orders.py
svv/pdf.py
svv/urls.py
--- a/static/js/forms.js	Thu Dec 23 18:20:26 2010 +0200
+++ b/static/js/forms.js	Thu Dec 23 19:56:44 2010 +0200
@@ -26,6 +26,30 @@
             this.removeAttr("disabled");
     }
 
+    $.fn.checked = function (flag) {
+        if (flag == undefined)
+            // XXX: jQuery returns true here?
+            return !!this.attr("checked");
+
+
+        if (flag)
+            this.attr("checked", "checked");
+        else
+            this.removeAttr("checked");
+    }
+
+    /*
+     * The given checkbox acts as an enable/disable toggle for this form control
+     */
+    $.fn.formEnabledBy = function (checkbox) {
+        var target = this;
+
+        checkbox.change(function () {
+            target.disabled(!checkbox.checked());
+        });
+        checkbox.change();
+    }
+
     /*
      * When non-zero <select>/<option> is selected, apply that option as pre-filled values for other form items
      */
--- a/svv/controllers.py	Thu Dec 23 18:20:26 2010 +0200
+++ b/svv/controllers.py	Thu Dec 23 19:56:44 2010 +0200
@@ -185,80 +185,45 @@
         # default to ignore
         pass
 
-### XXX: random test controllers
-
-class Document (AppHandler) :
+class DocumentHandler (AppHandler) :
     """
         PDF generation/export
     """
 
     def respond (self, url_values) :
-
-        title = url_values.get('name', "Hello World")
-
-        tpl = pdf.PageTemplate('id', 
-            header_columns  = (
-                ("", ""),
-                ("", ""),
-                ("", ""),
-                ("Vuokrasopimus", "%(today)s\n" + title + "\n"),
-            ),
-            footer_columns  = (
-                ("Teekkarispeksi Ry", "www.teekkarispeksi.fi"),
-                ("Tekniikkavastaava", "Juha Kallas\n045 xxx yyzz\njskallas@cc.hut.fi"),
-                ("Varastovastaava", "Joel Pirttimaa\n045 xxx yyzz\njhpirtti@cc.hut.fi"),
-                ("", ""),
-            ),
-        )
-
-        doc = pdf.DocumentTemplate([tpl],
-            title = title, author = "A. N. Onous"
-        )
-
-        # stylesheet
-        styles = pdf.Styles
+        """
+            Generate the document, and return it as a .pdf file, with the filename generated from the document's title.
+        """
+        
+        pdf_file = self.generate(**url_values)
 
-        # tree root
-        list_seq = pdf.ListItem.seq
-        tree = 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, title + 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."),
-            ]),
-        ])
+        # file wrapper
+        # XXX: is this any use at all for StringIO?
+        pdf_file = werkzeug.wrap_file(self.request.environ, pdf_file)
 
-        from reportlab.platypus import Paragraph as p
+        # respond with file wrapper
+        return Response(pdf_file, mimetype='application/pdf', direct_passthrough=True)
 
-        elements = [
-                p("Vuokrasopimus", styles.h1),
-                p("Teekkarispeksi ry AV-tekniikka", styles.h3),
-        ] + list(tree.render_pdf()) + [
-                p("Nouto", styles.h2),
-                p("\t\tAika: _______________\tPaikka: _______________", styles.text),
-                p("Palautus", styles.h2),
-                p("\t\tAika: _______________\tPaikka: _______________", styles.text),
-                
-                pdf.SignatureBlock(("Vuokranantaja", "Vuokraaja"), ("%(column)s", "Nimen selvennys", "Aika ja paikka"), {
-                    ('Vuokranantaja', 'Nimen selvennys'): "Joel Pirttimaa",
-                    ('Vuokranantaja', 'Aika ja paikka'): 'Otaniemi, %(today)s',
-                    ('Vuokraaja', 'Aika ja paikka'): 'Otaniemi, %(today)s',
-                }),
-        ]
-       
-        # render elements to buf as PDF code
-        pdf_code = doc.render_string(elements)
+    def generate (self, **url_values) :
+        """
+            Generate the PDF document as a file-like object.
+        """
 
-        return Response(pdf_code, mimetype='application/pdf')
+        document, elements = self.generate_document(**url_values)
+
+        # render and return StringIO
+        return document.render_buf(elements)
+
+    def generate_document (self, **url_values) :
+        """
+            Return the
+                (DocumentTemplate, elements)
+            to generate the PDF document from.
+        """
+
+        raise NotImplementedError()
+
+### XXX: random test controllers
 
 class Index (PageHandler) :
     DATA = (
--- a/svv/orders.py	Thu Dec 23 18:20:26 2010 +0200
+++ b/svv/orders.py	Thu Dec 23 19:56:44 2010 +0200
@@ -3,9 +3,10 @@
     Order data model/view/handler
 """
 
-from svv.controllers import PageHandler
+from svv.controllers import PageHandler, DocumentHandler
 from svv.html import tags
 from svv import database as db
+from svv import pdf
 
 import datetime
 import logging
@@ -31,12 +32,7 @@
 
         super(FormError, self).__init__(error)
 
-class OrderForm (object) :
-    """
-        A single instance of a <form>, where we can process submitted data from the client, storing the associated
-        Order-related data, and then render a form for any of that related data.
-    """
-    
+class BaseForm (object) :
     # any POST data we have processed, updated from process()
     data = None
 
@@ -54,26 +50,8 @@
         """
             Update our attributes with default values
         """
-
-        self.customer_id = None
-        self.customer_name = None
-
-        self.contact_id = None
-        self.contact_name = None
-        self.contact_phone = None
-        self.contact_email = None
-        self.contact_customer = None
-
-        self.event_name = None
-        self.event_description = None
-
-        tomorrow = datetime.date.today() + datetime.timedelta(days=1)
         
-        # default to tomorrow afternoon
-        self.event_start = datetime.datetime.combine(tomorrow, datetime.time(16, 00))
-        
-        # automatically determined once start is set
-        self.event_end = None
+        raise NotImplementedError()
 
     def fail_field (self, form_error, field=None) :
         """
@@ -236,6 +214,128 @@
         # return full set of values
         return (id_value, ) + tuple(value for name, col, value in fields)
 
+    def process (self, data) :
+        """
+            Bind ourselves to the given incoming POST data, and update our order field attributes.
+
+                data        - the submitted POST data as a MultiDict
+
+            Returns True if all fields were processed without errors, False otherwise.
+        """
+
+        raise NotImplmentedError()
+    
+        return not self.errors
+
+    def render_text_input (self, name, value=None, multiline=False) :
+        """
+            Render HTML for a generic text field input.
+                
+                name            - field name, as used for POST
+                value           - default field value
+                multiline       - use a multi-line <textarea> instead
+        """
+
+        if multiline :
+            # XXX: textarea can't be self-closing for some reason?
+            return tags.textarea(name=name, id=name, _selfclosing=False, _whitespace_sensitive=True)(value)
+
+        else :
+            return tags.input(type='text', name=name, id=name, value=value)
+
+    def render_select_input (self, name, options, value=None) :
+        """
+            Render HTML for a generic select control.
+
+                name            - field name, as used for POST
+                options         - sequence of (value, title) options. `value` may be None to omit.
+                value           - the selected value
+        """
+
+        return tags.select(name=name, id=name)(
+            (
+                tags.option(value=opt_value, selected=('selected' if opt_value == value else None))(opt_title)
+            ) for opt_value, opt_title in options
+        )
+
+    def render_datetime_input (self, name, value=None) :
+        """
+            Render HTML for a generic datetime control (using jQuery).
+
+                name            - field name, as used for POST
+                value           - selected date
+        """
+            
+        return (
+            self.render_text_input(name, (value.strftime(self.DATETIME_FORMAT) if value else None)),
+
+            tags.script("$(document).ready(function () { $('#" + name + "').datetimepicker(); });"),
+        )
+
+    def render_form_field (self, name, title, description, inputs) :
+        """
+            Render the label, input control, error note and description for a single field, along with their containing <li>.
+        """
+
+        # any errors for this field?
+        errors = self.errors[name]
+
+        return tags.li(class_='field' + (' failed' if errors else ''))(
+            tags.label((
+                title,
+                tags.strong(u"(Virheellinen)") if errors else None,
+            ), for_=name),
+
+            inputs,
+
+            tags.p(description),
+            
+            # possible errors
+            tags.ul(class_='errors')(tags.li(error.message) for error in errors) if errors else None,
+        )
+
+    def render (self, action, submit=u"Tallenna") :
+        """
+            Render the entire <form>, using any loaded/processed values.
+
+                action          - the target URL for the form to POST to
+                submit          - label for the submit button
+        """
+        
+        raise NotImplementedError()
+
+
+class OrderForm (BaseForm) :
+    """
+        A single instance of a <form>, where we can process submitted data from the client, storing the associated
+        Order-related data, and then render a form for any of that related data.
+    """
+    
+    def defaults (self) :
+        """
+            Update our attributes with default values
+        """
+
+        self.customer_id = None
+        self.customer_name = None
+
+        self.contact_id = None
+        self.contact_name = None
+        self.contact_phone = None
+        self.contact_email = None
+        self.contact_customer = None
+
+        self.event_name = None
+        self.event_description = None
+
+        tomorrow = datetime.date.today() + datetime.timedelta(days=1)
+        
+        # default to tomorrow afternoon
+        self.event_start = datetime.datetime.combine(tomorrow, datetime.time(16, 00))
+        
+        # automatically determined once start is set
+        self.event_end = None
+
     def process_customer (self) :
         """
             Process the incoming customer_* fields, returning (customer_id, customer_name).
@@ -410,51 +510,6 @@
         return self.app.query(sql)
 
 
-    def render_text_input (self, name, value=None, multiline=False) :
-        """
-            Render HTML for a generic text field input.
-                
-                name            - field name, as used for POST
-                value           - default field value
-                multiline       - use a multi-line <textarea> instead
-        """
-
-        if multiline :
-            # XXX: textarea can't be self-closing for some reason?
-            return tags.textarea(name=name, id=name, _selfclosing=False, _whitespace_sensitive=True)(value)
-
-        else :
-            return tags.input(type='text', name=name, id=name, value=value)
-
-    def render_select_input (self, name, options, value=None) :
-        """
-            Render HTML for a generic select control.
-
-                name            - field name, as used for POST
-                options         - sequence of (value, title) options. `value` may be None to omit.
-                value           - the selected value
-        """
-
-        return tags.select(name=name, id=name)(
-            (
-                tags.option(value=opt_value, selected=('selected' if opt_value == value else None))(opt_title)
-            ) for opt_value, opt_title in options
-        )
-
-    def render_datetime_input (self, name, value=None) :
-        """
-            Render HTML for a generic datetime control (using jQuery).
-
-                name            - field name, as used for POST
-                value           - selected date
-        """
-            
-        return (
-            self.render_text_input(name, (value.strftime(self.DATETIME_FORMAT) if value else None)),
-
-            tags.script("$(document).ready(function () { $('#" + name + "').datetimepicker(); });"),
-        )
-
     def render_customer_input (self) :
         """
             Render HTML for customer_id/name field inputs.
@@ -522,29 +577,6 @@
 
         )
 
-    def render_form_field (self, name, title, description, inputs) :
-        """
-            Render the label, input control, error note and description for a single field, along with their containing <li>.
-        """
-
-        # any errors for this field?
-        errors = self.errors[name]
-
-        return tags.li(class_='field' + (' failed' if errors else ''))(
-            tags.label((
-                title,
-                tags.strong(u"(Virheellinen)") if errors else None,
-            ), for_=name),
-
-            inputs,
-
-            tags.p(description),
-            
-            # possible errors
-            tags.ul(class_='errors')(tags.li(error.message) for error in errors) if errors else None,
-        )
-
-
     def render (self, action, submit=u"Tallenna") :
         """
             Render the entire <form>, using any loaded/processed values.
@@ -609,6 +641,135 @@
             tags.input(type='submit', value=submit),
         )
 
+class OrderContractForm (BaseForm) :
+    """
+        Form for generating an order's contract document
+    """
+
+    DEFAULT_PLACE = u"Otaniemi"
+
+    def defaults (self) :
+
+        today = datetime.date.today()
+
+        self.prefill_placetime = False
+        self.prefill_placetime_default = "%s, %s" % (self.DEFAULT_PLACE, today.strftime("%d.%m.%Y"))
+        self.prefill_ourname = False
+        self.prefill_ourname_default = None
+
+    def build_name_for_armed_input (self, name) :
+        """
+            Return the name used for the checkbox associated with the named armed text input
+        """
+        
+        return name + '_enabled'
+
+    def render_checkbox_input (self, name, checked=None) :
+        """
+            Render HTML for a checkbox.
+        """
+
+        return tags.input(type="checkbox", name=name, id=name, value="1",
+            checked=("checked" if checked else None)
+        )
+
+    def render_armed_text_input (self, name, value=False, default=None) :
+        """
+            Render HTML for a text field that must be enabled by a checkbox before being used.
+
+                value       - the three-state value
+                                False       - not checked, use default as value
+                                None        - checked, use empty value
+                                str         - checked, ues value
+        """
+
+        checkbox_name = self.build_name_for_armed_input(name)
+
+        if value is False :
+            checked = False
+            value = default
+        
+        else :
+            checked = True
+
+        return (
+            self.render_checkbox_input(checkbox_name, checked),
+            self.render_text_input(name, value),
+
+            tags.script("$(document).ready(function () { $('#" + name + "').formEnabledBy($('#" + checkbox_name + "')); });")
+        )
+
+    def process_checkbox_field (self, name, required=None) :
+        """
+            Process an incoming checkbox input's value.
+
+            The value must be a literal "1" to be accepted as True, and only a missing or empty value will be
+            recognized as False.
+        """
+
+        value = self.process_raw_field(name, required)
+
+        if not value and required :
+            raise FormError(name, value, "Must be checked")
+
+        elif not value :
+            # unchecked
+            return False
+
+        elif value == "1" :
+            # checked
+            return True
+        
+        else :
+            raise FormError(name, value, "Unrecognized checkbox state")
+
+    def process_armed_text_field (self, name, required=None) :
+        """
+            A text field that must be enabled by a checkbox before being used.
+
+            If not enabled, returns False. Otherwise, text field value, and fails if empty but required.
+        """
+
+        checkbox_name = self.build_name_for_armed_input(name)
+        
+        if self.process_checkbox_field(checkbox_name) :
+            # use input value
+            return self.process_string_field(name, required=required)
+
+        else :
+            # not selected
+            return False
+
+    def render (self, action, submit=u"Tulosta Vuokrasopimus") :
+        
+        # self.url_for(OrderContractDocument, id=id)
+        return tags.form(action=action, method='POST')(
+            tags.fieldset(
+                tags.legend(u"Vuokrasopimus"),
+
+                tags.ol(
+                    self.render_form_field('prefill_placetime', u"Esitäytä aika/paikka", u"Esitäytä allekirjoitusosion aika/paikka rivi", (
+                        self.render_armed_text_input('prefill_placetime', self.prefill_placetime, self.prefill_placetime_default)
+                    )),
+
+                    self.render_form_field('prefill_ourname', u"Esitäytä vuokranantaja", u"Esitäytä allekirjoitusosion vuokranantajan nimi", (
+                        self.render_armed_text_input('prefill_ourname', self.prefill_ourname, self.prefill_ourname_default)
+                    )),
+                ),
+            ),
+
+            tags.input(type='submit', value=submit),
+        )
+
+
+    def process (self, data) :
+        # bind the raw post data
+        self.data = data
+
+        self.prefill_placetime = self.process_armed_text_field('prefill_placetime')
+        self.prefill_ourname = self.process_armed_text_field('prefill_ourname')
+        
+        return not self.errors
 
 class OrdersView (PageHandler) :
     """
@@ -874,6 +1035,24 @@
             # fetch data from database
             self.form.load(id)
 
+    def render_contract_form (self, id) :
+        """
+            Render the contract panel for our view
+        """
+
+        form = OrderContractForm(self.app)
+
+        # prefilled values?
+        if self.POST :
+            # feed form POST data
+            form.process(self.POST)
+
+        else :
+            form.defaults()
+
+        # render
+        return form.render(action=self.url_for(OrderContractDocument, id=id))
+
     def render_content (self, id) :
         """
             Render our form
@@ -882,7 +1061,8 @@
         return (
             tags.h1(u"Tilaus #%d" % (id, )),
             tags.h3(u"%s - %s (%s)" % (self.form.customer_name, self.form.event_name, self.form.event_start.strftime('%d.%m.%Y'))),
-
+            
+            self.render_contract_form(id),
             self.form.render(action=self.url_for(OrderView, id=id))
         )
 
@@ -949,3 +1129,114 @@
             self.form.render(action=self.url_for(NewOrderView))
         )
 
+class OrderContractDocument (DocumentHandler) :
+    """
+        Generate and return PDF document for rental contract.
+    """
+
+    def load_params (self, data) :
+        """
+            Return OrderContractForm with parameters for generation
+        """
+
+        form = OrderContractForm(self.app)
+
+        form.defaults()
+
+        if data :
+            # XXX: can't fail
+            form.process(data)
+
+        return form
+
+    def load_order (self, id):
+        """
+            Return OrderModel object for given ID
+        """
+
+        # XXX: really need that OrderModel object :)
+        form = OrderForm(self.app)
+        form.load(id)
+
+        return form
+    
+    def generate_document (self, id) :
+
+        # retrieve from db
+        order = self.load_order(id)
+
+        # params set by form
+        params = self.load_params(self.POST)
+
+        title = "Teekkarispeksi Ry - Vuokrasopimus"
+        author = "Teekkarispeksi Ry"
+
+        tpl = pdf.PageTemplate('page',
+            header_columns  = (
+                ("", ""),
+                ("", ""),
+                ("", ""),
+                ("Vuokrasopimus", "\n".join((order.customer_name, order.event_name, 'dd.mm.yy hh:mm-hh:mm'))),
+            ),
+            footer_columns  = (
+                ("Teekkarispeksi Ry", "www.teekkarispeksi.fi"),
+                ("Tekniikkavastaava", "Juha Kallas\n045 xxx yyzz\njskallas@cc.hut.fi"),
+                ("Varastovastaava", "Joel Pirttimaa\n045 xxx yyzz\njhpirtti@cc.hut.fi"),
+                ("", ""),
+            ),
+        )
+
+        doc = pdf.DocumentTemplate([tpl],
+            title = title, author = author,
+        )
+
+        # stylesheet
+        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."),
+            ]),
+        ])
+
+
+
+        sig_prefill = {}
+
+        if params.prefill_placetime :
+            sig_prefill[('Vuokranantaja', 'Aika ja paikka')] = sig_prefill[('Vuokraaja', 'Aika ja paikka')] = params.prefill_placetime
+
+        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),
+                
+                pdf.SignatureBlock(("Vuokranantaja", "Vuokraaja"), ("%(column)s", "Nimen selvennys", "Aika ja paikka"), sig_prefill),
+        ]
+
+        # ok
+        return doc, elements
+       
+
--- a/svv/pdf.py	Thu Dec 23 18:20:26 2010 +0200
+++ b/svv/pdf.py	Thu Dec 23 19:56:44 2010 +0200
@@ -420,9 +420,9 @@
                 pageSize=page_size
         )
 
-    def render_string (self, elements) :
+    def render_buf (self, elements) :
         """
-            Build the document using the given list of Flowables, returning the PDF as a single str.
+            Build the document using the given list of Flowables, returning a StringIO containing the PDF.
         """
 
         buf = StringIO()
@@ -430,6 +430,19 @@
         # build
         self.build(elements, buf)
 
+        # prepare for read
+        buf.seek(0)
+
+        return buf
+
+    def render_string (self, elements) :
+        """
+            Build the document using the given list of Flowables, returning the PDF as a single str.
+        """
+        
+        # render
+        buf = self.render_buf(elements)
+
         # binary data out
         return buf.getvalue()
 
--- a/svv/urls.py	Thu Dec 23 18:20:26 2010 +0200
+++ b/svv/urls.py	Thu Dec 23 19:56:44 2010 +0200
@@ -5,22 +5,22 @@
 from werkzeug.routing import Map, Rule
 
 # controllers
-from svv.controllers import Index, Document
+from svv.controllers import Index 
 from svv.customers import CustomersView, CustomerView
-from svv.orders import OrdersView, OrderView, NewOrderView
+from svv.orders import OrdersView, OrderView, NewOrderView, OrderContractDocument
 
 # map URLs -> AppHandler
 URLS = Map((
     Rule('/orders/', endpoint=OrdersView),
+    Rule('/orders/new', endpoint=NewOrderView),
     Rule('/orders/<int:id>', endpoint=OrderView),
-    Rule('/orders/new', endpoint=NewOrderView),
+    Rule('/orders/<int:id>/Vuokrasopimus.pdf', endpoint=OrderContractDocument),
 
     Rule('/customers', endpoint=CustomersView),
     Rule('/customers/<int:id>', endpoint=CustomerView),
 
     # test stuff
     Rule('/', endpoint=Index),
-    Rule('/pdf/<string:name>.pdf', endpoint=Document),
 ))