Code

- added CGI :remove:<propname> and :add:<propname> which specify item ids to
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 18 Oct 2002 03:34:58 +0000 (03:34 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 18 Oct 2002 03:34:58 +0000 (03:34 +0000)
  remove / add in <propname> 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
doc/announcement.txt
frontends/ZRoundup/ZRoundup.py
roundup/cgi/client.py
roundup/templates/classic/html/issue.item
test/test_mailsplit.py

index ea5809c9fcc80ad0d40fdcd60f6670b95f10170a..dbe7c133aa9050b55eafa7fac38284c7e09cecec 100644 (file)
--- 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:<propname> and :add:<propname> which specify
-                  item ids to remove / add in <propname> 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
 ======= ========= =============================================================
 
index ed65f008baf07336aa4af9d68910a7ca3baa906f..213ddf6fbdc4681e5b64609db745cb25c5345446 100644 (file)
@@ -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)
index b0780ccf821781b2a289690437752ddd3a2cc0ee..bcde6b1b017b2e2fb859ed6a25967806376c3757 100644 (file)
@@ -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())
index 03eaf1102c14436c9cc85723a02b60bf95d037b9..10fd44897bd8e423e252cc1e8304f868725af3e4 100644 (file)
@@ -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:<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.
+
         '''
         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:<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'):
@@ -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:
index b795939f530149b3fa979d26343eec425007155d..3215703e28111ef2ecddec2dd7b18527f7b75870 100644 (file)
@@ -42,7 +42,8 @@ You are not allowed to view this page.
  <th nowrap>Nosy List</th>
  <td>
   <span tal:replace="structure context/nosy/field" />
-  <span tal:replace="structure python:db.user.classhelp('username,realname,address,phone')" />
+  <span tal:replace="structure
+python:db.user.classhelp('username,realname,address,phone')" /><br>
  </td>
 </tr>
 
@@ -114,13 +115,15 @@ You are not allowed to view this page.
 
  <table class="messages" tal:condition="context/messages">
   <tr><th colspan=4 class="header">Messages</th></tr>
-  <tr><th>Message</th><th>Author</th><th>Date</th><th>Summary</th></tr>
   <tr tal:repeat="msg context/messages/reverse">
    <td><a tal:attributes="href string:msg${msg/id}"
           tal:content="string:msg${msg/id}"></a></td>
    <td tal:content="msg/author">author</td>
-   <td tal:content="msg/date">date</td>
+   <td nowrap tal:content="msg/date/pretty">date</td>
    <td tal:content="msg/summary">summary</td>
+   <td>
+    <a tal:attributes="href string:?:remove:messages=${msg/id}&:action=edit">remove</a>
+   </td>
   </tr>
  </table>
 
index ccfd84f2f4dfe6582d3213d512e10e8050c6d8f4..f4cef60d9ad99446ba65ac3d5668ce18b17a7f17 100644 (file)
@@ -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')