From 9690b906d04670f9950d453b41ba5c20484b147e Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 12 Feb 2003 06:41:58 +0000 Subject: [PATCH] - 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 : + @ git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1498 57a73879-2fb5-44c3-a270-3262357dd7e2 --- CHANGES.txt | 3 + roundup/cgi/client.py | 612 ++++++++++++++++++++++++------------------ test/test_cgi.py | 336 ++++++++++++----------- 3 files changed, 525 insertions(+), 426 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4feb8ab..cc816c3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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 diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py index d2cb1a7..ef3d2ad 100644 --- a/roundup/cgi/client.py +++ b/roundup/cgi/client.py @@ -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 : 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:=id(s) - The ids will be removed from the multilink property. - :add:=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:=id(s) + The ids will be removed from the multilink property. + :add:=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:=id(s) - The ids will be removed from the multilink property. - :add:=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 + # [@:+]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') diff --git a/test/test_cgi.py b/test/test_cgi.py index cfa4192..6625b7f 100644 --- a/test/test_cgi.py +++ b/test/test_cgi.py @@ -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), -- 2.39.5