## 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 PILImage = None try: from cStringIO import StringIO except ImportError: from StringIO import StringIO from validators import NoDefault, All import validators import time, md5, whrandom, os from simplehtmlgen import html, Exclude, NoValue True, False = (1==1), (0==1) from schema import Schema from interfaces import IField, IValidator import protocols from declarative import Declarative, DeclarativeMeta import variabledecode def renderHTML(field, request): return adaptHTMLView(field).html(request) class FormRequest(object): def __init__(self, httpRequest=None, defaults=None, options=None, errors=None, state=None, validator=None, field=None, prefix='', lastName=None, form=None, sortOrder=None): self.httpRequest = httpRequest or {} self.defaults = defaults self.options = options or {} self.errors = errors self.state = state assert not validator or not field, "You may only provide one of the arguments field or validator" if validator: self.rawValidator = validator self.validator = validators.adaptValidator(validator) try: self.field = adaptHTMLView(self.rawValidator) except NotImplementedError: self.field = None assert self.field, "Validator has no view: %r" % self.rawValidator else: self.field = field try: self.validator = validators.adaptValidator(field) except NotImplementedError: self.validator = None self.prefix = prefix self.lastName = lastName self.form = form if sortOrder is None: # @@: sys.maxint? Whatever... self.sortOrder = 9999 else: self.sortOrder = sortOrder def __repr__(self): return '<%s %s at %s>' % ( self.__class__.__name__, self.name or 'ROOT', hex(id(self))) def repeating__get(self): return getattr(self.validator, 'repeating', False) repeating = property(repeating__get) def compound__get(self): return getattr(self.validator, 'compound', False) compound = property(compound__get) def __getitem__(self, key): if self.compound: assert isinstance(key, str), "Compound fields must be addressed by string keys (you gave %r)" % key return self._keyedSubRequest(key) elif self.repeating: assert isinstance(key, int), "Repeating fields must be addressed by integer keys (you gave %r)" % key return self._indexedSubRequest(key) else: assert 0, "The controlling validator (%r) is neither repeating or compound" % self.validator def _keyedSubRequest(self, key): sub = self.validator.fields[key] #if isinstance(sub, list): # sub = validators.All.join(*sub) if self.validator.order: try: sortOrder = self.validator.order.index(key) except ValueError: sortOrder = None else: sortOrder = sub.declarativeCount try: defaults = self.defaults[key] except (KeyError, TypeError): defaults = None try: options = self.options[key] except KeyError: options = {} try: errors = self.errors[key] except (KeyError, TypeError): errors = None if self.prefix: prefix = "%s.%s" % (self.prefix, key) else: prefix = key return self.__class__( httpRequest=self.httpRequest, defaults=defaults, options=options, errors=errors, state=self.state, validator=sub, prefix=prefix, lastName=key, form=self.form, sortOrder=sortOrder) def _indexedSubRequest(self, index): sub = validators.All.join(*self.validator.validators) # @@: This feels ugly to me; why just the first validator? #sub = self.validator.validators[0] try: defaults = self.defaults[index] except (IndexError, KeyError, TypeError): defaults = self.defaults try: if (isinstance(self.options, dict) and self.options.has_key('sub')): options = self.options['sub'] else: options = self.options options = options[index] except (IndexError, KeyError, TypeError): options = self.options try: if not isinstance(self.errors, (str, unicode)): errors = self.errors[index] else: errors = None except (IndexError, KeyError, TypeError): errors = None if self.prefix: prefix = "%s-%i" % (self.prefix, index) else: # @@: Should this signal an error? prefix = str(index) return self.__class__( httpRequest=self.httpRequest, defaults=defaults, options=options, errors=errors, state=self.state, validator=sub, prefix=prefix, lastName=self.lastName) def description__get(self): try: if self.field.description: return self.field.description except AttributeError: pass try: if self.validator.description: return self.validator.description except AttributeError: pass if self.lastName: return self._convertName(self.lastName) else: return None description = property(description__get, None, None) def requiresLabel__get(self): return self.field.requiresLabel requiresLabel = property(requiresLabel__get, None, None) def hidden__get(self): return self.option('hidden', self.field) hidden = property(hidden__get, None, None) _underlineRE = re.compile('_') _capsRE = re.compile('[A-Z]+') def _convertName(self, name): name = self._underlineRE.sub(' ', name) name = self._capsRE.sub(lambda m, n=name, s=self: s._convertNameSub(m, n), name) return name def _convertNameSub(self, match, name): m = match.group(0).lower() if len(m) > 1: if match.end() == len(name): return ' %s' % m return '%s %s' % (m[:-1], m[-1]) return ' %s' % m def html(self): return self.field.html(self) def __nonzero__(self): # @@: is it messed up that we can be "empty" (len==0), but # still be nonzero? Is it messed up otherwise, though? return True def __len__(self): if self.compound: return len(self.validator.fields) elif self.repeating: add = self.option('addRepetitions', self.validator, default=0) try: return self.option('repetitions', self.validator)+add except AttributeError: pass if self.prefix: repetitionName = '%s--repetitions' % self.prefix else: repetitionName = '__repetitions__' if self.httpRequest.has_key(repetitionName): return int(self.httpRequest[repetitionName])+add if not isinstance(self.defaults, dict): try: return len(self.defaults)+add except TypeError: pass return 1+add else: return 0 def keys(self): assert self.compound, "Non-compound fields do not have keys" return self.validator.fields.keys() def items(self): return [(k, self[k]) for k in self.keys()] def values(self): return [self[k] for k in self.keys()] def option(self, optionName, caller, default=NoDefault): assert caller, "You must provide a source object, to allow for implicit options" try: return self.options[optionName] except KeyError: try: return getattr(caller, optionName) except AttributeError: if default is NoDefault: raise return default def default(self, subName=None): if subName is None: if self.httpRequest: return self.httpRequest.get(self.prefix) else: return self.defaults else: if self.prefix: name = "%s.%s" % (prefix, subName) else: name = subName if self.httpRequest: return self.httpRequest.get(name) else: try: return self.defaults[name] except (KeyError, TypeError): return None def subName(self, subName): if self.prefix: return "%s.%s" % (self.prefix, subName) else: return subName def name__get(self): return self.prefix name = property(name__get) class Field(Declarative): protocols.advise( instancesProvide=[IField]) _description = None static = False hidden = False requiresLabel = True validator = validators.Identity _loadedJavascript = {} def description__get(self): if self._description is not None: return self._description return None assert 0, "@@: auto-name based on validator or something" def description__set(self, value): self._description = value description = property(description__get, description__set) def html(self, request): if request.option('static', self): return self.htmlStatic(request) elif request.option('hidden', self): return self.htmlHidden(request) else: return self.htmlInput(request) def htmlHidden(self, request): """The HTML for a hidden input ()""" return html.input.hidden( name=request.name, value=request.default()) def htmlStatic(self, request): return html( html.quote(request.default()), self.htmlHidden(request)) def htmlInput(self, request): """The HTML input code""" raise NotImplementedError def formJavascript(self, options, state): return {} def loadJavascript(self, filename): if not self._loadedJavascript.has_key(filename): f = open(os.path.join(os.path.dirname(__file__), 'javascript', filename)) self._loadedJavascript[filename] = f.read() f.close() return self._loadedJavascript[filename] def _validatorToHTMLView(ob, protocol): try: view = ob.view except AttributeError: result = None else: result = adaptHTMLView(view) if result is None: try: validators = ob.validators except AttributeError: pass else: for validator in validators: result = adaptHTMLView(validator) if result: return result return result protocols.declareAdapter(_validatorToHTMLView, [IField], forProtocols=[IValidator]) def adaptHTMLView(obj): if isinstance(obj, type) and issubclass(obj, Declarative): obj = obj.singleton() result = protocols.adapt(obj, IField, factory=_validatorToHTMLView) return result class Form(Declarative): action = None method = "POST" schema = None formName = None enctype = Exclude def __init__(self, *args, **kw): Declarative.__init__(self, *args, **kw) self._javascript = {} def html(self, httpRequest=None, defaults=None, options=None, errors=None, state=None): assert not self._javascript, "Leftover Javascript! %s" % self._javascript assert self.action, "You must provide an action" request = FormRequest( httpRequest=httpRequest, defaults=defaults, options=options, errors=errors, state=state, validator=self.schema, form=self) contents = request.html() js = '\n\n'.join(self._javascript.values()) if js: js = html.script(language="JavaScript", c=html.comment('\n', js, '\n// ')) contents = html( html.input.hidden(name='_formName_', value=self.formName), js, contents) return html.form( action=self.action, method=self.method, name=self.formName or Exclude, enctype=self.enctype, c=contents) #c=html( #html.input.hidden(name="_formID_", value=self.formName), #contents)) def addJavaScript(self, name, javascript): self._javascript[name] = javascript class SchemaLayout(Field): appendLabel = ':' appendError = '' errorClass = 'formerror' useFieldset = False legend = None requiresLabel = False fieldsetClass = 'formfieldset' def html(self, request): assert request.compound, "SchemaLayouts are meant to be used with Schema objects (%r with %r)" % (request, self) items = request.items() items.sort(lambda a, b: cmp(a[1].sortOrder, b[1].sortOrder)) text = [] hidden = [] for name, subRequest in items: if subRequest.hidden: hidden.append(subRequest.html()) else: text.append(self.htmlField(name, subRequest)) return self.wrapFieldset(self.wrapFields('\n'.join(text+hidden), request), request) def getError(self, request): error = request.errors if isinstance(error, dict): return error.get(None, None) elif isinstance(error, list): return None else: return error def htmlField(self, name, request): return html(self.formatError(request), self.formatLabel(request), request.html(), html.br) def wrapFields(self, text, request): return text def formatError(self, request): error = self.getError(request) if error: return html(html.b(error, request.option('appendError', self), class_=request.option('errorClass', self)), html.br) else: return '' def formatLabel(self, request): label = '' if request.requiresLabel: label = request.description if label: label = label + request.option('appendLabel', self) return label def wrapFieldset(self, text, request): if not request.option('useFieldset', self): return text legend = request.option('legend', self) if legend: legend = html.legend(legend) else: legend = '' return html.fieldset(legend, text, class_=request.option('fieldsetClass', self)) Schema.view = SchemaLayout() class TableLayout(SchemaLayout): width = Exclude labelClass = 'formlabel' fieldClass = 'formfield' labelAlign = Exclude tableClass = 'formtable' def htmlField(self, name, request): return html.tr( html.td(self.formatLabel(request), align=request.option('labelAlign', self), class_=request.option('labelClass', self)), html.td(self.formatError(request), request.html(), class_=request.option('fieldClass', self))) def wrapFields(self, text, request): return html.table(width=request.option('width', self), class_=request.option('tableClass', self), c=text) class FormTableLayout(SchemaLayout): layout = None appendLabel = '' def html(self, request): layout = request.option('layout', self) assert layout, "You must provide a layout for %s" % self assert request.compound, "SchemaLayouts are meant to be used with Schema objects (%r with %r)" % (request, self) text = [] for line in layout: if isinstance(line, (str, unicode)): line = [line] text.append(self.htmlLine(line, request)) # @@: Should check that there aren't forgotten fields here return self.wrapFieldset(self.wrapFields(''.join(text), request), request) def htmlLine(self, line, request): cells = [] for item in line: if item.startswith('='): cells.append(html.td(item)) continue if item.startswith(':'): sub = request[item[1:]] label = '' else: sub = request[item] label = self.formatLabel(sub) if label: label = html(label, html.br) cells.append(html.td('\n', label, self.formatError(sub), sub.html(), valign="bottom")) cells.append('\n') return html.table(html.tr(c=cells)) class ForEachLayout(SchemaLayout): emptyHTML = '' separatingHTML = '' def html(self, request): assert request.repeating, "ForEachLayouts are meant to be used with ForEach objects (%r with %r)" % (request, self) i = -1 text = [] hidden = [] length = len(request) for i in xrange(length): if i != 0: text.append(request.option('separatingHTML', self)) sub = request[i] if sub.hidden: hidden.append(sub.html()) else: text.append(self.htmlField(i, request[i])) if length == 0: text.append(request.option('emptyHTML', self)) text.append(self.repetitionHidden(length, request)) return self.wrapFieldset(self.wrapFields(''.join(text+hidden), request), request) def repetitionHidden(self, length, request): if not request.name: name = '__repetitions__' else: name = '%s--repetitions' % request.name return html.input.hidden(name=name, value=str(length)) validators.ForEach.view = ForEachLayout() 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 invalid data (e.g., for the back button on a wizard). You can load that data back in by passing the fields to FormRequest/From as httpRequest. The confirm option will use JavaScript to confirm that the user really wants to submit the form. Useful for buttons that delete things. Examples:: >>> prfield(SubmitButton(description='submit')) >>> prfield(SubmitButton(confirm='Really?')) """ methodToInvoke = None extraArgs = () extraKW = {} invokeAsFunction = False defaultSubmit = False suppressValidation = False confirm = None defaultDescription = "Submit" description = '' validator = validators.Validator(ifMissing=None) requiresLabel = False def htmlInput(self, request): if request.option('confirm', self): query = 'return window.confirm(\'%s\')' % \ htmlEncode(javascriptQuote(request.option('confirm', self))) else: query = Exclude description = request.option('description', self) or \ request.option('defaultDescription', self) return html.input.submit( name=request.name, value=description, onClick=query) def htmlHidden(self, request): if default: return html.input.hidden( name=request.name, value=request.option('description', self)) else: return '' def execute(self, servlet, indexes, result, httpRequest): if self.methodToInvoke: assert not self.invokeAsFunction, "Not yet implemented" meth = getattr(servlet, self.methodToInvoke) return meth(indexes, result, httpRequest, *self.extraArgs, **self.extraKW) else: return result class ImageSubmit(SubmitButton): """ Like SubmitButton, but with an image. Examples:: >>> prfield(ImageSubmit(), options={'imgSrc': 'test.gif'}) """ imgHeight = Exclude imgWidth = Exclude border = 0 def htmlInput(self, request): return html.input.image( name=request.name, value=request.option('description', self), src=request.option('imgSrc', self), height=request.option('imgHeight', self), width=request.option('imgWidth', self), border=request.option('border', self)) # @@: need to test if the .x/.y will work class Hidden(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). Examples:: >>> prfield(Hidden(), defaults='a&value') """ requiresLabel = False hidden = True def htmlInput(self, request): return self.htmlHidden(request) class SecureHidden(Hidden): ''' Like Hidden, but clients cannot 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. Examples:: >>> key = "secret" >>> sec = SecureHidden(secretKey=key) >>> prfield(sec, defaults="secure") >>> prfield(sec, defaults=1) ''' ## @@: Currently doesn't work over AppServer restart ## (should do persistent store for generated secret key) secretKey = None def __init__(self, *args, **kw): Hidden.__init__(self, *args, **kw) if not self.secretKey: self.secretKey = generateSecretKey() self.validator = All.join( SecureHiddenValidator(secretKey=self.secretKey), self.validator) def htmlInput(self, request): secretKey = request.option('secretKey', self) if request.errors: # This means the value was invalid, so we can't allow it! default = '' else: default = request.default() hash = md5hash(secretKey + str(default)) return html( html.input.hidden( name=request.subName('hash'), value=hash), html.input.hidden( name=request.name, value=default)) class SecureHiddenValidator(validators.Validator): """ Validator for SecureHiddenField. Examples:: >>> key = "secret" >>> sec = SecureHidden(secretKey=key) >>> prfield(sec, defaults="yep") >>> h = "ed16dcd1637d0655112a5ac6479ac107" >>> vals = {None: 'yep', 'hash': h} >>> validators.toPython(SecureHiddenValidator(secretKey=key), vals) 'yep' >>> vals[None] = 'nope' >>> validators.toPython(SecureHiddenValidator(secretKey=key), vals) Traceback: ... Invalid: Invalid value (not generated by server) submitted """ secretKey = None protocols = ['http'] def __init__(self, *args, **kw): validators.Validator.__init__(self, *args, **kw) assert self.secretKey, "You must provide a secretKey" def toPython(self, value, state): if not isinstance(value, dict): raise validators.Invalid('Invalid value (field missing)', value, state) hash = md5hash(self.secretKey + value[None]) if not hash == value['hash']: raise validators.Invalid("Invalid value (not generated by server) submitted", value, state) return value[None] class Text(Field): """ Basic text field. Examples:: >>> t = Text() >>> prfield(t) >>> prfield(t, defaults="&whatever&") >>> prfield(t(maxLength=20, size=10)) """ size = Exclude maxLength = Exclude def __init__(self, **kw): Field.__init__(self, **kw) if self.maxLength is not Exclude: self.validator = All.join( validators.MaxLength(self.maxLength), self.validator) def htmlInput(self, request): return html.input.text( name=request.name, value=request.default(), maxlength=request.option('maxLength', self), size=request.option('size', self)) class TextArea(Field): """ Basic textarea field. Examples:: >>> prfield(TextArea(), defaults='') """ rows = 10 cols = 60 wrap = "SOFT" def htmlInput(self, request): return html.textarea( name=request.name, rows=request.option('rows', self), cols=request.option('cols', self), wrap=request.option('wrap', self) or Exclude, c=html.quote(request.default())) class Password(Text): """ Basic password field. Examples:: >>> prfield(Password(maxLength=10), defaults='pass') """ def htmlInput(self, request): return html.input.password( name=request.name, value=request.default(), maxlength=request.option('maxLength', self), size=request.option('size', self)) class MD5Password(Field): """ This field hashes the password with Javascript when the form is submitted. This way clear text passwords won't be sent. A salt is included, so that attackers can't replay the hashed password at a later date (so long as the salt isn't valid at the later date; see TimedMD5Password). Salt may be a function that returns a the value. Examples:: >>> salt = lambda : 'frenchfry' >>> prfield(MD5Password(salt=salt)) """ salt = None size = Exclude maxLength = Exclude def __init__(self, **kw): Field.__init__(self, **kw) self.validator = All.join( MD5PasswordValidator(), self.validator) def htmlInput(self, request): out = StringIO() out.write('\n') if request.default(): salt, passwordEncoded = default shownPassword = "*****" else: salt = request.default(subName='salt') passwordEncoded = request.default(subName='encoded') shownPassword = "" if not salt: salt = request.option('salt', self) try: salt = salt() except TypeError: pass if not passwordEncoded: passwordEncoded = 'empty' out.write(html.input.hidden( name=request.subName("salt"), value=salt)) out.write(html.input.hidden( name=request.subName("encoded"), value=passwordEncoded)) out.write(html.input.password( name=request.subName("password"), value=shownPassword, size=request.option('size', self), maxlength=request.option('maxLength', self))) return out.getvalue() def onSubmit(self, request): return "password_hash(this, '%s', '%s', '%s')" \ % (request.subName('salt'), request.subName('encoded'), request.subName('password')) class MD5PasswordValidator(validators.Validator): """ This turns the result of the submission into a (salt, hashed) tuple, where salt is None and hashed is the actual password if the Javascript hashing was not run (e.g., Javascript was turned off). """ protocols = ['http'] def toPython(self, value, state): if value['encoded'] == "empty": return (None, value['password']) else: return (value['salt'], value['encoded']) class PasswordFunctionConverter(validators.Validator): """ Turns a (salt, hashed_password) pair (as returned from MD5Password) into a function that checks a plaintext password against the hashed version, as in field['passfield']('somepassword') """ protocols = ['http'] def toPython(self, value, state): 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 TimedMD5Password(MD5Password): """ 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') Examples:: >>> prfield(TimedMD5Password()) """ timeToExpire = 20*60 # 20 minutes allowInsecure = True def __init__(self, **kw): MD5Password.__init__(self, **kw) self.validator = All.join( ExpiredSalt(timeToExpire=self.timeToExpire, allowInsecure=self.allowInsecure), PasswordFunctionConverter(), self.validator) def salt(self): return time.time() class ExpiredSalt(validators.Validator): """ Checks if a time.time() based salt is expired. """ timeToExpire = 20*60 allowInsecure = True protocols = ['http'] def validatePython(self, value, state): salt = value['salt'] if salt is None: if not self.allowInsecure: raise Invalid("You must have JavaScript enabled", value, state) else: try: salt = int(salt) except: ## this should never happen raise Invalid("Invalid salt", value, state) if salt < time.time() - self.timeToExpire: ## @@ "session" isn't really the right term raise Invalid("You're session has expired, please reload the page and enter your password again", value, salt) class Select(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. Examples:: >>> prfield(Select(), options={'selections': [(1, 'One'), (2, 'Two')]}, defaults='2') >>> prfield(Select(selections=[(1, 'One')], nullInput='Choose')) """ selections = [] nullInput = None size = Exclude def encode(self, value): if value is None and self.nullInput: return '' return htmlStr(value) def htmlInput(self, request): selections = request.option('selections', self) nullInput = request.option('nullInput', self) if not request.default() and nullInput: selections = [(None, nullInput)] + selections out = StringIO() self.htmlInputRender(out, selections, request) return out.getvalue() def htmlInputRender(self, out, selections, request): out.write(html.select( name=request.name, size=request.option('size', self), c=[html.option(v, value=self.encode(k), selected=self.selected(k, request.default()) and "selected" or Exclude) for (k, v) in selections])) def selected(self, key, default): return htmlStr(key) == htmlStr(default) class Ordering(Select): """ Rendered as a select field, this allows the user to reorder items. The result is a list of the items in the new order. Examples:: >>> o = Ordering(selections=[('a', 'A'), ('b', 'B')]) >>> prfield(o)
""" showReset = False def __init__(self, *args, **kw): Select.__init__(self, *args, **kw) self.validator = All.join(OrderingValidator, self.validator) def htmlInputRender(self, out, selections, request): size = len(selections) if request.default(): newSelections = [] for defaultKey in request.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 cardinality of the selections" selections = newSelections encodedValue = '' for key, value in selections: encodedValue = encodedValue + urllib.quote(htmlStr(key)) + " " out.write( html.select( name=request.subName('func'), size=size, c=[html.option(value, value=key) for key, value in selections])) out.write(html.br()) for name, action in self.buttons(request): out.write(html.input.button( value=name, onClick=action, onDblClick=action)) js = StringIO() self.writeJavascript(js, request) out.write(html.script( language="JavaScript", c=html.comment(js.getvalue()))) out.write(html.input.hidden( name=request.name, value=encodedValue)) def buttons(self, request): buttons = [('up', 'up(this)'), ('down', 'down(this)')] if request.option('showReset', self): buttons.append(('reset', 'resetEntries(this)')) return buttons def writeJavascript(self, out, request): name = request.subName('func') hiddenName = request.name out.write(self.loadJavascript('ordering.js') % (name, hiddenName)) class OrderingValidator(validators.Validator): protocols = ['http'] def toPython(self, value, state): return map(urllib.unquote, value.split()) class OrderingDeleting(Ordering): """ Like Ordering, but also allows deleting entries Examples:: >>> o = OrderingDeleting(selections=[('a', 'A'), ('b', 'B')]) >>> prfield(o, options={'confirmOnDelete': 'Yeah?'})
""" confirmOnDelete = None def buttons(self, request): buttons = Ordering.buttons(self, request) confirmOnDelete = request.option('confirmOnDelete', self) if confirmOnDelete: deleteButton = ('delete', 'window.confirm(\'%s\') ? deleteEntry(this) : false' % htmlEncode(javascriptQuote(confirmOnDelete))) else: deleteButton = ('delete', 'deleteEntry(this)') newButtons = [] for button in buttons: if button[1] == 'resetEntries(this)': newButtons.append(deleteButton) deleteButton = None newButtons.append(button) if deleteButton: newButtons.append(deleteButton) return newButtons def writeJavascript(self, out, request): Ordering.writeJavascript(self, out, request) out.write(''' function deleteEntry(formElement) { var select; select = getSelect(formElement); select.options[select.selectedIndex] = null; saveValue(select); } ''') class Radio(Select): """ Radio selection; very similar to a select, but with a radio. Example:: >>> prfield(Radio(selections=[('a', 'A'), ('b', 'B')]), ... defaults='b')

""" def htmlInputRender(self, out, selections, request): id = 0 for key, value in selections: id = id + 1 if self.selected(key, request.default()): checked = 'checked' else: checked = Exclude out.write(html.input.radio( name=request.name, value=self.encode(key), id="%s_%i" % (request.name, id), checked=checked)) out.write(html.label( for_='%s_%i' % (request.name, id), c=htmlEncode(value))) out.write(html.br()) class MultiSelect(Select): """ Selection that allows multiple items to be selected. A list will always be returned. The size is, by default, the same as the number of selections (so no scrolling by the user is necessary), up to maxSize. Examples:: >>> sel = MultiSelect(selections=[('&a', '&A'), ('&b', '&B'), (1, 1)]) >>> prfield(sel) >>> prfield(sel, defaults=['&b', '1']) """ size = NoDefault maxSize = 10 validator = All.join(validators.Set(), Select.validator) def htmlInputRender(self, out, selections, request): size = request.option('size', self) if size is NoDefault: size = min(len(selections), request.option('maxSize', self)) out.write(html.select( name=request.name, size=size, multiple="multiple", c=[html.option(value, value=self.encode(key), selected=self.selected(key, request.default()) and "selected" or Exclude) for key, value in selections])) def selected(self, key, default): if not isinstance(default, (tuple, list)): if default is None: return False default = [default] return htmlStr(key) in map(htmlStr, default) def htmlHidden(self, request): default = request.default() if not isinstance(default, (tuple, list)): if default is None: default = [] else: default = [default] return html( *[html.input.hidden(name=request.name, value=value) for value in default]) class MultiCheckbox(MultiSelect): """ Like MultiSelect, but with checkboxes. Examples:: >>> sel = MultiCheckbox(selections=[('&a', '&A'), ('&b', '&B'), (1, 1)]) >>> prfield(sel, defaults='&a')


""" def htmlInputRender(self, out, selections, request): id = 0 for key, value in selections: id = id + 1 out.write(html.input.checkbox( name=request.name, id="%s_%i" % (request.name, id), value=self.encode(key), checked=self.selected(key, request.default()) and "checked" or Exclude)) out.write(html.label( " " + str(value), for_="%s_%i" % (request.name, id))) out.write(html.br()) class Checkbox(Field): """ Simple checkbox. Examples:: >>> prfield(Checkbox(), defaults=0) >>> prfield(Checkbox(), defaults=1) """ def __init__(self, *args, **kw): Field.__init__(self, *args, **kw) self.validator = All.join( validators.FancyValidator(ifMissing=False), self.validator) def htmlInput(self, request): return html.input.checkbox( name=request.name, checked = request.default() and "checked" or Exclude) class File(Field): """ accept 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. Examples:: >>> prfield(File()) """ accept = Exclude size = Exclude returnString = False def __init__(self, **kw): Field.__init__(self, **kw) if self.returnString: # @@: maybe force to a file (StringIO), if not returnString, # for hidden field support. self.validator = All.join( FileToStringValidator(), self.validator) def htmlInput(self, request): request.form.enctype = "multipart/form-data" accept = request.option('accept', self) if accept and accept is not Exclude: mimeList = ",".join(accept) else: mimeList = Exclude return html.input.file( name=request.name, size=request.option('size', self), accept=mimeList) class FileToStringValidator(validators.Validator): """ Converts files or file-like objects (as returned from cgi.FieldStorage) into plain strings. """ protocols = ['http'] def toPython(self, value, state): if hasattr(value, 'value'): return value.value elif hasattr(value, 'read'): return value.read() else: return value class TextAreaFile(TextArea): """ 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. Examples:: >>> f = TextAreaFile() >>> prfield(f)
>>> prfield(f, defaults='this is a test')
""" def __init__(self, *args, **kw): TextArea.__init__(self, *args, **kw) self.validator = All.join(TextAreaFileValidator(), self.validator) def htmlInput(self, request): request.form.enctype = "multipart/form-data" return html( html.textarea( htmlEncode(request.default()), name=request.name, rows=request.option('rows', self), cols=request.option('cols', self), wrap=request.option('wrap', self) or Exclude), html.br(), html.input.file( name=request.subName('upload'), accept='text/plain')) class TextAreaFileValidator(validators.Validator): protocols = ['http'] def toPython(self, value, state): field = value.get('upload', '') if type(field) is not type(""): if hasattr(field, 'value'): field = field.value elif hasattr(field, 'read'): field = field.read() if field: return field return value[None] class FileUploadValidator(validators.Validator): protocols = ['http'] def toPython(self, value, state): assert self.secretKey, "You must set secretKey" field = value.get(None) if field is None or (isinstance(field, str) and not field): filename = value['filename'] if not filename: return None if value['confirm'] != md5hash(self.secretKey + filename): raise Invalid('Invalid filename', value, state) return UploadedImage(filename=filename, path=self.uploadPath) return UploadedImage(field=field, path=self.uploadPath) class ImageFileUpload(File): """ 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. Examples:: >>> ifu = ImageFileUpload(uploadPath='/tmp', ... uploadURL='/tmpimages', ... secretKey='popcorn') >>> prfield(ifu) >>> f = open('/tmp/tmp.jpg', 'w') >>> f.close() >>> prfield(ifu, ... defaults=UploadedImage(filename='tmp.jpg', path='/tmp')) """ uploadPath = None uploadURL = None accept = ["image/gif", "image/jpg", "image/png"] secretKey = None def __init__(self, **kw): global PILImage File.__init__(self, **kw) assert self.uploadPath, "You must provide an uploadPath" assert self.uploadURL, "You must provide a base URL for files uploaded to the filepath" if self.uploadPath and self.uploadPath[-1] == "/": self.uploadPath = self.uploadPath[:-1] if PILImage is None: try: from PIL import Image as PILImage except ImportError: try: import Image as PILImage except ImportError: pass if not self.secretKey: self.secretKey = generateSecretKey() self.validator = All.join( FileUploadValidator(secretKey=self.secretKey), self.validator) def htmlInput(self, request): if request.default(): if PILImage: try: image = PILImage.open( os.path.join(self.uploadPath, request.default().filename)) size = image.size except IOError: # @@: Should we catch more exceptions? size = (Exclude, Exclude) else: size = (Exclude, Exclude) secretKey = self.secretKey prefix = html( html.img(src="%s/%s" % (self.uploadPath, request.default().filename), width=size[0], height=size[1]), html.input.hidden( name=request.subName('filename'), value=request.default().filename), html.input.hidden( name=request.subName('confirm'), value=md5hash(secretKey + request.default().filename))) else: prefix = '' return prefix + str(File.htmlInput(self, request)) class UploadedImage(object): """ Returned as the value of ImageFileUpload """ 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 PILImage, "If uploads return string objects, PIL must be installed to determine type" t = PILImage.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 FileUpload(File): """ Allows file uploading, without having to reupload the file when an error occurs. Examples:: >>> fu = FileUpload(uploadPath='/tmp', secretKey='popcorn') >>> prfield(fu) >>> prfield(fu, defaults=UploadedFile(filename='tmpfile.txt', path='/tmp')) File uploaded (only upload again if you uploaded the wrong file) """ uploadPath = None secretKey = None def __init__(self, *args, **kw): File.__init__(self, *args, **kw) assert self.uploadPath, "You must provide an uploadPath" if self.uploadPath and self.uploadPath[-1] == "/": self.uploadPath = self.uploadPath[:-1] self.validator = All.join( FileUploadValidator(secretKey=self.secretKey), self.validator) def htmlInput(self, request): if request.default(): secretKey = self.secretKey prefix = html( html.input.hidden(name=request.subName('filename'), value=request.default().filename), html.input.hidden(name=request.subName('confirm'), value=md5hash(secretKey + request.default().filename)), 'File uploaded (only upload again if you uploaded the wrong file)\n') else: prefix = '' return prefix + str(File.htmlInput(self, request)) class UploadedFile(UploadedImage): """ Returned as the result from from FileUpload. """ 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. Examples:: >>> prfield(StaticText('some HTML')) some HTML >>> prfield(StaticText('whatever'), options={'hidden': 1}) """ validator = validators.Constant(None, ifMissing=None) text = '' requiresLabel = False __unpackargs__ = ('text',) def htmlInput(self, request): default = request.default() if default is not None: return htmlStr(default) else: return htmlStr(self.text) def htmlHidden(self, request): return '' class ColorPicker(Field): """ This field allows the user to pick a color from a popup window. This window contains a pallete of colors. They can also enter the hex value of the color. A color swatch is updated with their chosen color. Examples:: >>> cp = ColorPicker(colorPickerURL='/colorpick.html') >>> prfield(cp)
 
>>> prfield(cp, defaults='#ff0000') ... ... """ colorPickerURL = None def __init__(self, **kw): Field.__init__(self, **kw) def htmlInput(self, request): self.addJavascript(request) colorPickerURL = request.option('colorPickerURL', self) assert colorPickerURL, 'You must give a base URL for the color picker' name = request.name colorID = request.subName('pick') defaultColor = request.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, onChange="document.getElementById('%s').style.backgroundColor = this.value; return true" % colorID, name=name, value=request.default()), html.input.button(value="pick", onClick="colorpick(this, '%s', '%s')" % (name, colorID))))]) def addJavascript(self, request): request.form.addJavaScript( name='ColorPicker:%s' % request.option('colorPickerURL', self), javascript="""\ function colorpick(element, textFieldName, colorID) { win = window.open('%s?form=' + escape(element.form.attributes.name.value) + '&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 CreditCard(name, cards=['amex', 'mastercard', 'visa']): # assert DateTime, "You cannot use CreditCard 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 = Select('type', # selections=map(lambda n, c=_creditCards: (c[n][1], c[n][0]), cards), # dynamic=False) # n = Text('number', # size=20, maxLength=25) # e = Text('expiration', # size=8, maxLength=10, # validator=[validators.DateConverter(acceptDay=False), # validators.DateValidator(earliestDate=DateTime.now(), messages={"after": "That card has expired"})]) # c = Compound(name, [t, n, e], # formValidators=[validators.CreditCardValidator('type', 'number')]) # return c # def CityStateZip(name): # ## @@: Someday this will check if the postal code matches the state # c = Text('city', size=20, validator=[validators.NotEmpty()]) # s = Text('state', size=3, maxLength=2, # validator=[validators.StateProvince()]) # z = Text('zip', size=10, maxLength=10, # validator=[validators.PostalCode()]) # return Compound(name, [c, s, z]) # class Verify(Compound): # def __init__(self, name, fieldClass, fieldArgs=None, # fieldKW=None, **kw): # fieldArgs = fieldArgs or () # fieldKW = fieldKW or {} # formValidators = kw.setdefault('formValidators', [])[:] # formValidators.append(validators.FieldsMatch([name, 'verify'])) # kw['formValidators'] = formValidatorsm # password = fieldClass(name, *fieldArgs, **fieldKW) # verify = fieldClass('verify', *fieldArgs, **fieldKW) # Compound.__init__(self, name, [password, verify], **kw) # def attemptConvert(self, fields, nameMap=identity): # allGood, data = Compound.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 PasswordVerify(name, **kw): # return Verify(name, Password, fieldKW=kw) ######################################## ## Utility functions ######################################## 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. Examples:: >>> javascriptQuote('a') 'a' >>> javascriptQuote('\\n') '\\\\n' >>> javascriptQuote('\\\\') '\\\\\\\\' """ return repr('"' + htmlStr(value))[2:-1] def htmlStr(s): """ str() converts None to 'None'. For an HTML value, a more appropriate translation would be ''. Examples:: >>> htmlStr('"hey"') '"hey"' >>> htmlStr(None) '' """ if s is None: return '' else: return str(s) def htmlEncode(s): """ Examples:: >>> htmlEncode('"hey"') '"hey"' >>> htmlEncode(None) '' """ if s is None: return '' else: return cgi.escape(str(s), 1) def generateSecretKey(): """ Creates a random key, usually used with md5hash to sign a value. Examples:: >>> generateSecretKey() == generateSecretKey() False """ return hex(whrandom.randint(0, 0xffff))[2:] def prfield(field, **kw): """ Prints a field, useful for doctests. """ class Form: pass req = FormRequest(form=Form(), field=field, **kw) print req.html() def md5hash(value): """ Creates a digest of the string value; the digest is is hexidecimal. Eamples:: >>> md5hash('hey') '6057f13c496ecf7fd777ceb9e79ae285' >>> len(md5hash('whatever')) 32 """ m = md5.new() m.update(value) return m.hexdigest()