Code

Re-enabled login and registration access after lopping them off via
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Tue, 23 Oct 2001 01:00:18 +0000 (01:00 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Tue, 23 Oct 2001 01:00:18 +0000 (01:00 +0000)
disabling access for anonymous users.
Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
a couple of bugs while I was there. Probably introduced a couple, but
things seem to work OK at the moment.

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

roundup-admin
roundup/cgi_client.py
roundup/htmltemplate.py
roundup/roundupdb.py
roundup/templates/classic/instance_config.py
roundup/templates/extended/instance_config.py

index 176ab71fa94504bf3671533703893d04624553dd..88849aa68fe3204714f740eba9ad94b9ac18f5f5 100755 (executable)
@@ -16,7 +16,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundup-admin,v 1.36 2001-10-21 00:45:15 richard Exp $
+# $Id: roundup-admin,v 1.37 2001-10-23 01:00:18 richard Exp $
 
 import sys
 if int(sys.version[0]) < 2:
@@ -35,13 +35,13 @@ class AdminTool:
 
     def __init__(self):
         self.commands = {}
-        for k, v in AdminTool.__dict__.items():
+        for k in AdminTool.__dict__.keys():
             if k[:3] == 'do_':
-                self.commands[k[3:]] = v
+                self.commands[k[3:]] = getattr(self, k)
         self.help = {}
-        for k, v in AdminTool.__dict__.items():
+        for k in AdminTool.__dict__.keys():
             if k[:5] == 'help_':
-                self.help[k[5:]] = v
+                self.help[k[5:]] = getattr(self, k)
 
     def usage(message=''):
         if message: message = 'Problem: '+message+'\n'
@@ -133,7 +133,7 @@ Command help:
         '''
         help = self.help.get(args[0], None)
         if help:
-            help(self)
+            help()
             return
         help = self.commands.get(args[0], None)
         if help:
@@ -212,7 +212,11 @@ Command help:
         designators = string.split(args[1], ',')
         l = []
         for designator in designators:
-            classname, nodeid = roundupdb.splitDesignator(designator)
+            try:
+                classname, nodeid = roundupdb.splitDesignator(designator)
+            except roundupdb.DesignatorError, message:
+                print 'Error: %s'%message
+                return 1
             if self.comma_sep:
                 l.append(self.db.getclass(classname).get(nodeid, propname))
             else:
@@ -236,7 +240,11 @@ Command help:
             key, value = prop.split('=')
             props[key] = value
         for designator in designators:
-            classname, nodeid = roundupdb.splitDesignator(designator)
+            try:
+                classname, nodeid = roundupdb.splitDesignator(designator)
+            except roundupdb.DesignatorError, message:
+                print 'Error: %s'%message
+                return 1
             cl = self.db.getclass(classname)
             properties = cl.getprops()
             for key, value in props.items():
@@ -427,7 +435,11 @@ Command help:
 
         Lists the journal entries for the node identified by the designator.
         '''
-        classname, nodeid = roundupdb.splitDesignator(args[0])
+        try:
+            classname, nodeid = roundupdb.splitDesignator(args[0])
+        except roundupdb.DesignatorError, message:
+            print 'Error: %s'%message
+            return 1
         # TODO: handle the -c option?
         print self.db.getclass(classname).history(nodeid)
         return 0
@@ -441,7 +453,11 @@ Command help:
         '''
         designators = string.split(args[0], ',')
         for designator in designators:
-            classname, nodeid = roundupdb.splitDesignator(designator)
+            try:
+                classname, nodeid = roundupdb.splitDesignator(designator)
+            except roundupdb.DesignatorError, message:
+                print 'Error: %s'%message
+                return 1
             self.db.getclass(classname).retire(nodeid)
         return 0
 
@@ -610,7 +626,7 @@ Command help:
 
         # do the command
         try:
-            return function(self, args[1:])
+            return function(args[1:])
         finally:
             self.db.close()
 
@@ -671,6 +687,9 @@ if __name__ == '__main__':
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.36  2001/10/21 00:45:15  richard
+# Added author identification to e-mail messages from roundup.
+#
 # Revision 1.35  2001/10/20 11:58:48  richard
 # Catch errors in login - no username or password supplied.
 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
index 88fc686db371986c55a9d9854782ec6d4ddb9fa7..3cd9a0ce1ee9c04bbea23247cd5ea966b229a4fe 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: cgi_client.py,v 1.38 2001-10-22 03:25:01 richard Exp $
+# $Id: cgi_client.py,v 1.39 2001-10-23 01:00:18 richard Exp $
 
 import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
 import base64, Cookie, time
@@ -240,8 +240,8 @@ class Client:
         if show_customization is None:
             show_customization = self.customization_widget()
 
-        htmltemplate.index(self, self.TEMPLATES, self.db, cn, filterspec,
-            filter, columns, sort, group,
+        index = htmltemplate.IndexTemplate(self, self.TEMPLATES, cn)
+        index.render(filterspec, filter, columns, sort, group,
             show_customization=show_customization)
         self.pagefoot()
 
@@ -276,7 +276,9 @@ class Client:
         nodeid = self.nodeid
 
         # use the template to display the item
-        htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
+        item = htmltemplate.ItemTemplate(self, self.TEMPLATES, self.classname)
+        item.render(nodeid)
+
         self.pagefoot()
     showissue = shownode
     showmsg = shownode
@@ -433,8 +435,12 @@ class Client:
                 traceback.print_exc(None, s)
                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
         self.pagehead('New %s'%self.classname.capitalize(), message)
-        htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
-            self.form)
+
+        # call the template
+        newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
+            self.classname)
+        newitem.render(self.form)
+
         self.pagefoot()
     newissue = newnode
     newuser = newnode
@@ -466,8 +472,9 @@ class Client:
                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
 
         self.pagehead('New %s'%self.classname.capitalize(), message)
-        htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
-            self.form)
+        newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
+            self.classname)
+        newitem.render(self.form)
         self.pagefoot()
 
     def classes(self, message=None):
@@ -541,6 +548,7 @@ class Client:
             password = self.form['__login_password'].value
         else:
             password = ''
+        print self.user, password
         # make sure the user exists
         try:
             uid = self.db.user.lookup(self.user)
@@ -585,6 +593,10 @@ class Client:
         ''' create a new user based on the contents of the form and then
         set the cookie
         '''
+        # re-open the database as "admin"
+        self.db.close()
+        self.db = self.instance.open('admin')
+
         # TODO: pre-check the required fields and username key property
         cl = self.db.classes['user']
         props, dummy = parsePropsFromForm(self.db, cl, self.form)
@@ -626,10 +638,6 @@ class Client:
             self.user = user
         self.db.close()
 
-        # make sure totally anonymous access is OK
-        if self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
-            return self.login()
-
         # re-open the database for real, using the user
         self.db = self.instance.open(self.user)
 
@@ -647,18 +655,28 @@ class Client:
         # appends the name of the file to the URL so the download file name
         # is correct, but doesn't actually use it.
         action = path[0]
-        if action == 'list_classes':
-            self.classes()
-            return
-        if action == 'login':
-            self.login()
-            return
         if action == 'login_action':
             self.login_action()
             return
+
+        # make sure anonymous are allowed to register
+        if self.ANONYMOUS_REGISTER == 'deny' and self.user is None:
+            return self.login()
+
         if action == 'newuser_action':
             self.newuser_action()
             return
+
+        # make sure totally anonymous access is OK
+        if self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
+            return self.login()
+
+        if action == 'list_classes':
+            self.classes()
+            return
+        if action == 'login':
+            self.login()
+            return
         if action == 'logout':
             self.logout()
             return
@@ -834,6 +852,12 @@ def parsePropsFromForm(db, cl, form, nodeid=0):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.38  2001/10/22 03:25:01  richard
+# Added configuration for:
+#  . anonymous user access and registration (deny/allow)
+#  . filter "widget" location on index page (top, bottom, both)
+# Updated some documentation.
+#
 # Revision 1.37  2001/10/21 07:26:35  richard
 # feature #473127: Filenames. I modified the file.index and htmltemplate
 #  source so that the filename is used in the link and the creation
index 0a78595a5784b5efe7eb4673e7c84b0c8fd7d339..fc0baa454079c1a11e94759ef31d127ab02e9597 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: htmltemplate.py,v 1.32 2001-10-22 03:25:01 richard Exp $
+# $Id: htmltemplate.py,v 1.33 2001-10-23 01:00:18 richard Exp $
 
 import os, re, StringIO, urllib, cgi, errno
 
 import hyperdb, date, password
 
-class Base:
-    def __init__(self, db, templates, classname, nodeid=None, form=None,
-            filterspec=None):
-        # TODO: really not happy with the way templates is passed on here
-        self.db, self.templates = db, templates
-        self.classname, self.nodeid = classname, nodeid
-        self.form, self.filterspec = form, filterspec
-        self.cl = self.db.classes[self.classname]
-        self.properties = self.cl.getprops()
-
-class Plain(Base):
-    ''' display a String property directly;
-
-        display a Date property in a specified time zone with an option to
-        omit the time from the date stamp;
-
-        for a Link or Multilink property, display the key strings of the
-        linked nodes (or the ids if the linked class has no key property)
-    '''
-    def __call__(self, property, escape=0):
+class TemplateFunctions:
+    def __init__(self):
+        self.form = None
+        self.nodeid = None
+        self.filterspec = None
+        self.globals = {}
+        for key in TemplateFunctions.__dict__.keys():
+            if key[:3] == 'do_':
+                self.globals[key[3:]] = getattr(self, key)
+
+    def do_plain(self, property, escape=0):
+        ''' display a String property directly;
+
+            display a Date property in a specified time zone with an option to
+            omit the time from the date stamp;
+
+            for a Link or Multilink property, display the key strings of the
+            linked nodes (or the ids if the linked class has no key property)
+        '''
         if not self.nodeid and self.form is None:
             return '[Field: not called from item]'
         propclass = self.properties[property]
@@ -75,11 +74,10 @@ class Plain(Base):
             return cgi.escape(value)
         return value
 
-class Field(Base):
-    ''' display a property like the plain displayer, but in a text field
-        to be edited
-    '''
-    def __call__(self, property, size=None, height=None, showid=0):
+    def do_field(self, property, size=None, height=None, showid=0):
+        ''' display a property like the plain displayer, but in a text field
+            to be edited
+        '''
         if not self.nodeid and self.form is None and self.filterspec is None:
             return '[Field: not called from item]'
         propclass = self.properties[property]
@@ -159,10 +157,9 @@ class Field(Base):
             s = 'Plain: bad propclass "%s"'%propclass
         return s
 
-class Menu(Base):
-    ''' for a Link property, display a menu of the available choices
-    '''
-    def __call__(self, property, size=None, height=None, showid=0):
+    def do_menu(self, property, size=None, height=None, showid=0):
+        ''' for a Link property, display a menu of the available choices
+        '''
         propclass = self.properties[property]
         if self.nodeid:
             value = self.cl.get(self.nodeid, property)
@@ -208,18 +205,17 @@ class Menu(Base):
             return '\n'.join(l)
         return '[Menu: not a link]'
 
-#XXX deviates from spec
-class Link(Base):
-    '''For a Link or Multilink property, display the names of the linked
-       nodes, hyperlinked to the item views on those nodes.
-       For other properties, link to this node with the property as the
-       text.
-
-       If is_download is true, append the property value to the generated
-       URL so that the link may be used as a download link and the
-       downloaded file name is correct.
-    '''
-    def __call__(self, property=None, is_download=0):
+    #XXX deviates from spec
+    def do_link(self, property=None, is_download=0):
+        '''For a Link or Multilink property, display the names of the linked
+           nodes, hyperlinked to the item views on those nodes.
+           For other properties, link to this node with the property as the
+           text.
+
+           If is_download is true, append the property value to the generated
+           URL so that the link may be used as a download link and the
+           downloaded file name is correct.
+        '''
         if not self.nodeid and self.form is None:
             return '[Link: not called from item]'
         propclass = self.properties[property]
@@ -263,11 +259,10 @@ class Link(Base):
         else:
             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
 
-class Count(Base):
-    ''' for a Multilink property, display a count of the number of links in
-        the list
-    '''
-    def __call__(self, property, **args):
+    def do_count(self, property, **args):
+        ''' for a Multilink property, display a count of the number of links in
+            the list
+        '''
         if not self.nodeid:
             return '[Count: not called from item]'
         propclass = self.properties[property]
@@ -276,14 +271,13 @@ class Count(Base):
             return str(len(value))
         return '[Count: not a Multilink]'
 
-# XXX pretty is definitely new ;)
-class Reldate(Base):
-    ''' display a Date property in terms of an interval relative to the
-        current date (e.g. "+ 3w", "- 2d").
+    # XXX pretty is definitely new ;)
+    def do_reldate(self, property, pretty=0):
+        ''' display a Date property in terms of an interval relative to the
+            current date (e.g. "+ 3w", "- 2d").
 
-        with the 'pretty' flag, make it pretty
-    '''
-    def __call__(self, property, pretty=0):
+            with the 'pretty' flag, make it pretty
+        '''
         if not self.nodeid and self.form is None:
             return '[Reldate: not called from item]'
         propclass = self.properties[property]
@@ -303,11 +297,10 @@ class Reldate(Base):
             return pretty
         return str(interval)
 
-class Download(Base):
-    ''' show a Link("file") or Multilink("file") property using links that
-        allow you to download files
-    '''
-    def __call__(self, property, **args):
+    def do_download(self, property, **args):
+        ''' show a Link("file") or Multilink("file") property using links that
+            allow you to download files
+        '''
         if not self.nodeid:
             return '[Download: not called from item]'
         propclass = self.properties[property]
@@ -326,11 +319,10 @@ class Download(Base):
         return '[Download: not a link]'
 
 
-class Checklist(Base):
-    ''' for a Link or Multilink property, display checkboxes for the available
-        choices to permit filtering
-    '''
-    def __call__(self, property, **args):
+    def do_checklist(self, property, **args):
+        ''' for a Link or Multilink property, display checkboxes for the
+            available choices to permit filtering
+        '''
         propclass = self.properties[property]
         if (not isinstance(propclass, hyperdb.Link) and not
                 isinstance(propclass, hyperdb.Multilink)):
@@ -373,38 +365,42 @@ class Checklist(Base):
                 'value="-1">'%(checked, property))
         return '\n'.join(l)
 
-class Note(Base):
-    ''' display a "note" field, which is a text area for entering a note to
-        go along with a change. 
-    '''
-    def __call__(self, rows=5, cols=80):
-       # TODO: pull the value from the form
+    def do_note(self, rows=5, cols=80):
+        ''' display a "note" field, which is a text area for entering a note to
+            go along with a change. 
+        '''
+        # TODO: pull the value from the form
         return '<textarea name="__note" rows=%s cols=%s></textarea>'%(rows,
             cols)
 
-# XXX new function
-class List(Base):
-    ''' list the items specified by property using the standard index for
-        the class
-    '''
-    def __call__(self, property, reverse=0):
-        propclass = self.properties[property]
-        if isinstance(not propclass, hyperdb.Multilink):
+    # XXX new function
+    def do_list(self, property, reverse=0):
+        ''' list the items specified by property using the standard index for
+            the class
+        '''
+        propcl = self.properties[property]
+        if not isinstance(propcl, hyperdb.Multilink):
             return '[List: not a Multilink]'
-        fp = StringIO.StringIO()
         value = self.cl.get(self.nodeid, property)
         if reverse:
             value.reverse()
-        # TODO: really not happy with the way templates is passed on here
-        index(fp, self.templates, self.db, propclass.classname, nodeids=value,
-            show_display_form=0)
+
+        # render the sub-index into a string
+        fp = StringIO.StringIO()
+        try:
+            write_save = self.client.write
+            self.client.write = fp.write
+            index = IndexTemplate(self.client, self.templates, propcl.classname)
+            index.render(nodeids=value, show_display_form=0)
+        finally:
+            self.client.write = write_save
+
         return fp.getvalue()
 
-# XXX new function
-class History(Base):
-    ''' list the history of the item
-    '''
-    def __call__(self, **args):
+    # XXX new function
+    def do_history(self, **args):
+        ''' list the history of the item
+        '''
         if self.nodeid is None:
             return "[History: node doesn't exist]"
 
@@ -421,11 +417,10 @@ class History(Base):
         l.append('</table>')
         return '\n'.join(l)
 
-# XXX new function
-class Submit(Base):
-    ''' add a submit button for the item
-    '''
-    def __call__(self):
+    # XXX new function
+    def do_submit(self):
+        ''' add a submit button for the item
+        '''
         if self.nodeid:
             return '<input type="submit" value="Submit Changes">'
         elif self.form is not None:
@@ -443,10 +438,11 @@ class IndexTemplateReplace:
         self.locals = locals
         self.props = props
 
-    def go(self, text, replace=re.compile(
-            r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
-            r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
-        return replace.sub(self, text)
+    replace=re.compile(
+        r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
+        r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
+    def go(self, text):
+        return self.replace.sub(self, text)
 
     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
         if m.group('name'):
@@ -461,278 +457,280 @@ class IndexTemplateReplace:
             return eval(command, self.globals, self.locals)
         print '*** unhandled match', m.groupdict()
 
-def sortby(sort_name, columns, filter, sort, group, filterspec):
-    l = []
-    w = l.append
-    for k, v in filterspec.items():
-        k = urllib.quote(k)
-        if type(v) == type([]):
-            w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
-        else:
-            w('%s=%s'%(k, urllib.quote(v)))
-    if columns:
-        w(':columns=%s'%','.join(map(urllib.quote, columns)))
-    if filter:
-        w(':filter=%s'%','.join(map(urllib.quote, filter)))
-    if group:
-        w(':group=%s'%','.join(map(urllib.quote, group)))
-    m = []
-    s_dir = ''
-    for name in sort:
-        dir = name[0]
-        if dir == '-':
-            name = name[1:]
+class IndexTemplate(TemplateFunctions):
+    def __init__(self, client, templates, classname):
+        self.client = client
+        self.templates = templates
+        self.classname = classname
+
+        # derived
+        self.db = self.client.db
+        self.cl = self.db.classes[self.classname]
+        self.properties = self.cl.getprops()
+
+        TemplateFunctions.__init__(self)
+
+    col_re=re.compile(r'<property\s+name="([^>]+)">')
+    def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
+            show_display_form=1, nodeids=None, show_customization=1):
+        self.filterspec = filterspec
+
+        w = self.client.write
+
+        # get the filter template
+        try:
+            filter_template = open(os.path.join(self.templates,
+                self.classname+'.filter')).read()
+            all_filters = self.col_re.findall(filter_template)
+        except IOError, error:
+            if error.errno != errno.ENOENT: raise
+            filter_template = None
+            all_filters = []
+
+        # display the filter section
+        if (hasattr(self.client, 'FILTER_POSITION') and
+                self.client.FILTER_POSITION in ('top and bottom', 'top')):
+            w('<form>\n')
+            self.filter_section(filter_template, filter, columns, group,
+                all_filters, all_columns, show_display_form, show_customization)
+            w('</form>\n')
+
+        # make sure that the sorting doesn't get lost either
+        if sort:
+            w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
+
+        # XXX deviate from spec here ...
+        # load the index section template and figure the default columns from it
+        template = open(os.path.join(self.templates,
+            self.classname+'.index')).read()
+        all_columns = self.col_re.findall(template)
+        if not columns:
+            columns = []
+            for name in all_columns:
+                columns.append(name)
         else:
-            dir = ''
-        if sort_name == name:
-            if dir == '-':
-                s_dir = ''
+            # re-sort columns to be the same order as all_columns
+            l = []
+            for name in all_columns:
+                if name in columns:
+                    l.append(name)
+            columns = l
+
+        # now display the index section
+        w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
+        w('<tr class="list-header">\n')
+        for name in columns:
+            cname = name.capitalize()
+            if show_display_form:
+                sb = self.sortby(name, filterspec, columns, filter, group, sort)
+                anchor = "%s?%s"%(self.classname, sb)
+                w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
+                    anchor, cname))
             else:
-                s_dir = '-'
-        else:
-            m.append(dir+urllib.quote(name))
-    m.insert(0, s_dir+urllib.quote(sort_name))
-    # so things don't get completely out of hand, limit the sort to two columns
-    w(':sort=%s'%','.join(m[:2]))
-    return '&'.join(l)
-
-def index(client, templates, db, classname, filterspec={}, filter=[],
-        columns=[], sort=[], group=[], show_display_form=1, nodeids=None,
-        show_customization=1,
-        col_re=re.compile(r'<property\s+name="([^>]+)">')):
-    globals = {
-        'plain': Plain(db, templates, classname, filterspec=filterspec),
-        'field': Field(db, templates, classname, filterspec=filterspec),
-        'menu': Menu(db, templates, classname, filterspec=filterspec),
-        'link': Link(db, templates, classname, filterspec=filterspec),
-        'count': Count(db, templates, classname, filterspec=filterspec),
-        'reldate': Reldate(db, templates, classname, filterspec=filterspec),
-        'download': Download(db, templates, classname, filterspec=filterspec),
-        'checklist': Checklist(db, templates, classname, filterspec=filterspec),
-        'list': List(db, templates, classname, filterspec=filterspec),
-        'history': History(db, templates, classname, filterspec=filterspec),
-        'submit': Submit(db, templates, classname, filterspec=filterspec),
-        'note': Note(db, templates, classname, filterspec=filterspec)
-    }
-    cl = db.classes[classname]
-    properties = cl.getprops()
-    w = client.write
-    w('<form>')
-
-    try:
-        template = open(os.path.join(templates, classname+'.filter')).read()
-        all_filters = col_re.findall(template)
-    except IOError, error:
-        if error.errno != errno.ENOENT: raise
-        template = None
-        all_filters = []
-    if template and filter:
-        # display the filter section
-        w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
-        w('<tr class="location-bar">')
-        w(' <th align="left" colspan="2">Filter specification...</th>')
-        w('</tr>')
-        replace = IndexTemplateReplace(globals, locals(), filter)
-        w(replace.go(template))
-        w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
-        w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
+                w('<td><span class="list-header">%s</span></td>\n'%cname)
+        w('</tr>\n')
+
+        # this stuff is used for group headings - optimise the group names
+        old_group = None
+        group_names = []
+        if group:
+            for name in group:
+                if name[0] == '-': group_names.append(name[1:])
+                else: group_names.append(name)
+
+        # now actually loop through all the nodes we get from the filter and
+        # apply the template
+        if nodeids is None:
+            nodeids = self.cl.filter(filterspec, sort, group)
+        for nodeid in nodeids:
+            # check for a group heading
+            if group_names:
+                this_group = [self.cl.get(nodeid, name) for name in group_names]
+                if this_group != old_group:
+                    l = []
+                    for name in group_names:
+                        prop = self.properties[name]
+                        if isinstance(prop, hyperdb.Link):
+                            group_cl = self.db.classes[prop.classname]
+                            key = group_cl.getkey()
+                            value = self.cl.get(nodeid, name)
+                            if value is None:
+                                l.append('[unselected %s]'%prop.classname)
+                            else:
+                                l.append(group_cl.get(self.cl.get(nodeid,
+                                    name), key))
+                        elif isinstance(prop, hyperdb.Multilink):
+                            group_cl = self.db.classes[prop.classname]
+                            key = group_cl.getkey()
+                            for value in self.cl.get(nodeid, name):
+                                l.append(group_cl.get(value, key))
+                        else:
+                            value = self.cl.get(nodeid, name)
+                            if value is None:
+                                value = '[empty %s]'%name
+                            else:
+                                value = str(value)
+                            l.append(value)
+                    w('<tr class="section-bar">'
+                      '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
+                        len(columns), ', '.join(l)))
+                    old_group = this_group
+
+            # display this node's row
+            replace = IndexTemplateReplace(self.globals, locals(), columns)
+            self.nodeid = nodeid
+            w(replace.go(template))
+            self.nodeid = None
+
         w('</table>')
 
-    # If the filters aren't being displayed, then hide their current
-    # value in the form
-    if not filter:
-        for k, v in filterspec.items():
-            if type(v) == type([]): v = ','.join(v)
-            w('<input type="hidden" name="%s" value="%s">'%(k, v))
-
-    # make sure that the sorting doesn't get lost either
-    if sort:
-        w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
-
-    # XXX deviate from spec here ...
-    # load the index section template and figure the default columns from it
-    template = open(os.path.join(templates, classname+'.index')).read()
-    all_columns = col_re.findall(template)
-    if not columns:
-        columns = []
-        for name in all_columns:
-            columns.append(name)
-    else:
-        # re-sort columns to be the same order as all_columns
-        l = []
-        for name in all_columns:
-            if name in columns:
-                l.append(name)
-        columns = l
-
-    # display the filter section
-    if hasattr(client, 'FILTER_POSITION') and client.FILTER_POSITION in ('top and bottom', 'top'):
-        filter_section(w, cl, filter, columns, group, all_filters, all_columns,
-            show_display_form, show_customization)
-
-    # now display the index section
-    w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
-    w('<tr class="list-header">\n')
-    for name in columns:
-        cname = name.capitalize()
-        if show_display_form:
-            anchor = "%s?%s"%(classname, sortby(name, columns, filter,
-                sort, group, filterspec))
-            w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
-                anchor, cname))
-        else:
-            w('<td><span class="list-header">%s</span></td>\n'%cname)
-    w('</tr>\n')
-
-    # this stuff is used for group headings - optimise the group names
-    old_group = None
-    group_names = []
-    if group:
-        for name in group:
-            if name[0] == '-': group_names.append(name[1:])
-            else: group_names.append(name)
-
-    # now actually loop through all the nodes we get from the filter and
-    # apply the template
-    if nodeids is None:
-        nodeids = cl.filter(filterspec, sort, group)
-    for nodeid in nodeids:
-        # check for a group heading
-        if group_names:
-            this_group = [cl.get(nodeid, name) for name in group_names]
-            if this_group != old_group:
-                l = []
-                for name in group_names:
-                    prop = properties[name]
-                    if isinstance(prop, hyperdb.Link):
-                        group_cl = db.classes[prop.classname]
-                        key = group_cl.getkey()
-                        value = cl.get(nodeid, name)
-                        if value is None:
-                            l.append('[unselected %s]'%prop.classname)
-                        else:
-                            l.append(group_cl.get(cl.get(nodeid, name), key))
-                    elif isinstance(prop, hyperdb.Multilink):
-                        group_cl = db.classes[prop.classname]
-                        key = group_cl.getkey()
-                        for value in cl.get(nodeid, name):
-                            l.append(group_cl.get(value, key))
-                    else:
-                        value = cl.get(nodeid, name)
-                        if value is None:
-                            value = '[empty %s]'%name
-                        else:
-                            value = str(value)
-                        l.append(value)
-                w('<tr class="section-bar">'
-                  '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
-                    len(columns), ', '.join(l)))
-                old_group = this_group
-
-        # display this node's row
-        for value in globals.values():
-            if hasattr(value, 'nodeid'):
-                value.nodeid = nodeid
-        replace = IndexTemplateReplace(globals, locals(), columns)
-        w(replace.go(template))
-
-    w('</table>')
-
-    # display the filter section
-    if hasattr(client, 'FILTER_POSITION') and client.FILTER_POSITION in ('top and bottom', 'bottom'):
-        filter_section(w, cl, filter, columns, group, all_filters, all_columns,
-            show_display_form, show_customization)
-
-
-def filter_section(w, cl, filter, columns, group, all_filters, all_columns,
-        show_display_form, show_customization):
-    # now add in the filter/columns/group/etc config table form
-    w('<input type="hidden" name="show_customization" value="%s">' %
-        show_customization )
-    w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
-    names = []
-    properties = cl.getprops()
-    for name in properties.keys():
-        if name in all_filters or name in all_columns:
-            names.append(name)
-    w('<tr class="location-bar">')
-    if show_customization:
-        action = '-'
-    else:
-        action = '+'
-        # hide the values for filters, columns and grouping in the form
-        # if the customization widget is not visible
-        for name in names:
-            if all_filters and name in filter:
-                w('<input type="hidden" name=":filter" value="%s">' % name)
-            if all_columns and name in columns:
-                w('<input type="hidden" name=":columns" value="%s">' % name)
-            if all_columns and name in group:
-                w('<input type="hidden" name=":group" value="%s">' % name)
-
-    if show_display_form:
-        # TODO: The widget style can go into the stylesheet
-        w('<th align="left" colspan=%s>'
-          '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
-          'customisation...</th></tr>\n'%(len(names)+1, action))
+        # display the filter section
+        if (hasattr(self.client, 'FILTER_POSITION') and
+                self.client.FILTER_POSITION in ('top and bottom', 'bottom')):
+            w('<form>\n')
+            self.filter_section(filter_template, filter, columns, group,
+                all_filters, all_columns, show_display_form, show_customization)
+            w('</form>\n')
+
+
+    def filter_section(self, template, filter, columns, group, all_filters,
+            all_columns, show_display_form, show_customization):
+
+        w = self.client.write
+
+        if template and filter:
+            # display the filter section
+            w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
+            w('<tr class="location-bar">')
+            w(' <th align="left" colspan="2">Filter specification...</th>')
+            w('</tr>')
+            replace = IndexTemplateReplace(self.globals, locals(), filter)
+            w(replace.go(template))
+            w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
+            w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
+            w('</table>')
+
+        # now add in the filter/columns/group/etc config table form
+        w('<input type="hidden" name="show_customization" value="%s">' %
+            show_customization )
+        w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
+        names = []
+        for name in self.properties.keys():
+            if name in all_filters or name in all_columns:
+                names.append(name)
+        w('<tr class="location-bar">')
         if show_customization:
-            w('<tr class="location-bar"><th>&nbsp;</th>')
+            action = '-'
+        else:
+            action = '+'
+            # hide the values for filters, columns and grouping in the form
+            # if the customization widget is not visible
             for name in names:
-                w('<th>%s</th>'%name.capitalize())
-            w('</tr>\n')
-
-            # Filter
-            if all_filters:
-                w('<tr><th width="1%" align=right class="location-bar">'
-                  'Filters</th>\n')
-                for name in names:
-                    if name not in all_filters:
-                        w('<td>&nbsp;</td>')
-                        continue
-                    if name in filter: checked=' checked'
-                    else: checked=''
-                    w('<td align=middle>\n')
-                    w(' <input type="checkbox" name=":filter" value="%s" '
-                      '%s></td>\n'%(name, checked))
-                w('</tr>\n')
+                if all_filters and name in filter:
+                    w('<input type="hidden" name=":filter" value="%s">' % name)
+                if all_columns and name in columns:
+                    w('<input type="hidden" name=":columns" value="%s">' % name)
+                if all_columns and name in group:
+                    w('<input type="hidden" name=":group" value="%s">' % name)
 
-            # Columns
-            if all_columns:
-                w('<tr><th width="1%" align=right class="location-bar">'
-                  'Columns</th>\n')
+        if show_display_form:
+            # TODO: The widget style can go into the stylesheet
+            w('<th align="left" colspan=%s>'
+              '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
+              'customisation...</th></tr>\n'%(len(names)+1, action))
+            if show_customization:
+                w('<tr class="location-bar"><th>&nbsp;</th>')
                 for name in names:
-                    if name not in all_columns:
-                        w('<td>&nbsp;</td>')
-                        continue
-                    if name in columns: checked=' checked'
-                    else: checked=''
-                    w('<td align=middle>\n')
-                    w(' <input type="checkbox" name=":columns" value="%s"'
-                      '%s></td>\n'%(name, checked))
+                    w('<th>%s</th>'%name.capitalize())
                 w('</tr>\n')
 
-                # Grouping
-                w('<tr><th width="1%" align=right class="location-bar">'
-                  'Grouping</th>\n')
-                for name in names:
-                    prop = properties[name]
-                    if name not in all_columns:
-                        w('<td>&nbsp;</td>')
-                        continue
-                    if name in group: checked=' checked'
-                    else: checked=''
-                    w('<td align=middle>\n')
-                    w(' <input type="checkbox" name=":group" value="%s"'
-                      '%s></td>\n'%(name, checked))
+                # Filter
+                if all_filters:
+                    w('<tr><th width="1%" align=right class="location-bar">'
+                      'Filters</th>\n')
+                    for name in names:
+                        if name not in all_filters:
+                            w('<td>&nbsp;</td>')
+                            continue
+                        if name in filter: checked=' checked'
+                        else: checked=''
+                        w('<td align=middle>\n')
+                        w(' <input type="checkbox" name=":filter" value="%s" '
+                          '%s></td>\n'%(name, checked))
+                    w('</tr>\n')
+
+                # Columns
+                if all_columns:
+                    w('<tr><th width="1%" align=right class="location-bar">'
+                      'Columns</th>\n')
+                    for name in names:
+                        if name not in all_columns:
+                            w('<td>&nbsp;</td>')
+                            continue
+                        if name in columns: checked=' checked'
+                        else: checked=''
+                        w('<td align=middle>\n')
+                        w(' <input type="checkbox" name=":columns" value="%s"'
+                          '%s></td>\n'%(name, checked))
+                    w('</tr>\n')
+
+                    # Grouping
+                    w('<tr><th width="1%" align=right class="location-bar">'
+                      'Grouping</th>\n')
+                    for name in names:
+                        prop = self.properties[name]
+                        if name not in all_columns:
+                            w('<td>&nbsp;</td>')
+                            continue
+                        if name in group: checked=' checked'
+                        else: checked=''
+                        w('<td align=middle>\n')
+                        w(' <input type="checkbox" name=":group" value="%s"'
+                          '%s></td>\n'%(name, checked))
+                    w('</tr>\n')
+
+                w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
+                w('<td colspan="%s">'%len(names))
+                w('<input type="submit" name="action" value="Redisplay"></td>')
                 w('</tr>\n')
 
-            w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
-            w('<td colspan="%s">'%len(names))
-            w('<input type="submit" name="action" value="Redisplay"></td>')
-            w('</tr>\n')
+            w('</table>\n')
 
-        w('</table>\n')
-        w('</form>\n')
+    def sortby(self, sort_name, filterspec, columns, filter, group, sort):
+        l = []
+        w = l.append
+        for k, v in filterspec.items():
+            k = urllib.quote(k)
+            if type(v) == type([]):
+                w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
+            else:
+                w('%s=%s'%(k, urllib.quote(v)))
+        if columns:
+            w(':columns=%s'%','.join(map(urllib.quote, columns)))
+        if filter:
+            w(':filter=%s'%','.join(map(urllib.quote, filter)))
+        if group:
+            w(':group=%s'%','.join(map(urllib.quote, group)))
+        m = []
+        s_dir = ''
+        for name in sort:
+            dir = name[0]
+            if dir == '-':
+                name = name[1:]
+            else:
+                dir = ''
+            if sort_name == name:
+                if dir == '-':
+                    s_dir = ''
+                else:
+                    s_dir = '-'
+            else:
+                m.append(dir+urllib.quote(name))
+        m.insert(0, s_dir+urllib.quote(sort_name))
+        # so things don't get completely out of hand, limit the sort to
+        # two columns
+        w(':sort=%s'%','.join(m[:2]))
+        return '&'.join(l)
 
 #
 #   ITEM TEMPLATES
@@ -744,10 +742,11 @@ class ItemTemplateReplace:
         self.cl = cl
         self.nodeid = nodeid
 
-    def go(self, text, replace=re.compile(
-            r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
-            r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
-        return replace.sub(self, text)
+    replace=re.compile(
+        r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
+        r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
+    def go(self, text):
+        return self.replace.sub(self, text)
 
     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
         if m.group('name'):
@@ -762,83 +761,78 @@ class ItemTemplateReplace:
             return eval(command, self.globals, self.locals)
         print '*** unhandled match', m.groupdict()
 
-def item(client, templates, db, classname, nodeid, replace=re.compile(
-            r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
-            r'(?P<endprop></property>)|'
-            r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
-
-    globals = {
-        'plain': Plain(db, templates, classname, nodeid),
-        'field': Field(db, templates, classname, nodeid),
-        'menu': Menu(db, templates, classname, nodeid),
-        'link': Link(db, templates, classname, nodeid),
-        'count': Count(db, templates, classname, nodeid),
-        'reldate': Reldate(db, templates, classname, nodeid),
-        'download': Download(db, templates, classname, nodeid),
-        'checklist': Checklist(db, templates, classname, nodeid),
-        'list': List(db, templates, classname, nodeid),
-        'history': History(db, templates, classname, nodeid),
-        'submit': Submit(db, templates, classname, nodeid),
-        'note': Note(db, templates, classname, nodeid)
-    }
-
-    cl = db.classes[classname]
-    properties = cl.getprops()
-
-    if properties.has_key('type') and properties.has_key('content'):
-        pass
-        # XXX we really want to return this as a downloadable...
-        #  currently I handle this at a higher level by detecting 'file'
-        #  designators...
-
-    w = client.write
-    w('<form action="%s%s">'%(classname, nodeid))
-    s = open(os.path.join(templates, classname+'.item')).read()
-    replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
-    w(replace.go(s))
-    w('</form>')
-
-
-def newitem(client, templates, db, classname, form, replace=re.compile(
-            r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
-            r'(?P<endprop></property>)|'
-            r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
-    globals = {
-        'plain': Plain(db, templates, classname, form=form),
-        'field': Field(db, templates, classname, form=form),
-        'menu': Menu(db, templates, classname, form=form),
-        'link': Link(db, templates, classname, form=form),
-        'count': Count(db, templates, classname, form=form),
-        'reldate': Reldate(db, templates, classname, form=form),
-        'download': Download(db, templates, classname, form=form),
-        'checklist': Checklist(db, templates, classname, form=form),
-        'list': List(db, templates, classname, form=form),
-        'history': History(db, templates, classname, form=form),
-        'submit': Submit(db, templates, classname, form=form),
-        'note': Note(db, templates, classname, form=form)
-    }
-
-    cl = db.classes[classname]
-    properties = cl.getprops()
-
-    w = client.write
-    try:
-        s = open(os.path.join(templates, classname+'.newitem')).read()
-    except:
-        s = open(os.path.join(templates, classname+'.item')).read()
-    w('<form action="new%s" method="POST" enctype="multipart/form-data">'%classname)
-    for key in form.keys():
-        if key[0] == ':':
-            value = form[key].value
-            if type(value) != type([]): value = [value]
-            for value in value:
-                w('<input type="hidden" name="%s" value="%s">'%(key, value))
-    replace = ItemTemplateReplace(globals, locals(), None, None)
-    w(replace.go(s))
-    w('</form>')
+
+class ItemTemplate(TemplateFunctions):
+    def __init__(self, client, templates, classname):
+        self.client = client
+        self.templates = templates
+        self.classname = classname
+
+        # derived
+        self.db = self.client.db
+        self.cl = self.db.classes[self.classname]
+        self.properties = self.cl.getprops()
+
+        TemplateFunctions.__init__(self)
+
+    def render(self, nodeid):
+        self.nodeid = nodeid
+
+        if (self.properties.has_key('type') and
+                self.properties.has_key('content')):
+            pass
+            # XXX we really want to return this as a downloadable...
+            #  currently I handle this at a higher level by detecting 'file'
+            #  designators...
+
+        w = self.client.write
+        w('<form action="%s%s">'%(self.classname, nodeid))
+        s = open(os.path.join(self.templates, self.classname+'.item')).read()
+        replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
+        w(replace.go(s))
+        w('</form>')
+
+
+class NewItemTemplate(TemplateFunctions):
+    def __init__(self, client, templates, classname):
+        self.client = client
+        self.templates = templates
+        self.classname = classname
+
+        # derived
+        self.db = self.client.db
+        self.cl = self.db.classes[self.classname]
+        self.properties = self.cl.getprops()
+
+        TemplateFunctions.__init__(self)
+
+    def render(self, form):
+        self.form = form
+        w = self.client.write
+        c = self.classname
+        try:
+            s = open(os.path.join(self.templates, c+'.newitem')).read()
+        except:
+            s = open(os.path.join(self.templates, c+'.item')).read()
+        w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
+        for key in form.keys():
+            if key[0] == ':':
+                value = form[key].value
+                if type(value) != type([]): value = [value]
+                for value in value:
+                    w('<input type="hidden" name="%s" value="%s">'%(key, value))
+        replace = ItemTemplateReplace(self.globals, locals(), None, None)
+        w(replace.go(s))
+        w('</form>')
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.32  2001/10/22 03:25:01  richard
+# Added configuration for:
+#  . anonymous user access and registration (deny/allow)
+#  . filter "widget" location on index page (top, bottom, both)
+# Updated some documentation.
+#
 # Revision 1.31  2001/10/21 07:26:35  richard
 # feature #473127: Filenames. I modified the file.index and htmltemplate
 #  source so that the filename is used in the link and the creation
index 546e74652909ed5efbf72c61932b1084c25d1b6a..099c1c61532614eded43bda2ecdbe181191cc16b 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundupdb.py,v 1.14 2001-10-21 07:26:35 richard Exp $
+# $Id: roundupdb.py,v 1.15 2001-10-23 01:00:18 richard Exp $
 
 import re, os, smtplib, socket
 
 import hyperdb, date
 
+class DesignatorError(ValueError):
+    pass
 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
     ''' Take a foo123 and return ('foo', 123)
     '''
     m = dre.match(designator)
+    if m is None:
+        raise DesignatorError, '"%s" not a node designator'%designator
     return m.group(1), m.group(2)
 
 class Database:
@@ -303,6 +307,11 @@ Roundup issue tracker
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.14  2001/10/21 07:26:35  richard
+# feature #473127: Filenames. I modified the file.index and htmltemplate
+#  source so that the filename is used in the link and the creation
+#  information is displayed.
+#
 # Revision 1.13  2001/10/21 00:45:15  richard
 # Added author identification to e-mail messages from roundup.
 #
index 176a9816f470bb0b1d469711a920ec474cdabe9a..f02335d041ddcb46b5c596640a55562a7a2b6c96 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: instance_config.py,v 1.7 2001-10-22 03:25:01 richard Exp $
+# $Id: instance_config.py,v 1.8 2001-10-23 01:00:18 richard Exp $
 
 MAIL_DOMAIN=MAILHOST=HTTP_HOST=None
 HTTP_PORT=0
@@ -62,14 +62,23 @@ ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN
 # Somewhere for roundup to log stuff internally sent to stdout or stderr
 LOG = os.path.join(INSTANCE_HOME, 'roundup.log')
 
+# Where to place the web filtering HTML on the index page
+FILTER_POSITION = 'bottom'      # one of 'top', 'bottom', 'top and bottom'
+
 # Deny or allow anonymous access to the web interface
-ANONYMOUS_ACCESS = 'deny'
+ANONYMOUS_ACCESS = 'deny'       # either 'deny' or 'allow'
 
 # Deny or allow anonymous users to register through the web interface
-ANONYMOUS_REGISTER = 'deny'
+ANONYMOUS_REGISTER = 'deny'     # either 'deny' or 'allow'
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.7  2001/10/22 03:25:01  richard
+# Added configuration for:
+#  . anonymous user access and registration (deny/allow)
+#  . filter "widget" location on index page (top, bottom, both)
+# Updated some documentation.
+#
 # Revision 1.6  2001/10/01 06:10:42  richard
 # stop people setting up roundup with our addresses as default - need to
 # handle this better in the init
index 643c409818cc4d6bb33fe82c2120b44254ed35b7..3260bfa44271d3f0b5393e733c94c3a7dadfd580 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: instance_config.py,v 1.7 2001-10-22 03:25:01 richard Exp $
+# $Id: instance_config.py,v 1.8 2001-10-23 01:00:18 richard Exp $
 
 MAIL_DOMAIN=MAILHOST=HTTP_HOST=None
 HTTP_PORT=0
@@ -62,6 +62,9 @@ ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN
 # Somewhere for roundup to log stuff internally sent to stdout or stderr
 LOG = os.path.join(INSTANCE_HOME, 'roundup.log')
 
+# Where to place the web filtering HTML on the index page
+FILTER_POSITION = 'bottom'      # one of 'top', 'bottom', 'top and bottom'
+
 # Deny or allow anonymous access to the web interface
 ANONYMOUS_ACCESS = 'deny'
 
@@ -70,6 +73,12 @@ ANONYMOUS_REGISTER = 'deny'
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.7  2001/10/22 03:25:01  richard
+# Added configuration for:
+#  . anonymous user access and registration (deny/allow)
+#  . filter "widget" location on index page (top, bottom, both)
+# Updated some documentation.
+#
 # Revision 1.6  2001/10/01 06:10:42  richard
 # stop people setting up roundup with our addresses as default - need to
 # handle this better in the init