--- 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
+
+