## 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. """ import warnings warnings.warn("formencode.fields is deprecated with no replacement; " "if you are using it please maintain your own copy of this " "file", DeprecationWarning, 2) import urllib PILImage = None import os from htmlgen import html True, False = (1==1), (0==1) from declarative import Declarative class NoDefault: pass class none_dict(dict): def __getattr__(self, attr): if attr.startswith('_'): raise AttributeError return self.get(attr) class Context(object): def __init__(self, name_prefix='', id_prefix='', defaults=None, **kw): self.name_prefix = name_prefix self.id_prefix = id_prefix self.defaults = defaults for name, value in kw.items(): setattr(self, name, value) self.options = none_dict(kw) def name(self, field, adding=None): if not field.name: assert self.name_prefix, ( "Field has not name, and context has no name_prefix") name = self.name_prefix elif self.name_prefix: name = self.name_prefix + field.name else: name = field.name if adding: return name + '.' + adding else: return name def id(self, field): return self.id_prefix + field.name def default(self, field): if self.defaults: name = self.name(field) return self.defaults.get(name) else: return None def push_attr(self, **kw): if 'add_name' in kw: if self.name_prefix: kw['name_prefix'] = self.name_prefix+'.'+kw.pop('add_name') else: kw['name_prefix'] = kw.pop('add_name') restore = {} for name, value in kw.items(): restore[name] = getattr(self, name, PopValue.no_value) setattr(self, name, value) return PopValue(self, restore) class PopValue(object): no_value = [] def __init__(self, object, restore_values): self.object = object self.restore_values = restore_values def pop_attr(self): for name, value in self.restore_values.items(): if value is self.no_value: delattr(self.object, name) else: setattr(self.object, name, value) class Field(Declarative): description = None id = None static = False hidden = False requires_label = True default = None name = None width = None enctype = None def __init__(self, *args, **kw): if args: context = args[0] args = args[1:] kw['name'] = context.name_prefix + kw.get('name', '') super(Field, self).__init__(*args, **kw) def render(self, context): if self.hidden: return self.html_hidden(context) elif self.static: return self.html_static(context) else: return self.html(context) def html_hidden(self, context): """The HTML for a hidden input ()""" return html.input( type='hidden', id=context.id(self), name=context.name(self), value=context.default(self)) def html_static(self, context): return html( self.html_hidden(context), context.default(self)) def html(self, context): """The HTML input code""" raise NotImplementedError def style_width(self): if self.width: if isinstance(self.width, int): return 'width: %s%%' % self.width else: return 'width: %s' % self.width else: return None style_width = property(style_width) def load_javascript(self, filename): f = open(os.path.join(os.path.dirname(__file__), 'javascript', filename)) c = f.read() f.close() return c class Form(Declarative): action = None id = None method = "POST" fields = [] form_name = None enctype = None def __init__(self, *args, **kw): Declarative.__init__(self, *args, **kw) def render(self, context): assert self.action, "You must provide an action" contents = html( [f.render(context) for f in self.fields]) enctype = self.enctype for field in self.fields: if field.enctype: if enctype is None or enctype == field.enctype: enctype = field.enctype else: raise ValueError( "Conflicting enctypes; need %r, field %r wants %r" % (enctype, field, field.enctype)) return html.form( action=self.action, method=self.method, name=context.name(self), id=context.id(self), enctype=enctype, c=contents) class Layout(Field): """ Represents a set of fields. Keyword arguments or attributes that are of type ``Field`` will be collected in the ``fields`` list:: >>> class MyForm(Layout): ... name = Text() ... address = Textarea() >>> [f.name for f in MyForm.fields] ['name', 'address'] >>> MyForm.name.name 'name' >>> another = MyForm(city=Text()) >>> [f.name for f in another.fields] ['name', 'address', 'city'] """ append_to_label = ':' use_fieldset = False legend = None requires_label = False fieldset_class = 'formfieldset' fields = [] __mutableattributes__ = ('fields',) def __classinit__(cls, new_attrs): Field.__classinit__(cls, new_attrs) found = [] for name, value in new_attrs.items(): # @@: Should we capture the name somehow? if isinstance(value, Field): if not value.name: value.name = name else: value.name = name + '.' + value.name found.append(value) found.sort(lambda a, b: cmp(a.declarative_count, b.declarative_count)) cls.fields.extend(found) def __init__(self, *args, **kw): self.fields = self.fields[:] found = [] for name, value in kw.items(): if isinstance(value, Field): found.append(value) if not value.name: value.name = name else: value.name = name + '.' + value.name del kw[name] found.sort(lambda a, b: cmp(a.declarative_count, b.declarative_count)) self.fields.extend(found) super(Field, self).__init__(*args, **kw) def render(self, context): if self.name: restore = context.push_attr(add_name=self.name+'.') else: restore = None try: if self.hidden: return html([f.html_hidden(context) for f in self.fields]) normal = [] hidden = [] for field in self.fields: if field.hidden: hidden.append(field.render(context)) else: normal.append(field) return html(self.wrap(hidden, normal, context)) finally: if restore: restore.pop_attr() def wrap(self, hidden, normal, context): hidden.append(self.wrap_fields( [self.wrap_field(field, context) for field in normal], context)) return hidden def wrap_field(self, field, context): return html(self.format_label(field, context), field.render(context), html.br) def format_label(self, field, context): label = '' if self.requires_label: label = field.description if label: label = label + self.append_to_label return label def wrap_fields(self, rendered_fields, context): if not self.use_fieldset: return rendered_fields legend = self.legend if legend: legend = html.legend(legend) else: legend = '' return html.fieldset(legend, rendered_fields, class_=self.fieldset_class) class TableLayout(Layout): width = None label_class = 'formlabel' field_class = 'formfield' label_align = None table_class = 'formtable' def wrap_field(self, field, context): return html.tr( html.td(self.format_label(field, context), align=self.label_align, class_=self.label_class), html.td(field.render(context), class_=self.field_class)) def wrap_fields(self, rendered_fields, context): return html.table(rendered_fields, width=self.width, class_=self.table_class) class FormTableLayout(Layout): layout = None append_to_label = '' def wrap(self, hidden, normal, context): fields = {} for field in normal: fields[field.name] = field layout = self.layout assert layout, "You must provide a layout for %s" % self output = [] for line in layout: if isinstance(line, (str, unicode)): line = [line] output.append(self.html_line(line, fields, context)) hidden.append(self.wrap_fields(output, context)) return hidden def html_line(self, line, fields, context): """ Formats lines: '=text' means a literal of 'text', 'name' means the named field, ':name' means the named field, but without a label. """ cells = [] for item in line: if item.startswith('='): cells.append(html.td(item)) continue if item.startswith(':'): field = fields[item[1:]] label = '' else: field = fields[item] label = self.format_label(field, context) if label: label = html(label, html.br) cells.append(html.td('\n', label, field.render(context), valign="bottom")) cells.append('\n') return html.table(html.tr(cells)) 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?')) """ confirm = None default_description = "Submit" description = '' requires_label = False def html(self, context): if self.confirm: query = ('return window.confirm(\'%s\')' % javascript_quote(self.confirm)) else: query = None description = ((self.description) or self.default_description) return html.input( type='submit', name=context.name(self), value=description, onclick=query) def html_hidden(self, context): if context.default(self): return html.input.hidden( name=context.name(self), value=context.default(self)) else: return '' class ImageSubmit(SubmitButton): """ Like SubmitButton, but with an image. Examples:: >>> prfield(ImageSubmit(img_src='test.gif')) """ img_height = None img_width = None border = 0 def html(self, context): return html.input( type='image', name=context.name(self), value=self.description, src=self.img_src, height=self.img_height, width=self.img_width, border=self.border, alt=self.description) 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={'f': 'a&value'}) """ requires_label = False hidden = True def html(self, context): return self.html_hidden(context) class Text(Field): """ Basic text field. Examples:: >>> t = Text() >>> prfield(t) >>> prfield(t, defaults={'f': "&whatever&"}) >>> prfield(t(maxlength=20, size=10)) """ size = None maxlength = None width = None def html(self, context): return html.input( type='text', name=context.name(self), value=context.default(self), maxlength=self.maxlength, size=self.size, style=self.style_width) class Textarea(Field): """ Basic textarea field. Examples:: >>> prfield(Textarea(), defaults={'f': ''}) """ rows = 10 cols = 60 wrap = "SOFT" width = None def html(self, context): return html.textarea( name=context.name(self), rows=self.rows, cols=self.cols, wrap=self.wrap or None, style=self.style_width, c=context.default(self)) class Password(Text): """ Basic password field. Examples:: >>> prfield(Password(maxlength=10), defaults={'f': 'pass'}) """ def html(self, context): return html.input( type='password', name=context.name(self), value=context.default(self), maxlength=self.maxlength, size=self.size, style=self.style_width) 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(selections=[(1, 'One'), (2, 'Two')]), defaults=dict(f='2')) >>> prfield(Select(selections=[(1, 'One')], null_input='Choose')) """ selections = [] null_input = None size = None def html(self, context, subsel=None): selections = self.selections null_input = self.null_input if not context.default(self) and null_input: # @@: list()? selections = [('', null_input)] + selections if subsel: return subsel(selections, context) else: return self.selection_html(selections, context) def selection_html(self, selections, context): return html.select( name=context.name(self), size=self.size, c=[html.option(desc, value=value, selected=self.selected(value, context.default(self))) for (value, desc) in selections]) def selected(self, key, default): if str(key) == str(default): return 'selected' else: return None 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, chop=('')))
""" confirm_on_delete = None def buttons(self, context): buttons = Ordering.buttons(self, context) confirm_on_delete = self.confirm_on_delete if confirm_on_delete: delete_button = ( 'delete', 'window.confirm(\'%s\') ? delete_entry(this) : false' % javascript_quote(confirm_on_delete)) else: delete_button = ('delete', 'delete_entry(this)') new_buttons = [] for button in buttons: if button[1] == 'reset_entries(this)': new_buttons.append(delete_button) delete_button = None new_buttons.append(button) if delete_button: new_buttons.append(delete_button) return new_buttons def javascript(self, context): js = Ordering.javascript(self, context) return js + (''' 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=dict(f='b'))

""" def selection_html(self, selections, context): id = 0 result = [] for value, desc in selections: id = id + 1 if self.selected(value, context.default(self)): checked = 'checked' else: checked = None result.append(html.input( type='radio', name=context.name(self), value=value, id="%s_%i" % (context.name(self), id), checked=checked)) result.append(html.label( for_='%s_%i' % (context.name(self), id), c=desc)) result.append(html.br()) return result 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=dict(f=['&b', '1'])) """ size = NoDefault max_size = 10 def selection_html(self, selections, context): result = [] size = self.size if size is NoDefault: size = min(len(selections), self.max_size) result.append(html.select( name=context.name(self), size=size, multiple="multiple", c=[html.option(desc, value=value, selected=self.selected(value, context.default(self)) and "selected" or None) for value, desc in selections])) def selected(self, key, default): if not isinstance(default, (tuple, list)): if default is None: return False default = [default] return str(key) in map(str, default) def html_hidden(self, context): default = context.default(self) if not isinstance(default, (tuple, list)): if default is None: default = [] else: default = [default] return html( [html.input.hidden(name=context.name(self), value=value) for value in default]) def selection_html(self, selections, context): result = [] size = self.size if size is NoDefault: size = min(len(selections), self.max_size) result.append(html.select( name=context.name(self), size=size, multiple="multiple", c=[html.option(desc, value=value, selected=self.selected(value, context.default(self)) and "selected" or None) for value, desc in selections])) return result class MultiCheckbox(MultiSelect): """ Like MultiSelect, but with checkboxes. Examples:: >>> sel = MultiCheckbox(selections=[('&a', '&A'), ('&b', '&B'), (1, 1)]) >>> prfield(sel, defaults=dict(f='&a'))


""" def selection_html(self, selections, context): result = [] id = 0 for value, desc in selections: id = id + 1 result.append(html.input( type='checkbox', name=context.name(self), id="%s_%i" % (context.name(self), id), value=value, checked=self.selected(value, context.default(self)) and "checked" or None)) result.append(html.label( " " + str(desc), for_="%s_%i" % (context.name(self), id))) result.append(html.br()) return result class Checkbox(Field): """ Simple checkbox. Examples:: >>> prfield(Checkbox(), defaults=dict(f=0)) >>> prfield(Checkbox(), defaults=dict(f=1)) """ def html(self, context): return html.input( type='checkbox', name=context.name(self), checked = context.default(self) and "checked" or None) 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 = None size = None enctype = "multipart/form-data" def html(self, context): accept = self.accept if accept and accept is not None: mime_list = ",".join(accept) else: mime_list = None return html.input( type='file', name=context.name(self), size=self.size, accept=mime_list) class StaticText(Field): """ A static piece of text to be put into the field, useful only for layout purposes. Examples:: >>> prfield(StaticText(text='some HTML')) some HTML >>> prfield(StaticText(text='whatever', hidden=1)) """ text = '' requires_label = False def html(self, context): default = context.default(self) if default is not None: return str(default) else: return str(self.text) def html_hidden(self, context): 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(color_picker_url='/colorpick.html') >>> prfield(cp, defaults={'f': '#ff0000'})
""" color_picker_url = None def html(self, context): js = self.javascript(context) color_picker_url = self.color_picker_url assert color_picker_url, ( 'You must give a base URL for the color picker') name = context.name(self) color_id = context.name(self, adding='pick') default_color = context.default(self) or '#ffffff' return html.table( cellspacing=0, border=0, c=[html.tr( html.td(width=20, id=color_id, style="background-color: %s; border: thin black solid;" % default_color, c=" "), html.td( html.input(type='text', size=8, onchange="document.getElementById('%s').style.backgroundColor = this.value; return true" % color_id, name=name, value=context.default(self)), html.input(type='button', value="pick", onclick="colorpick(this, '%s', '%s')" % (name, color_id))))]) def javascript(self, context): return """\ function colorpick(element, textFieldName, color_id) { win = window.open('%s?form=' + escape(element.form.attributes.name.value) + '&field=' + escape(textFieldName) + '&colid=' + escape(color_id), '_blank', 'dependent=no,directories=no,width=300,height=130,location=no,menubar=no,status=no,toolbar=no'); } """ % self.color_picker_url ######################################## ## Utility functions ######################################## def javascript_quote(value): """ Quote a Python value as a Javascript literal. 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:: >>> javascript_quote('a') 'a' >>> javascript_quote('\\n') '\\\\n' >>> javascript_quote('\\\\') '\\\\\\\\' """ return repr('"' + str(value))[2:-1] def prfield(field, chop=None, **kw): """ Prints a field, useful for doctests. """ if not kw.has_key('name'): kw['name'] = 'f' name = kw.pop('name') context = Context(**kw) context.form = Form() result = html.str(field(name=name).render(context)) if chop: pos1 = result.find(chop[0]) pos2 = result.find(chop[1]) if pos1 == -1 or pos2 == -1: print 'chop (%s) not found' % repr(chop) else: result = result[:pos1] + result[pos2+len(chop[1]):] print result if __name__ == '__main__': import doctest import doctest_xml_compare doctest_xml_compare.install() doctest.testmod()