Code

Added reindex command to roundup-admin.
[roundup.git] / roundup / cgi_client.py
index 164a252d0e696f534e39e5f1489202ad7b011e7d..b471c1b206be9611455fc61719447ae5f0440447 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: cgi_client.py,v 1.105 2002-02-20 05:52:10 richard Exp $
+# $Id: cgi_client.py,v 1.134 2002-07-09 04:19:09 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
 """
 
-import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
+import os, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib
 import binascii, Cookie, time, random
 
 import roundupdb, htmltemplate, date, hyperdb, password
@@ -47,11 +47,19 @@ class Client:
     '''
 
     def __init__(self, instance, request, env, form=None):
+        hyperdb.traceMark()
         self.instance = instance
         self.request = request
         self.env = env
         self.path = env['PATH_INFO']
         self.split_path = self.path.split('/')
+        self.instance_path_name = env['INSTANCE_NAME']
+        url = self.env['SCRIPT_NAME'] + '/'
+        machine = self.env['SERVER_NAME']
+        port = self.env['SERVER_PORT']
+        if port != '80': machine = machine + ':' + port
+        self.base = urlparse.urlunparse(('http', env['HTTP_HOST'], url,
+            None, None, None))
 
         if form is None:
             self.form = cgi.FieldStorage(environ=env)
@@ -65,11 +73,22 @@ class Client:
             self.debug = 0
 
     def getuid(self):
-        return self.db.user.lookup(self.user)
+        try:
+            return self.db.user.lookup(self.user)
+        except KeyError:
+            if self.user is None:
+                # user is not logged in and username 'anonymous' doesn't
+                # exist in the database
+                err = _('anonymous users have read-only access only')
+            else:
+                err = _("sanity check: unknown user name `%s'")%self.user
+            raise Unauthorised, errmsg
 
-    def header(self, headers={'Content-Type':'text/html'}):
+    def header(self, headers=None):
         '''Put up the appropriate header.
         '''
+        if headers is None:
+            headers = {'Content-Type':'text/html'}
         if not headers.has_key('Content-Type'):
             headers['Content-Type'] = 'text/html'
         self.request.send_response(200)
@@ -80,7 +99,7 @@ class Client:
         if self.debug:
             self.headers_sent = headers
 
-    single_submit_script = '''
+    global_javascript = '''
 <script language="javascript">
 submitted = false;
 function submit_once() {
@@ -91,62 +110,150 @@ function submit_once() {
     submitted = true;
     return 1;
 }
+
+function help_window(helpurl, width, height) {
+    HelpWin = window.open('%(base)s%(instance_path_name)s/' + helpurl, 'HelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
+}
+
 </script>
 '''
+    def make_index_link(self, name):
+        '''Turn a configuration entry into a hyperlink...
+        '''
+        # get the link label and spec
+        spec = getattr(self.instance, name+'_INDEX')
+
+        d = {}
+        d[':sort'] = ','.join(map(urllib.quote, spec['SORT']))
+        d[':group'] = ','.join(map(urllib.quote, spec['GROUP']))
+        d[':filter'] = ','.join(map(urllib.quote, spec['FILTER']))
+        d[':columns'] = ','.join(map(urllib.quote, spec['COLUMNS']))
+        d[':pagesize'] = spec.get('PAGESIZE','50')
+
+        # snarf the filterspec
+        filterspec = spec['FILTERSPEC'].copy()
+
+        # now format the filterspec
+        for k, l in filterspec.items():
+            # fix up the CURRENT USER if needed (handle None too since that's
+            # the old flag value)
+            if l in (None, 'CURRENT USER'):
+                if not self.user:
+                    continue
+                l = [self.db.user.lookup(self.user)]
+
+            # add
+            d[urllib.quote(k)] = ','.join(map(urllib.quote, l))
+
+        # finally, format the URL
+        return '<a href="%s?%s">%s</a>'%(spec['CLASS'],
+            '&'.join([k+'='+v for k,v in d.items()]), spec['LABEL'])
+
 
     def pagehead(self, title, message=None):
-        url = self.env['SCRIPT_NAME'] + '/'
-        machine = self.env['SERVER_NAME']
-        port = self.env['SERVER_PORT']
-        if port != '80': machine = machine + ':' + port
-        base = urlparse.urlunparse(('http', machine, url, None, None, None))
+        '''Display the page heading, with information about the tracker and
+            links to more information
+        '''
+
+        # include any important message
         if message is not None:
             message = _('<div class="system-msg">%(message)s</div>')%locals()
         else:
             message = ''
+
+        # style sheet (CSS)
         style = open(os.path.join(self.instance.TEMPLATES, 'style.css')).read()
+
+        # figure who the user is
         user_name = self.user or ''
-        if self.user == 'admin':
-            admin_links = _(' | <a href="list_classes">Class List</a>' \
-                          ' | <a href="user">User List</a>' \
-                          ' | <a href="newuser">Add User</a>')
-        else:
-            admin_links = ''
-        if self.user not in (None, 'anonymous'):
+        if user_name not in ('', 'anonymous'):
             userid = self.db.user.lookup(self.user)
+        else:
+            userid = None
+
+        # figure all the header links
+        if hasattr(self.instance, 'HEADER_INDEX_LINKS'):
+            links = []
+            for name in self.instance.HEADER_INDEX_LINKS:
+                spec = getattr(self.instance, name + '_INDEX')
+                # skip if we need to fill in the logged-in user id there's
+                # no user logged in
+                if (spec['FILTERSPEC'].has_key('assignedto') and
+                        spec['FILTERSPEC']['assignedto'] in ('CURRENT USER',
+                        None) and userid is None):
+                    continue
+                links.append(self.make_index_link(name))
+        else:
+            # no config spec - hard-code
+            links = [
+                _('All <a href="issue?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>'),
+                _('Unassigned <a href="issue?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>')
+            ]
+
+        # if they're logged in, include links to their information, and the
+        # ability to add an issue
+        if user_name not in ('', 'anonymous'):
             user_info = _('''
-<a href="issue?assignedto=%(userid)s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=-activity&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">My Issues</a> |
 <a href="user%(userid)s">My Details</a> | <a href="logout">Logout</a>
 ''')%locals()
+
+            # figure the "add class" links
+            if hasattr(self.instance, 'HEADER_ADD_LINKS'):
+                classes = self.instance.HEADER_ADD_LINKS
+            else:
+                classes = ['issue']
+            l = []
+            for class_name in classes:
+                cap_class = class_name.capitalize()
+                links.append(_('Add <a href="new%(class_name)s">'
+                    '%(cap_class)s</a>')%locals())
+
+            # if there's no config header link spec, force a user link here
+            if not hasattr(self.instance, 'HEADER_INDEX_LINKS'):
+                links.append(_('<a href="issue?assignedto=%(userid)s&status=-1,unread,chatting,open,pending&:filter=status,resolution,assignedto&:sort=-activity&:columns=id,activity,status,resolution,title,creator&:group=type&show_customization=1">My Issues</a>')%locals())
         else:
             user_info = _('<a href="login">Login</a>')
-        if self.user is not None:
-            add_links = _('''
-| Add
-<a href="newissue">Issue</a>
-''')
-        else:
             add_links = ''
-        single_submit_script = self.single_submit_script
+
+        # if the user is admin, include admin links
+        admin_links = ''
+        if user_name == 'admin':
+            links.append(_('<a href="list_classes">Class List</a>'))
+            links.append(_('<a href="user">User List</a>'))
+            links.append(_('<a href="newuser">Add User</a>'))
+
+        # add the search links
+        if hasattr(self.instance, 'HEADER_SEARCH_LINKS'):
+            classes = self.instance.HEADER_SEARCH_LINKS
+        else:
+            classes = ['issue']
+        l = []
+        for class_name in classes:
+            cap_class = class_name.capitalize()
+            links.append(_('Search <a href="search%(class_name)s">'
+                '%(cap_class)s</a>')%locals())
+
+        # now we have all the links, join 'em
+        links = '\n | '.join(links)
+
+        # include the javascript bit
+        global_javascript = self.global_javascript%self.__dict__
+
+        # finally, format the header
         self.write(_('''<html><head>
 <title>%(title)s</title>
 <style type="text/css">%(style)s</style>
 </head>
-%(single_submit_script)s
+%(global_javascript)s
 <body bgcolor=#ffffff>
 %(message)s
 <table width=100%% border=0 cellspacing=0 cellpadding=2>
 <tr class="location-bar"><td><big><strong>%(title)s</strong></big></td>
 <td align=right valign=bottom>%(user_name)s</td></tr>
 <tr class="location-bar">
-<td align=left>All
-<a href="issue?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>
-| Unassigned
-<a href="issue?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>
-%(add_links)s
-%(admin_links)s</td>
+<td align=left>%(links)s</td>
 <td align=right>%(user_info)s</td>
-</table>
+</table><br>
 ''')%locals())
 
     def pagefoot(self):
@@ -195,34 +302,67 @@ function submit_once() {
             return arg.value.split(',')
         return []
 
+    def index_sort(self):
+        # first try query string
+        x = self.index_arg(':sort')
+        if x:
+            return x
+        # nope - get the specs out of the form
+        specs = []
+        for colnm in self.db.getclass(self.classname).getprops().keys():
+            desc = ''
+            try:
+                spec = self.form[':%s_ss' % colnm]
+            except KeyError:
+                continue
+            spec = spec.value
+            if spec:
+                if spec[-1] == '-':
+                    desc='-'
+                    spec = spec[0]
+                specs.append((int(spec), colnm, desc))
+        specs.sort()
+        x = []
+        for _, colnm, desc in specs:
+            x.append('%s%s' % (desc, colnm))
+        return x
+    
     def index_filterspec(self, filter):
         ''' pull the index filter spec from the form
 
         Links and multilinks want to be lists - the rest are straight
         strings.
         '''
-        props = self.db.classes[self.classname].getprops()
-        # all the form args not starting with ':' are filters
         filterspec = {}
-        for key in self.form.keys():
-            if key[0] == ':': continue
-            if not props.has_key(key): continue
-            if key not in filter: continue
-            prop = props[key]
-            value = self.form[key]
-            if (isinstance(prop, hyperdb.Link) or
-                    isinstance(prop, hyperdb.Multilink)):
-                if type(value) == type([]):
-                    value = [arg.value for arg in value]
+        props = self.db.classes[self.classname].getprops()
+        for colnm in filter:
+            widget = ':%s_fs' % colnm
+            try:
+                val = self.form[widget]
+            except KeyError:
+                try:
+                    val = self.form[colnm]
+                except KeyError:
+                    # they checked the filter box but didn't enter a value
+                    continue
+            propdescr = props.get(colnm, None)
+            if propdescr is None:
+                print "huh? %r is in filter & form, but not in Class!" % colnm
+                raise "butthead programmer"
+            if (isinstance(propdescr, hyperdb.Link) or
+                isinstance(propdescr, hyperdb.Multilink)):
+                if type(val) == type([]):
+                    val = [arg.value for arg in val]
                 else:
-                    value = value.value.split(',')
-                l = filterspec.get(key, [])
-                l = l + value
-                filterspec[key] = l
+                    val = val.value.split(',')
+                l = filterspec.get(colnm, [])
+                l = l + val
+                filterspec[colnm] = l
             else:
-                filterspec[key] = value.value
+                filterspec[colnm] = val.value
+            
         return filterspec
-
+    
     def customization_widget(self):
         ''' The customization widget is visible by default. The widget
             visibility is remembered by show_customization.  Visibility
@@ -238,41 +378,91 @@ function submit_once() {
             
         return visible
 
+    # TODO: make this go away some day...
     default_index_sort = ['-activity']
     default_index_group = ['priority']
     default_index_filter = ['status']
     default_index_columns = ['id','activity','title','status','assignedto']
     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
-    def index(self):
-        ''' put up an index
-        '''
-        self.classname = 'issue'
+    default_pagesize = '50'
+
+    def _get_customisation_info(self):
         # see if the web has supplied us with any customisation info
         defaults = 1
-        for key in ':sort', ':group', ':filter', ':columns':
+        for key in ':sort', ':group', ':filter', ':columns', ':pagesize':
             if self.form.has_key(key):
                 defaults = 0
                 break
         if defaults:
-            # no info supplied - use the defaults
-            sort = self.default_index_sort
-            group = self.default_index_group
-            filter = self.default_index_filter
-            columns = self.default_index_columns
-            filterspec = self.default_index_filterspec
+            # try the instance config first
+            if hasattr(self.instance, 'DEFAULT_INDEX'):
+                d = self.instance.DEFAULT_INDEX
+                self.classname = d['CLASS']
+                sort = d['SORT']
+                group = d['GROUP']
+                filter = d['FILTER']
+                columns = d['COLUMNS']
+                filterspec = d['FILTERSPEC']
+                pagesize = d.get('PAGESIZE', '50')
+
+            else:
+                # nope - fall back on the old way of doing it
+                self.classname = 'issue'
+                sort = self.default_index_sort
+                group = self.default_index_group
+                filter = self.default_index_filter
+                columns = self.default_index_columns
+                filterspec = self.default_index_filterspec
+                pagesize = self.default_pagesize
         else:
-            sort = self.index_arg(':sort')
-            group = self.index_arg(':group')
-            filter = self.index_arg(':filter')
-            columns = self.index_arg(':columns')
-            filterspec = self.index_filterspec(filter)
+            # make list() extract the info from the CGI environ
+            self.classname = 'issue'
+            sort = group = filter = columns = filterspec = pagesize = None
+        return columns, filter, group, sort, filterspec, pagesize
+
+    def index(self):
+        ''' put up an index - no class specified
+        '''
+        columns, filter, group, sort, filterspec, pagesize = \
+            self._get_customisation_info()
         return self.list(columns=columns, filter=filter, group=group,
-            sort=sort, filterspec=filterspec)
+            sort=sort, filterspec=filterspec, pagesize=pagesize)
+
+    def searchnode(self):
+        columns, filter, group, sort, filterspec, pagesize = \
+            self._get_customisation_info()
+##        show_nodes = 1
+##        if len(self.form.keys()) == 0:
+##            # get the default search filters from instance_config
+##            if hasattr(self.instance, 'SEARCH_FILTERS'):
+##                for f in self.instance.SEARCH_FILTERS:
+##                    spec = getattr(self.instance, f)
+##                    if spec['CLASS'] == self.classname:
+##                        filter = spec['FILTER']
+##                
+##            show_nodes = 0
+##            show_customization = 1
+##        return self.list(columns=columns, filter=filter, group=group,
+##            sort=sort, filterspec=filterspec,
+##            show_customization=show_customization, show_nodes=show_nodes,
+##            pagesize=pagesize)
+        cn = self.classname
+        self.pagehead(_('%(instancename)s: Index of %(classname)s')%{
+            'classname': cn, 'instancename': self.instance.INSTANCE_NAME})
+        index = htmltemplate.IndexTemplate(self, self.instance.TEMPLATES, cn)
+        self.write('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
+        all_columns = self.db.getclass(cn).getprops().keys()
+        all_columns.sort()
+        index.filter_section('', filter, columns, group, all_columns, sort,
+                             filterspec, pagesize, 0)
+        self.pagefoot()
+        index.db = index.cl = index.properties = None
+        index.clear()
 
     # XXX deviates from spec - loses the '+' (that's a reserved character
     # in URLS
     def list(self, sort=None, group=None, filter=None, columns=None,
-            filterspec=None, show_customization=None):
+            filterspec=None, show_customization=None, show_nodes=1, pagesize=None):
         ''' call the template index with the args
 
             :sort    - sort by prop name, optionally preceeded with '-'
@@ -289,18 +479,33 @@ function submit_once() {
         cl = self.db.classes[cn]
         self.pagehead(_('%(instancename)s: Index of %(classname)s')%{
             'classname': cn, 'instancename': self.instance.INSTANCE_NAME})
-        if sort is None: sort = self.index_arg(':sort')
+        if sort is None: sort = self.index_sort()
         if group is None: group = self.index_arg(':group')
         if filter is None: filter = self.index_arg(':filter')
         if columns is None: columns = self.index_arg(':columns')
         if filterspec is None: filterspec = self.index_filterspec(filter)
         if show_customization is None:
             show_customization = self.customization_widget()
+        if self.form.has_key('search_text'):
+            search_text = self.form['search_text'].value
+        else:
+            search_text = ''
+        if pagesize is None:
+            if self.form.has_key(':pagesize'):
+                pagesize = self.form[':pagesize'].value
+            else:
+                pagesize = '50'
+        pagesize = int(pagesize)
+        if self.form.has_key(':startwith'):
+            startwith = int(self.form[':startwith'].value)
+        else:
+            startwith = 0
 
         index = htmltemplate.IndexTemplate(self, self.instance.TEMPLATES, cn)
         try:
-            index.render(filterspec, filter, columns, sort, group,
-                show_customization=show_customization)
+            index.render(filterspec, search_text, filter, columns, sort, 
+                group, show_customization=show_customization, 
+                show_nodes=show_nodes, pagesize=pagesize, startwith=startwith)
         except htmltemplate.MissingTemplateError:
             self.basicClassEditPage()
         self.pagefoot()
@@ -392,15 +597,45 @@ function submit_once() {
 
         w(_('</textarea><br><input type="submit" value="Save Changes"></form>'))
 
-    def shownode(self, message=None):
+    def classhelp(self):
+        '''Display a table of class info
+        '''
+        w = self.write
+        cn = self.form['classname'].value
+        cl = self.db.classes[cn]
+        props = self.form['properties'].value.split(',')
+        if cl.labelprop(1) in props:
+            sort = [cl.labelprop(1)]
+        else:
+            sort = props[0]
+
+        w('<table border=1 cellspacing=0 cellpaddin=2>')
+        w('<tr>')
+        for name in props:
+            w('<th align=left>%s</th>'%name)
+        w('</tr>')
+        for nodeid in cl.filter(None, {}, sort, []): #cl.list():
+            w('<tr>')
+            for name in props:
+                value = cgi.escape(str(cl.get(nodeid, name)))
+                w('<td align="left" valign="top">%s</td>'%value)
+            w('</tr>')
+        w('</table>')
+
+    def shownode(self, message=None, num_re=re.compile('^\d+$')):
         ''' display an item
         '''
         cn = self.classname
         cl = self.db.classes[cn]
+        if self.form.has_key(':multilink'):
+            link = self.form[':multilink'].value
+            designator, linkprop = link.split(':')
+            xtra = ' for <a href="%s">%s</a>' % (designator, designator)
+        else:
+            xtra = ''
 
         # possibly perform an edit
         keys = self.form.keys()
-        num_re = re.compile('^\d+$')
         # don't try to set properties if the user has just logged in
         if keys and not self.form.has_key('__login_name'):
             try:
@@ -430,7 +665,7 @@ function submit_once() {
         id = self.nodeid
         if cl.getkey():
             id = cl.get(id, cl.getkey())
-        self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
+        self.pagehead('%s: %s %s'%(self.classname.capitalize(), id, xtra), message)
 
         nodeid = self.nodeid
 
@@ -442,51 +677,12 @@ function submit_once() {
         self.pagefoot()
     showissue = shownode
     showmsg = shownode
-
-    def _add_assignedto_to_nosy(self, props):
-        ''' add the assignedto value from the props to the nosy list
-        '''
-        if not props.has_key('assignedto'):
-            return
-        assignedto_id = props['assignedto']
-        if not props.has_key('nosy'):
-            # load current nosy
-            if self.nodeid:
-                cl = self.db.classes[self.classname]
-                l = cl.get(self.nodeid, 'nosy')
-                if assignedto_id in l:
-                    return
-                props['nosy'] = l
-            else:
-                props['nosy'] = []
-        if assignedto_id not in props['nosy']:
-            props['nosy'].append(assignedto_id)
+    searchissue = searchnode
 
     def _changenode(self, props):
         ''' change the node based on the contents of the form
         '''
         cl = self.db.classes[self.classname]
-        # set status to chatting if 'unread' or 'resolved'
-        try:
-            # determine the id of 'unread','resolved' and 'chatting'
-            unread_id = self.db.status.lookup('unread')
-            resolved_id = self.db.status.lookup('resolved')
-            chatting_id = self.db.status.lookup('chatting')
-            current_status = cl.get(self.nodeid, 'status')
-            if props.has_key('status'):
-                new_status = props['status']
-            else:
-                # apparently there's a chance that some browsers don't
-                # send status...
-                new_status = current_status
-        except KeyError:
-            pass
-        else:
-            if new_status == unread_id or (new_status == resolved_id
-                    and current_status == resolved_id):
-                props['status'] = chatting_id
-
-        self._add_assignedto_to_nosy(props)
 
         # create the message
         message, files = self._handle_message()
@@ -504,18 +700,6 @@ function submit_once() {
         cl = self.db.classes[self.classname]
         props = parsePropsFromForm(self.db, cl, self.form)
 
-        # set status to 'unread' if not specified - a status of '- no
-        # selection -' doesn't make sense
-        if not props.has_key('status'):
-            try:
-                unread_id = self.db.status.lookup('unread')
-            except KeyError:
-                pass
-            else:
-                props['status'] = unread_id
-
-        self._add_assignedto_to_nosy(props)
-
         # check for messages and files
         message, files = self._handle_message()
         if message:
@@ -549,7 +733,9 @@ function submit_once() {
         # in a nutshell, don't do anything if there's no note or there's no
         # NOSY
         if self.form.has_key('__note'):
-            note = self.form['__note'].value
+            note = self.form['__note'].value.strip()
+        if not note:
+            return None, files
         if not props.has_key('messages'):
             return None, files
         if not isinstance(props['messages'], hyperdb.Multilink):
@@ -560,15 +746,11 @@ function submit_once() {
             return None, files
 
         # handle the note
-        if note:
-            if '\n' in note:
-                summary = re.split(r'\n\r?', note)[0]
-            else:
-                summary = note
-            m = ['%s\n'%note]
-        elif not files:
-            # don't generate a useless message
-            return None, files
+        if '\n' in note:
+            summary = re.split(r'\n\r?', note)[0]
+        else:
+            summary = note
+        m = ['%s\n'%note]
 
         # handle the messageid
         # TODO: handle inreplyto
@@ -610,7 +792,8 @@ function submit_once() {
                     designator, property = value.split(':')
                     link, nodeid = roundupdb.splitDesignator(designator)
                     link = self.db.classes[link]
-                    value = link.get(nodeid, property)
+                    # take a dupe of the list so we're not changing the cache
+                    value = link.get(nodeid, property)[:]
                     value.append(nid)
                     link.set(nodeid, **{property: value})
             elif key == ':link':
@@ -646,6 +829,12 @@ function submit_once() {
         '''
         cn = self.classname
         cl = self.db.classes[cn]
+        if self.form.has_key(':multilink'):
+            link = self.form[':multilink'].value
+            designator, linkprop = link.split(':')
+            xtra = ' for <a href="%s">%s</a>' % (designator, designator)
+        else:
+            xtra = ''
 
         # possibly perform a create
         keys = self.form.keys()
@@ -673,8 +862,9 @@ function submit_once() {
                 s = StringIO.StringIO()
                 traceback.print_exc(None, s)
                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
-        self.pagehead(_('New %(classname)s')%{'classname':
-            self.classname.capitalize()}, message)
+        self.pagehead(_('New %(classname)s %(xtra)s')%{
+                'classname': self.classname.capitalize(),
+                'xtra': xtra }, message)
 
         # call the template
         newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES,
@@ -725,6 +915,13 @@ function submit_once() {
         '''
         cn = self.classname
         cl = self.db.classes[cn]
+        props = parsePropsFromForm(self.db, cl, self.form)
+        if self.form.has_key(':multilink'):
+            link = self.form[':multilink'].value
+            designator, linkprop = link.split(':')
+            xtra = ' for <a href="%s">%s</a>' % (designator, designator)
+        else:
+            xtra = ''
 
         # possibly perform a create
         keys = self.form.keys()
@@ -735,8 +932,10 @@ function submit_once() {
                 if not mime_type:
                     mime_type = "application/octet-stream"
                 # save the file
-                nid = cl.create(content=file.file.read(), type=mime_type,
-                    name=file.filename)
+                props['type'] = mime_type
+                props['name'] = file.filename
+                props['content'] = file.file.read()
+                nid = cl.create(**props)
                 # handle linked nodes
                 self._post_editnode(nid)
                 # and some nice feedback for the user
@@ -747,14 +946,15 @@ function submit_once() {
                 traceback.print_exc(None, s)
                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
 
-        self.pagehead(_('New %(classname)s')%{'classname':
-             self.classname.capitalize()}, message)
+        self.pagehead(_('New %(classname)s %(xtra)s')%{
+                'classname': self.classname.capitalize(),
+                'xtra': xtra }, message)
         newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES,
             self.classname)
         newitem.render(self.form)
         self.pagefoot()
 
-    def showuser(self, message=None):
+    def showuser(self, message=None, num_re=re.compile('^\d+$')):
         '''Display a user page for editing. Make sure the user is allowed
             to edit this node, and also check for password changes.
         '''
@@ -773,7 +973,6 @@ function submit_once() {
         # perform any editing
         #
         keys = self.form.keys()
-        num_re = re.compile('^\d+$')
         if keys:
             try:
                 props = parsePropsFromForm(self.db, user, self.form,
@@ -817,7 +1016,7 @@ function submit_once() {
         ''' display a file
         '''
         nodeid = self.nodeid
-        cl = self.db.file
+        cl = self.db.classes[self.classname]
         mime_type = cl.get(nodeid, 'type')
         if mime_type == 'message/rfc822':
             mime_type = 'text/plain'
@@ -1087,7 +1286,7 @@ function submit_once() {
         self.db.commit()
 
     def do_action(self, action, dre=re.compile(r'([^\d]+)(\d+)'),
-            nre=re.compile(r'new(\w+)')):
+            nre=re.compile(r'new(\w+)'), sre=re.compile(r'search(\w+)')):
         '''Figure the user's action and do it.
         '''
         # here be the "normal" functionality
@@ -1097,6 +1296,9 @@ function submit_once() {
         if action == 'list_classes':
             self.classes()
             return
+        if action == 'classhelp':
+            self.classhelp()
+            return
         if action == 'login':
             self.login()
             return
@@ -1112,15 +1314,15 @@ function submit_once() {
             try:
                 cl = self.db.classes[self.classname]
             except KeyError:
-                raise NotFound
+                raise NotFound, self.classname
             try:
                 cl.get(self.nodeid, 'id')
             except IndexError:
-                raise NotFound
+                raise NotFound, self.nodeid
             try:
                 func = getattr(self, 'show%s'%self.classname)
             except AttributeError:
-                raise NotFound
+                raise NotFound, 'show%s'%self.classname
             func()
             return
 
@@ -1130,6 +1332,17 @@ function submit_once() {
             self.classname = m.group(1)
             try:
                 func = getattr(self, 'new%s'%self.classname)
+            except AttributeError:
+                raise NotFound, 'new%s'%self.classname
+            func()
+            return
+
+        # see if we're to put up the new node page
+        m = sre.match(action)
+        if m:
+            self.classname = m.group(1)
+            try:
+                func = getattr(self, 'search%s'%self.classname)
             except AttributeError:
                 raise NotFound
             func()
@@ -1140,7 +1353,7 @@ function submit_once() {
         try:
             self.db.getclass(self.classname)
         except KeyError:
-            raise NotFound
+            raise NotFound, self.classname
         self.list()
 
 
@@ -1152,78 +1365,20 @@ class ExtendedClient(Client):
     showtimelog = Client.shownode
     newsupport = Client.newnode
     newtimelog = Client.newnode
+    searchsupport = Client.searchnode
 
     default_index_sort = ['-activity']
     default_index_group = ['priority']
     default_index_filter = ['status']
     default_index_columns = ['activity','status','title','assignedto']
     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
+    default_pagesize = '50'
 
-    def pagehead(self, title, message=None):
-        url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
-        machine = self.env['SERVER_NAME']
-        port = self.env['SERVER_PORT']
-        if port != '80': machine = machine + ':' + port
-        base = urlparse.urlunparse(('http', machine, url, None, None, None))
-        if message is not None:
-            message = _('<div class="system-msg">%(message)s</div>')%locals()
-        else:
-            message = ''
-        style = open(os.path.join(self.instance.TEMPLATES, 'style.css')).read()
-        user_name = self.user or ''
-        if self.user == 'admin':
-            admin_links = _(' | <a href="list_classes">Class List</a>' \
-                          ' | <a href="user">User List</a>' \
-                          ' | <a href="newuser">Add User</a>')
-        else:
-            admin_links = ''
-        if self.user not in (None, 'anonymous'):
-            userid = self.db.user.lookup(self.user)
-            user_info = _('''
-<a href="issue?assignedto=%(userid)s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=-activity&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">My Issues</a> |
-<a href="support?assignedto=%(userid)s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=-activity&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">My Support</a> |
-<a href="user%(userid)s">My Details</a> | <a href="logout">Logout</a>
-''')%locals()
-        else:
-            user_info = _('<a href="login">Login</a>')
-        if self.user is not None:
-            add_links = _('''
-| Add
-<a href="newissue">Issue</a>,
-<a href="newsupport">Support</a>,
-''')
-        else:
-            add_links = ''
-        single_submit_script = self.single_submit_script
-        self.write(_('''<html><head>
-<title>%(title)s</title>
-<style type="text/css">%(style)s</style>
-</head>
-%(single_submit_script)s
-<body bgcolor=#ffffff>
-%(message)s
-<table width=100%% border=0 cellspacing=0 cellpadding=2>
-<tr class="location-bar"><td><big><strong>%(title)s</strong></big></td>
-<td align=right valign=bottom>%(user_name)s</td></tr>
-<tr class="location-bar">
-<td align=left>All
-<a href="issue?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>,
-<a href="support?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">Support</a>
-| Unassigned
-<a href="issue?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>,
-<a href="support?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">Support</a>
-%(add_links)s
-%(admin_links)s</td>
-<td align=right>%(user_info)s</td>
-</table>
-''')%locals())
-
-def parsePropsFromForm(db, cl, form, nodeid=0):
+def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
     '''Pull properties for the given class out of the form.
     '''
     props = {}
     keys = form.keys()
-    num_re = re.compile('^\d+$')
     for key in keys:
         if not cl.properties.has_key(key):
             continue
@@ -1299,6 +1454,141 @@ def parsePropsFromForm(db, cl, form, nodeid=0):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.133  2002/07/08 15:32:05  gmcm
+# Pagination of index pages.
+# New search form.
+#
+# Revision 1.132  2002/07/08 07:26:14  richard
+# ehem
+#
+# Revision 1.131  2002/07/08 06:53:57  richard
+# Not sure why the cgi_client had an indexer argument.
+#
+# Revision 1.130  2002/06/27 12:01:53  gmcm
+# If the form has a :multilink, put a back href in the pageheader (back to the linked-to node).
+# Some minor optimizations (only compile regexes once).
+#
+# Revision 1.129  2002/06/20 23:52:11  richard
+# Better handling of unauth attempt to edit stuff
+#
+# Revision 1.128  2002/06/12 21:28:25  gmcm
+# Allow form to set user-properties on a Fileclass.
+# Don't assume that a Fileclass is named "files".
+#
+# Revision 1.127  2002/06/11 06:38:24  richard
+#  . #565996 ] The "Attach a File to this Issue" fails
+#
+# Revision 1.126  2002/05/29 01:16:17  richard
+# Sorry about this huge checkin! It's fixing a lot of related stuff in one go
+# though.
+#
+# . #541941 ] changing multilink properties by mail
+# . #526730 ] search for messages capability
+# . #505180 ] split MailGW.handle_Message
+#   - also changed cgi client since it was duplicating the functionality
+# . build htmlbase if tests are run using CVS checkout (removed note from
+#   installation.txt)
+# . don't create an empty message on email issue creation if the email is empty
+#
+# Revision 1.125  2002/05/25 07:16:24  rochecompaan
+# Merged search_indexing-branch with HEAD
+#
+# Revision 1.124  2002/05/24 02:09:24  richard
+# Nothing like a live demo to show up the bugs ;)
+#
+# Revision 1.123  2002/05/22 05:04:13  richard
+# Oops
+#
+# Revision 1.122  2002/05/22 04:12:05  richard
+#  . applied patch #558876 ] cgi client customization
+#    ... with significant additions and modifications ;)
+#    - extended handling of ML assignedto to all places it's handled
+#    - added more NotFound info
+#
+# Revision 1.121  2002/05/21 06:08:10  richard
+# Handle migration
+#
+# Revision 1.120  2002/05/21 06:05:53  richard
+#  . #551483 ] assignedto in Client.make_index_link
+#
+# Revision 1.119  2002/05/15 06:21:21  richard
+#  . node caching now works, and gives a small boost in performance
+#
+# As a part of this, I cleaned up the DEBUG output and implemented TRACE
+# output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
+# CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
+# (using if __debug__ which is compiled out with -O)
+#
+# Revision 1.118  2002/05/12 23:46:33  richard
+# ehem, part 2
+#
+# Revision 1.117  2002/05/12 23:42:29  richard
+# ehem
+#
+# Revision 1.116  2002/05/02 08:07:49  richard
+# Added the ADD_AUTHOR_TO_NOSY handling to the CGI interface.
+#
+# Revision 1.115  2002/04/02 01:56:10  richard
+#  . stop sending blank (whitespace-only) notes
+#
+# Revision 1.114.2.4  2002/05/02 11:49:18  rochecompaan
+# Allow customization of the search filters that should be displayed
+# on the search page.
+#
+# Revision 1.114.2.3  2002/04/20 13:23:31  rochecompaan
+# We now have a separate search page for nodes.  Search links for
+# different classes can be customized in instance_config similar to
+# index links.
+#
+# Revision 1.114.2.2  2002/04/19 19:54:42  rochecompaan
+# cgi_client.py
+#     removed search link for the time being
+#     moved rendering of matches to htmltemplate
+# hyperdb.py
+#     filtering of nodes on full text search incorporated in filter method
+# roundupdb.py
+#     added paramater to call of filter method
+# roundup_indexer.py
+#     added search method to RoundupIndexer class
+#
+# Revision 1.114.2.1  2002/04/03 11:55:57  rochecompaan
+#  . Added feature #526730 - search for messages capability
+#
+# Revision 1.114  2002/03/17 23:06:05  richard
+# oops
+#
+# Revision 1.113  2002/03/14 23:59:24  richard
+#  . #517734 ] web header customisation is obscure
+#
+# Revision 1.112  2002/03/12 22:52:26  richard
+# more pychecker warnings removed
+#
+# Revision 1.111  2002/02/25 04:32:21  richard
+# ahem
+#
+# Revision 1.110  2002/02/21 07:19:08  richard
+# ... and label, width and height control for extra flavour!
+#
+# Revision 1.109  2002/02/21 07:08:19  richard
+# oops
+#
+# Revision 1.108  2002/02/21 07:02:54  richard
+# The correct var is "HTTP_HOST"
+#
+# Revision 1.107  2002/02/21 06:57:38  richard
+#  . Added popup help for classes using the classhelp html template function.
+#    - add <display call="classhelp('priority', 'id,name,description')">
+#      to an item page, and it generates a link to a popup window which displays
+#      the id, name and description for the priority class. The description
+#      field won't exist in most installations, but it will be added to the
+#      default templates.
+#
+# Revision 1.106  2002/02/21 06:23:00  richard
+# *** empty log message ***
+#
+# Revision 1.105  2002/02/20 05:52:10  richard
+# better error handling
+#
 # Revision 1.104  2002/02/20 05:45:17  richard
 # Use the csv module for generating the form entry so it's correct.
 # [also noted the sf.net feature request id in the change log]