diff --git a/product/Formulator/CREDITS.txt b/product/Formulator/CREDITS.txt new file mode 100644 index 0000000000000000000000000000000000000000..cdfa5d1c7cb3cf7fee16090f52e9555adfed40a8 --- /dev/null +++ b/product/Formulator/CREDITS.txt @@ -0,0 +1,77 @@ +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. + + Sébastien 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! + + + + diff --git a/product/Formulator/DummyField.py b/product/Formulator/DummyField.py new file mode 100644 index 0000000000000000000000000000000000000000..2e01259693c60332df73eadd221b6d1873957b5e --- /dev/null +++ b/product/Formulator/DummyField.py @@ -0,0 +1,38 @@ +""" +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) + + diff --git a/product/Formulator/Errors.py b/product/Formulator/Errors.py new file mode 100644 index 0000000000000000000000000000000000000000..8ad8a15e1abb37536921299e4bb20025eb938681 --- /dev/null +++ b/product/Formulator/Errors.py @@ -0,0 +1,36 @@ +"""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) diff --git a/product/Formulator/FSForm.py b/product/Formulator/FSForm.py new file mode 100644 index 0000000000000000000000000000000000000000..8c9358d8a21286703f4f9f6b20504b5f200b9ae0 --- /dev/null +++ b/product/Formulator/FSForm.py @@ -0,0 +1,114 @@ +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) diff --git a/product/Formulator/Field.py b/product/Formulator/Field.py new file mode 100644 index 0000000000000000000000000000000000000000..e93af811ebd4f3a9ea8acd86f97392ff4aaae6d0 --- /dev/null +++ b/product/Formulator/Field.py @@ -0,0 +1,633 @@ +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 + + + diff --git a/product/Formulator/FieldHelpTopic.py b/product/Formulator/FieldHelpTopic.py new file mode 100644 index 0000000000000000000000000000000000000000..77a47ecf613f991e556155b2bdc95a06a3642161 --- /dev/null +++ b/product/Formulator/FieldHelpTopic.py @@ -0,0 +1,35 @@ +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) + diff --git a/product/Formulator/FieldRegistry.py b/product/Formulator/FieldRegistry.py new file mode 100644 index 0000000000000000000000000000000000000000..e29e6139ab8536fa3790b53c4b518630c02dd754 --- /dev/null +++ b/product/Formulator/FieldRegistry.py @@ -0,0 +1,142 @@ +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)) + diff --git a/product/Formulator/Form.py b/product/Formulator/Form.py new file mode 100644 index 0000000000000000000000000000000000000000..e50a3f590a4749b49b7f1c66eeb1c4cb66613e67 --- /dev/null +++ b/product/Formulator/Form.py @@ -0,0 +1,1046 @@ +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() + + + + + + + diff --git a/product/Formulator/FormToXML.py b/product/Formulator/FormToXML.py new file mode 100644 index 0000000000000000000000000000000000000000..cae794308240e616f88d88dbf3800e92ad1b451a --- /dev/null +++ b/product/Formulator/FormToXML.py @@ -0,0 +1,85 @@ +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') diff --git a/product/Formulator/HISTORY.txt b/product/Formulator/HISTORY.txt new file mode 100644 index 0000000000000000000000000000000000000000..cd9803eb1612557c2cde23674e9c3d96fc2724c0 --- /dev/null +++ b/product/Formulator/HISTORY.txt @@ -0,0 +1,429 @@ +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. + + + diff --git a/product/Formulator/HelperFields.py b/product/Formulator/HelperFields.py new file mode 100644 index 0000000000000000000000000000000000000000..02d7d6b3edbec325f63abe3f8772e65d4a264394 --- /dev/null +++ b/product/Formulator/HelperFields.py @@ -0,0 +1,6 @@ +# include some helper fields which are in their own files +from MethodField import MethodField +from ListTextAreaField import ListTextAreaField +from TALESField import TALESField + + diff --git a/product/Formulator/INSTALL.txt b/product/Formulator/INSTALL.txt new file mode 100644 index 0000000000000000000000000000000000000000..b702911dd89b088f154699305cd896abe563e894 --- /dev/null +++ b/product/Formulator/INSTALL.txt @@ -0,0 +1,139 @@ +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. + + + diff --git a/product/Formulator/LICENSE.txt b/product/Formulator/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae1ac136dd720e5d39639c51e633b5f32e3ca256 --- /dev/null +++ b/product/Formulator/LICENSE.txt @@ -0,0 +1,29 @@ +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. diff --git a/product/Formulator/ListTextAreaField.py b/product/Formulator/ListTextAreaField.py new file mode 100644 index 0000000000000000000000000000000000000000..d7599d8ecc702395e45a6e51eeb4bf5e5f411f10 --- /dev/null +++ b/product/Formulator/ListTextAreaField.py @@ -0,0 +1,56 @@ +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 + + + + diff --git a/product/Formulator/MethodField.py b/product/Formulator/MethodField.py new file mode 100644 index 0000000000000000000000000000000000000000..9d30f2f038d96b1ae511910fa9136fcccff7664f --- /dev/null +++ b/product/Formulator/MethodField.py @@ -0,0 +1,75 @@ +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 + + diff --git a/product/Formulator/PatternChecker.py b/product/Formulator/PatternChecker.py new file mode 100644 index 0000000000000000000000000000000000000000..3bc139b6c56158a6a1f025271fc0a31a86b22d51 --- /dev/null +++ b/product/Formulator/PatternChecker.py @@ -0,0 +1,151 @@ +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') + + + + + + + + + diff --git a/product/Formulator/ProductForm.py b/product/Formulator/ProductForm.py new file mode 100644 index 0000000000000000000000000000000000000000..1de030c0c29c1141abac0d069db933f39f15e766 --- /dev/null +++ b/product/Formulator/ProductForm.py @@ -0,0 +1,138 @@ +""" +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) diff --git a/product/Formulator/README.txt b/product/Formulator/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..280465f60821132ce4121044faf5d12fc460c54b --- /dev/null +++ b/product/Formulator/README.txt @@ -0,0 +1,43 @@ +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. :) diff --git a/product/Formulator/StandardFields.py b/product/Formulator/StandardFields.py new file mode 100644 index 0000000000000000000000000000000000000000..c78af20f3df61306acaa1ab87a97b9dfc1a84294 --- /dev/null +++ b/product/Formulator/StandardFields.py @@ -0,0 +1,311 @@ +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 + diff --git a/product/Formulator/TALESField.py b/product/Formulator/TALESField.py new file mode 100644 index 0000000000000000000000000000000000000000..186737a93a873dd2345eef040ef8ae52edf34950 --- /dev/null +++ b/product/Formulator/TALESField.py @@ -0,0 +1,98 @@ +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 + + diff --git a/product/Formulator/TODO.txt b/product/Formulator/TODO.txt new file mode 100644 index 0000000000000000000000000000000000000000..77af1679d8ca67eee233066909a639240dddf41f --- /dev/null +++ b/product/Formulator/TODO.txt @@ -0,0 +1,20 @@ +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. + diff --git a/product/Formulator/Validator.py b/product/Formulator/Validator.py new file mode 100644 index 0000000000000000000000000000000000000000..66b2e1f0ec55d0a674899e473aacdcf3993a9dc9 --- /dev/null +++ b/product/Formulator/Validator.py @@ -0,0 +1,791 @@ +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() diff --git a/product/Formulator/Widget.py b/product/Formulator/Widget.py new file mode 100644 index 0000000000000000000000000000000000000000..dc6d43428b4c8f055f2e1c37c98b1be734eefb1b --- /dev/null +++ b/product/Formulator/Widget.py @@ -0,0 +1,1422 @@ +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, " ") + 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, " ") + 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 += ' ' + field.render_sub_field('ampm', + ampm, REQUEST, key=key) + if use_timezone: + time_result += ' ' + field.render_sub_field('timezone', + timezone, REQUEST, key=key) + return date_result + ' ' + 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 = ' ' + 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(' ', ' ') + + 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() + diff --git a/product/Formulator/XMLObjects.py b/product/Formulator/XMLObjects.py new file mode 100644 index 0000000000000000000000000000000000000000..b8f1185a62785a037d1c176f94d9aa1b6409dc9e --- /dev/null +++ b/product/Formulator/XMLObjects.py @@ -0,0 +1,96 @@ +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 diff --git a/product/Formulator/XMLToForm.py b/product/Formulator/XMLToForm.py new file mode 100644 index 0000000000000000000000000000000000000000..c2539e555988f9b5a3820cbce34b876954d422a2 --- /dev/null +++ b/product/Formulator/XMLToForm.py @@ -0,0 +1,127 @@ +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) diff --git a/product/Formulator/__init__.py b/product/Formulator/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..151fbf1bc153d2f73f995d8855e3378ebab841ea --- /dev/null +++ b/product/Formulator/__init__.py @@ -0,0 +1,90 @@ +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) + diff --git a/product/Formulator/dtml/FieldHelpTopic.dtml b/product/Formulator/dtml/FieldHelpTopic.dtml new file mode 100644 index 0000000000000000000000000000000000000000..b9e4590d8ac3a41b63c4b18903bd996ce32362b9 --- /dev/null +++ b/product/Formulator/dtml/FieldHelpTopic.dtml @@ -0,0 +1,21 @@ +<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> diff --git a/product/Formulator/dtml/fieldAdd.dtml b/product/Formulator/dtml/fieldAdd.dtml new file mode 100644 index 0000000000000000000000000000000000000000..ccc135d37429e34e17f14f0fe18ef73d12d2a554 --- /dev/null +++ b/product/Formulator/dtml/fieldAdd.dtml @@ -0,0 +1,52 @@ +<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> diff --git a/product/Formulator/dtml/fieldEdit.dtml b/product/Formulator/dtml/fieldEdit.dtml new file mode 100644 index 0000000000000000000000000000000000000000..bae25d5f3412c5a5da79f95e04b6ecf32debc133 --- /dev/null +++ b/product/Formulator/dtml/fieldEdit.dtml @@ -0,0 +1,65 @@ +<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> + + + diff --git a/product/Formulator/dtml/fieldListHeader.dtml b/product/Formulator/dtml/fieldListHeader.dtml new file mode 100644 index 0000000000000000000000000000000000000000..e30fef5dfeb2ec2ecbf5d31116a82da0f91621dc --- /dev/null +++ b/product/Formulator/dtml/fieldListHeader.dtml @@ -0,0 +1,17 @@ +<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 diff --git a/product/Formulator/dtml/fieldMessages.dtml b/product/Formulator/dtml/fieldMessages.dtml new file mode 100644 index 0000000000000000000000000000000000000000..c70b659273d375b3bd536ad5f410094412b458cb --- /dev/null +++ b/product/Formulator/dtml/fieldMessages.dtml @@ -0,0 +1,23 @@ +<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> + diff --git a/product/Formulator/dtml/fieldOverride.dtml b/product/Formulator/dtml/fieldOverride.dtml new file mode 100644 index 0000000000000000000000000000000000000000..86b6133350bdb38d97287b0d80440e8aa32fb927 --- /dev/null +++ b/product/Formulator/dtml/fieldOverride.dtml @@ -0,0 +1,58 @@ +<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> diff --git a/product/Formulator/dtml/fieldTales.dtml b/product/Formulator/dtml/fieldTales.dtml new file mode 100644 index 0000000000000000000000000000000000000000..352b0a18c5f6984d70ecae4df17376f50dff041a --- /dev/null +++ b/product/Formulator/dtml/fieldTales.dtml @@ -0,0 +1,64 @@ +<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> diff --git a/product/Formulator/dtml/fieldTest.dtml b/product/Formulator/dtml/fieldTest.dtml new file mode 100644 index 0000000000000000000000000000000000000000..aa644e1f614717443137366e13046fe22cdf98c1 --- /dev/null +++ b/product/Formulator/dtml/fieldTest.dtml @@ -0,0 +1,53 @@ +<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> diff --git a/product/Formulator/dtml/formAdd.dtml b/product/Formulator/dtml/formAdd.dtml new file mode 100644 index 0000000000000000000000000000000000000000..a8c230950bcc56aa654be4149dd73c29b5b26c6f --- /dev/null +++ b/product/Formulator/dtml/formAdd.dtml @@ -0,0 +1,62 @@ +<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> diff --git a/product/Formulator/dtml/formOrder.dtml b/product/Formulator/dtml/formOrder.dtml new file mode 100644 index 0000000000000000000000000000000000000000..2961661487a977c2d532deae29e1b244e9b41b2b --- /dev/null +++ b/product/Formulator/dtml/formOrder.dtml @@ -0,0 +1,142 @@ +<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;"> <a href="&dtml-field_id;/manage_main"><img src="&dtml-BASEPATH1;/&dtml-icon;" alt="&dtml-meta_type;" title="&dtml-meta_type;" border="0"></a> <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> diff --git a/product/Formulator/dtml/formSettings.dtml b/product/Formulator/dtml/formSettings.dtml new file mode 100644 index 0000000000000000000000000000000000000000..51ea7a57d7f24708bfa71c14e3d05b5c4d98925a --- /dev/null +++ b/product/Formulator/dtml/formSettings.dtml @@ -0,0 +1,34 @@ +<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> diff --git a/product/Formulator/dtml/formTest.dtml b/product/Formulator/dtml/formTest.dtml new file mode 100644 index 0000000000000000000000000000000000000000..d5eb0b0a4dd36e05b4a926c3a2d357ff47b4c84f --- /dev/null +++ b/product/Formulator/dtml/formTest.dtml @@ -0,0 +1,72 @@ +<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 </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> + diff --git a/product/Formulator/dtml/formXML.dtml b/product/Formulator/dtml/formXML.dtml new file mode 100644 index 0000000000000000000000000000000000000000..4a9419eaedb4a9fe8f4775f38dfd119520ead1e5 --- /dev/null +++ b/product/Formulator/dtml/formXML.dtml @@ -0,0 +1,17 @@ +<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> + diff --git a/product/Formulator/help/BasicForm.py b/product/Formulator/help/BasicForm.py new file mode 100644 index 0000000000000000000000000000000000000000..4803085571c33b00a24f2ff2c8d9180b65521fb9 --- /dev/null +++ b/product/Formulator/help/BasicForm.py @@ -0,0 +1,39 @@ + +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' + """ + + + + diff --git a/product/Formulator/help/Field.py b/product/Formulator/help/Field.py new file mode 100644 index 0000000000000000000000000000000000000000..78c14ab50d6356b215dbea186019761c877b7cbf --- /dev/null +++ b/product/Formulator/help/Field.py @@ -0,0 +1,179 @@ +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' + """ + + + + + diff --git a/product/Formulator/help/Form.py b/product/Formulator/help/Form.py new file mode 100644 index 0000000000000000000000000000000000000000..2302d928254a7c9e836c9c1e685eb2c1ecf44c7e --- /dev/null +++ b/product/Formulator/help/Form.py @@ -0,0 +1,268 @@ +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' + """ + + + diff --git a/product/Formulator/help/ZMIForm.py b/product/Formulator/help/ZMIForm.py new file mode 100644 index 0000000000000000000000000000000000000000..3306c285840048622de796ba7655840fe3a04cae --- /dev/null +++ b/product/Formulator/help/ZMIForm.py @@ -0,0 +1,24 @@ + +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' + """ + + + + diff --git a/product/Formulator/help/dogfood.txt b/product/Formulator/help/dogfood.txt new file mode 100644 index 0000000000000000000000000000000000000000..acfbcc29325fa8f83507f8a5c43119a77a0a44e3 --- /dev/null +++ b/product/Formulator/help/dogfood.txt @@ -0,0 +1,181 @@ +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. + diff --git a/product/Formulator/help/fieldEdit.txt b/product/Formulator/help/fieldEdit.txt new file mode 100644 index 0000000000000000000000000000000000000000..8db1494c6d27453fe6c86b45ec2306f3eb0652ba --- /dev/null +++ b/product/Formulator/help/fieldEdit.txt @@ -0,0 +1,62 @@ +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. + + diff --git a/product/Formulator/help/fieldMessages.txt b/product/Formulator/help/fieldMessages.txt new file mode 100644 index 0000000000000000000000000000000000000000..af44d7d8e4e82ee5b00f5368fec791ed91b71f8c --- /dev/null +++ b/product/Formulator/help/fieldMessages.txt @@ -0,0 +1,9 @@ +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. diff --git a/product/Formulator/help/fieldOverride.txt b/product/Formulator/help/fieldOverride.txt new file mode 100644 index 0000000000000000000000000000000000000000..0872bb6094f4e62f45097aad73e40139e966df21 --- /dev/null +++ b/product/Formulator/help/fieldOverride.txt @@ -0,0 +1,50 @@ +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 + diff --git a/product/Formulator/help/fieldTales.txt b/product/Formulator/help/fieldTales.txt new file mode 100644 index 0000000000000000000000000000000000000000..4905684395ecef8263a639197e26060c47404cc6 --- /dev/null +++ b/product/Formulator/help/fieldTales.txt @@ -0,0 +1,101 @@ +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. + diff --git a/product/Formulator/help/fieldTest.txt b/product/Formulator/help/fieldTest.txt new file mode 100644 index 0000000000000000000000000000000000000000..04abaf6ac823b0e588035facb74e1b2a6be883ef --- /dev/null +++ b/product/Formulator/help/fieldTest.txt @@ -0,0 +1,24 @@ +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 diff --git a/product/Formulator/help/formContents.txt b/product/Formulator/help/formContents.txt new file mode 100644 index 0000000000000000000000000000000000000000..cb1734a7bb3b0909c7c18f3afe0e68ac1a8b5d6e --- /dev/null +++ b/product/Formulator/help/formContents.txt @@ -0,0 +1,27 @@ +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. + + + + diff --git a/product/Formulator/help/formOrder.txt b/product/Formulator/help/formOrder.txt new file mode 100644 index 0000000000000000000000000000000000000000..58b1eb76256502f24049a7d5573145d686482ca0 --- /dev/null +++ b/product/Formulator/help/formOrder.txt @@ -0,0 +1,85 @@ +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. + + + diff --git a/product/Formulator/help/formSettings.txt b/product/Formulator/help/formSettings.txt new file mode 100644 index 0000000000000000000000000000000000000000..e4509adca1607c68c1627613c26ca89788d42c7d --- /dev/null +++ b/product/Formulator/help/formSettings.txt @@ -0,0 +1,42 @@ +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. diff --git a/product/Formulator/help/formTest.txt b/product/Formulator/help/formTest.txt new file mode 100644 index 0000000000000000000000000000000000000000..dee3cc197acc5cec0e57aba3b13b1c4e69784a63 --- /dev/null +++ b/product/Formulator/help/formTest.txt @@ -0,0 +1,27 @@ +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. diff --git a/product/Formulator/help/formXML.txt b/product/Formulator/help/formXML.txt new file mode 100644 index 0000000000000000000000000000000000000000..18c49d491e4b774dc20bbbaa1fb24b3dc620defc --- /dev/null +++ b/product/Formulator/help/formXML.txt @@ -0,0 +1,17 @@ +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'). diff --git a/product/Formulator/help/formulator_howto.txt b/product/Formulator/help/formulator_howto.txt new file mode 100644 index 0000000000000000000000000000000000000000..163284dab1ddc54b3c375f0fd1c39c7cfe847c72 --- /dev/null +++ b/product/Formulator/help/formulator_howto.txt @@ -0,0 +1,271 @@ +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. + + + diff --git a/product/Formulator/help/formulator_motto.txt b/product/Formulator/help/formulator_motto.txt new file mode 100644 index 0000000000000000000000000000000000000000..3f45280a58b797fabef15da7fcf8de9b4655cd36 --- /dev/null +++ b/product/Formulator/help/formulator_motto.txt @@ -0,0 +1,7 @@ +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!") + diff --git a/product/Formulator/homepage.html b/product/Formulator/homepage.html new file mode 100644 index 0000000000000000000000000000000000000000..021d8da4bd43a91f457cdac3e40459961a581fe4 --- /dev/null +++ b/product/Formulator/homepage.html @@ -0,0 +1,49 @@ +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_<fieldname>*; 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> diff --git a/product/Formulator/tests/README.txt b/product/Formulator/tests/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..d8fac69d15441468d9aaf8343e62479c74122b85 --- /dev/null +++ b/product/Formulator/tests/README.txt @@ -0,0 +1,2 @@ +This directory now contains some unit tests for Formulator. + diff --git a/product/Formulator/tests/__init__.py b/product/Formulator/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..17fb5500b0142f588c0271501d6b7face9f19fc7 --- /dev/null +++ b/product/Formulator/tests/__init__.py @@ -0,0 +1 @@ +# this file is here to make 'tests' a module of its own diff --git a/product/Formulator/tests/test_Form.py b/product/Formulator/tests/test_Form.py new file mode 100644 index 0000000000000000000000000000000000000000..c8a64b5551503183ea8208feeb9610bf196a4764 --- /dev/null +++ b/product/Formulator/tests/test_Form.py @@ -0,0 +1,175 @@ +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() diff --git a/product/Formulator/tests/test_all.py b/product/Formulator/tests/test_all.py new file mode 100644 index 0000000000000000000000000000000000000000..d2e0c1d6ce43446c17e5413ab5805a57a33a5536 --- /dev/null +++ b/product/Formulator/tests/test_all.py @@ -0,0 +1,27 @@ +# 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() diff --git a/product/Formulator/tests/test_serialize.py b/product/Formulator/tests/test_serialize.py new file mode 100644 index 0000000000000000000000000000000000000000..aa5157b395acc6bb2a3ee0017a8c7f3b7fd090b3 --- /dev/null +++ b/product/Formulator/tests/test_serialize.py @@ -0,0 +1,400 @@ +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() + diff --git a/product/Formulator/tests/test_validators.py b/product/Formulator/tests/test_validators.py new file mode 100644 index 0000000000000000000000000000000000000000..a2da6b2b12035586f821cf4f245061ae793d5f43 --- /dev/null +++ b/product/Formulator/tests/test_validators.py @@ -0,0 +1,466 @@ +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ü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() + diff --git a/product/Formulator/version.txt b/product/Formulator/version.txt new file mode 100644 index 0000000000000000000000000000000000000000..5ab86037439f04f4509351121894fac5ab668485 --- /dev/null +++ b/product/Formulator/version.txt @@ -0,0 +1 @@ +Formulator 1.6.1 diff --git a/product/Formulator/www/BasicField.gif b/product/Formulator/www/BasicField.gif new file mode 100644 index 0000000000000000000000000000000000000000..bf36aa7d32c59249021ee0a9e563e10806e0de30 Binary files /dev/null and b/product/Formulator/www/BasicField.gif differ diff --git a/product/Formulator/www/CheckBoxField.gif b/product/Formulator/www/CheckBoxField.gif new file mode 100644 index 0000000000000000000000000000000000000000..b34917af9ec1b706818ce44ff1c4180e78c62d44 Binary files /dev/null and b/product/Formulator/www/CheckBoxField.gif differ diff --git a/product/Formulator/www/DateTimeField.gif b/product/Formulator/www/DateTimeField.gif new file mode 100644 index 0000000000000000000000000000000000000000..d8d1b52e1b24b59eb151a5a9192fe33302ba0892 Binary files /dev/null and b/product/Formulator/www/DateTimeField.gif differ diff --git a/product/Formulator/www/EmailField.gif b/product/Formulator/www/EmailField.gif new file mode 100644 index 0000000000000000000000000000000000000000..6d3157a71b39344c5dc28ec70cbf8ade957bcc13 Binary files /dev/null and b/product/Formulator/www/EmailField.gif differ diff --git a/product/Formulator/www/FileField.gif b/product/Formulator/www/FileField.gif new file mode 100644 index 0000000000000000000000000000000000000000..ab0b9696a16c5e70163d56e55d1a58a38edad1e2 Binary files /dev/null and b/product/Formulator/www/FileField.gif differ diff --git a/product/Formulator/www/FloatField.gif b/product/Formulator/www/FloatField.gif new file mode 100644 index 0000000000000000000000000000000000000000..6eb2450049e59767c263a821ce3c1de44d052ac0 Binary files /dev/null and b/product/Formulator/www/FloatField.gif differ diff --git a/product/Formulator/www/Form.gif b/product/Formulator/www/Form.gif new file mode 100644 index 0000000000000000000000000000000000000000..d402b282620c226e2dfafda51cb5c9cdebbec360 Binary files /dev/null and b/product/Formulator/www/Form.gif differ diff --git a/product/Formulator/www/IntegerField.gif b/product/Formulator/www/IntegerField.gif new file mode 100644 index 0000000000000000000000000000000000000000..0f4d61745d6f4a40228ad17b5761f5db1f663941 Binary files /dev/null and b/product/Formulator/www/IntegerField.gif differ diff --git a/product/Formulator/www/LabelField.gif b/product/Formulator/www/LabelField.gif new file mode 100644 index 0000000000000000000000000000000000000000..bf36aa7d32c59249021ee0a9e563e10806e0de30 Binary files /dev/null and b/product/Formulator/www/LabelField.gif differ diff --git a/product/Formulator/www/LinesField.gif b/product/Formulator/www/LinesField.gif new file mode 100644 index 0000000000000000000000000000000000000000..b39a3d47c688369849419229239cde39c4bf4a90 Binary files /dev/null and b/product/Formulator/www/LinesField.gif differ diff --git a/product/Formulator/www/LinkField.gif b/product/Formulator/www/LinkField.gif new file mode 100644 index 0000000000000000000000000000000000000000..e18c597131b07fc949a8d9400c296422f31e6e7d Binary files /dev/null and b/product/Formulator/www/LinkField.gif differ diff --git a/product/Formulator/www/ListField.gif b/product/Formulator/www/ListField.gif new file mode 100644 index 0000000000000000000000000000000000000000..31fcd78323cd09dc68a332f5d22ff66da592512c Binary files /dev/null and b/product/Formulator/www/ListField.gif differ diff --git a/product/Formulator/www/MethodField.gif b/product/Formulator/www/MethodField.gif new file mode 100644 index 0000000000000000000000000000000000000000..bdeb79acbb258f7bcbef38eef3d911bc7934f899 Binary files /dev/null and b/product/Formulator/www/MethodField.gif differ diff --git a/product/Formulator/www/MultiCheckBoxField.gif b/product/Formulator/www/MultiCheckBoxField.gif new file mode 100644 index 0000000000000000000000000000000000000000..233094d8dc5296eedb8ec91a8ed77f4505cff24e Binary files /dev/null and b/product/Formulator/www/MultiCheckBoxField.gif differ diff --git a/product/Formulator/www/MultiListField.gif b/product/Formulator/www/MultiListField.gif new file mode 100644 index 0000000000000000000000000000000000000000..7a82b8cf9270dd4938df3d1e2af0cc81d146b59e Binary files /dev/null and b/product/Formulator/www/MultiListField.gif differ diff --git a/product/Formulator/www/MultipleListField.gif b/product/Formulator/www/MultipleListField.gif new file mode 100644 index 0000000000000000000000000000000000000000..33067287fbd057adc2ac65baaea31f0b36618007 Binary files /dev/null and b/product/Formulator/www/MultipleListField.gif differ diff --git a/product/Formulator/www/PasswordField.gif b/product/Formulator/www/PasswordField.gif new file mode 100644 index 0000000000000000000000000000000000000000..3af5e028c0d6184a15bcb46b09f19f73f5f23242 Binary files /dev/null and b/product/Formulator/www/PasswordField.gif differ diff --git a/product/Formulator/www/PatternField.gif b/product/Formulator/www/PatternField.gif new file mode 100644 index 0000000000000000000000000000000000000000..1bf1f4ecf1cd0894343dc46e043642aca02c4b7a Binary files /dev/null and b/product/Formulator/www/PatternField.gif differ diff --git a/product/Formulator/www/RadioField.gif b/product/Formulator/www/RadioField.gif new file mode 100644 index 0000000000000000000000000000000000000000..9941e245e85f9015d8598ef286f68b6558451978 Binary files /dev/null and b/product/Formulator/www/RadioField.gif differ diff --git a/product/Formulator/www/RangedIntegerField.gif b/product/Formulator/www/RangedIntegerField.gif new file mode 100644 index 0000000000000000000000000000000000000000..8731abf91f850343f7dcf9b1ce33cf2dbc725bea Binary files /dev/null and b/product/Formulator/www/RangedIntegerField.gif differ diff --git a/product/Formulator/www/StringField.gif b/product/Formulator/www/StringField.gif new file mode 100644 index 0000000000000000000000000000000000000000..bf36aa7d32c59249021ee0a9e563e10806e0de30 Binary files /dev/null and b/product/Formulator/www/StringField.gif differ diff --git a/product/Formulator/www/TextAreaField.gif b/product/Formulator/www/TextAreaField.gif new file mode 100644 index 0000000000000000000000000000000000000000..8ffd9c45b8254f89788577f2da5f15f6c4f7d5fd Binary files /dev/null and b/product/Formulator/www/TextAreaField.gif differ