forms: split off generic forms code from orders.py
authorTero Marttila <terom@fixme.fi>
Fri, 07 Jan 2011 02:46:29 +0200
changeset 34 260413f89ba9
parent 33 471837eb3d96
child 35 13b5dd3a7a5f
forms: split off generic forms code from orders.py
svv/forms.py
svv/orders.py
--- /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