Code

- registration is now a two-step process, with confirmation from the email
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Tue, 25 Feb 2003 10:19:32 +0000 (10:19 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Tue, 25 Feb 2003 10:19:32 +0000 (10:19 +0000)
  address supplied in the registration form
- fixed sf bug 687771 too (now handle all cases of @/:)

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

CHANGES.txt
TODO.txt
roundup/__init__.py
roundup/admin.py
roundup/backends/back_anydbm.py
roundup/backends/back_metakit.py
roundup/backends/rdbms_common.py
roundup/backends/sessions.py
roundup/cgi/client.py
roundup/cgi/templating.py
roundup/init.py

index eaacd1911b716790b0cae3d34fef937546844a36..2e9c214aef5e9f4a3a61476d4dcf77a1c9a6627d 100644 (file)
@@ -59,6 +59,8 @@ are given with the most recent entry first.
 - re-worked detectors initialisation - woohoo, no more cross-importing!
 - can now configure CC to author only for messages creating issues (sf
   feature 625808)
+- registration is now a two-step process, with confirmation from the email
+  address supplied in the registration form
 
 
 2003-??-?? 0.5.6
index 618d7300b7789f032c87023d210a7c7887153daa..3b995f2095a25682d9a43d5792810bff8e59930b 100644 (file)
--- a/TODO.txt
+++ b/TODO.txt
@@ -29,6 +29,7 @@ pending mailgw    Use in-reply-to for determining message lineage when subject
                   line lets us down
 pending mailgw    Allow different brackets delimiting [issueNNN] in Subject
 pending email     email sig could use a "remove me from this list"
+                  <URL>/<designator>?:remove:nosy=me
 pending project   switch to a Roundup instance for Roundup bug/feature tracking
 pending security  authenticate over a secure connection
 pending security  optionally auth with Basic HTTP auth instead of cookies
index e46226e61f87009c71e2687f09b93a94b0063ec7..e7d2da0fa178abae3d28d5421b893d31f6d7f206 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: __init__.py,v 1.19 2003-02-20 07:11:39 richard Exp $
+# $Id: __init__.py,v 1.20 2003-02-25 10:19:31 richard Exp $
 
 ''' Roundup - issue tracking for knowledge workers.
 
@@ -67,6 +67,6 @@ written by Ka-Ping Yee in the "doc" directory. If nothing else, it has a
 much prettier cake :)
 '''
 
-__version__ = '0.6.0'
+__version__ = '0.6.0pr1'
 
 # vim: set filetype=python ts=4 sw=4 et si
index e13ee775f5df944d10ad30cee6562822cd26f1a8..94e38462d4eb60dda9283f68ecf66d4893fdef73 100644 (file)
@@ -16,7 +16,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: admin.py,v 1.37 2003-02-15 23:19:01 kedder Exp $
+# $Id: admin.py,v 1.38 2003-02-25 10:19:31 richard Exp $
 
 '''Administration commands for maintaining Roundup trackers.
 '''
@@ -328,7 +328,8 @@ Command help:
         # XXX perform a unit test based on the user's selections
 
         # install!
-        init.install(tracker_home, template, backend)
+        init.install(tracker_home, template)
+        init.write_select_db(tracker_home, backend)
 
         print _('''
  You should now edit the tracker configuration file:
index ab7c06ac9cd0cbf39f7478e1e653b6bf1de103c1..71ffdb038f783f9ab94ca084038398ef43192bc9 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_anydbm.py,v 1.104 2003-02-18 01:57:38 richard Exp $
+#$Id: back_anydbm.py,v 1.105 2003-02-25 10:19:31 richard Exp $
 '''
 This module defines a backend that saves the hyperdatabase in a database
 chosen by anydbm. It is guaranteed to always be available in python
@@ -26,7 +26,7 @@ serious bugs, and is not available)
 import whichdb, anydbm, os, marshal, re, weakref, string, copy
 from roundup import hyperdb, date, password, roundupdb, security
 from blobfiles import FileStorage
-from sessions import Sessions
+from sessions import Sessions, OneTimeKeys
 from roundup.indexer import Indexer
 from roundup.backends import locking
 from roundup.hyperdb import String, Password, Date, Interval, Link, \
@@ -68,6 +68,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         self.transactions = []
         self.indexer = Indexer(self.dir)
         self.sessions = Sessions(self.config)
+        self.otks = OneTimeKeys(self.config)
         self.security = security.Security(self)
         # ensure files are group readable and writable
         os.umask(0002)
index 91a59343127189cc2ad17c778a286f3a5cccb4d0..234a817cec77f9511bfb18338a7e2faa9c4aa2f6 100755 (executable)
@@ -30,7 +30,7 @@
 '''
 from roundup import hyperdb, date, password, roundupdb, security
 import metakit
-from sessions import Sessions
+from sessions import Sessions, OneTimeKeys
 import re, marshal, os, sys, weakref, time, calendar
 from roundup import indexer
 import locking
@@ -60,6 +60,7 @@ class _Database(hyperdb.Database, roundupdb.Database):
         self._db = self.__open()
         self.indexer = Indexer(self.config.DATABASE, self._db)
         self.sessions = Sessions(self.config)
+        self.otks = OneTimeKeys(self.config)
         self.security = security.Security(self)
 
         os.umask(0002)
index e663a19207a942658210323826095888822f0f33..5b6aab436c20d1714c6ebaa583f34eb0cc992340 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: rdbms_common.py,v 1.34 2003-02-18 01:57:39 richard Exp $
+# $Id: rdbms_common.py,v 1.35 2003-02-25 10:19:32 richard Exp $
 ''' Relational database (SQL) backend common code.
 
 Basics:
@@ -33,7 +33,7 @@ from roundup.backends import locking
 # support
 from blobfiles import FileStorage
 from roundup.indexer import Indexer
-from sessions import Sessions
+from sessions import Sessions, OneTimeKeys
 
 # number of rows to keep in memory
 ROW_CACHE_SIZE = 100
@@ -53,6 +53,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         self.classes = {}
         self.indexer = Indexer(self.dir)
         self.sessions = Sessions(self.config)
+        self.otks = OneTimeKeys(self.config)
         self.security = security.Security(self)
 
         # additional transaction support for external files and the like
index 31c48c97d87244089bd6b4a91ddd6fef7d511500..9c5325a01445b184b23b5bb9bdc7a2ab1710d957 100644 (file)
@@ -1,15 +1,19 @@
-#$Id: sessions.py,v 1.3 2002-09-10 00:11:50 richard Exp $
+#$Id: sessions.py,v 1.4 2003-02-25 10:19:32 richard Exp $
 '''
 This module defines a very basic store that's used by the CGI interface
-to store session information.
+to store session and one-time-key information.
+
+Yes, it's called "sessions" - because originally it only defined a session
+class. It's now also used for One Time Key handling too.
+
 '''
 
 import anydbm, whichdb, os, marshal
 
-class Sessions:
-    ''' Back onto an anydbm store.
+class BasicDatabase:
+    ''' Provide a nice encapsulation of an anydbm store.
 
-        Keys are session id strings, values are marshalled data.
+        Keys are id strings, values are automatically marshalled data.
     '''
     def __init__(self, config):
         self.config = config
@@ -18,7 +22,7 @@ class Sessions:
         os.umask(0002)
 
     def clear(self):
-        path = os.path.join(self.dir, 'sessions')
+        path = os.path.join(self.dir, self.name)
         if os.path.exists(path):
             os.remove(path)
         elif os.path.exists(path+'.db'):    # dbm appends .db
@@ -38,26 +42,33 @@ class Sessions:
             db_type = 'dbm'
         return db_type
 
-    def get(self, sessionid, value):
+    def get(self, infoid, value):
         db = self.opendb('c')
         try:
-            if db.has_key(sessionid):
-                values = marshal.loads(db[sessionid])
+            if db.has_key(infoid):
+                values = marshal.loads(db[infoid])
             else:
                 return None
             return values.get(value, None)
         finally:
             db.close()
 
-    def set(self, sessionid, **newvalues):
+    def getall(self, infoid):
+        db = self.opendb('c')
+        try:
+            return marshal.loads(db[infoid])
+        finally:
+            db.close()
+
+    def set(self, infoid, **newvalues):
         db = self.opendb('c')
         try:
-            if db.has_key(sessionid):
-                values = marshal.loads(db[sessionid])
+            if db.has_key(infoid):
+                values = marshal.loads(db[infoid])
             else:
                 values = {}
             values.update(newvalues)
-            db[sessionid] = marshal.dumps(values)
+            db[infoid] = marshal.dumps(values)
         finally:
             db.close()
 
@@ -68,11 +79,11 @@ class Sessions:
         finally:
             db.close()
 
-    def destroy(self, sessionid):
+    def destroy(self, infoid):
         db = self.opendb('c')
         try:
-            if db.has_key(sessionid):
-                del db[sessionid]
+            if db.has_key(infoid):
+                del db[infoid]
         finally:
             db.close()
 
@@ -81,7 +92,7 @@ class Sessions:
            eccentricities.
         '''
         # figure the class db type
-        path = os.path.join(os.getcwd(), self.dir, 'sessions')
+        path = os.path.join(os.getcwd(), self.dir, self.name)
         db_type = self.determine_db_type(path)
 
         # new database? let anydbm pick the best dbm
@@ -94,3 +105,10 @@ class Sessions:
 
     def commit(self):
         pass
+
+class Sessions(BasicDatabase):
+    name = 'sessions'
+
+class OneTimeKeys(BasicDatabase):
+    name = 'otks'
+
index 1f23faea242d87b281c686f1b7db7b4d5eda0b2e..435f7334947f37ed4e56ef5b651d5660864e20e2 100644 (file)
@@ -1,19 +1,18 @@
-# $Id: client.py,v 1.96 2003-02-20 07:13:14 richard Exp $
+# $Id: client.py,v 1.97 2003-02-25 10:19:32 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
 """
 
 import os, os.path, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib
-import binascii, Cookie, time, random
+import binascii, Cookie, time, random, MimeWriter, smtplib, socket, quopri
 
 from roundup import roundupdb, date, hyperdb, password
 from roundup.i18n import _
-
 from roundup.cgi.templating import Templates, HTMLRequest, NoTemplate
 from roundup.cgi import cgitb
-
 from roundup.cgi.PageTemplates import PageTemplate
+from roundup.rfc2822 import encode_header
 
 class HTTPException(Exception):
       pass
@@ -104,6 +103,8 @@ class Client:
     FV_OK_MESSAGE = re.compile(r'[@:]ok_message')
     FV_ERROR_MESSAGE = re.compile(r'[@:]error_message')
 
+    FV_QUERYNAME = re.compile(r'[@:]queryname')
+
     # edit form variable handling (see unit tests)
     FV_LABELS = r'''
        ^(
@@ -236,8 +237,8 @@ class Client:
         except SendStaticFile, file:
             self.serve_static_file(str(file))
         except Unauthorised, message:
-            self.classname=None
-            self.template=''
+            self.classname = None
+            self.template = ''
             self.error_message.append(message)
             self.write(self.renderContext())
         except NotFound:
@@ -275,7 +276,7 @@ class Client:
         sessions = self.db.sessions
 
         # look up the user session cookie
-        cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
+        cookie = Cookie.SimpleCookie(self.env.get('HTTP_COOKIE', ''))
         user = 'anonymous'
 
         # bump the "revision" of the cookie since the format changed
@@ -454,6 +455,7 @@ class Client:
         ('editCSV',  'editCSVAction'),
         ('new',      'newItemAction'),
         ('register', 'registerAction'),
+        ('confrego', 'confRegoAction'),
         ('login',    'loginAction'),
         ('logout',   'logout_action'),
         ('search',   'searchAction'),
@@ -461,7 +463,7 @@ class Client:
         ('show',     'showAction'),
     )
     def handle_action(self):
-        ''' Determine whether there should be an _action called.
+        ''' Determine whether there should be an Action called.
 
             The action is defined by the form variable :action which
             identifies the method on this object to call. The four basic
@@ -469,33 +471,31 @@ class Client:
              "edit"      -> self.editItemAction
              "new"       -> self.newItemAction
              "register"  -> self.registerAction
+             "confrego"  -> self.confRegoAction
              "login"     -> self.loginAction
              "logout"    -> self.logout_action
              "search"    -> self.searchAction
              "retire"    -> self.retireAction
         '''
-        if not self.form.has_key(':action'):
+        if self.form.has_key(':action'):
+            action = self.form[':action'].value.lower()
+        elif self.form.has_key('@action'):
+            action = self.form['@action'].value.lower()
+        else:
             return None
         try:
             # get the action, validate it
-            action = self.form[':action'].value
             for name, method in self.actions:
                 if name == action:
                     break
             else:
                 raise ValueError, 'No such action "%s"'%action
-
             # call the mapped action
             getattr(self, method)()
         except Redirect:
             raise
         except Unauthorised:
             raise
-        except:
-            self.db.rollback()
-            s = StringIO.StringIO()
-            traceback.print_exc(None, s)
-            self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
 
     def write(self, content):
         if not self.headers_done:
@@ -651,18 +651,16 @@ class Client:
         # Let the user know what's going on
         self.ok_message.append(_('You are logged out'))
 
+    chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
     def registerAction(self):
         '''Attempt to create a new user based on the contents of the form
         and then set the cookie.
 
         return 1 on successful login
         '''
-        # create the new user
-        cl = self.db.user
-
         # parse the props from the form
         try:
-            props = self.parsePropsFromForm()
+            props = self.parsePropsFromForm()[0][('user', None)]
         except (ValueError, KeyError), message:
             self.error_message.append(_('Error: ') + str(message))
             return
@@ -671,19 +669,125 @@ class Client:
         if not self.registerPermission(props):
             raise Unauthorised, _("You do not have permission to register")
 
+        try:
+            self.db.user.lookup(props['username'])
+            self.error_message.append('Error: A user with the username "%s" '
+                'already exists'%props['username'])
+            return
+        except KeyError:
+            pass
+
+        # generate the one-time-key and store the props for later
+        otk = ''.join([random.choice(self.chars) for x in range(32)])
+        for propname, proptype in self.db.user.getprops().items():
+            value = props.get(propname, None)
+            if value is None:
+                pass
+            elif isinstance(proptype, hyperdb.Date):
+                props[propname] = str(value)
+            elif isinstance(proptype, hyperdb.Interval):
+                props[propname] = str(value)
+            elif isinstance(proptype, hyperdb.Password):
+                props[propname] = str(value)
+        self.db.otks.set(otk, **props)
+
+        # send email to the user's email address
+        message = StringIO.StringIO()
+        writer = MimeWriter.MimeWriter(message)
+        tracker_name = self.db.config.TRACKER_NAME
+        s = 'Complete your registration to %s'%tracker_name
+        writer.addheader('Subject', encode_header(s))
+        writer.addheader('To', props['address'])
+        writer.addheader('From', roundupdb.straddr((tracker_name,
+            self.db.config.ADMIN_EMAIL)))
+        writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000",
+            time.gmtime()))
+        # add a uniquely Roundup header to help filtering
+        writer.addheader('X-Roundup-Name', tracker_name)
+        # avoid email loops
+        writer.addheader('X-Roundup-Loop', 'hello')
+        writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
+        body = writer.startbody('text/plain; charset=utf-8')
+
+        # message body, encoded quoted-printable
+        content = StringIO.StringIO('''
+To complete your registration of the user "%(name)s" with %(tracker)s,
+please visit the following URL:
+
+   http://localhost:8001/test/?@action=confrego&otk=%(otk)s
+'''%{'name': props['username'], 'tracker': tracker_name, 'url': self.base,
+        'otk': otk})
+        quopri.encode(content, body, 0)
+
+        # now try to send the message
+        try:
+            # send the message as admin so bounces are sent there
+            # instead of to roundup
+            smtp = smtplib.SMTP(self.db.config.MAILHOST)
+            smtp.sendmail(self.db.config.ADMIN_EMAIL, [props['address']],
+                message.getvalue())
+        except socket.error, value:
+            self.error_message.append("Error: couldn't send "
+                "confirmation email: mailhost %s"%value)
+            return
+        except smtplib.SMTPException, value:
+            self.error_message.append("Error: couldn't send "
+                "confirmation email: %s"%value)
+            return
+
+        # commit changes to the database
+        self.db.commit()
+
+        # redirect to the "you're almost there" page
+        raise Redirect, '%s?:template=rego_step1_done'%self.base
+
+    def registerPermission(self, props):
+        ''' Determine whether the user has permission to register
+
+            Base behaviour is to check the user has "Web Registration".
+        '''
+        # registration isn't allowed to supply roles
+        if props.has_key('roles'):
+            return 0
+        if self.db.security.hasPermission('Web Registration', self.userid):
+            return 1
+        return 0
+
+    def confRegoAction(self):
+        ''' Grab the OTK, use it to load up the new user details
+        '''
+        # pull the rego information out of the otk database
+        otk = self.form['otk'].value
+        props = self.db.otks.getall(otk)
+        for propname, proptype in self.db.user.getprops().items():
+            value = props.get(propname, None)
+            if value is None:
+                pass
+            elif isinstance(proptype, hyperdb.Date):
+                props[propname] = date.Date(value)
+            elif isinstance(proptype, hyperdb.Interval):
+                props[propname] = date.Interval(value)
+            elif isinstance(proptype, hyperdb.Password):
+                props[propname] = password.Password()
+                props[propname].unpack(value)
+
         # re-open the database as "admin"
         if self.user != 'admin':
             self.opendb('admin')
-            
+
         # create the new user
         cl = self.db.user
-        try:
+# XXX we need to make the "default" page be able to display errors!
+#        try:
+        if 1:
             props['roles'] = self.instance.config.NEW_WEB_USER_ROLES
-            self.userid = cl.create(**props['user'])
+            self.userid = cl.create(**props)
+            # clear the props from the otk database
+            self.db.otks.destroy(otk)
             self.db.commit()
-        except (ValueError, KeyError), message:
-            self.error_message.append(message)
-            return
+#        except (ValueError, KeyError), message:
+#            self.error_message.append(str(message))
+#            return
 
         # log the new user in
         self.user = cl.get(self.userid, 'username')
@@ -702,20 +806,8 @@ class Client:
         message = _('You are now registered, welcome!')
 
         # redirect to the item's edit page
-        raise Redirect, '%s%s%s?+ok_message=%s'%(
-            self.base, self.classname, self.userid,  urllib.quote(message))
-
-    def registerPermission(self, props):
-        ''' Determine whether the user has permission to register
-
-            Base behaviour is to check the user has "Web Registration".
-        '''
-        # registration isn't allowed to supply roles
-        if props.has_key('roles'):
-            return 0
-        if self.db.security.hasPermission('Web Registration', self.userid):
-            return 1
-        return 0
+        raise Redirect, '%suser%s?@ok_message=%s'%(
+            self.base, self.userid,  urllib.quote(message))
 
     def editItemAction(self):
         ''' Perform an edit of an item in the database.
@@ -723,16 +815,18 @@ class Client:
            See parsePropsFromForm and _editnodes for special variables
         '''
         # parse the props from the form
-        if 1:
+# XXX reinstate exception handling
 #        try:
+        if 1:
             props, links = self.parsePropsFromForm()
 #        except (ValueError, KeyError), message:
 #            self.error_message.append(_('Error: ') + str(message))
 #            return
 
         # handle the props
-        if 1:
+# XXX reinstate exception handling
 #        try:
+        if 1:
             message = self._editnodes(props, links)
 #        except (ValueError, KeyError, IndexError), message:
 #            self.error_message.append(_('Error: ') + str(message))
@@ -832,8 +926,6 @@ class Client:
         ''' Use the props in all_props to perform edit and creation, then
             use the link specs in all_links to do linking.
         '''
-#        print '='*75
-#        print 'ALL_PROPS', all_props
         # figure dependencies and re-work links
         deps = {}
         links = {}
@@ -848,10 +940,6 @@ class Client:
                 deps.setdefault((cn, nodeid), []).append(value)
                 links.setdefault(value, []).append((cn, nodeid, propname))
 
-#        print '*'*75
-#        print 'LINKS', links
-#        print 'DEPS', deps
-
         # figure chained dependencies ordering
         order = []
         done = {}
@@ -862,12 +950,10 @@ class Client:
                 if done.has_key(needed):
                     continue
                 tlist = deps.get(needed, [])
-#                print 'SOLVING', needed, tlist
                 for target in tlist:
                     if not done.has_key(target):
                         break
                 else:
-#                    print 'DONE', needed
                     done[needed] = 1
                     order.append(needed)
                     change = 1
@@ -1060,10 +1146,16 @@ class Client:
                 _('You do not have permission to search %s' %self.classname))
 
         # add a faked :filter form variable for each filtering prop
-# XXX migrate to new : @ + 
         props = self.db.classes[self.classname].getprops()
+        queryname = ''
         for key in self.form.keys():
-            if not props.has_key(key): continue
+            # special vars
+            if self.FV_QUERYNAME.match(key):
+                queryname = self.form[key].value.strip()
+                continue
+
+            if not props.has_key(key):
+                continue
             if isinstance(self.form[key], type([])):
                 # search for at least one entry which is not empty
                 for minifield in self.form[key]:
@@ -1073,32 +1165,30 @@ class Client:
                     continue
             else:
                 if not self.form[key].value: continue
-            self.form.value.append(cgi.MiniFieldStorage(':filter', key))
+            self.form.value.append(cgi.MiniFieldStorage('@filter', key))
 
         # handle saving the query params
-        if self.form.has_key(':queryname'):
-            queryname = self.form[':queryname'].value.strip()
-            if queryname:
-                # parse the environment and figure what the query _is_
-                req = HTMLRequest(self)
-                url = req.indexargs_href('', {})
-
-                # handle editing an existing query
-                try:
-                    qid = self.db.query.lookup(queryname)
-                    self.db.query.set(qid, klass=self.classname, url=url)
-                except KeyError:
-                    # create a query
-                    qid = self.db.query.create(name=queryname,
-                        klass=self.classname, url=url)
+        if queryname:
+            # parse the environment and figure what the query _is_
+            req = HTMLRequest(self)
+            url = req.indexargs_href('', {})
+
+            # handle editing an existing query
+            try:
+                qid = self.db.query.lookup(queryname)
+                self.db.query.set(qid, klass=self.classname, url=url)
+            except KeyError:
+                # create a query
+                qid = self.db.query.create(name=queryname,
+                    klass=self.classname, url=url)
 
-                    # and add it to the user's query multilink
-                    queries = self.db.user.get(self.userid, 'queries')
-                    queries.append(qid)
-                    self.db.user.set(self.userid, queries=queries)
+                # and add it to the user's query multilink
+                queries = self.db.user.get(self.userid, 'queries')
+                queries.append(qid)
+                self.db.user.set(self.userid, queries=queries)
 
-                # commit the query change to the database
-                self.db.commit()
+            # commit the query change to the database
+            self.db.commit()
 
     def searchPermission(self):
         ''' Determine whether the user has permission to search this class.
@@ -1152,12 +1242,18 @@ class Client:
         return 1
 
 
-    def showAction(self):
-        ''' Show a node
+    def showAction(self, typere=re.compile('[@:]type'),
+            numre=re.compile('[@:]number')):
+        ''' Show a node of a particular class/id
         '''
-# XXX allow : @ +
-        t = self.form[':type'].value
-        n = self.form[':number'].value
+        t = n = ''
+        for key in self.form.keys():
+            if typere.match(key):
+                t = self.form[key].value.strip()
+            elif numre.match(key):
+                n = self.form[key].value.strip()
+        if not t:
+            raise ValueError, 'Invalid %s number'%t
         url = '%s%s%s'%(self.db.config.TRACKER_WEB, t, n)
         raise Redirect, url
 
index 3e174c1fdde2a5abe921a0cedacbf66f6d54a410..dff1bbcbe405b5cdb409b5e20ddc7d88918a0756 100644 (file)
@@ -1568,37 +1568,48 @@ env: %(env)s
         return '\n'.join(l)
 
     def indexargs_url(self, url, args):
-        ''' embed the current index args in a URL '''
+        ''' Embed the current index args in a URL
+        '''
         sc = self.special_char
         l = ['%s=%s'%(k,v) for k,v in args.items()]
-        if self.columns and not args.has_key(':columns'):
+
+        # pull out the special values (prefixed by @ or :)
+        specials = {}
+        for key in args.keys():
+            if key[0] in '@:':
+                specials[key[1:]] = args[key]
+
+        # ok, now handle the specials we received in the request
+        if self.columns and not specials.has_key('columns'):
             l.append(sc+'columns=%s'%(','.join(self.columns)))
-        if self.sort[1] is not None and not args.has_key(':sort'):
+        if self.sort[1] is not None and not specials.has_key('sort'):
             if self.sort[0] == '-':
                 val = '-'+self.sort[1]
             else:
                 val = self.sort[1]
             l.append(sc+'sort=%s'%val)
-        if self.group[1] is not None and not args.has_key(':group'):
+        if self.group[1] is not None and not specials.has_key('group'):
             if self.group[0] == '-':
                 val = '-'+self.group[1]
             else:
                 val = self.group[1]
             l.append(sc+'group=%s'%val)
-        if self.filter and not args.has_key(':filter'):
+        if self.filter and not specials.has_key('filter'):
             l.append(sc+'filter=%s'%(','.join(self.filter)))
+        if self.search_text and not specials.has_key('search_text'):
+            l.append(sc+'search_text=%s'%self.search_text)
+        if not specials.has_key('pagesize'):
+            l.append(sc+'pagesize=%s'%self.pagesize)
+        if not specials.has_key('startwith'):
+            l.append(sc+'startwith=%s'%self.startwith)
+
+        # finally, the remainder of the filter args in the request
         for k,v in self.filterspec.items():
             if not args.has_key(k):
                 if type(v) == type([]):
                     l.append('%s=%s'%(k, ','.join(v)))
                 else:
                     l.append('%s=%s'%(k, v))
-        if self.search_text and not args.has_key(':search_text'):
-            l.append(sc+'search_text=%s'%self.search_text)
-        if not args.has_key(':pagesize'):
-            l.append(sc+'pagesize=%s'%self.pagesize)
-        if not args.has_key(':startwith'):
-            l.append(sc+'startwith=%s'%self.startwith)
         return '%s?%s'%(url, '&'.join(l))
     indexargs_href = indexargs_url
 
index b0e2b5b8bc03f0e38078dc3276f84f76e3541347..9a7ffc51241d5c8bb94459809671d1963bee99fe 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: init.py,v 1.24 2002-09-10 12:44:42 richard Exp $
+# $Id: init.py,v 1.25 2003-02-25 10:19:31 richard Exp $
 
 __doc__ = """
 Init (create) a roundup instance.
@@ -37,7 +37,6 @@ def copytree(src, dst, symlinks=0):
     links are copied.
 
     This was copied from shutil.py in std lib.
-
     """
     names = os.listdir(src)
     try:
@@ -55,7 +54,7 @@ def copytree(src, dst, symlinks=0):
         else:
             install_util.copyDigestedFile(srcname, dstname)
 
-def install(instance_home, template, backend):
+def install(instance_home, template):
     '''Install an instance using the named template and backend.
 
     instance_home - the directory to place the instance data in
@@ -97,6 +96,10 @@ def install(instance_home, template, backend):
 
     builder.installHtmlBase(template_name, instance_home)
 
+
+def write_select_db(instance_home, backend):
+    ''' Write the file that selects the backend for the tracker
+    '''
     # now select database
     db = '''# WARNING: DO NOT EDIT THIS FILE!!!
 from roundup.backends.back_%s import Database, Class, FileClass, IssueClass