## FunFormKit, a Webware Form processor ## Copyright (C) 2001, Ian Bicking ## ## This library is free software; you can redistribute it and/or ## modify it under the terms of the GNU Lesser General Public ## License as published by the Free Software Foundation; either ## version 2.1 of the License, or (at your option) any later version. ## ## This library is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## Lesser General Public License for more details. ## ## You should have received a copy of the GNU Lesser General Public ## License along with this library; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## ## NOTE: In the context of the Python environment, I interpret "dynamic ## linking" as importing -- thus the LGPL applies to the contents of ## the modules, but make no requirements on code importing these ## modules. """ FunFormKit, a Webware Form processor Copyright (C) 2001, Ian Bicking This defines a Form mixin with a number of classes used to define the fields in forms. A number of classes are declared in this module, but it is fairly safe to import * from this module. See the documentation for the Form class for more information. """ try: from MiscUtils import NoDefault except ImportError: class NoDefault: pass import threading, cgi from Signature import Signature try: from cStringIO import StringIO except ImportError: from StringIO import StringIO import string from types import * True, False = (0==0), (1==0) __all__ = ['FormServlet', 'FormDefinition'] def identity(v): return v class FormError(Exception): """Some runtime form errors raise this exception.""" pass class FormServlet: def __init__(self, formDefinitions): if type(formDefinitions) not in (type([]), type(())): formDefinitions = [formDefinitions] self._formDefinitions = {} for formDef in formDefinitions: if self._formDefinitions.has_key(formDef.name()): raise TypeError, "Two or more forms for a single servlet share the same name: %s" % repr(formDef.name()) self._formDefinitions[formDef.name()] = formDef def formDefinitions(self): """ Accessor method for self._formDefinitions """ return self._formDefinitions def renderableForm(self, formDefinition=None, defaults=None, rawDefaults=None, optionSet=None): """ Returns a form, curried with the current state of the form -- any validation errors, default values, options, etc. This form will be more easily displayed. """ if defaults is None: defaults = {} if optionSet is None: optionSet = {} if formDefinition is None: if len(self._formDefinitions) > 1: raise TypeError, "You must provide a formDefinition argument if your servlet implements more than one form" formDefinition = self._formDefinitions.values()[0] if type(formDefinition) is type(""): formDefinition = self._formDefinitions[formDefinition] if self._erroneousRequest and self._erroneousFormName == formDefinition.name(): requestFields = self._erroneousRequest.fields() errors = self._errors else: requestFields = rawDefaults errors = {} return RenderableForm(formDefinition, requestFields=requestFields, defaults=defaults, optionSet=optionSet, errors=errors) def processForm(self, transaction=None): """Processes any form data, and returns two values: formProcessed, data formProcessed will be true if the form was submitted with no validation errors. If False, data will be 'noform' or 'invalid' -- noform if no form has yet been submitted, invalid if invalid. If the form was correctly submitted, then data will be whatever was returned by the processing function. You may wish to redirect the page, or call another function if the form was submitted properly. If it was not, you should display the form (via .renderableForm()) """ self._erroneousRequest = None self._errors = None self._erroneousFormName = None if transaction: req = transaction.request() else: req = self.request() currentForm = None if req.hasField('_formID_'): formID = req.field('_formID_') if formID[:7] == "dynamic": currentForm = MutableFormDefinitionStore[int(formID[-6:])] else: currentForm = self._formDefinitions[req.field('_formID_')] if not currentForm: return False, 'noform' valid = False for fieldName, field in currentForm.fields().items(): if field.isSubmit() and field.suppressValidation() and \ req.field(field.name(), None): valid = True data = req.fields() actionMethod = field.methodToInvoke() invokeAsFunction = field.invokeAsFunction() noneAsDefault = field.noneAsDefault() break if not valid: valid, data, actionMethod, invokeAsFunction, noneAsDefault = \ currentForm.validateConvert(req.fields()) if valid: if actionMethod: method = getattr(self, actionMethod) else: method = lambda data: data if not invokeAsFunction: value = method(data) else: value = invokeFunctionWithDict(method, data, noneAsDefault=noneAsDefault) return True, value else: self._erroneousRequest = req self._erroneousFormName = currentForm.name() self._errors = data return False, 'invalid' class RenderableForm: """ This is kind of a wrapper around the FormDefinition. FormDefinition is meant to be static across calls, not holding any real state. So RenderableForm is an object that is created and collected all the time. It will be the primary interface you use to generate the form -- which you can do with .htFormTable() (easy and quick), Cheetah (flexible and fairly easy too), or just with self.write(). To use Cheetah, you should be able to put the RenderableForm that you get from FormServlet in the namespace, and use $form.username, $form.username.error, $form.error, etc. For self.write(), use things like rf["username"], rf["username"].error(), rf.error(), etc. Be sure to start and end the form properly! Use $form.start or self.write(rf.start()). htFormTable() deals with this itself, of course. """ def __init__(self, formDefinition, requestFields = None, defaults = {}, errors = {}, optionSet = {}): """The arguments are passed pretty much directly through from FormServlet.renderableForm().""" self._formDefinition = formDefinition self._requestFields = requestFields self._defaults = defaults self._errors = errors self._optionSet = optionSet def __getattr__(self, item): """You *never* get the actual fields back from RenderableForm, but rather you get the fields wrapped in a RenderableField class. There are several accessors that follow this.""" ## Some Python 2.1 thing... I don't really understand why ## these attributes are called. Should fix/figure out at ## some point. if item in ['__del__', '__eq__']: raise AttributeError else: return self.renderableField(item) def __getitem__(self, item): return self.renderableField(item) def renderableField(self, name): field = self._formDefinition.field(name) default = self._defaults.get(name, '') options = self._optionSet.get(name, {}) return RenderableField(self, field, default, options) def error(self): if self._errors.get('form'): return self._formDefinition._errorFormatter(self._errors['form']) else: return '' def errorText(self): return self._errors.get('form', '') def fieldList(self, fieldNames): return map(lambda x, s=self: s.renderableField(x), fieldNames) def fields(self): return self.fieldList(self._formDefinition.fieldNames()) def visibleFields(self): return self.fieldList(self._formDefinition.visibleFieldNames(optionSet=self._optionSet)) def hiddenFields(self): return self.fieldList(self._formDefinition.hiddenFieldNames(optionSet=self._optionSet)) def submitFields(self): return self.fieldList(self._formDefinition.submitFieldNames(optionSet=self._optionSet)) def htStartForm(self, includeHiddenFields = False): return self._formDefinition.htStartForm( requestFields=self._requestFields, defaults=self._defaults, optionSet=self._optionSet, includeHiddenFields=includeHiddenFields) def htEndForm(self): return self._formDefinition.htEndForm( None, self._optionSet) def htHiddenFields(self): result = [] for fieldName in self._formDefinition.fieldNames(): field = self._formDefinition.field(fieldName) if field.isHidden(options=self._optionSet.get(fieldName, {})): result.append(str(self[fieldName])) return string.join(result, "\n") def start(self, includeHiddenFields=False): return self.htStartForm(includeHiddenFields) def end(self): return self.htEndForm() def hidden(self): return self.htHiddenFields() def htFormTable(self, bgcolor=None): out = StringIO() out.write(self.htStartForm(True)) out.write(self.error()) if bgcolor: out.write('\n' % bgcolor) else: out.write('
\n') for field in self.fields(): if field.isHidden(): continue elif field.isSubmit(): out.write('\n' % (field.error(), field.html())) else: out.write('\n\n') out.write('
%s%s
%s\n' % field.description()) self._writeField(field, out) out.write('
') out.write(self.htEndForm()) return out.getvalue() def _writeField(self, field, out): if field.repetitions() > 1: out.write(field.repeatingHidden()) if field.isCompound(): out.write('') for subField in field.repeatingFields()[0].compoundFields(): out.write('\n' % subField.description()) for subField in field.repeatingFields(): out.write('\n') for subSubField in subField.compoundFields(): out.write('\n') out.write('\n') out.write('
%s
') self._writeField(subSubField, out) out.write('
\n') else: first = 1 for subField in field.repeatingFields(): if first: first = 0 else: out.write('
\n') self._writeField(subField, out) elif field.isCompound(): out.write('') fields = field.compoundFields() out.write('\n') for subField in fields: out.write('\n' % subField.description()) out.write('\n\n') for subField in fields: out.write('\n') out.write('\n
%s
') self._writeField(subField, out) out.write('
\n') else: out.write(str(field.error())) out.write(str(field.html())) def htFormLayout(self, layout, descriptionClass=None, bgcolor=None, spacing=0): out = StringIO() out.write(self.htStartForm(True)) out.write(self.error()) if descriptionClass: c1 = '' % descriptionClass c2 = '' else: c1 = c2 = '' if bgcolor: bgcolor = ' bgcolor="%s"' % bgcolor else: bgcolor = '' if spacing: spacing = '%s\n' % (' ' * spacing) else: spacing = '' for line in layout: out.write('' % bgcolor) if type(line) is StringType and line \ and line[0] in (':', '='): if line[0] == '=': out.write(line[1:]) else: field = self.renderableField(line[1:]) self._writeField(field, out) out.write('
\n') continue if type(line) not in (ListType, TupleType): line = [line] fields = [] first = 1 for name in line: if name.endswith('*'): star = 1 name = name[:-1] else: star = 0 field = self.renderableField(name) fields.append(field) if first: first = 0 else: out.write(spacing) if field.isSubmit(): continue out.write('\n') out.write('\n') first = 1 for field in fields: if first: first = 0 else: out.write(spacing) out.write('\n') out.write('
%s%s%s' % (c1, field.description(), c2)) if star: out.write('*') out.write('
') self._writeField(field, out) out.write('

\n') out.write(self.htEndForm()) return out.getvalue() class RenderableField: """ RenderableField is a wrapper around a field, tightly coupled with the RenderableForm that created it. RenderableField is a close friend (really just a wrapper) of RenderableForm. So it uses private variables at will. """ def __init__(self, renderableForm, field, default, options, nameMap=identity): self._rf = renderableForm self._field = field self._default = default self._options = options self._nameMap = nameMap def html(self): value = self._field.html(requestFields = self._rf._requestFields, defaults = {self._field.name(): self._default}, optionSet = {self._field.name(): self._options}, nameMap=self._nameMap) return value def __str__(self): return self.html() def error(self): error = self.errorText() if error: return self._rf._formDefinition._errorFormatter(error) else: return '' def getError(self, errors, name): if not errors: return None if name.find(':') != -1: errors = self.getError(errors, name[:name.find(':')]) return self.getError(errors, name[name.find(':')+1:]) elif name.find('.') != -1: errors = self.getError(errors, name[:name.find('.')]) if not errors: return None try: return errors[int(name[name.find('.')+1:])] except IndexError: return '' else: return errors.get(name) def repeatingHidden(self): return self._field.repeatingHidden( self._default, self._options, self._nameMap) def repeatingFields(self): assert self.repetitions() > 1 values = [] for field, default, options, nameMap in \ self._field.repeatingFieldAccessors(self._default, self._options, self._nameMap): values.append(RenderableField(self._rf, field, default, options, nameMap)) return values def compoundFields(self): assert self.isCompound() values = [] for field, default, options, nameMap in \ self._field.fieldAccessors(self._default, self._options, self._nameMap): values.append(RenderableField(self._rf, field, default, options, nameMap)) return values def repetitions(self): return self._field.repetitions(self._options) def errorText(self): name = self._nameMap(self._field.name()) return self.getError(self._rf._errors, name) def description(self): return self._field.description() def name(self): return self._nameMap(self._field.name()) def isSubmit(self): return self._field.isSubmit() def isHidden(self): return self._field.isHidden(options=self._rf._optionSet.get(self._nameMap(self._field.name()), {})) def isCompound(self): return self._field.isCompound() def option(self, name, default=None): return self._field.option(name, options=self._rf._optionSet.get(self._fieldName, {}), default=default) def defaultFormatError(value): """ It prints the error message in red with white text. """ if type(value) is type([]): value = string.join(filter(None, value), "
\n") return '%s
' \ % value nameCount = 1 nameCountLock = threading.Lock() def getName(): global nameCount, nameCountLock nameCountLock.acquire() name = 'form%i' % nameCount nameCount = nameCount + 1 nameCountLock.release() return name class FormDefinition: """ A FormDefinition declares what fields the form will have, among other details about the form. You shouldn't be creating these definitions dynamically. (For experimental support for doing that, look at MutableFormDefinition) @@ 2001-07-21 ib: get rid of the handlerServletURL or something -- as it is, it can't deal with changing adapters, or generally being reflective about the location Webware is installed. """ def __init__(self, handlerServletURL, fields, formValidators=[], method="POST", enctype=None, name=None, errorFormatter=defaultFormatError): """ * handlerServletURL is the URL of servlet that handles the results of the form. * fields is a list of fields (see the Fields module for more about them). * formValidators is a list of validators. See Validator.FormValidators for more about their interface. name should be unique within the servlet. enctype you can safely ignore -- the
will automatically be set if you use a FileField. method is obvious. """ self._handlerServletURL = handlerServletURL if not name: name = getName() self._name = name self._method = method self._formValidators = formValidators self._fields = {} self._fieldOrder = [] self._submitFields = {} self._errorFormatter = errorFormatter for field in fields: name = field.name() self._fields[name] = field self._fieldOrder.append(name) if not enctype and field.preferedEnctype(): enctype = field.preferedEnctype() if field.isSubmit(): self._submitFields[name] = field self._enctype = enctype def formID(self): return self._name def submitFields(self): return self._submitFields def fieldNames(self): return self._fieldOrder def visibleFieldNames(self, optionSet={}): return filter(lambda name, self=self, op=optionSet: not self._fields[name].isHidden(options=op.get(name, {})), self._fieldOrder) def hiddenFieldNames(self, optionSet={}): return filter(lambda name, self=self, op=optionSet: self._fields[name].isHidden(options=op.get(name, {})), self._fieldOrder) def submitFieldNames(self, optionSet={}): return filter(lambda name, self=self, op=optionSet: self._fields[name].isSubmit(options=op.get(name, {})), self._fieldOrder) def field(self, fieldName): return self._fields[fieldName] def fields(self): return self._fields def name(self): return self._name def htStartForm(self, requestFields, defaults, optionSet, includeHiddenFields=False): """ Returns the beginning part of a form. If includeHiddenFields is true, will also include input elements for any hidden fields in the form. """ if self._enctype: encString = ' enctype="%s"' % self._enctype else: encString = '' onSubmitList = [] for field in self.fields().values(): s = field.onSubmit() if s: onSubmitList.append(s) if onSubmitList: onSubmit = ' onSubmit="%s"' % string.join(onSubmitList, '; ') else: onSubmit = '' if optionSet.get('form', {}).get('handlerServletURL'): handlerServletURL = optionSet['form']['handlerServletURL'] else: handlerServletURL = self._handlerServletURL formString = ''' \n''' % \ (handlerServletURL, self._method, self._name, onSubmit, encString, self.formID()) jsDict = {} for fieldName, field in self._fields.items(): jsDict.update(field.formJavascript(optionSet.get(fieldName, {}))) if jsDict: formString = '%s\n' % (formString, '\n\n'.join(jsDict.values())) if includeHiddenFields: return formString + self.htHiddenFields(requestFields, defaults, optionSet) return formString def htHiddenFields(self, requestFields, defaults, optionSet): result = [] for fieldName, field in self._fields.items(): if field.isHidden(options=optionSet.get(fieldName, {})): result.append(field.html(optionSet=optionSet, defaults=defaults, requestFields=requestFields)) return string.join(result, '\n') def htEndForm(self, defaults, optionSet): return '
\n' def mutable(self): """This is the only way to modify a form definition""" return MutableFormDefinition(self._handlerServletURL, self._fields, self._fieldOrder, self._formValidators, self._method, self._enctype, self._name, self._errorFormatter) def validateConvert(self, request): allGood = True errors = {} data = {} methodPass, invokeAsFunction, noneAsDefault = None, False, False for fieldName, field in self._fields.items(): good, value = field.attemptConvert(request) if good: data[fieldName] = value if field.isSubmit() and \ (value or (field.isDefaultSubmit() and methodPass is None)): methodPass = field.methodToInvoke() invokeAsFunction = field.invokeAsFunction() noneAsDefault = field.noneAsDefault() assert not field.suppressValidation(), "Uh oh, validation didn't get suppressed like promised" else: allGood = False errors[fieldName] = value for formValidator in self._formValidators: #if not allGood and not formValidator.validatePartialForm(): # continue if allGood: formErrors = formValidator.validate(data) elif formValidator.validatePartialForm(): formErrors = formValidator.validatePartial(data) else: formErrors = {} if not formErrors: continue allGood = False if type(formErrors) is type(""): formErrors = {"form": formErrors} assert type(formErrors) is type({}), \ "form validators should return a string or dictionary" for fieldName, errorMessage in formErrors.items(): if errors.has_key(fieldName): if type(errors[fieldName]) is not type([]): errors[fieldName] = [errors[fieldName]] errors[fieldName].append(errorMessage) else: errors[fieldName] = errorMessage return allGood, errors or data, methodPass, invokeAsFunction, \ noneAsDefault class MutableFormDefinition(FormDefinition): """A FormDefinition that can be changed. Create with .mutable() from a real FormDefinition.""" def __init__(self, handlerServletURL, fieldDict, fieldOrder, formValidators, method, enctype, name, errorFormatter=defaultFormatError): self._handlerServletURL = handlerServletURL self._fields = fieldDict.copy() self._fieldOrder = fieldOrder self._formValidators = formValidators self._method = method self._enctype = enctype self._errorFormatter = errorFormatter self._name = name self._serial = generateSerial() MutableFormDefinitionStore[self._serial] = self def formID(self): return 'dynamic_%s_%06i' % (self._name, self._serial) def addField(self, field): self._fields[field.name()] = field self._fieldOrder.append(field.name()) def removeField(self, fieldName): if self._fields.has_key(fieldName): del self._fields[fieldName] self._fieldOrder.remove(fieldName) def replaceField(self, field): self._fields[field.name()] = field FormSerialLock = threading.Lock() FormSerial = 0 MutableFormDefinitionStore = {} def generateSerial(): global FormSerialLock, FormSerial FormSerialLock.acquire() FormSerial = FormSerial + 1 serial = FormSerial FormSerialLock.release() return serial def invokeFunctionWithDict(func, dict, noneAsDefault = False): sig = Signature(func) dict = dict.copy() args = sig.ordinary_args() defaults = sig.defaults() a = [] # position args kw = {} # keyword args for arg in args: if not dict.has_key(arg): if not defaults.has_key(arg): raise TypeError, "function argument name not contained in FormDefinition: %s" % arg else: if defaults.has_key(arg): if dict[arg] is not None or not noneAsDefault: kw[arg] = dict[arg] del dict[arg] else: a.append(dict[arg]) del dict[arg] if sig.special_args().has_key('keyword'): kw.update(dict) else: assert not dict, "function does not use arguments from FormDefinition: %s" % dict.keys() apply(func, a, kw)