From a0e5c7a4e9913541e8b349a76e361053f464ce6e Mon Sep 17 00:00:00 2001 From: richard Date: Fri, 18 Oct 2002 03:34:58 +0000 Subject: [PATCH] - added CGI :remove: and :add: which specify item ids to remove / add in multilink. (is used in issue message display, allowing removal of messages) git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1365 57a73879-2fb5-44c3-a270-3262357dd7e2 --- TODO.txt | 3 +- doc/announcement.txt | 1 + frontends/ZRoundup/ZRoundup.py | 35 +++++--- roundup/cgi/client.py | 105 +++++++++++++++++----- roundup/templates/classic/html/issue.item | 9 +- test/test_mailsplit.py | 8 +- 6 files changed, 122 insertions(+), 39 deletions(-) diff --git a/TODO.txt b/TODO.txt index ea5809c..dbe7c13 100644 --- a/TODO.txt +++ b/TODO.txt @@ -59,13 +59,12 @@ pending web multilink item removal action (with retirement) pending web search "refinement" - pre-fill the search page with the current search parameters pending web column-heading sort stuff isn't implemented -pending web handle :remove: and :add: which specify - item ids to remove / add in multilink. active web UNIX init.d script for roundup-server bug docs need to mention somewhere how sorting works - it's mentioned in the design doc - multilink sorting by length is dumb bug web query editing isn't fully implemented +bug web no testing for parsePropsFromForm ======= ========= ============================================================= diff --git a/doc/announcement.txt b/doc/announcement.txt index ed65f00..213ddf6 100644 --- a/doc/announcement.txt +++ b/doc/announcement.txt @@ -11,6 +11,7 @@ looking into fixing it for the next patch release. Roundup requires python 2.1.3 or later for correct operation. We've had a good crack at bugs (thanks to all who contributed!): + - fixed filter() with no sort/group (sf bug 618614) - fixed register with no session (sf bug 618611) - fixed log / pid file path handling in roundup-server (sf bug 617981) diff --git a/frontends/ZRoundup/ZRoundup.py b/frontends/ZRoundup/ZRoundup.py index b0780cc..bcde6b1 100644 --- a/frontends/ZRoundup/ZRoundup.py +++ b/frontends/ZRoundup/ZRoundup.py @@ -14,7 +14,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: ZRoundup.py,v 1.15 2002-10-16 06:48:50 richard Exp $ +# $Id: ZRoundup.py,v 1.16 2002-10-18 03:34:58 richard Exp $ # ''' ZRoundup module - exposes the roundup web interface to Zope @@ -38,7 +38,7 @@ import urlparse from Globals import InitializeClass, HTMLFile from OFS.SimpleItem import Item from OFS.PropertyManager import PropertyManager -from Acquisition import Implicit +from Acquisition import Explicit, Implicit from Persistence import Persistent from AccessControl import ClassSecurityInfo from AccessControl import ModuleSecurityInfo @@ -119,8 +119,8 @@ class ZRoundup(Item, PropertyManager, Implicit, Persistent): icon = "misc_/ZRoundup/icon" - security.declarePrivate('_opendb') - def _opendb(self): + security.declarePrivate('roundup_opendb') + def roundup_opendb(self): '''Open the roundup instance database for a transaction. ''' instance = roundup.instance.open(self.instance_home) @@ -131,7 +131,7 @@ class ZRoundup(Item, PropertyManager, Implicit, Persistent): url = urlparse.urlparse( self.absolute_url() ) path = url[2] path_components = path.split( '/' ) - + # special case when roundup is '/' in this virtual host, if path == "/" : env['SCRIPT_NAME'] = "/" @@ -151,6 +151,7 @@ class ZRoundup(Item, PropertyManager, Implicit, Persistent): # It doesn't occur with apache/roundup.cgi, though. form = FormWrapper(self.REQUEST.form) + print (env['SCRIPT_NAME'], env['PATH_INFO']) return instance.Client(instance, request, env, form) security.declareProtected('View', 'index_html') @@ -170,7 +171,7 @@ class ZRoundup(Item, PropertyManager, Implicit, Persistent): RESPONSE.setHeader( "Location" , url ) return RESPONSE - client = self._opendb() + client = self.roundup_opendb() # fake the path that roundup should use client.split_path = ['index'] return client.main() @@ -178,10 +179,26 @@ class ZRoundup(Item, PropertyManager, Implicit, Persistent): def __getitem__(self, item): '''All other URL accesses are passed throuh to roundup ''' + return PathElement(self, item) + +class PathElement(Item, Implicit, Persistent): + def __init__(self, parent, path): + self.parent = parent + self.path = path + + def __getitem__(self, item): + ''' Get a subitem. + ''' + return PathElement(self.path + '/' + item) + + def __call__(self, *args, **kw): + ''' Actually call through to roundup to handle the request. + ''' + print '*****', self.path try: - client = self._opendb() + client = self.parent.roundup_opendb() # fake the path that roundup should use - client.path = item + client.path = self.path # and call roundup to do something client.main() return '' @@ -193,8 +210,6 @@ class ZRoundup(Item, PropertyManager, Implicit, Persistent): traceback.print_exc() # all other exceptions in roundup are valid raise - raise KeyError, item - InitializeClass(ZRoundup) modulesecurity.apply(globals()) diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py index 03eaf11..10fd448 100644 --- a/roundup/cgi/client.py +++ b/roundup/cgi/client.py @@ -1,4 +1,4 @@ -# $Id: client.py,v 1.54 2002-10-17 06:11:25 richard Exp $ +# $Id: client.py,v 1.55 2002-10-18 03:34:58 richard Exp $ __doc__ = """ WWW request handler (also used in the stand-alone server). @@ -663,6 +663,11 @@ class Client: :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. + ''' cl = self.db.classes[self.classname] @@ -1141,6 +1146,12 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')): If a ":required" parameter is supplied, then the names property values must be supplied or a ValueError will be raised. + + 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'): @@ -1154,19 +1165,37 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')): keys = form.keys() properties = cl.getprops() for key in keys: - if not properties.has_key(key): + # 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 remove action for'\ + ' the property "%s" which doesn\'t exist'%propname continue - proptype = properties[key] + proptype = properties[propname] # Get the form value. This value may be a MiniFieldStorage or a list # of MiniFieldStorages. value = form[key] + print (mlaction, propname, value) + # make sure non-multilinks only get one value if not isinstance(proptype, hyperdb.Multilink): if isinstance(value, type([])): raise ValueError, 'You have submitted more than one value'\ - ' for the %s property'%key + ' for the %s property'%propname # we've got a MiniFieldStorage, so pull out the value and strip # surrounding whitespace value = value.value.strip() @@ -1180,23 +1209,23 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')): if not value: # ignore empty password values continue - if not form.has_key('%s:confirm'%key): + if not form.has_key('%s:confirm'%propname): raise ValueError, 'Password and confirmation text do not match' - confirm = form['%s:confirm'%key] + confirm = form['%s:confirm'%propname] if isinstance(confirm, type([])): raise ValueError, 'You have submitted more than one value'\ - ' for the %s property'%key + ' 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.Date): if value: - value = date.Date(form[key].value.strip()) + value = date.Date(value.value.strip()) else: continue elif isinstance(proptype, hyperdb.Interval): if value: - value = date.Interval(form[key].value.strip()) + value = date.Interval(value.value.strip()) else: continue elif isinstance(proptype, hyperdb.Link): @@ -1211,12 +1240,13 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')): value = db.classes[link].lookup(value) except KeyError: raise ValueError, _('property "%(propname)s": ' - '%(value)s not a %(classname)s')%{'propname':key, - 'value': value, 'classname': link} + '%(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':key, 'message': message} + 'propname': propname, 'message': message} elif isinstance(proptype, hyperdb.Multilink): if isinstance(value, type([])): # it's a list of MiniFieldStorages @@ -1235,37 +1265,66 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')): except KeyError: raise ValueError, _('property "%(propname)s": ' '"%(value)s" not an entry of %(classname)s')%{ - 'propname':key, 'value': entry, 'classname': link} + 'propname': propname, 'value': entry, + 'classname': link} except TypeError, message: raise ValueError, _('you may only enter ID values ' 'for property "%(propname)s": %(message)s')%{ - 'propname':key, 'message': message} + 'propname': propname, 'message': message} l.append(entry) l.sort() - value = l + + # 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 + try: + existing = cl.get(nodeid, propname) + except KeyError: + existing = [] + 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() + elif isinstance(proptype, hyperdb.Boolean): - props[key] = value = value.lower() in ('yes', 'true', 'on', '1') + value = value.lower() in ('yes', 'true', 'on', '1') + props[propname] = value elif isinstance(proptype, hyperdb.Number): - props[key] = value = int(value) + props[propname] = value = int(value) # register this as received if required? - if key in required and value is not None: - required.remove(key) + if propname in required and value is not None: + required.remove(propname) # get the old value if nodeid: try: - existing = cl.get(nodeid, key) + 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(key): raise + if not properties.has_key(propname): + raise # if changed, set it if value != existing: - props[key] = value + props[propname] = value else: - props[key] = value + props[propname] = value # see if all the required properties have been supplied if required: diff --git a/roundup/templates/classic/html/issue.item b/roundup/templates/classic/html/issue.item index b795939..3215703 100644 --- a/roundup/templates/classic/html/issue.item +++ b/roundup/templates/classic/html/issue.item @@ -42,7 +42,8 @@ You are not allowed to view this page. Nosy List - +
@@ -114,13 +115,15 @@ You are not allowed to view this page. - - + +
Messages
MessageAuthorDateSummary
authordatedate summary + remove +
diff --git a/test/test_mailsplit.py b/test/test_mailsplit.py index ccfd84f..f4cef60 100644 --- a/test/test_mailsplit.py +++ b/test/test_mailsplit.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: test_mailsplit.py,v 1.11 2002-09-10 00:19:55 richard Exp $ +# $Id: test_mailsplit.py,v 1.12 2002-10-18 03:34:58 richard Exp $ import unittest, cStringIO @@ -208,6 +208,12 @@ userfoo@foo.com summary, content = parseContent(s, 0, 0) self.assertEqual(content, s) + def testMultilineSummary(self): + s = 'This is a long sentence that would normally\nbe split. More words.' + summary, content = parseContent(s, 0, 0) + self.assertEqual(summary, 'This is a long sentence that would ' + 'normally\nbe split.') + def suite(): return unittest.makeSuite(MailsplitTestCase, 'test') -- 2.30.2