summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 74832d7)
raw | patch | inline | side by side (parent: 74832d7)
author | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Mon, 17 Feb 2003 06:44:01 +0000 (06:44 +0000) | ||
committer | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Mon, 17 Feb 2003 06:44:01 +0000 (06:44 +0000) |
than the split step of last week. This means that "msg-1@link@files" will
work now.
I had to change the password confirmation special var from "name:confirm"
to ":confirm:name" so it conformed with the pattern set by ":add:" etc.
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1516 57a73879-2fb5-44c3-a270-3262357dd7e2
work now.
I had to change the password confirmation special var from "name:confirm"
to ":confirm:name" so it conformed with the pattern set by ":add:" etc.
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1516 57a73879-2fb5-44c3-a270-3262357dd7e2
roundup/cgi/client.py | patch | blob | history | |
roundup/cgi/templating.py | patch | blob | history | |
test/test_cgi.py | patch | blob | history |
diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
index 75c0348f7f783ee0b901391703b647aaf8102d72..3559e75d2975c532ed27348ffb8aca2f651af6b9 100644 (file)
--- a/roundup/cgi/client.py
+++ b/roundup/cgi/client.py
-# $Id: client.py,v 1.88 2003-02-17 01:04:31 richard Exp $
+# $Id: client.py,v 1.89 2003-02-17 06:44:00 richard Exp $
__doc__ = """
WWW request handler (also used in the stand-alone server).
FV_OK_MESSAGE = re.compile(r'[@:]ok_message')
FV_ERROR_MESSAGE = re.compile(r'[@:]error_message')
- # specials for parsePropsFromForm
- FV_REQUIRED = re.compile(r'[@:]required')
- FV_ADD = re.compile(r'([@:])add\1')
- FV_REMOVE = re.compile(r'([@:])remove\1')
- FV_CONFIRM = re.compile(r'.+[@:]confirm')
- FV_LINK = re.compile(r'([@:])link\1(.+)')
-
- # deprecated
- FV_NOTE = re.compile(r'[@:]note')
- FV_FILE = re.compile(r'[@:]file')
+ # edit form variable handling (see unit tests)
+ FV_LABELS = r'''
+ ^(
+ (?P<note>[@:]note)|
+ (?P<file>[@:]file)|
+ (
+ ((?P<classname>%s)(?P<id>[-\d]+))? # optional leading designator
+ ((?P<required>[@:]required$)| # :required
+ (
+ (
+ (?P<add>[@:]add[@:])| # :add:<prop>
+ (?P<remove>[@:]remove[@:])| # :remove:<prop>
+ (?P<confirm>[@:]confirm[@:])| # :confirm:<prop>
+ (?P<link>[@:]link[@:])| # :link:<prop>
+ ([@:]) # just a separator
+ )?
+ (?P<propname>[^@:]+) # <prop>
+ )
+ )
+ )
+ )$'''
# Note: index page stuff doesn't appear here:
# columns, sort, sortdir, filter, group, groupdir, search_text,
return cl.create(**props)
def parsePropsFromForm(self, num_re=re.compile('^\d+$')):
- ''' Pull properties for the given class out of the form.
+ ''' Pull properties out of the form.
In the following, <bracketed> values are variable, ":" may be
one of ":" or "@", and other text "required" is fixed.
- Properties are specified as form variables
+ Properties are specified as form variables:
+
+ <propname>
+ - property on the current context item
+
<designator>:<propname>
+ - property on the indicated item
+
+ <classname>-<N>:<propname>
+ - property on the Nth new item of classname
- where the propery belongs to the context class or item if the
- designator is not specified. The designator may specify a
- negative item id value (ie. "issue-1") and a new item of the
- specified class will be created for each negative id found.
+ Once we have determined the "propname", we check to see if it
+ is one of the special form values:
- If a "<designator>:required" parameter is supplied,
- then the named property values must be supplied or a
- ValueError will be raised.
+ :required
+ The named property values must be supplied or a ValueError
+ will be raised.
- Other special form values:
- [classname|designator]:remove:<propname>=id(s)
+ :remove:<propname>=id(s)
The ids will be removed from the multilink property.
- [classname|designator]:add:<propname>=id(s)
+
+ :add:<propname>=id(s)
The ids will be added to the multilink property.
- [classname|designator]:link:<propname>=<classname>
+ :link:<propname>=<designator>
Used to add a link to new items created during edit.
These are collected up and returned in all_links. This will
result in an additional linking operation (either Link set or
Multilink append) after the edit/create is done using
- all_props in _editnodes. The <propname> on
- [classname|designator] will be set/appended the id of the
- newly created item of class <classname>.
-
- Note: the colon may be either ":" or "@".
+ all_props in _editnodes. The <propname> on the current item
+ will be set/appended the id of the newly created item of
+ class <designator> (where <designator> must be
+ <classname>-<N>).
Any of the form variables may be prefixed with a classname or
designator.
Two special form values are supported for backwards
compatibility:
:note - create a message (with content, author and date), link
- to the context item
+ to the context item. This is ALWAYS desginated "msg-1".
:file - create a file, attach to the current item and any
- message created by :note
+ message created by :note. This is ALWAYS designated
+ "file-1".
'''
# some very useful variables
db = self.db
form = self.form
- if not hasattr(self, 'FV_ITEMSPEC'):
- # generate the regexp for detecting
- # <classname|designator>[@:+]property
+ if not hasattr(self, 'FV_SPECIAL'):
+ # generate the regexp for handling special form values
classes = '|'.join(db.classes.keys())
- self.FV_ITEMSPEC = re.compile(r'(%s)([-\d]+)[@:](.+)$'%classes)
+ # specials for parsePropsFromForm
+ # handle the various forms (see unit tests)
+ self.FV_SPECIAL = re.compile(self.FV_LABELS%classes, re.VERBOSE)
self.FV_DESIGNATOR = re.compile(r'(%s)([-\d]+)'%classes)
# these indicate the default class / item
keys = form.keys()
timezone = db.getUserTimezone()
+ # sentinels for the :note and :file props
+ have_note = have_file = 0
+
+ # extract the usable form labels from the form
+ matches = []
for key in keys:
- # see if this value modifies a different item to the default
- m = self.FV_ITEMSPEC.match(key)
+ m = self.FV_SPECIAL.match(key)
if m:
+ matches.append((key, m.groupdict()))
+
+ # now handle the matches
+ for key, d in matches:
+ if d['classname']:
# we got a designator
- cn = m.group(1)
+ cn = d['classname']
cl = self.db.classes[cn]
- nodeid = m.group(2)
- propname = m.group(3)
- elif key == ':note':
- # backwards compatibility: the special note field
+ nodeid = d['id']
+ propname = d['propname']
+ elif d['note']:
+ # the special note field
cn = 'msg'
cl = self.db.classes[cn]
nodeid = '-1'
propname = 'content'
all_links.append((default_cn, default_nodeid, 'messages',
[('msg', '-1')]))
- elif key == ':file':
- # backwards compatibility: the special file field
+ have_note = 1
+ elif d['file']:
+ # the special file field
cn = 'file'
cl = self.db.classes[cn]
nodeid = '-1'
propname = 'content'
all_links.append((default_cn, default_nodeid, 'files',
[('file', '-1')]))
- if self.form.has_key(':note'):
- all_links.append(('msg', '-1', 'files', [('file', '-1')]))
+ have_file = 1
else:
# default
cn = default_cn
cl = default_cl
nodeid = default_nodeid
- propname = key
+ propname = d['propname']
# the thing this value relates to is...
this = (cn, nodeid)
- # is this a link command?
- if self.FV_LINK.match(propname):
- value = []
- for entry in extractFormList(form[key]):
- m = self.FV_DESIGNATOR.match(entry)
- if not m:
- raise ValueError, \
- 'link "%s" value "%s" not a designator'%(key, entry)
- value.append((m.groups(1), m.groups(2)))
- all_links.append((cn, nodeid, propname[6:], value))
-
# get more info about the class, and the current set of
# form props for it
if not all_propdef.has_key(cn):
all_props[this] = {}
props = all_props[this]
+ # is this a link command?
+ if d['link']:
+ value = []
+ for entry in extractFormList(form[key]):
+ m = self.FV_DESIGNATOR.match(entry)
+ if not m:
+ raise ValueError, \
+ 'link "%s" value "%s" not a designator'%(key, entry)
+ value.append((m.group(1), m.group(2)))
+ all_links.append((cn, nodeid, propname, value))
+ continue
+
# detect the special ":required" variable
- if self.FV_REQUIRED.match(key):
+ if d['required']:
all_required[this] = extractFormList(form[key])
continue
# see if we're performing a special multilink action
mlaction = 'set'
- if self.FV_REMOVE.match(propname):
- propname = propname[8:]
+ if d['remove']:
mlaction = 'remove'
- elif self.FV_ADD.match(propname):
- propname = propname[5:]
+ elif d['add']:
mlaction = 'add'
# does the property exist?
raise ValueError, 'You have submitted a %s action for'\
' the property "%s" which doesn\'t exist'%(mlaction,
propname)
+ # the form element is probably just something we don't care
+ # about - ignore it
continue
proptype = propdef[propname]
# now that we have the props field, we need a teensy little
# extra bit of help for the old :note field...
- if key == ':note' and value:
+ if d['note'] and value:
props['author'] = self.db.getuid()
props['date'] = date.Date()
if not value:
# ignore empty password values
continue
- for key in keys:
- if self.FV_CONFIRM.match(key):
+ for key, d in matches:
+ if d['confirm'] and d['propname'] == propname:
confirm = form[key]
break
else:
if propname in required and value is not None:
required.remove(propname)
+ # check to see if we need to specially link a file to the note
+ if have_note and have_file:
+ all_links.append(('msg', '-1', 'files', [('file', '-1')]))
+
# see if all the required properties have been supplied
s = []
for thing, required in all_required.items():
index 6992e061f3625c758953e864dd880af262cb00fe..57d3f21427fb66b1034aa9e135f4fbc43afeea0d 100644 (file)
def confirm(self, size = 30):
''' Render a second form edit field for the property, used for
confirmation that the user typed the password correctly. Generates
- a field with name "name:confirm".
+ a field with name ":confirm:name".
'''
- return '<input type="password" name="%s:confirm" size="%s">'%(
+ return '<input type="password" name=":confirm:name" size="%s">'%(
self._name, size)
class NumberHTMLProperty(HTMLProperty):
diff --git a/test/test_cgi.py b/test/test_cgi.py
index 4cb01137d93fcc57102ff0b0478c61fc58edbfa4..92db0e75371d2a116786fe279bc936cf8d5f9700 100644 (file)
--- a/test/test_cgi.py
+++ b/test/test_cgi.py
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
-# $Id: test_cgi.py,v 1.9 2003-02-14 00:31:46 richard Exp $
+# $Id: test_cgi.py,v 1.10 2003-02-17 06:44:01 richard Exp $
-import unittest, os, shutil, errno, sys, difflib, cgi
+import unittest, os, shutil, errno, sys, difflib, cgi, re
from roundup.cgi import client
from roundup import init, instance, password, hyperdb, date
multilink=hyperdb.Multilink('test'), date=hyperdb.Date(),
interval=hyperdb.Interval())
+ # compile the labels re
+ classes = '|'.join(self.db.classes.keys())
+ self.FV_SPECIAL = re.compile(client.Client.FV_LABELS%classes,
+ re.VERBOSE)
+
def parseForm(self, form, classname='test', nodeid=None):
cl = client.Client(self.instance, None, {'PATH_INFO':'/'},
makeForm(form))
except OSError, error:
if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+ #
+ # form label extraction
+ #
+ def tl(self, s, c, i, a, p):
+ m = self.FV_SPECIAL.match(s)
+ self.assertNotEqual(m, None)
+ d = m.groupdict()
+ self.assertEqual(d['classname'], c)
+ self.assertEqual(d['id'], i)
+ for action in 'required add remove link note file'.split():
+ if a == action:
+ self.assertNotEqual(d[action], None)
+ else:
+ self.assertEqual(d[action], None)
+ self.assertEqual(d['propname'], p)
+
+ def testLabelMatching(self):
+ self.tl('<propname>', None, None, None, '<propname>')
+ self.tl(':required', None, None, 'required', None)
+ self.tl(':confirm:<propname>', None, None, 'confirm', '<propname>')
+ self.tl(':add:<propname>', None, None, 'add', '<propname>')
+ self.tl(':remove:<propname>', None, None, 'remove', '<propname>')
+ self.tl(':link:<propname>', None, None, 'link', '<propname>')
+ self.tl('test1:<prop>', 'test', '1', None, '<prop>')
+ self.tl('test1:required', 'test', '1', 'required', None)
+ self.tl('test1:add:<prop>', 'test', '1', 'add', '<prop>')
+ self.tl('test1:remove:<prop>', 'test', '1', 'remove', '<prop>')
+ self.tl('test1:link:<prop>', 'test', '1', 'link', '<prop>')
+ self.tl('test1:confirm:<prop>', 'test', '1', 'confirm', '<prop>')
+ self.tl('test-1:<prop>', 'test', '-1', None, '<prop>')
+ self.tl('test-1:required', 'test', '-1', 'required', None)
+ self.tl('test-1:add:<prop>', 'test', '-1', 'add', '<prop>')
+ self.tl('test-1:remove:<prop>', 'test', '-1', 'remove', '<prop>')
+ self.tl('test-1:link:<prop>', 'test', '-1', 'link', '<prop>')
+ self.tl('test-1:confirm:<prop>', 'test', '-1', 'confirm', '<prop>')
+ self.tl(':note', None, None, 'note', None)
+ self.tl(':file', None, None, 'file', None)
+
#
# Empty form
#
self.assertRaises(ValueError, self.parseForm, {'password': ['', '']},
'user')
self.assertRaises(ValueError, self.parseForm, {'password': 'foo',
- 'password:confirm': ['', '']}, 'user')
+ ':confirm:password': ['', '']}, 'user')
def testSetPassword(self):
self.assertEqual(self.parseForm({'password': 'foo',
- 'password:confirm': 'foo'}, 'user'),
+ ':confirm:password': 'foo'}, 'user'),
({('user', None): {'password': 'foo'}}, []))
def testSetPasswordConfirmBad(self):
self.assertRaises(ValueError, self.parseForm, {'password': 'foo'},
'user')
self.assertRaises(ValueError, self.parseForm, {'password': 'foo',
- 'password:confirm': 'bar'}, 'user')
+ ':confirm:password': 'bar'}, 'user')
def testEmptyPasswordNotSet(self):
nodeid = self.db.user.create(username='1',
nodeid = self.db.user.create(username='2',
password=password.Password('foo'))
self.assertEqual(self.parseForm({'password': '',
- 'password:confirm': ''}, 'user', nodeid),
+ ':confirm:password': ''}, 'user', nodeid),
({('user', nodeid): {}}, []))
#
#
def testMultiple(self):
self.assertEqual(self.parseForm({'string': 'a', 'issue-1@title': 'b'}),
- ({('test', None): {'string': 'a'}, ('issue', '-1'):
- {'title': 'b'}}, []))
+ ({('test', None): {'string': 'a'},
+ ('issue', '-1'): {'title': 'b'}
+ }, []))
+
+ def testMultipleExistingContext(self):
nodeid = self.db.test.create()
self.assertEqual(self.parseForm({'string': 'a', 'issue-1@title': 'b'},
- 'test', nodeid), ({('test', nodeid): {'string': 'a'},
+ 'test', nodeid),({('test', nodeid): {'string': 'a'},
('issue', '-1'): {'title': 'b'}}, []))
+
+ def testLinking(self):
self.assertEqual(self.parseForm({
'string': 'a',
- 'issue-1@:add:nosy': '1',
- 'issue-2@:link:superseder': 'issue-1',
+ 'issue-1@add@nosy': '1',
+ 'issue-2@link@superseder': 'issue-1',
}),
({('test', None): {'string': 'a'},
('issue', '-1'): {'nosy': ['1']},
- ('issue', '-2'): {}},
- [('issue', '-2', 'superseder',
- [(('issue', '-1'), ('issue', '-1'))])
- ]
+ ('issue', '-2'): {}
+ },
+ [('issue', '-2', 'superseder', [('issue', '-1')])
+ ]
)
)
def testLinkBadDesignator(self):
self.assertRaises(ValueError, self.parseForm,
- {'test-1@:link:link': 'blah'})
+ {'test-1@link@link': 'blah'})
self.assertRaises(ValueError, self.parseForm,
- {'test-1@:link:link': 'issue'})
+ {'test-1@link@link': 'issue'})
def testBackwardsCompat(self):
res = self.parseForm({':note': 'spam'}, 'issue')