from types import DictionaryType, ListType, TupleType, StringType, UnicodeType
import sys
import BaseValidatorClasses
try:
DEFAULT_ENCODING = sys.getdefaultencoding()
except:
DEFAULT_ENCODING = 'ascii'
class Form:
''' '''
def __init__( self, action="", method="post", validators = [], languageStandard='XHTML', encoding=DEFAULT_ENCODING, decoding_strict=1 ):
'''The Form constructor basically sets up a bunch of containers and switches.
definition is currently not used, but may be a way to have a form build itself later.
action is where the post goes (servlet, etc.) method is either get or post (defaults to post).
languageStandard sets the markup language the generated tags are expected to
conform to ( XHTML | HTML )
encoding is the kind of ascii (encoded unicode or other character set) that we're expecting...
assuming it's not just plain ascii already. For example, if you're expecting utf-8 from a form, set this to
utf-8 when you make a new Form() instance.
NB: To get this to work, your web server will have to send the html in the right encoding; if using apache,
use AddDefaultCharset NNNN (see http://httpd.apache.org/docs/mod/core.html#adddefaultcharset)
decoding_strict is a flag that tells the form to raise an exception when it gets confused decoding encoding strings...
if this is turned off, it simply quietly passes through perhaps bogus data, and you'll get garbage characters from your
fields, BUT it won't crash...
'''
self._formid = None
self._fields = [ ]
self._action = action
self._validators = validators[:]
self._domID = ''
self._encoding = encoding.lower()
self._inputData = { }
self._validated = None
self._error = None
self._strictFlag = decoding_strict
if method.lower() == 'get':
self._postWithGet = 1
else:
self._postWithGet = None
def process( self, inputData ):
'''inputData is a dictionary, where keys are the field names.
InputData is probably from post and querystring variables from a servlet,
but so long as it's a dictionary with keys that correspond to fields,
and values that fields can understand, it's kosher.'''
self._validated = 1
self._error = None
if inputData:
assert type(inputData) == DictionaryType, "process inputData must be a dictionary"
if self._encoding != 'ascii':
for key in inputData.keys():
if type(inputData[key]) is type(['a','b','c']):
inputData[key] = map( self._decodeUnicode, inputData[key] )
elif type(inputData[key]) is type('string'):
inputData[key] = self._decodeUnicode( inputData[key] )
self._inputData = inputData
for field in self.fields():
field._processRawValues( inputData )
try:
for v in self._validators:
if v.validatePartialForm() or not self.errors():
v.validate( self.values() )
except BaseValidatorClasses.InvalidField, msg:
self._error = str(msg)
if self._error or self.errors():
self._validated = 0
def _decodeUnicode( self, value ):
'''private method that attempts to guess if a given value is in fact encoded unicode, and if so, decodes it.
If it is NOT encoded unicode, or it fails, it simply returns the original value.'''
if isinstance(value, UnicodeType):
return value
try:
decodedValue = unicode( value, self._encoding )
if len(value) != len(decodedValue):
return decodedValue
else:
return value
except UnicodeError:
if self._strictFlag:
raise UnicodeError, '''Failed trying to decode the string value '%s' from encoding '%s'. (Current sys.defaultencoding is '%s'). Perhaps the source of this value is using another encoding? (Check your apache/browser combo).''' % ( value, self._encoding, sys.getdefaultencoding() )
else:
return value
def _setFormID( self, number ):
'''This is cruft to allow us to re-associate posted fields with the correct form class instance.
A servlet's forms are stored in a list, and each is given a numeric identifier. This identifier
is printed to html in a hidden form field. When a post happens, FormKit can recover that field
and thereby know which form to process.'''
self._formid = number
def seed( self, data={ } ):
'''seed sends values (defaults) to the self.fields() field classes. data is a dictionary,
with the keys being the names of the field class instances. These values are NOT validated
or converted (but will be after they go through a submit-query and the resulting .process()).
Multiple calls to this method are allowed, and encouraged. If a second (or any subsequent)
call sends the same key as a previous call, it will stomp the values (as expected).
SPECIAL NOTE! It may be obvious, but empty strings DO stomp former values. This is good;
empty strings from a database SHOULD stomp "real" data from querystring or whatever.
'None' values DO NOT stomp previous values (as dictated by class Field )
'''
assert type(data) == DictionaryType, "If you pass in seed data, it must be a dictionary"
for field in self.fields():
field._processDefaults( data )
def addField( self, field ):
'''adds a field object into the form'''
self._fields.append( field )
def removeAllFields( self ):
'''removes all fields in the form object'''
self.reset()
self._fields = []
def removeField( self, fieldName ):
'''removes the field object called "fieldName" from the form; be careful with this method!'''
index = -1
count = 0
for field in self._fields:
if field.name() == fieldName:
index = count
count += 1
if index > 0:
self._fields.pop(index)
else:
raise 'No field %s found' % fieldName
def reset( self ):
'''resets form to empty field values, clears errors, clears defaults, sets some flags back to off'''
self._inputData = { }
self._validated = self._converted = None
for field in self.fields():
field._reset()
def addFormValidator( self, validator ):
'''Adds a form validator'''
self._validators.append(validator)
def setDOMID( self, string='' ):
'''For setting a DOM id (no internal consequences other than the id='<string>' appears in the form start tag)'''
self._domID = string
def setMethod( self, method ):
'''set the post method: get or post'''
method = method.lower()
assert method in ('get','post'), "You can only use 'get' or 'post'"
if method == 'get':
self._postWithGet = 1
def setAction( self, target ):
'''changes the form action (URL)'''
assert type(target) == StringType, "target should be a string"
self._action = target
def setEncoding( self, encoding=DEFAULT_ENCODING ):
'''sets the encoding type for anticipated data (see __init__ docstrings)'''
self._encoding = encoding
def setTabOrder( self, order=None ):
'''sets the tab order of the fields (using the tabindex parameter to html tags). Default is
to set them in the order that they were added to the form; alternatively, pass in a list of integers
that is the same length as the number of fields, or simply poke at each field with field.setTabIndex()'''
fields = self.fields()
if order:
assert len(order) == len(fields), "Any passed in order sequence must have the same number of members as the number of fields in the form"
for i in range(len(order)):
fields[i].setTabIndex( order[i] )
else:
for i in range(len(fields)):
fields[i].setTabIndex( i+1 )
def isSuccessful( self ):
return self._validated
def __getattr__( self, fieldName ):
'''convenience method that allows us to refer to fields of form
with .thing notation. Just returns the field class instance itself.'''
return self.field( fieldName )
def field( self, fieldName ):
'''returns the asked for field class instance.'''
for field in self.fields():
if field.name() == fieldName:
return field
return None
def fields( self ):
'''returns all of the fields as a list of field class instances; this list
preserves the order in which the fields were added'''
return self._fields
def defaults( self ):
'''returns all of the default values for all fields as a dictionary'''
defs = {}
for field in self.fields():
defs.update( { field.name() : field.default() } )
return defs
def rawValues( self ):
'''returns dict raw (un-validated, un-converted, etc.) values from the form'''
return self._inputData
def value( self, fieldName ):
'''returns the VALUE of the specified field class instance.'''
field = self.field( fieldName )
return unicode(field.value())
def values( self ):
'''returns all of the values for all fields as a dictionary'''
values = {}
for field in self.fields():
values.update( { field.name() : field.value() } )
return values
def tag( self, fieldName ):
'''returns the html <input> tag string of the asked-for field class instance.'''
field = self.field( fieldName )
return field.tag()
def error(self):
'''Form-wide error message access method (returns None if no error)'''
if self._error:
return self._error
else:
return None
def errors( self ):
'''returns all of the error strings for all fields as a dictionary.
The key is the field name, the error is the value.'''
errors = {}
for field in self.fields():
if field.error():
errors.update( { field.name() : field.error() } )
return errors
def startTags( self ):
'''returns the html that's required to start a form; basically an opening <form> tag with attributes'''
if self._postWithGet:
method = "get"
else:
method = "post"
encoding = ''
for field in self._fields:
try:
temp = field._requireMultipartEncoding()
encoding = ''' enctype="multipart/form-data"'''
except AttributeError:
pass
if self._domID:
return """<form action="%s"%s method="%s" name="%s" id="%s">""" % ( self._action, encoding, method, self._domID, self._domID )
else:
return """<form action="%s"%s method="%s">""" % ( self._action, encoding, method )
def endTags( self ):
'''returns the html that's required to end a form; basically a closing </form> tag, but
including a hidden field to establish the form's name (for multiple forms)'''
return """<input type="hidden" name="__formID__" value="%s" />\n</form>""" % self._formid
def dump( self ):
'''returns a big string of the html required to SIMPLY paint the form for quick-and-dirty pages and for debugging'''
tmpStr = self.startTags() + "\n"
tmpStr += '''<table cellpadding="10">\n'''
for field in self.fields():
tmpStr += '''<tr>\n\t<td valign="top" align="right">%s</td>\n''' % field.label()
tmpStr += '''\t<td>'''
if field.error():
tmpStr += '''<span style="color:red;">%s</span><br /> ''' % field.error()
tmpStr += field.tag()
tmpStr += "</td>\n</tr>\n"
tmpStr += '''</table>'''
tmpStr += self.endTags() + "\n"
return tmpStr