Commit 8b8f9f00 authored by Nicolas Delaby's avatar Nicolas Delaby

Merge Formulator and FormulatorPatch from ERP5Form


git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@26872 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent de1888a7
Formulator Credits
Martijn Faassen (faassen@vet.uu.nl) -- Main developer, design and
implementation.
Many thanks go to:
Kit Blake (kitblake at v2.nl) -- UI help and design help.
Yury Don (yura at vpcit.ru) -- contributed EmailField and FloatField,
design and implementation help.
Stephan Richter (srichter at iuveno-net.de) -- contributed LinkField and
FileField. Contributed PatternChecker
module used by PatternField. Other
design and implementation help.
Nicola Larosa (nico at tekNico.net) -- feedback and bugfixes.
Magnus Heino (magus.heino at rivermen.se) -- feedback and bugfixes.
Joel Burton (jburton at scw.org) -- feedback and bugfixes.
Ulrich Eck (ueck at net-labs.de) -- much help and patience with the
TALES tab.
Dirk Datzert (Dirk.Datzert at rasselstein-hoesch.de) -- feedback and bugfixes.
Max Petrich (petrich.max at kis-solution.de) -- feedback and bugfixes.
Matt Behrens (matt.behrens at kohler.com) -- feedback and bugfixes.
Nikolay Kim (fafhrd at datacom.kz) -- code inspiration for
XMLToForm/FormToXML.
Godefroid Chapelle (gotcha at swing.be) -- Bugfixes.
Alan Runyan (runyaga at runyaga.com) -- Fix to email regular expression.
Sascha Welter (welter at network-ag.com) -- Extensive help with email
regular expression.
Clemens Klein-Robbenhaar (robbenhaar at espresto.com) -- Many bugfixes
and feature
additions.
Christian Zagrodnick (cz at gocept.com) -- Unicode awareness fixes and
XML entry form.
Iutin Vyacheslav (iutin at whirix.com) -- am/pm feature for DateTime
fields.
Kapil Thangavelu (k_vertigo at objectrealms.net) -- Enabled ':record' rendering.
Pierre-Julien Grizel (grizel at ingeniweb.com) -- ProductForm.
Sbastien Robin (seb at nexedi.com) -- more consistent ordering in XML
serialization.
Guido Wesdorp (guido at infrae.com) -- Added extra_item attribute on
compound fields.
Yura Petrov (ypetrov at naumen.ru) -- Various FSForm related
improvements.
Vladimir Voznesensky (vovic at smtp.ru) -- Enabling/disabling of fields.
Special thanks also goes to Rik Hoekstra.
Also a thank you to those few valiant souls who suffered through the
bugs of ZFormulator, the previous implementation. Let's hope this
one's better!
"""
This module contains some magic glue to make it seem as if we
can refer to field classes before they've been defined, through the
'fields' class.
This way, they can be used to create properties on fields.
When the field classes have been defined, get_field()
can be used on FieldProperty objects to get an
actual field object.
"""
from FieldRegistry import FieldRegistry
class DummyFieldFactory:
def __getattr__(self, name):
return DummyField(name)
fields = DummyFieldFactory()
class DummyField:
def __init__(self, desired_meta_class):
self.desired_meta_class = desired_meta_class
def __call__(self, id, **kw):
self.id = id
self.kw = kw
return self
def get_value(self, name):
return self.kw.get(name, "")
def get_real_field(self):
"""Get an actual field for this property.
"""
return apply(FieldRegistry.get_field_class(self.desired_meta_class),
(self.id,), self.kw)
"""Exception Classes for Formulator"""
# These classes are placed here so that they can be imported into TTW Python
# scripts. To do so, add the following line to your Py script:
# from Products.Formulator.Errors import ValidationError, FormValidationError
from Products.PythonScripts.Utility import allow_class
class FormValidationError(Exception):
def __init__(self, errors, result):
Exception.__init__(self,"Form Validation Error")
self.errors = errors
self.result = result
allow_class(FormValidationError)
class ValidationError(Exception):
def __init__(self, error_key, field):
Exception.__init__(self, error_key)
self.error_key = error_key
self.field_id = field.id
self.field = field
self.error_text = field.get_error_message(error_key)
allow_class(ValidationError)
class FieldDisabledError(AttributeError):
def __init__(self, error_key, field):
AttributeError.__init__(self, error_key)
self.field_id = field.id
self.field = field
allow_class(FieldDisabledError)
import Globals
from AccessControl import ClassSecurityInfo
try:
import Products.FileSystemSite
except ImportError:
# use CMF product
from Products.CMFCore.CMFCorePermissions import View
from Products.CMFCore.FSObject import FSObject
from Products.CMFCore.DirectoryView import registerFileExtension,\
registerMetaType, expandpath
else:
# use FileSystemSite product
from Products.FileSystemSite.Permissions import View
from Products.FileSystemSite.FSObject import FSObject
from Products.FileSystemSite.DirectoryView import registerFileExtension,\
registerMetaType, expandpath
from Products.Formulator.Form import ZMIForm
from Products.Formulator.XMLToForm import XMLToForm
class FSForm(FSObject, ZMIForm):
"""FSForm."""
meta_type = 'Filesystem Formulator Form'
manage_options = (
(
{'label':'Customize', 'action':'manage_main'},
{'label':'Test', 'action':'formTest'},
)
)
_updateFromFS = FSObject._updateFromFS
security = ClassSecurityInfo()
security.declareObjectProtected(View)
def __init__(self, id, filepath, fullname=None, properties=None):
FSObject.__init__(self, id, filepath, fullname, properties)
def _createZODBClone(self):
# not implemented yet
return None
def _readFile(self, reparse):
file = open(expandpath(self._filepath), 'rb')
try:
data = file.read()
finally:
file.close()
# update the form with the xml data
try:
XMLToForm(data, self)
except:
# bare except here, but I hope this is ok, as the
# exception should be reraised
# (except if the LOG raises another one ...
# should we be more paranoid here?)
import zLOG
zLOG.LOG(
'Formulator.FSForm', zLOG.ERROR,
'error reading form from file ' +
expandpath(self._filepath))
raise
#### The following is mainly taken from Form.py ACCESSORS section ###
## def get_field_ids(self):
## self._updateFromFS()
## return ZMIForm.get_field_ids(self)
## def get_fields_in_group(self, group):
## self._updateFromFS()
## return ZMIForm.get_fields_in_group(self, group)
## def has_field(self, id):
## self._updateFromFS()
## return ZMIForm.has_field(self, id)
## def get_field(self, id):
## self._updateFromFS()
## return ZMIForm.get_field(self, id)
## def get_groups(self):
## self._updateFromFS()
## return ZMIForm.get_groups(self)
## def get_form_encoding(self):
## self._updateFromFS()
## return ZMIForm.get_form_encoding(self)
## def header(self):
## self._updateFromFS()
## return ZMIForm.header(self)
## def get_xml(self):
## self._updateFromFS()
## return ZMIForm.get_xml(self)
## def all_meta_types(self):
## self._updateFromFS()
## return ZMIForm.all_meta_types(self)
## security.declareProtected('View management screens', 'get_group_rows')
## def get_group_rows(self):
## self._updateFromFS()
## return ZMIForm.get_group_rows(self)
Globals.InitializeClass(FSForm)
registerFileExtension('form', FSForm)
registerMetaType('FSForm', FSForm)
import Globals
import Acquisition
from Globals import Persistent, DTMLFile
from AccessControl import ClassSecurityInfo
import OFS
from Shared.DC.Scripts.Bindings import Bindings
from Errors import ValidationError
from Products.Formulator.Widget import MultiItemsWidget
from zLOG import LOG
class Field:
"""Base class of all fields.
A field is an object consisting of a widget and a validator.
"""
security = ClassSecurityInfo()
# this is a field
is_field = 1
# this is not an internal field (can be overridden by subclass)
internal_field = 0
# can alternatively render this field with Zope's :record syntax
# this will be the record's name
field_record = None
def __init__(self, id, **kw):
self.id = id
# initialize values of fields in form
self.initialize_values(kw)
# initialize tales expression for fields in form
self.initialize_tales()
# initialize overrides of fields in form
self.initialize_overrides()
# initialize message values with defaults
message_values = {}
for message_name in self.validator.message_names:
message_values[message_name] = getattr(self.validator,
message_name)
self.message_values = message_values
security.declareProtected('Change Formulator Fields', 'initialize_values')
def initialize_values(self, dict):
"""Initialize values for properties, defined by fields in
associated form.
"""
values = {}
for field in self.form.get_fields(include_disabled=1):
id = field.id
value = dict.get(id, field.get_value('default'))
values[id] = value
self.values = values
security.declareProtected('Change Formulator Fields',
'initialize_tales')
def initialize_tales(self):
"""Initialize tales expressions for properties (to nothing).
"""
tales = {}
for field in self.form.get_fields():
id = field.id
tales[id] = ""
self.tales = tales
security.declareProtected('Change Formulator Fields',
'initialize_overrides')
def initialize_overrides(self):
"""Initialize overrides for properties (to nothing).
"""
overrides = {}
for field in self.form.get_fields():
id = field.id
overrides[id] = ""
self.overrides = overrides
security.declareProtected('Access contents information', 'has_value')
def has_value(self, id):
"""Return true if the field defines such a value.
"""
if self.values.has_key(id) or self.form.has_field(id):
return 1
else:
return 0
security.declareProtected('Access contents information', 'get_orig_value')
def get_orig_value(self, id):
"""Get value for id; don't do any override calculation.
"""
if self.values.has_key(id):
return self.values[id]
else:
return self.form.get_field(id).get_value('default')
security.declareProtected('Access contents information', 'get_value')
def get_value(self, id, **kw):
"""Get value for id.
Optionally pass keyword arguments that get passed to TALES
expression.
"""
tales_expr = self.tales.get(id, "")
if tales_expr:
# For some reason, path expressions expect 'here' and 'request'
# to exist, otherwise they seem to fail. python expressions
# don't seem to have this problem.
# add 'here' if not in kw
if not kw.has_key('here'):
kw['here'] = self.aq_parent
kw['request'] = self.REQUEST
value = tales_expr.__of__(self)(
field=self,
form=self.aq_parent, **kw)
else:
override = self.overrides.get(id, "")
if override:
# call wrapped method to get answer
value = override.__of__(self)()
else:
# get normal value
value = self.get_orig_value(id)
# if normal value is a callable itself, wrap it
if callable(value):
return value.__of__(self)
else:
return value
security.declareProtected('View management screens', 'get_override')
def get_override(self, id):
"""Get override method for id (not wrapped)."""
return self.overrides.get(id, "")
security.declareProtected('View management screens', 'get_tales')
def get_tales(self, id):
"""Get tales expression method for id."""
return self.tales.get(id, "")
security.declareProtected('Access contents information', 'is_required')
def is_required(self):
"""Check whether this field is required (utility function)
"""
return self.has_value('required') and self.get_value('required')
security.declareProtected('View management screens', 'get_error_names')
def get_error_names(self):
"""Get error messages.
"""
return self.validator.message_names
security.declareProtected('Access contents information',
'generate_field_key')
def generate_field_key(self, validation=0, key=None):
"""Generate the key Silva uses to render the field in the form.
"""
# Patched by JPS for ERP5 in order to
# dynamically change the name
if key is not None:
return 'field_%s' % key
if self.field_record is None:
return 'field_%s' % self.id
elif validation:
return self.id
elif isinstance(self.widget, MultiItemsWidget):
return "%s.%s:record:list" % (self.field_record, self.id)
else:
return '%s.%s:record' % (self.field_record, self.id)
def generate_subfield_key(self, id, validation=0, key=None):
"""Generate the key Silva uses to render a sub field.
Added key parameter for ERP5
Added key parameter for ERP5 in order to be compatible with listbox/matrixbox
"""
if key is None: key = self.id
if self.field_record is None or validation:
return 'subfield_%s_%s' % (key, id)
return '%s.subfield_%s_%s:record' % (self.field_record, key, id)
security.declareProtected('View management screens', 'get_error_message')
def get_error_message(self, name):
try:
return self.message_values[name]
except KeyError:
if name in self.validator.message_names:
return getattr(self.validator, name)
else:
return "Unknown error: %s" % name
security.declarePrivate('_render_helper')
def _render_helper(self, key, value, REQUEST, render_prefix=None):
value = self._get_default(key, value, REQUEST)
__traceback_info__ = ('key=%s value=%r' % (key, value))
if self.get_value('hidden', REQUEST=REQUEST):
return self.widget.render_hidden(self, key, value, REQUEST)
elif (not self.get_value('editable', REQUEST=REQUEST)):
return self.widget.render_view(self, value, REQUEST=REQUEST,
render_prefix=render_prefix)
else:
return self.widget.render(self, key, value, REQUEST,
render_prefix=render_prefix)
security.declarePrivate('_get_default')
def _get_default(self, key, value, REQUEST):
if value is not None:
return value
try:
value = REQUEST.form[key]
except (KeyError, AttributeError):
# fall back on default
return self.get_value('default')
# if we enter a string value while the field expects unicode,
# convert to unicode first
# this solves a problem when re-rendering a sticky form with
# values from request
if (self.has_value('unicode') and self.get_value('unicode') and
type(value) == type('')):
return unicode(value, self.get_form_encoding())
else:
return value
security.declarePrivate('_get_user_input_value')
def _get_user_input_value(self, key, REQUEST):
"""
Try to get a value of the field from the REQUEST
"""
return REQUEST.form[key]
security.declareProtected('View', 'render')
def render(self, value=None, REQUEST=None, key=None, render_prefix=None):
"""Render the field widget.
value -- the value the field should have (for instance
from validation).
REQUEST -- REQUEST can contain raw (unvalidated) field
information. If value is None, REQUEST is searched
for this value.
if value and REQUEST are both None, the 'default' property of
the field will be used for the value.
"""
return self._render_helper(self.generate_field_key(key=key), value, REQUEST,
render_prefix)
security.declareProtected('View', 'render_view')
def render_view(self, value=None, REQUEST=None, render_prefix=None):
"""Render value to be viewed.
"""
return self.widget.render_view(self, value, REQUEST=REQUEST)
security.declareProtected('View', 'render_pdf')
def render_pdf(self, value=None, REQUEST=None, key=None, **kw):
"""
render_pdf renders the field for reportlab
"""
return self.widget.render_pdf(self, value)
security.declareProtected('View', 'render_html')
def render_html(self, *args, **kw):
"""
render_html is used to as definition of render method in Formulator.
"""
return self.render(*args, **kw)
security.declareProtected('View', 'render_htmlgrid')
def render_htmlgrid(self, value=None, REQUEST=None, key=None, render_prefix=None):
"""
render_htmlgrid returns a list of tuple (title, html render)
"""
# What about CSS ? What about description ? What about error ?
widget_key = self.generate_field_key(key=key)
value = self._get_default(widget_key, value, REQUEST)
__traceback_info__ = ('key=%s value=%r' % (key, value))
return self.widget.render_htmlgrid(self, widget_key, value, REQUEST, render_prefix=render_prefix)
security.declareProtected('View', 'render_odf')
def render_odf(self, field=None, key=None, value=None, REQUEST=None,
render_format='ooo', render_prefix=None):
return self.widget.render_odf(self, key, value, REQUEST, render_format,
render_prefix)
security.declareProtected('View', 'render_css')
def render_css(self, REQUEST=None):
"""
Generate css content which will be added inline.
XXX key parameter may be needed.
"""
return self.widget.render_css(self, REQUEST)
security.declareProtected('View', 'get_css_list')
def get_css_list(self, REQUEST=None):
"""
Returns list of css sheets needed by the field
to be included in global css imports
"""
return self.widget.get_css_list(self, REQUEST)
security.declareProtected('View', 'get_javascript_list')
def get_javascript_list(self, REQUEST=None):
"""
Returns list of javascript needed by the field
to be included in global js imports
"""
return self.widget.get_javascript_list(self, REQUEST)
security.declareProtected('View', 'render_dict')
def render_dict(self, value=None, REQUEST=None, key=None, **kw):
"""
This is yet another field rendering. It is designed to allow code to
understand field's value data by providing its type and format when
applicable.
"""
return self.widget.render_dict(self, value)
def render_from_request(self, REQUEST):
"""Convenience method; render the field widget from REQUEST
(unvalidated data), or default if no raw data is found.
"""
return self._render_helper(self.generate_field_key(), None, REQUEST)
security.declareProtected('View', 'render_sub_field')
def render_sub_field(self, id, value=None, REQUEST=None, key=None, render_prefix=None):
"""Render a sub field, as part of complete rendering of widget in
a form. Works like render() but for sub field.
Added key parameter for ERP5 in order to be compatible with listbox/matrixbox
"""
return self.sub_form.get_field(id)._render_helper(
self.generate_subfield_key(id, key=key), value, REQUEST, render_prefix)
security.declareProtected('View', 'render_sub_field_from_request')
def render_sub_field_from_request(self, id, REQUEST):
"""Convenience method; render the field widget from REQUEST
(unvalidated data), or default if no raw data is found.
"""
return self.sub_form.get_field(id)._render_helper(
self.generate_subfield_key(id), None, REQUEST)
security.declarePrivate('_validate_helper')
def _validate_helper(self, key, REQUEST):
value = self.validator.validate(self, key, REQUEST)
# now call external validator after all other validation
external_validator = self.get_value('external_validator')
if external_validator and not external_validator(value, REQUEST):
self.validator.raise_error('external_validator_failed', self)
return value
security.declareProtected('View', 'validate')
def validate(self, REQUEST):
"""Validate/transform the field.
"""
return self._validate_helper(
self.generate_field_key(validation=1), REQUEST)
security.declareProtected('View', 'need_validate')
def need_validate(self, REQUEST):
"""Return true if validation is needed for this field.
"""
return self.validator.need_validate(
self, self.generate_field_key(validation=1), REQUEST)
security.declareProtected('View', 'validate_sub_field')
def validate_sub_field(self, id, REQUEST, key=None):
"""Validates a subfield (as part of field validation).
"""
return self.sub_form.get_field(id)._validate_helper(
self.generate_subfield_key(id, validation=1, key=key), REQUEST)
def PrincipiaSearchSource(self):
def getSearchSource(obj):
obj_type = type(obj)
if obj_type is MethodField.Method:
return obj.method_name
elif obj_type is TALESField.TALESMethod:
return obj._text
return str(obj)
return ''.join(map(getSearchSource, (self.values.values()+self.tales.values()+self.overrides.values())))
Globals.InitializeClass(Field)
class ZMIField(
Acquisition.Implicit,
Persistent,
OFS.SimpleItem.Item,
Field,
):
"""Base class for a field implemented as a Python (file) product.
"""
security = ClassSecurityInfo()
security.declareObjectProtected('View')
# the various tabs of a field
manage_options = (
{'label':'Edit', 'action':'manage_main',
'help':('Formulator', 'fieldEdit.txt')},
{'label':'TALES', 'action':'manage_talesForm',
'help':('Formulator', 'fieldTales.txt')},
{'label':'Override', 'action':'manage_overrideForm',
'help':('Formulator', 'fieldOverride.txt')},
{'label':'Messages', 'action':'manage_messagesForm',
'help':('Formulator', 'fieldMessages.txt')},
{'label':'Test', 'action':'fieldTest',
'help':('Formulator', 'fieldTest.txt')},
) + OFS.SimpleItem.SimpleItem.manage_options
security.declareProtected('View', 'title')
def title(self):
"""The title of this field."""
return self.get_value('title')
# display edit screen as main management screen
security.declareProtected('View management screens', 'manage_main')
manage_main = DTMLFile('dtml/fieldEdit', globals())
security.declareProtected('Change Formulator Fields', 'manage_edit')
def manage_edit(self, REQUEST):
"""Submit Field edit form.
"""
try:
# validate the form and get results
result = self.form.validate(REQUEST)
except ValidationError, err:
if REQUEST:
message = "Error: %s - %s" % (err.field.get_value('title'),
err.error_text)
return self.manage_main(self,REQUEST,
manage_tabs_message=message)
else:
raise
self._edit(result)
if REQUEST:
message="Content changed."
return self.manage_main(self,REQUEST,
manage_tabs_message=message)
security.declareProtected('Change Formulator Fields', 'manage_edit_xmlrpc')
def manage_edit_xmlrpc(self, map):
"""Edit Field Properties through XMLRPC
"""
# BEWARE: there is no validation on the values passed through the map
self._edit(map)
def _edit(self, result):
# first check for any changes
values = self.values
# if we are in unicode mode, convert result to unicode
# acquire get_unicode_mode and get_stored_encoding from form..
if self.get_unicode_mode():
new_result = {}
for key, value in result.items():
if type(value) == type(''):
# in unicode mode, Formulator UI always uses UTF-8
value = unicode(value, 'UTF-8')
new_result[key] = value
result = new_result
changed = []
for key, value in result.items():
# store keys for which we want to notify change
if not values.has_key(key) or values[key] != value:
changed.append(key)
# now do actual update of values
values.update(result)
self.values = values
# finally notify field of all changed values if necessary
for key in changed:
method_name = "on_value_%s_changed" % key
if hasattr(self, method_name):
getattr(self, method_name)(values[key])
security.declareProtected('Change Formulator Forms', 'manage_beforeDelete')
def manage_beforeDelete(self, item, container):
"""Remove name from list if object is deleted.
"""
# update group info in form
if hasattr(item.aq_explicit, 'is_field'):
container.field_removed(item.id)
security.declareProtected('Change Formulator Forms', 'manage_afterAdd')
def manage_afterAdd(self, item, container):
"""What happens when we add a field.
"""
# update group info in form
if hasattr(item.aq_explicit, 'is_field'):
container.field_added(item.id)
# methods screen
security.declareProtected('View management screens',
'manage_overrideForm')
manage_overrideForm = DTMLFile('dtml/fieldOverride', globals())
security.declareProtected('Change Formulator Forms', 'manage_override')
def manage_override(self, REQUEST):
"""Change override methods.
"""
try:
# validate the form and get results
result = self.override_form.validate(REQUEST)
except ValidationError, err:
if REQUEST:
message = "Error: %s - %s" % (err.field.get_value('title'),
err.error_text)
return self.manage_overrideForm(self,REQUEST,
manage_tabs_message=message)
else:
raise
# update overrides of field with results
if not hasattr(self, "overrides"):
self.overrides = result
else:
self.overrides.update(result)
self.overrides = self.overrides
if REQUEST:
message="Content changed."
return self.manage_overrideForm(self,REQUEST,
manage_tabs_message=message)
# tales screen
security.declareProtected('View management screens',
'manage_talesForm')
manage_talesForm = DTMLFile('dtml/fieldTales', globals())
security.declareProtected('Change Formulator Forms', 'manage_tales')
def manage_tales(self, REQUEST):
"""Change TALES expressions.
"""
try:
# validate the form and get results
result = self.tales_form.validate(REQUEST)
except ValidationError, err:
if REQUEST:
message = "Error: %s - %s" % (err.field.get_value('title'),
err.error_text)
return self.manage_talesForm(self,REQUEST,
manage_tabs_message=message)
else:
raise
self._edit_tales(result)
if REQUEST:
message="Content changed."
return self.manage_talesForm(self, REQUEST,
manage_tabs_message=message)
def _edit_tales(self, result):
if not hasattr(self, 'tales'):
self.tales = result
else:
self.tales.update(result)
self.tales = self.tales
security.declareProtected('Change Formulator Forms', 'manage_tales_xmlrpc')
def manage_tales_xmlrpc(self, map):
"""Change TALES expressions through XMLRPC.
"""
# BEWARE: there is no validation on the values passed through the map
from TALESField import TALESMethod
result = {}
for key, value in map.items():
result[key] = TALESMethod(value)
self._edit_tales(result)
# display test screen
security.declareProtected('View management screens', 'fieldTest')
fieldTest = DTMLFile('dtml/fieldTest', globals())
# messages screen
security.declareProtected('View management screens', 'manage_messagesForm')
manage_messagesForm = DTMLFile('dtml/fieldMessages', globals())
# field list header
security.declareProtected('View management screens', 'fieldListHeader')
fieldListHeader = DTMLFile('dtml/fieldListHeader', globals())
# field description display
security.declareProtected('View management screens', 'fieldDescription')
fieldDescription = DTMLFile('dtml/fieldDescription', globals())
security.declareProtected('Change Formulator Fields', 'manage_messages')
def manage_messages(self, REQUEST):
"""Change message texts.
"""
messages = self.message_values
unicode_mode = self.get_unicode_mode()
for message_key in self.get_error_names():
message = REQUEST[message_key]
if unicode_mode:
message = unicode(message, 'UTF-8')
messages[message_key] = message
self.message_values = messages
if REQUEST:
message="Content changed."
return self.manage_messagesForm(self,REQUEST,
manage_tabs_message=message)
security.declareProtected('View', 'index_html')
def index_html(self, REQUEST):
"""Render this field.
"""
return self.render(REQUEST=REQUEST)
security.declareProtected('Access contents information', '__getitem__')
def __getitem__(self, key):
return self.get_value(key)
security.declareProtected('View management screens', 'isTALESAvailable')
def isTALESAvailable(self):
"""Return true only if TALES is available.
"""
try:
from Products.PageTemplates.Expressions import getEngine
return 1
except ImportError:
return 0
Globals.InitializeClass(ZMIField)
PythonField = ZMIField # NOTE: for backwards compatibility
class ZClassField(Field):
"""Base class for a field implemented as a ZClass.
"""
pass
from Globals import DTMLFile
from HelpSys import HelpTopic
class FieldHelpTopic(HelpTopic.HelpTopic):
"""A special help topic for fields.
"""
meta_type = 'Help Topic'
def __init__(self, id, title, field_class,
permissions=None, categories=None):
self.id = id
self.title = title
self.field_class = field_class
if permissions is not None:
self.permissions = permissions
if categories is not None:
self.categories = categories
index_html = DTMLFile('dtml/FieldHelpTopic', globals())
def SearchableText(self):
"""Full text of the Help Topic, for indexing purposes."""
return "" # return self.index_html()
def get_groups(self):
"""Get form groups of this field.
"""
return self.field_class.form.get_groups()
def get_fields_in_group(self, group):
"""Get the fields in the group.
"""
return self.field_class.form.get_fields_in_group(group)
import os
import OFS
from Globals import ImageFile
from FieldHelpTopic import FieldHelpTopic
class FieldRegistry:
"""A registry of fields, maintaining a dictionary with
the meta_type of the field classes as key and the field class as
values. Updates the Form as necessary as well.
"""
def __init__(self):
"""Initializer of FieldRegistry.
"""
self._fields = {}
def get_field_class(self, fieldname):
"""Get a certain field class by its name (meta_type)
fieldname -- the name of the field to get from the registry
"""
return self._fields[fieldname]
def get_field_classes(self):
"""Return all fields.
"""
return self._fields
def registerField(self, field_class, icon=None):
"""Register field with Formulator.
field_class -- the class of the field to be registered
icon -- optional filename of the icon
"""
# put it in registry dictionary
self._fields[field_class.meta_type] = field_class
# set up dummy fields in field's form
initializeFieldForm(field_class)
# set up the icon if a filename is supplied
if icon:
setupIcon(field_class, icon, 'Formulator')
def registerFieldHelp(self, *args, **kw):
"""XXX: this is a quick fix to avoid bloating the ZODB.
Proper fix should only add FieldHelp when it's missing.
"""
pass
def initializeFields(self):
"""Initialize all field classes in field forms to use actual field
objects so we can finally eat our own dogfood.
"""
# for each field, realize fields in form
# this is finally possible as all field classes are now
# fully defined.
for field_class in self._fields.values():
field_class.form._realize_fields()
field_class.override_form._realize_fields()
field_class.tales_form._realize_fields()
# initialize registry as a singleton
FieldRegistry = FieldRegistry()
def initializeFieldForm(field_class):
"""Initialize the properties (fields and values) on a particular
field class. Also add the tales and override methods.
"""
from Form import BasicForm
from DummyField import fields
form = BasicForm()
override_form = BasicForm()
tales_form = BasicForm()
for field in getPropertyFields(field_class.widget):
form.add_field(field, "widget")
tales_field = fields.TALESField(field.id,
title=field.get_value('title'),
description="",
default="",
display_width=40,
required=0)
tales_form.add_field(tales_field, "widget")
method_field = fields.MethodField(field.id,
title=field.get_value("title"),
description="",
default="",
required=0)
override_form.add_field(method_field, "widget")
for field in getPropertyFields(field_class.validator):
form.add_field(field, "validator")
tales_field = fields.TALESField(field.id,
title=field.get_value('title'),
description="",
default="",
display_with=40,
required=0)
tales_form.add_field(tales_field, "validator")
method_field = fields.MethodField(field.id,
title=field.get_value("title"),
description="",
default="",
required=0)
override_form.add_field(method_field, "validator")
field_class.form = form
field_class.override_form = override_form
field_class.tales_form = tales_form
def getPropertyFields(obj):
"""Get property fields from a particular widget/validator.
"""
fields = []
for property_name in obj.property_names:
fields.append(getattr(obj, property_name))
return fields
def setupIcon(klass, icon, repository):
"""Load icon into Zope image object and put it in Zope's
repository for use by the ZMI, for a particular class.
klass -- the class of the field we're adding
icon -- the icon
"""
# set up misc_ respository if not existing yet
if not hasattr(OFS.misc_.misc_, repository):
setattr(OFS.misc_.misc_,
repository,
OFS.misc_.Misc_(repository, {}))
# get name of icon in the misc_ directory
icon_name = os.path.split(icon)[1]
# set up image object from icon file
icon_image = ImageFile(icon, globals())
icon_image.__roles__ = None
# put icon image object in misc_/Formulator/
getattr(OFS.misc_.misc_, repository)[icon_name] = icon_image
# set icon attribute in field_class to point to this image obj
setattr(klass, 'icon', 'misc_/%s/%s' %
(repository, icon_name))
import Globals, AccessControl
import OFS
from Acquisition import aq_base
from Globals import DTMLFile, Persistent
from AccessControl import ClassSecurityInfo
from AccessControl.Role import RoleManager
from OFS.ObjectManager import ObjectManager
from OFS.PropertyManager import PropertyManager
from OFS.SimpleItem import Item
import Acquisition
from urllib import quote
import os
import string
from StringIO import StringIO
from Errors import ValidationError, FormValidationError, FieldDisabledError
from FieldRegistry import FieldRegistry
from Widget import render_tag
from DummyField import fields
from FormToXML import formToXML
from XMLToForm import XMLToForm
from ComputedAttribute import ComputedAttribute
# FIXME: manage_renameObject hack needs these imports
from Acquisition import aq_base
from App.Dialogs import MessageDialog
from OFS.CopySupport import CopyError, eNotSupported
import sys
class Form:
"""Form base class.
"""
security = ClassSecurityInfo()
# need to make settings form upgrade
encoding = 'UTF-8'
stored_encoding = 'ISO-8859-1'
unicode_mode = 0
# CONSTRUCTORS
def __init__(self, action, method, enctype, name,
encoding, stored_encoding, unicode_mode):
"""Initialize form.
"""
# make groups dict with entry for default group
self.groups = {"Default": []}
# the display order of the groups
self.group_list = ["Default"]
# form submit info
self.name = name # for use by javascript
self.action = action
self.method = method
self.enctype = enctype
self.encoding = encoding
self.stored_encoding = stored_encoding
self.unicode_mode = unicode_mode
# MANIPULATORS
security.declareProtected('Change Formulator Forms', 'field_added')
def field_added(self, field_id, group=None):
"""A field was added to the form.
"""
# get indicated group or the first group if none was indicated
group = group or self.group_list[0]
# add it to the indicated group (create group if nonexistent)
groups = self.groups
field_list = groups.get(group, [])
field_list.append(field_id)
groups[group] = field_list
if group not in self.group_list:
self.group_list.append(group)
self.group_list = self.group_list
self.groups = groups
security.declareProtected('Change Formulator Forms', 'field_removed')
def field_removed(self, field_id):
"""A field was removed from the form.
"""
for field_list in self.groups.values():
if field_id in field_list:
field_list.remove(field_id)
break # should be done as soon as we found it once
self.groups = self.groups
security.declareProtected('Change Formulator Forms', 'move_field_up')
def move_field_up(self, field_id, group):
groups = self.groups
field_list = groups[group]
i = field_list.index(field_id)
if i == 0:
return 0 # can't move further up, so we're done
# swap fields, moving i up
field_list[i], field_list[i - 1] = field_list[i - 1], field_list[i]
self.groups = groups
return 1
security.declareProtected('Change Formulator Forms', 'move_field_down')
def move_field_down(self, field_id, group):
groups = self.groups
field_list = groups[group]
i = field_list.index(field_id)
if i == len(field_list) - 1:
return 0 # can't move further down, so we're done
# swap fields, moving i down
field_list[i], field_list[i + 1] = field_list[i + 1], field_list[i]
self.groups = groups
return 1
security.declareProtected('Change Formulator Forms', 'move_field_group')
def move_field_group(self, field_ids, from_group, to_group):
"""Moves a fields from one group to the other.
"""
if len(field_ids) == 0:
return 0
if from_group == to_group:
return 0
groups = self.groups
from_list = groups[from_group]
to_list = groups[to_group]
for field in self.get_fields_in_group(from_group, include_disabled=1)[:]:
if field.id in field_ids:
from_list.remove(field.id)
to_list.append(field.id)
self.groups = groups
return 1
security.declareProtected('Change Formulator Forms', 'add_group')
def add_group(self, group):
"""Add a new group.
"""
groups = self.groups
if groups.has_key(group):
return 0 # group already exists (NOTE: should we raise instead?)
groups[group] = []
# add the group to the bottom of the list of groups
self.group_list.append(group)
self.group_list = self.group_list
self.groups = groups
return 1
security.declareProtected('Change Formulator Forms', 'remove_group')
def remove_group(self, group):
"""Remove a group.
"""
groups = self.groups
if group == self.group_list[0]:
return 0 # can't remove first group
if not groups.has_key(group):
return 0 # group does not exist (NOTE: should we raise instead?)
# move whatever is in the group now to the end of the first group
groups[self.group_list[0]].extend(groups[group])
# now remove the key
del groups[group]
# remove it from the group order list as well
self.group_list.remove(group)
self.group_list = self.group_list
self.groups = groups
return 1
security.declareProtected('Change Formulator Forms', 'rename_group')
def rename_group(self, group, name):
"""Rename a group.
"""
group_list = self.group_list
groups = self.groups
if not groups.has_key(group):
return 0 # can't rename unexisting group
if groups.has_key(name):
return 0 # can't rename into existing name
i = group_list.index(group)
group_list[i] = name
groups[name] = groups[group]
del groups[group]
self.group_list = group_list
self.groups = groups
return 1
security.declareProtected('Change Formulator Forms', 'move_group_up')
def move_group_up(self, group):
"""Move a group up in the group list.
"""
group_list = self.group_list
i = group_list.index(group)
if i == 1:
return 0 # can't move further up, so we're done
# swap groups, moving i up
group_list[i], group_list[i - 1] = group_list[i - 1], group_list[i]
self.group_list = group_list
return 1
security.declareProtected('Change Formulator Forms', 'move_group_down')
def move_group_down(self, group):
"""Move a group down in the group list.
"""
group_list = self.group_list
i = group_list.index(group)
if i == len(group_list) - 1:
return 0 # can't move further up, so we're done
# swap groups, moving i down
group_list[i], group_list[i + 1] = group_list[i + 1], group_list[i]
self.group_list = group_list
return 1
# ACCESSORS
security.declareProtected('View', 'get_fields')
def get_fields(self, include_disabled=0):
"""Get all fields for all groups (in the display order).
"""
result = []
for group in self.get_groups(include_empty=1):
result.extend(self.get_fields_in_group(group, include_disabled))
return result
security.declareProtected('View', 'get_field_ids')
def get_field_ids(self, include_disabled=0):
"""Get all the ids of the fields in the form.
"""
result = []
for field in self.get_fields(include_disabled):
result.append(field.id)
return result
security.declareProtected('View', 'get_fields_in_group')
def get_fields_in_group(self, group, include_disabled=0):
"""Get all fields in a group (in the display order).
"""
result = []
for field_id in self.groups.get(group, []):
try:
field = self.get_field(field_id, include_disabled)
except FieldDisabledError:
pass
else:
result.append(field)
return result
security.declareProtected('View', 'has_field')
def has_field(self, id, include_disabled):
"""Check whether the form has a field of a certain id.
"""
# define in subclass
pass
security.declareProtected('View', 'get_field')
def get_field(self, id):
"""Get a field of a certain id.
"""
# define in subclass
pass
security.declareProtected('View', 'get_groups')
def get_groups(self, include_empty=0):
"""Get a list of all groups, in display order.
If include_empty is false, suppress groups that do not have
enabled fields.
"""
if include_empty:
return self.group_list
return [group for group in self.group_list
if self.get_fields_in_group(group)]
security.declareProtected('View', 'get_form_encoding')
def get_form_encoding(self):
"""Get the encoding the form is in. Should be the same as the
encoding of the page, if specified, for unicode to work. Default
is 'UTF-8'.
"""
return getattr(self, 'encoding', 'UTF-8')
security.declareProtected('View', 'get_stored_encoding')
def get_stored_encoding(self):
"""Get the encoding of the stored field properties.
"""
return getattr(self, 'stored_encoding', 'ISO-8859-1')
security.declareProtected('View', 'get_unicode_mode')
def get_unicode_mode(self):
"""Get unicode mode information.
"""
return getattr(self, 'unicode_mode', 0)
security.declareProtected('View', 'render')
def render(self, dict=None, REQUEST=None):
"""Render form in a default way.
"""
dict = dict or {}
result = StringIO()
w = result.write
w(self.header())
for group in self.get_groups():
w('<h2>%s</h2>\n' % group)
w('<table border="0" cellspacing="0" cellpadding="2">\n')
for field in self.get_fields_in_group(group):
if dict.has_key(field.id):
value = dict[field.id]
else:
value = None
w('<tr>\n')
if not field.get_value('hidden'):
w('<td>%s</td>\n' % field.get_value('title'))
else:
w('<td></td>')
w('<td>%s</td>\n' % field.render(value, REQUEST))
w('</tr>\n')
w('</table>\n')
w('<input type="submit" value=" OK ">\n')
w(self.footer())
return result.getvalue()
security.declareProtected('View', 'render_view')
def render_view(self, dict=None):
"""Render contents (default simplistic way).
"""
dict = dict or {}
result = StringIO()
w = result.write
for group in self.get_groups():
w('<h2>%s</h2>\n' % group)
w('<table border="0" cellspacing="0" cellpadding="2">\n')
for field in self.get_fields_in_group(group):
if dict.has_key(field.id):
value = dict[field.id]
else:
value = None
w('<tr>\n')
w('<td>%s</td>\n' % field.get_value('title'))
w('<td>%s</td>\n' % field.render_view(value))
w('</tr>\n')
w('</table>\n')
return result.getvalue()
security.declareProtected('View', 'validate')
def validate(self, REQUEST):
"""Validate all enabled fields in this form. Stop validating and
pass up ValidationError if any occurs.
"""
result = {}
for field in self.get_fields():
# skip any fields we don't need to validate
if not field.need_validate(REQUEST):
continue
value = field.validate(REQUEST)
# store under id
result[field.id] = value
# store as alternate name as well if necessary
alternate_name = field.get_value('alternate_name')
if alternate_name:
result[alternate_name] = value
return result
security.declareProtected('View', 'validate_to_request')
def validate_to_request(self, REQUEST):
"""Validation, stop validating as soon as error.
"""
result = self.validate(REQUEST)
for key, value in result.items():
REQUEST.set(key, value)
return result
security.declareProtected('View', 'validate_all')
def validate_all(self, REQUEST):
"""Validate all enabled fields in this form, catch any ValidationErrors
if they occur and raise a FormValidationError in the end if any
Validation Errors occured.
"""
result = {}
errors = []
for field in self.get_fields():
# skip any field we don't need to validate
if not field.need_validate(REQUEST):
continue
try:
value = field.validate(REQUEST)
# store under id
result[field.id] = value
# store as alternate name as well if necessary
alternate_name = field.get_value('alternate_name')
if alternate_name:
result[alternate_name] = value
except ValidationError, err:
errors.append(err)
if len(errors) > 0:
raise FormValidationError(errors, result)
return result
security.declareProtected('View', 'validate_all_to_request')
def validate_all_to_request(self, REQUEST):
"""Validation, continue validating all fields, catch errors.
Everything that could be validated will be added to REQUEST.
"""
try:
result = self.validate_all(REQUEST)
except FormValidationError, e:
# put whatever result we have in REQUEST
for key, value in e.result.items():
REQUEST.set(key, value)
# reraise exception
raise
for key, value in result.items():
REQUEST.set(key, value)
return result
security.declareProtected('View', 'session_store')
def session_store(self, session, REQUEST):
"""Store form data in REQUEST into session.
"""
data = session.getSessionData()
for field in self.get_fields():
id = field.id
data.set(id, REQUEST[id])
security.declareProtected('View', 'session_retrieve')
def session_retrieve(self, session, REQUEST):
"""Retrieve form data from session into REQUEST.
"""
data = session.getSessionData()
for field in self.get_fields():
id = field.id
REQUEST.set(id, data.get(id))
security.declareProtected('View', 'header')
def header(self):
"""Starting form tag.
"""
# FIXME: backwards compatibility; name attr may not be present
if not hasattr(self, "name"):
self.name = ""
name = self.name
if self.enctype is not "":
if name:
return render_tag("form",
name=name,
action=self.action,
method=self.method,
enctype=self.enctype) + ">"
else:
return render_tag("form",
action=self.action,
method=self.method,
enctype=self.enctype) + ">"
else:
if name:
return render_tag("form",
name=name,
action=self.action,
method=self.method) + ">"
else:
return render_tag("form",
action=self.action,
method=self.method) + ">"
security.declareProtected('View', 'footer')
def footer(self):
"""Closing form tag.
"""
return "</form>"
security.declareProtected('Change Formulator Forms', 'get_xml')
def get_xml(self):
"""Get this form in XML serialization.
"""
return formToXML(self)
security.declareProtected('Change Formulator Forms', 'set_xml')
def set_xml(self, xml, override_encoding=None):
"""change form according to xml"""
XMLToForm(xml, self, override_encoding)
def _management_page_charset(self):
if not self.unicode_mode:
return self.stored_encoding
else:
return 'UTF-8'
security.declareProtected('Access contents information',
'management_page_charset')
management_page_charset = ComputedAttribute(_management_page_charset)
security.declareProtected('View', 'set_encoding_header')
def set_encoding_header(self):
"""Set the encoding in the RESPONSE object.
This can be used to make sure a page is in the same encoding the
textual form contents is in.
"""
if not self.unicode_mode:
encoding = self.stored_encoding
else:
encoding = 'UTF-8'
self.REQUEST.RESPONSE.setHeader(
'Content-Type',
'text/html;charset=%s' % encoding)
Globals.InitializeClass(Form)
class BasicForm(Persistent, Acquisition.Implicit, Form):
"""A form that manages its own fields, not using ObjectManager.
Can contain dummy fields defined by DummyField.
"""
security = ClassSecurityInfo()
def __init__(self, action="", method="POST", enctype="", name="",
encoding="UTF-8", stored_encoding='ISO-8859-1',
unicode_mode=0):
BasicForm.inheritedAttribute('__init__')(
self, action, method, enctype,
name, encoding, stored_encoding, unicode_mode)
self.title = 'Basic Form' # XXX to please FormToXML..
self.fields = {}
security.declareProtected('Change Formulator Forms', 'add_field')
def add_field(self, field, group=None):
"""Add a field to the form to a certain group.
"""
# update group info
self.field_added(field.id, group)
# add field to list
self.fields[field.id] = field
self.fields = self.fields
security.declareProtected('Change Formulator Forms', 'add_fields')
def add_fields(self, fields, group=None):
"""Add a number of fields to the form at once (in a group).
"""
for field in fields:
self.add_field(field, group)
security.declareProtected('Change Formulator Forms', 'remove_field')
def remove_field(self, field):
"""Remove field from form.
"""
# update group info
self.field_removed(field.id)
# remove field from list
del self.fields[field.id]
self.fields = self.fields
security.declareProtected('View', 'has_field')
def has_field(self, id, include_disabled=0):
"""Check whether the form has a field of a certain id.
If disabled fields are not included, pretend they're not there.
"""
field = self.fields.get(id, None)
if field is None:
return 0
return include_disabled or field.get_value('enabled')
security.declareProtected('View', 'get_field')
def get_field(self, id, include_disabled=0):
"""get a field of a certain id."""
field = self.fields[id]
if include_disabled or field.get_value('enabled'):
return field
raise FieldDisabledError("Field %s is disabled" % id, field)
def _realize_fields(self):
"""Make the fields in this form actual fields, not just dummy fields.
"""
for field in self.get_fields(include_disabled=1):
if hasattr(field, 'get_real_field'):
field = field.get_real_field()
self.fields[field.id] = field
self.fields = self.fields
Globals.InitializeClass(BasicForm)
def create_settings_form():
"""Create settings form for ZMIForm.
"""
form = BasicForm('manage_settings')
title = fields.StringField('title',
title="Title",
required=0,
default="")
row_length = fields.IntegerField('row_length',
title='Number of groups in row (in order tab)',
required=1,
default=4)
name = fields.StringField('name',
title="Form name",
required=0,
default="")
action = fields.StringField('action',
title='Form action',
required=0,
default="")
method = fields.ListField('method',
title='Form method',
items=[('POST', 'POST'),
('GET', 'GET')],
required=1,
size=1,
default='POST')
enctype = fields.ListField('enctype',
title='Form enctype',
items=[('No enctype', ""),
('application/x-www-form-urlencoded',
'application/x-www-form-urlencoded'),
('multipart/form-data',
'multipart/form-data')],
required=0,
size=1,
default=None)
encoding = fields.StringField('encoding',
title='Encoding of pages the form is in',
default="UTF-8",
required=1)
stored_encoding = fields.StringField('stored_encoding',
title='Encoding of form properties',
default='ISO-8859-1',
required=1)
unicode_mode = fields.CheckBoxField('unicode_mode',
title='Form properties are unicode',
default=0,
required=1)
form.add_fields([title, row_length, name, action, method,
enctype, encoding, stored_encoding, unicode_mode])
return form
class ZMIForm(ObjectManager, PropertyManager, RoleManager, Item, Form):
"""
A Formulator Form, fields are managed by ObjectManager.
"""
meta_type = "Formulator Form"
security = ClassSecurityInfo()
# should be helpful with ZClasses, but not sure why I
# had it in here as a comment in the first place..
security.declareObjectProtected('View')
# the tabs we want to show
manage_options = (
(
{'label':'Contents', 'action':'manage_main',
'help':('Formulator', 'formContents.txt')},
{'label':'Test', 'action':'formTest',
'help':('Formulator', 'formTest.txt')},
{'label':'Order', 'action':'formOrder',
'help':('Formulator', 'formOrder.txt')},
{'label':'Settings', 'action':'formSettings',
'help':('Formulator', 'formSettings.txt')},
{'label':'XML', 'action':'formXML',
'help':('Formulator', 'formXML.txt')},
) +
PropertyManager.manage_options +
RoleManager.manage_options +
Item.manage_options
)
def __init__(self, id, title, unicode_mode=0):
"""Initialize form.
id -- id of form
title -- the title of the form
"""
ZMIForm.inheritedAttribute('__init__')(self, "", "POST", "", id,
'UTF-8', 'ISO-8859-1',
unicode_mode)
self.id = id
self.title = title
self.row_length = 4
def all_meta_types(self):
"""Get all meta types addable to this field. The ZMI uses
this method (original defined in ObjectManager).
"""
return self._meta_types
def manage_renameObject(self, id, new_id, REQUEST=None):
"""Rename a particular sub-object, the *old* way.
FIXME: hack that could be removed once Zope 2.4.x
goes back to a useful semantics..."""
try: self._checkId(new_id)
except: raise CopyError, MessageDialog(
title='Invalid Id',
message=sys.exc_info()[1],
action ='manage_main')
ob=self._getOb(id)
if not ob.cb_isMoveable():
raise CopyError, eNotSupported % id
self._verifyObjectPaste(ob)
try: ob._notifyOfCopyTo(self, op=1)
except: raise CopyError, MessageDialog(
title='Rename Error',
message=sys.exc_info()[1],
action ='manage_main')
self._delObject(id)
ob = aq_base(ob)
ob._setId(new_id)
# Note - because a rename always keeps the same context, we
# can just leave the ownership info unchanged.
self._setObject(new_id, ob, set_owner=0)
if REQUEST is not None:
return self.manage_main(self, REQUEST, update_menu=1)
return None
#security.declareProtected('View', 'get_fields_raw')
#def get_fields_raw(self):
# """Get all fields, in arbitrary order.
# """
# return filter(lambda obj: hasattr(obj.aq_explicit, 'is_field'),
# self.objectValues())
security.declareProtected('View', 'has_field')
def has_field(self, id, include_disabled=0):
"""Check whether the form has a field of a certain id.
"""
field = self._getOb(id, None)
if field is None or not hasattr(aq_base(field), 'is_field'):
return 0
return include_disabled or field.get_value('enabled')
security.declareProtected('View', 'get_field')
def get_field(self, id, include_disabled=0):
"""Get a field of a certain id
"""
field = self._getOb(id, None)
if field is None or not hasattr(aq_base(field), 'is_field'):
raise AttributeError, "No field %s" % id
if include_disabled or field.get_value('enabled'):
return field
raise FieldDisabledError("Field %s disabled" % id, field)
security.declareProtected('Change Formulator Forms', 'manage_addField')
def manage_addField(self, id, title, fieldname, REQUEST=None):
"""Add a new field to the form.
id -- the id of the field to add
title -- the title of the field to add; this will be used in
displays of the field on forms
fieldname -- the name of the field (meta_type) to add
Result -- empty string
"""
title = string.strip(title)
if not title:
title = id # title is always required, use id if not provided
# get the field class we want to add
field_class = FieldRegistry.get_field_class(fieldname)
# create field instance
field = field_class(id, title=title, description="")
# add the field to the form
id = self._setObject(id, field)
# respond to add_and_edit button if necessary
add_and_edit(self, id, REQUEST)
return ''
security.declareProtected('View management screens', 'formTest')
formTest = DTMLFile('dtml/formTest', globals())
settings_form = create_settings_form()
security.declareProtected('View management screens', 'formSettings')
formSettings = DTMLFile('dtml/formSettings', globals())
security.declareProtected('View management screens', 'formOrder')
formOrder = DTMLFile('dtml/formOrder', globals())
security.declareProtected('View management screens', 'formXML')
formXML = DTMLFile('dtml/formXML', globals())
security.declareProtected('Change Formulator Forms', 'manage_editXML')
def manage_editXML(self, form_data, REQUEST):
"""Change form using XML.
"""
self.set_xml(form_data)
return self.formXML(self, REQUEST,
manage_tabs_message="Changed form")
security.declareProtected('Change Formulator Forms', 'manage_settings')
def manage_settings(self, REQUEST):
"""Change settings in settings screen.
"""
try:
result = self.settings_form.validate_all(REQUEST)
except FormValidationError, e:
message = "Validation error(s).<br />" + string.join(
map(lambda error: "%s: %s" % (error.field.get_value('title'),
error.error_text), e.errors), "<br />")
return self.formSettings(self, REQUEST,
manage_tabs_message=message)
# if we need to switch encoding, get xml representation before setting
if result['unicode_mode'] != self.unicode_mode:
xml = self.get_xml()
# now set the form settings
# convert XML to or from unicode mode if necessary
unicode_message = None
if result['unicode_mode'] != self.unicode_mode:
# get XML (using current stored_encoding)
xml = self.get_xml()
# now save XML data again using specified encoding
if result['unicode_mode']:
encoding = 'unicode'
unicode_message = "Converted to unicode."
else:
encoding = result['stored_encoding']
unicode_message = ("Converted from unicode to %s encoding" %
encoding)
self.set_xml(xml, encoding)
# now set the form settings
for key, value in result.items():
setattr(self, key, value)
message="Settings changed."
if unicode_message is not None:
message = message + ' ' + unicode_message
return self.formSettings(self, REQUEST,
manage_tabs_message=message)
security.declareProtected('Change Formulator Forms', 'manage_refresh')
def manage_refresh(self, REQUEST):
"""Refresh internal data structures of this form.
FIXME: this doesn't work right now
"""
# self.update_groups()
REQUEST.RESPONSE.redirect('manage_main')
security.declarePrivate('_get_field_ids')
def _get_field_ids(self, group, REQUEST):
"""Get the checked field_ids that we're operating on
"""
field_ids = []
for field in self.get_fields_in_group(group, include_disabled=1):
if REQUEST.form.has_key(field.id):
field_ids.append(field.id)
return field_ids
security.declareProtected('View management screens',
'get_group_rows')
def get_group_rows(self):
"""Get the groups in rows (for the order screen).
"""
row_length = self.row_length
groups = self.get_groups(include_empty=1)
# get the amount of rows
rows = len(groups) / row_length
# if we would have extra groups not in a row, add a row
if len(groups) % self.row_length != 0:
rows = rows + 1
# now create a list of group lists and return it
result = []
for i in range(rows):
start = i * row_length
result.append(groups[start: start + row_length])
return result
security.declareProtected('View', 'get_largest_group_length')
def get_largest_group_length(self):
"""Get the largest group length available; necessary for
'order' screen user interface.
"""
max = 0
for group in self.get_groups(include_empty=1):
fields = self.get_fields_in_group(group)
if len(fields) > max:
max = len(fields)
return max
security.declareProtected('Change Formulator Forms',
'manage_move_field_up')
def manage_move_field_up(self, group, REQUEST):
"""Moves up a field in a group.
"""
field_ids = self._get_field_ids(group, REQUEST)
if (len(field_ids) == 1 and
self.move_field_up(field_ids[0], group)):
message = "Field %s moved up." % field_ids[0]
else:
message = "Can't move field up."
return self.formOrder(self, REQUEST,
manage_tabs_message=message)
security.declareProtected('Change Formulator Forms',
'manage_move_field_down')
def manage_move_field_down(self, group, REQUEST):
"""Moves down a field in a group.
"""
field_ids = self._get_field_ids(group, REQUEST)
if (len(field_ids) == 1 and
self.move_field_down(field_ids[0], group)):
message = "Field %s moved down." % field_ids[0]
else:
message = "Can't move field down."
return self.formOrder(self, REQUEST,
manage_tabs_message=message)
security.declareProtected('Change Formulator Forms',
'manage_move_group')
def manage_move_group(self, group, to_group, REQUEST):
"""Moves fields to a different group.
"""
field_ids = self._get_field_ids(group, REQUEST)
if (to_group != 'Move to:' and
self.move_field_group(field_ids, group, to_group)):
fields = string.join(field_ids, ", ")
message = "Fields %s transferred from %s to %s." % (fields,
group,
to_group)
else:
message = "Can't transfer fields."
return self.formOrder(self, REQUEST,
manage_tabs_message=message)
security.declareProtected('Change Formulator Forms',
'manage_add_group')
def manage_add_group(self, new_group, REQUEST):
"""Adds a new group.
"""
group = string.strip(new_group)
if (group and group != 'Select group' and
self.add_group(group)):
message = "Group %s created." % (group)
else:
message = "Can't create group."
return self.formOrder(self, REQUEST,
manage_tabs_message=message)
security.declareProtected('Change Formulator Forms',
'manage_remove_group')
def manage_remove_group(self, group, REQUEST):
"""Removes group.
"""
if self.remove_group(group):
message = "Group %s removed." % (group)
else:
message = "Can't remove group."
return self.formOrder(self, REQUEST,
manage_tabs_message=message)
security.declareProtected('Change Formulator Forms',
'manage_rename_group')
def manage_rename_group(self, group, REQUEST):
"""Renames group.
"""
if REQUEST.has_key('new_name'):
new_name = string.strip(REQUEST['new_name'])
if self.rename_group(group, new_name):
message = "Group %s renamed to %s." % (group, new_name)
else:
message = "Can't rename group."
else:
message = "No new name supplied."
return self.formOrder(self, REQUEST,
manage_tabs_message=message)
security.declareProtected('Change Formulator Forms',
'manage_move_group_up')
def manage_move_group_up(self, group, REQUEST):
"""Move a group up.
"""
if self.move_group_up(group):
message = "Group %s moved up." % group
else:
message = "Can't move group %s up" % group
return self.formOrder(self, REQUEST,
manage_tabs_message=message)
security.declareProtected('Change Formulator Forms',
'manage_move_group_down')
def manage_move_group_down(self, group, REQUEST):
"""Move a group down.
"""
if self.move_group_down(group):
message = "Group %s moved down." % group
else:
message = "Can't move group %s down" % group
return self.formOrder(self, REQUEST,
manage_tabs_message=message)
PythonForm = ZMIForm # NOTE: backwards compatibility
Globals.InitializeClass(ZMIForm)
manage_addForm = DTMLFile("dtml/formAdd", globals())
def manage_add(self, id, title="", unicode_mode=0, REQUEST=None):
"""Add form to folder.
id -- the id of the new form to add
title -- the title of the form to add
Result -- empty string
"""
# add actual object
id = self._setObject(id, ZMIForm(id, title, unicode_mode))
# respond to the add_and_edit button if necessary
add_and_edit(self, id, REQUEST)
return ''
def add_and_edit(self, id, REQUEST):
"""Helper method to point to the object's management screen if
'Add and Edit' button is pressed.
id -- id of the object we just added
"""
if REQUEST is None:
return
try:
u = self.DestinationURL()
except:
u = REQUEST['URL1']
if hasattr(REQUEST, 'submit_add_and_edit'):
u = "%s/%s" % (u, quote(id))
REQUEST.RESPONSE.redirect(u+'/manage_main')
def initializeForm(field_registry):
"""Sets up ZMIForm with fields from field_registry.
"""
form_class = ZMIForm
meta_types = []
for meta_type, field in field_registry.get_field_classes().items():
# don't set up in form if this is a field for internal use only
if field.internal_field:
continue
# set up individual add dictionaries for meta_types
dict = { 'name': field.meta_type,
'action':
'manage_addProduct/Formulator/manage_add%sForm' % meta_type }
meta_types.append(dict)
# set up add method
setattr(form_class,
'manage_add%sForm' % meta_type,
DTMLFile('dtml/fieldAdd', globals(), fieldname=meta_type))
# set up meta_types that can be added to form
form_class._meta_types = tuple(meta_types)
# set up settings form
form_class.settings_form._realize_fields()
from StringIO import StringIO
from cgi import escape
import types
#def write(s):
# if type(s) == type(u''):
# print "Unicode:", repr(s)
def formToXML(form, prologue=1):
"""Takes a formulator form and serializes it to an XML representation.
"""
f = StringIO()
write = f.write
if prologue:
write('<?xml version="1.0"?>\n\n')
write('<form>\n')
# export form settings
for field in form.settings_form.get_fields(include_disabled=1):
id = field.id
value = getattr(form, id)
if id == 'title':
value = escape(value)
if id == 'unicode_mode':
if value:
value = 'true'
else:
value = 'false'
write(' <%s>%s</%s>\n' % (id, value, id))
# export form groups
write(' <groups>\n')
for group in form.get_groups(include_empty=1):
write(' <group>\n')
write(' <title>%s</title>\n' % escape(group))
write(' <fields>\n\n')
for field in form.get_fields_in_group(group, include_disabled=1):
write(' <field><id>%s</id> <type>%s</type>\n' % (field.id, field.meta_type))
write(' <values>\n')
items = field.values.items()
items.sort()
for key, value in items:
if value is None:
continue
if value==True: # XXX Patch
value = 1 # XXX Patch
if value==False: # XXX Patch
value = 0 # XXX Patch
if callable(value): # XXX Patch
write(' <%s type="method">%s</%s>\n' % # XXX Patch
(key, escape(str(value.method_name)), key)) # XXX Patch
elif type(value) == type(1.1):
write(' <%s type="float">%s</%s>\n' % (key, escape(str(value)), key))
elif type(value) == type(1):
write(' <%s type="int">%s</%s>\n' % (key, escape(str(value)), key))
elif type(value) == type([]):
write(' <%s type="list">%s</%s>\n' % (key, escape(str(value)), key))
else:
if type(value) not in (types.StringType, types.UnicodeType):
value = str(value)
write(' <%s>%s</%s>\n' % (key, escape(value), key))
write(' </values>\n')
write(' <tales>\n')
items = field.tales.items()
items.sort()
for key, value in items:
if value:
write(' <%s>%s</%s>\n' % (key, escape(str(value._text)), key))
write(' </tales>\n')
write(' <messages>\n')
for message_key in field.get_error_names():
write(' <message name="%s">%s</message>\n' %
(escape(message_key), escape(field.get_error_message(message_key))))
write(' </messages>\n')
write(' </field>\n')
write(' </fields>\n')
write(' </group>\n')
write(' </groups>\n')
write('</form>')
if form.unicode_mode:
return f.getvalue().encode('UTF-8')
else:
return unicode(f.getvalue(), form.stored_encoding).encode('UTF-8')
Formulator changes
1.6.1
Bugs Fixed
- Adding Fields to empty Groups had not been possible
- ZMI "Order" tab of an empty form did raise an exception
1.6.0
Features Added
- FileSystemSite/DirectoryView improvements:
* XML filesystem representation of Formulator forms can now
also be used with CMF (if FileSystemSite is not installed).
* FSForm gets automatically registered with the directory
view system if CMF or FileSystemSite is installed.
- Infrastructure for Validators not to get taken into account in
validation procedures (need_validate).
- A new label field. Doesn't participate in validation. It shows
its text as a label in the form.
- Unicode mode. A form can now be put in 'unicode mode', which
means it stores all its textual data as unicode strings. This
allows for easier integration with Zope systems that use unicode
internally, such as Silva.
- Disabling of fields. A field can now be disabled from being
displayed or validated by unchecking the 'Enabled' validator
property. This can be done dynamically as well using TALES
overrides.
Bugs Fixed
- The css_class value of a DateTime field had been ignored. It
is now properly passed down to its subfields, so all subfield
elements are rendered with the given css_class value.
1.5.0
Features Added
- Added ProductForm, which provides a wrapping around
Formulator.BasicForm, allowing it to be created inside a
product but used outside it.
- Allow turning off of XML prologue section.
- Optimization of TALESMethod by caching compiled expression.
This speeds SilvaMetadata indexing up by a lot if a fallback
on default is made, especially in the case of Python
expressions, as it avoids lots of compilation overhead.
- Extra attribute defined for list/multicheckbox/radio fields
called 'extra_item', which allows setting HTML attributes to
individual list item/checkbox/radio button.
Bugs Fixed
- XML serialization should be more consistent now; field properties
are now ordered by name upon serialization.
- Allow XML export of BasicForm.
1.4.2
Bugs Fixed
- Sticky forms should now work correctly in the presence of unicode.
Encoded data is automatically converted to unicode if the information
is pulled from the REQUEST form.
1.4.1
Bugs Fixed
- It was not possible to make DateTime fields not required when
'allow_empty_time' was enabled. Fixed.
1.4.0
Features Added
- Added limited ability to output unicode for selected
fields. Only works properly in Zope 2.6.x, and the HTML pages
these forms are in need an output encoding set (such as
UTF-8, which is also Formulator's default encoding). If
'unicode' checkbox is checked Formulator will try to interpret
its input in the Form's encoding (default is UTF-8). It will
also try to display its values in that encoding. Note that
only field values and items currently work with unicode -- the
rest of the textual properties of a field are still stored as
8-bits. If you make sure that these properties are encoded as
UTF-8 (or whatever encoding you choose for the form) things
should be okay, however.
- Can now also change forms using XML (not just view it).
- DateTime fields can now optionally input AM/PM.
- DateTime fields can now optionally be set to allow time to
be left empty.
- 'whitespace_preserve' option on string type fields. If turned on,
whitespace will not be automatically stripped and will count as
input.
- 'render_view' method on fields to render the value outside a
widget.
- Added some code support used by SilvaMetadata to enable rendering
of fields with Zope's ':record' syntax.
Bugs Fixed
- Fixed a Python2.2 compatibility bug in XMLObjects.py
- DateTimeField now picks up default values from REQUEST
properly if necessary.
- XML representation of the LinkField "check_timeout" value
messed the type="float" attribute.
- Additional unit tests.
1.3.1 (2002/12/20)
Features Added
- Error messages can now be included in the XML serialization.
- Ability to encode lists as a special type in values.
Bugs Fixed
- Some more proper encodings.
- Handle case where group has no field.
- Handle DateTime field better.
1.3.0 (2002/11/26)
Features Added
- FormToXML and XMLToForm modules have functions to serialize
(most of) form to XML and read it in again (over an existing
form).
- New XML tab for forms which shows the XML serialization (no
saving option yet).
- FSForm.py uses XML serialization to provide a formulator form
version for FileSystemSite. It does not get imported by
default.
Bugs Fixed
- The email validator has an improved regular expression.
- Fix error that occured when trying to render DateTimeField as
hidden.
1.2.0 (2002/03/06)
Features Added
- Changes to exception infrastructure so errors can now be
imported and caught in a through the web Python script. Example::
from Products.Formulator.Errors import ValidationError, FormValidationError
- added __getitem__ to Field so instead of using get_value() you can
also do this in Python: form.field['title'], and in ZPT you can
use this in path expressions: form/field/title
- made a start with Formulator unit tests; some validators get
automatically tested now.
Bugs Fixed
- Removed dependencies of the name of 'Add and Edit' button to make
internationalization of the management interface easier.
- added permission to make ZClasses work a bit better (but they
still don't cooperate well with Formulator, I think. I don't use
ZClasses, so I hope to hear from this from ZClass users)
- Form's properties tab now visible and form tabs stopped
misbehaving.
- Lists and such should handle multiple items with the same value
a bit better, selecting only one.
- the LinkField now checks site-internal links better.
1.1.0 (2001/10/26)
Bugs Fixed
- Fixed bug in form settings tab.
- the LinkField now checks site-internal links better.
1.0.9 (2001/10/05)
Features Added
- New TALES tab for fields as a more powerful Override tab;
PageTemplates needs to be installed to make it work.
- added 'name' attribute for forms. When the form header is
rendered, name will be an attribute. This can be used to
control forms with Javascript.
Bugs Fixed
- More compliance with Zope product guidelines; moved dtml
files from www dir to dtml dir.
- Fixed a bug in that form titles would not work. Forms now have
titles, and you can change them in the settings tab. (Formulator
does not use the title property internally though)
1.0.1 (2001/07/27)
Bugs Fixed
- Fixed bug with renaming groups. Previously, renamed groups were not
properly stored in the ZODB.
- Made MultiSelectionValidator (used by MultiListField among others)
deal better with integer values.
- Hacked around CopySupport changes in Zope 2.4.0; renames work
again now.
1.0 (2001/07/10)
Features Added
- New field: RawTextAreaField. A textarea field that doesn't
do a lot of processing on the text input.
- Checked in BSD license text.
Bugs Fixed
- Fixed minor bug in year handling of DateTimeField.
- Now hidden fields also take text from 'extra' property.
- Fixed bug in MultiItemsWidget; would not deal with only a
single item being selected.
0.9.5 (2001/06/27)
Features Added
- Added FileField (with browse button). Can be used to upload
files if form is set to multipart/form-data.
- Added LinkField for URLs.
- Made ListField and RadioField more tolerant of integer
(and possibly other) values, not only strings.
- Made ListField and RadioField happy to deal with non-tuples too in the
items list. In this case, the item text and value will be identical.
- Refactored ListWidget and RadioWidget so they share code; they both
inherit from SingleItemsWidget now.
- Added LinesField to submit a list of lines in a textarea.
- Added MultiListField and MultiCheckBoxField, both use new
MultiItemsWidget and MultiSelectionValidator.
- Added EXPERIMENTAL PatternField.
0.9.4 (2001/06/20)
Features Added
- Added API docs for Form, BasicForm and ZMIForm.
- Renamed the confusingly named PythonForm and PythonField to
ZMIForm and ZMIField, as they are used from the Zope Management
Interface and not from Python.
- Added render() method to form for basic form rendering.
- Added Formulator HOWTO document.
Bugs Fixed
- Removed some validation code that wasn't in use anymore (items_method).
- Removed 'has_field_id' in Form as this duplicated
the functionality of 'has_field'.
- Turned <br> in Python sources to <br /> for XHTML compliance.
- Tweaked radiobutton; text is now closer to the button itself,
different buttons are further apart.
0.9.3 (2001/06/12)
Features Added
- added RadioField for simple display of radio buttons.
- added action, method and enctype property to form settings.
These are displayed using the special form.header() and form.footer()
methods.
- added override tab to allow all properties to be overridden by
method calls instead. 'items_method' in ListField went
away.
- added ability to display DateTimeFields using drop down lists
instead of text input. Added some other bells and whistles to
DateTimeField. Changed some of the inner workings of composite
fields; component fields are now unique per field instance
instead of shared between them.
- is_required() utility method on field to check whether a field
is required.
- some internal features, such the ability to have a method
called as soon as a property has changed.
Bugs Fixed
- Fixed typos in security assertions.
- use REQUEST.form instead of REQUEST where possible.
- display month and day with initial zero in DateTimeField.
- Fixed bug in validate_all_to_request(); what can be validated
will now be added to REQUEST if possible, even if a
FormValidationError is raised.
0.9.2 (2001/05/23)
Features Added
- Ability to rename groups, including the first 'Default' group.
- Improved support for sticky forms; form.render() can now
take an optional second argument, REQUEST, which can come
from a previous form submit. Even unvalidated fields will
then be sticky.
- fields can call an extra optional external validation
function (such as a Python script).
- New alternate name property: the alternate name is added to
the result dictionary or REQUEST object after validation. This
can be useful to support field names which wouldn't be valid
field names, which can occur in some locales.
- New extra property; can be used to add extra attributes to
a HTML tag.
- Some IntegerField properties can now be left empty if
no value is required, instead of having to set them to 0.
- Merged functionality of RangedIntegerField into IntegerField.
RangedIntegerField is not addable anymore, though supported
as a clone of IntegerField for backwards compatibility. Leaving
'start' and 'end' empty in the new IntegerField will mean those
checks will not be performed.
Bugs Fixed
- Added more missing security declarations.
- html_quote added in various places to make fields display
various HTML entities the right way.
0.9.1 (2001/05/13)
Features Added
- Widgets now have a 'hidden' property. If set, the widget is
drawn as a 'hidden' field. 'hidden' fields do get validated
normally, however.
- Changed API of Widget and Validator slightly; render() and
validate() methods now take an extra 'key' argument indicating
the name the field should have in the form. This is necessarily
to handle sub fields of composite fields.
- Added EmailField and FloatField.
- Added some infrastructure to support 'composite fields'; fields
composed out of multiple sub fields.
- Added DateTimeField, the first example of a composite field
(field made of other fields).
Bugs Fixed
- General code cleanups; removed some unused methods.
- Fixed security assertion for validate_all_to_request() method.
- MethodFields now check whether they have 'View' permission to
execute listed Python Script or DTML Method.
- RangedInteger is now < end, instead of <=, compatible with the
documentation.
0.9 (2001/04/30)
Initial Release
- Initial public release of Formulator.
# include some helper fields which are in their own files
from MethodField import MethodField
from ListTextAreaField import ListTextAreaField
from TALESField import TALESField
Installing Formulator
Requirements
Formulator should work with Zope versions 2.6 or higher:
http://www.zope.org/Products/Zope
For reading in forms as XML you need to have minidom installed;
this should come with a normal python 2.1 distribution. This is
not required to use Formulator, however.
Upgrading
to 1.6.0 from earlier versions
There should be no problems.
to 1.4.2 from earlier versions
There should be no problems.
to 1.4.1 from earlier versions
There should be no problems.
to 1.4.0 from earlier versions
There should be no problems.
to 1.3.1 from earlier versions
There should be no problems (see note for 0.9.2 though in the
unusual case you're upgrading from that..this is the last time
I'll mention it :).
to 1.3.0 from earlier versions
There should be no problems, but see the note if you're
upgrading from 0.9.2 or below (but I'd be surprised if you
were!).
to 1.2.0 from earlier versions
There should be no problems, but see the note if you're upgrading
from version 0.9.2 or below.
to 1.1.0 from earlier versions
There should be no problems. If you're upgrading from 0.9.2 or
below however, please see the upgrading note for 0.9.3. Do note
that the Override tab is scheduled to be phased out eventually in
favor of the TALES tab. This will take a while yet, though.
to 1.0.9 from earlier versions
There should be no problems. If you're upgrading from 0.9.2 or
below however, please see the upgrading note for 0.9.3. Do note
that the Override tab is scheduled to be phased out eventually in
favor of the TALES tab. This will take a while yet, though.
to 1.0.1 from earlier versions
There should be no problems. If you're upgrading from 0.9.2 or
below, please see the upgrading note for 0.9.3.
to 1.0 from earlier versions
There should be no problems. If you're upgrading from 0.9.2 or
below, please see the upgrading note for 0.9.3.
to 0.9.5 from earlier versions
There should be no problems in upgrading from 0.9.4 or 0.9.3.
If you're upgrading from 0.9.2 or below, see the upgrading note
for 0.9.3.
to 0.9.4 from earlier versions
There should be no problems in upgrading from 0.9.3.
If you're upgrading from 0.9.2 or below, see the upgrading
note for 0.9.3.
to 0.9.3 from earlier versions
'items_method' in ListField is gone; you'll have to adjust make
your forms use 'items' in the override tab now instead. Sorry
about that, it *was* marked experimental. :)
There should be no other problems in upgrading.
to 0.9.2 from earlier versions
There should be no significant upgrade problems; your forms
should still work. RangedIntegerFields should show up as
IntegerFields, which subsume their functionality.
to 0.9.1 from earlier versions
There should be no significant upgrade problems; your forms
should still work.
Quickstart
Formulator follows the normal Zope filesystem product installation
procedure; just unpack the tarball to your products directory and
restart Zope.
Now the same at a more leisurely pace.
Unpacking
Formulator comes as a 'Formulator-x.x.tgz' file, where 'x.x'
stands for the Formulator version number. On Unix, you can use::
tar xvzf Formulator-x.x.tgz
to unpack the file. On Windows you can use your favorite archiving
software, such as WinZip.
This will create a Formulator directory.
Installing the Product
Move this directory to your Zope's Products directory. Normally
this is 'yourzope/lib/python/Products'.
Now restart your Zope.
Verifying Installation
If all went well, Formulator should now be visible in Zope in the
Products screen ('/Control_Panel/Products'). In a Zope folder, you
should now see a 'Formulator Form' in your 'Add' list. You should
be able to add a form to a folder now.
Copyright (c) 2001, 2002, 2003 Infrae. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
3. Neither the name of Infrae nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INFRAE OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import string
from DummyField import fields
import Widget, Validator
from Field import ZMIField
class ListTextAreaWidget(Widget.TextAreaWidget):
default = fields.ListTextAreaField('default',
title='Default',
default=[],
required=0)
def render(self, field, key, value, REQUEST, render_prefix=None):
if value is None:
value = field.get_value('default')
lines = []
for element_text, element_value in value:
lines.append("%s | %s" % (element_text, element_value))
return Widget.TextAreaWidget.render(self, field, key,
string.join(lines, '\n'),
REQUEST)
ListTextAreaWidgetInstance = ListTextAreaWidget()
class ListLinesValidator(Validator.LinesValidator):
"""A validator that can deal with lines that have a | separator
in them to split between text and value of list items.
"""
def validate(self, field, key, REQUEST):
value = Validator.LinesValidator.validate(self, field, key, REQUEST)
result = []
for line in value:
elements = string.split(line, "|")
if len(elements) >= 2:
text, value = elements[:2]
else:
text = line
value = line
text = string.strip(text)
value = string.strip(value)
result.append((text, value))
return result
ListLinesValidatorInstance = ListLinesValidator()
class ListTextAreaField(ZMIField):
meta_type = "ListTextAreaField"
# field only has internal use
internal_field = 1
widget = ListTextAreaWidgetInstance
validator = ListLinesValidatorInstance
import string
from DummyField import fields
import Widget, Validator
from Globals import Persistent
import Acquisition
from Field import ZMIField
from AccessControl import getSecurityManager
class MethodWidget(Widget.TextWidget):
default = fields.MethodField('default',
title='Default',
default="",
required=0)
def render(self, field, key, value, REQUEST, render_prefix=None):
if value == None:
method_name = field.get_value('default')
else:
if value != "":
method_name = value.method_name
else:
method_name = ""
return Widget.TextWidget.render(self, field, key, method_name, REQUEST)
MethodWidgetInstance = MethodWidget()
class Method(Persistent, Acquisition.Implicit):
"""A method object; calls method name in acquisition context.
"""
def __init__(self, method_name):
self.method_name = method_name
def __call__(self, *arg, **kw):
# get method from acquisition path
method = getattr(self, self.method_name)
# check if we have 'View' permission for this method
# (raises error if not)
getSecurityManager().checkPermission('View', method)
# okay, execute it with supplied arguments
return apply(method, arg, kw)
class BoundMethod(Method):
"""A bound method calls a method on a particular object.
Should be used internally only.
"""
def __init__(self, object, method_name):
BoundMethod.inheritedAttribute('__init__')(self, method_name)
self.object = object
def __call__(self, *arg, **kw):
method = getattr(self.object, self.method_name)
return apply(method, arg, kw)
class MethodValidator(Validator.StringBaseValidator):
def validate(self, field, key, REQUEST):
value = Validator.StringBaseValidator.validate(self, field, key,
REQUEST)
if value == "" and not field.get_value('required'):
return value
return Method(value)
MethodValidatorInstance = MethodValidator()
class MethodField(ZMIField):
meta_type = 'MethodField'
internal_field = 1
widget = MethodWidgetInstance
validator = MethodValidatorInstance
import re
# Symbols that are used to represent groups of characters
NUMBERSYMBOL = 'd' # 0-9
CHARSYMBOL = 'e' # a-zA-Z
NUMCHARSYMBOL = 'f' # a-zA-Z0-9
# List of characters, that are special to Regex. Listing them here and
# therefore escaping them will help making the Validator secure.
# NOTE: Please do not add '*', since it is used to determine inifinite
# long char symbol rows. (See examples at the of the file.)
DANGEROUSCHARS = '\\()+?.$'
class PatternChecker:
"""
This class defines a basic user friendly checker and processor of
string values according to pattern.
It can verify whether a string value fits a certain pattern of
digits and letters and possible special characters.
"""
# a dictionary that converts an array of symbols to regex expressions
symbol_regex_dict = {NUMBERSYMBOL : '([0-9]{%i,%s})',
CHARSYMBOL : '([a-zA-Z]{%i,%s})',
NUMCHARSYMBOL : '([0-9a-zA-Z]{%i,%s})'}
def _escape(self, match_object):
"""Escape a single character.
"""
return '\\' + match_object.group(0)
def _escape_special_characters(self, s):
"""Escape the characters that have a special meaning in regex.
"""
return re.sub('[' + DANGEROUSCHARS + ']', self._escape, s)
def _unescape_special_characters(self, s):
"""Reverse the escaping, so that the final string is as close as
possible to the original one.
"""
return re.sub('\\\\', '', s)
def _replace_symbol_by_regex(self, match_object):
"""Replace the character symbol with their respective regex.
"""
length = len(match_object.group(0))
# Yikes, what a hack! But I could not come up with something better.
if match_object.group(0)[-1] == '*':
min = length - 1
max = ''
else:
min = length
max = str(min)
return self.symbol_regex_dict[match_object.group(0)[0]] %(min, max)
def make_regex_from_pattern(self, pattern):
"""Replaces all symbol occurences and creates a complete regex
string.
"""
regex = self._escape_special_characters(pattern)
for symbol in [NUMBERSYMBOL, CHARSYMBOL, NUMCHARSYMBOL]:
regex = re.sub(symbol+'{1,}\*?', self._replace_symbol_by_regex, regex)
return '^ *' + regex + ' *$'
def construct_value_from_match(self, result, pattern):
"""After we validated the string, we put it back together; this is
good, since we can easily clean up the data this way.
"""
value = self._escape_special_characters(pattern)
_symbols = '['+NUMBERSYMBOL + CHARSYMBOL + NUMCHARSYMBOL + ']'
re_obj = re.compile(_symbols+'{1,}\*?')
for res in result.groups():
match = re_obj.search(value)
value = value[:match.start()] + res + value[match.end():]
return value
def clean_value(self, value):
"""Clean up unnecessary white characters.
"""
# same as string.strip, but since I am using re everywhere here,
# why not use it now too?
value = re.sub('^\s*', '', value)
value = re.sub('\s*$', '', value)
# make out of several white spaces, one whitespace...
value = re.sub(' *', ' ', value)
return value
def validate_value(self, patterns, value):
"""Validate method that manges the entire validation process.
The validator goes through each pattern and
tries to get a match to the value (second parameter). At the end, the
first pattern of the list is taken to construct the value again; this
ensures data cleansing and a common data look.
"""
value = self.clean_value(value)
result = None
for pattern in patterns:
regex = self.make_regex_from_pattern(pattern)
re_obj = re.compile(regex)
result = re_obj.search(value)
if result:
break
if not result:
return None
value = self.construct_value_from_match(result, patterns[0])
return self._unescape_special_characters(value)
if __name__ == '__main__':
val = PatternChecker()
# American long ZIP
print val.validate_value(['ddddd-dddd'], '34567-1298')
print val.validate_value(['ddddd-dddd'], ' 34567-1298 \t ')
# American phone number
print val.validate_value(['(ddd) ddd-dddd', 'ddd-ddd-dddd',
'ddd ddd-dddd'],
'(345) 678-1298')
print val.validate_value(['(ddd) ddd-dddd', 'ddd-ddd-dddd',
'ddd ddd-dddd'],
'345-678-1298')
# American money
print val.validate_value(['$ d*.dd'], '$ 1345345.00')
# German money
print val.validate_value(['d*.dd DM'], '267.98 DM')
# German license plate
print val.validate_value(['eee ee-ddd'], 'OSL HR-683')
# German phone number (international)
print val.validate_value(['+49 (d*) d*'], '+49 (3574) 7253')
print val.validate_value(['+49 (d*) d*'], '+49 (3574) 7253')
"""
ProductForm.py
This file is an adaptation from part of Plone's FormTool.py tool.
It provides a wrapping around Formulator.BasicForm, allowing it
to be created inside a product but used outside it.
"""
import string
from AccessControl import ClassSecurityInfo
from Globals import InitializeClass
import FormValidationError, BasicForm
import StandardFields
class ProductForm(BasicForm):
"""Wraps Formulator.BasicForm and provides some convenience methods that
make BasicForms easier to work with from external methods."""
security = ClassSecurityInfo()
security.declareObjectPublic()
security.declareProtected('View', 'get_field')
def get_field(self, id):
"""Get a field of a certain id, wrapping in context of self
"""
return self.fields[id].__of__(self)
security.declarePublic('addField')
def addField(self, field_id, fieldType, group=None, **kwargs):
"""
Adds a Formulator Field to the wrapped BasicForm.
fieldType: An abbreviation for the Field type.
'String' generates a StringField, 'Int' generates an IntField, etc.
Uses a StringField if no suitable Field type is found.
field_id: Name of the variable in question. Note that Formulator adds
'field_' to variable names, so you will need to refer to the variable
foo as field_foo in form page templates.
group: Formulator group for the field.
Additional arguments: addField passes all other arguments on to the
new Field object. In addition, it allows you to modify the
Field's error messages by passing in arguments of the form
name_of_message = 'New error message'
See Formulator.StandardFields for details.
"""
if fieldType[-5:]!='Field':
fieldType = fieldType+'Field'
formulatorFieldClass = None
if hasattr(StandardFields, fieldType):
formulatorFieldClass = getattr(StandardFields, fieldType)
else:
formulatorFieldClass = getattr(StandardFields, 'StringField')
# pass a title parameter to the Field
kwargs['title'] = field_id
fieldObject = apply(formulatorFieldClass, (field_id, ), kwargs)
# alter Field error messages
# Note: This messes with Formulator innards and may break in the future.
# Unfortunately, Formulator doesn't do this already in Field.__init__
# and there isn't a Python-oriented method for altering message values
# so at present it's the only option.
for arg in kwargs.keys():
if fieldObject.message_values.has_key(arg):
fieldObject.message_values[arg] = kwargs[arg]
# Add the new Field to the wrapped BasicForm object
BasicForm.add_field(self, fieldObject, group)
security.declarePublic('validate')
def validate(self, REQUEST, errors=None):
"""
Executes the validator for each field in the wrapped BasicForm.add_field
Returns the results in a dictionary.
"""
if errors is None:
errors = REQUEST.get('errors', {})
# This is a bit of a hack to make some of Formulator's quirks
# transparent to developers. Formulator expects form fields to be
# prefixed by 'field_' in the request. To remove this restriction,
# we mangle the REQUEST, renaming keys from key to 'field_' + key
# before handing off to Formulator's validators. We will undo the
# mangling afterwards.
for field in self.get_fields():
key = field.id
value = REQUEST.get(key)
if value:
# get rid of the old key
try:
del REQUEST[key]
except:
pass
# move the old value to 'field_' + key
# if there is already a value at 'field_' + key,
# move it to 'field_field_' + key, and repeat
# to prevent key collisions
newKey = 'field_' + key
newValue = REQUEST.get(newKey)
while newValue:
REQUEST[newKey] = value
newKey = 'field_' + newKey
value = newValue
newValue = REQUEST.get(newKey)
REQUEST[newKey] = value
try:
result=self.validate_all(REQUEST)
except FormValidationError, e:
for error in e.errors:
errors[error.field.get_value('title')]=error.error_text
# unmangle the REQUEST
for field in self.get_fields():
key = field.id
value = 1
while value:
key = 'field_' + key
value = REQUEST.get(key)
if value:
REQUEST[key[6:]] = value
try:
del REQUEST[key]
except:
pass
return errors
InitializeClass(ProductForm)
Formulator
Formulator is a tool to help with the creation and validation of web
forms. Form fields are stored as objects in Zope, in a special Form
folder.
Features
* manage form fields through the Zope management interface.
* manage field look & feel as well as validation and processing
behavior.
* automatic field validation.
* determine field order and group fields together.
* easy extensibility with new field types.
* online help.
* serialization of form to XML and back.
Installation and Requirements
See INSTALL.txt for more information on installing Formulator.
Information
Formulator comes with online help, so click on 'Help!' in the Zope
management screens. If you want your brain to explode, read the
'How Formulator Eats its Own Dogfood' help topic.
Information is also available at the Formulator web site:
http://www.zope.org/Members/faassen/Formulator
There are also instructions to join the Formulator mailing list there.
Discussion about Formulator should preferably happen on the mailing list
first, though you can always mail me as well. But please consider the
list if you have questions or suggestions.
Even more info can be found by reading the source. :)
from Form import BasicForm
from Field import ZMIField
from DummyField import fields
from MethodField import BoundMethod
from DateTime import DateTime
import Validator, Widget
import OFS
class StringField(ZMIField):
meta_type = "StringField"
widget = Widget.TextWidgetInstance
validator = Validator.StringValidatorInstance
class PasswordField(ZMIField):
meta_type = "PasswordField"
widget = Widget.PasswordWidgetInstance
validator = Validator.StringValidatorInstance
class EmailField(ZMIField):
meta_type = "EmailField"
widget = Widget.TextWidgetInstance
validator = Validator.EmailValidatorInstance
class PatternField(ZMIField):
meta_type = "PatternField"
widget = Widget.TextWidgetInstance
validator = Validator.PatternValidatorInstance
class CheckBoxField(ZMIField):
meta_type = "CheckBoxField"
widget = Widget.CheckBoxWidgetInstance
validator = Validator.BooleanValidatorInstance
class IntegerField(ZMIField):
meta_type = "IntegerField"
widget = Widget.IntegerWidgetInstance
validator = Validator.IntegerValidatorInstance
class RangedIntegerField(ZMIField):
meta_type = "RangedIntegerField"
# this field is not addable anymore and deprecated. For
# backwards compatibility it's a clone of IntegerField,
# though it may go away in the future.
internal_field = 1
widget = Widget.TextWidgetInstance
validator = Validator.IntegerValidatorInstance
class FloatField(ZMIField):
meta_type = "FloatField"
widget = Widget.FloatWidgetInstance
validator = Validator.FloatValidatorInstance
class TextAreaField(ZMIField):
meta_type = "TextAreaField"
widget = Widget.TextAreaWidgetInstance
validator = Validator.TextValidatorInstance
class RawTextAreaField(ZMIField):
meta_type = "RawTextAreaField"
widget = Widget.TextAreaWidgetInstance
validator = Validator.StringValidatorInstance
class ListField(ZMIField):
meta_type = "ListField"
widget = Widget.ListWidgetInstance
validator = Validator.SelectionValidatorInstance
class MultiListField(ZMIField):
meta_type = "MultiListField"
widget = Widget.MultiListWidgetInstance
validator = Validator.MultiSelectionValidatorInstance
class LinesField(ZMIField):
meta_type = "LinesField"
widget = Widget.LinesTextAreaWidgetInstance
validator = Validator.LinesValidatorInstance
class RadioField(ZMIField):
meta_type = "RadioField"
widget = Widget.RadioWidgetInstance
validator = Validator.SelectionValidatorInstance
class MultiCheckBoxField(ZMIField):
meta_type = "MultiCheckBoxField"
widget = Widget.MultiCheckBoxWidgetInstance
validator = Validator.MultiSelectionValidatorInstance
class FileField(ZMIField):
meta_type = "FileField"
widget = Widget.FileWidgetInstance
validator = Validator.FileValidatorInstance
class LinkField(ZMIField):
meta_type = "LinkField"
widget = Widget.LinkWidgetInstance
validator = Validator.LinkValidatorInstance
class LabelField(ZMIField):
"""Just a label, doesn't really validate.
"""
meta_type = "LabelField"
widget = Widget.LabelWidgetInstance
validator = Validator.SuppressValidatorInstance
class DateTimeField(ZMIField):
meta_type = "DateTimeField"
widget = Widget.DateTimeWidgetInstance
validator = Validator.DateTimeValidatorInstance
def __init__(self, id, **kw):
# icky but necessary...
apply(ZMIField.__init__, (self, id), kw)
input_style = self.get_value('input_style')
if input_style == 'text':
self.sub_form = create_datetime_text_sub_form()
elif input_style == 'list':
self.sub_form = create_datetime_list_sub_form()
else:
assert 0, "Unknown input_style '%s'" % input_style
def on_value_input_style_changed(self, value):
if value == 'text':
self.sub_form = create_datetime_text_sub_form()
elif value == 'list':
self.sub_form = create_datetime_list_sub_form()
year_field = self.sub_form.get_field('year', include_disabled=1)
year_field.overrides['items'] = BoundMethod(self,
'override_year_items')
else:
assert 0, "Unknown input_style."
self.on_value_css_class_changed(self.values['css_class'])
def on_value_css_class_changed(self, value):
for field in self.sub_form.get_fields():
field.values['css_class'] = value
field._p_changed = 1
def override_year_items(self):
"""The method gets called to get the right amount of years.
"""
start_datetime = self.get_value('start_datetime')
end_datetime = self.get_value('end_datetime')
current_year = DateTime().year()
if start_datetime:
first_year = start_datetime.year()
else:
first_year = current_year
if end_datetime:
last_year = end_datetime.year() + 1
else:
last_year = first_year + 11
return create_items(first_year, last_year, digits=4)
def _get_user_input_value(self, key, REQUEST):
"""
Try to get a value of the field from the REQUEST
"""
if REQUEST.form['subfield_%s_%s' % (key, 'year')]:
return None
gmt_timezones = [('GMT%s' %zone, 'GMT%s' %zone,) for zone in range(-12, 0)]\
+ [('GMT', 'GMT',),] \
+ [('GMT+%s' %zone, 'GMT+%s' %zone,) for zone in range(1, 13)]
def create_datetime_text_sub_form():
sub_form = BasicForm()
year = IntegerField('year',
title="Year",
required=0,
display_width=4,
display_maxwidth=4,
max_length=4)
month = IntegerField('month',
title="Month",
required=0,
display_width=2,
display_maxwidth=2,
max_length=2)
day = IntegerField('day',
title="Day",
required=0,
display_width=2,
display_maxwidth=2,
max_length=2)
sub_form.add_group("date")
sub_form.add_fields([year, month, day], "date")
hour = IntegerField('hour',
title="Hour",
required=0,
display_width=2,
display_maxwidth=2,
max_length=2)
minute = IntegerField('minute',
title="Minute",
required=0,
display_width=2,
display_maxwidth=2,
max_length=2)
ampm = StringField('ampm',
title="am/pm",
required=0,
display_width=2,
display_maxwidth=2,
max_length=2)
timezone = ListField('timezone',
title = "Timezone",
required = 0,
default = 'GMT',
items = gmt_timezones,
size = 1)
sub_form.add_fields([hour, minute, ampm, timezone], "time")
return sub_form
def create_datetime_list_sub_form():
""" Patch Products.Formulator.StandardFields so we can add timezone subfield """
sub_form = BasicForm()
year = ListField('year',
title="Year",
required=0,
default="",
items=create_items(2000, 2010, digits=4),
size=1)
month = ListField('month',
title="Month",
required=0,
default="",
items=create_items(1, 13, digits=2),
size=1)
day = ListField('day',
title="Day",
required=0,
default="",
items=create_items(1, 32, digits=2),
size=1)
sub_form.add_group("date")
sub_form.add_fields([year, month, day], "date")
hour = IntegerField('hour',
title="Hour",
required=0,
display_width=2,
display_maxwidth=2,
max_length=2)
minute = IntegerField('minute',
title="Minute",
required=0,
display_width=2,
display_maxwidth=2,
max_length=2)
ampm = ListField('ampm',
title="am/pm",
required=0,
default="am",
items=[("am","am"),
("pm","pm")],
size=1)
timezone = ListField('timezone',
title = "Timezone",
required = 0,
default = 'GMT',
items = gmt_timezones,
size = 1)
sub_form.add_group("time")
sub_form.add_fields([hour, minute, ampm, timezone], "time")
return sub_form
def create_items(start, end, digits=0):
result = [("-", "")]
if digits:
format_string = "%0" + str(digits) + "d"
else:
format_string = "%s"
for i in range(start, end):
s = format_string % i
result.append((s, s))
return result
import string
from DummyField import fields
import Widget, Validator
from Globals import Persistent
import Acquisition
from Field import ZMIField
from AccessControl import getSecurityManager
class TALESWidget(Widget.TextWidget):
default = fields.MethodField('default',
title='Default',
default="",
required=0)
def render(self, field, key, value, REQUEST, render_prefix=None):
if value == None:
text = field.get_value('default')
else:
if value != "":
text = value._text
else:
text = ""
return Widget.TextWidget.render(self, field, key, text, REQUEST)
def render_view(self, field, value, REQUEST=None, render_prefix=None):
"""
Render TALES as read only
"""
if value == None:
text = field.get_value('default', REQUEST=REQUEST)
else:
if value != "":
text = value._text
else:
text = ""
return text
TALESWidgetInstance = TALESWidget()
class TALESNotAvailable(Exception):
pass
try:
# try to import getEngine from TALES
from Products.PageTemplates.Expressions import getEngine
class TALESMethod(Persistent, Acquisition.Implicit):
"""A method object; calls method name in acquisition context.
"""
def __init__(self, text):
self._text = text
def __call__(self, **kw):
expr = getattr(self, '_v_expr', None)
if expr is None:
self._v_expr = expr = getEngine().compile(self._text)
return getEngine().getContext(kw).evaluate(expr)
# check if we have 'View' permission for this method
# (raises error if not)
# getSecurityManager().checkPermission('View', method)
TALES_AVAILABLE = 1
except ImportError:
# cannot import TALES, so supply dummy TALESMethod
class TALESMethod(Persistent, Acquisition.Implicit):
"""A dummy method in case TALES is not available.
"""
def __init__(self, text):
self._text = text
def __call__(self, **kw):
raise TALESNotAvailable
TALES_AVAILABLE = 0
class TALESValidator(Validator.StringBaseValidator):
def validate(self, field, key, REQUEST):
value = Validator.StringBaseValidator.validate(self, field, key,
REQUEST)
if value == "" and not field.get_value('required'):
return value
return TALESMethod(value)
TALESValidatorInstance = TALESValidator()
class TALESField(ZMIField):
meta_type = 'TALESField'
internal_field = 1
widget = TALESWidgetInstance
validator = TALESValidatorInstance
Formulator TODO
- When using a BasicForm a field cannot get to the form's
get_form_encoding method (where the ZMIForm uses acquisistion
to achieve this).
- Make composite fields work well as hidden fields.
- Add combobox field
- Add various button fields
- Investigate duration and time only field.
- internationalisation (or error messages first)
- HTML filtering field?
- Add more unit tests.
import string, re
import PatternChecker
from DummyField import fields
from DateTime import DateTime
from threading import Thread
from urllib import urlopen
from urlparse import urljoin
from Errors import ValidationError
from DateTime.DateTime import DateError, TimeError
class ValidatorBase:
"""Even more minimalistic base class for validators.
"""
property_names = ['enabled','editable']
message_names = []
enabled = fields.CheckBoxField('enabled',
title="Enabled",
description=(
"If a field is not enabled, it will considered to be not "
"in the form during rendering or validation. Be careful "
"when you change this state dynamically (in the TALES tab): "
"a user could submit a field that since got disabled, or "
"get a validation error as a field suddenly got enabled that "
"wasn't there when the form was drawn."),
default=1)
editable = fields.CheckBoxField('editable',
title="Editable",
description=(
"If a field is not editable, then the user can only see"
"the value. This allows to drawn very different forms depending"
"on use permissions."),
default=1)
def raise_error(self, error_key, field):
raise ValidationError(error_key, field)
def validate(self, field, key, REQUEST):
pass # override in subclass
def need_validate(self, field, key, REQUEST):
"""Default behavior is always validation.
"""
return 1
class Validator(ValidatorBase):
"""Validates input and possibly transforms it to output.
"""
property_names = ValidatorBase.property_names + ['external_validator']
external_validator = fields.MethodField('external_validator',
title="External Validator",
description=(
"When a method name is supplied, this method will be "
"called each time this field is being validated. All other "
"validation code is called first, however. The value (result of "
"previous validation) and the REQUEST object will be passed as "
"arguments to this method. Your method should return true if the "
"validation succeeded. Anything else will cause "
"'external_validator_failed' to be raised."),
default="",
required=0)
message_names = ValidatorBase.message_names + ['external_validator_failed']
external_validator_failed = "The input failed the external validator."
class StringBaseValidator(Validator):
"""Simple string validator.
"""
property_names = Validator.property_names + ['required', 'whitespace_preserve']
required = fields.CheckBoxField('required',
title='Required',
description=(
"Checked if the field is required; the user has to fill in some "
"data."),
default=0)
whitespace_preserve = fields.CheckBoxField('whitespace_preserve',
title="Preserve whitespace",
description=(
"Checked if the field preserves whitespace. This means even "
"just whitespace input is considered to be data."),
default=0)
message_names = Validator.message_names + ['required_not_found']
required_not_found = 'Input is required but no input given.'
def validate(self, field, key, REQUEST):
# We had to add this patch for hidden fields of type "list"
value = REQUEST.get(key, REQUEST.get('default_%s' % (key, )))
if value is None:
if field.get_value('required'):
raise Exception, 'Required field %s has not been transmitted. Check that all required fields are in visible groups.' % (repr(field.id), )
else:
raise KeyError, 'Field %s is not present in request object.' % (repr(field.id), )
if isinstance(value, str):
if field.has_value('whitespace_preserve'):
if not field.get_value('whitespace_preserve'):
value = string.strip(value)
else:
# XXX Compatibility: use to prevent KeyError exception from get_value
value = string.strip(value)
if field.get_value('required') and value == "":
self.raise_error('required_not_found', field)
return value
class StringValidator(StringBaseValidator):
property_names = StringBaseValidator.property_names +\
['unicode', 'max_length', 'truncate']
unicode = fields.CheckBoxField('unicode',
title='Unicode',
description=(
"Checked if the field delivers a unicode string instead of an "
"8-bit string."),
default=0)
max_length = fields.IntegerField('max_length',
title='Maximum length',
description=(
"The maximum amount of characters that can be entered in this "
"field. If set to 0 or is left empty, there is no maximum. "
"Note that this is server side validation."),
default="",
required=0)
truncate = fields.CheckBoxField('truncate',
title='Truncate',
description=(
"If checked, truncate the field if it receives more input than is "
"allowed. The normal behavior in this case is to raise a validation "
"error, but the text can be silently truncated instead."),
default=0)
message_names = StringBaseValidator.message_names +\
['too_long']
too_long = 'Too much input was given.'
def validate(self, field, key, REQUEST):
value = StringBaseValidator.validate(self, field, key, REQUEST)
if field.get_value('unicode'):
# use acquisition to get encoding of form
value = unicode(value, field.get_form_encoding())
max_length = field.get_value('max_length') or 0
truncate = field.get_value('truncate')
if max_length > 0 and len(value) > max_length:
if truncate:
value = value[:max_length]
else:
self.raise_error('too_long', field)
return value
StringValidatorInstance = StringValidator()
class EmailValidator(StringValidator):
message_names = StringValidator.message_names + ['not_email']
not_email = 'You did not enter an email address.'
# This regex allows for a simple username or a username in a
# multi-dropbox (%). The host part has to be a normal fully
# qualified domain name, allowing for 6 characters (.museum) as a
# TLD. No bang paths (uucp), no dotted-ip-addresses, no angle
# brackets around the address (we assume these would be added by
# some custom script if needed), and of course no characters that
# don't belong in an e-mail address.
pattern = re.compile('^[0-9a-zA-Z_\'&.%+-]+@([0-9a-zA-Z]([0-9a-zA-Z-]*[0-9a-zA-Z])?\.)+[a-zA-Z]{2,6}$')
def validate(self, field, key, REQUEST):
value = StringValidator.validate(self, field, key, REQUEST)
if value == "" and not field.get_value('required'):
return value
if self.pattern.search(string.lower(value)) == None:
self.raise_error('not_email', field)
return value
EmailValidatorInstance = EmailValidator()
class PatternValidator(StringValidator):
# does the real work
checker = PatternChecker.PatternChecker()
property_names = StringValidator.property_names +\
['pattern']
pattern = fields.StringField('pattern',
title="Pattern",
required=1,
default="",
description=(
"The pattern the value should conform to. Patterns are "
"composed of digits ('d'), alphabetic characters ('e') and "
"alphanumeric characters ('f'). Any other character in the pattern "
"should appear literally in the value in that place. Internal "
"whitespace is checked as well but may be included in any amount. "
"Example: 'dddd ee' is a Dutch zipcode (postcode). "
"NOTE: currently experimental and details may change!")
)
message_names = StringValidator.message_names +\
['pattern_not_matched']
pattern_not_matched = "The entered value did not match the pattern."
def validate(self, field, key, REQUEST):
value = StringValidator.validate(self, field, key, REQUEST)
if value == "" and not field.get_value('required'):
return value
value = self.checker.validate_value([field.get_value('pattern')],
value)
if value is None:
self.raise_error('pattern_not_matched', field)
return value
PatternValidatorInstance = PatternValidator()
class BooleanValidator(Validator):
def validate(self, field, key, REQUEST):
result = REQUEST.get(key, REQUEST.get('default_%s' % key))
if result is None:
raise KeyError('Field %r is not present in request object.' % field.id)
# XXX If the checkbox is hidden, Widget_render_hidden is used instead of
# CheckBoxWidget_render, and ':int' suffix is missing.
return result and result != '0' and 1 or 0
BooleanValidatorInstance = BooleanValidator()
class IntegerValidator(StringBaseValidator):
property_names = StringBaseValidator.property_names +\
['start', 'end']
start = fields.IntegerField('start',
title='Start',
description=(
"The integer entered by the user must be larger than or equal to "
"this value. If left empty, there is no minimum."),
default="",
required=0)
end = fields.IntegerField('end',
title='End',
description=(
"The integer entered by the user must be smaller than this "
"value. If left empty, there is no maximum."),
default="",
required=0)
message_names = StringBaseValidator.message_names +\
['not_integer', 'integer_out_of_range']
not_integer = 'You did not enter an integer.'
integer_out_of_range = 'The integer you entered was out of range.'
def validate(self, field, key, REQUEST):
value = StringBaseValidator.validate(self, field, key, REQUEST)
# we need to add this check again
if value == "" and not field.get_value('required'):
return value
try:
if value.find(' ')>0:
value = value.replace(' ','')
value = int(value)
except ValueError:
self.raise_error('not_integer', field)
start = field.get_value('start')
end = field.get_value('end')
if start != "" and value < start:
self.raise_error('integer_out_of_range', field)
if end != "" and value >= end:
self.raise_error('integer_out_of_range', field)
return value
IntegerValidatorInstance = IntegerValidator()
class FloatValidator(StringBaseValidator):
message_names = StringBaseValidator.message_names + ['not_float']
not_float = "You did not enter a floating point number."
def validate(self, field, key, REQUEST):
value = StringBaseValidator.validate(self, field, key, REQUEST)
if value == "" and not field.get_value('required'):
return value
value = value.replace(' ','')
input_style = field.get_value('input_style')
if value.find(',') >= 0:
value = value.replace(',','.')
if value.find('%')>=0:
value = value.replace('%','')
try:
value = float(value)
if input_style.find('%')>=0:
value = value/100
except ValueError:
self.raise_error('not_float', field)
return value
FloatValidatorInstance = FloatValidator()
class LinesValidator(StringBaseValidator):
property_names = StringBaseValidator.property_names +\
['unicode', 'max_lines', 'max_linelength', 'max_length']
unicode = fields.CheckBoxField('unicode',
title='Unicode',
description=(
"Checked if the field delivers a unicode string instead of an "
"8-bit string."),
default=0)
max_lines = fields.IntegerField('max_lines',
title='Maximum lines',
description=(
"The maximum amount of lines a user can enter. If set to 0, "
"or is left empty, there is no maximum."),
default="",
required=0)
max_linelength = fields.IntegerField('max_linelength',
title="Maximum length of line",
description=(
"The maximum length of a line. If set to 0 or is left empty, there "
"is no maximum."),
default="",
required=0)
max_length = fields.IntegerField('max_length',
title="Maximum length (in characters)",
description=(
"The maximum total length in characters that the user may enter. "
"If set to 0 or is left empty, there is no maximum."),
default="",
required=0)
message_names = StringBaseValidator.message_names +\
['too_many_lines', 'line_too_long', 'too_long']
too_many_lines = 'You entered too many lines.'
line_too_long = 'A line was too long.'
too_long = 'You entered too many characters.'
def validate(self, field, key, REQUEST):
value = StringBaseValidator.validate(self, field, key, REQUEST)
# Added as a patch for hidden values
if isinstance(value, (list, tuple)):
value = string.join(value, "\n")
# we need to add this check again
if value == "" and not field.get_value('required'):
return []
if field.get_value('unicode'):
value = unicode(value, field.get_form_encoding())
# check whether the entire input is too long
max_length = field.get_value('max_length') or 0
if max_length and len(value) > max_length:
self.raise_error('too_long', field)
# split input into separate lines
lines = string.split(value, "\n")
# check whether we have too many lines
max_lines = field.get_value('max_lines') or 0
if max_lines and len(lines) > max_lines:
self.raise_error('too_many_lines', field)
# strip extraneous data from lines and check whether each line is
# short enough
max_linelength = field.get_value('max_linelength') or 0
result = []
whitespace_preserve = field.get_value('whitespace_preserve')
for line in lines:
if not whitespace_preserve:
line = string.strip(line)
if max_linelength and len(line) > max_linelength:
self.raise_error('line_too_long', field)
result.append(line)
return result
LinesValidatorInstance = LinesValidator()
class TextValidator(LinesValidator):
def validate(self, field, key, REQUEST):
value = LinesValidator.validate(self, field, key, REQUEST)
# we need to add this check again
if value == [] and not field.get_value('required'):
return ""
# join everything into string again with \n and return
return string.join(value, "\n")
TextValidatorInstance = TextValidator()
class SelectionValidator(StringBaseValidator):
property_names = StringBaseValidator.property_names +\
['unicode']
unicode = fields.CheckBoxField('unicode',
title='Unicode',
description=(
"Checked if the field delivers a unicode string instead of an "
"8-bit string."),
default=0)
message_names = StringBaseValidator.message_names +\
['unknown_selection']
unknown_selection = 'You selected an item that was not in the list.'
def validate(self, field, key, REQUEST):
value = StringBaseValidator.validate(self, field, key, REQUEST)
if value == "" and not field.get_value('required'):
return value
# get the text and the value from the list of items
for item in list(field.get_value('items', cell=getattr(REQUEST,'cell',None))) + [field.get_value('default', cell=getattr(REQUEST,'cell',None))]:
try:
item_text, item_value = item
except (ValueError, TypeError):
item_text = item
item_value = item
# check if the value is equal to the string/unicode version of
# item_value; if that's the case, we can return the *original*
# value in the list (not the submitted value). This way, integers
# will remain integers.
# XXX it is impossible with the UI currently to fill in unicode
# items, but it's possible to do it with the TALES tab
if field.get_value('unicode') and isinstance(item_value, unicode):
str_value = item_value.encode(field.get_form_encoding())
else:
str_value = str(item_value)
if str_value == value:
return item_value
# if we didn't find the value, return error
self.raise_error('unknown_selection', field)
SelectionValidatorInstance = SelectionValidator()
class MultiSelectionValidator(Validator):
property_names = Validator.property_names + ['required', 'unicode']
required = fields.CheckBoxField('required',
title='Required',
description=(
"Checked if the field is required; the user has to fill in some "
"data."),
default=1)
unicode = fields.CheckBoxField('unicode',
title='Unicode',
description=(
"Checked if the field delivers a unicode string instead of an "
"8-bit string."),
default=0)
message_names = Validator.message_names + ['required_not_found',
'unknown_selection']
required_not_found = 'Input is required but no input given.'
unknown_selection = 'You selected an item that was not in the list.'
def validate(self, field, key, REQUEST):
if REQUEST.get('default_%s' % (key, )) is None:
LOG('MultiSelectionValidator_validate', 0, 'Field %s is not present in request object (marker field default_%s not found).' % (repr(field.id), key))
raise KeyError, 'Field %s is not present in request object (marker field default_%s not found).' % (repr(field.id), key)
values = REQUEST.get(key, [])
# NOTE: a hack to deal with single item selections
if not isinstance(values, list):
# put whatever we got in a list
values = [values]
# if we selected nothing and entry is required, give error, otherwise
# give entry list
if len(values) == 0:
if field.get_value('required'):
self.raise_error('required_not_found', field)
else:
return values
# convert everything to unicode if necessary
if field.get_value('unicode'):
values = [unicode(value, field.get_form_encoding())
for value in values]
# create a dictionary of possible values
value_dict = {}
for item in field.get_value('items', cell=getattr(REQUEST,'cell',None)): # Patch by JPS for Listbox
try:
item_text, item_value = item
except ValueError:
item_text = item
item_value = item
value_dict[item_value] = 0
default_value = field.get_value('default', cell=getattr(REQUEST,'cell',None))
if isinstance(default_value, (list, tuple)):
for v in default_value:
value_dict[v] = 0
else:
value_dict[default_value] = 0
# check whether all values are in dictionary
result = []
for value in values:
# FIXME: hack to accept int values as well
try:
int_value = int(value)
except ValueError:
int_value = None
if int_value is not None and value_dict.has_key(int_value):
result.append(int_value)
continue
if value_dict.has_key(value):
result.append(value)
continue
self.raise_error('unknown_selection', field)
# everything checks out
return result
MultiSelectionValidatorInstance = MultiSelectionValidator()
class FileValidator(Validator):
def validate(self, field, key, REQUEST):
return REQUEST.get(key, None)
FileValidatorInstance = FileValidator()
class LinkHelper:
"""A helper class to check if links are openable.
"""
status = 0
def __init__(self, link):
self.link = link
def open(self):
try:
urlopen(self.link)
except:
# all errors will definitely result in a failure
pass
else:
# FIXME: would like to check for 404 errors and such?
self.status = 1
class LinkValidator(StringValidator):
property_names = StringValidator.property_names +\
['check_link', 'check_timeout', 'link_type']
check_link = fields.CheckBoxField('check_link',
title='Check Link',
description=(
"Check whether the link is not broken."),
default=0)
check_timeout = fields.FloatField('check_timeout',
title='Check Timeout',
description=(
"Maximum amount of seconds to check link. Required"),
default=7.0,
required=1)
link_type = fields.ListField('link_type',
title='Type of Link',
default="external",
size=1,
items=[('External Link', 'external'),
('Internal Link', 'internal'),
('Relative Link', 'relative')],
description=(
"Define the type of the link. Required."),
required=1)
message_names = StringValidator.message_names + ['not_link']
not_link = 'The specified link is broken.'
def validate(self, field, key, REQUEST):
value = StringValidator.validate(self, field, key, REQUEST)
if value == "" and not field.get_value('required'):
return value
link_type = field.get_value('link_type')
if link_type == 'internal':
value = urljoin(REQUEST['BASE0'], value)
elif link_type == 'relative':
value = urljoin(REQUEST['URL1'], value)
# otherwise must be external
# FIXME: should try regular expression to do some more checking here?
# if we don't need to check the link, we're done now
if not field.get_value('check_link'):
return value
# resolve internal links using Zope's resolve_url
if link_type in ['internal', 'relative']:
try:
REQUEST.resolve_url(value)
except:
self.raise_error('not_link', field)
# check whether we can open the link
link = LinkHelper(value)
thread = Thread(target=link.open)
thread.start()
thread.join(field.get_value('check_timeout'))
del thread
if not link.status:
self.raise_error('not_link', field)
return value
LinkValidatorInstance = LinkValidator()
class DateTimeValidator(Validator):
"""
Added support for key in every call to validate_sub_field
"""
property_names = Validator.property_names + ['required',
'start_datetime',
'end_datetime',
'allow_empty_time']
required = fields.CheckBoxField('required',
title='Required',
description=(
"Checked if the field is required; the user has to enter something "
"in the field."),
default=1)
start_datetime = fields.DateTimeField('start_datetime',
title="Start datetime",
description=(
"The date and time entered must be later than or equal to "
"this date/time. If left empty, no check is performed."),
default=None,
input_style="text",
required=0)
end_datetime = fields.DateTimeField('end_datetime',
title="End datetime",
description=(
"The date and time entered must be earlier than "
"this date/time. If left empty, no check is performed."),
default=None,
input_style="text",
required=0)
allow_empty_time = fields.CheckBoxField('allow_empty_time',
title="Allow empty time",
description=(
"Allow time to be left empty. Time will default to midnight "
"on that date."),
default=0)
message_names = Validator.message_names + ['required_not_found',
'not_datetime',
'datetime_out_of_range']
required_not_found = 'Input is required but no input given.'
not_datetime = 'You did not enter a valid date and time.'
datetime_out_of_range = 'The date and time you entered were out of range.'
def validate(self, field, key, REQUEST):
try:
year = field.validate_sub_field('year', REQUEST, key=key)
month = field.validate_sub_field('month', REQUEST, key=key)
if field.get_value('hide_day'):
day = 1
else:
day = field.validate_sub_field('day', REQUEST, key=key)
if field.get_value('date_only'):
hour = 0
minute = 0
elif field.get_value('allow_empty_time'):
hour = field.validate_sub_field('hour', REQUEST, key=key)
minute = field.validate_sub_field('minute', REQUEST, key=key)
if hour == '' and minute == '':
hour = 0
minute = 0
elif hour == '' or minute == '':
raise ValidationError('not_datetime', field)
else:
hour = field.validate_sub_field('hour', REQUEST, key=key)
minute = field.validate_sub_field('minute', REQUEST, key=key)
except ValidationError:
self.raise_error('not_datetime', field)
# handling of completely empty sub fields
if ((year == '' and month == '') and
(field.get_value('hide_day') or day == '') and
(field.get_value('date_only') or (hour == '' and minute == '')
or (hour == 0 and minute == 0))):
if field.get_value('required'):
self.raise_error('required_not_found', field)
else:
# field is not required, return None for no entry
return None
# handling of partially empty sub fields; invalid datetime
if ((year == '' or month == '') or
(not field.get_value('hide_day') and day == '') or
(not field.get_value('date_only') and
(hour == '' or minute == ''))):
self.raise_error('not_datetime', field)
if field.get_value('ampm_time_style'):
ampm = field.validate_sub_field('ampm', REQUEST, key=key)
if field.get_value('allow_empty_time'):
if ampm == '':
ampm = 'am'
hour = int(hour)
# handling not am or pm
# handling hour > 12
if ((ampm != 'am') and (ampm != 'pm')) or (hour > 12):
self.raise_error('not_datetime', field)
if (ampm == 'pm') and (hour == 0):
self.raise_error('not_datetime', field)
elif ampm == 'pm' and hour < 12:
hour += 12
# handle possible timezone input
timezone = ''
if field.get_value('timezone_style'):
timezone = field.validate_sub_field('timezone', REQUEST, key=key)
try:
# handling of hidden day, which can be first or last day of the month:
if field.get_value('hidden_day_is_last_day'):
if int(month) == 12:
tmp_year = int(year) + 1
tmp_month = 1
else:
tmp_year = int(year)
tmp_month = int(month) + 1
tmp_day = DateTime(tmp_year, tmp_month, 1, hour, minute)
result = tmp_day - 1
else:
result = DateTime(int(year),
int(month),
int(day),
hour,
minute)
year = result.year()
result = DateTime('%s/%s/%s %s:%s %s' % (year,
int(month),
int(day),
hour,
minute, timezone))
# ugh, a host of string based exceptions (not since Zope 2.7)
except ('DateTimeError', 'Invalid Date Components', 'TimeError',
DateError, TimeError) :
self.raise_error('not_datetime', field)
# check if things are within range
start_datetime = field.get_value('start_datetime')
if (start_datetime not in (None, '') and
result < start_datetime):
self.raise_error('datetime_out_of_range', field)
end_datetime = field.get_value('end_datetime')
if (end_datetime not in (None, '') and
result >= end_datetime):
self.raise_error('datetime_out_of_range', field)
return result
DateTimeValidatorInstance = DateTimeValidator()
class SuppressValidator(ValidatorBase):
"""A validator that is actually not used.
"""
def need_validate(self, field, key, REQUEST):
"""Don't ever validate; suppress result in output.
"""
return 0
SuppressValidatorInstance = SuppressValidator()
import string
from DummyField import fields
from DocumentTemplate.DT_Util import html_quote
from DateTime import DateTime
from cgi import escape
import types
from DocumentTemplate.ustr import ustr
class Widget:
"""A field widget that knows how to display itself as HTML.
"""
property_names = ['title', 'description',
'default', 'css_class', 'alternate_name',
'hidden']
title = fields.StringField('title',
title='Title',
description=(
"The title of this field. This is the title of the field that "
"will appear in the form when it is displayed. Required."),
default="",
required=1)
description = fields.TextAreaField('description',
title='Description',
description=(
"Description of this field. The description property can be "
"used to add a short description of what a field does; such as "
"this one."),
default="",
width="20", height="3",
required=0)
css_class = fields.StringField('css_class',
title='CSS class',
description=(
"The CSS class of the field. This can be used to style your "
"formulator fields using cascading style sheets. Not required."),
default="",
required=0)
alternate_name = fields.StringField('alternate_name',
title='Alternate name',
description=(
"An alternative name for this field. This name will show up in "
"the result dictionary when doing validation, and in the REQUEST "
"if validation goes to request. This can be used to support names "
"that cannot be used as Zope ids."),
default="",
required=0)
hidden = fields.CheckBoxField('hidden',
title="Hidden",
description=(
"This field will be on the form, but as a hidden field. The "
"contents of the hidden field will be the default value. "
"Hidden fields are not visible but will be validated."),
default=0)
# NOTE: for ordering reasons (we want extra at the end),
# this isn't in the base class property_names list, but
# instead will be referred to by the subclasses.
extra = fields.StringField('extra',
title='Extra',
description=(
"A string containing extra HTML code for attributes. This "
"string will be literally included in the rendered field."
"This property can be useful if you want "
"to add an onClick attribute to use with JavaScript, for instance."),
default="",
required=0)
def render(self, field, key, value, REQUEST):
"""Renders this widget as HTML using property values in field.
"""
return "[widget]"
def render_hidden(self, field, key, value, REQUEST, render_prefix=None):
"""Renders this widget as a hidden field.
"""
try:
extra = field.get_value('extra')
except KeyError:
# In case extra is not defined as in DateTimeWidget
extra = ''
result = ''
# We must adapt the rendering to the type of the value
# in order to get the correct type back
if isinstance(value, (tuple, list)):
for v in value:
result += render_element("input",
type="hidden",
name="%s:list" % key,
value=v,
extra=extra)
else:
result = render_element("input",
type="hidden",
name=key,
value=value,
extra=extra)
return result
def render_view(self, field, value, REQUEST=None, render_prefix=None):
"""Renders this widget for public viewing.
"""
# default implementation
if value is None:
return ''
return value
render_pdf = render_view
def render_html(self, *args, **kw):
return self.render(*args, **kw)
def render_htmlgrid(self, field, key, value, REQUEST, render_prefix=None):
"""
render_htmlgrid returns a list of tuple (title, html render)
"""
# XXX Calling _render_helper on the field is not optimized
return ((field.get_value('title'),
field._render_helper(key, value, REQUEST, render_prefix=render_prefix)),)
def render_css(self, field, REQUEST):
"""
Default render css for widget - to be overwritten in field classes.
Should return valid css code as string.
The value returned by this method will be used as inline style for a field.
"""
pass
def get_css_list(self, field, REQUEST):
"""
Return CSS needed by the widget - to be overwritten in field classes.
Should return a list of CSS file names.
These names will be appended to global css_list and included in a rendered page.
"""
return []
def get_javascript_list(self, field, REQUEST):
"""
Return JS needed by the widget - to be overwritten in field classes.
Should return a list of javascript file names.
These names will be appended to global js_list and included in a rendered page.
"""
return []
def render_dict(self, field, value):
"""
This is yet another field rendering. It is designed to allow code to
understand field's value data by providing its type and format when
applicable.
"""
return None
class TextWidget(Widget):
"""Text widget
"""
property_names = Widget.property_names +\
['display_width', 'display_maxwidth', 'extra']
default = fields.StringField('default',
title='Default',
description=(
"You can place text here that will be used as the default "
"value of the field, unless the programmer supplies an override "
"when the form is being generated."),
default="",
required=0)
display_width = fields.IntegerField('display_width',
title='Display width',
description=(
"The width in characters. Required."),
default=20,
required=1)
display_maxwidth = fields.IntegerField('display_maxwidth',
title='Maximum input',
description=(
"The maximum input in characters that the widget will allow. "
"Required. If set to 0 or is left empty, there is no maximum. "
"Note that is client side behavior only."),
default="",
required=0)
def render(self, field, key, value, REQUEST, render_prefix=None):
"""Render text input field.
"""
display_maxwidth = field.get_value('display_maxwidth') or 0
if display_maxwidth > 0:
return render_element("input",
type="text",
name=key,
css_class=field.get_value('css_class'),
value=value,
size=field.get_value('display_width'),
maxlength=display_maxwidth,
extra=field.get_value('extra'))
else:
return render_element("input",
type="text",
name=key,
css_class=field.get_value('css_class'),
value=value,
size=field.get_value('display_width'),
extra=field.get_value('extra'))
def render_view(self, field, value, REQUEST=None, render_prefix=None):
"""Render text as non-editable.
This renderer is designed to be type error resistant.
in we get a non string value. It does escape the result
and produces clean xhtml.
Patch the render_view of TextField to enclose the value within <span> html tags if css class defined
"""
if value is None:
return ''
if isinstance(value, types.ListType) or isinstance(value, types.TupleType):
old_value = value
else:
old_value = [str(value)]
value = []
for line in old_value:
value.append(escape(line))
value = '<br/>'.join(value)
css_class = field.get_value('css_class')
if css_class not in ('', None):
# All strings should be escaped before rendering in HTML
# except for editor field
return "<span class='%s'>%s</span>" % (css_class, value)
return value
TextWidgetInstance = TextWidget()
class PasswordWidget(TextWidget):
def render(self, field, key, value, REQUEST, render_prefix=None):
"""Render password input field.
"""
display_maxwidth = field.get_value('display_maxwidth') or 0
if display_maxwidth > 0:
return render_element("input",
type="password",
name=key,
css_class=field.get_value('css_class'),
value=value,
size=field.get_value('display_width'),
maxlength=display_maxwidth,
extra=field.get_value('extra'))
else:
return render_element("input",
type="password",
name=key,
css_class=field.get_value('css_class'),
value=value,
size=field.get_value('display_width'),
extra=field.get_value('extra'))
def render_view(self, field, value, REQUEST=None, render_prefix=None):
return "[password]"
PasswordWidgetInstance = PasswordWidget()
class CheckBoxWidget(Widget):
property_names = Widget.property_names + ['extra']
default = fields.CheckBoxField('default',
title='Default',
description=(
"Default setting of the widget; either checked or unchecked. "
"(true or false)"),
default=0)
def render(self, field, key, value, REQUEST, render_prefix=None):
"""Render checkbox.
"""
rendered = [render_element("input",
type="hidden",
name="default_%s:int" % (key, ),
value="0")
]
if value:
rendered.append(render_element("input",
type="checkbox",
name=key,
css_class=field.get_value('css_class'),
checked=None,
extra=field.get_value('extra'))
)
else:
rendered.append(render_element("input",
type="checkbox",
name=key,
css_class=field.get_value('css_class'),
extra=field.get_value('extra'))
)
return "".join(rendered)
def render_view(self, field, value, REQUEST=None, render_prefix=None):
"""Render checkbox in view mode.
"""
if value:
return render_element("input",
type="checkbox",
css_class=field.get_value('css_class'),
checked=1,
extra=field.get_value('extra'),
disabled='disabled')
else:
return render_element("input",
type="checkbox",
css_class=field.get_value('css_class'),
extra=field.get_value('extra'),
disabled='disabled')
CheckBoxWidgetInstance = CheckBoxWidget()
class TextAreaWidget(Widget):
"""Textarea widget
"""
property_names = Widget.property_names +\
['width', 'height', 'extra']
default = fields.TextAreaField('default',
title='Default',
description=(
"Default value of the text in the widget."),
default="",
width=20, height=3,
required=0)
width = fields.IntegerField('width',
title='Width',
description=(
"The width (columns) in characters. Required."),
default=40,
required=1)
height = fields.IntegerField('height',
title="Height",
description=(
"The height (rows) in characters. Required."),
default=5,
required=1)
def render(self, field, key, value, REQUEST, render_prefix=None):
width = field.get_value('width', REQUEST=REQUEST)
height = field.get_value('height', REQUEST=REQUEST)
return render_element("textarea",
name=key,
css_class=field.get_value('css_class'),
cols=width,
rows=height,
contents=html_quote(value),
extra=field.get_value('extra'))
def render_view(self, field, value, REQUEST, render_prefix=None):
if value is None:
return ''
return value
TextAreaWidgetInstance = TextAreaWidget()
class LinesTextAreaWidget(TextAreaWidget):
property_names = Widget.property_names +\
['width', 'height', 'view_separator', 'extra']
default = fields.LinesField('default',
title='Default',
description=(
"Default value of the lines in the widget."),
default=[],
width=20, height=3,
required=0)
view_separator = fields.StringField('view_separator',
title='View separator',
description=(
"When called with render_view, this separator will be used to "
"render individual items."),
width=20,
default='<br />\n',
whitespace_preserve=1,
required=1)
def render(self, field, key, value, REQUEST, render_prefix=None):
"""
If type definition is missing for LinesField, the input text will be
splitted into list like ['f', 'o', 'o'] with original Formulator's
implementation. So explicit conversion to list is required before
passing to LinesTextAreaWidget's render and render_view methods.
"""
if isinstance(value, (str, unicode)):
value = [value]
value = string.join(value, "\n")
return TextAreaWidget.render(self, field, key, value, REQUEST)
def render_view(self, field, value, REQUEST=None, render_prefix=None):
if value is None:
return ''
elif isinstance(value, (str, unicode)):
value = [value]
return string.join(value, field.get_value('view_separator'))
LinesTextAreaWidgetInstance = LinesTextAreaWidget()
class FileWidget(TextWidget):
def render(self, field, key, value, REQUEST, render_prefix=None):
"""Render text input field.
"""
display_maxwidth = field.get_value('display_maxwidth') or 0
if display_maxwidth > 0:
return render_element("input",
type="file",
name=key,
css_class=field.get_value('css_class'),
value=value,
size=field.get_value('display_width'),
maxlength=display_maxwidth,
extra=field.get_value('extra'))
else:
return render_element("input",
type="file",
name=key,
css_class=field.get_value('css_class'),
value=value,
size=field.get_value('display_width'),
extra=field.get_value('extra'))
def render_view(self, field, value, REQUEST=None, render_prefix=None):
return "[File]"
FileWidgetInstance = FileWidget()
class ItemsWidget(Widget):
"""A widget that has a number of items in it.
"""
items = fields.ListTextAreaField('items',
title='Items',
description=(
"Items in the field. Each row should contain an "
"item. Use the | (pipe) character to separate what is shown "
"to the user from the submitted value. If no | is supplied, the "
"shown value for the item will be identical to the submitted value. "
"Internally the items property returns a list. If a list item "
"is a single value, that will be used for both the display and "
"the submitted value. A list item can also be a tuple consisting "
"of two elements. The first element of the tuple should be a string "
"that is name of the item that should be displayed. The second "
"element of the tuple should be the value that will be submitted. "
"If you want to override this property you will therefore have "
"to return such a list."),
default=[],
width=20,
height=5,
required=0)
# NOTE: for ordering reasons (we want extra at the end),
# this isn't in the base class property_names list, but
# instead will be referred to by the subclasses.
extra_item = fields.StringField('extra_item',
title='Extra per item',
description=(
"A string containing extra HTML code for attributes. This "
"string will be literally included each of the rendered items of the "
"field. This property can be useful if you want "
"to add a disabled attribute to disable all contained items, for "
"instance."),
default="",
required=0)
class SingleItemsWidget(ItemsWidget):
"""A widget with a number of items that has only a single
selectable item.
"""
default = fields.StringField('default',
title='Default',
description=(
"The default value of the widget; this should be one of the "
"elements in the list of items."),
default="",
required=0)
first_item = fields.CheckBoxField('first_item',
title="Select First Item",
description=(
"If checked, the first item will always be selected if "
"no initial default value is supplied."),
default=0)
def render_items(self, field, key, value, REQUEST, render_prefix=None):
# get items
cell = getattr(REQUEST, 'cell', None)
items = field.get_value('items', REQUEST=REQUEST, cell=cell)
if not items:
# single item widget should have at least one child in order to produce
# valid XHTML; disable it so user can not select it
return [self.render_item('', '', '', '', 'disabled="disabled"')]
# check if we want to select first item
if not value and field.get_value('first_item', REQUEST=REQUEST,
cell=cell) and len(items) > 0:
try:
text, value = items[0]
except ValueError:
value = items[0]
css_class = field.get_value('css_class')
extra_item = field.get_value('extra_item')
# if we run into multiple items with same value, we select the
# first one only (for now, may be able to fix this better later)
selected_found = 0
rendered_items = []
for item in items:
try:
item_text, item_value = item
except ValueError:
item_text = item
item_value = item
if item_value == value and not selected_found:
rendered_item = self.render_selected_item(escape(ustr(item_text)),
item_value,
key,
css_class,
extra_item)
selected_found = 1
else:
rendered_item = self.render_item(escape(ustr(item_text)),
item_value,
key,
css_class,
extra_item)
rendered_items.append(rendered_item)
# XXX We want to make sure that we always have the current value in items. -yo
if not selected_found and value:
value = escape(ustr(value))
rendered_item = self.render_selected_item('??? (%s)' % value,
value,
key,
css_class,
extra_item)
rendered_items.append(rendered_item)
return rendered_items
def render_view(self, field, value, REQUEST=None, render_prefix=None):
"""
This method is not as efficient as using a StringField in read only.
Always consider to change the field in your Form.
"""
if value is None:
return ''
title_list = [x[0] for x in field.get_value("items", REQUEST=REQUEST) if x[1]==value]
if len(title_list) == 0:
return "??? (%s)" % escape(value)
else:
return title_list[0]
return value
render_pdf = render_view
class MultiItemsWidget(ItemsWidget):
"""A widget with a number of items that has multiple selectable
items.
"""
default = fields.LinesField('default',
title='Default',
description=(
"The initial selections of the widget. This is a list of "
"zero or more values. If you override this property from Python "
"your code should return a Python list."),
width=20, height=3,
default=[],
required=0)
view_separator = fields.StringField('view_separator',
title='View separator',
description=(
"When called with render_view, this separator will be used to "
"render individual items."),
width=20,
default='<br />\n',
whitespace_preserve=1,
required=1)
def render_items(self, field, key, value, REQUEST, render_prefix=None):
# list is needed, not a tuple
if isinstance(value, tuple):
value = list(value)
# need to deal with single item selects
if not isinstance(value, list):
value = [value]
# XXX -yo
selected_found = {}
items = field.get_value('items', REQUEST=REQUEST, cell=getattr(REQUEST, 'cell', None)) # Added request
from Products.ERP5Form.MultiLinkField import MultiLinkFieldWidget
if not items and not isinstance(self, MultiLinkFieldWidget):
# multi items widget should have at least one child in order to produce
# valid XHTML; disable it so user can not select it.
# This cannot be applied to MultiLinkFields, which are just some <a>
# links
return [self.render_item('', '', '', '', 'disabled="disabled"')]
css_class = field.get_value('css_class')
extra_item = field.get_value('extra_item')
rendered_items = []
for item in items:
try:
item_text, item_value = item
except ValueError:
item_text = item
item_value = item
if item_value in value:
rendered_item = self.render_selected_item(
escape(ustr(item_text)),
escape(ustr(item_value)),
key,
css_class,
extra_item)
# XXX -yo
index = value.index(item_value)
selected_found[index] = 1
else:
rendered_item = self.render_item(
escape(ustr(item_text)),
escape(ustr(item_value)),
key,
css_class,
extra_item)
rendered_items.append(rendered_item)
# XXX We want to make sure that we always have the current value in items. -yo
for index in range(len(value)):
v = value[index]
if index not in selected_found and v:
v = escape(v)
rendered_item = self.render_selected_item('??? (%s)' % v,
v,
key,
css_class,
extra_item)
rendered_items.append(rendered_item)
# Moved marked field to Render
# rendered_items.append(render_element('input', type='hidden', name="default_%s:int" % (key, ), value="0"))
return rendered_items
def render_items_view(self, field, value):
if type(value) is not type([]):
value = [value]
items = field.get_value('items')
d = {}
for item in items:
try:
item_text, item_value = item
except ValueError:
item_text = item
item_value = item
d[item_value] = item_text
result = []
for e in value:
result.append(d[e])
return result
def render_view(self, field, value, REQUEST=None, render_prefix=None):
if value is None:
return ''
return string.join(self.render_items_view(field, value),
field.get_value('view_separator'))
class ListWidget(SingleItemsWidget):
"""List widget.
"""
property_names = Widget.property_names +\
['first_item', 'items', 'size', 'extra', 'extra_item']
size = fields.IntegerField('size',
title='Size',
description=(
"The display size in rows of the field. If set to 1, the "
"widget will be displayed as a drop down box by many browsers, "
"if set to something higher, a list will be shown. Required."),
default=5,
required=1)
def render(self, field, key, value, REQUEST, render_prefix=None):
rendered_items = self.render_items(field, key, value, REQUEST)
input_hidden = render_element('input', type='hidden',
name="default_%s:int" % (key, ), value="0")
list_widget = render_element(
'select',
name=key,
css_class=field.get_value('css_class', REQUEST=REQUEST),
size=field.get_value('size', REQUEST=REQUEST),
contents=string.join(rendered_items, "\n"),
extra=field.get_value('extra', REQUEST=REQUEST))
return "\n".join([list_widget, input_hidden])
def render_item(self, text, value, key, css_class, extra_item):
return render_element('option', contents=text, value=value,
extra=extra_item)
def render_selected_item(self, text, value, key, css_class, extra_item):
return render_element('option', contents=text, value=value,
selected=None, extra=extra_item)
ListWidgetInstance = ListWidget()
class MultiListWidget(MultiItemsWidget):
"""List widget with multiple select.
"""
property_names = Widget.property_names +\
['items', 'size', 'view_separator', 'extra', 'extra_item']
size = fields.IntegerField('size',
title='Size',
description=(
"The display size in rows of the field. If set to 1, the "
"widget will be displayed as a drop down box by many browsers, "
"if set to something higher, a list will be shown. Required."),
default=5,
required=1)
def render(self, field, key, value, REQUEST, render_prefix=None):
rendered_items = self.render_items(field, key, value, REQUEST)
input_hidden = render_element('input', type='hidden', name="default_%s:int" % (key, ), value="0")
multi_list = render_element(
'select',
name=key,
multiple=None,
css_class=field.get_value('css_class', REQUEST=REQUEST),
size=field.get_value('size', REQUEST=REQUEST),
contents=string.join(rendered_items, "\n"),
extra=field.get_value('extra', REQUEST=REQUEST))
return "\n".join([multi_list,input_hidden])
def render_item(self, text, value, key, css_class, extra_item):
return render_element('option', contents=text, value=value,
extra=extra_item)
def render_selected_item(self, text, value, key, css_class, extra_item):
return render_element('option', contents=text, value=value,
selected=None, extra=extra_item)
MultiListWidgetInstance = MultiListWidget()
class RadioWidget(SingleItemsWidget):
"""radio buttons widget.
"""
property_names = Widget.property_names +\
['first_item', 'items', 'orientation', 'extra_item']
orientation = fields.ListField('orientation',
title='Orientation',
description=(
"Orientation of the radio buttons. The radio buttons will "
"be drawn either vertically or horizontally."),
default="vertical",
required=1,
size=1,
items=[('Vertical', 'vertical'),
('Horizontal', 'horizontal')])
def render(self, field, key, value, REQUEST, render_prefix=None):
input_hidden = render_element('input', type='hidden',
name="default_%s" % (key, ), value="")
rendered_items = self.render_items(field, key, value, REQUEST)
rendered_items.append(input_hidden)
orientation = field.get_value('orientation')
if orientation == 'horizontal':
return string.join(rendered_items, "&nbsp;&nbsp;")
else:
return string.join(rendered_items, "<br />")
def render_item(self, text, value, key, css_class, extra_item):
return render_element('input',
type="radio",
css_class=css_class,
name=key,
value=value,
extra=extra_item) + text
def render_selected_item(self, text, value, key, css_class, extra_item):
return render_element('input',
type="radio",
css_class=css_class,
name=key,
value=value,
checked=None,
extra=extra_item) + text
RadioWidgetInstance = RadioWidget()
class MultiCheckBoxWidget(MultiItemsWidget):
"""multiple checkbox widget.
"""
property_names = Widget.property_names +\
['items', 'orientation', 'view_separator', 'extra_item']
orientation = fields.ListField('orientation',
title='Orientation',
description=(
"Orientation of the check boxes. The check boxes will "
"be drawn either vertically or horizontally."),
default="vertical",
required=1,
size=1,
items=[('Vertical', 'vertical'),
('Horizontal', 'horizontal')])
def render(self, field, key, value, REQUEST, render_prefix=None):
rendered_items = self.render_items(field, key, value, REQUEST)
rendered_items.append(render_element('input', type='hidden', name="default_%s:int" % (key, ), value="0"))
orientation = field.get_value('orientation')
if orientation == 'horizontal':
return string.join(rendered_items, "&nbsp;&nbsp;")
else:
return string.join(rendered_items, "<br />")
def render_item(self, text, value, key, css_class, extra_item):
return render_element('input',
type="checkbox",
css_class=css_class,
name=key,
value=value,
extra=extra_item) + text
def render_selected_item(self, text, value, key, css_class, extra_item):
return render_element('input',
type="checkbox",
css_class=css_class,
name=key,
value=value,
checked=None,
extra=extra_item) + text
MultiCheckBoxWidgetInstance = MultiCheckBoxWidget()
class DateTimeWidget(Widget):
"""
Added support for key in every call to render_sub_field
"""
sql_format_year = '%Y'
sql_format_month = '%m'
sql_format_day = '%d'
format_to_sql_format_dict = {'dmy': (sql_format_day , sql_format_month, sql_format_year),
'ymd': (sql_format_year , sql_format_month, sql_format_day ),
'mdy': (sql_format_month, sql_format_day , sql_format_year),
'my' : (sql_format_month, sql_format_year ),
'ym' : (sql_format_year , sql_format_month)
}
sql_format_default = format_to_sql_format_dict['ymd']
hide_day = fields.CheckBoxField('hide_day',
title="Hide Day",
description=(
"The day will be hidden on the output. Instead the default"
"Day will be taken"),
default=0)
hidden_day_is_last_day = fields.CheckBoxField('hidden_day_is_last_day',
title="Hidden Day is last day of the Month",
description=(
"Defines wether hidden day means, you want the last day of the month"
"Else it will be the first day"),
default=0)
timezone_style = fields.CheckBoxField('timezone_style',
title="Display timezone",
description=("Display timezone"),
default=0)
default = fields.DateTimeField('default',
title="Default",
description=("The default datetime."),
default=None,
display_style="text",
display_order="ymd",
input_style="text",
required=0)
default_now = fields.CheckBoxField('default_now',
title="Default to now",
description=(
"Default date and time will be the date and time at showing of "
"the form (if the default is left empty)."),
default=0)
date_separator = fields.StringField('date_separator',
title='Date separator',
description=(
"Separator to appear between year, month, day."),
default="/",
required=0,
display_width=2,
display_maxwith=2,
max_length=2)
time_separator = fields.StringField('time_separator',
title='Time separator',
description=(
"Separator to appear between hour and minutes."),
default=":",
required=0,
display_width=2,
display_maxwith=2,
max_length=2)
input_style = fields.ListField('input_style',
title="Input style",
description=(
"The type of input used. 'text' will show the date part "
"as text, while 'list' will use dropdown lists instead."),
default="text",
items=[("text", "text"),
("list", "list")],
size=1)
input_order = fields.ListField('input_order',
title="Input order",
description=(
"The order in which date input should take place. Either "
"year/month/day, day/month/year or month/day/year."),
default="ymd",
items=[("year/month/day", "ymd"),
("day/month/year", "dmy"),
("month/day/year", "mdy")],
required=1,
size=1)
date_only = fields.CheckBoxField('date_only',
title="Display date only",
description=(
"Display the date only, not the time."),
default=0)
ampm_time_style = fields.CheckBoxField('ampm_time_style',
title="AM/PM time style",
description=(
"Display time in am/pm format."),
default=0)
property_names = Widget.property_names +\
['default_now', 'date_separator', 'time_separator',
'input_style', 'input_order', 'date_only',
'ampm_time_style', 'timezone_style', 'hide_day',
'hidden_day_is_last_day']
def getInputOrder(self, field):
input_order = field.get_value('input_order')
if field.get_value('hide_day'):
if input_order == 'ymd':
input_order = 'ym'
elif input_order in ('dmy', 'mdy'):
input_order = 'my'
return input_order
def render_dict(self, field, value, render_prefix=None):
"""
This is yet another field rendering. It is designed to allow code to
understand field's value data by providing its type and format when
applicable.
It returns a dict with 3 keys:
type : Text representation of value's type.
format: Type-dependant-formated formating information.
This only describes the field format settings, not the actual
format of provided value.
query : Passthrough of given value.
"""
format_dict = self.format_to_sql_format_dict
input_order = format_dict.get(self.getInputOrder(field),
self.sql_format_default)
if isinstance(value, unicode):
value = value.encode(field.get_form_encoding())
return {'query': value,
'format': field.get_value('date_separator').join(input_order),
'type': 'date'}
def render(self, field, key, value, REQUEST, render_prefix=None):
use_ampm = field.get_value('ampm_time_style')
use_timezone = field.get_value('timezone_style')
# FIXME: backwards compatibility hack:
if not hasattr(field, 'sub_form'):
field.sub_form = create_datetime_text_sub_form()
# Is it still usefull to test the None value,
# as DateTimeField should be considerer as the other field
# and get an empty string as default value?
# XXX hasattr(REQUEST, 'form') seems useless,
# because REQUEST always has a form property
if (value in (None, '')) and (field.get_value('default_now')) and \
((REQUEST is None) or (not hasattr(REQUEST, 'form')) or \
(not REQUEST.form.has_key('subfield_%s_%s' % (key, 'year')))):
value = DateTime()
year = None
month = None
day = None
hour = None
minute = None
ampm = None
timezone = None
if isinstance(value, DateTime):
year = "%04d" % value.year()
month = "%02d" % value.month()
day = "%02d" % value.day()
if use_ampm:
hour = "%02d" % value.h_12()
else:
hour = "%02d" % value.hour()
minute = "%02d" % value.minute()
ampm = value.ampm()
timezone = value.timezone()
input_order = self.getInputOrder(field)
if input_order == 'ymd':
order = [('year', year),
('month', month),
('day', day)]
elif input_order == 'dmy':
order = [('day', day),
('month', month),
('year', year)]
elif input_order == 'mdy':
order = [('month', month),
('day', day),
('year', year)]
elif input_order == 'my':
order = [('month', month),
('year', year)]
elif input_order == 'ym':
order = [('year', year),
('month', month)]
else:
order = [('year', year),
('month', month),
('day', day)]
result = []
for sub_field_name, sub_field_value in order:
result.append(field.render_sub_field(sub_field_name,
sub_field_value, REQUEST, key=key))
date_result = string.join(result, field.get_value('date_separator'))
if not field.get_value('date_only'):
time_result = (field.render_sub_field('hour', hour, REQUEST, key=key) +
field.get_value('time_separator') +
field.render_sub_field('minute', minute, REQUEST, key=key))
if use_ampm:
time_result += '&nbsp;' + field.render_sub_field('ampm',
ampm, REQUEST, key=key)
if use_timezone:
time_result += '&nbsp;' + field.render_sub_field('timezone',
timezone, REQUEST, key=key)
return date_result + '&nbsp;&nbsp;&nbsp;' + time_result
else:
return date_result
def format_value(self, field, value, mode='html'):
# Is it still usefull to test the None value,
# as DateTimeField should be considerer as the other field
# and get an empty string as default value?
if value in (None, ''):
return ''
use_ampm = field.get_value('ampm_time_style')
use_timezone = field.get_value('timezone_style')
year = "%04d" % value.year()
month = "%02d" % value.month()
day = "%02d" % value.day()
if use_ampm:
hour = "%02d" % value.h_12()
else:
hour = "%02d" % value.hour()
minute = "%02d" % value.minute()
ampm = value.ampm()
timezone = value.timezone()
order = self.getInputOrder(field)
if order == 'ymd':
output = [year, month, day]
elif order == 'dmy':
output = [day, month, year]
elif order == 'mdy':
output = [month, day, year]
elif order == 'my':
output = [month, year]
elif order == 'ym':
output = [year, month]
else:
output = [year, month, day]
date_result = string.join(output, field.get_value('date_separator'))
if mode in ('html', ):
space = '&nbsp;'
else:
space = ' '
if not field.get_value('date_only'):
time_result = hour + field.get_value('time_separator') + minute
if use_ampm:
time_result += space + ampm
if use_timezone:
time_result += space + timezone
return date_result + (space * 3) + time_result
else:
return date_result
def render_view(self, field, value, REQUEST=None, render_prefix=None):
return self.format_value(field, value, mode='html')
def render_pdf(self, field, value, render_prefix=None):
return self.format_value(field, value, mode='pdf')
DateTimeWidgetInstance = DateTimeWidget()
class LabelWidget(Widget):
"""Widget that is a label only. It simply returns its default value.
"""
property_names = ['title', 'description',
'default', 'css_class', 'hidden', 'extra']
default = fields.TextAreaField(
'default',
title="Label text",
description="Label text to render",
default="",
width=20, height=3,
required=0)
def render(self, field, value, REQUEST=None, render_prefix=None):
return render_element("div",
css_class=field.get_value('css_class'),
contents=field.get_value('default'))
# XXX should render view return the same information as render?
def render_view(self, field, value, REQUEST=None, render_prefix=None):
return field.get_value('default')
LabelWidgetInstance = LabelWidget()
def render_tag(tag, **kw):
"""Render the tag. Well, not all of it, as we may want to / it.
"""
attr_list = []
# special case handling for css_class
if kw.has_key('css_class'):
if kw['css_class'] != "":
attr_list.append('class="%s"' % kw['css_class'])
del kw['css_class']
# special case handling for extra 'raw' code
if kw.has_key('extra'):
extra = kw['extra'] # could be empty string but we don't care
del kw['extra']
else:
extra = ""
# handle other attributes
for key, value in kw.items():
if value == None:
value = key
attr_list.append('%s="%s"' % (key, html_quote(value)))
attr_str = string.join(attr_list, " ")
return "<%s %s %s" % (tag, attr_str, extra)
def render_element(tag, **kw):
if kw.has_key('contents'):
contents = kw['contents']
del kw['contents']
return "%s>%s</%s>" % (apply(render_tag, (tag, ), kw), contents, tag)
else:
return apply(render_tag, (tag, ), kw) + " />"
##############################################################################
#
# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
# Jean Paul Smets <jp@nexedi.com>
# Jerome Perrin <jerome@nexedi.com>
# Yoshinori Okuji <yo@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
class IntegerWidget(TextWidget) :
def render(self, field, key, value, REQUEST, render_prefix=None) :
"""Render an editable integer.
"""
if isinstance(value, float):
value = int(value)
display_maxwidth = field.get_value('display_maxwidth') or 0
if display_maxwidth > 0:
return render_element("input",
type="text",
name=key,
css_class=field.get_value('css_class'),
value=value,
size=field.get_value('display_width'),
maxlength=display_maxwidth,
extra=field.get_value('extra'))
else:
return render_element("input",
type="text",
name=key,
css_class=field.get_value('css_class'),
value=value,
size=field.get_value('display_width'),
extra=field.get_value('extra'))
def render_view(self, field, value, REQUEST=None, render_prefix=None):
"""Render a non-editable interger."""
if isinstance(value, float):
value = int(value)
return TextWidget.render_view(self, field, value, REQUEST=REQUEST)
IntegerWidgetInstance = IntegerWidget()
class FloatWidget(TextWidget):
property_names = TextWidget.property_names +\
['input_style','precision']
input_style = fields.ListField('input_style',
title="Input style",
description=(
"The type of float we should enter. "),
default="-1234.5",
items=[("-1234.5", "-1234.5"),
("-1 234.5", "-1 234.5"),
("-12.3%", "-12.3%"),],
required=1,
size=1)
precision = fields.IntegerField('precision',
title='Precision',
description=(
"Number of digits after the decimal point"),
default=None,
required=0)
def format_value(self, field, value):
"""Formats the value as requested"""
if value not in (None,''):
precision = field.get_value('precision')
input_style = field.get_value('input_style')
percent = 0
original_value = value
if input_style.find('%')>=0:
percent=1
try:
value = float(value) * 100
except ValueError:
return value
try :
float_value = float(value)
if precision not in (None, ''):
float_value = round(float_value, precision)
value = str(float_value)
except ValueError:
return value
else:
if 'e' in value:
# %f will not use exponential format
value = '%f' % float(original_value)
value_list = value.split('.')
integer = value_list[0]
if input_style.find(' ')>=0:
integer = value_list[0]
i = len(integer)%3
value = integer[:i]
while i != len(integer):
value += ' ' + integer[i:i+3]
i += 3
else:
value = value_list[0]
if precision != 0:
value += '.'
if precision not in (None,''):
for i in range(0,precision):
if i < len(value_list[1]):
value += value_list[1][i]
else:
value += '0'
else:
value += value_list[1]
if percent:
value += '%'
return value.strip()
return ''
def render(self, field, key, value, REQUEST, render_prefix=None):
"""Render Float input field
"""
value = self.format_value(field, value)
display_maxwidth = field.get_value('display_maxwidth') or 0
extra_keys = {}
if display_maxwidth > 0:
extra_keys['maxlength'] = display_maxwidth
return render_element( "input",
type="text",
name=key,
css_class=field.get_value('css_class'),
value=value,
size=field.get_value('display_width'),
extra=field.get_value('extra'),
**extra_keys)
def render_view(self, field, value, REQUEST=None, render_prefix=None):
"""
Render Float display field.
This patch add:
* replacement of spaces by unbreakable spaces if the content is float-like
* support of extra CSS class when render as pure text
"""
value = self.format_value(field, value)
float_value = None
try:
float_value = float(value.replace(' ', ''))
except:
pass
if float_value != None:
value = value.replace(' ', '&nbsp;')
extra = field.get_value('extra')
if extra not in (None, ''):
value = "<div %s>%s</div>" % (extra, value)
css_class = field.get_value('css_class')
if css_class not in ('', None):
return "<span class='%s'>%s</span>" % (css_class, value)
return value
def render_pdf(self, field, value, render_prefix=None):
"""Render the field as PDF."""
return self.format_value(field, value)
def render_dict(self, field, value, render_prefix=None):
"""
This is yet another field rendering. It is designed to allow code to
understand field's value data by providing its type and format when
applicable.
It returns a dict with 3 keys:
type : Text representation of value's type.
format: Type-dependant-formated formating information.
This only describes the field format settings, not the actual
format of provided value.
query : Passthrough of given value.
"""
input_style = field.get_value('input_style')
precision = field.get_value('precision')
if precision not in (None, '') and precision != 0:
for x in xrange(1, precision):
input_style += '5'
else:
input_style = input_style.split('.')[0]
if isinstance(value, unicode):
value = value.encode(field.get_form_encoding())
return {'query': value,
'format': input_style,
'type': 'float'}
FloatWidgetInstance = FloatWidget()
class LinkWidget(TextWidget):
def render_view(self, field, value, REQUEST=None, render_prefix=None):
"""Render link.
"""
link_type = field.get_value('link_type', REQUEST=REQUEST)
if REQUEST is None:
REQUEST = get_request()
if link_type == 'internal':
value = urljoin(REQUEST['BASE0'], value)
elif link_type == 'relative':
value = urljoin(REQUEST['URL1'], value)
return '<a href="%s">%s</a>' % (value,
field.get_value('title', cell=getattr(REQUEST,'cell',None)))
LinkWidgetInstance = LinkWidget()
from xml.dom.minidom import parse, parseString, Node
# an extremely simple system for loading in XML into objects
class Object:
pass
class XMLObject:
def __init__(self):
self.elements = Object()
self.first = Object()
self.attributes = {}
self.text = ''
def getElementNames(self):
return [element for element in dir(self.elements)
if not element.startswith('__')]
def getAttributes(self):
return self.attributes
def elementToObject(parent, node):
# create an object to represent element node
object = XMLObject()
# make object attributes off node attributes
for key, value in node.attributes.items():
object.attributes[key] = value
# make lists of child elements (or ignore them)
for child in node.childNodes:
nodeToObject(object, child)
# add ourselves to parent node
name = str(node.nodeName)
l = getattr(parent.elements, name, [])
l.append(object)
setattr(parent.elements, name, l)
def attributeToObject(parent, node):
# should never be called
pass
def textToObject(parent, node):
# add this text to parents text content
parent.text += node.data
def processingInstructionToObject(parent, node):
# don't do anything with these
pass
def commentToObject(parent, node):
# don't do anything with these
pass
def documentToObject(parent, node):
elementToObject(parent, node.documentElement)
def documentTypeToObject(parent, node):
# don't do anything with these
pass
_map = {
Node.ELEMENT_NODE: elementToObject,
Node.ATTRIBUTE_NODE: attributeToObject,
Node.TEXT_NODE: textToObject,
# Node.CDATA_SECTION_NODE:
# Node.ENTITY_NODE:
Node.PROCESSING_INSTRUCTION_NODE: processingInstructionToObject,
Node.COMMENT_NODE: commentToObject,
Node.DOCUMENT_NODE: documentToObject,
Node.DOCUMENT_TYPE_NODE: documentTypeToObject,
# Node.NOTATION_NODE:
}
def nodeToObject(parent, node):
_map[node.nodeType](parent, node)
def simplify_single_entries(object):
for name in object.getElementNames():
l = getattr(object.elements, name)
# set the first subelement (in case it's just one, this is easy)
setattr(object.first, name, l[0])
# now do the same for rest
for element in l:
simplify_single_entries(element)
def XMLToObjectsFromFile(path):
return XMLToObjects(parse(path))
def XMLToObjectsFromString(s):
return XMLToObjects(parseString(s))
def XMLToObjects(document):
object = XMLObject()
documentToObject(object, document)
document.unlink()
simplify_single_entries(object)
return object
import XMLObjects
from Products.Formulator.TALESField import TALESMethod
from Products.Formulator.MethodField import Method
def XMLToForm(s, form, override_encoding=None):
"""Takes an xml string and changes formulator form accordingly.
Heavily inspired by code from Nikolay Kim.
If override_encoding is set, form data is read assuming given
encoding instead of the one in the XML data itself. The form will
have to be modified afterwards to this stored_encoding itself.
"""
top = XMLObjects.XMLToObjectsFromString(s)
# wipe out groups
form.groups = {'Default':[]}
form.group_list = ['Default']
if override_encoding is None:
try:
unicode_mode = top.first.form.first.unicode_mode.text
except AttributeError:
unicode_mode = 'false'
# retrieve encoding information from XML
if unicode_mode == 'true':
# just use unicode strings being read in
encoding = None
else:
# store strings as specified encoding
try:
encoding = top.first.form.first.stored_encoding.text
except AttributeError:
encoding = 'ISO-8859-1'
else:
if override_encoding == 'unicode':
encoding = None
else:
encoding = override_encoding
# get the settings
settings = [field.id for field in form.settings_form.get_fields()]
for setting in settings:
value = getattr(top.first.form.first, setting, None)
if value is None:
continue
if setting == 'unicode_mode':
v = value.text == 'true'
elif setting == 'row_length':
v = int(value.text)
else:
v = encode(value.text, encoding)
setattr(form, setting, v)
# create groups
has_default = 0
for group in top.first.form.first.groups.elements.group:
# get group title and create group
group_title = encode(group.first.title.text, encoding)
if group_title == 'Default':
has_default = 1
form.add_group(group_title)
# create fields in group
if not hasattr(group.first.fields.elements, 'field'):
# empty <fields> element
continue
for entry in group.first.fields.elements.field:
id = str(encode(entry.first.id.text, encoding))
meta_type = encode(entry.first.type.text, encoding)
try:
form._delObject(id)
except (KeyError, AttributeError):
pass
form.manage_addField(id, '', meta_type)
field = form._getOb(id)
if group_title != 'Default':
form.move_field_group([id], 'Default', group_title)
# set values
values = entry.first.values
for name in values.getElementNames():
value = getattr(values.first, name)
if value.attributes.get('type') == 'float':
field.values[name] = float(value.text)
elif value.attributes.get('type') == 'int':
field.values[name] = int(value.text)
elif value.attributes.get('type') == 'method': # XXX Patch
field.values[name] = Method(value.text) # XXX Patch
elif value.attributes.get('type') == 'list':
# XXX bare eval here (this may be a security leak ?)
field.values[name] = eval(
encode(value.text, encoding))
else:
field.values[name] = encode(value.text, encoding)
# special hack for the DateTimeField
if field.meta_type=='DateTimeField':
field.on_value_input_style_changed(
field.get_value('input_style'))
# set tales
tales = entry.first.tales
for name in tales.getElementNames():
field.tales[name] = TALESMethod(
encode(getattr(tales.first, name).text, encoding))
# set messages
if hasattr(entry.first, 'messages'):
messages = entry.first.messages
entries = getattr(messages.elements, 'message', [])
for entry in entries:
name = entry.attributes.get('name')
text = encode(entry.text, encoding)
field.message_values[name] = text
# for persistence machinery
field.values = field.values
field.tales = field.tales
field.message_values = field.message_values
# delete default group
if not has_default:
form.move_group_down('Default')
form.remove_group('Default')
def encode(text, encoding):
if encoding is None:
return text
else:
return text.encode(encoding)
from Globals import DTMLFile
import Form
import StandardFields, HelperFields
from FieldRegistry import FieldRegistry
import Errors
from Products.PythonScripts.Utility import allow_module
try:
import Products.FileSystemSite
except ImportError:
try:
import Products.CMFCore
except ImportError:
pass
else:
import FSForm
else:
import FSForm
# Allow Errors to be imported TTW
allow_module('Products.Formulator.Errors')
def initialize(context):
"""Initialize the Formulator product.
"""
# register field classes
FieldRegistry.registerField(StandardFields.StringField,
'www/StringField.gif')
FieldRegistry.registerField(StandardFields.CheckBoxField,
'www/CheckBoxField.gif')
FieldRegistry.registerField(StandardFields.IntegerField,
'www/IntegerField.gif')
FieldRegistry.registerField(StandardFields.TextAreaField,
'www/TextAreaField.gif')
FieldRegistry.registerField(StandardFields.RawTextAreaField,
'www/TextAreaField.gif')
FieldRegistry.registerField(StandardFields.LinesField,
'www/LinesField.gif')
FieldRegistry.registerField(StandardFields.ListField,
'www/ListField.gif')
FieldRegistry.registerField(StandardFields.MultiListField,
'www/MultiListField.gif')
FieldRegistry.registerField(StandardFields.RadioField,
'www/RadioField.gif')
FieldRegistry.registerField(StandardFields.MultiCheckBoxField,
'www/MultiCheckBoxField.gif')
FieldRegistry.registerField(StandardFields.PasswordField,
'www/PasswordField.gif')
FieldRegistry.registerField(StandardFields.EmailField,
'www/EmailField.gif')
FieldRegistry.registerField(StandardFields.PatternField,
'www/PatternField.gif')
FieldRegistry.registerField(StandardFields.FloatField,
'www/FloatField.gif')
FieldRegistry.registerField(StandardFields.DateTimeField,
'www/DateTimeField.gif')
FieldRegistry.registerField(StandardFields.FileField,
'www/FileField.gif')
FieldRegistry.registerField(StandardFields.LinkField,
'www/LinkField.gif')
FieldRegistry.registerField(StandardFields.LabelField,
'www/LabelField.gif')
# some helper fields
FieldRegistry.registerField(HelperFields.ListTextAreaField)
FieldRegistry.registerField(HelperFields.MethodField)
FieldRegistry.registerField(HelperFields.TALESField)
# obsolete field (same as helper; useable but not addable)
FieldRegistry.registerField(StandardFields.RangedIntegerField,
'www/RangedIntegerField.gif')
# register help for the product
context.registerHelp()
# register field help for all fields
FieldRegistry.registerFieldHelp(context)
# register the form itself
context.registerClass(
Form.ZMIForm,
constructors = (Form.manage_addForm,
Form.manage_add),
icon = 'www/Form.gif')
# make Dummy Fields into real fields
FieldRegistry.initializeFields()
# do initialization of Form class to make fields addable
Form.initializeForm(FieldRegistry)
<dtml-var standard_html_header>
<h3>Formulator Field - <dtml-var id></h3>
<dtml-in get_groups>
<dtml-let group=sequence-item fields="get_fields_in_group(group)">
<dtml-if fields>
<h4><i><dtml-var "_.string.capitalize(group)"> properties</i></h4>
<dtml-in fields>
<dtml-let field=sequence-item>
<b><dtml-var "field.get_value('title')"> (<dtml-var "field.id">)</b>
<p><dtml-var "field.get_value('description')"></p>
</dtml-let>
</dtml-in>
</dtml-if>
</dtml-let>
</dtml-in>
<h4>More help</h4>
<p><a href="fieldEdit.txt">Field edit screen help</a></p>
<dtml-var standard_html_footer>
<dtml-var manage_page_header>
<dtml-var "manage_form_title(this(), _,
form_title='Add %s' % fieldname,
)">
<p class="form-help">
Add a <dtml-var fieldname> to the form.
</p>
<form action="manage_addField" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<input type="text" name="id" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40" />
</td>
</tr>
<input type="hidden" name="fieldname" value="&dtml-fieldname;">
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit_add"
value=" Add " />
<input class="form-element" type="submit" name="submit_add_and_edit"
value=" Add and Edit " />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-let help_product="'Formulator'" help_topic=meta_type>
<dtml-var manage_tabs>
</dtml-let>
<p class="form-help">
Edit <dtml-var meta_type> properties here.
</p>
<form action="manage_edit" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<dtml-in "form.get_groups()">
<dtml-let group=sequence-item fields="form.get_fields_in_group(group)">
<dtml-if fields>
<tr>
<td colspan="3" class="form-title">
<dtml-var "_.string.capitalize(group)"> properties
</td>
</tr>
<dtml-var fieldListHeader>
<dtml-let current_field="this()">
<dtml-in fields>
<dtml-let field=sequence-item field_id="field.id"
value="current_field.get_orig_value(field_id)"
override="current_field.get_override(field_id)"
tales="current_field.get_tales(field_id)">
<tr>
<td align="left" valign="top">
<div class="form-label">
<dtml-if "tales or override">[</dtml-if><dtml-var "field.title()"><dtml-if "field.has_value('required') and field.get_value('required')">*</dtml-if><dtml-if "tales or override">]</dtml-if>
</div>
</td>
<td align="left" valign="top">
<dtml-var "field.render(value)">
</td>
<td><div class="form-element">
<dtml-var "field.meta_type">
</div></td>
</tr>
</dtml-let>
</dtml-in>
</dtml-let>
</dtml-if>
</dtml-let>
</dtml-in>
<tr>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value="Save Changes" />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
<tr class="list-header">
<td align="left" valign="top">
<div class="form-label">
Name
</div>
</td>
<td align="left" valign="top">
<div class="form-label">
Value
</div>
</td>
<td align="left" valign="top">
<div class="form-label">
Field
</div>
</td>
</tr>
\ No newline at end of file
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
Edit <dtml-var meta_type> error messages here.
</p>
<form action="manage_messages" method="POST">
<table border="0">
<dtml-in "get_error_names()">
<dtml-let name=sequence-item value="get_error_message(name)">
<tr>
<td class="form-label"><dtml-var name></td>
<td><textarea name="&dtml-name;" cols="50" rows="4"><dtml-var value></textarea></td>
</tr>
</dtml-let>
</dtml-in>
<tr><td><input type="submit" value=" OK "></td></tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
Edit <dtml-var meta_type> method overrides here.
</p>
<form action="manage_override" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<dtml-in "override_form.get_groups()">
<dtml-let group=sequence-item fields="override_form.get_fields_in_group(group)">
<dtml-if fields>
<tr>
<td colspan="3" class="form-title">
<dtml-var "_.string.capitalize(group)"> properties
</td>
</tr>
<dtml-var fieldListHeader>
<dtml-let current_field="this()">
<dtml-in fields>
<dtml-let field=sequence-item field_id="field.id"
value="current_field.get_override(field.id)">
<tr>
<td align="left" valign="top">
<div class="form-label">
<dtml-var "field.title()">
</div>
</td>
<td align="left" valign="top">
<dtml-var "field.render(value)">
</td>
<td><div class="form-element">
<dtml-var "current_field.form.get_field(field.id).meta_type">
</div></td>
</tr>
</dtml-let>
</dtml-in>
</dtml-let>
</dtml-if>
</dtml-let>
</dtml-in>
<tr>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value="Save Changes" />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
Edit <dtml-var meta_type> method TALES expressions here.
<dtml-if "not isTALESAvailable()"><br>
<span style="color: #FF0000;">
Zope Page Templates and therefore TALES is not installed.
This tab can therefore not be used.
</span>
</dtml-if>
</p>
<form action="manage_tales" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<dtml-in "override_form.get_groups()">
<dtml-let group=sequence-item fields="tales_form.get_fields_in_group(group)">
<dtml-if fields>
<tr>
<td colspan="3" class="form-title">
<dtml-var "_.string.capitalize(group)"> properties
</td>
</tr>
<dtml-var fieldListHeader>
<dtml-let current_field="this()">
<dtml-in fields>
<dtml-let field=sequence-item field_id="field.id"
value="current_field.get_tales(field.id)">
<tr>
<td align="left" valign="top">
<div class="form-label">
<dtml-var "field.title()">
</div>
</td>
<td align="left" valign="top">
<dtml-var "field.render(value)">
</td>
<td><div class="form-element">
<dtml-var "current_field.form.get_field(field.id).meta_type">
</div></td>
</tr>
</dtml-let>
</dtml-in>
</dtml-let>
</dtml-if>
</dtml-let>
</dtml-in>
<tr>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value="Save Changes" />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
Test this <dtml-var meta_type>.
</p>
<dtml-if fieldTestActivated>
<dtml-try>
<p><div class="form-text">Test successful: <dtml-var "validate(REQUEST)"></div></p>
<dtml-except ValidationError>
<p>There was a validation error:</p>
<table cellspacing="0" cellpadding="2" border="0">
<tr class="list-header">
<td class="form-label">field_id</td>
<td class="form-label">error_key</td>
<td class="form-label">error_text</td>
</tr>
<dtml-let error=error_value>
<tr>
<td class="form-text">
<dtml-var "error.field_id">
</td>
<td class="form-text">
<dtml-var "error.error_key">
</td>
<td class="form-text">
<dtml-var "error.error_text">
</td>
</tr>
</dtml-let>
</table>
</dtml-try>
<hr>
</dtml-if>
<form action="fieldTest" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td class="form-label" >
<dtml-var title>
</td>
<td align="left" valign="top">
<dtml-var "render()">
</td>
</tr>
<input type="hidden" name="fieldTestActivated" value="1">
<tr><td><input type="submit" value="Test"></td></tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var "manage_form_title(this(), _,
form_title='Add Formulator Form',
)">
<p class="form-help">
Formulator Forms allow you to create solid web forms more easily.
</p>
<form action="manage_add" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<input type="text" name="id" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Unicode mode
</div>
</td>
<td align="left" valign="top">
<input type="checkbox" name="unicode_mode" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit_add"
value=" Add " />
<input class="form-element" type="submit" name="submit_add_and_edit"
value=" Add and Edit " />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
Change the display order and grouping of the fields in this form.
</p>
<table border="1" cellspacing="1" cellpadding="3">
<dtml-let all_groups="get_groups(include_empty=1)"
group_length="get_largest_group_length()"
first_group="all_groups and all_groups[0] or None">
<dtml-in "get_group_rows()">
<tr>
<dtml-let groups=sequence-item>
<dtml-in groups>
<dtml-let group=sequence-item>
<td nowrap valign="top">
<table border="0" cellspacing="0" cellpadding="0">
<form action="." method="POST">
<input type="hidden" name="group" value="&dtml-group;">
<tr><td align="center" class="list-header">
<div class="list-nav">
<dtml-var group html_quote>
</div>
</td></tr>
<tr><td align="left">
<dtml-let fields="get_fields_in_group(group)" fields_amount="_.len(fields)">
<table border="0" cellspacing="0" cellpadding="0">
<dtml-in fields>
<dtml-let field=sequence-item field_id="field.id">
<tr><td height="25">
<div class="list-item">
<input type="checkbox" name="&dtml-field_id;">&nbsp;<a href="&dtml-field_id;/manage_main"><img src="&dtml-BASEPATH1;/&dtml-icon;" alt="&dtml-meta_type;" title="&dtml-meta_type;" border="0"></a>&nbsp;<a href="&dtml-field_id;/manage_main"><dtml-var field_id></a>
</div>
</td></tr>
</dtml-let>
</dtml-in>
<dtml-in "_.range(group_length - fields_amount)">
<tr><td height="25"></td></tr>
</dtml-in>
</dtml-let>
</table>
</td></tr>
<tr><td align="center">
<input class="form-element" type="submit" name="manage_move_field_up:method"
value="Move Up">
</td></tr>
<tr><td align="center">
<input class="form-element" type="submit" name="manage_move_field_down:method"
value="Move Dn"><br><br>
</td></tr>
<tr><td align="center">
<div class="form-element">
<select class="form-element" name="to_group" size="1">
<option>Move to:</option>
<dtml-in all_groups>
<option><dtml-var sequence-item html_quote></option>
</dtml-in>
</select>
</div>
</td></tr>
<tr><td align="center">
<input class="form-element" type="submit" name="manage_move_group:method"
value="Transfer">
</td></tr>
<dtml-if "group != first_group">
<tr><td align="center" class="list-header">
<div class="list-item">
Group
</div>
</td></tr>
<tr><td align="center">
<input class="form-element" type="submit" name="manage_move_group_up:method"
value="Move Up">
</td></tr>
<tr><td align="center">
<input class="form-element" type="submit" name="manage_move_group_down:method"
value="Move Dn"><br><br>
</td></tr>
<tr><td align="center">
<input type="text" name="new_name" value="" size="10">
</td></tr>
<tr><td align="center">
<input class="form-element" type="submit" name="manage_rename_group:method"
value="Rename"><br>
</td></tr>
<tr><td align="center">
<input class="form-element" type="submit" name="manage_remove_group:method"
value="Remove"><br>
</td></tr>
<dtml-else>
<tr><td align="center" class="list-header">
<div class="list-item">
Group
</div>
</td></tr>
<tr><td align="center">
<input type="text" name="new_group" value="" size="10">
</td></tr>
<tr><td align="center">
<input type="submit" name="manage_add_group:method" value="Create"><br><br>
</td></tr>
<tr><td align="center">
<input type="text" name="new_name" value="" size="10">
</td></tr>
<tr><td align="center">
<input class="form-element" type="submit" name="manage_rename_group:method"
value="Rename"><br>
</td></tr>
</dtml-if>
</form>
</table>
</td>
</dtml-let>
</dtml-in>
</dtml-let>
</tr>
</dtml-in>
</dtml-let>
</table>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
Settings for this form.
</p>
<dtml-let me="this()">
<dtml-var "settings_form.header()">
<table border="0">
<dtml-in "settings_form.get_fields()"><dtml-let field=sequence-item>
<tr>
<td class="form-label"><dtml-var "field.get_value('title')"></td>
<td><dtml-if "_.getattr(me, field.id)"><dtml-var "field.render(_.getattr(me, field.id, None))"><dtml-else><dtml-var "field.render()"></dtml-if></td>
</tr>
</dtml-let></dtml-in>
<tr>
<td><input type="submit" value="Change"></td>
</tr>
</table>
<dtml-var "settings_form.footer()">
</dtml-let>
<p>Upgrade</p>
<form action="manage_refresh" method="POST">
<p><input type="submit" value="Upgrade"></p>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
Test this form.
</p>
<dtml-if formTestActivated>
<dtml-try>
<dtml-call "validate_all(REQUEST)">
<p>All fields were validated correctly.</p>
<dtml-except FormValidationError>
<p>Not all fields could be validated.</p>
<table cellspacing="0" cellpadding="2" border="0">
<tr class="list-header">
<td><div class="form-label">field_id&nbsp;</div></td>
<td><div class="form-label">error_key</div></td>
<td><div class="form-label">error_text</div></td>
</tr>
<dtml-in "error_value.errors">
<dtml-let error=sequence-item>
<tr>
<td class="form-text">
<dtml-var "error.field_id">
</td>
<td class="form-text">
<dtml-var "error.error_key">
</td>
<td class="form-text">
<dtml-var "error.error_text">
</td>
</tr>
</dtml-let>
</dtml-in>
</table>
</dtml-try>
<hr>
</dtml-if>
<form action="formTest" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<dtml-in "get_groups()">
<dtml-let group=sequence-item fields="get_fields_in_group(group)">
<dtml-if fields>
<tr><td colspan="2" class="list-header"><div class="form-label"><dtml-var group></div></td></tr>
<dtml-in fields>
<dtml-let field=sequence-item>
<tr>
<td align="left" valign="top">
<div class="form-label">
<dtml-var "field.title()">
</div>
</td>
<td align="left" valign="top">
<dtml-var "field.render()">
</td>
</tr>
</dtml-let>
</dtml-in>
</dtml-if>
</dtml-let>
</dtml-in>
<input type="hidden" name="formTestActivated" value="1">
<tr><td><input type="submit" value="Test"></td></tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-call "REQUEST.set('management_page_charset', 'UTF-8')">
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
XML Representation of this form.
</p>
<form action="manage_editXML" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<textarea wrap="off" style="width: 100%;" cols="50" rows="20" name="form_data"><dtml-var get_xml></textarea>
</table>
<input type="submit" name="save_xml" value=" Save " />
</form>
<dtml-var manage_page_footer>
class BasicForm:
"""
Use BasicForm to construct and use Formulator forms from
Python code.
"""
__extends__ = ('Formulator.Form.Form',)
def add_field(field, group=None):
"""
Add a field to a group on
the form. The 'group' argument is optional, and if no group is
given the field is added to the first group. The field is
always added to the bottom of the group.
Permission -- 'Change Formulator Forms'
"""
def add_fields(fields, group=None):
"""
Add a list of fields to a on the form. The 'group' argument is
optional; if no group is given the fields are added to the first
group. Fields are added in the order given to the bottom of the
group.
Permission -- 'Change Formulator Forms'
"""
def remove_field(field):
"""
Remove a particular field from the form.
Permission -- 'Change Formulator Forms'
"""
class Field:
"""Formulator field base class; shared functionality of all
fields.
"""
def initialize_values(dict):
"""
Initialize the values of field properties. 'dict' is a
dictionary. A dictionary entry has as key the property id,
and as value the value the property should take initially.
If there is no entry in the dictionary for a particular
property, the default value for that property will be used.
All old property values will be cleared.
Permission -- 'Change Formulator Fields'
"""
def initialize_overrides():
"""
Clear (initialize to nothing) overrides for all properties.
Permission -- 'Change Formulator Fields'
"""
def has_value(id):
"""
Returns true if a property with name 'id' exists.
Permission -- 'Access contents information'
"""
def get_orig_value(id):
"""
Get value of property without looking at possible overrides.
Permission -- 'Access contents information'
"""
def get_value(id, **kw):
"""
Get property value for an id. Call override if override is
defined, otherwise use property value. Alternatively the
dictionary interface can also be used ('__getitem__()').
Keywords arguments can optionally be passed, which will end up
in the namespace of any TALES expression that gets called.
Permission -- 'Access contents information'
"""
def __getitem__(self, key):
"""
Get property value for an id. Call override if override is
defined, otherwise use property value. Alternatively
'get_value()' can be used to do this explicitly.
In Python, you can access property values using::
form.field['key']
and in Zope Page Templates you can use the following path
notation::
form/field/key
Permission -- 'Access contents information'
"""
def get_override(id):
"""
Get the override method for an id, or empty string
if no such override method exists.
Permission -- 'Access contents information'
"""
def is_required():
"""
A utility method that returns true if this field is required.
(checks for 'required' property).
Permission -- 'Access contents information'
"""
def get_error_names():
"""
Get all keys of error messages that the validator
of this field provides.
Permission -- 'View management screens'
"""
def get_error_message(name):
"""
Get the contents of a particular error message with key
'name'.
Permission -- 'View management screens'
"""
def render(value=None, REQUEST=None):
"""
Get the rendered HTML for the widget of this field, to
display the fields on a form.
'value' -- If the 'value' parameter is not None, this will be
the pre-filled value of the field on the form.
'REQUEST' -- If the 'value' parameter is 'None' and 'REQUEST' is
supplied, raw (unvalidated) values will be looked up in
'REQUEST' to display in the field.
If neither 'value' or 'REQUEST' are supplied, the field's
default value will be used instead.
Permission -- 'View'
"""
def render_from_request(REQUEST):
"""
A convenience method to render the field widget using
the raw data from 'REQUEST' if any is available. The field's
default value will be used if no raw data can be found.
Pemrissions -- 'View'
"""
def render_sub_field(id, value=None, REQUEST=None):
"""
Render a sub field of this field. This is used by composite
fields that are composed of multiple sub fields such as
'DateTimeField'. 'id' is the id of the sub field. 'value' and
'REQUEST' work like in 'render()', but for the sub field.
Permission -- 'View'
"""
def render_sub_field_from_request(id, REQUEST):
"""
A convenience method to render a sub field widget from
'REQUEST' (unvalidated data).
Permission -- 'View'
"""
def validate(REQUEST):
"""
Validate this field using the raw unvalidated data found
in 'REQUEST'.
Returns the validated and processed value, or raises a
ValidationError.
Permission -- 'View'
"""
def validate_sub_field(id, REQUEST):
"""
Validate a sub field of this field using the raw unvalidated
data found in 'REQUEST'. This is used by composite fields
composed of multiple sub fields such as 'DateTimeField'.
Returns the validated and processed value, or raises a
ValidationError.
Permission -- 'View'
"""
def render_view(value):
"""
Render supplied value for viewing, not editing. This can be used
to show form results, for instance.
Permission -- 'View'
"""
class Form:
"""A Formulator Form; this is the base class of all forms.
"""
def move_field_up(field_id, group):
"""
Move the field with 'field_id' up in the group with name 'group'.
Returns 1 if move succeeded, 0 if it failed.
Permission -- 'Change Formulator Forms'
"""
def move_field_down(field_id, group):
"""
Move the field with 'field_id' down in the group with name 'group'.
Returns 1 if move succeeded, 0 if it failed.
Permission -- 'Change Formulator Forms'
"""
def move_field_group(field_ids, from_group, to_group):
"""
Move a number of field ids in the list 'field_ids' from 'from_group'
to 'to_group'.
Returns 1 if move succeeded, 0 if it failed.
Permission -- 'Change Formulator Forms'
"""
def add_group(group):
"""
Add a new group with the name 'group'. The new group must have
a unique name in this form and will be added to the bottom of the
list of groups.
Returns 1 if the new group could be added, 0 if it failed.
Permission -- 'Change Formulator Forms'
"""
def remove_group(group):
"""
Remove an existing group with the name 'group'. All fields that
may have been in the group will be moved to the end of the
first group. The first group can never be removed.
Returns 1 if the group could be removed, 0 if it failed.
Permission -- 'Change Formulator Forms'
"""
def rename_group(group, name):
"""
Give an existing group with the name 'group' a new name. The
new name must be unique.
Returns 1 if the rename succeeded, 0 if it failed.
Permission -- 'Change Formulator Forms'
"""
def move_group_up(group):
"""
Move the group with name 'group' up in the list of groups.
Returns 1 if the move succeeded, 0 if it failed
Permission -- 'Change Formulator Forms'
"""
def move_group_down(group):
"""
Move the group with name 'group' down in the list of groups.
Returns 1 if the move succeeded, 0 if it failed.
Permission -- 'Change Formulator Forms'
"""
def get_fields():
"""
Returns a list of all fields in the form (in all groups). The
order of the fields will be field order in the groups, and the
groups will be in the group order.
Permission -- 'View'
"""
def get_field_ids():
"""
As 'get_fields()', but instead returns a list of ids of all the fields.
Permission -- 'View'
"""
def get_fields_in_group(group):
"""
Get a list in field order in a particular group.
Permission -- 'View'
"""
def has_field(id):
"""
Check whether the form has a field of a certain id. Returns true
if the field exists.
Permission -- 'View'
"""
def get_field(id):
"""
Get a field with a certain id.
Permission -- 'View'
"""
def get_groups():
"""
Get a list of all groups in the form, in group order.
Permission -- 'View'
"""
def render(self, dict=None, REQUEST=None):
"""
Returns a basic HTML rendering (in a table) of this form.
For more sophisticated renderings you'll have to write
DTML or ZPT code yourself.
You can supply an optional 'dict' argument; this should be a
dictionary ('_', the namespace stack, is legal). The
dictionary can contain data that should be pre-filled in the
form (indexed by field id). The optional 'REQUEST' argument
can contain raw form data, which will be used in case nothing
can be found in the dictionary (or if the dictionary does not
exist).
Permission -- 'View'
"""
def validate(REQUEST):
"""
Validate all the fields in this form, looking in REQUEST for
the raw field values. If any validation error occurs,
ValidationError is raised and validation is stopped.
Returns a dictionary with as keys the field ids and as values
the validated and processed field values.
Exceptions that are raised can be caught in the following way
(also in through the web Python scripts)::
from Products.Formulator.Errors import ValidationError
try:
myform.validate(REQUEST)
except ValidationError, e:
print 'error' # handle error 'e'
Permission -- 'View'
"""
def validate_to_request(REQUEST):
"""
Validate all the fields in this form, looking in REQUEST for
the raw field values. If any validation error occurs,
ValidationError is raised and validation is stopped.
Returns a dictionary with as keys the field ids and as values
the validated and processed field values. In addition, this
result will also be added to REQUEST (also with the field ids
as keys).
Exceptions that are raised can be caught in the following way
(also in through the web Python scripts)::
from Products.Formulator.Errors import ValidationError
try:
myform.validate_to_request(REQUEST)
except ValidationError, e:
print 'error' # handle error 'e'
Permission -- 'View'
"""
def validate_all(REQUEST):
"""
Validate all the fields in this form, looking in REQUEST for
the raw field values. If any ValidationError occurs, they are
caught and added to a list of errors; after all validations a
FormValidationError is then raised with this list of
ValidationErrors as the 'errors' attribute.
Returns a dictionary with as keys the field ids and as values
the validated and processed field values.
Exceptions that are raised can be caught in the following way
(also in through the web Python scripts)::
from Products.Formulator.Errors import ValidationError, FormValidationError
try:
myform.validate_all(REQUEST)
except FormValidationError, e:
print 'error' # handle error 'e', which contains 'errors'
Permission -- 'View'
"""
def validate_all_to_request(REQUEST):
"""
Validate all the fields in this form, looking in REQUEST for
the raw field values. If any ValidationError occurs, they are
caught and added to a list of errors; after all validations a
FormValidationError is then raised with this list of
ValidationErrors as the 'errors' attribute.
Returns a dictionary with as keys the field ids and as values
the validated and processed field values. In addition, the
validated fields will be added to REQUEST, as in
'validate_to_request()'. This will always be done for all
fields that validated successfully, even if the validation of
other fields failed and a FormValidationError is raised.
Exceptions that are raised can be caught in the following way
(also in through the web Python scripts)::
from Products.Formulator.Errors import ValidationError, FormValidationError
try:
myform.validate_all_to_request(REQUEST)
except FormValidationError, e:
print 'error' # handle error 'e', which contains 'errors'
Permission -- 'View'
"""
def session_store(session, REQUEST):
"""
Store any validated form data in REQUEST in a session object
(Core Session Tracking).
Permission -- 'View'
"""
def session_retrieve(session, REQUEST):
"""
Retrieve validated form data from session (Core Session
Tracking) into REQUEST.
Permission -- 'View'
"""
def header():
"""
Get the HTML code for the start of a form. This produces a
'<form>' tag with the 'action' and 'method' attributes
that have been set in the Form.
Permission -- 'View'
"""
def footer():
"""
Get the code for the end of the form ('</form>').
Permission -- 'View'
"""
class ZMIForm:
"""Form used from Zope Management Interface. Inherits from
ObjectManager to present folderish view.
"""
__extends__ = ('Formulator.Form.Form',
'OFSP.ObjectManager.ObjectManager',
'OFSP.ObjectManagerItem.ObjectManagerItem')
def manage_addField(id, title, fieldname, REQUEST=None):
"""
Add a new field with 'id' and 'title' of field type
'fieldname' to this ZMIForm. 'REQUEST' is optional. Note that
it's better to use BasicForm and 'add_field' if you want to
use Formulator Forms outside the Zope Management Interface.
Permission -- 'Change Formulator Forms'
"""
How Formulator Eats Its Own Dogfood
**NOTE**: You do not have to read this or understand this in order to
use or even extend Formulator. Your brain may explode. Have fun.
Formulator eats its own dogfood; fields have editable properties
also represented by fields. A field class may contain an instance of
itself in the end! This is accomplished by some hard to comprehend
code, which I've still tried to write down as clearly as
possible. Since at times I still can't figure it out myself, I've
included this textual description.
The following files are in play (in 'Products/Formulator'):
'FieldRegistry.py'
All field classes are registered here (instead of with the
standard Zope addable product registry).
'Field.py'
The main Field classes.
'Form.py'
The main Form classes. BasicForm is used internally for Forms
without any web management UI. PythonForm is a form with a web UI
(derived from ObjectManager).
'StandardFields.py'
Contains a bunch of standard fields that can be used by Formulator,
such as StringField, IntegerField and TextArea field.
'DummyField.py'
Contains a dummy implementation of a field that can be used by
fields before the field classes have been fully defined.
'Widget.py'
Contains the code for displaying a field as HTML. A widget has a
'property_names' list associated with it, as well a number of
class attributes for the fields that help define the parameters of
the widget (dimensions, default value, etc). These are in fact not
real fields at widget creation time, but DummyField instances.
'Validator.py'
Contains the code for validating field input. Like a widget, it
contains a 'property_names' list and a number of
field-but-not-really (DummyField) class attributes.
'__init__.py'
Sets up the fields first, and then registers the Formulator
product (Formulator Form addable) with Zope itself. Somewhat more
complicated than the average Product __init__.py.
'FieldHelpTopic.py'
Used to make the Zope help system automatically generate help for
each field class.
'HelperFields.py'
Collects helper (internal) fields together for easy importing.
'MethodField.py'
Experimental MethodField. Right now only used internally by
ListFields.
'ListTextAreaField.py'
Used internally by ListFields.
'www'
This directory contains dtml files and icons used in the
management screens of Formulator.
'help'
Help files.
Startup Sequence
Before 'initialize()' in '__init__.py' is called:
* the widget and validator classes is initialized, using the
dummy FieldProperty objects as if they are fields as class
attributes.
* Singleton widget instance and validator instance objects are
created.
* The field classes is initialized, with widget and validator
class attributes.
* A singleton FieldRegistry object is created.
'initialize()' - fields are registered with FieldRegistry:
* A reference to the field class is stored in the FieldRegistry.
* A BasicForm instance is created for each field class. Then the
DummyFields associated with the field class' widget and validator
objects are added to the BasicForm (to the Widget and the Validator
groups).
* Each field class now has a BasicForm containing the (dummy) fields
that define its look and feel and behavior.
* The appropriate field icons are also added to field classes.
* the Form is registered with Zope
* finally, initializeFormulator in Form.py is called.
'initialize()' - help is registered:
* the .txt files in the help directory are registered with Zope.
* A special FieldHelpTopic is created for each registered field.
'initialize()' - final touches:
* initializeForm in Form.py registers each (non-internal) field in
the registry with Python Form, so that users can add the fields
to the form.
* initializeFields in the FieldRegistry makes the DummyFields that
stood in for Field properties into real fields, now that
everything else has been registered and set up.
Default properties
It is (for me) hard to understand where default properties are
coming from and should come from. Therefore I've created this
description.
A field has a 'default' property field. This is defined in the field
*class* 'form' class attribute.
A field has a 'default' property value. This is defined on the field
*instance*, in the 'values' dictionary.
Field properties have a 'default' field property and value of their
own, as they are fields! Infinite regress? The *form* is shared by
all fields of the same type, as it's a class attribute. So while
there is infinite regress there, it does not cost infinite amounts
of memory.
A StringField has a 'default' property field that is itself a
StringField. It also has a 'default' property value that is the
empty string. On instantiation of a StringField, the 'default'
property value is either taken from a keyword argument 'default'
given to the __init__ function or, if none is present, from the
default value of the 'default' property field.
When a field is constructed using the Zope management interface this
will use the manage_addField() method, defined on the form.
manage_addField will create a field without any property values, so
the constructor of the field will use the defaults, which it will
take from the defaults of the property_fields.
The propery_fields *have* (indirectly through FieldDummy instances)
been constructed with default values given as keyword arguments.
So this is how it all works out in the end. I hope. My brain just
exploded; how's yours?
Practical advice; don't think too hard about it. If you want
particular property field defaults, pass them as keyword arguments
when you construct a DummyField for use in a Widget or Validator;
if instead you're fine with whatever default the field will come
up with, don't pass a keyword argument.
Creating new types of fields is actually quite easy; trust me.
Formulator Field - Edit
Description
A field has a number of properties that determine its look and
feel as well as its behavior. You can configure these properties
in this tab. You can also use the TALES tab and the Override tab
to associate dynamic behavior with field properties, though the
Override tab is eventually to be phased out in favor of the TALES
tab. Overridden fields will have their names be shown in square
brackets.
Which properties appear in this view depends on what kind of field
you are editing.
Each field has two sets of properties; widget properties and
validator properties.
Widget properties
The widget properties determine the look and feel of the field
that you see on the web page (which HTML code is generated for the
field); i.e. what GUI *widget* you see.
A very common widget property shared by all fields is called
'Title'; all fields have titles -- the name you will see when the
field is displayed on the screen.
Another very common widget property is the 'Default' value of the
widget. This is what will be filled in before the user changes
anything to the form, unless you pass a value to the 'render'
function; see the API reference for more information.
Many widgets also have size information; the StringField for
instance has a 'Display width' property which determines how large
the field should appear on the screen, as well as a 'Maximum
input' property that determines how much the user can enter
(though this is independent from actual server-side validation).
For some widget properties such as 'Maximum input' in StringField
you can set the value to '0'; in that the HTML widget won't care
how much the user inputs.
Validator properties
This set of properties determines how the field validates the
information that is submitted for this field.
A very common validator property is the 'required' property. If a
field is required, the field cannot be left empty by the user when
submitting a web page. Validation in that case will result in
failure.
In case of the 'StringField', one validation property is called
'Maximum length'; the field cannot contain more characters than
that.
For some validator properties such as 'Maximum length' in
StringField, you can set the value to '0'. The validator will then
not care how much the user entered -- there won't be any maximum.
Formulator Field - Messages
Description
Each field has a set of messages that can be displayed in case
validation fails. Standard messages are provided, but you can
change these messages for the particular field in this view. The
message text is the 'error_text' that will show up in case
validation fails.
Formulator Field - Override
Description
Note: the Override tab is being phased out in favor of the TALES
tab.
Sometimes you'd like a field property to be dynamic instead of
just a value filled in in the 'Edit' tab of the field. If you fill
in the name of an 'override' method for a property in the Override
tab, that method will be called whenever you (or the code) asks
for the value of that property using get_value(). Properties which
are overridden are shown between square brackets ([ and ]) in the
main Edit tab, and the value of the property in the edit tab will
be ignored. To stop using an override for a particular property,
remove the method name in the override tab.
An override method should return an object of the same type as the
property field would return after validation. For instance, an
override method for a StringField would return a simple string,
for an IntegerField it would return an integer, and for a
CheckBoxField it would return true or false (or something that can
be interpreted that way).
Example
A good example of the use of the override tab is the 'items'
property of a ListField; frequently you may want to get these
items from elsewhere, for instance from a database. In this case
you would fill in the name of the override method for 'items' that
retrieves the right data.
The 'right data' in this case is that which validation of the
builtin field ListTextArea would return. This is a list of tuples,
one tuple for each element. Each tuple consists of two strings;
the name that should be displayed to the user for that item, and
the actual value that will be submitted.
This for instance is a Python script 'random_numbers' that will return
ten random numbers as the elements::
# random_numbers
import random
result = []
for i in range(10):
number = random.randint(0, 100)
tuple = str(number), str(number)
result.append(tuple)
return result
Formulator Field - TALES
Description
Sometimes you'd like a field property to be dynamic instead of
just a value filled in in the 'Edit' tab of the field. To
make your fields more dynamic, you can enter TALES expressions
in the TALES tab. Whenever you (or some code) asks for the value of
that field property next with 'get_value()', the TALES expression
will be evaluated and the result will be the value of that
property.
Properties which are overridden with a TALES expression are shown
between square brackets ([ and ]) in the main Edit tab, and the
value of the property in the edit tab will be ignored. To stop
using a TALES expression for a particular property, remove the
expression in the TALES tab.
A TALES expression should return an object of the same type as the
property field would return after validation. For instance, a
TALES expression for a StringField would return a simple string,
for an IntegerField it would return an integer, and for a
CheckBoxField it would return true or false (or something that
Python accepts as such).
More information about TALES
The specification:
http://dev.zope.org/Wikis/DevSite/Projects/ZPT/TALES%20Specification%201.3
Predefined variables
Two predefined variables are in the expression namespace, 'form',
and 'field'. You can use them to call their methods (though one
should be careful with some, to avoid infinite recursion), and
also methods and scripts that are the form's (or field's)
acquisition context. You can also pass them to the methods you
call.
Relation to the Override tab
The TALES tab is meant to eventually obsolute the Override tab;
the use of the Override tab can therefore be considered
deprecated. Once Zope Page Templates (and thus TALES) become part
of the Zope core distribution, I plan to phase out the Override
tab altogether.
If an override tab says this:
foo
where foo is a Python Script that is acquired by the form, for
instance, you can now do:
python:form.foo()
This is longer, but the advantage is that you can now pass
parameters, for instance:
python:form.bar(1, 'hey')
Example
A good example of the use of the TALES tab is the 'items' property
of a ListField; frequently you may want to get these items from
elsewhere, for instance from a database. In this case you would
fill in the name of the override method for 'items' that retrieves
the right data.
The 'right data' in this case is that which validation of the
builtin field ListTextArea would return. This is a list of tuples,
one tuple for each element. Each tuple consists of two strings;
the name that should be displayed to the user for that item, and
the actual value that will be submitted.
This for instance is a Python script 'random_numbers' that will
return 'n' random numbers as the elements, where 'n' is the (single)
parameter to the Python script::
# random_numbers
import random
result = []
for i in range(n):
number = random.randint(0, 100)
tuple = str(number), str(number)
result.append(tuple)
return result
You can call this script with the following expression for items,
which will give 10 random numbers.
python:form.random_numbers(10)
Caveat: in the current Formulator implementation it is very hard
to actually go through validation successfully, as exactly the
same random numbers need to be generated twice; once for the
display phase, and once during the validation phase. The
implementation currently assumes the list won't change through
multiple calls to calculate the 'items' property.
Formulator Field - Test
Description
With the 'test' view, you can test the look and feel and
validation behavior of the field. Fill in the field and press the
'Test' button to test it.
You will now see the test form again. If your input was
successfully validated by the field, you will see a message 'Test
successful:' above the test form, followed by the result of the
validation processing (this may be empty text in case you filled
in nothing).
If the field could not be validated, you will see the message 'There was a
validation error:' instead, followed by a description of the error:
* 'field_id' gives the field name.
* 'error_key' is the name of the error message; you can find it
in the 'messages' tab.
* 'error_text' is the actual text associated with the
'error_key'; you can modify it in the 'messages' tab.
\ No newline at end of file
Formulator Form - Contents
Description
The contents view of a Formulator Form behaves much like normal
Zope Folder. The difference is that the only objects listed are
Formulator Fields, and if you look at the 'Add' list you can see
you can only add different types of fields on this screen.
Similarly, you can't copy and paste objects into the form that are
not fields, and it is not allowed to copy fields to normal folders
either.
If you click on a field, you will enter the field edit screen, where
you can modify field properties.
Other Help
The other tabs of the Formulator Form contain their own help text;
fields also provide help.
For more information on how to use the contents view of
Formulator Form, look at the online help for Folders.
Formulator Form - Order
Description
The 'order' view allows you to change the order in which fields
should be displayed in the form. You can also group fields together;
this may be helpful when you create a complicated form.
Any field that gets added to the form ends up in the first
group. This group always exists and cannot be removed.
You can click on the field id to go directly to the field management
screen.
Note that you *cannot* add or remove any fields in this form; you
have to use the main 'contents' view of the form for that.
How this information is used
The order and grouping information is used internally by the
'test' tab of the form, and you can also use it in your own
code. See the Formulator API Reference (to be written; for now
read the source!) for more information on how to do that.
Reordering fields inside a group
You can reorder fields in a group by moving them up and down in
the group. Select the field you want to move using the checkbox in
front of the field id. Then click on the 'Move Up' or 'Move Dn'
button, to move the field up or down respectively. You will see a
feedback message at the top of the screen. You can only move a
single field at a time this way, you select only the field you
want to move!
Creating a new group
The first group has a button called 'Create' at the bottom,
with an input box above it. Fill in the name of the new group that
you want to create there, and press the 'Create' button. The new group
will be added (as the last group visible).
How the groups are displayed
In order to show more information on a single screen, the groups
are ordered in columns from left to right, then in rows from top
to bottom. The 'settings' view allows you to modify how many
groups should appear in a single row (the default is 4
groups). Changing this has no impact on the functionality of the
form itself.
When you add a new group, it will be added to the right of the
current last group, if this still fits in a row. Otherwise a new
row will be created and the group will be displayed there.
Moving fields to a different group
When you create a new group, it remains empty. You can move
several fields into a group by using the 'Transfer' button of a
group. First select one ore more fields in an old group that you
want to move away. Then, use the 'Move to:' dropdown list to
select the group to which you want to move the selected fields.
Then, press the 'Transfer' button. The fields should now disappear
from the origin group and appear in the target group.
Reordering groups
You can change the order of the gropus by using the 'Move Up' and
'Move Dn' buttons in the 'Group' section of a group. This moves
the entire group in the group order. You cannot move the first
group (and it therefore has no group movement buttons).
Renaming groups
You can rename a group by filling in the new name in the input
box above the 'Rename' button, and then pressing that button.
Removing groups
You can remove a group by pressing the 'Remove' button in the 'Group'
section of the group. The entire group will disappear. Any fields that
were in the group will move to the bottom of the first group;
you cannot lose any fields this way.
Formulator Form - Settings
Description
You can set some basic settings of the form.
Number of groups in row (in order tab)
Change the amount of groups that should appear in a single row
in the 'order' view. The default is '4'.
Form action
The method the form should call when it is submitted. If you use
the 'header()' method of Form, it will use this as the 'action'
attribute of the HTML form.
Form method
The submit method of the form (not to be confused with a method in
Python of Zope). 'POST' is generally used for forms that change
underlying data, while 'GET' is generally used for forms that do a
query. In case of 'GET' the fields in the form will be encoded in
the URL (so you can for instance bookmark the URL). The 'header()'
method of the Form will use this as the 'method' attribute of the
HTML form.
Form enctype
The encoding type of the form. If no encoding type is selected,
the default for HTML will be used, which is
'application/x-www-form-urlencoded'. No enctype is therefore
usually just fine. For forms that allow the uploading of a file,
use 'multipart/form-data'. The 'header()' method of the Form will
use this as the 'enctype' attribute of the HTML form.
Upgrade
The 'Upgrade' button in this section is really not useful yet.
It's used internally to upgrade unreleased development versions of
Formulator to the current version. Perhaps this will become more
useful in the future.
Formulator Form - Test
Description
In this view, you can test your form. The fields in your form will
be rendered as a simple HTML form. The fields are grouped and come
in a certain order; this can be defined with the form's 'order'
tab.
You can fill in some values and click on the 'Test' button to
submit the form.
You will now see the test form again. If all of your input was
successfully validated by the fields in the form, you the message
'All fields were validated correctly' above your form.
If some of the fields could not be validated, you will instead see
the message 'Not all fields could be validated' appear, and a list
with the validation errors:
* 'field_id' indicates the field that gave this validation error.
* 'error_key' is the name of the error message; you can find it
on that field's 'messages' tab.
* 'error_text' is the actual text associated with that
'error_key'; you can modify it on the field's messages tab.
Formulator Form - XML
Description
In this view, you can see an XML serialization of the form. If you
are using FSForm along with FileSystemSite, you can use this XML
by putting it on the filesystem as a .form file. FileSystemSite
will then pick it up and reflect it into the ZODB. This way you
can develop and maintain Formulator Forms on the filesystem.
FileSystemSite can be found here:
http://www.zope.org/Members/k_vertigo/Products/FileSystemSite
To enable Formulator support for FileSystemSite, do a 'from
Products.Formulator import FSForm' somewhere in your own code (for
instance in your product's '__init__.py').
Formulator HOWTO
Introduction
This HOWTO is intended to give an introduction to the use of
Formulator from the Zope Management Interface and from DTML,
although much of this applies to use from ZPT or Python as
well. Note that Formulator comes with online help for each tab as
well as API help, so be sure to check that as well. This document
will only give an overview of the possibilities and not all the
details.
Formulator Scope
Formulator is a tool to create and validate web forms in Zope.
Formulator takes care of the rendering of the fields in the form,
as well as the validation and processing of the data that is
submitted. Formulator's scope is limited to forms: "do web forms,
web forms only, and web forms well." Formulator does currently not
even take care of the precise layout of a form -- each form is
layouted differently and thus layout is left to the developer.
Formulator does allow for easy integration with external systems,
however.
Creating a Formulator Form and Fields
It is easy to create a Formulator Form, just pick it from the add
list and add it to a folder. I usually only have one form in a
folder and call it 'form' to make automatic layout handling
easier; I'll say more about the reason for this later.
The default view of the form looks just like a folder, except that
the only things that are addable are Formulator Fields. When you
add a field to a form, it'll show up in the Form, just like an
object shows up in a normal Zope Folder.
Fields
When you click on a field, you see a list of its properties in the
field's 'Edit' screen. This is a good time to explain that
Formulator has an extensive help system, and that if you click on
'help' in the 'Edit' screen you'll see a list with a short
description of what each property does.
If you click on the 'Test' tab in the Field, you will see the
field displayed as it would appear in the form. If you fill in
some value in the field and click on the 'Test' button, you can
test its validation behavior. If everything could be validated and
processed all right, you'll see the resulting value. If it could
not be validated however, you see an error, showing the error_key
and error_text.
The best way to learn about what the different fields do and how
their properties work is to try them out. Just change some
properties and see what happens in the Test screen. And be sure to
look at the help.
Other Form tabs
The form 'Test' tab is not difficult to explain; it shows all the
fields you have added to the form. You can test the behavior of
the entire form here.
In the 'Order' part you can group fields and order them inside
their groups. The order determines the order in which they appear
on the 'Test' screen, and can also can be used in your own
code. Initially there is only a single 'Default' group, but you
can add new groups and change their names.
In the 'Settings' tab you can determine the form properties. You
can set the form submit action and method here, which you can
later use with the 'header()' and 'footer()' methods of the form.
Other Field tabs
The field 'Override' screen allows you to make the field call an
override method (most commonly a Python Script) for a property.
Instead of using the property value in the 'Edit' screen, the
method with the name listed in the override tab will be called to
retrieve a value then. The returned value must be the same as the
one that property's field generates; for an IntegerField this is
an integer, for instance. The titles of overridden fields will be
displayed between square brackets ('[ ]') in the 'Edit' screen.
In the 'Messages' screen you can set the text of the error
messages that field can generate upon validation errors.
On the examples in this HOWTO
All the examples in this HOWTO are contained in the file
'formulator_howto_examples.zexp', which you can download from the
Formulator product page
(http://www.zope.org/Members/faassen/formulator) and import into
your Zope. In the examples, all the forms are called 'form'.
Rendering a form manually with DTML ('manual' folder)
First, I will show how to use DTML to manually layout a form. This
takes the most work, but also allows the most flexibility. In all
these examples I will assume the form is called 'form'.
The form contains three fields; a StringField 'animal', a
StringField 'color', and an IntegerField 'number'. 'index_html' is
the DTML Method that does the manual layout::
<dtml-var standard_html_header>
<!-- show the header of the form, using 'Form action' and
'Form method' form settings (<form action="..." method="...">)
-->
<dtml-var "form.header()">
<!-- a simple table for layout purposes -->
<table border="0">
<!-- each field will be on a line by itself -->
<tr>
<!-- first display the title property of the animal field -->
<td><dtml-var "form.animal.get_value('title')"></td>
<!-- render the field -->
<td><dtml-var "form.animal.render()"></td>
</tr>
<!-- the same for the color field -->
<tr>
<td><dtml-var "form.color.get_value('title')"></td>
<td><dtml-var "form.color.render()"></td>
</tr>
<!-- and the number field -->
<tr>
<td><dtml-var "form.number.get_value('title')"></td>
<td><dtml-var "form.number.render()"></td>
</tr>
<!-- the submit button -->
<tr>
<td><input type="submit" value=" OK "></td>
</tr>
</table>
<!-- the form footer -->
<dtml-var "form.footer()">
<dtml-var standard_html_footer>
This shows a form with the three fields. You can easily rearrange
the layout just by changing the HTML.
Rendering a form automatically with DTML ('automatic' folder)
For many simple forms you don't need to do the layout yourself all
the time. We can use Formulator and acquisition to make layout a
lot easier. If we know each form is in a separate folder and is
called 'form', we can place DTML method in the root of the site
that can render any such form. In this example 'index_html' will
do the automated rendering directly. In real-world sites you'd
usually use another method (for instance called 'form_body') to
render because not all folders would contain forms. In that case
it'd be easier to put the form rendering code in another method
(for instance called 'form_body'), which you can then call from
your other code. Here's 'index_html'::
<dtml-var standard_html_header>
<!-- show the header of the form, using 'Form action' and
'Form method' form settings (<form action="..." method="...">)
-->
<dtml-var "form.header()">
<!-- a simple table for layout purposes -->
<table border="0">
<!-- get a list of all fields in the form -->
<dtml-in "form.get_fields()">
<!-- rename each sequence item to 'field' so they can
be used more easily -->
<dtml-let field=sequence-item>
<!-- each field will be on a line by itself -->
<tr>
<!-- display the title property of this field -->
<td><dtml-var "field.get_value('title')"></td>
<!-- render the field -->
<td><dtml-var "field.render()"></td>
</tr>
</dtml-let>
</dtml-in>
<!-- the submit button -->
<tr>
<td><input type="submit" value=" OK "></td>
</tr>
</table>
<!-- the form footer -->
<dtml-var "form.footer()">
<dtml-var standard_html_footer>
The nice thing about the automatic approach is that now you can
change the Formulator form as much as you like; this code will
always automatically display them. Even better, if you add
subfolders with forms in them, acquisition makes those forms
display automatically as well! If you have only simple forms on a
site, this could be the only DTML Method you need.
Form validation ('validation' folder)
I will use the same 'index_html' as in the automatic form
rendering example and the 'animal/color/number' form to
demonstrate form validation.
I've set the 'Form action' property of the form to 'feedback'.
When the form is submitted it, Zope will access the 'feedback'
DTML Method. The form data will be coming into 'feedback' in the
'REQUEST' object (more precisely the 'REQUEST.form' object).
The 'feedback' method should do a number of things:
* validate all fields (tell formulator to take care of this).
* handle any validation errors.
* if there were no validation errors, do something with the
form results.
Here's 'feedback', with comments::
<dtml-var standard_html_header>
<dtml-try>
<!-- try the validation, results should be put in
REQUEST (keyed under the field id) -->
<dtml-call "form.validate_all_to_request(REQUEST)">
<dtml-except FormValidationError>
<!-- if something went wrong with any field validation,
a FormValidationError will be raised, which we
will then catch here -->
<!-- we will display the errors here -->
<ul>
<dtml-in "error_value.errors">
<li>
<dtml-var "field.get_value('title')">:
<dtml-var error_text>
</li>
</dtml-in>
</ul>
<dtml-else>
<!-- if no FormValidationError was raised, we're done
with validation and our results will now be in
REQUEST (and in DTML namespace). -->
<!-- we could do anything with them, but we'll simply
display them -->
Hah, you are a <dtml-var color> <dtml-var animal> with
<dtml-var number> legs.
</dtml-try>
<dtml-var standard_html_footer>
Note that often you can use acquisition with the validation page
as well, so you can reuse most of its functionality.
Some mottos to inspire:
Formulator - Web forms, web forms well, and web forms only
Von Wiege bis zur Bahre, Formulare, Formulare! (with thanks to
Joachim Werner - it means "from crib to coffin, forms, forms!")
Formulator is an extensible framework that eases the creation and
validation of web forms.
Important links:
* Subscribe to the <a
href="http://lists.sourceforge.net/lists/listinfo/formulator-general">Formulator
mailing list</a>, for general discussions and questions on
Formulator usage.
* If you're interested in the further development of Formulator,
subscribe to the <a
href="http://lists.infrae.com/mailman/listinfo/formulator-dev">Formulator-dev
list</a>
* <a href="http://sourceforge.net/projects/formulator/">Formulator
SourceForge project page</a>
* <a href="http://cvs.infrae.com/Formulator/">Formulator CVS web</a>
* Check out Formulator from CVS anonymously like this::
cvs -z3 -d:pserver:anonymous@cvs.infrae.com:/cvs/infrae co Formulator
Important hint:
*Don't ever* use *field_&lt;fieldname&gt;*; anything
prefixed with *field_* in REQUEST is a Formulator
implementation detail. Instead, don't forget to validate the form,
for instance using *validate_all_to_request()*. See the
Formulator API help and Howto for more information. Forgetting to
validate the form is the most frequently made Formulator mistake
that I've encountered.
Documentation:
* <a href="formulator_howto">Formulator HOWTO</a>
* <a href="http://www.jquade.de./formulator-slides-de">Very nice slides about Formulator by Jens Quade (in German)</a>
* <a href="http://www.zopelabs.com/cookbook/1032909599">A Zopelabs recipe by Scott Burton on using Formulator with Zope Page Templates</a>
* <a href="http://www.zope.org/Members/beno/HowTo/HowTo/Formulator_With_ZPT">Howto on using Formulator with Zope Page Templates by Beno</a>
Some less important links:
* <a href="http://freshmeat.net/projects/formulator/">Formulator on Freshmeat</a>
* <a href="http://www.advogato.org/proj/Formulator/">Formulator project page on Advogato</a>
This directory now contains some unit tests for Formulator.
# this file is here to make 'tests' a module of its own
import unittest, re
import Zope
# XXX this does not work for zope2.x if x < 3
# can we fake this? should we do this?
from Testing import makerequest
from Products.Formulator.Form import ZMIForm
from Products.Formulator.Errors import ValidationError, FormValidationError
from Products.Formulator.MethodField import Method
from Products.Formulator.TALESField import TALESMethod
from Products.PythonScripts.PythonScript import PythonScript
""" random assembly testing some reported bugs.
This is _not_ a structured or even complete test suite
"""
class FormTestCase(unittest.TestCase):
def setUp(self):
get_transaction().begin()
self.connection = Zope.DB.open()
self.root = makerequest.makerequest(
self.connection.root()['Application'])
self.root.manage_addProduct['Formulator'] \
.manage_add('form', 'Test Form')
self.form = self.root.form
def tearDown(self):
get_transaction().abort()
self.connection.close()
def test_has_field(self):
""" test if has_field works, if one asks for a non-field attribute.
this has raised AttributeError "aq_explicit" in previous versions
"""
self.failIf(self.form.has_field('title'))
def _test_list_values(self):
""" test if a list of values returned by TALES (override) expressions
is interpreted properly.
If a TALES tab returns a sequence of items and some item is
actually a string of length 2 (e.g. "ok"), this previously
has lead to a item text of 'o' and a display value of 'k'
(as the this is actually a sequence of length 2 ...)
See http://sourceforge.net/mailarchive/forum.php?thread_id=1359918&forum_id=1702
Actually the original problem still does not work,
as passing a list of int's is not yet supported.
If it should, please uncomment the second part of the test.
"""
# XXX deactivated: this maybe should not be fixed at all
self.form.manage_addField('list_field', 'Test List Field', 'ListField')
# adding a python script to be called by the override tab
# FIXME: the following does not work, as the fake-request
# does not have a "form" atribute (?)
#self.root.manage_addProduct['PythonScripts'] \
# .manage_addPythonScript('override_test', 'Test for override')
#
#self.root._getOb('override_test').write("return ['ok', 'no']\n")
self.form.override_test = PythonScript('override_test')
self.form.override_test.write("return ['ok', 'no']\n")
# ps._makeFunction()
list_field = getattr(self.form, 'list_field')
list_field.values['items'] = [ ('ok', 'ok'), ('no', 'no') ]
items1 = list_field.render()
# test TALES
list_field.tales['items'] = TALESMethod("python:['ok', 'no']")
items2 = list_field.render()
self.assertEquals(items1, items2)
# test override
del list_field.tales['items']
list_field.overrides['items'] = Method('override_test')
items2 = list_field.render()
self.assertEquals(items1, items2)
# test if TALES returns a list of e.g. int
#list_field.values['items'] = [ ('42', '42'), ('88', '88') ]
#
#items1 = list_field.render()
#
#list_field.tales['items'] = TALESMethod("python:[42, 88]")
#items2 = list_field.render()
#
#self.assertEquals(items1, items2)
def test_labels(self):
self.form.manage_addField(
'label_field', 'Test Label Field', 'LabelField')
self.form.label_field.overrides['default'] = "Some label"
self.form.manage_addField(
'int_field', 'Test integer field', 'IntegerField')
result = self.form.validate_all(
{'field_int_field': '3'})
self.assertEquals({'int_field': 3}, result)
def test_datetime_css_class_rendering(self):
# test that a bug is fixed, which causing the css_class value
# not to be rendered
self.form.manage_addProduct['Formulator']\
.manage_addField('date_time','Test Field','DateTimeField')
field = self.form.date_time
css_matcher = re.compile('class="([^"]*)"')
# initially no css class is set
self.assertEquals(0, len(css_matcher.findall(field.render())))
# edit the field, bypassing validation ...
field._edit({'css_class':'some_class'})
# now we should have five matches for the five subfields ...
css_matches = css_matcher.findall(field.render())
self.assertEquals(5, len(css_matches))
# ... and all have the given value:
for m in css_matches:
self.assertEquals('some_class',m)
# change the input style: the css needs to be
# propagated to the newly created subfields
current_style = field['input_style']
other_style = {'list':'text', 'text':'list'} [current_style]
field._edit({'input_style':other_style})
# still the css classes should remain the same
css_matches = css_matcher.findall(field.render())
self.assertEquals(5, len(css_matches))
for m in css_matches:
self.assertEquals('some_class',m)
# now just change to another value:
field._edit({'css_class':'other_class'})
css_matches = css_matcher.findall(field.render())
self.assertEquals(5, len(css_matches))
for m in css_matches:
self.assertEquals('other_class',m)
# and clear the css_class field:
field._edit({'css_class':''})
css_matches = css_matcher.findall(field.render())
self.assertEquals(0, len(css_matches))
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(FormTestCase, 'test'))
return suite
def main():
unittest.TextTestRunner().run(test_suite())
if __name__ == '__main__':
main()
# Copyright (c) 2002 Infrae. All rights reserved.
# See also LICENSE.txt
# $Revision: 1.2 $
import unittest
import Zope
try:
from Zope import startup
startup()
except ImportError:
# startup is only in Zope2.6
pass
from Products.Formulator.tests import test_Form, test_validators, test_serialize
def test_suite():
suite = unittest.TestSuite()
suite.addTest(test_Form.test_suite())
suite.addTest(test_validators.test_suite())
suite.addTest(test_serialize.test_suite())
return suite
def main():
unittest.TextTestRunner(verbosity=1).run(test_suite())
if __name__ == '__main__':
main()
import unittest
import Zope
from Products.Formulator.Form import ZMIForm
from Products.Formulator.XMLToForm import XMLToForm
from Products.Formulator.FormToXML import formToXML
from Products.Formulator.Errors import ValidationError, FormValidationError
class FakeRequest:
""" a fake request for testing.
Actually we need this only for item acces,
and for evaluating to false always, for
the manage_XXX methods to not try to render
a response.
"""
def __init__(self):
self.dict = {}
def __getitem__(self, key):
return self.dict[key]
def __setitem__(self, key, value):
self.dict[key] = value
def get(self, key, default_value):
return self.dict.get(key, default_value)
def update(self, other_dict):
self.dict.update(other_dict)
def clear(self):
self.dict.clear()
def __nonzero__(self):
return 0
class SerializeTestCase(unittest.TestCase):
def test_simpleSerialize(self):
form = ZMIForm('test', 'My test')
xml = '''\
<?xml version="1.0" encoding="iso-8859-1" ?>
<form>
<title></title>
<name>tab_status_form</name>
<action></action>
<enctype></enctype>
<method></method>
<groups>
<group>
<title>Default</title>
<fields>
<field><id>message</id> <type>RawTextAreaField</type>
<values>
<alternate_name></alternate_name>
<hidden type="int">0</hidden>
<max_length></max_length>
<width type="int">65</width>
<external_validator></external_validator>
<height type="int">7</height>
<required type="int">0</required>
<css_class></css_class>
<default></default>
<title>Message</title>
<truncate type="int">0</truncate>
<description></description>
<extra>wrap="soft"</extra>
</values>
<tales>
</tales>
</field>
<field><id>publish_datetime</id> <type>DateTimeField</type>
<values>
<date_only type="int">0</date_only>
<alternate_name></alternate_name>
<input_style>list</input_style>
<hidden type="int">0</hidden>
<input_order>dmy</input_order>
<time_separator>:</time_separator>
<date_separator>/</date_separator>
<external_validator></external_validator>
<required type="int">0</required>
<default_now type="int">0</default_now>
<css_class></css_class>
<title>Publish time</title>
<description></description>
</values>
<tales>
<time_separator>python:form.time_punctuation</time_separator>
<date_separator>python:form.date_punctuation</date_separator>
</tales>
</field>
<field><id>expiration_datetime</id> <type>DateTimeField</type>
<values>
<date_only type="int">0</date_only>
<alternate_name></alternate_name>
<input_style>list</input_style>
<css_class></css_class>
<hidden type="int">0</hidden>
<input_order>dmy</input_order>
<time_separator>:</time_separator>
<date_separator>/</date_separator>
<external_validator></external_validator>
<required type="int">0</required>
<default_now type="int">0</default_now>
<title>Expiration time</title>
<description>If this document should expire, set the time.</description>
</values>
<tales>
<time_separator>python:form.time_punctuation</time_separator>
<date_separator>python:form.date_punctuation</date_separator>
</tales>
</field>
<field><id>expires_flag</id> <type>CheckBoxField</type>
<values>
<alternate_name></alternate_name>
<hidden type="int">0</hidden>
<css_class></css_class>
<default type="int">0</default>
<title>Expire flag</title>
<description>Turn on expiration time?</description>
<external_validator></external_validator>
<extra></extra>
</values>
<tales>
</tales>
</field>
</fields>
</group>
</groups>
</form>'''
XMLToForm(xml, form)
s = formToXML(form)
f = open('output1.txt', 'w')
f.write(s)
f.close()
form2 = ZMIForm('another', 'Something')
XMLToForm(xml, form2)
f = open('output2.txt', 'w')
f.write(formToXML(form2))
f.close()
def test_escaping(self):
""" test if the necessary elements are escaped in the XML.
(Actually this test is very incomplete)
"""
form = ZMIForm('test', '<EncodingTest>')
# XXX don't test escaping of name, as needs to be javascript
# valid anyway?
form.name = 'name'
form.add_group('a & b')
form.manage_addField('string_field', '<string> Field', 'StringField')
form.manage_addField('int_field', '<int> Field', 'IntegerField')
form.manage_addField('float_field', '<Float> Field', 'FloatField')
form.manage_addField('date_field', '<Date> Field', 'DateTimeField')
form.manage_addField('list_field', '<List> Field', 'ListField')
form.manage_addField('multi_field', '<Checkbox> Field', 'MultiCheckBoxField')
form2 = ZMIForm('test2', 'ValueTest')
xml = formToXML(form)
XMLToForm(xml, form2)
for field in form.get_fields():
self.assert_(form2.has_field(field.getId()))
field2 = getattr(form2, field.getId())
# XXX test if values are the same
self.assertEquals(field.values, field2.values)
# test if default renderings are the same
self.assertEquals(field.render(), field2.render())
self.assertEquals(form.title, form2.title)
self.assertEquals(form.name, form2.name)
self.assertEquals(form.action, form2.action)
self.assertEquals(form.enctype, form2.enctype)
self.assertEquals(form.method, form2.method)
# if we have forgotten something, this will usually remind us ;-)
self.assertEquals(form.render(), form2.render())
def test_messages(self):
""" test if the error messages are exported
"""
form = ZMIForm('test', '<EncodingTest>')
form.manage_addField('int_field', 'int Field', 'IntegerField')
form2 = ZMIForm('test2', 'ValueTest')
request = FakeRequest()
for message_key in form.int_field.get_error_names():
request[message_key] = 'test message for error key <%s>' % message_key
form.int_field.manage_messages(REQUEST=request)
xml = formToXML(form)
XMLToForm(xml, form2)
# print xml
request.clear()
request['field_int_field'] = 'not a number'
try:
form.validate_all(request)
self.fail('form should fail in validation')
except FormValidationError, e:
self.assertEquals(1, len(e.errors))
text1 = e.errors[0].error_text
try:
form2.validate_all(request)
self.fail('form2 should fail in validation')
except FormValidationError, e:
self.assertEquals(1, len(e.errors))
text2 = e.errors[0].error_text
self.assertEquals(text1, text2)
def test_fieldValueTypes(self):
""" test checking if the field values are of the proper type.
after reading from XML some field values may not have the right type,
if they have a special type (currently int and "list").
Also tests if rendering and validation are the same
between the original form and the one after one form -> xml -> form
roundtrip.
"""
form = ZMIForm('test', 'ValueTest')
form.manage_addField('int_field', 'Test Integer Field', 'IntegerField')
form.manage_addField('float_field', 'Test Float Field', 'FloatField')
form.manage_addField('date_field', 'Test Date Field', 'DateTimeField')
form.manage_addField('list_field', 'Test List Field', 'ListField')
form.manage_addField('multi_field', 'Test Checkbox Field', 'MultiCheckBoxField')
form.manage_addField('link_field', 'Test Link Field', 'LinkField')
form.manage_addField('empty_field', 'Test Empty Field', 'StringField')
int_field = getattr(form, 'int_field')
float_field = getattr(form, 'float_field')
date_field = getattr(form, 'date_field')
list_field = getattr(form, 'list_field')
multi_field = getattr(form, 'multi_field')
link_field = getattr(form, 'link_field')
empty_field = getattr(form, 'empty_field')
# XXX editing fields by messing with a fake request
# -- any better way to do this?
default_values = {'field_title': 'Test Title',
'field_display_width': '92',
'field_required':'checked',
'field_enabled':'checked',
}
try:
request = FakeRequest()
request.update(default_values)
request.update( {'field_default':'42',
'field_enabled':'checked'})
int_field.manage_edit(REQUEST=request)
request.clear()
request.update(default_values)
request.update( {'field_default':'1.7'})
float_field.manage_edit(REQUEST=request)
# XXX cannot test "defaults to now", as this may fail randomly
request.clear()
request.update(default_values)
request.update( {'field_input_style':'list',
'field_input_order':'mdy',
'field_date_only':'',
'field_css_class':'test_css',
'field_time_separator':'$'})
date_field.manage_edit(REQUEST=request)
request.clear()
request.update(default_values)
request.update( {'field_default':'foo',
'field_size':'1',
'field_items':'Foo | foo\n Bar | bar'})
list_field.manage_edit(REQUEST=request)
request.clear()
request.update(default_values)
request.update( {'field_default':'foo',
'field_size':'3',
'field_items':'Foo | foo\n Bar | bar\nBaz | baz',
'field_orientation':'horizontal',
'field_view_separator':'<br />\n',
})
multi_field.manage_edit(REQUEST=request)
request.clear()
request.update(default_values)
request.update( {'field_default':'http://www.absurd.org',
'field_required':'1',
'field_check_timeout':'5.0',
'field_link_type':'external',
})
link_field.manage_edit(REQUEST=request)
request.clear()
request.update(default_values)
request.update( {'field_default':'None',
'field_required':'',
})
empty_field.manage_edit(REQUEST=request)
except ValidationError, e:
self.fail('error when editing field %s; error message: %s' %
(e.field_id, e.error_text) )
form2 = ZMIForm('test2', 'ValueTest')
xml = formToXML(form)
XMLToForm(xml, form2)
for field in form.get_fields():
self.assert_(form2.has_field(field.getId()))
field2 = getattr(form2, field.getId())
# XXX test if values are the same
self.assertEquals(field.values, field2.values)
# test if default renderings are the same
self.assertEquals(field.render(), field2.render())
# brute force compare ...
self.assertEquals(form.render(), form2.render())
request.clear()
request['field_int_field'] = '42'
request['field_float_field'] = '2.71828'
request['subfield_date_field_month'] = '11'
request['subfield_date_field_day'] = '11'
request['subfield_date_field_year'] = '2011'
request['subfield_date_field_hour'] = '09'
request['subfield_date_field_minute'] = '59'
request['field_list_field'] = 'bar'
request['field_multi_field'] = ['bar', 'baz']
request['field_link_field'] = 'http://www.zope.org'
try:
result1 = form.validate_all(request)
except FormValidationError, e:
# XXX only render first error ...
self.fail('error when editing form1, field %s; error message: %s' %
(e.errors[0].field_id, e.errors[0].error_text) )
try:
result2 = form2.validate_all(request)
except FormValidationError, e:
# XXX only render first error ...
self.fail('error when editing form1, field %s; error message: %s' %
(e.errors[0].field_id, e.errors[0].error_text) )
self.assertEquals(result1, result2)
self.assertEquals(42, result2['int_field'])
self.assertEquals(2.71828, result2['float_field'])
# check link field timeout value
self.assertEquals(link_field.get_value('check_timeout'),
form2.link_field.get_value('check_timeout'))
# XXX not tested: equal form validation failure on invalid input
def test_emptyGroup(self):
""" test bugfix: empty groups are allowed in the XMLForm """
form = ZMIForm('test', 'GroupTest')
form.add_group('empty')
form2 = ZMIForm('test2', 'GroupTestCopy')
xml = formToXML(form)
XMLToForm(xml, form2)
# print xml
# XXX actually the empty group is not rendered anyway, but
# if we get here, we are behind the bug anyway ...
self.assertEquals(form.render(), form2.render())
self.assertEquals(form.get_groups(), form2.get_groups())
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(SerializeTestCase, 'test'))
return suite
def main():
unittest.TextTestRunner().run(test_suite())
if __name__ == '__main__':
main()
import unittest
import ZODB
import OFS.Application
from Products.Formulator import Validator
from Products.Formulator.StandardFields import DateTimeField
class TestField:
def __init__(self, id, **kw):
self.id = id
self.kw = kw
def get_value(self, name):
# XXX hack
return self.kw.get(name, 0)
def get_error_message(self, key):
return "nothing"
def get_form_encoding(self):
# XXX fake ... what if installed python does not support utf-8?
return "utf-8"
class ValidatorTestCase(unittest.TestCase):
def assertValidatorRaises(self, exception, error_key, f, *args, **kw):
try:
apply(f, args, kw)
except Validator.ValidationError, e:
if e.error_key != error_key:
self.fail('Got wrong error. Expected %s received %s' %
(error_key, e))
else:
return
self.fail('Expected error %s but no error received.' % error_key)
class StringValidatorTestCase(ValidatorTestCase):
def setUp(self):
self.v = Validator.StringValidatorInstance
def tearDown(self):
pass
def test_basic(self):
result = self.v.validate(
TestField('f', max_length=0, truncate=0, required=0, unicode=0),
'f', {'f' : 'foo'})
self.assertEqual('foo', result)
def test_htmlquotes(self):
result = self.v.validate(
TestField('f', max_length=0, truncate=0, required=0, unicode=0),
'f', {'f' : '<html>'})
self.assertEqual('<html>', result)
def test_encoding(self):
utf8_string = 'M\303\274ller' # this is a M&uuml;ller
unicode_string = unicode(utf8_string, 'utf-8')
result = self.v.validate(
TestField('f', max_length=0, truncate=0, required=0, unicode=1),
'f', {'f' : utf8_string})
self.assertEqual(unicode_string, result)
def test_strip_whitespace(self):
result = self.v.validate(
TestField('f', max_length=0, truncate=0, required=0, unicode=0),
'f', {'f' : ' foo '})
self.assertEqual('foo', result)
def test_error_too_long(self):
self.assertValidatorRaises(
Validator.ValidationError, 'too_long',
self.v.validate,
TestField('f', max_length=10, truncate=0, required=0, unicode=0),
'f', {'f' : 'this is way too long'})
def test_error_truncate(self):
result = self.v.validate(
TestField('f', max_length=10, truncate=1, required=0, unicode=0),
'f', {'f' : 'this is way too long'})
self.assertEqual('this is way too long'[:10], result)
def test_error_required_not_found(self):
# empty string
self.assertValidatorRaises(
Validator.ValidationError, 'required_not_found',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1, unicode=0),
'f', {'f': ''})
# whitespace only
self.assertValidatorRaises(
Validator.ValidationError, 'required_not_found',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1, unicode=0),
'f', {'f': ' '})
# not in dict
self.assertValidatorRaises(
Validator.ValidationError, 'required_not_found',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1, unicode=0),
'f', {})
def test_whitespace_preserve(self):
result = self.v.validate(
TestField('f', max_length=0, truncate=0, required=0, unicode=0,
whitespace_preserve=1),
'f', {'f' : ' '})
self.assertEqual(' ', result)
result = self.v.validate(
TestField('f', max_length=0, truncate=0, required=0, unicode=0,
whitespace_preserve=0),
'f', {'f' : ' '})
self.assertEqual('', result)
result = self.v.validate(
TestField('f', max_length=0, truncate=0, required=0, unicode=0,
whitespace_preserve=1),
'f', {'f' : ' foo '})
self.assertEqual(' foo ', result)
class EmailValidatorTestCase(ValidatorTestCase):
def setUp(self):
self.v = Validator.EmailValidatorInstance
def test_basic(self):
result = self.v.validate(
TestField('f', max_length=0, truncate=0, required=1, unicode=0),
'f', {'f': 'foo@bar.com'})
self.assertEquals('foo@bar.com', result)
result = self.v.validate(
TestField('f', max_length=0, truncate=0, required=1, unicode=0),
'f', {'f': 'm.faassen@vet.uu.nl'})
self.assertEquals('m.faassen@vet.uu.nl', result)
def test_error_not_email(self):
# a few wrong email addresses should raise error
self.assertValidatorRaises(
Validator.ValidationError, 'not_email',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1, unicode=0),
'f', {'f': 'foo@bar.com.'})
self.assertValidatorRaises(
Validator.ValidationError, 'not_email',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1, unicode=0),
'f', {'f': '@bar.com'})
def test_error_required_not_found(self):
# empty string
self.assertValidatorRaises(
Validator.ValidationError, 'required_not_found',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1, unicode=0),
'f', {'f': ''})
# skip PatternValidator for now
class BooleanValidatorTestCase(ValidatorTestCase):
def setUp(self):
self.v = Validator.BooleanValidatorInstance
def tearDown(self):
pass
def test_basic(self):
result = self.v.validate(
TestField('f'),
'f', {'f': ''})
self.assertEquals(0, result)
result = self.v.validate(
TestField('f'),
'f', {'f': 1})
self.assertEquals(1, result)
result = self.v.validate(
TestField('f'),
'f', {'f': 0})
self.assertEquals(0, result)
result = self.v.validate(
TestField('f'),
'f', {})
self.assertEquals(0, result)
class IntegerValidatorTestCase(ValidatorTestCase):
def setUp(self):
self.v = Validator.IntegerValidatorInstance
def test_basic(self):
result = self.v.validate(
TestField('f', max_length=0, truncate=0,
required=0, start="", end=""),
'f', {'f': '15'})
self.assertEquals(15, result)
result = self.v.validate(
TestField('f', max_length=0, truncate=0,
required=0, start="", end=""),
'f', {'f': '0'})
self.assertEquals(0, result)
result = self.v.validate(
TestField('f', max_length=0, truncate=0,
required=0, start="", end=""),
'f', {'f': '-1'})
self.assertEquals(-1, result)
def test_no_entry(self):
# result should be empty string if nothing entered
result = self.v.validate(
TestField('f', max_length=0, truncate=0,
required=0, start="", end=""),
'f', {'f': ''})
self.assertEquals("", result)
def test_ranges(self):
# first check whether everything that should be in range is
# in range
for i in range(0, 100):
result = self.v.validate(
TestField('f', max_length=0, truncate=0, required=1,
start=0, end=100),
'f', {'f': str(i)})
self.assertEquals(i, result)
# now check out of range errors
self.assertValidatorRaises(
Validator.ValidationError, 'integer_out_of_range',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start=0, end=100),
'f', {'f': '100'})
self.assertValidatorRaises(
Validator.ValidationError, 'integer_out_of_range',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start=0, end=100),
'f', {'f': '200'})
self.assertValidatorRaises(
Validator.ValidationError, 'integer_out_of_range',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start=0, end=100),
'f', {'f': '-10'})
# check some weird ranges
self.assertValidatorRaises(
Validator.ValidationError, 'integer_out_of_range',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start=10, end=10),
'f', {'f': '10'})
self.assertValidatorRaises(
Validator.ValidationError, 'integer_out_of_range',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start=0, end=0),
'f', {'f': '0'})
self.assertValidatorRaises(
Validator.ValidationError, 'integer_out_of_range',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start=0, end=-10),
'f', {'f': '-1'})
def test_error_not_integer(self):
self.assertValidatorRaises(
Validator.ValidationError, 'not_integer',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start="", end=""),
'f', {'f': 'foo'})
self.assertValidatorRaises(
Validator.ValidationError, 'not_integer',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start="", end=""),
'f', {'f': '1.0'})
self.assertValidatorRaises(
Validator.ValidationError, 'not_integer',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start="", end=""),
'f', {'f': '1e'})
def test_error_required_not_found(self):
# empty string
self.assertValidatorRaises(
Validator.ValidationError, 'required_not_found',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start="", end=""),
'f', {'f': ''})
# whitespace only
self.assertValidatorRaises(
Validator.ValidationError, 'required_not_found',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start="", end=""),
'f', {'f': ' '})
# not in dict
self.assertValidatorRaises(
Validator.ValidationError, 'required_not_found',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1,
start="", end=""),
'f', {})
class FloatValidatorTestCase(ValidatorTestCase):
def setUp(self):
self.v = Validator.FloatValidatorInstance
def test_basic(self):
result = self.v.validate(
TestField('f', max_length=0, truncate=0,
required=0),
'f', {'f': '15.5'})
self.assertEqual(15.5, result)
result = self.v.validate(
TestField('f', max_length=0, truncate=0,
required=0),
'f', {'f': '15.0'})
self.assertEqual(15.0, result)
result = self.v.validate(
TestField('f', max_length=0, truncate=0,
required=0),
'f', {'f': '15'})
self.assertEqual(15.0, result)
def test_error_not_float(self):
self.assertValidatorRaises(
Validator.ValidationError, 'not_float',
self.v.validate,
TestField('f', max_length=0, truncate=0, required=1),
'f', {'f': '1f'})
class DateTimeValidatorTestCase(ValidatorTestCase):
def setUp(self):
self.v = Validator.DateTimeValidatorInstance
def test_normal(self):
result = self.v.validate(
DateTimeField('f'),
'f', {'subfield_f_year': '2002',
'subfield_f_month': '12',
'subfield_f_day': '1',
'subfield_f_hour': '10',
'subfield_f_minute': '30'})
self.assertEquals(2002, result.year())
self.assertEquals(12, result.month())
self.assertEquals(1, result.day())
self.assertEquals(10, result.hour())
self.assertEquals(30, result.minute())
def test_ampm(self):
result = self.v.validate(
DateTimeField('f', ampm_time_style=1),
'f', {'subfield_f_year': '2002',
'subfield_f_month': '12',
'subfield_f_day': '1',
'subfield_f_hour': '10',
'subfield_f_minute': '30',
'subfield_f_ampm': 'am'})
self.assertEquals(2002, result.year())
self.assertEquals(12, result.month())
self.assertEquals(1, result.day())
self.assertEquals(10, result.hour())
self.assertEquals(30, result.minute())
result = self.v.validate(
DateTimeField('f', ampm_time_style=1),
'f', {'subfield_f_year': '2002',
'subfield_f_month': '12',
'subfield_f_day': '1',
'subfield_f_hour': '10',
'subfield_f_minute': '30',
'subfield_f_ampm': 'pm'})
self.assertEquals(2002, result.year())
self.assertEquals(12, result.month())
self.assertEquals(1, result.day())
self.assertEquals(22, result.hour())
self.assertEquals(30, result.minute())
self.assertValidatorRaises(
Validator.ValidationError, 'not_datetime',
self.v.validate,
DateTimeField('f', ampm_time_style=1),
'f', {'subfield_f_year': '2002',
'subfield_f_month': '12',
'subfield_f_day': '1',
'subfield_f_hour': '10',
'subfield_f_minute': '30'})
def test_date_only(self):
result = self.v.validate(
DateTimeField('f', date_only=1),
'f', {'subfield_f_year': '2002',
'subfield_f_month': '12',
'subfield_f_day': '1'})
self.assertEquals(2002, result.year())
self.assertEquals(12, result.month())
self.assertEquals(1, result.day())
self.assertEquals(0, result.hour())
self.assertEquals(0, result.minute())
result = self.v.validate(
DateTimeField('f', date_only=1),
'f', {'subfield_f_year': '2002',
'subfield_f_month': '12',
'subfield_f_day': '1',
'subfield_f_hour': '10',
'subfield_f_minute': '30'})
self.assertEquals(2002, result.year())
self.assertEquals(12, result.month())
self.assertEquals(1, result.day())
self.assertEquals(0, result.hour())
self.assertEquals(0, result.minute())
def test_allow_empty_time(self):
result = self.v.validate(
DateTimeField('f', allow_empty_time=1),
'f', {'subfield_f_year': '2002',
'subfield_f_month': '12',
'subfield_f_day': '1'})
self.assertEquals(2002, result.year())
self.assertEquals(12, result.month())
self.assertEquals(1, result.day())
self.assertEquals(0, result.hour())
self.assertEquals(0, result.minute())
result = self.v.validate(
DateTimeField('f', allow_empty_time=1),
'f', {'subfield_f_year': '2002',
'subfield_f_month': '12',
'subfield_f_day': '1',
'subfield_f_hour': '10',
'subfield_f_minute': '30'})
self.assertEquals(2002, result.year())
self.assertEquals(12, result.month())
self.assertEquals(1, result.day())
self.assertEquals(10, result.hour())
self.assertEquals(30, result.minute())
def test_allow_empty_time2(self):
result = self.v.validate(
DateTimeField('f', allow_empty_time=1, required=0), 'f', {})
self.assertEquals(None, result)
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(StringValidatorTestCase, 'test'))
suite.addTest(unittest.makeSuite(EmailValidatorTestCase, 'test'))
suite.addTest(unittest.makeSuite(BooleanValidatorTestCase, 'test'))
suite.addTest(unittest.makeSuite(IntegerValidatorTestCase, 'test'))
suite.addTest(unittest.makeSuite(FloatValidatorTestCase, 'test'))
suite.addTest(unittest.makeSuite(DateTimeValidatorTestCase, 'test'))
return suite
def main():
unittest.TextTestRunner().run(test_suite())
if __name__ == '__main__':
main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment