# coding: utf-8
"""
Order data model/view/handler
"""
from svv.controllers import PageHandler, DocumentHandler
from svv.html import tags
from svv import database as db
from svv import pdf, markup
import datetime
import logging
import collections
try :
# Python2.6 stdlib
import json
except ImportError :
# simplejson, API should be similar
import simplejson as json
log = logging.getLogger('svv.orders')
class Customer (object) :
"""
Data-mapping for the customers table
"""
# customer name (organization or natural person)
name = None
class Contact (object) :
"""
Data-mapping for the contacts table
"""
# human-readable details
name = None
phone = None
email = None
# relation to Customer
customer = None
class Order (object) :
"""
Data-mapping for orders table.
"""
# relation to Customer
customer = None
# relation to Contact
contact = None
# event info
event_name = None
event_description = None
event_start = None
event_end = None
def __init__ (self) :
"""
Construct an Order with default values
"""
## Set default values
tomorrow = datetime.date.today() + datetime.timedelta(days=1)
# default to tomorrow afternoon
self.event_start = datetime.datetime.combine(tomorrow, datetime.time(16, 00))
self.event_end = None # determined by UI behaviour
def format_event_time (self) :
"""
Return a concise string describing the event's approximate time span.
"""
start = self.event_start
end = self.event_end
# different days
if (start.year, start.month) != (end.year, end.month) :
# the full dates
date = "%s - %s" % (start.strftime("%d.%m.%Y"), end.strftime("%d.%m.%Y"))
elif (start.day != end.day) :
# day range
date = "%s-%s.%s" % (start.strftime("%d"), end.strftime("%d"), start.strftime("%m.%Y"))
else : # start.date() == end.date()
# single day
date = start.strftime("%d.%m.%Y")
# time
time = "%s-%s" % (start.strftime("%H:%M"), end.strftime("%H:%M"))
# format
return "%s %s" % (date, time)
# bind against database schema
db.mapper(Customer, db.customers)
db.mapper(Contact, db.contacts)
db.mapper(Order, db.orders, properties=dict(
customer = db.relation(Customer),
contact = db.relation(Contact),
))
class FormError (Exception) :
"""
A user-level error in a form field
"""
def __init__ (self, field, value, error) :
"""
field - name of field with error
value - the errenous value in the form that we recieved it
may be None if it was the *lack* of a value that caused the issue
error - descriptive text for user
"""
self.field = field
self.value = value
super(FormError, self).__init__(error)
class BaseForm (object) :
# any POST data we have processed, updated from process()
data = None
def __init__ (self, app) :
"""
app - bind this form to the app state (db etc)
"""
self.app = app
# accumulated errors
self.errors = collections.defaultdict(list)
def defaults (self) :
"""
Update our attributes with default values
"""
raise NotImplementedError()
def fail_field (self, form_error, field=None) :
"""
Mark the field mentioned inside the given FormError as failed.
form_error - the FormError to store
field - the name of the field to store the error under, if not the same as in form_error
"""
field = field or form_error.field
log.warn("Marking field %s as failed: %s", field, form_error)
self.errors[field].append(form_error)
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 process_raw_field (self, name, default=None, required=None) :
"""
Process a generic incoming data field.
default - value to return if no value was present
required - raise a FormError if no value present
Returns the value as a str, or default
"""
if name in self.data :
return self.data[name]
elif required :
raise FormError(name, None, "Required field")
else :
return default
def process_text_field (self, name, default=None, required=None, strip=True) :
"""
Process a generic incoming string field.
Trims extra whitespace from around the value, unless strip=False is given.
Returns the value as unicode, or default.
"""
value = self.process_raw_field(name, required=required)
if value is None :
return default
try :
# XXX: decode somehow, or can werkzeug handle that?
value = unicode(value)
except UnicodeDecodeError :
raise FormError(name, value, "Failed to decode Unicode characters")
if strip :
value = value.strip()
return value
def process_integer_field (self, name, default=None, required=None) :
"""
Process a generic incoming int field.
Returns the value as int, or default.
"""
value = self.process_raw_field(name, required=required)
if value is None :
return default
try :
return int(value)
except ValueError :
raise FormError(name, value, "Must be a number")
DATETIME_FORMAT = "%d.%m.%Y %H:%M"
def process_datetime_field (self, name, default=None, required=None, format=DATETIME_FORMAT) :
"""
Process an incoming datetime field.
Returns the value as datetime, or default.
"""
value = self.process_raw_field(name, required=required)
if value is None :
return default
try :
return datetime.datetime.strptime(value, format)
except ValueError, ex :
raise FormError(name, value, "Invalid date/time value: " + str(ex))
def process_checkbox_field (self, name, required=None) :
"""
Process an incoming checkbox input's value.
Any non-empty/non-whitespace value will be accepted as True.
"""
value = self.process_raw_field(name, required)
if not value and required :
raise FormError(name, value, "Must be checked")
elif value and value.strip() :
# checked
return True
else :
# unchecked
return False
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_text_field(name, required=required)
else :
# not selected
return False
def process_multifield (self, table, id, fields) :
"""
Process a set of user-given field values for an object with an unique id, and some set of additional fields.
If the id is given, look up the corresponding field values, and return those.
If any of the fields are given, either look up a matching row, or create a new one, and return its id.
Returns an (id_name, field_name, ...) N-tuple.
"""
id_name, id_col, id_value = id
if id_value :
# look up object from db
columns = [col for name, col, value in fields]
sql = db.select(columns, (id_col == id_value))
for row in self.app.query(sql) :
# XXX: sanity-check row values vs our values
# new values
fields = tuple(
(name, col, row[col]) for name, col, value in fields
)
# ok, just use the first one
break
else :
# not found!
raise FormError(id_name, id_value, "Item selected does not seem to exist")
log.info("Lookup %s=%d -> %s", id_name, id_value, dict((name, value) for name, col, value in fields))
elif any(value for name, col, value in fields) :
# look up identical object from db?
sql = db.select([id_col], db.and_(*[(col == value) for name, col, value in fields]))
for row in self.app.query(sql) :
if id_value :
log.warn("Duplicate %s=%d for %s", id_name, id_value, dict((name, value) for name, col, value in fields))
# found object's id
id_value = row[id_col]
log.info("Found %s -> %d", dict((name, value) for name, col, value in fields), id_value)
# create new object?
if not id_value :
sql = db.insert(table).values(dict((col, value) for name, col, value in fields))
id_value, = self.app.insert(sql)
log.info("Create %s -> %d", dict((name, value) for name, col, value in fields), id_value)
else :
# not known
log.debug("No %s known for order...", id_name)
# 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, rows=10, autoscale=True) :
"""
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
rows - number of rows for <textarea> per default
autoscale - automatically scale the <textarea> to the number of lines of text
"""
if multiline :
if autoscale and value :
rows = value.count('\n') * 5 / 4
# XXX: textarea can't be self-closing for some reason?
return tags.textarea(name=name, id=name, rows=rows, _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_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 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).
"""
try :
customer_id = self.process_integer_field('customer_id')
customer_name = self.process_text_field('customer_name')
if not customer_id and not customer_name :
raise FormError('customer_name', None, "Must enter a customer")
return self.process_multifield(db.customers,
('customer_id', db.customers.c.id, customer_id),
(
('customer_name', db.customers.c.name, customer_name),
),
)
except FormError, e :
# list it
self.fail_field(e, 'customer_name')
return None, None
def process_contact (self, customer_id, customer_name) :
"""
Process the incoming contact_* fields, returning
(contact_id, contact_name, contact_phone, contact_email, contact_customer)
"""
try :
contact_id = self.process_integer_field('contact_id')
contact_name = self.process_text_field('contact_name')
contact_phone = self.process_text_field('contact_phone')
contact_email = self.process_text_field('contact_email')
contact_customer = customer_id
if not contact_id and not contact_name and (contact_phone or contact_email) and customer_name :
# use customer name as contact name, if otherwise missing
contact_name = customer_name
if not contact_id and not (contact_name or contact_phone or contact_email) :
raise FormError('contact_name', None, "Must enter a contact")
return self.process_multifield(db.contacts,
('contact_id', db.contacts.c.id, contact_id),
(
('contact_name', db.contacts.c.name, contact_name),
('contact_phone', db.contacts.c.phone, contact_phone),
('contact_email', db.contacts.c.email, contact_email),
('contact_customer', db.contacts.c.customer_id, contact_customer),
),
)
except FormError, e :
# list it
self.fail_field(e, 'contact_name' if e.field == 'contact_id' else None)
return None, None, None, None, None
def process_event (self) :
"""
Process the incoming event_* fields, returning
(event_name, event_description, event_start, event_end)
"""
try :
event_name = self.process_text_field('event_name')
event_description = self.process_text_field('event_description', strip=False)
event_start = self.process_datetime_field('event_start')
event_end = self.process_datetime_field('event_end')
if event_end < event_start :
raise FormError('event_start', event_end, "Event must end after start")
return (event_name, event_description, event_start, event_end)
except FormError, e :
# list it
self.fail_field(e)
return None, None, None, None
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.
"""
# bind the raw post data
self.data = data
# customer
self.customer_id, self.customer_name = self.process_customer()
# contact
self.contact_id, self.contact_name, self.contact_phone, self.contact_email, self.contact_customer = self.process_contact(self.customer_id, self.customer_name)
if self.contact_customer and not self.customer_id :
# TODO: be really smart?
pass
# event
self.event_name, self.event_description, self.event_start, self.event_end = self.process_event()
return not self.errors
def load (self, order) :
"""
Load our field values from the given Order.
"""
if order.customer :
self.customer_id = order.customer.id
self.customer_name = order.customer.name
if order.contact :
self.contact_id = order.contact.id
self.contact_name = order.contact.name
self.contact_phone = order.contact.phone
self.contact_email = order.contact.email
self.event_name = order.event_name
self.event_description = order.event_description
self.event_start = order.event_start
self.event_end = order.event_end
def build_customer_list (self) :
"""
Query a (id, name) list of customers.
"""
sql = db.select([db.customers.c.id, db.customers.c.name])
return self.app.query(sql)
def build_contact_list (self, customer_id=None) :
"""
Query a (id, name, phone, email) list of contacts, optionally for given customer if given.
"""
sql = db.select([db.contacts.c.id, db.contacts.c.name, db.contacts.c.phone, db.contacts.c.email])
if customer_id :
sql = sql.order_by((db.contacts.c.customer_id == customer_id))
return self.app.query(sql)
def render_customer_input (self) :
"""
Render HTML for customer_id/name field inputs.
"""
# all known customers
customers = list(self.build_customer_list())
return (
self.render_select_input('customer_id', [(0, u"Luo uusi")] + customers, self.customer_id),
self.render_text_input('customer_name', self.customer_name),
tags.script(r"$(document).ready(function () { $('#customer_id').formSelectsPreset({textTarget: $('#customer_name')}); });"),
)
def render_contact_input (self) :
"""
Render HTML for contact name field <input>s
"""
# recommended contacts for selected customer, if known
contacts = list(self.build_contact_list(self.customer_id))
return (
self.render_select_input('contact_id', [(0, u"Luo uusi")] + [(id, name) for id, name, phone, email in contacts], self.contact_id),
self.render_text_input('contact_name', self.contact_name),
tags.script("""\
$(document).ready(function () {
contact_phones = %(phones)s;
contact_emails = %(emails)s;
$('#contact_id').formSelectsPreset({
textTarget: $('#contact_name'),
mapTargets: [
[$('#contact_phone'), contact_phones],
[$('#contact_email'), contact_emails]
]
});
});
""" % dict(
phones = json.dumps(dict((row[db.contacts.c.id], row[db.contacts.c.phone]) for row in contacts)),
emails = json.dumps(dict((row[db.contacts.c.id], row[db.contacts.c.email]) for row in contacts)),
)),
)
def render_event_input (self) :
"""
Render HTML for event start/end field <input>s
"""
return (
self.render_datetime_input('event_start', self.event_start),
" - ",
self.render_datetime_input('event_end', self.event_end),
tags.script(r"""
$(document).ready(function () {
var event_start = $('#event_start');
var event_end = $('#event_end');
// XXX: datetimepicker breaks beforeShow completely (event having one breaks)
event_start.change(function () {
// copy value as default
var start_date = event_start.datetimepicker("getDate");
if (start_date) {
event_end.datetimepicker("option", {
defaultDate: start_date,
minDate: start_date,
});
}
});
// init default as well
event_start.change();
});""" ),
)
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
"""
return tags.form(action=action, method='POST')(
(
tags.h3(u"Lomakkeessa oli virheitä"),
tags.p(u"Korjaa lomake ja lähetä uudelleen"),
tags.ul(class_='errors')(
tags.li(tags.a(href='#' + error.field)(
tags.em(error.field),
error.message,
)) for field_errors in self.errors.itervalues() for error in field_errors
),
) if self.errors else None,
tags.fieldset(
tags.legend(u"Tilaaja"),
tags.ol(
self.render_form_field('customer_name', u"Tilaaja", u"Tilaavan yhdistyksen/henkilön nimi", (
self.render_customer_input()
)),
self.render_form_field('contact_name', u"Yhteyshenkilö", u"Yhteyshenkilön nimi, jos eri kun tilaaja", (
self.render_contact_input()
)),
self.render_form_field('contact_phone', u"Puhelinnumero", u"Yhteyshenkilön puhelinnumero", (
self.render_text_input('contact_phone', self.contact_phone)
)),
self.render_form_field('contact_email', u"Sähköpostiosoite", u"Yhteyshenkilön sähköpostiosoite", (
self.render_text_input('contact_email', self.contact_email)
)),
),
),
tags.fieldset(
tags.legend(u"Tapahtuma"),
tags.ol(
self.render_form_field('event_name', u"Tapahtuma", u"Tapahtuman lyhyt nimi", (
self.render_text_input('event_name', self.event_name)
)),
self.render_form_field('event_description', u"Lisätiedot", u"Tapahtuman tarkemmat tiedot", (
self.render_text_input('event_description', self.event_description, multiline=True)
)),
self.render_form_field('event_start', u"Ajankohta",
(
u"Tapahtuman ajankohta (kamat noudetaan - palautetaan)",
tags.br(),
u"(%s)" % (datetime.datetime.now().strftime(self.DATETIME_FORMAT)),
),
(self.render_event_input()),
),
),
),
tags.input(type='submit', value=submit),
)
class OrderContractForm (BaseForm) :
"""
Form for generating an order's contract document
"""
DEFAULT_PLACE = u"Otaniemi"
DEFAULT_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: _______________
"""
# user clicked 'Edit' button, i.e. they want to view the rest of the edit fields
edit = None
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
self.edit = False
self.contract_text = self.DEFAULT_TEXT
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)
)),
(
self.render_form_field('contract_text', u"Sopimusteksti", u"Vuokrasopimuksen vapaamuotoinen otsikko, teksti, sopimusehdot, yms. Voidaan käyttää muuttujakenttiä (i.e. {...}). XXX: listaa kentät", (
self.render_text_input('contract_text', self.contract_text, multiline=True, autoscale=True)
)),
) if self.edit else None,
tags.li(
tags.input(type='submit', value=submit),
tags.input(type='submit', name='edit', value=u"Muokkaa lisää"),
),
),
),
)
def process (self, data) :
"""
Bind incoming data.
Returns true if no error input, and not requesting the full editing form ("Edit more" button).
"""
# 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')
self.edit = self.process_checkbox_field('edit')
self.contract_text = self.process_text_field('contract_text', default=self.DEFAULT_TEXT)
return not self.errors and not self.edit
class OrdersView (PageHandler) :
"""
Render overview list of all orders in database.
Currently divided into current/upcoming/past listings
"""
def process (self) :
"""
prepare
"""
# database sesssion
self.session = self.app.session()
def build_orders_list (self, order_by=(Order.event_start)) :
"""
Build summary list of orders, returning a series of Order objects, ordered by event_start, per default.
"""
return self.session.query(Order).options(db.eagerload(Order.customer), db.eagerload(Order.contact)).order_by(order_by).all()
def group_orders_by_time (self, orders) :
"""
Sort the given set of orders into a set of categories by time.
Returns a
(past, current, future)
set of order summary items.
"""
past = []
current = []
future = []
today = datetime.date.today()
def _classify (order) :
"""
Return list to append order to
"""
if order.event_end.date() < today :
return past
elif order.event_start.date() <= today :
return current
else :
return future
for order in orders :
# resolve list for order
category = _classify(order)
category.append(order)
return past, current, future
# columns for sorting
# XXX: sorting not actually implemented...
COLUMNS = (
('order_id', u"ID", Order.id),
('customer_name', u"Tilaaja", Customer.name),
('event_name', u"Tapahtuma", Order.event_name),
('contact_name', u"Yhteyshenkilö", Contact.name),
('event_start', u"Ajankohta", Order.event_start),
)
def render_orders_list_rows (self, orders) :
"""
Render HTML for each of the orders in the order list, inserting sub-captions when the month changes
"""
# XXX: still broken
from svv import urls
# track each row's year/month
last_date = None
# alternating rows
row_counter = 0
for order in orders :
# this order's date - only consider the starting datetime
date = order.event_start.date()
# month changed?
if last_date and (date.year, date.month) != (last_date.year, last_date.month) :
# sub-caption
yield tags.tr(class_='sub-caption')(
tags.th(colspan=len(self.COLUMNS))(
date.strftime("%B %Y")
)
)
# reset
row_counter = 0
# track
last_date = date
# actual row
yield tags.tr(class_=('alternate' if row_counter % 2 else None))(
# Id
tags.td(class_='row-header')(
tags.a(href=self.url_for(OrderView, id=order.id))(
u"#%d" % (order.id)
)
),
# Tilaaja
tags.td(
tags.a(href=self.url_for(urls.CustomerView, id=order.customer.id))(
order.customer.name
) if order.customer else "-"
),
# Tapahtuma
tags.td(
tags.a(href=self.url_for(OrderView, id=order.id))(
order.event_name
)
),
# Yhteyshenkilö
tags.td(
tags.a(href='#')(
order.contact.name
) if order.contact else "-"
),
# Ajankohta
tags.td(
order.format_event_time(),
),
)
row_counter += 1
def render_orders_list (self, orders, caption) :
"""
Render HTML for sorted order list
"""
return tags.table(cellspacing="0")(
tags.caption(caption),
tags.thead(
tags.tr(
tags.th(
title
) for name, title, col in self.COLUMNS
),
),
tags.tbody(
# render using separate loop for sub-captions
self.render_orders_list_rows(orders)
)
)
def render_content (self) :
# full order list
orders_full = self.build_orders_list()
# categorize
past, current, future = self.group_orders_by_time(orders_full)
return (
tags.h1("Tilauslista"),
(
(
self.render_orders_list(orders, title),
) for title, orders in (
(u"Tämänhetkiset", current),
(u"Tulevat", future),
(u"Menneet", past),
) if orders
),
)
class OrderView (PageHandler) :
"""
Render form for database object, let the user make updates, update the database.
"""
def update (self, order, form) :
"""
Update order values from submitted form data
"""
# modify
order.customer_id = form.customer_id
order.contact_id = form.contact_id
order.event_name = form.event_name
order.event_description = form.event_description
order.event_start = form.event_start
order.event_end = form.event_end
# commit
self.session.commit()
def process (self, id) :
"""
Set up our object form.
Process incoming POST data.
"""
# 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)
# form
self.form = OrderForm(self.app)
# load object data
self.form.load(self.order)
# update order?
if self.POST :
# feed form our POST data
if self.form.process(self.POST) :
# submit data was OK, update order from form
self.update(self.order, self.form)
else :
# errors
pass
def render_contract_form (self, id) :
"""
Render the contract panel for our view
"""
form = OrderContractForm(self.app)
form.defaults()
# prefilled values?
if self.POST :
# feed form POST data
form.process(self.POST)
# render
return form.render(action=self.url_for(OrderContractDocument, id=id))
def render_content (self, id) :
"""
Render our form
"""
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))
)
class NewOrderView (PageHandler) :
"""
Render form for input, let the user correct their errors, create the order, and redirect out.
"""
def create (self, form) :
"""
Create the new order from the given form data, returning the new order's ID
"""
# db session
session = self.app.session()
# order model
order = Order()
# set attrs
order.customer_id = form.customer_id
order.contact_id = form.contact_id
order.event_name = form.event_name
order.event_description = form.event_description
order.event_start = form.event_start
order.event_end = form.event_end
# add and commit
session.add(order)
session.commit()
# return with new id
return order.id
def process (self) :
"""
Set up up our form.
"""
self.form = OrderForm(self.app)
# use either POST data or defaults
if self.POST :
# try and process the input, checking for any failures...
if self.form.process(self.POST) :
# should be good, create it!
order_id = self.create(self.form)
# redirect there now that our business is done and the order exists
return self.redirect_for(OrderView, id=order_id)
else :
# errors in form input
pass
else :
# init from defaults
self.form.defaults()
def render_content (self) :
"""
Render our form
"""
return (
tags.h1(u"Uusi tilaus"),
self.form.render(action=self.url_for(NewOrderView))
)
class OrderContractDocument (DocumentHandler) :
"""
Generate and return PDF document for rental contract.
"""
def process (self, id):
"""
Return OrderModel object for given 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)
# form
self.form = OrderContractForm(self.app)
self.form.defaults()
# XXX: also use GET data?
data = self.request.values
if self.form.process(data) :
# render PDF
pass
else :
# show form again
# XXX: fix the pagelayout shit, this is brutal
handler = PageHandler(self.app, self.request, self.urlmap)
# monkey-map
handler.render_content = self.render_content
log.warn("Monkeyhack %r -> %r", self, handler)
# XXX: this invokes handler.process(), but...
return handler.respond(id=id)
def render_content (self, id) :
"""
Display PDF gen form.
"""
return (
tags.h1(u"Vuokrasopimus"),
self.form.render(action=self.url_for(OrderContractDocument, id=id)),
)
def generate_document (self, id) :
"""
Return PDF document to generate
"""
order = self.order
params = self.form
title = "Teekkarispeksi Ry - Vuokrasopimus"
author = "Teekkarispeksi Ry"
tpl = pdf.PageTemplate('page',
header_columns = (
("", ""),
("", ""),
("", ""),
("Vuokrasopimus", [order.customer.name, order.event_name, 'dd.mm.yy hh:mm-hh:mm']),
),
footer_columns = (
("Teekkarispeksi Ry", ("www.teekkarispeksi.fi", )),
("Tekniikkavastaava", ("Juha Kallas", "045 xxx yyzz", "jskallas@cc.hut.fi")),
("Varastovastaava", ("Joel Pirttimaa", "045 xxx yyzz", "jhpirtti@cc.hut.fi")),
("", ""),
),
)
doc = pdf.DocumentTemplate([tpl],
title = title, author = author,
)
# stylesheet
styles = pdf.Styles()
from reportlab.platypus import Paragraph as p
# format
text = params.contract_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 = {}
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 = text_elements + [
pdf.SignatureBlock(("Vuokranantaja", "Vuokraaja"), ("%(column)s", "Nimen selvennys", "Aika ja paikka"), sig_prefill, fullheight=True),
]
# ok
return doc, elements