## 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. """ Fields for use with Forms. The Field class gives the basic interface, and then there's bunches of classes for the specific kinds of fields. It's not unreasonable to do a import * from this module. """ import string, re, urllib, cgi try: from mx import DateTime except ImportError: try: import DateTime except ImportError: DateTime = None try: from PIL import Image except ImportError: try: import Image except ImportError: Image = None try: from cStringIO import StringIO except ImportError: from StringIO import StringIO try: from MiscUtils import NoDefault except ImportError: class NoDefault: pass import Validator import Form import time, md5, whrandom, os from types import * from SimpleHTMLGen import html, Exclude class NotImplementedError(Exception): pass def htmlStr(s): """ str() converts None to 'None'. For an HTML value, a more appropriate translation would be ''. """ if s is None: return '' else: return str(s) def htmlEncode(s): if s is None: return '' else: return cgi.escape(str(s), 1) def identity(val): return val True, False = (1==1), (0==1) class Field: """ Field is the (abstract) base class for field definitions, as used by Form. The initializer for all fields takes the required field name as the first argument. They also take a description argument which is the readable description of the field -- generally used as a label. Also, they take the validators keyword argument, which is a list of validators/converters to be queried/applied about the submitted data. Each field will typically take other keyword arguments as well, as is appropriate. Never assume the keywords appear in any specific order, always use explicit keywords. Fields have a few important external methods: * .html(requestFields=None, optionSet={}, defaults={}): Return the HTML for the field. If you have an option for {fieldName: {"hide": 1}} then .htHidden() will be called, otherwise .htInput(). * .htInput(default, options): Returns an HTML string for the input. If a request object is given, it will use this to fill its default value. This is mostly used so that when a form is reprinted the values the user entered will remain. * .htHidden(default, options): Return a hidden field that uses the requestFields or defaults to load its value. * .attemptConvert(fields): Returns a tuple: the first value is whether the submitted value was valid, the second is either the valid, converted value; or the error message. * .name() and .description(): Accessors. Actual fields should override certain methods: * .__init__(): Take whatever extra information you need to define your field. You may wish to add validators before you call Field.__init__, if it is appropriate. For instance, TextField takes a maxLength argument which it uses for the MAXLENGTH HTML attribute. It then also adds a validator to ensure (server-side) that this maximum length is not exceded. Fields should always ensure correctness and never depend on client-side restrictions. * .htInput() * .valueFromFields(fields): Compound HTML inputs may receive their data in several POST/GET variables. This method takes a dictionary and processes it to return a single value. For instance, the DateField class takes a year, day, and month variable and returns a DateTime object. .valueFromFields can raise the InvalidField exception (see ValidatorConverter) if there is an error while trying to construct a value. * .htHidden() and unvalidatedValueFromFields: Only necessary if you are overriding .valueFromFields(). Fields should have no internal state besides the values given to them at instantiation. They should be reusable and reentrant. """ _capitalRE = re.compile(r'[A-Z]') def __init__(self, name, validators=None, description=None, options=None, extraHTML=None, repeatable=NoDefault, required=False): self._name = name assert htmlEncode(self._name) == self._name, "field names must not contain characters that require quoting in HTML (%s)" % repr(self._name) assert name.find('.') == -1, "field names must not contain '.' (%s)" % repr(name) assert name.find(':') == -1, "field names must not contain ':' (%s)" % repr(name) self._extraHTML = extraHTML if not validators: self._validators = [] else: self._validators = validators if not options: self._options = {} else: self._options = options if not description: # We'll make a description from the name, using the # convention of names that have words seperated by # capital letters. words = [] template = name while 1: match = self._capitalRE.search(template) if match: words.append(template[:match.start()]) template = string.lower(template[match.start()]) + \ template[match.start()+1:] else: words.append(template) break description = string.capitalize(string.join(words, " ")) self._description = description if options and options.get('repeat') is not None: self._repeatable = True assert repeatable is NoDefault or repeatable, \ "You gave the repeatable option, even though you explicitly said this field was not repeatable" else: if repeatable is NoDefault: repeatable = False self._repeatable = repeatable self._required = required def name(self): return self._name def description(self): if self._required: return htmlEncode(self._description) \ + html.font('*', color="#ff0000") else: return htmlEncode(self._description) def option(self, name, options={}, default=None): return options.get(name, self._options.get(name, default)) def html(self, requestFields=None, defaults={}, optionSet={}, nameMap=identity): options = optionSet.get(self.name(), {}) if self._options: newOptions = self._options.copy() newOptions.update(options) options = newOptions if requestFields is not None: defaultValue = self.unvalidatedValueFromFields(requestFields, nameMap=nameMap) else: ## @@: I don't think nameMap is right here, not sure #defaultValue = defaults.get(nameMap(self.name()), '') defaultValue = defaults.get(self.name(), '') if options.get('repeat') and options['repeat'] > 1: assert self._repeatable, "You must indicate a field is repeatable when it is defined" out = [html.input(type="hidden", name=suffixMap(".repetitions", nameMap)(self.name()), value=options['repeat'])] for i in range(options['repeat']): nm = suffixMap(".%i" % i, nameMap) out.append(self.htWidget(defaultValue, options, nameMap=nm)) return out else: return self.htWidget(defaultValue, options, nameMap=nameMap) def repeatingHidden(self, default, options, nameMap): repetitions = self.repetitions(options) assert repetitions > 1, "Field does not repeat" return html.input(type="hidden", name=suffixMap('.repetitions', nameMap)(self.name()), value=repetitions) def repeatingFieldAccessors(self, default, options, nameMap): repetitions = self.repetitions(options) assert repetitions > 1, "Field does not repeat" options = options.copy() options['repeat'] = 1 fields = [] for i in range(repetitions): nm = suffixMap('.%i' % i, nameMap) if not type(default) in (ListType, TupleType): d = default elif i >= len(default): d = None else: d = default[i] fields.append((self, d, options, nm)) return fields def htWidget(self, default, options, nameMap=identity): if options.get("static"): return htmlEncode(default) + \ self.htHidden(default, options, nameMap=nameMap) elif options.get("hide"): return self.htHidden(default, options, nameMap=nameMap) else: return self.htInput(default, options, nameMap=nameMap) def htHidden(self, default, options, nameMap=identity): """The HTML for a hidden input () This should be overridden if you override valueFromFields()""" if default is None: default = '' return html.input(type="hidden", name=nameMap(self.name()), value=default) def htInput(self, default, options, nameMap=identity): """The HTML input code""" raise NotImplementedError def attemptConvert(self, fields, nameMap=identity): key = suffixMap(".repetitions", nameMap)(self.name()) if fields.has_key(key): return self.attemptConvertRepeat( fields, int(fields[key]), nameMap=nameMap) hasError = False try: value = self.valueFromFields(fields, nameMap=nameMap) except Validator.InvalidField, msg: errorMessage = msg hasError = True else: for validator in self._validators: try: value = validator.attemptConvert(value) except Validator.InvalidField, msg: errorMessage = msg hasError = True break if hasError: return False, htmlStr(errorMessage) else: return True, value def attemptConvertRepeat(self, fields, repetitions, nameMap=identity): if not self._repeatable: return False, None output = [] errors = [] allGood = True for i in range(repetitions): good, data = self.attemptConvert(fields, nameMap=suffixMap(".%i" % i, nameMap)) if good: output.append(data) errors.append(None) else: allGood = False errors.append(data) output.append(None) if allGood: return True, output else: return False, errors def valueFromFields(self, fields, nameMap=identity): """Override this to deal with compound HTML fields. Also be sure to override unvalidatedValueFromFields.""" return fields.get(nameMap(self.name()), None) def fieldExists(self, fields, nameMap=identity): return fields.has_key(nameMap(self.name())) def unvalidatedValueFromFields(self, fields, nameMap=identity): """Like valueFromFields, but shouldn't raise exceptions -- create a fake object if necessary. Mixed with defaults to give the transient default value. Don't return None instead of a string, either.""" value = self.valueFromFields(fields, nameMap=nameMap) if value is None: return '' else: return value def preferedEnctype(self): """Mostly this is just here for , which requires enctype="multipart/form-data". Fields that don't care (everything else) return None.""" return None def isHidden(self, options={}): return self.option("hide", options, False) def isSubmit(self, options={}): return False def isCompound(self, options={}): return False def repetitions(self, options={}): return self.option('repeat', options, 1) def onSubmit(self): return None def formJavascript(self, options, nameMap=identity): return {} def _opAttr(self, s, value, text=True): """Utility function""" if not value: return '' elif text: return ' %s="%s"' % (s, htmlEncode(value)) else: return ' ' + s def _extraHTMLCode(self, options): """Utility function""" if self._extraHTML: html = ' ' + self._extraHTML else: html = '' if options.has_key('extraHTML'): return html + ' ' + options['extraHTML'] else: return html class CompoundField(Field): def __init__(self, name, fields, formValidators=None, **kw): self._fields = fields self._formValidators = formValidators or [] Field.__init__(self, name, **kw) def fields(self): return self._fields def innerOption(self, field, dict, default, nameMap=identity): name = field.name() if type(dict) is not DictionaryType: return default if not dict.has_key(name): return default else: return dict[name] def htHidden(self, default, options, nameMap=identity): out = [] for field in self.fields(): out.append( field.htHidden(self.innerOption(field, default, ''), self.innerOption(field, options, {}), nameMap=self.prefixMap(nameMap))) return '\n'.join(out) def fieldAccessors(self, default, options, nameMap=identity): values = [] for field in self.fields(): values.append( (field, self.innerOption(field, default, '', nameMap=nameMap), self.innerOption(field, options, {}, nameMap=nameMap), self.prefixMap(nameMap))) return values def htInput(self, default, options, nameMap=identity): out = [] for field in self.fields(): out.append( field.htInput(self.innerOption(field, default, '', nameMap=nameMap), self.innerOption(field, options, {}, nameMap=nameMap), nameMap=self.prefixMap(nameMap))) return '\n'.join(out) def attemptConvert(self, fields, nameMap=identity): key = suffixMap(".repetitions", nameMap)(self.name()) if fields.has_key(key): return self.attemptConvertRepeat( fields, int(fields[key]), nameMap=nameMap) allGood = True errors = {} data = {} for field in self._fields: good, value = field.attemptConvert(fields, self.prefixMap(nameMap)) if good: data[field.name()] = value ## @@: No submit buttons and such else: allGood = False errors[field.name()] = value for formValidator in self._formValidators: if not allGood and not formValidator.validatePartialForm(): continue formErrors = formValidator.validate(data) if not formErrors: continue allGood = False if type(formErrors) is StringType: formErrors = {"form": formErrors} assert type(formErrors) is DictType, "form validators should return a string or a dictionary (got type %s)" % type(formErrors) for fieldName, errorMessage in formErrors.items(): if errors.has_key(fieldName): if type(errors[fieldName]) is not ListType: errors[fieldName] = [errors[fieldName]] errors[fieldName].append(errorMessage) else: errors[fieldName] = errorMessage if allGood: return True, data else: return False, errors def valueFromFields(self, fields, nameMap=identity): assert 0, "I don't think this should be called" dict = {} for field in self.fields(): dict[field.name()] = field.valueFromFields(fields, nameMap=self.prefixMap(nameMap)) return dict def fieldExists(self, fields, nameMap=identity): for field in self.fields(): v = field.fieldExists(fields, nameMap=self.prefixMap(nameMap)) if v: return 1 return 0 def unvalidatedValueFromFields(self, fields, nameMap=identity): assert 0, "I don't think this should be called" dict = {} for field in self.fields(): dict[field.name()] = field.unvalidatedValueFromFields(fields, nameMap=self.prefixMap(nameMap)) return dict def preferedEnctype(self): for field in self.fields(): if field.preferedEnctype(): return field.preferedEnctype() return None def isCompound(self): return True def prefixMap(self, otherMap): return prefixMap("%s:" % otherMap(self.name())) def formJavascript(self, options, nameMap=identity): dict = {} for field in self.fields(): dict.update(field.formJavascript( options, nameMap=self.prefixMap(nameMap))) return dict class MultiFieldMixin: """ This is a mixin for Field when the field deals with multiple return values (as in ?var=value1&var=value2). It should be mixed in first (with a decendant of Field), and you may wish to override valueListFromFields (for compound fields). You should take the optional argument listValidators = [] in your __init__, and set self._listValidators. These are validators that validate the entire list of values, as opposed to the individual values. (NotEmpty needs this, for instance) """ def attemptConvert(self, fields, nameMap=identity): errorMessageList = [] if hasattr(self, "_listValidators"): listValidators = self._listValidators or [] else: listValidators = [] try: valueList = self.valueListFromFields(fields, nameMap=nameMap) except Validator.InvalidField, msg: errorMessageList.append(str(msg)) else: for validator in listValidators: try: valueList = validator.attemptConvert(valueList) except Validator.InvalidField, msg: errorMessageList.append(str(msg)) break if not errorMessageList: newValues = [] for value in valueList: for validator in self._validators: try: value = validator.attemptConvert(value) except Validator.InvalidField, errorMessage: errorMessageList.append(str(errorMessage)) break newValues.append(value) if errorMessageList: return False, errorMessageList else: return True, newValues def valueListFromFields(self, fields, nameMap=identity): value = fields.get(nameMap(self.name()), []) if not type(value) is type([]): return [value] else: return value class SubmitButton(Field): """ Not really a field, but a widget of sorts. methodToInvoke is the name (string) of the servlet method that should be called when this button is hit. You can use suppressValidation for large-form navigation (wizards), when you want to save the partially-entered and perhaps not valid data (e.g., for the back button on a wizard). You can load that data back in (from request.fields()) using rawDefaults in FormServlet.renderableForm. The confirm option will use JavaScript to confirm that the user really wants to submit the form. Useful for buttons that delete things. """ def __init__(self, name, methodToInvoke = None, invokeAsFunction = False, noneAsDefault = False, suppressValidation = False, confirm=None, defaultSubmit=False, **kw): self._methodToInvoke = methodToInvoke self._suppressValidation = suppressValidation self._confirm = confirm self._invokeAsFunction = invokeAsFunction self._noneAsDefault = noneAsDefault self._defaultSubmit = defaultSubmit Field.__init__(self, name, validators = [], **kw) def suppressValidation(self): return self._suppressValidation def methodToInvoke(self): return self._methodToInvoke def invokeAsFunction(self): return self._invokeAsFunction def noneAsDefault(self): return self._noneAsDefault def htInput(self, default, options, nameMap=identity): if self._confirm: query = ' onClick="return window.confirm(\'%s\')"' % htmlEncode(javascriptQuote(self._confirm)) else: query = Exclude return html.input( type="submit", name=nameMap(self.name()), value=self.description(), onClick=query) def htHidden(self, default, options, nameMap=identity): if default: return html.input( type="hidden", name=nameMap(self.name()), value=self.description()) else: return '' def valueFromFields(self, fields, nameMap=identity): return fields.has_key(nameMap(self.name())) def isSubmit(self, options={}): return True def isDefaultSubmit(self, options={}): return self._defaultSubmit class ImageSubmit(SubmitButton): def __init__(self, name, imgSrc=None, imgHeight=Exclude, imgWidth=Exclude, border=0, **kw): assert imgSrc, "You must provide an imgSrc value" self._imgSrc = imgSrc self._imgHeight = imgHeight self._imgWidth = imgWidth self._border = border SubmitButton.__init__(self, name, **kw) def htInput(self, default, options, nameMap=identity): return html.input( type="image", name=nameMap(self.name()), value=self.description(), srg=self._imgSrc, height=self._imgHeight, width=self._imgWidth, border=self._border) def valueFromFields(self, fields, nameMap=identity): return fields.has_key(nameMap(self.name()) + ".x") def fieldExists(self, fields, nameMap=identity): return self.valueFromFields(fields, nameMap=nameMap) class HiddenField(Field): """ Hidden field. Set the value using form defaults. Since you'll always get string back, you are expected to only pass strings in (unless you use a converter like AsInt). """ def __init__(self, name, **kw): Field.__init__(self, name, description=None, **kw) def htInput(self, default, options, nameMap=identity): return html.input( type="hidden", name=nameMap(self.name()), value=default) def isHidden(self, options={}): return True class SecureHiddenField(HiddenField): """ Like HiddenField, but clients can't give fake values. The value is passed with a hash of the hidden value and a secret key, to verify that the value was generated on the server side. """ ## @@: Currently doesn't work over AppServer restart ## (should do persistent store for generated secret key) def __init__(self, name, validators=None, **kw): self._secretKey = _generateSecretKey() validators = [SecureHiddenFieldValidator(self._secretKey)] \ + (validators or []) HiddenField.__init__(self, name, validators=validators, extraHTML=None, **kw) def htInput(self, default, options, nameMap=identity): hash = md5hash(self._secretKey + htmlStr(default)) return html( html.input( type="hidden", name="%s.hash" % nameMap(self.name()), value=hash), html.input( type="hidden", name=nameMap(self.name()), value=default)) def valueFromFields(self, fields, nameMap=identity): return (fields.get(nameMap(self.name()), None), fields.get(nameMap(self.name()) + ".hash", '')) class SecureHiddenFieldValidator(Validator.ValidatorConverter): def __init__(self, secretKey): self._secretKey = secretKey Validator.ValidatorConverter.__init__(self) def validate(self, value): hash = md5hash(self._secretKey + value[0]) if not hash == value[1]: return "Invalid value (not generated by server) submitted" else: return None def convert(self, value): return value[0] def _generateSecretKey(): return hex(whrandom.randint(0, 0xffff))[2:] class TextField(Field): def __init__(self, name, size=Exclude, maxLength=Exclude, validators=None, **kw): if maxLength is not None: validators = [Validator.MaxLength(maxLength)] + (validators or []) if size is Exclude: size = maxLength self._maxLength = maxLength self._size = size Field.__init__(self, name, validators=validators, **kw) def htInput(self, default, options, nameMap=identity): return html.input( type="text", name=nameMap(self.name()), value=default, maxlength=self._maxLength, size=self._size) class TextareaField(Field): def __init__(self, name, rows=10, cols=60, wrap="SOFT", **kw): self._rows = rows self._cols = cols self._wrap = wrap Field.__init__(self, name, **kw) def htInput(self, default, options, nameMap=identity): return html.textarea( name=nameMap(self.name()), rows=self._rows, cols=self._cols, wrap=self._wrap or Exclude, c=htmlEncode(default)) class PasswordField(TextField): def htInput(self, default, options, nameMap=identity): return html.input( type="password", name=nameMap(self.name()), value=default, maxlength=self._maxLength, size=self._size) class MD5PasswordField(Field): def __init__(self, name, size=Exclude, maxLength=Exclude, defaultSalt=None, validators=None, **kw): self._defaultSalt = defaultSalt self._maxLength = maxLength self._size = size if maxLength: validators = [Validator.MaxLength(maxLength)] + (validators or []) Field.__init__(self, name, validators=validators, description=description, options=options) def htInput(self, default, options): html = StringIO() html.write('\n') if default: salt, passwordEncoded = default shownPassword = "*****" else: shownPassword = "" if options.has_key('salt'): salt = options['salt'] passwordEncoded = 'empty' else: salt = self._defaultSalt try: salt = salt() except TypeError: pass passwordEncoded = 'empty' html.write(html.input( type="hidden", name=nameMap(self.name() + "-salt"), value="salt")) html.write(html.input( type="hidden", name=nameMap(self.name() + "encoded"), value=passwordEncoded)) html.write(html.input( type="password", name=nameMap(self.name() + "-password"), value=shownPassword, size=self._size, maxlength=self._maxLength)) return html.getvalue() def valueFromFields(self, fields, nameMap=identity): passwordEncoded = fields.get(nameMap(self.name() + "-encoded")) salt = fields.get(nameMap(self.name() + "-salt")) passwordPlain = fields.get(nameMap(self.name() + "-password")) if passwordEncoded == "empty": return (None, passwordPlain) else: return (salt, passwordEncoded) def fieldExists(self, fields, nameMap=identity): return fields.get(nameMap(self.name() + "-password")) \ or fields.get(nameMap(self.name() + "-encoded")) def onSubmit(self, nameMap=identity): salt = nameMap(self.name() + "-salt") encoded = nameMap(self.name() + "-encoded") password = nameMap(self.name() + "-password") return "password_hash(this, '%s', '%s', '%s')" \ % (salt, encoded, password) class TimedMD5PasswordField(MD5PasswordField): """ Like MD5PasswordField, but generates a salt value that will expire in a certain amount of time. It also uses a converter that will turn the field into a function, against which you can validate a password, like field['passfield']('somepassword') """ def __init__(self, name, validators=None, size=None, maxLength=None, timeToExpire=None, allowInsecure=True, **kw): if not timeToExpire: timeToExpire = 20 * 60 # 20 minutes validators = [ExpiredSalt(timeToExpire, allowInsecure), PasswordFunctionConverter()] + (validators or []) MD5PasswordField.__init__(self, name, validators=validators, size=size, maxLength=maxLength, defaultSalt=self.generateSalt, **kw) def generateSalt(self): return time.time() class ExpiredSalt(Validator.ValidatorConverter): """ Checks if a time.time() based salt is expired """ def __init__(self, timeToExpire, allowInsecure): self._timeToExpire = timeToExpire self._allowInsecure = allowInsecure Validator.ValidatorConverter.__init__(self) def validate(self, value): salt = value[0] if salt is None: if self._allowInsecure: return None else: return "You must have JavaScript enabled" else: try: salt = int(salt) except: ## this should never happen return "Invalid salt" if salt < time.time() - self._timeToExpire: ## @@ "session" isn't really the right term return "You're session has expired, please reload the page and enter your password again" else: return None def md5hash(value): m = md5.new() m.update(value) return m.digest() class PasswordFunctionConverter(Validator.ValidatorConverter): """ Turns a (salt, hashed_password) pair (as returned from MD5PasswordField) into a function that checks a plaintext password against the hashed version, as in field['passfield']('somepassword') """ def convert(self, value): salt, hashed_password = value if not salt: return lambda p, h=hashed_password: p==h return lambda p, s=salt, h=hashed_password: p==md5hash(str(s)+str(h)) class SelectField(Field): """ Creates a select field, based on a list of value/description pairs. The values do not need to be strings. If nullInput is given, this will be the default value for an unselected box. This would be the "Select One" selection. If you want to give an error if they do not select one, then use the NotEmpty() validator. They will not get this selection if the form is being asked for a second time after they already gave a selection (i.e., they can't go back to the null selection if they've made a selection and submitted it, but are presented the form again). If you always want a null selection available, put that directly in the selections. """ def __init__(self, name, selections=None, nullInput=None, size=Exclude, dynamic=NoDefault, validators=None, **kw): if dynamic is NoDefault: self._dynamic = not selections else: self._dynamic = dynamic self._nullInput = nullInput self._size = size self._dynamic = dynamic self._selections = selections or [] if not self._dynamic: self._encodedKeys = {} keys = map(lambda x: x[0], self._selections) if nullInput: keys.append('') self._encodedKeys[''] = None if not filter(lambda x: type(x) is not type(""), keys): for s in keys: self._encodedKeys[s] = s validators = [Validator.InList(keys, allowSublists=self.allowSublists(), hideList=False)] \ + (validators or []) elif not filter(lambda x: type(x) is not IntegerType, keys): for n in keys: self._encodedKeys[n] = htmlStr(n) if self.allowSublists(): asInt = Validator.ValidateList(Validator.AsInt()) else: asInt = Validator.AsInt() validators = [asInt, Validator.InList(keys, hideList=False)] \ + validators else: for i in range(len(keys)): self._encodedKeys[keys[i]] = htmlStr(i) indexList = Validator.IndexListConverter(keys) if self.allowSublists(): indexList = Validator.ValidateList(indexList) validators = [indexList] \ + validators Field.__init__(self, name, validators=validators, **kw) def encode(self, value): if value is None and self._nullInput: return '' if self._dynamic: return htmlStr(value) else: return self._encodedKeys[value] def htInput(self, default, options, nameMap=identity): if self._dynamic and options.get("selections") != None: selections = options["selections"] else: selections = self._selections if not default and self._nullInput: selections = [(None, self._nullInput)] + selections out = StringIO() self.htInputRender(out, selections, default, options, nameMap=nameMap) return out.getvalue() def htInputRender(self, out, selections, default, options, nameMap=identity): out.write(html.select( name=nameMap(self.name()), size=self._size, c=[html.option(value, value=self.encode(key), selected=self.selected(key, default) and "selected" or Exclude) for key, value in selections])) def selected(self, key, default): if not self._dynamic and self._encodedKeys.has_key(key): if htmlStr(self._encodedKeys[key]) == htmlStr(default): return True return str(key) == str(default) def allowSublists(self): return False class OrderingField(SelectField): def __init__(self, name, showReset=False, **kw): self._showReset=showReset SelectField.__init__(self, name, **kw) def htInputRender(self, out, selections, default, options, nameMap=identity): size = len(selections) if default: newSelections = [] for defaultKey in default: for key, value in selections: if str(key) == str(defaultKey): newSelections.append((key, value)) break assert len(newSelections) == len(selections), "Defaults don't match up with the order of the selections" selections = newSelections encodedValue = '' for key, value in selections: encodedValue = encodedValue + urllib.quote(str(key)) + " " out.write( html.select( name=nameMap(self.name() + "-func"), size=size, c=[html.option(value, value=key) for key, value in selections])) out.write(html.br()) for name, action in self.buttons(): out.write(html.input.button( value=name, onClick=action, onDblClick=action)) js = StringIO() self.writeJavascript(js, nameMap=nameMap) out.write(html.script( language="JavaScript", c=html.comment(js.getvalue()))) out.write(html.input.hidden( name=nameMap(self.name()), value=encodedValue)) def valueFromFields(self, fields, nameMap=identity): value = fields.get(nameMap(self.name())) valueList = string.split(value) return map(urllib.unquote, valueList) def buttons(self): buttons = [('up', 'up(this)'), ('down', 'down(this)')] if self._showReset: buttons.append(('reset', 'resetEntries(this)')) return buttons def writeJavascript(self, out, nameMap=identity): name = nameMap(self.name() + "-func") hiddenName = nameMap(self.name()) out.write(''' function up(formElement) { var i, select; select = getSelect(formElement); i = select.selectedIndex; if (i == -1 || i == 0) { return; } swapOptions(select, i, i-1); select.selectedIndex = i-1; saveValue(select); } function down(formElement) { var i, select; select = getSelect(formElement); i = select.selectedIndex; if (i == -1 || i == select.length-1) { return; } swapOptions(select, i, i+1); select.selectedIndex = i+1; saveValue(select); } function getSelect(formElement) { return formElement.form['%s'] } function swapOptions(select, op1, op2) { var tmpValue, tmpText; tmpValue = select.options[op1].value; tmpText = select.options[op1].text; select.options[op1].value = select.options[op2].value; select.options[op1].text = select.options[op2].text; select.options[op2].value = tmpValue; select.options[op2].text = tmpText; } function saveValue(select) { if (origValues == false) { saveOrigValues(select); } var s = "", i; for (i=0; i < select.length; i++) { s = s + escape(select.options[i].value) + " "; } select.form['%s'].value = s; } function saveOrigValues(select) { origValues = new Array(); for (i=0; i 10): yearTextInput = True else: yearTextInput = False # Actually, I don't like the default at all... yearTextInput = True elif not yearTextInput: if not earliestDate or not latestDate: raise Form.FormError, "You cannot have a select-box year input if you don't have a specific date range (earliestDate AND latestDate)." self._yearTextInput = yearTextInput validators = [Validator.DateValidator(earliestDate = earliestDate, latestDate = latestDate, allowEmpty = allowEmpty)] + \ (validators or []) Field.__init__(self, name, validators=validators, **kw) def yearName(self, nameMap=identity): return nameMap(self.name() + "-year") def monthName(self, nameMap=identity): return nameMap(self.name() + "-month") def dayName(self, nameMap=identity): return nameMap(self.name() + "-day") def centuryName(self, nameMap=identity): return nameMap(self.name() + "-century") def htHidden(self, default, options, nameMap=identity): if default: year = default.year day = default.day month = default.month else: if self._allowEmpty: return '' else: # Huh, what to do now? raise Form.FormError, "You can't hide a date field without a default unless you allow empty dates" return ''' ''' \ % (self.yearName(nameMap), year, self.monthName(nameMap), month, self.dayName(nameMap), day) def valueFromFields(self, fields, nameMap=identity): if self._allowEmpty and not \ (fields.get(self.yearName(nameMap)) or fields.get(self.monthName(nameMap)) or fields.get(self.dayName(nameMap))): return None try: year = int(fields.get(self.yearName(nameMap))) month = int(fields.get(self.monthName(nameMap))) day = int(fields.get(self.dayName(nameMap))) if year < 100: if fields.has_key(self.centuryName(nameMap)): year = int(fields.get(self.centuryName(nameMap))) \ * 100 + year elif not self._earliestDate or \ self._earliestDate.year > 100: ## Wouldn't want to keep people from allowing ## historical dates, now would we... (oh, but ## what about BCE? Next revision...) raise Validator.InvalidField, "Ambiguous century" except (TypeError, ValueError): raise Validator.InvalidField, "Invalid date" else: try: return DateTime.DateTime(year, month, day) except DateTime.RangeError: raise Validator.InvalidField, "Invalid date (nonexistant day?)" def unvalidatedValueFromFields(self, fields, nameMap=identity): try: return self.valueFromFields(fields, nameMap) except Validator.InvalidField: return (self.attemptInt(fields.get(self.yearName(nameMap))), self.attemptInt(fields.get(self.monthName(nameMap))), self.attemptInt(fields.get(self.dayName()))) def attemptInt(self, value): try: return int(value) except ValueError: return None def htInput(self, default, options, nameMap=identity): s = StringIO() defaultYear, defaultMonth, defaultDay = None, None, None if default: if type(default) is type(()): defaultYear, defaultMonth, defaultDay = default else: defaultYear = default.year defaultMonth = default.month defaultDay = default.day s.write('\n') if defaultDay: s.write('\n' % (self.dayName(nameMap), defaultDay)) else: s.write('\n' % self.dayName(nameMap)) s.write(', ') if self._yearTextInput: if (self._earliestDate and self._earliestDate.year > 2000): century = int(self._earliestDate.year / 100) s.write('%s' % (self.centuryName(nameMap), century, century)) if defaultYear: s.write('\n' % (self.yearName(nameMap), htmlEncode(defaultYear))) else: s.write('\n' % self.yearName(nameMap)) else: if defaultYear: s.write('\n' % (self.yearName(nameMap), defaultYear)) else: s.write('' % self.yearName(nameMap)) else: s.write('\n') result = s.getvalue() s.close() return result def months(self): return ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] class FileField(Field): """ mimeTypes is the a list of MIME types to accept. Browsers pay very little attention to this, though. By default it will return a cgi FieldStorage object -- use .value to get the string, .file to get a file object, .filename to get the filename. Maybe other stuff too. If you set returnString=True it will return a string with the contents of the uploaded file. You can't have any validators unless you do returnString. @@ 2001-07-20 ib: Reports of errors because of valueFromFields. However, this seems to jive with what Webware is supposed to do. """ def __init__(self, name, validators=None, mimeTypes=None, size=Exclude, returnString=False, **kw): assert (not validators) or returnString, "You can't validate the fileObject -- you must have returnString=True to do validation" self._size = size if type(mimeTypes) is type(""): self._mimeTypes = [mimeTypes] else: self._mimeTypes = mimeTypes self._returnString = returnString Field.__init__(self, name, validators=validators, **kw) def htInput(self, default, options, nameMap=identity): if self._mimeTypes: mimeList = htmlEncode(string.join(self._mimeTypes, ",")) else: mimeList = Exclude return html.input.file( name=nameMap(self.name()), size=self._size, accept=mimeList, mimetypes=mimeList) def preferedEnctype(self): return "multipart/form-data" def htHidden(self, default, options, nameMap=identity): if self._returnString: return Field.htHidden(self, default, options, nameMap=nameMap) else: raise Form.FormError, "You cannot hide a file field unless you are prepared to get a string back" def valueFromFields(self, fields, nameMap=identity): field = fields.get(nameMap(self.name())) if self._returnString: return field.value else: return field class TextareaFileField(TextareaField): """ A textarea field that also has a file upload button -- unlike just putting the two together, when you upload a file and the form doesn't validate, the contents of the file will be in the textarea. File upload overrides textarea contents. """ def htInput(self, default, options, nameMap=identity): return html( html.textarea( htmlEncode(default), name=nameMap(self.name()), rows=self._rows, cols=self._cols, wrap=self._wrap or Exclude), html.br(), html.input.file( name=nameMap(self.name() + "-upload"), accept="text/plain", mimetypes="text/plain")) def valueFromFields(self, fields, nameMap=identity): field = fields.get(nameMap(self.name() + "-upload"), '') if type(field) is not type(""): field = field.value if field: return field return fields.get(nameMap(self.name()), '') def preferedEnctype(self): return "multipart/form-data" class ImageFileUploadField(FileField): """ Allows image uploading. Writes file into uploadFilepath, and returns a special object to access that file. Works over multiple form submits without reuploading the file. """ def __init__(self, name, uploadPath=None, uploadURL=None, **kw): assert uploadPath, "You must provide an uploadPath" assert uploadURL, "You must provide a base URL for files uploaded to the filepath" if uploadPath[-1] == "/": uploadPath = uploadPath[:-1] self._uploadPath = uploadPath self._uploadURL = uploadURL self._secretKey = _generateSecretKey() FileField.__init__(self, name, mimeTypes=["image/gif", "image/jpg", "image/png"], **kw) def htInput(self, default, options, nameMap=identity): if default: if PIL: image = Image.open(os.path.join(self._uploadFilePath, default.filename)) size = image.size else: size = (None, None) prefix = html( html.img(src="%s/%s" % (self._uploadPath, default.filename), width=image.size[0] or Exclude, height=image.size[1] or Exclude), html.input.hidden(name=nameMap(self.name() + "-confirm"), value=md5hash(self._secretKey + default.filename))) else: prefix = '' return prefix + str(FileField.htInput(self, None, options, nameMap=nameMap)) def valueFromFields(self, fields, nameMap=identity): field = fields.get(nameMap(self.name())) if field is None or (type(field) is StringType and not field): filename = fields.get(nameMap(self.name() + "-filename")) if not filename: return None if not fields.get(nameMap(self.name() + "-confirm"), '') \ == md5hash(self._secretKey + filename): return None ## @@ I'd rather signal an error here return UploadedImage(filename=filename, path=self._uploadPath) return UploadedImage(field=field, path=self._uploadPath) class UploadedImage: def __init__(self, field=None, filename=None, path=None): assert path, "I need a path" if field is not None: if type(field) is not type(""): filename = field.filename else: assert Image, "If uploads return string objects, PIL must be installed to determine type" t = Image.open(StringIO(field)).type if t == "JPEG": filename = "tmp.jpg" elif t == "GIF": filename = "tmp.gif" elif t == "PNG": filename = "tmp.png" else: filename = "tmp" filename, ext = os.path.splitext(filename) i = 1 while not os.path.exists(filename + "-%i" % i + ext): i = i + 1 self.filename = filename + "-%i" % i + ext self.fullpath = os.path.join(path, self.filename) else: assert filename, "I need a filename or a field" self.filename = filename self.fullpath = os.path.join(path, filename) def copyTo(self, destPath): f = open(self.fullpath) fout = open(destPath, "w") while 1: v = f.read(1023) if not v: break fout.write(v) f.close() fout.close() def moveTo(self, destPath): try: os.link(self.fullpath, destPath) os.unlink(self.fullpath) except OSError: ## Does this trap all possible linking errors? self.copyTo(destPath) os.unlink(self.fullpath) class FileUploadField(FileField): """ Allows file uploading, without having to reupload the file when an error occurs. """ def __init__(self, name, uploadPath=None, **kw): assert uploadPath, "You must provide an uploadPath" if uploadPath[-1] == "/": uploadPath = uploadPath[:-1] self._uploadPath = uploadPath self._secretKey = _generateSecretKey() FileField.__init__(self, name, **kw) def htInput(self, default, options, nameMap=identity): if default: prefix = html( 'File uploaded (only upload again if you uploaded the wrong file)\n', html.input.hidden(name=nameMap(self.name() + "-filename"), value=default.filename), html.input.hidden(name=nameMap(self.name() + "-confirm"), value=md5hash(self._secretKey + default.filename))) else: prefix = '' return prefix + str(FileField.htInput(self, None, options, nameMap=nameMap)) def valueFromFields(self, fields, nameMap=identity): field = fields.get(nameMap(self.name())) if field is None or (type(field) is StringType and not field): filename = fields.get(nameMap(self.name() + "-filename")) if not filename: return None if not fields.get(nameMap(self.name() + "-confirm"), '') \ == md5hash(self._secretKey + filename): return None return UploadedFile(filename=filename, path=self._uploadPath) return UploadedFile(field=field, path=self._uploadPath) class UploadedFile(UploadedImage): def __init__(self, field=None, filename=None, path=None): assert path, "I need a path" if field is not None: if type(field) is not type(""): filename = field.filename else: filename = "unknown.txt" filename, ext = os.path.splitext(filename) i = 1 while os.path.exists(os.path.join(path, filename + "-%i" % i + ext)): i = i + 1 self.filename = filename + "-%i" % i + ext self.fullpath = os.path.join(path, self.filename) f = open(self.fullpath, "w") if type(field) is type(""): f.write(field) else: while 1: v = field.file.read(4096) if not v: break f.write(v) f.close() else: assert filename, "I need a filename or field" self.filename = filename self.fullpath = os.path.join(path, filename) class StaticText(Field): """ A static piece of text to be put into the field, useful only for layout purposes """ def __init__(self, name, text='', **kw): self._text = text Field.__init__(self, name, **kw) def htInput(self, default, options, nameMap=identity): if default is not None: return htmlStr(default) else: return htmlStr(self._text) def htHidden(self, default, options, nameMap=identity): return '' def valueFromFields(self, fields, nameMap=identity): return None class ColorPickerField(Field): def __init__(self, name, colorPickerURL=None, **kw): assert colorPickerURL, 'You must give a base URL for the color picker' self._colorPickerURL = colorPickerURL Field.__init__(self, name, **kw) def htInput(self, default, options, nameMap=identity): name = nameMap(self.name()) colorID = nameMap(self.name() + '-pick') defaultColor = default or '#ffffff' return html.table( cellspacing=0, border=0, c=[html.tr( html.td(width=20, id=colorID, style="background-color: %s; border: thin black solid;" % defaultColor, c=" "), html.td( html.input.text(size=8, maxlength=6, name=name, value=default), html.input.button(value="pick", onClick="colorpick(this, '%s', '%s')" % (name, colorID))))]) def formJavascript(self, options, nameMap=identity): return {'ColorPickerField:%s' % self._colorPickerURL: '''\ function colorpick(element, textFieldName, colorID) { win = window.open('%s?form=' + escape(element.form.name) + '&field=' + escape(textFieldName) + '&colid=' + escape(colorID), '_blank', 'dependent=no,directories=no,width=300,height=130,location=no,menubar=no,status=no,toolbar=no'); } ''' % self._colorPickerURL} ######################################## ## Some compound field generators ######################################## def CreditCardField(name, cards=['amex', 'mastercard', 'visa']): assert DateTime, "You cannot use CreditCardField unless you have mxDateTime installed" _creditCards = { 'amex': ('American Express', 'amex'), 'mastercard': ('Master Card', 'mastercard'), 'visa': ('Visa', 'visa'), 'optima': ('Optima', None), 'discover/novus': ('Discover/NOVUS', 'discover'), 'discover': ('Discover', 'discover'), 'diners': ('Diner\'s Club', 'diners'), } t = SelectField('type', selections=map(lambda n, c=_creditCards: (c[n][1], c[n][0]), cards), dynamic=False) n = TextField('number', size=20, maxLength=25) e = TextField('expiration', size=8, maxLength=10, validators=[Validator.DateConverter(acceptDay=False), Validator.DateValidator(earliestDate=DateTime.now(), messages={"after": "That card has expired"})]) c = CompoundField(name, [t, n, e], formValidators=[Validator.CreditCardValidator('type', 'number')]) return c def CityStateZipField(name): ## @@: Someday this will check if the postal code matches the state c = TextField('city', size=20, validators=[Validator.NotEmpty()]) s = TextField('state', size=3, maxLength=2, validators=[Validator.StateProvince()]) z = TextField('zip', size=10, maxLength=10, validators=[Validator.PostalCode()]) return CompoundField(name, [c, s, z]) class VerifyField(CompoundField): def __init__(self, name, fieldClass, fieldArgs=None, fieldKW=None, **kw): fieldArgs = fieldArgs or () fieldKW = fieldKW or {} formValidators = kw.setdefault('formValidators', [])[:] formValidators.append(Validator.FieldsMatch([name, 'verify'])) kw['formValidators'] = formValidators password = fieldClass(name, *fieldArgs, **fieldKW) verify = fieldClass('verify', *fieldArgs, **fieldKW) CompoundField.__init__(self, name, [password, verify], **kw) def attemptConvert(self, fields, nameMap=identity): allGood, data = CompoundField.attemptConvert(self, fields, nameMap=nameMap) if not allGood: return allGood, data else: return allGood, data[self.name()] def fieldAccessors(self, default, options, nameMap=identity): values = [] for field in self.fields(): values.append( (field, default, options, self.prefixMap(nameMap))) return values def PasswordVerifyField(name, **kw): return VerifyField(name, PasswordField, fieldKW=kw) ######################################## ## Utility functions ######################################## def asList(value): if value is None: return [] elif type(value) is type([]): return value elif type(value) is type(()): return list(value) else: return [value] def javascriptQuote(value): """I'm depending on the fact that repr falls back on single quote when both single and double quote are there. Also, JavaScript uses the same octal \\ing that Python uses.""" return repr('"' + htmlStr(value))[2:-1] ## Just some functions for manipulating little lambda-based mapping ## functions (where None=identity function) def composeMap(outerMap, innerMap): if not outerMap: return innerMap elif not innerMap: return outerMap else: return lambda v, o=outerMap, i=innerMap: o(i(v)) def suffixMap(suffix, compose=None): func = lambda v, s=suffix: v+s if compose: return composeMap(compose, func) else: return func def prefixMap(prefix, compose=None): func = lambda v, p=prefix: p+v if compose: return composeMap(compose, func) else: return func ## Make it same for import * __all__ = ['SubmitButton', 'ImageSubmit', 'StaticText', 'UploadedFile'] for name, var in globals().items(): if name.endswith('Field') and name != 'Field': __all__.append(name)