--- a/svv/orders.py Wed Dec 22 21:30:27 2010 +0200
+++ b/svv/orders.py Thu Dec 23 00:57:46 2010 +0200
@@ -8,67 +8,342 @@
from svv import database as db
import datetime
-
-class OrdersView (PageHandler) :
- def render (self) :
- return tags.h1("Orders list")
-
-class OrderView (PageHandler) :
- def render (self) :
- return tags.h1("Order info")
+import logging
-class NewOrderView (PageHandler) :
- def render_form_field (self, title, description, name, input) :
- return tags.li(class_='field')(
- tags.label(title, for_=name),
+log = logging.getLogger('svv.orders')
- input,
+class FormError (Exception) :
+ """
+ A user-level error in a form field
+ """
- # tags.span("Error!"),
+ def __init__ (self, field, error) :
+ """
+ field - name of field with error
+ error - descriptive text for user
+ """
- tags.p(description),
+ self.field = field
+
+ 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.
+ """
+
+ # 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
+
+ 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_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.post :
+ return self.post[name]
+
+ elif required :
+ raise FormError(name, "Required field")
+
+ else :
+ return default
+
+ def process_string_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, "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, "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, "Invalid date/time value: " + str(ex))
+
+ 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 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, "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_customer (self) :
+ """
+ Process the incoming customer_* fields, returning (customer_id, customer_name).
+ """
+
+ return self.process_multifield(db.customers,
+ ('customer_id', db.customers.c.id, self.process_integer_field('customer_id')),
+ (
+ ('customer_name', db.customers.c.name, self.process_string_field('customer_name')),
+ ),
)
- def get_customer_list (self) :
+ def process_contact (self, customer_id) :
"""
- Get (id, name) list of customers
+ Process the incoming contact_* fields, returning
+ (contact_id, contact_name, contact_phone, contact_email, contact_customer)
"""
- return self.app.query(db.select([db.customers.c.id, db.customers.c.name]))
+ return self.process_multifield(db.contacts,
+ ('contact_id', db.contacts.c.id, self.process_integer_field('contact_id')),
+ (
+ ('contact_name', db.contacts.c.name, self.process_string_field('contact_name')),
+ ('contact_phone', db.contacts.c.phone, self.process_string_field('contact_phone')),
+ ('contact_email', db.contacts.c.email, self.process_string_field('contact_email')),
+ ('contact_customer', db.contacts.c.customer, customer_id),
+ ),
+ )
- def get_contact_list (self, customer_id=None) :
+ def process_event (self) :
"""
- Get (id, name, phone, email) list of contacts, optionally for given customer if given.
+ Process the incoming event_* fields, returning
+ (event_name, event_description, event_start, event_end)
"""
- query = db.select([db.contacts.c.id, db.contacts.c.name, db.contacts.c.phone, db.contacts.c.email])
+ event_name = self.process_string_field('event_name')
+ event_description = self.process_string_field('event_description', strip=False)
+ event_start = self.process_datetime_field('event_start')
+ event_end = self.process_datetime_field('event_end')
+
+ return (event_name, event_description, event_start, event_end)
+
+ 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
+ """
+
+ # 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(customer_id)
+
+ 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()
+
+
+ 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 :
- query = query.where((db.contacts.c.customer == customer_id))
+ sql = sql.where((db.contacts.c.customer == customer_id))
- return self.app.query(query)
+ 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_customer_input (self) :
"""
- Render HTML for customer field <input>s
+ Render HTML for customer_id/name field inputs.
"""
- # pre-selected values?
- customer_id = self.POST.get('customer_id')
- customer_name = self.POST.get('customer_name')
-
- # available values
- customers = self.get_customer_list()
+ # all known customers
+ customers = self.build_customer_list()
return (
- tags.select(name='customer_id', id='customer_id')(
- tags.option(value=0)(u"Luo uusi"),
-
- [(
- tags.option(value=id, selected=('selected' if id == customer_id or name == customer_name else None))(name)
- ) for id, name in customers],
- ),
- tags.input(type='text', name='customer_name', id='customer_name'),
+ self.render_select_input('customer_id', customers, self.customer_id),
+ self.render_text_input('customer_name', self.customer_name),
tags.script(r"$(document).ready(function () { $('#customer_id').formSelectPreset({textTarget: $('#customer_name')}); });"),
)
@@ -77,67 +352,25 @@
"""
Render HTML for contact name field <input>s
"""
-
- # pre-selected values
- customer_id = self.POST.get('customer_id')
- contact_id = self.POST.get('contact_id')
- contact_name = self.POST.get('contact_name')
- contact_phone = self.POST.get('contact_phone')
- contact_email = self.POST.get('contact_email')
-
- # available values
- contacts = self.get_contact_list()
+ # recommended contacts for selected customer, if known
+ contacts = self.get_contact_list(self.customer_id)
return (
- tags.select(name='contact_id', id='contact_id')(
- tags.option(value=0)(u"Luo uusi"),
-
- [(
- tags.option(value=id, selected=('selected' if id == contact_id else None))(name)
- ) for id, name, phone, email in contacts],
- ),
-
- tags.input(type='text', name='contact_name', id='contact_name'),
+ self.render_select_input('contact_id', ((id, name) for id, name, phone, email in contacts), self.contact_id)
+ self.render_text_input('contact_name', self.contact_name),
tags.script(r"$(document).ready(function () { $('#contact_id').formSelectPreset({textTarget: $('#contact_name')}); });"),
)
- DATETIME_FORMAT = "%d.%m.%Y %H:%M"
-
- def get_POST_datetime (self, name, default=None) :
- """
- Return a datetime for something the client POST'd
- """
-
- value = self.POST.get(name)
-
- if value :
- # XXX: handle invalid format..
- return datetime.datetime.strptime(value, self.DATETIME_FORMAT)
-
- else :
- return default
-
def render_event_input (self) :
"""
Render HTML for event start/end field <input>s
"""
- # XXX: sensible defaults?
- tomorrow = datetime.date.today() + datetime.timedelta(days=1)
- default_start = datetime.datetime.combine(tomorrow, datetime.time(16, 00))
-
- # automatically determined once start is set
- default_end = None
-
- # pre-selected values
- event_start = self.get_POST_datetime('event_start', default_start)
- event_end = self.get_POST_datetime('event_end', default_end)
-
return (
- tags.input(type='text', name='event_start', id='event_start', value=(event_start.strftime(self.DATETIME_FORMAT) if event_start else None)),
+ self.render_text_input('event_start', (self.event_start.strftime(self.DATETIME_FORMAT) if event_start else None)),
" - ",
- tags.input(type='text', name='event_end', id='event_end', value=(event_end.strftime(self.DATETIME_FORMAT) if event_end else None)),
+ self.render_text_input('event_end', (self.event_end.strftime(self.DATETIME_FORMAT) if event_end else None)),
tags.script(r"""
$(document).ready(function () {
@@ -147,6 +380,15 @@
event_start.datetimepicker();
event_end.datetimepicker();
+/* Buggy shit doesn't work
+
+ {
+ beforeShow: function (input, inst) {
+ // copy default value from event_start
+ event_end.datetimepicker("option", "defaultDate", event_start.datetimepicker("getDate"));
+ }
+ }
+
event_start.change(function () {
// copy value as default
var start_date = event_start.datetimepicker("getDate");
@@ -156,28 +398,55 @@
// init default as well
event_start.change();
+*/
});""" ),
)
- def render (self) :
- return tags.form(action=self.build_url(NewOrderView), method='POST')(
- tags.h1(u"Uusi tilaus"),
+ 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>.
+ """
+ return tags.li(class_='field')(
+ tags.label(title, for_=name),
+
+ inputs,
+
+ # XXX: somewhere where we tag these!
+ # tags.span("Error!"),
+
+ tags.p(description),
+ )
+
+
+ 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.fieldset(
tags.legend(u"Tilaaja"),
tags.ol(
- self.render_form_field(u"Tilaaja", u"Tilaavan yhdistyksen/henkilön nimi", 'customer_name', self.render_customer_input()),
-
- self.render_form_field(u"Yhteyshenkilö", u"Yhteyshenkilön nimi, jos eri kun tilaaja", 'contact_name', self.render_contact_input()),
-
- self.render_form_field(u"Puhelin", u"Yhteyshenkilön puhelinnumero", 'contact_phone', (
- tags.input(type='text', name='contact_phone')
+ self.render_form_field('customer_name', u"Tilaaja", u"Tilaavan yhdistyksen/henkilön nimi", (
+ self.render_customer_input()
)),
- self.render_form_field(u"Sähköposti", u"Yhteyshenkilön sähköpostiosoite", 'contact_email', (
- tags.input(type='text')
+ 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"Puhelin", u"Yhteyshenkilön puhelinnumero", (
+ self.render_text_input('contact_phone', self.contact_phone)
+ )),
+
+ self.render_form_field('contact_email', u"Sähköposti", u"Yhteyshenkilön sähköpostiosoite", (
+ self.render_text_input('contact_email', self.contact_email)
)),
),
),
@@ -186,18 +455,61 @@
tags.legend(u"Tapahtuma"),
tags.ol(
- self.render_form_field(u"Tapahtuma", u"Tapahtuman lyhyt nimi", 'event_name', (
- tags.input(type='text', name='event_name')
+ 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(u"Lisätiedot", u"Tapahtuman tarkemmat tiedot", 'event_description', (
- tags.textarea("", rows=8, name='event_description')
+ 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(u"Ajankohta", u"Tapahtuman ajankohta (kamat noudetaan - palautetaan)", 'event_start', self.render_event_input()),
+ self.render_form_field('event_start', u"Ajankohta", u"Tapahtuman ajankohta (kamat noudetaan - palautetaan)", (
+ self.render_event_input()
+ )),
),
),
- tags.input(type='submit', value="Tallenna"),
+ tags.input(type='submit', value=submit),
)
+
+class OrdersView (PageHandler) :
+ def render (self) :
+ return tags.h1("Orders list")
+
+class OrderView (PageHandler) :
+ def render (self, id) :
+ return tags.h1("Order info for #%d" % (id, ))
+
+class NewOrderView (PageHandler) :
+ """
+
+ """
+
+ def render (self) :
+ if self.POST :
+ print self.POST
+
+ #
+
+ # if we've gotten this far, then we can create it!
+ sql = db.insert(db.orders).values(
+ customer = customer_id,
+ contact = contact_id,
+
+ event_name = event_name,
+ event_description = event_description,
+ event_start = event_start,
+ event_end = event_end,
+ )
+
+ # go!
+ order_id, = self.app.insert(sql)
+
+ # ok, we don't need the /new URL anymore, we can just show the order page
+ return self.redirect_for(OrderView, id=order_id)
+
+ # render form
+ return self.render_form()
+
+