--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svv/forms.py Fri Jan 07 02:46:29 2011 +0200
@@ -0,0 +1,401 @@
+"""
+ Form rendering and handling
+"""
+
+from svv.html import tags
+from svv import database as db
+
+import collections
+import datetime
+import logging
+
+try :
+ # Python2.6 stdlib
+ import json
+
+except ImportError :
+ # simplejson, API should be similar
+ import simplejson as json
+
+
+log = logging.getLogger('svv.forms')
+
+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()
+
+
+
--- a/svv/orders.py Fri Jan 07 02:11:52 2011 +0200
+++ b/svv/orders.py Fri Jan 07 02:46:29 2011 +0200
@@ -4,21 +4,13 @@
"""
from svv.controllers import PageHandler, DocumentHandler
+from svv.forms import FormError, BaseForm, json
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')
@@ -110,384 +102,6 @@
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