# HG changeset patch # User Tero Marttila # Date 1293063754 -7200 # Node ID 90a3c570c227c06242b76a2c2ef7956e4cecf1a3 # Parent 4bdb45071c89e13af55cac436b63fecc00eeb38a NewOrderView now works ~fully, including error handling diff -r 4bdb45071c89 -r 90a3c570c227 static/forms.css --- a/static/forms.css Thu Dec 23 01:07:42 2010 +0200 +++ b/static/forms.css Thu Dec 23 02:22:34 2010 +0200 @@ -4,6 +4,16 @@ * Inspiration taken from http://articles.sitepoint.com/article/fancy-form-design-css */ +/* General form errors, and field-specific error lists will display in red */ +form ul.errors, +form ul.errors a +{ + list-style-type: disc; + color: #C00; + + margin: 0em 1em 1em; +} + fieldset { /* A fieldset will not be completely full-width, and will be vertically separated from adjacent fieldsets*/ @@ -66,6 +76,14 @@ font-size: small; } +/* A inside the label is an error message */ +fieldset label strong +{ + color: #C00; + + text-transform: upppercase; +} + /* The field element are consistently aligned */ form input, form textarea @@ -76,6 +94,13 @@ padding: 4px; } +/* A field that failed validation is highlighted */ +form .failed input, +form .failed textarea +{ + border: thin solid red; +} + form select, form input[type=submit] { diff -r 4bdb45071c89 -r 90a3c570c227 static/layout.css --- a/static/layout.css Thu Dec 23 01:07:42 2010 +0200 +++ b/static/layout.css Thu Dec 23 02:22:34 2010 +0200 @@ -36,6 +36,7 @@ float: left; /* Evil? */ + /* XXX: this seems to break #fragment links */ padding-bottom: 2000px; margin-bottom: -2000px; diff -r 4bdb45071c89 -r 90a3c570c227 svv/controllers.py --- a/svv/controllers.py Thu Dec 23 01:07:42 2010 +0200 +++ b/svv/controllers.py Thu Dec 23 02:22:34 2010 +0200 @@ -137,12 +137,8 @@ ) # perform the actual rendering (run generators etc.) - html_text = unicode(html.document(head, layout)) + return unicode(html.document(head, layout)) - # response object - # XXX: unicode? - return Response(html_text, mimetype='text/html') - def render (self, **url_values) : """ Render full page HTML @@ -158,15 +154,23 @@ return response def respond (self, url_values) : - response = None + """ + Build and return a response from the following steps: - if self.request.form : - # process POST data for e.g. redirect - response = self.process(**url_values) + * process() + * render() -> render_content() as HTML + """ + + # process e.g. POST data for e.g. redirect + response = self.process(**url_values) if not response : # render page HTML - response = self.render(**url_values) + html = self.render(**url_values) + + # response object + # XXX: unicode? + return Response(html, mimetype='text/html') # ok return response diff -r 4bdb45071c89 -r 90a3c570c227 svv/orders.py --- a/svv/orders.py Thu Dec 23 01:07:42 2010 +0200 +++ b/svv/orders.py Thu Dec 23 02:22:34 2010 +0200 @@ -9,6 +9,7 @@ import datetime import logging +import collections log = logging.getLogger('svv.orders') @@ -17,13 +18,16 @@ A user-level error in a form field """ - def __init__ (self, field, error) : + 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) @@ -43,6 +47,9 @@ self.app = app + # accumulated errors + self.errors = collections.defaultdict(list) + def defaults (self) : """ Update our attributes with default values @@ -78,11 +85,11 @@ Returns the value as a str, or default """ - if name in self.post : - return self.post[name] + if name in self.data : + return self.data[name] elif required : - raise FormError(name, "Required field") + raise FormError(name, None, "Required field") else : return default @@ -106,7 +113,7 @@ value = unicode(value) except UnicodeDecodeError : - raise FormError(name, "Failed to decode Unicode characters") + raise FormError(name, value, "Failed to decode Unicode characters") if strip : value = value.strip() @@ -129,7 +136,7 @@ return int(value) except ValueError : - raise FormError(name, "Must be a number") + raise FormError(name, value, "Must be a number") DATETIME_FORMAT = "%d.%m.%Y %H:%M" @@ -149,7 +156,7 @@ return datetime.datetime.strptime(value, format) except ValueError, ex : - raise FormError(name, "Invalid date/time value: " + str(ex)) + raise FormError(name, value, "Invalid date/time value: " + str(ex)) def process_multifield (self, table, id, fields) : """ @@ -170,7 +177,7 @@ sql = db.select(columns, (id_col == id_value)) - for row in sql : + for row in self.app.query(sql) : # XXX: sanity-check row values vs our values # new values @@ -183,7 +190,7 @@ else : # not found! - raise FormError(id_name, "Item selected does not seem to exist") + 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)) @@ -220,47 +227,88 @@ 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')), - ), - ) + try : + customer_id = self.process_integer_field('customer_id') + customer_name = self.process_string_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) : """ 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_string_field('contact_name') + contact_phone = self.process_string_field('contact_phone') + contact_email = self.process_string_field('contact_email') + contact_customer = customer_id - 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), - ), - ) + 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, 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_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') - 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') + 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) + 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 @@ -270,7 +318,7 @@ 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) + self.contact_id, self.contact_name, self.contact_phone, self.contact_email, self.contact_customer = self.process_contact(self.customer_id) if self.contact_customer and not self.customer_id : # TODO: be really smart? @@ -278,7 +326,22 @@ # event self.event_name, self.event_description, self.event_start, self.event_end = self.process_event() + + return not self.errors + 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_customer_list (self) : """ @@ -353,10 +416,10 @@ """ # all known customers - customers = self.build_customer_list() + customers = list(self.build_customer_list()) return ( - self.render_select_input('customer_id', customers, self.customer_id), + 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').formSelectPreset({textTarget: $('#customer_name')}); });"), @@ -370,7 +433,7 @@ contacts = self.build_contact_list(self.customer_id) return ( - self.render_select_input('contact_id', ((id, name) for id, name, phone, email in contacts), self.contact_id), + 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(r"$(document).ready(function () { $('#contact_id').formSelectPreset({textTarget: $('#contact_name')}); });"), @@ -419,15 +482,21 @@ Render the label, input control, error note and description for a single field, along with their containing
  • . """ - return tags.li(class_='field')( - tags.label(title, for_=name), + # 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, - # XXX: somewhere where we tag these! - # tags.span("Error!"), - tags.p(description), + + # possible errors + tags.ul(class_='errors')(tags.li(error.message) for error in errors) if errors else None, ) @@ -440,6 +509,18 @@ """ 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"), @@ -494,38 +575,63 @@ class NewOrderView (PageHandler) : """ - + Render form for input, let the user correct their errors, create the order, and redirect out. """ - def render_content (self) : - - form = OrderForm(self.app) - form.defaults() + def create (self, form) : + """ + Create the new order from the given form data, returning the new order's ID + """ - return form.render(action=self.url_for(NewOrderView)) - - # XXX: under construction.. - - if self.POST : - print self.POST - + # if we've gotten this far, then we can create it! + sql = db.insert(db.orders).values( + customer = form.customer_id, + contact = form.contact_id, - # 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 = form.event_name, + event_description = form.event_description, + event_start = form.event_start, + event_end = form.event_end, + ) + + # go! + order_id, = self.app.insert(sql) + + # great + 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) - event_name = event_name, - event_description = event_description, - event_start = event_start, - event_end = event_end, - ) + # 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 - # go! - order_id, = self.app.insert(sql) + else : + # init from defaults + self.form.defaults() + + def render_content (self) : + """ + Render our form + """ - # ok, we don't need the /new URL anymore, we can just show the order page - return self.redirect_for(OrderView, id=order_id) + return ( + tags.h1(u"Uusi tilaus"), + self.form.render(action=self.url_for(NewOrderView)) + ) - -