Code

- implemented extension to form parsing to allow editing of multiple items
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Wed, 12 Feb 2003 06:41:58 +0000 (06:41 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Wed, 12 Feb 2003 06:41:58 +0000 (06:41 +0000)
  and creation of multiple items (but only one per class)
- the colon ":" special form variable designator may now be any of : + @

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1498 57a73879-2fb5-44c3-a270-3262357dd7e2

CHANGES.txt
roundup/cgi/client.py
test/test_cgi.py

index 4feb8abf3f40e43f9cd975e19ce2e545f9d430a4..cc816c3e8e538f3ffc36e479ac08ead8f5ec83eb 100644 (file)
@@ -49,6 +49,9 @@ are given with the most recent entry first.
 - fixed error in indexargs_url (thanks Patrick Ohly)
 - fixed getnode (sf bug 684531)
 - included UN*X manual pages from Bastian Kleineidam
+- implemented extension to form parsing to allow editing of multiple items
+  and creation of multiple items (but only one per class)
+- the colon ":" special form variable designator may now be any of : + @
 
 
 2003-??-?? 0.5.6
index d2cb1a79943066a1cbc68e2e792f513fc9598ca9..ef3d2ad842a647b26dbd0562961e4cf0cfca9a70 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.78 2003-02-12 00:00:28 richard Exp $
+# $Id: client.py,v 1.79 2003-02-12 06:41:58 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
@@ -78,8 +78,31 @@ class Client:
 
      Once a user logs in, they are assigned a session. The Client instance
      keeps the nodeid of the session as the "session" attribute.
+
+
+    Special form variables:
+     Note that in various places throughout this code, special form
+     variables of the form :<name> are used. The colon (":") part may
+     actually be one of several characters from the set:
+
+       : @ + 
+
     '''
 
+    # special form variables
+    FV_TEMPLATE = re.compile(r'[@+:]template')
+    FV_OK_MESSAGE = re.compile(r'[@+:]ok_message')
+    FV_ERROR_MESSAGE = re.compile(r'[@+:]error_message')
+    FV_REQUIRED = re.compile(r'[@+:]required')
+    FV_LINK = re.compile(r'[@+:]link')
+    FV_MULTILINK = re.compile(r'[@+:]multilink')
+    FV_NOTE = re.compile(r'[@+:]note')
+    FV_FILE = re.compile(r'[@+:]file')
+    FV_ADD = re.compile(r'([@+:])add\1')
+    FV_REMOVE = re.compile(r'([@+:])remove\1')
+    FV_CONFIRM = re.compile(r'.+[@+:]confirm')
+    FV_SPLITTER = re.compile(r'[@+:]')
+
     def __init__(self, instance, request, env, form=None):
         hyperdb.traceMark()
         self.instance = instance
@@ -119,6 +142,7 @@ class Client:
         self.additional_headers = {}
         self.response_code = 200
 
+
     def main(self):
         ''' Wrap the real main in a try/finally so we always close off the db.
         '''
@@ -300,11 +324,21 @@ class Client:
         self.classname = None
         self.nodeid = None
 
+        # see if a template or messages are specified
+        template_override = ok_message = error_message = None
+        for key in self.form.keys():
+            if self.FV_TEMPLATE.match(key):
+                template_override = self.form[key].value
+            elif self.FV_OK_MESSAGE.match(key):
+                ok_message = self.form[key].value
+            elif self.FV_ERROR_MESSAGE.match(key):
+                error_message = self.form[key].value
+
         # determine the classname and possibly nodeid
         path = self.path.split('/')
         if not path or path[0] in ('', 'home', 'index'):
-            if self.form.has_key(':template'):
-                self.template = self.form[':template'].value
+            if template_override is not None:
+                self.template = template_override
             else:
                 self.template = ''
             return
@@ -336,14 +370,14 @@ class Client:
             raise NotFound, self.classname
 
         # see if we have a template override
-        if self.form.has_key(':template'):
-            self.template = self.form[':template'].value
+        if template_override is not None:
+            self.template = template_override
 
         # see if we were passed in a message
-        if self.form.has_key(':ok_message'):
-            self.ok_message.append(self.form[':ok_message'].value)
-        if self.form.has_key(':error_message'):
-            self.error_message.append(self.form[':error_message'].value)
+        if ok_message:
+            self.ok_message.append(ok_message)
+        if error_message:
+            self.error_message.append(error_message)
 
     def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):
         ''' Serve the file from the content property of the designated item.
@@ -602,7 +636,7 @@ class Client:
 
         # parse the props from the form
         try:
-            props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
+            props = self.parsePropsFromForm()
         except (ValueError, KeyError), message:
             self.error_message.append(_('Error: ') + str(message))
             return
@@ -618,7 +652,6 @@ class Client:
         # create the new user
         cl = self.db.user
         try:
-            props = parsePropsFromForm(self.db, cl, self.form)
             props['roles'] = self.instance.config.NEW_WEB_USER_ROLES
             self.userid = cl.create(**props)
             self.db.commit()
@@ -643,7 +676,7 @@ class Client:
         message = _('You are now registered, welcome!')
 
         # redirect to the item's edit page
-        raise Redirect, '%s%s%s?:ok_message=%s'%(
+        raise Redirect, '%s%s%s?+ok_message=%s'%(
             self.base, self.classname, self.userid,  urllib.quote(message))
 
     def registerPermission(self, props):
@@ -675,20 +708,11 @@ class Client:
              "files" property. Attach the file to the message created from
              the :note if it's supplied.
 
-            :required=property,property,...
-             The named properties are required to be filled in the form.
-
-            :remove:<propname>=id(s)
-             The ids will be removed from the multilink property.
-            :add:<propname>=id(s)
-             The ids will be added to the multilink property.
-
+           See parsePropsFromForm for more special variables
         '''
-        cl = self.db.classes[self.classname]
-
         # parse the props from the form
         try:
-            props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
+            props = self.parsePropsFromForm()
         except (ValueError, KeyError), message:
             self.error_message.append(_('Error: ') + str(message))
             return
@@ -717,15 +741,11 @@ class Client:
         if props:
             message = _('%(changes)s edited ok')%{'changes':
                 ', '.join(props.keys())}
-        elif self.form.has_key(':note') and self.form[':note'].value:
-            message = _('note added')
-        elif (self.form.has_key(':file') and self.form[':file'].filename):
-            message = _('file added')
         else:
             message = _('nothing changed')
 
         # redirect to the item's edit page
-        raise Redirect, '%s%s%s?:ok_message=%s'%(self.base, self.classname,
+        raise Redirect, '%s%s%s?+ok_message=%s'%(self.base, self.classname,
             self.nodeid,  urllib.quote(message))
 
     def editItemPermission(self, props):
@@ -758,11 +778,9 @@ class Client:
             This follows the same form as the editItemAction, with the same
             special form values.
         '''
-        cl = self.db.classes[self.classname]
-
         # parse the props from the form
         try:
-            props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
+            props = self.parsePropsFromForm()
         except (ValueError, KeyError), message:
             self.error_message.append(_('Error: ') + str(message))
             return
@@ -820,7 +838,7 @@ class Client:
 
         # redirect to the new item's page
         raise Redirect, '%s%s%s?:ok_message=%s'%(self.base, self.classname,
-            nid,  urllib.quote(message))
+            nid, urllib.quote(message))
 
     def newItemPermission(self, props):
         ''' Determine whether the user has permission to create (edit) this
@@ -1192,247 +1210,329 @@ class Client:
                     link = self.db.classes[link]
                     link.set(nodeid, **{property: nid})
 
-def fixNewlines(text):
-    ''' Homogenise line endings.
+    def parsePropsFromForm(self, num_re=re.compile('^\d+$')):
+        ''' Pull properties for the given class out of the form.
 
-        Different web clients send different line ending values, but
-        other systems (eg. email) don't necessarily handle those line
-        endings. Our solution is to convert all line endings to LF.
-    '''
-    text = text.replace('\r\n', '\n')
-    return text.replace('\r', '\n')
+            If a ":required" parameter is supplied, then the names
+            property values must be supplied or a ValueError will be raised.
 
-def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
-    ''' Pull properties for the given class out of the form.
+            Other special form values:
+             :remove:<propname>=id(s)
+              The ids will be removed from the multilink property.
+             :add:<propname>=id(s)
+              The ids will be added to the multilink property.
 
-        If a ":required" parameter is supplied, then the names property values
-        must be supplied or a ValueError will be raised.
+            Note: the colon may be one of:  : @ +
 
-        Other special form values:
-         :remove:<propname>=id(s)
-          The ids will be removed from the multilink property.
-         :add:<propname>=id(s)
-          The ids will be added to the multilink property.
-    '''
-    required = []
-    if form.has_key(':required'):
-        value = form[':required']
-        if isinstance(value, type([])):
-            required = [i.value.strip() for i in value]
-        else:
-            required = [i.strip() for i in value.value.split(',')]
-
-    props = {}
-    keys = form.keys()
-    properties = cl.getprops()
-    timezone = db.getUserTimezone()
-
-    for key in keys:
-        # see if we're performing a special multilink action
-        mlaction = 'set'
-        if key.startswith(':remove:'):
-            propname = key[8:]
-            mlaction = 'remove'
-        elif key.startswith(':add:'):
-            propname = key[5:]
-            mlaction = 'add'
-        else:
-            propname = key
-
-        # does the property exist?
-        if not properties.has_key(propname):
-            if mlaction != 'set':
-                raise ValueError, 'You have submitted a %s action for'\
-                    ' the property "%s" which doesn\'t exist'%(mlaction,
-                    propname)
-            continue
-        proptype = properties[propname]
-
-        # Get the form value. This value may be a MiniFieldStorage or a list
-        # of MiniFieldStorages.
-        value = form[key]
-
-        # handle unpacking of the MiniFieldStorage / list form value
-        if isinstance(proptype, hyperdb.Multilink):
-            # multiple values are OK
-            if isinstance(value, type([])):
-                # it's a list of MiniFieldStorages
-                value = [i.value.strip() for i in value]
+            Any of the form variables may be prefixed with a classname or
+            designator.
+
+            The return from this method is a dict of 
+                classname|designator: properties
+
+        '''
+        # some very useful variables
+        db = self.db
+        form = self.form
+
+        if not hasattr(self, 'FV_CLASSSPEC'):
+            # generate the regexp for detecting
+            # <classname|designator>[@:+]property
+            classes = '|'.join(db.classes.keys())
+            self.FV_CLASSSPEC = re.compile(r'(%s)[@+:](.+)$'%classes)
+            self.FV_ITEMSPEC = re.compile(r'(%s)(\d+)[@+:](.+)$'%classes)
+
+        # these indicate the default class / item
+        default_cn = self.classname
+        default_cl = self.db.classes[default_cn]
+        default_nodeid = str(self.nodeid or '')
+
+        # we'll store info about the individual class/item edit in these
+        all_required = {}       # one entry per class/item
+        all_props = {}          # one entry per class/item
+        all_propdef = {}        # note - only one entry per class
+
+        # we should always return something, even empty, for the context
+        all_props[default_cn+default_nodeid] = {}
+
+        keys = form.keys()
+        timezone = db.getUserTimezone()
+
+        for key in keys:
+            # see if this value modifies a different class/item to the default
+            m = self.FV_CLASSSPEC.match(key)
+            if m:
+                # we got a classname
+                cn = m.group(1)
+                cl = self.db.classes[cn]
+                nodeid = ''
+                propname = m.group(2)
             else:
-                # it's a MiniFieldStorage, but may be a comma-separated list
-                # of values
-                value = [i.strip() for i in value.value.split(',')]
+                m = self.FV_ITEMSPEC.match(key)
+                if m:
+                    # we got a designator
+                    cn = m.group(1)
+                    cl = self.db.classes[cn]
+                    nodeid = m.group(2)
+                    propname = m.group(3)
+                else:
+                    # default
+                    cn = default_cn
+                    cl = default_cl
+                    nodeid = default_nodeid
+                    propname = key
+
+            # the thing this value relates to is...
+            this = cn+nodeid
+
+            # get more info about the class, and the current set of
+            # form props for it
+            if not all_propdef.has_key(cn):
+                all_propdef[cn] = cl.getprops()
+            propdef = all_propdef[cn]
+            if not all_props.has_key(this):
+                all_props[this] = {}
+            props = all_props[this]
+
+            # detect the special ":required" variable
+            if self.FV_REQUIRED.match(key):
+                value = form[key]
+                if isinstance(value, type([])):
+                    required = [i.value.strip() for i in value]
+                else:
+                    required = [i.strip() for i in value.value.split(',')]
+                all_required[this] = required
+                continue
 
-            # filter out the empty bits
-            value = filter(None, value)
-        else:
-            # multiple values are not OK
-            if isinstance(value, type([])):
-                raise ValueError, 'You have submitted more than one value'\
-                    ' for the %s property'%propname
-            # we've got a MiniFieldStorage, so pull out the value and strip
-            # surrounding whitespace
-            value = value.value.strip()
-
-        # handle by type now
-        if isinstance(proptype, hyperdb.Password):
-            if not value:
-                # ignore empty password values
+            # get the required values list
+            if not all_required.has_key(this):
+                all_required[this] = []
+            required = all_required[this]
+
+            # see if we're performing a special multilink action
+            mlaction = 'set'
+            if self.FV_REMOVE.match(propname):
+                propname = propname[8:]
+                mlaction = 'remove'
+            elif self.FV_ADD.match(propname):
+                propname = propname[5:]
+                mlaction = 'add'
+
+            # does the property exist?
+            if not propdef.has_key(propname):
+                if mlaction != 'set':
+                    raise ValueError, 'You have submitted a %s action for'\
+                        ' the property "%s" which doesn\'t exist'%(mlaction,
+                        propname)
                 continue
-            if not form.has_key('%s:confirm'%propname):
-                raise ValueError, 'Password and confirmation text do not match'
-            confirm = form['%s:confirm'%propname]
-            if isinstance(confirm, type([])):
-                raise ValueError, 'You have submitted more than one value'\
-                    ' for the %s property'%propname
-            if value != confirm.value:
-                raise ValueError, 'Password and confirmation text do not match'
-            value = password.Password(value)
-
-        elif isinstance(proptype, hyperdb.Link):
-            # see if it's the "no selection" choice
-            if value == '-1' or not value:
-                # if we're creating, just don't include this property
-                if not nodeid:
-                    continue
-                value = None
-            else:
-                # handle key values
-                link = proptype.classname
-                if not num_re.match(value):
-                    try:
-                        value = db.classes[link].lookup(value)
-                    except KeyError:
-                        raise ValueError, _('property "%(propname)s": '
-                            '%(value)s not a %(classname)s')%{
-                            'propname': propname, 'value': value,
-                            'classname': link}
-                    except TypeError, message:
-                        raise ValueError, _('you may only enter ID values '
-                            'for property "%(propname)s": %(message)s')%{
-                            'propname': propname, 'message': message}
-        elif isinstance(proptype, hyperdb.Multilink):
-            # perform link class key value lookup if necessary
-            link = proptype.classname
-            link_cl = db.classes[link]
-            l = []
-            for entry in value:
-                if not entry: continue
-                if not num_re.match(entry):
-                    try:
-                        entry = link_cl.lookup(entry)
-                    except KeyError:
-                        raise ValueError, _('property "%(propname)s": '
-                            '"%(value)s" not an entry of %(classname)s')%{
-                            'propname': propname, 'value': entry,
-                            'classname': link}
-                    except TypeError, message:
-                        raise ValueError, _('you may only enter ID values '
-                            'for property "%(propname)s": %(message)s')%{
-                            'propname': propname, 'message': message}
-                l.append(entry)
-            l.sort()
-
-            # now use that list of ids to modify the multilink
-            if mlaction == 'set':
-                value = l
-            else:
-                # we're modifying the list - get the current list of ids
-                if props.has_key(propname):
-                    existing = props[propname]
-                elif nodeid:
-                    existing = cl.get(nodeid, propname, [])
+            proptype = propdef[propname]
+
+            # Get the form value. This value may be a MiniFieldStorage or a list
+            # of MiniFieldStorages.
+            value = form[key]
+
+            # handle unpacking of the MiniFieldStorage / list form value
+            if isinstance(proptype, hyperdb.Multilink):
+                # multiple values are OK
+                if isinstance(value, type([])):
+                    # it's a list of MiniFieldStorages
+                    value = [i.value.strip() for i in value]
                 else:
-                    existing = []
+                    # it's a MiniFieldStorage, but may be a comma-separated list
+                    # of values
+                    value = [i.strip() for i in value.value.split(',')]
 
-                # now either remove or add
-                if mlaction == 'remove':
-                    # remove - handle situation where the id isn't in the list
-                    for entry in l:
+                # filter out the empty bits
+                value = filter(None, value)
+            else:
+                # multiple values are not OK
+                if isinstance(value, type([])):
+                    raise ValueError, 'You have submitted more than one value'\
+                        ' for the %s property'%propname
+                # we've got a MiniFieldStorage, so pull out the value and strip
+                # surrounding whitespace
+                value = value.value.strip()
+
+            # handle by type now
+            if isinstance(proptype, hyperdb.Password):
+                if not value:
+                    # ignore empty password values
+                    continue
+                for key in keys:
+                    if self.FV_CONFIRM.match(key):
+                        confirm = form[key]
+                        break
+                else:
+                    raise ValueError, 'Password and confirmation text do '\
+                        'not match'
+                if isinstance(confirm, type([])):
+                    raise ValueError, 'You have submitted more than one value'\
+                        ' for the %s property'%propname
+                if value != confirm.value:
+                    raise ValueError, 'Password and confirmation text do '\
+                        'not match'
+                value = password.Password(value)
+
+            elif isinstance(proptype, hyperdb.Link):
+                # see if it's the "no selection" choice
+                if value == '-1' or not value:
+                    # if we're creating, just don't include this property
+                    if not nodeid:
+                        continue
+                    value = None
+                else:
+                    # handle key values
+                    link = proptype.classname
+                    if not num_re.match(value):
+                        try:
+                            value = db.classes[link].lookup(value)
+                        except KeyError:
+                            raise ValueError, _('property "%(propname)s": '
+                                '%(value)s not a %(classname)s')%{
+                                'propname': propname, 'value': value,
+                                'classname': link}
+                        except TypeError, message:
+                            raise ValueError, _('you may only enter ID values '
+                                'for property "%(propname)s": %(message)s')%{
+                                'propname': propname, 'message': message}
+            elif isinstance(proptype, hyperdb.Multilink):
+                # perform link class key value lookup if necessary
+                link = proptype.classname
+                link_cl = db.classes[link]
+                l = []
+                for entry in value:
+                    if not entry: continue
+                    if not num_re.match(entry):
                         try:
-                            existing.remove(entry)
-                        except ValueError:
+                            entry = link_cl.lookup(entry)
+                        except KeyError:
                             raise ValueError, _('property "%(propname)s": '
-                                '"%(value)s" not currently in list')%{
-                                'propname': propname, 'value': entry}
+                                '"%(value)s" not an entry of %(classname)s')%{
+                                'propname': propname, 'value': entry,
+                                'classname': link}
+                        except TypeError, message:
+                            raise ValueError, _('you may only enter ID values '
+                                'for property "%(propname)s": %(message)s')%{
+                                'propname': propname, 'message': message}
+                    l.append(entry)
+                l.sort()
+
+                # now use that list of ids to modify the multilink
+                if mlaction == 'set':
+                    value = l
                 else:
-                    # add - easy, just don't dupe
-                    for entry in l:
-                        if entry not in existing:
-                            existing.append(entry)
-                value = existing
-                value.sort()
-
-        # other types should be None'd if there's no value
-        elif value:
-            if isinstance(proptype, hyperdb.String):
-                # fix the CRLF/CR -> LF stuff
-                value = fixNewlines(value)
-            elif isinstance(proptype, hyperdb.Date):
-                value = date.Date(value, offset=timezone)
-            elif isinstance(proptype, hyperdb.Interval):
-                value = date.Interval(value)
-            elif isinstance(proptype, hyperdb.Boolean):
-                value = value.lower() in ('yes', 'true', 'on', '1')
-            elif isinstance(proptype, hyperdb.Number):
-                value = float(value)
-        else:
-            # if we're creating, just don't include this property
-            if not nodeid:
-                continue
-            value = None
+                    # we're modifying the list - get the current list of ids
+                    if props.has_key(propname):
+                        existing = props[propname]
+                    elif nodeid:
+                        existing = cl.get(nodeid, propname, [])
+                    else:
+                        existing = []
+
+                    # now either remove or add
+                    if mlaction == 'remove':
+                        # remove - handle situation where the id isn't in
+                        # the list
+                        for entry in l:
+                            try:
+                                existing.remove(entry)
+                            except ValueError:
+                                raise ValueError, _('property "%(propname)s": '
+                                    '"%(value)s" not currently in list')%{
+                                    'propname': propname, 'value': entry}
+                    else:
+                        # add - easy, just don't dupe
+                        for entry in l:
+                            if entry not in existing:
+                                existing.append(entry)
+                    value = existing
+                    value.sort()
+
+            # other types should be None'd if there's no value
+            elif value:
+                if isinstance(proptype, hyperdb.String):
+                    # fix the CRLF/CR -> LF stuff
+                    value = fixNewlines(value)
+                elif isinstance(proptype, hyperdb.Date):
+                    value = date.Date(value, offset=timezone)
+                elif isinstance(proptype, hyperdb.Interval):
+                    value = date.Interval(value)
+                elif isinstance(proptype, hyperdb.Boolean):
+                    value = value.lower() in ('yes', 'true', 'on', '1')
+                elif isinstance(proptype, hyperdb.Number):
+                    value = float(value)
+            else:
+                # if we're creating, just don't include this property
+                if not nodeid:
+                    continue
+                value = None
 
-        # get the old value
-        if nodeid:
-            try:
-                existing = cl.get(nodeid, propname)
-            except KeyError:
-                # this might be a new property for which there is no existing
-                # value
-                if not properties.has_key(propname):
-                    raise
+            # get the old value
+            if nodeid:
+                try:
+                    existing = cl.get(nodeid, propname)
+                except KeyError:
+                    # this might be a new property for which there is
+                    # no existing value
+                    if not propdef.has_key(propname):
+                        raise
+
+                # make sure the existing multilink is sorted
+                if isinstance(proptype, hyperdb.Multilink):
+                    existing.sort()
+
+                # "missing" existing values may not be None
+                if not existing:
+                    if isinstance(proptype, hyperdb.String) and not existing:
+                        # some backends store "missing" Strings as empty strings
+                        existing = None
+                    elif isinstance(proptype, hyperdb.Number) and not existing:
+                        # some backends store "missing" Numbers as 0 :(
+                        existing = 0
+                    elif isinstance(proptype, hyperdb.Boolean) and not existing:
+                        # likewise Booleans
+                        existing = 0
+
+                # if changed, set it
+                if value != existing:
+                    props[propname] = value
+            else:
+                # don't bother setting empty/unset values
+                if value is None:
+                    continue
+                elif isinstance(proptype, hyperdb.Multilink) and value == []:
+                    continue
+                elif isinstance(proptype, hyperdb.String) and value == '':
+                    continue
 
-            # make sure the existing multilink is sorted
-            if isinstance(proptype, hyperdb.Multilink):
-                existing.sort()
-
-            # "missing" existing values may not be None
-            if not existing:
-                if isinstance(proptype, hyperdb.String) and not existing:
-                    # some backends store "missing" Strings as empty strings
-                    existing = None
-                elif isinstance(proptype, hyperdb.Number) and not existing:
-                    # some backends store "missing" Numbers as 0 :(
-                    existing = 0
-                elif isinstance(proptype, hyperdb.Boolean) and not existing:
-                    # likewise Booleans
-                    existing = 0
-
-            # if changed, set it
-            if value != existing:
                 props[propname] = value
-        else:
-            # don't bother setting empty/unset values
-            if value is None:
-                continue
-            elif isinstance(proptype, hyperdb.Multilink) and value == []:
-                continue
-            elif isinstance(proptype, hyperdb.String) and value == '':
-                continue
 
-            props[propname] = value
+            # register this as received if required?
+            if propname in required and value is not None:
+                required.remove(propname)
 
-        # register this as received if required?
-        if propname in required and value is not None:
-            required.remove(propname)
+        # see if all the required properties have been supplied
+        s = []
+        for thing, required in all_required.items():
+            if not required:
+                continue
+            if len(required) > 1:
+                p = 'properties'
+            else:
+                p = 'property'
+            s.append('Required %s %s %s not supplied'%(thing, p,
+                ', '.join(required)))
+        if s:
+            raise ValueError, '\n'.join(s)
 
-    # see if all the required properties have been supplied
-    if required:
-        if len(required) > 1:
-            p = 'properties'
-        else:
-            p = 'property'
-        raise ValueError, 'Required %s %s not supplied'%(p, ', '.join(required))
+        return all_props
 
-    return props
+def fixNewlines(text):
+    ''' Homogenise line endings.
 
+        Different web clients send different line ending values, but
+        other systems (eg. email) don't necessarily handle those line
+        endings. Our solution is to convert all line endings to LF.
+    '''
+    text = text.replace('\r\n', '\n')
+    return text.replace('\r', '\n')
index cfa4192c9d4b32ba56b27d57468aad50469d3caa..6625b7f090a9ecb495117de5a89eff8766a3a24a 100644 (file)
@@ -8,7 +8,7 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
-# $Id: test_cgi.py,v 1.6 2003-01-20 23:05:20 richard Exp $
+# $Id: test_cgi.py,v 1.7 2003-02-12 06:41:58 richard Exp $
 
 import unittest, os, shutil, errno, sys, difflib, cgi
 
@@ -24,6 +24,10 @@ def makeForm(args):
             form.list.append(cgi.MiniFieldStorage(k, v))
     return form
 
+class config:
+    TRACKER_NAME = 'testing testing'
+    TRACKER_WEB = 'http://testing.testing/'
+
 class FormTestCase(unittest.TestCase):
     def setUp(self):
         self.dirname = '_test_cgi_form'
@@ -44,10 +48,19 @@ class FormTestCase(unittest.TestCase):
             roles='User', realname='Contrary, Mary')
 
         test = self.instance.dbinit.Class(self.db, "test",
+            string=hyperdb.String(),
             boolean=hyperdb.Boolean(), link=hyperdb.Link('test'),
             multilink=hyperdb.Multilink('test'), date=hyperdb.Date(),
             interval=hyperdb.Interval())
 
+    def parseForm(self, form, classname='test', nodeid=None):
+        cl = client.Client(self.instance, None, {'PATH_INFO':'/'},
+            makeForm(form))
+        cl.classname = classname
+        cl.nodeid = nodeid
+        cl.db = self.db
+        return cl.parsePropsFromForm()
+
     def tearDown(self):
         self.db.close()
         try:
@@ -59,285 +72,268 @@ class FormTestCase(unittest.TestCase):
     # Empty form
     #
     def testNothing(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({})), {})
+        self.assertEqual(self.parseForm({}), {'test': {}})
 
     def testNothingWithRequired(self):
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({':required': 'title'}))
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({':required': 'title,status',
-            'status':'1'}))
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({':required': ['title','status'],
-            'status':'1'}))
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({':required': 'status',
-            'status':''}))
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({':required': 'nosy',
-            'nosy':''}))
+        self.assertRaises(ValueError, self.parseForm, {':required': 'string'})
+        self.assertRaises(ValueError, self.parseForm,
+            {':required': 'title,status', 'status':'1'}, 'issue')
+        self.assertRaises(ValueError, self.parseForm,
+            {':required': ['title','status'], 'status':'1'}, 'issue')
+        self.assertRaises(ValueError, self.parseForm,
+            {':required': 'status', 'status':''}, 'issue')
+        self.assertRaises(ValueError, self.parseForm,
+            {':required': 'nosy', 'nosy':''}, 'issue')
 
     #
     # Nonexistant edit
     #
     def testEditNonexistant(self):
-        self.assertRaises(IndexError, client.parsePropsFromForm, self.db,
-            self.db.test, makeForm({'boolean': ''}), '1')
+        self.assertRaises(IndexError, self.parseForm, {'boolean': ''},
+            'test', '1')
 
     #
     # String
     #
     def testEmptyString(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'title': ''})), {})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'title': ' '})), {})
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({'title': ['', '']}))
+        self.assertEqual(self.parseForm({'string': ''}), {'test': {}})
+        self.assertEqual(self.parseForm({'string': ' '}), {'test': {}})
+        self.assertRaises(ValueError, self.parseForm, {'string': ['', '']})
 
     def testSetString(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'title': 'foo'})), {'title': 'foo'})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'title': 'a\r\nb\r\n'})), {'title': 'a\nb'})
+        self.assertEqual(self.parseForm({'string': 'foo'}),
+            {'test': {'string': 'foo'}})
+        self.assertEqual(self.parseForm({'string': 'a\r\nb\r\n'}),
+            {'test': {'string': 'a\nb'}})
         nodeid = self.db.issue.create(title='foo')
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'title': 'foo'}), nodeid), {})
+        self.assertEqual(self.parseForm({'title': 'foo'}, 'issue', nodeid),
+            {'issue'+nodeid: {}})
 
     def testEmptyStringSet(self):
         nodeid = self.db.issue.create(title='foo')
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'title': ''}), nodeid), {'title': None})
+        self.assertEqual(self.parseForm({'title': ''}, 'issue', nodeid),
+            {'issue'+nodeid: {'title': None}})
         nodeid = self.db.issue.create(title='foo')
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'title': ' '}), nodeid), {'title': None})
+        self.assertEqual(self.parseForm({'title': ' '}, 'issue', nodeid),
+            {'issue'+nodeid: {'title': None}})
 
     #
     # Link
     #
     def testEmptyLink(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'status': ''})), {})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'status': ' '})), {})
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({'status': ['', '']}))
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'status': '-1'})), {})
+        self.assertEqual(self.parseForm({'link': ''}), {'test': {}})
+        self.assertEqual(self.parseForm({'link': ' '}), {'test': {}})
+        self.assertRaises(ValueError, self.parseForm, {'link': ['', '']})
+        self.assertEqual(self.parseForm({'link': '-1'}), {'test': {}})
 
     def testSetLink(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'status': 'unread'})), {'status': '1'})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'status': '1'})), {'status': '1'})
+        self.assertEqual(self.parseForm({'status': 'unread'}, 'issue'),
+            {'issue': {'status': '1'}})
+        self.assertEqual(self.parseForm({'status': '1'}, 'issue'),
+            {'issue': {'status': '1'}})
         nodeid = self.db.issue.create(status='unread')
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'status': 'unread'}), nodeid), {})
+        self.assertEqual(self.parseForm({'status': 'unread'}, 'issue', nodeid),
+            {'issue'+nodeid: {}})
 
     def testUnsetLink(self):
         nodeid = self.db.issue.create(status='unread')
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'status': '-1'}), nodeid), {'status': None})
+        self.assertEqual(self.parseForm({'status': '-1'}, 'issue', nodeid),
+            {'issue'+nodeid: {'status': None}})
 
     def testInvalidLinkValue(self):
 # XXX This is not the current behaviour - should we enforce this?
-#        self.assertRaises(IndexError, client.parsePropsFromForm, self.db,
-#            self.db.issue, makeForm({'status': '4'}))
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({'status': 'frozzle'}))
+#        self.assertRaises(IndexError, self.parseForm,
+#            {'status': '4'}))
+        self.assertRaises(ValueError, self.parseForm, {'link': 'frozzle'})
 
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.test, makeForm({'link': 'frozzle'}))
+        self.assertRaises(ValueError, self.parseForm, {'link': 'frozzle'})
 
     #
     # Multilink
     #
     def testEmptyMultilink(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': ''})), {})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': ' '})), {})
+        self.assertEqual(self.parseForm({'nosy': ''}), {'test': {}})
+        self.assertEqual(self.parseForm({'nosy': ' '}), {'test': {}})
 
     def testSetMultilink(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': '1'})), {'nosy': ['1']})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': 'admin'})), {'nosy': ['1']})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': ['1','2']})), {'nosy': ['1','2']})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': '1,2'})), {'nosy': ['1','2']})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': 'admin,2'})), {'nosy': ['1','2']})
+        self.assertEqual(self.parseForm({'nosy': '1'}, 'issue'),
+            {'issue': {'nosy': ['1']}})
+        self.assertEqual(self.parseForm({'nosy': 'admin'}, 'issue'),
+            {'issue': {'nosy': ['1']}})
+        self.assertEqual(self.parseForm({'nosy': ['1','2']}, 'issue'),
+            {'issue': {'nosy': ['1','2']}})
+        self.assertEqual(self.parseForm({'nosy': '1,2'}, 'issue'),
+            {'issue': {'nosy': ['1','2']}})
+        self.assertEqual(self.parseForm({'nosy': 'admin,2'}, 'issue'),
+            {'issue': {'nosy': ['1','2']}})
 
     def testEmptyMultilinkSet(self):
         nodeid = self.db.issue.create(nosy=['1','2'])
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': ''}), nodeid), {'nosy': []})
+        self.assertEqual(self.parseForm({'nosy': ''}, 'issue', nodeid), 
+            {'issue'+nodeid: {'nosy': []}})
         nodeid = self.db.issue.create(nosy=['1','2'])
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': ' '}), nodeid), {'nosy': []})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': '1,2'}), nodeid), {})
+        self.assertEqual(self.parseForm({'nosy': ' '}, 'issue', nodeid), 
+            {'issue'+nodeid: {'nosy': []}})
+        self.assertEqual(self.parseForm({'nosy': '1,2'}, 'issue', nodeid),
+            {'issue'+nodeid: {}})
 
     def testInvalidMultilinkValue(self):
 # XXX This is not the current behaviour - should we enforce this?
-#        self.assertRaises(IndexError, client.parsePropsFromForm, self.db,
-#            self.db.issue, makeForm({'nosy': '4'}))
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({'nosy': 'frozzle'}))
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({'nosy': '1,frozzle'}))
-
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.test, makeForm({'multilink': 'frozzle'}))
+#        self.assertRaises(IndexError, self.parseForm,
+#            {'nosy': '4'}))
+        self.assertRaises(ValueError, self.parseForm, {'nosy': 'frozzle'},
+            'issue')
+        self.assertRaises(ValueError, self.parseForm, {'nosy': '1,frozzle'},
+            'issue')
+        self.assertRaises(ValueError, self.parseForm, {'multilink': 'frozzle'})
 
     def testMultilinkAdd(self):
         nodeid = self.db.issue.create(nosy=['1'])
         # do nothing
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':add:nosy': ''}), nodeid), {})
+        self.assertEqual(self.parseForm({':add:nosy': ''}, 'issue', nodeid),
+            {'issue'+nodeid: {}})
 
         # do something ;)
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':add:nosy': '2'}), nodeid), {'nosy': ['1','2']})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':add:nosy': '2,mary'}), nodeid), {'nosy': ['1','2','4']})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':add:nosy': ['2','3']}), nodeid), {'nosy': ['1','2','3']})
+        self.assertEqual(self.parseForm({':add:nosy': '2'}, 'issue', nodeid),
+            {'issue'+nodeid: {'nosy': ['1','2']}})
+        self.assertEqual(self.parseForm({':add:nosy': '2,mary'}, 'issue',
+            nodeid), {'issue'+nodeid: {'nosy': ['1','2','4']}})
+        self.assertEqual(self.parseForm({':add:nosy': ['2','3']}, 'issue',
+            nodeid), {'issue'+nodeid: {'nosy': ['1','2','3']}})
 
     def testMultilinkAddNew(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':add:nosy': ['2','3']})), {'nosy': ['2','3']})
+        self.assertEqual(self.parseForm({':add:nosy': ['2','3']}, 'issue'),
+            {'issue': {'nosy': ['2','3']}})
 
     def testMultilinkRemove(self):
         nodeid = self.db.issue.create(nosy=['1','2'])
         # do nothing
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':remove:nosy': ''}), nodeid), {})
+        self.assertEqual(self.parseForm({':remove:nosy': ''}, 'issue', nodeid),
+            {'issue'+nodeid: {}})
 
         # do something ;)
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':remove:nosy': '1'}), nodeid), {'nosy': ['2']})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':remove:nosy': 'admin,2'}), nodeid), {'nosy': []})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':remove:nosy': ['1','2']}), nodeid), {'nosy': []})
+        self.assertEqual(self.parseForm({':remove:nosy': '1'}, 'issue',
+            nodeid), {'issue'+nodeid: {'nosy': ['2']}})
+        self.assertEqual(self.parseForm({':remove:nosy': 'admin,2'},
+            'issue', nodeid), {'issue'+nodeid: {'nosy': []}})
+        self.assertEqual(self.parseForm({':remove:nosy': ['1','2']},
+            'issue', nodeid), {'issue'+nodeid: {'nosy': []}})
 
         # remove one that doesn't exist?
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({':remove:nosy': '4'}), nodeid)
+        self.assertRaises(ValueError, self.parseForm, {':remove:nosy': '4'},
+            'issue', nodeid)
 
     def testMultilinkRetired(self):
         self.db.user.retire('2')
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({'nosy': ['2','3']})), {'nosy': ['2','3']})
+        self.assertEqual(self.parseForm({'nosy': ['2','3']}, 'issue'),
+            {'issue': {'nosy': ['2','3']}})
         nodeid = self.db.issue.create(nosy=['1','2'])
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':remove:nosy': '2'}), nodeid), {'nosy': ['1']})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.issue,
-            makeForm({':add:nosy': '3'}), nodeid), {'nosy': ['1','2','3']})
+        self.assertEqual(self.parseForm({':remove:nosy': '2'}, 'issue',
+            nodeid), {'issue'+nodeid: {'nosy': ['1']}})
+        self.assertEqual(self.parseForm({':add:nosy': '3'}, 'issue', nodeid),
+            {'issue'+nodeid: {'nosy': ['1','2','3']}})
 
     def testAddRemoveNonexistant(self):
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({':remove:foo': '2'}))
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.issue, makeForm({':add:foo': '2'}))
+        self.assertRaises(ValueError, self.parseForm, {':remove:foo': '2'},
+            'issue')
+        self.assertRaises(ValueError, self.parseForm, {':add:foo': '2'},
+            'issue')
 
     #
     # Password
     #
     def testEmptyPassword(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.user,
-            makeForm({'password': ''})), {})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.user,
-            makeForm({'password': ''})), {})
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.user, makeForm({'password': ['', '']}))
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.user, makeForm({'password': 'foo',
-            'password:confirm': ['', '']}))
+        self.assertEqual(self.parseForm({'password': ''}, 'user'),
+            {'user': {}})
+        self.assertEqual(self.parseForm({'password': ''}, 'user'),
+            {'user': {}})
+        self.assertRaises(ValueError, self.parseForm, {'password': ['', '']},
+            'user')
+        self.assertRaises(ValueError, self.parseForm, {'password': 'foo',
+            'password:confirm': ['', '']}, 'user')
 
     def testSetPassword(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.user,
-            makeForm({'password': 'foo', 'password:confirm': 'foo'})),
-            {'password': 'foo'})
+        self.assertEqual(self.parseForm({'password': 'foo',
+            'password:confirm': 'foo'}, 'user'), {'user': {'password': 'foo'}})
 
     def testSetPasswordConfirmBad(self):
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.user, makeForm({'password': 'foo'}))
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.user, makeForm({'password': 'foo',
-            'password:confirm': 'bar'}))
+        self.assertRaises(ValueError, self.parseForm, {'password': 'foo'},
+            'user')
+        self.assertRaises(ValueError, self.parseForm, {'password': 'foo',
+            'password:confirm': 'bar'}, 'user')
 
     def testEmptyPasswordNotSet(self):
         nodeid = self.db.user.create(username='1',
             password=password.Password('foo'))
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.user,
-            makeForm({'password': ''}), nodeid), {})
+        self.assertEqual(self.parseForm({'password': ''}, 'user', nodeid),
+            {'user'+nodeid: {}})
         nodeid = self.db.user.create(username='2',
             password=password.Password('foo'))
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.user,
-            makeForm({'password': '', 'password:confirm': ''}), nodeid), {})
+        self.assertEqual(self.parseForm({'password': '',
+            'password:confirm': ''}, 'user', nodeid),
+            {'user'+nodeid: {}})
 
     #
     # Boolean
     #
     def testEmptyBoolean(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'boolean': ''})), {})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'boolean': ' '})), {})
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.test, makeForm({'boolean': ['', '']}))
+        self.assertEqual(self.parseForm({'boolean': ''}), {'test': {}})
+        self.assertEqual(self.parseForm({'boolean': ' '}), {'test': {}})
+        self.assertRaises(ValueError, self.parseForm, {'boolean': ['', '']})
 
     def testSetBoolean(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'boolean': 'yes'})), {'boolean': 1})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'boolean': 'a\r\nb\r\n'})), {'boolean': 0})
+        self.assertEqual(self.parseForm({'boolean': 'yes'}),
+            {'test': {'boolean': 1}})
+        self.assertEqual(self.parseForm({'boolean': 'a\r\nb\r\n'}),
+            {'test': {'boolean': 0}})
         nodeid = self.db.test.create(boolean=1)
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'boolean': 'yes'}), nodeid), {})
+        self.assertEqual(self.parseForm({'boolean': 'yes'}, 'test', nodeid),
+            {'test'+nodeid: {}})
         nodeid = self.db.test.create(boolean=0)
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'boolean': 'no'}), nodeid), {})
+        self.assertEqual(self.parseForm({'boolean': 'no'}, 'test', nodeid),
+            {'test'+nodeid: {}})
 
     def testEmptyBooleanSet(self):
         nodeid = self.db.test.create(boolean=0)
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'boolean': ''}), nodeid), {'boolean': None})
+        self.assertEqual(self.parseForm({'boolean': ''}, 'test', nodeid),
+            {'test'+nodeid: {'boolean': None}})
         nodeid = self.db.test.create(boolean=1)
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'boolean': ' '}), nodeid), {'boolean': None})
+        self.assertEqual(self.parseForm({'boolean': ' '}, 'test', nodeid),
+            {'test'+nodeid: {'boolean': None}})
 
     #
     # Date
     #
     def testEmptyDate(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'date': ''})), {})
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'date': ' '})), {})
-        self.assertRaises(ValueError, client.parsePropsFromForm, self.db,
-            self.db.test, makeForm({'date': ['', '']}))
+        self.assertEqual(self.parseForm({'date': ''}), {'test': {}})
+        self.assertEqual(self.parseForm({'date': ' '}), {'test': {}})
+        self.assertRaises(ValueError, self.parseForm, {'date': ['', '']})
 
     def testSetDate(self):
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'date': '2003-01-01'})),
-            {'date': date.Date('2003-01-01')})
+        self.assertEqual(self.parseForm({'date': '2003-01-01'}),
+            {'test': {'date': date.Date('2003-01-01')}})
         nodeid = self.db.test.create(date=date.Date('2003-01-01'))
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'date': '2003-01-01'}), nodeid), {})
+        self.assertEqual(self.parseForm({'date': '2003-01-01'}, 'test', 
+            nodeid), {'test'+nodeid: {}})
 
     def testEmptyDateSet(self):
         nodeid = self.db.test.create(date=date.Date('.'))
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'date': ''}), nodeid), {'date': None})
+        self.assertEqual(self.parseForm({'date': ''}, 'test', nodeid), 
+            {'test'+nodeid: {'date': None}})
         nodeid = self.db.test.create(date=date.Date('1970-01-01.00:00:00'))
-        self.assertEqual(client.parsePropsFromForm(self.db, self.db.test,
-            makeForm({'date': ' '}), nodeid), {'date': None})
+        self.assertEqual(self.parseForm({'date': ' '}, 'test', nodeid), 
+            {'test'+nodeid: {'date': None}})
+
+    #
+    # Test multiple items in form
+    #
+    def testMultiple(self):
+        self.assertEqual(self.parseForm({'string': 'a', 'issue@title': 'b'}),
+            {'test': {'string': 'a'}, 'issue': {'title': 'b'}})
+        nodeid = self.db.test.create()
+        self.assertEqual(self.parseForm({'string': 'a', 'issue@title': 'b'},
+            'test', nodeid),
+            {'test1': {'string': 'a'}, 'issue': {'title': 'b'}})
 
 def suite():
     l = [unittest.makeSuite(FormTestCase),