view roundup/cgi_client.py @ 359:3903aaaef4e1

Added samples for "external" CGI config
author Jürgen Hermann <jhermann@users.sourceforge.net>
date Tue, 06 Nov 2001 22:22:20 +0000
parents 48ceb1667983
children ccb394667145
line wrap: on
line source

#
# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
# This module is free software, and you may redistribute it and/or modify
# under the same terms as Python, so long as this copyright message and
# disclaimer are retained in their original form.
#
# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
# 
# $Id: cgi_client.py,v 1.51 2001-11-06 22:00:34 jhermann Exp $

import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
import binascii, Cookie, time

import roundupdb, htmltemplate, date, hyperdb, password

class Unauthorised(ValueError):
    pass

class NotFound(ValueError):
    pass

class Client:
    '''
    A note about login
    ------------------

    If the user has no login cookie, then they are anonymous. There
    are two levels of anonymous use. If there is no 'anonymous' user, there
    is no login at all and the database is opened in read-only mode. If the
    'anonymous' user exists, the user is logged in using that user (though
    there is no cookie). This allows them to modify the database, and all
    modifications are attributed to the 'anonymous' user.


    Customisation
    -------------
      FILTER_POSITION - one of 'top', 'bottom', 'top and bottom'
      ANONYMOUS_ACCESS - one of 'deny', 'allow'
      ANONYMOUS_REGISTER - one of 'deny', 'allow'

    '''
    FILTER_POSITION = 'bottom'       # one of 'top', 'bottom', 'top and bottom'
    ANONYMOUS_ACCESS = 'deny'        # one of 'deny', 'allow'
    ANONYMOUS_REGISTER = 'deny'      # one of 'deny', 'allow'

    def __init__(self, instance, request, env):
        self.instance = instance
        self.request = request
        self.env = env
        self.path = env['PATH_INFO']
        self.split_path = self.path.split('/')

        self.form = cgi.FieldStorage(environ=env)
        self.headers_done = 0
        try:
            self.debug = int(env.get("ROUNDUP_DEBUG", 0))
        except ValueError:
            # someone gave us a non-int debug level, turn it off
            self.debug = 0

    def getuid(self):
        return self.db.user.lookup(self.user)

    def header(self, headers={'Content-Type':'text/html'}):
        '''Put up the appropriate header.
        '''
        if not headers.has_key('Content-Type'):
            headers['Content-Type'] = 'text/html'
        self.request.send_response(200)
        for entry in headers.items():
            self.request.send_header(*entry)
        self.request.end_headers()
        self.headers_done = 1

    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))
        if message is not None:
            message = '<div class="system-msg">%s</div>'%message
        else:
            message = ''
        style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
        user_name = self.user or ''
        if self.user == 'admin':
            admin_links = ' | <a href="list_classes">Class List</a>'
        else:
            admin_links = ''
        if self.user not in (None, 'anonymous'):
            userid = self.db.user.lookup(self.user)
            user_info = '''
<a href="issue?assignedto=%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%s">My Details</a> | <a href="logout">Logout</a>
'''%(userid, userid)
        else:
            user_info = '<a href="login">Login</a>'
        if self.user is not None:
            add_links = '''
| Add
<a href="newissue">Issue</a>,
<a href="newuser">User</a>
'''
        else:
            add_links = ''
        self.write('''<html><head>
<title>%s</title>
<style type="text/css">%s</style>
</head>
<body bgcolor=#ffffff>
%s
<table width=100%% border=0 cellspacing=0 cellpadding=2>
<tr class="location-bar"><td><big><strong>%s</strong></big></td>
<td align=right valign=bottom>%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>
%s
%s</td>
<td align=right>%s</td>
</table>
'''%(title, style, message, title, user_name, add_links, admin_links,
    user_info))

    def pagefoot(self):
        if self.debug:
            self.write('<hr><small><dl>')
            self.write('<dt><b>Path</b></dt>')
            self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path))))
            keys = self.form.keys()
            keys.sort()
            if keys:
                self.write('<dt><b>Form entries</b></dt>')
                for k in self.form.keys():
                    v = str(self.form[k].value)
                    self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
            keys = self.env.keys()
            keys.sort()
            self.write('<dt><b>CGI environment</b></dt>')
            for k in keys:
                v = self.env[k]
                self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
            self.write('</dl></small>')
        self.write('</body></html>')

    def write(self, content):
        if not self.headers_done:
            self.header()
        self.request.wfile.write(content)

    def index_arg(self, arg):
        ''' handle the args to index - they might be a list from the form
            (ie. submitted from a form) or they might be a command-separated
            single string (ie. manually constructed GET args)
        '''
        if self.form.has_key(arg):
            arg =  self.form[arg]
            if type(arg) == type([]):
                return [arg.value for arg in arg]
            return arg.value.split(',')
        return []

    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]
                else:
                    value = value.value.split(',')
                l = filterspec.get(key, [])
                l = l + value
                filterspec[key] = l
            else:
                filterspec[key] = value.value
        return filterspec

    def customization_widget(self):
        ''' The customization widget is visible by default. The widget
            visibility is remembered by show_customization.  Visibility
            is not toggled if the action value is "Redisplay"
        '''
        if not self.form.has_key('show_customization'):
            visible = 1
        else:
            visible = int(self.form['show_customization'].value)
            if self.form.has_key('action'):
                if self.form['action'].value != 'Redisplay':
                    visible = self.form['action'].value == '+'
            
        return visible

    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'
        # see if the web has supplied us with any customisation info
        defaults = 1
        for key in ':sort', ':group', ':filter', ':columns':
            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
        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)
        return self.list(columns=columns, filter=filter, group=group,
            sort=sort, filterspec=filterspec)

    # 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):
        ''' call the template index with the args

            :sort    - sort by prop name, optionally preceeded with '-'
                     to give descending or nothing for ascending sorting.
            :group   - group by prop name, optionally preceeded with '-' or
                     to sort in descending or nothing for ascending order.
            :filter  - selects which props should be displayed in the filter
                     section. Default is all.
            :columns - selects the columns that should be displayed.
                     Default is all.

        '''
        cn = self.classname
        self.pagehead('Index of %s'%cn)
        if sort is None: sort = self.index_arg(':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()

        index = htmltemplate.IndexTemplate(self, self.TEMPLATES, cn)
        index.render(filterspec, filter, columns, sort, group,
            show_customization=show_customization)
        self.pagefoot()

    def shownode(self, message=None):
        ''' display an item
        '''
        cn = self.classname
        cl = self.db.classes[cn]

        # possibly perform an edit
        keys = self.form.keys()
        num_re = re.compile('^\d+$')
        if keys:
            try:
                props, changed = parsePropsFromForm(self.db, cl, self.form,
                    self.nodeid)
                cl.set(self.nodeid, **props)
                self._post_editnode(self.nodeid, changed)
                # and some nice feedback for the user
                message = '%s edited ok'%', '.join(changed)
            except:
                s = StringIO.StringIO()
                traceback.print_exc(None, s)
                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())

        # now the display
        id = self.nodeid
        if cl.getkey():
            id = cl.get(id, cl.getkey())
        self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)

        nodeid = self.nodeid

        # use the template to display the item
        item = htmltemplate.ItemTemplate(self, self.TEMPLATES, self.classname)
        item.render(nodeid)

        self.pagefoot()
    showissue = shownode
    showmsg = shownode

    def showuser(self, message=None):
        '''Display a user page for editing. Make sure the user is allowed
            to edit this node, and also check for password changes.
        '''
        if self.user == 'anonymous':
            raise Unauthorised

        user = self.db.user

        # get the username of the node being edited
        node_user = user.get(self.nodeid, 'username')

        if self.user not in ('admin', node_user):
            raise Unauthorised

        #
        # perform any editing
        #
        keys = self.form.keys()
        num_re = re.compile('^\d+$')
        if keys:
            try:
                props, changed = parsePropsFromForm(self.db, user, self.form,
                    self.nodeid)
                if self.nodeid == self.getuid() and 'password' in changed:
                    set_cookie = self.form['password'].value.strip()
                else:
                    set_cookie = 0
                user.set(self.nodeid, **props)
                self._post_editnode(self.nodeid, changed)
                # and some feedback for the user
                message = '%s edited ok'%', '.join(changed)
            except:
                s = StringIO.StringIO()
                traceback.print_exc(None, s)
                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
        else:
            set_cookie = 0

        # fix the cookie if the password has changed
        if set_cookie:
            self.set_cookie(self.user, set_cookie)

        #
        # now the display
        #
        self.pagehead('User: %s'%node_user, message)

        # use the template to display the item
        item = htmltemplate.ItemTemplate(self, self.TEMPLATES, 'user')
        item.render(self.nodeid)
        self.pagefoot()

    def showfile(self):
        ''' display a file
        '''
        nodeid = self.nodeid
        cl = self.db.file
        type = cl.get(nodeid, 'type')
        if type == 'message/rfc822':
            type = 'text/plain'
        self.header(headers={'Content-Type': type})
        self.write(cl.get(nodeid, 'content'))

    def _createnode(self):
        ''' create a node based on the contents of the form
        '''
        cl = self.db.classes[self.classname]
        props, dummy = parsePropsFromForm(self.db, cl, self.form)
        return cl.create(**props)

    def _post_editnode(self, nid, changes=None):
        ''' do the linking and message sending part of the node creation
        '''
        cn = self.classname
        cl = self.db.classes[cn]
        # link if necessary
        keys = self.form.keys()
        for key in keys:
            if key == ':multilink':
                value = self.form[key].value
                if type(value) != type([]): value = [value]
                for value in value:
                    designator, property = value.split(':')
                    link, nodeid = roundupdb.splitDesignator(designator)
                    link = self.db.classes[link]
                    value = link.get(nodeid, property)
                    value.append(nid)
                    link.set(nodeid, **{property: value})
            elif key == ':link':
                value = self.form[key].value
                if type(value) != type([]): value = [value]
                for value in value:
                    designator, property = value.split(':')
                    link, nodeid = roundupdb.splitDesignator(designator)
                    link = self.db.classes[link]
                    link.set(nodeid, **{property: nid})

        # generate an edit message
        # don't bother if there's no messages or nosy list 
        props = cl.getprops()
        note = None
        if self.form.has_key('__note'):
            note = self.form['__note']
            note = note.value
        send = len(cl.get(nid, 'nosy', [])) or note
        if (send and props.has_key('messages') and
                isinstance(props['messages'], hyperdb.Multilink) and
                props['messages'].classname == 'msg'):

            # handle the note
            if note:
                if '\n' in note:
                    summary = re.split(r'\n\r?', note)[0]
                else:
                    summary = note
                m = ['%s\n'%note]
            else:
                summary = 'This %s has been edited through the web.\n'%cn
                m = [summary]

            first = 1
            for name, prop in props.items():
                if changes is not None and name not in changes: continue
                if first:
                    m.append('\n-------')
                    first = 0
                value = cl.get(nid, name, None)
                if isinstance(prop, hyperdb.Link):
                    link = self.db.classes[prop.classname]
                    key = link.labelprop(default_to_id=1)
                    if value is not None and key:
                        value = link.get(value, key)
                    else:
                        value = '-'
                elif isinstance(prop, hyperdb.Multilink):
                    if value is None: value = []
                    l = []
                    link = self.db.classes[prop.classname]
                    key = link.labelprop(default_to_id=1)
                    for entry in value:
                        if key:
                            l.append(link.get(entry, key))
                        else:
                            l.append(entry)
                    value = ', '.join(l)
                m.append('%s: %s'%(name, value))

            # now create the message
            content = '\n'.join(m)
            message_id = self.db.msg.create(author=self.getuid(),
                recipients=[], date=date.Date('.'), summary=summary,
                content=content)
            messages = cl.get(nid, 'messages')
            messages.append(message_id)
            props = {'messages': messages}
            cl.set(nid, **props)

    def newnode(self, message=None):
        ''' Add a new node to the database.
        
        The form works in two modes: blank form and submission (that is,
        the submission goes to the same URL). **Eventually this means that
        the form will have previously entered information in it if
        submission fails.

        The new node will be created with the properties specified in the
        form submission. For multilinks, multiple form entries are handled,
        as are prop=value,value,value. You can't mix them though.

        If the new node is to be referenced from somewhere else immediately
        (ie. the new node is a file that is to be attached to a support
        issue) then supply one of these arguments in addition to the usual
        form entries:
            :link=designator:property
            :multilink=designator:property
        ... which means that once the new node is created, the "property"
        on the node given by "designator" should now reference the new
        node's id. The node id will be appended to the multilink.
        '''
        cn = self.classname
        cl = self.db.classes[cn]

        # possibly perform a create
        keys = self.form.keys()
        if [i for i in keys if i[0] != ':']:
            props = {}
            try:
                nid = self._createnode()
                self._post_editnode(nid)
                # and some nice feedback for the user
                message = '%s created ok'%cn
            except:
                s = StringIO.StringIO()
                traceback.print_exc(None, s)
                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
        self.pagehead('New %s'%self.classname.capitalize(), message)

        # call the template
        newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
            self.classname)
        newitem.render(self.form)

        self.pagefoot()
    newissue = newnode
    newuser = newnode

    def newfile(self, message=None):
        ''' Add a new file to the database.
        
        This form works very much the same way as newnode - it just has a
        file upload.
        '''
        cn = self.classname
        cl = self.db.classes[cn]

        # possibly perform a create
        keys = self.form.keys()
        if [i for i in keys if i[0] != ':']:
            try:
                file = self.form['content']
                type = mimetypes.guess_type(file.filename)[0]
                if not type:
                    type = "application/octet-stream"
                self._post_editnode(cl.create(content=file.file.read(),
                    type=type, name=file.filename))
                # and some nice feedback for the user
                message = '%s created ok'%cn
            except:
                s = StringIO.StringIO()
                traceback.print_exc(None, s)
                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())

        self.pagehead('New %s'%self.classname.capitalize(), message)
        newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
            self.classname)
        newitem.render(self.form)
        self.pagefoot()

    def classes(self, message=None):
        ''' display a list of all the classes in the database
        '''
        if self.user == 'admin':
            self.pagehead('Table of classes', message)
            classnames = self.db.classes.keys()
            classnames.sort()
            self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
            for cn in classnames:
                cl = self.db.getclass(cn)
                self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
                for key, value in cl.properties.items():
                    if value is None: value = ''
                    else: value = str(value)
                    self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
                        key, cgi.escape(value)))
            self.write('</table>')
            self.pagefoot()
        else:
            raise Unauthorised

    def login(self, message=None, newuser_form=None):
        self.pagehead('Login to roundup', message)
        self.write('''
<table>
<tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
<form action="login_action" method=POST>
<tr><td align=right>Login name: </td>
    <td><input name="__login_name"></td></tr>
<tr><td align=right>Password: </td>
    <td><input type="password" name="__login_password"></td></tr>
<tr><td></td>
    <td><input type="submit" value="Log In"></td></tr>
</form>
''')
        if self.user is None and self.ANONYMOUS_REGISTER == 'deny':
            self.write('</table>')
            self.pagefoot()
            return
        values = {'realname': '', 'organisation': '', 'address': '',
            'phone': '', 'username': '', 'password': '', 'confirm': ''}
        if newuser_form is not None:
            for key in newuser_form.keys():
                values[key] = newuser_form[key].value
        self.write('''
<p>
<tr><td colspan=2 class="strong-header">New User Registration</td></tr>
<tr><td colspan=2><em>marked items</em> are optional...</td></tr>
<form action="newuser_action" method=POST>
<tr><td align=right><em>Name: </em></td>
    <td><input name="realname" value="%(realname)s"></td></tr>
<tr><td align=right><em>Organisation: </em></td>
    <td><input name="organisation" value="%(organisation)s"></td></tr>
<tr><td align=right>E-Mail Address: </td>
    <td><input name="address" value="%(address)s"></td></tr>
<tr><td align=right><em>Phone: </em></td>
    <td><input name="phone" value="%(phone)s"></td></tr>
<tr><td align=right>Preferred Login name: </td>
    <td><input name="username" value="%(username)s"></td></tr>
<tr><td align=right>Password: </td>
    <td><input type="password" name="password" value="%(password)s"></td></tr>
<tr><td align=right>Password Again: </td>
    <td><input type="password" name="confirm" value="%(confirm)s"></td></tr>
<tr><td></td>
    <td><input type="submit" value="Register"></td></tr>
</form>
</table>
'''%values)
        self.pagefoot()

    def login_action(self, message=None):
        if not self.form.has_key('__login_name'):
            return self.login(message='Username required')
        self.user = self.form['__login_name'].value
        if self.form.has_key('__login_password'):
            password = self.form['__login_password'].value
        else:
            password = ''
        # make sure the user exists
        try:
            uid = self.db.user.lookup(self.user)
        except KeyError:
            name = self.user
            self.make_user_anonymous()
            return self.login(message='No such user "%s"'%name)

        # and that the password is correct
        pw = self.db.user.get(uid, 'password')
        if password != self.db.user.get(uid, 'password'):
            self.make_user_anonymous()
            return self.login(message='Incorrect password')

        self.set_cookie(self.user, password)
        return self.index()

    def set_cookie(self, user, password):
        # construct the cookie
        user = binascii.b2a_base64('%s:%s'%(user, password)).strip()
        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
        self.header({'Set-Cookie': 'roundup_user="%s"; Path="%s";'%(user,
            path)})

    def make_user_anonymous(self):
        # make us anonymous if we can
        try:
            self.db.user.lookup('anonymous')
            self.user = 'anonymous'
        except KeyError:
            self.user = None

    def logout(self, message=None):
        self.make_user_anonymous()
        # construct the logout cookie
        now = Cookie._getdate()
        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
        self.header({'Set-Cookie':
            'roundup_user=deleted; Max-Age=0; expires="%s"; Path="%s";'%(now,
            path)})
        return self.login()

    def newuser_action(self, message=None):
        ''' 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.user
        try:
            props, dummy = parsePropsFromForm(self.db, cl, self.form)
            uid = cl.create(**props)
        except ValueError, message:
            return self.login(message, newuser_form=self.form)
        self.user = cl.get(uid, 'username')
        password = cl.get(uid, 'password')
        self.set_cookie(self.user, self.form['password'].value)
        return self.index()

    def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
            nre=re.compile(r'new(\w+)')):

        # determine the uid to use
        self.db = self.instance.open('admin')
        cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
        user = 'anonymous'
        if (cookie.has_key('roundup_user') and
                cookie['roundup_user'].value != 'deleted'):
            cookie = cookie['roundup_user'].value
            user, password = binascii.a2b_base64(cookie).split(':')
            # make sure the user exists
            try:
                uid = self.db.user.lookup(user)
                # now validate the password
                if password != self.db.user.get(uid, 'password'):
                    user = 'anonymous'
            except KeyError:
                user = 'anonymous'

        # make sure the anonymous user is valid if we're using it
        if user == 'anonymous':
            self.make_user_anonymous()
        else:
            self.user = user
        self.db.close()

        # re-open the database for real, using the user
        self.db = self.instance.open(self.user)

        # now figure which function to call
        path = self.split_path
        if not path or path[0] in ('', 'index'):
            action = 'index'
        else:
            action = path[0]

        # Everthing ignores path[1:]
        #  - The file download link generator actually relies on this - it
        #    appends the name of the file to the URL so the download file name
        #    is correct, but doesn't actually use it.

        # everyone is allowed to try to log in
        if action == 'login_action':
            return self.login_action()

        # allow anonymous people to register
        if action == 'newuser_action':
            # if we don't have a login and anonymous people aren't allowed to
            # register, then spit up the login form
            if self.ANONYMOUS_REGISTER == 'deny' and self.user is None:
                return self.login()
            return self.newuser_action()

        # make sure totally anonymous access is OK
        if self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
            return self.login()

        # here be the "normal" functionality
        if action == 'index':
            return self.index()
        if action == 'list_classes':
            return self.classes()
        if action == 'login':
            return self.login()
        if action == 'logout':
            return self.logout()
        m = dre.match(action)
        if m:
            self.classname = m.group(1)
            self.nodeid = m.group(2)
            try:
                cl = self.db.classes[self.classname]
            except KeyError:
                raise NotFound
            try:
                cl.get(self.nodeid, 'id')
            except IndexError:
                raise NotFound
            try:
                func = getattr(self, 'show%s'%self.classname)
            except AttributeError:
                raise NotFound
            return func()
        m = nre.match(action)
        if m:
            self.classname = m.group(1)
            try:
                func = getattr(self, 'new%s'%self.classname)
            except AttributeError:
                raise NotFound
            return func()
        self.classname = action
        try:
            self.db.getclass(self.classname)
        except KeyError:
            raise NotFound
        self.list()

    def __del__(self):
        self.db.close()


class ExtendedClient(Client): 
    '''Includes pages and page heading information that relate to the
       extended schema.
    ''' 
    showsupport = Client.shownode
    showtimelog = Client.shownode
    newsupport = Client.newnode
    newtimelog = Client.newnode

    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']}

    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">%s</div>'%message
        else:
            message = ''
        style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
        user_name = self.user or ''
        if self.user == 'admin':
            admin_links = ' | <a href="list_classes">Class List</a>'
        else:
            admin_links = ''
        if self.user not in (None, 'anonymous'):
            userid = self.db.user.lookup(self.user)
            user_info = '''
<a href="issue?assignedto=%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=%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%s">My Details</a> | <a href="logout">Logout</a>
'''%(userid, userid, userid)
        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>,
<a href="newuser">User</a>
'''
        else:
            add_links = ''
        self.write('''<html><head>
<title>%s</title>
<style type="text/css">%s</style>
</head>
<body bgcolor=#ffffff>
%s
<table width=100%% border=0 cellspacing=0 cellpadding=2>
<tr class="location-bar"><td><big><strong>%s</strong></big></td>
<td align=right valign=bottom>%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>
%s
%s</td>
<td align=right>%s</td>
</table>
'''%(title, style, message, title, user_name, add_links, admin_links,
    user_info))

def parsePropsFromForm(db, cl, form, nodeid=0):
    '''Pull properties for the given class out of the form.
    '''
    props = {}
    changed = []
    keys = form.keys()
    num_re = re.compile('^\d+$')
    for key in keys:
        if not cl.properties.has_key(key):
            continue
        proptype = cl.properties[key]
        if isinstance(proptype, hyperdb.String):
            value = form[key].value.strip()
        elif isinstance(proptype, hyperdb.Password):
            value = password.Password(form[key].value.strip())
        elif isinstance(proptype, hyperdb.Date):
            value = date.Date(form[key].value.strip())
        elif isinstance(proptype, hyperdb.Interval):
            value = date.Interval(form[key].value.strip())
        elif isinstance(proptype, hyperdb.Link):
            value = form[key].value.strip()
            # see if it's the "no selection" choice
            if value == '-1':
                # don't set this property
                continue
            else:
                # handle key values
                link = cl.properties[key].classname
                if not num_re.match(value):
                    try:
                        value = db.classes[link].lookup(value)
                    except KeyError:
                        raise ValueError, 'property "%s": %s not a %s'%(
                            key, value, link)
        elif isinstance(proptype, hyperdb.Multilink):
            value = form[key]
            if type(value) != type([]):
                value = [i.strip() for i in value.value.split(',')]
            else:
                value = [i.value.strip() for i in value]
            link = cl.properties[key].classname
            l = []
            for entry in map(str, value):
                if not num_re.match(entry):
                    try:
                        entry = db.classes[link].lookup(entry)
                    except KeyError:
                        raise ValueError, \
                            'property "%s": "%s" not an entry of %s'%(key,
                            entry, link.capitalize())
                l.append(entry)
            l.sort()
            value = l
        props[key] = value
        # if changed, set it
        if nodeid and value != cl.get(nodeid, key):
            changed.append(key)
            props[key] = value
    return props, changed

#
# $Log: not supported by cvs2svn $
# Revision 1.50  2001/11/05 23:45:40  richard
# Fixed newuser_action so it sets the cookie with the unencrypted password.
# Also made it present nicer error messages (not tracebacks).
#
# Revision 1.49  2001/11/04 03:07:12  richard
# Fixed various cookie-related bugs:
#  . bug #477685 ] base64.decodestring breaks
#  . bug #477837 ] lynx does not like the cookie
#  . bug #477892 ] Password edit doesn't fix login cookie
# Also closed a security hole - a logged-in user could edit another user's
# details.
#
# Revision 1.48  2001/11/03 01:30:18  richard
# Oops. uses pagefoot now.
#
# Revision 1.47  2001/11/03 01:29:28  richard
# Login page didn't have all close tags.
#
# Revision 1.46  2001/11/03 01:26:55  richard
# possibly fix truncated base64'ed user:pass
#
# Revision 1.45  2001/11/01 22:04:37  richard
# Started work on supporting a pop3-fetching server
# Fixed bugs:
#  . bug #477104 ] HTML tag error in roundup-server
#  . bug #477107 ] HTTP header problem
#
# Revision 1.44  2001/10/28 23:03:08  richard
# Added more useful header to the classic schema.
#
# Revision 1.43  2001/10/24 00:01:42  richard
# More fixes to lockout logic.
#
# Revision 1.42  2001/10/23 23:56:03  richard
# HTML typo
#
# Revision 1.41  2001/10/23 23:52:35  richard
# Fixed lock-out logic, thanks Roch'e for pointing out the problems.
#
# Revision 1.40  2001/10/23 23:06:39  richard
# Some cleanup.
#
# Revision 1.39  2001/10/23 01:00:18  richard
# Re-enabled login and registration access after lopping them off via
# 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.
#
# 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
#  information is displayed.
#
# Revision 1.36  2001/10/21 04:44:50  richard
# bug #473124: UI inconsistency with Link fields.
#    This also prompted me to fix a fairly long-standing usability issue -
#    that of being able to turn off certain filters.
#
# Revision 1.35  2001/10/21 00:17:54  richard
# CGI interface view customisation section may now be hidden (patch from
#  Roch'e Compaan.)
#
# Revision 1.34  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.
#
# Revision 1.33  2001/10/17 00:18:41  richard
# Manually constructing cookie headers now.
#
# Revision 1.32  2001/10/16 03:36:21  richard
# CGI interface wasn't handling checkboxes at all.
#
# Revision 1.31  2001/10/14 10:55:00  richard
# Handle empty strings in HTML template Link function
#
# Revision 1.30  2001/10/09 07:38:58  richard
# Pushed the base code for the extended schema CGI interface back into the
# code cgi_client module so that future updates will be less painful.
# Also removed a debugging print statement from cgi_client.
#
# Revision 1.29  2001/10/09 07:25:59  richard
# Added the Password property type. See "pydoc roundup.password" for
# implementation details. Have updated some of the documentation too.
#
# Revision 1.28  2001/10/08 00:34:31  richard
# Change message was stuffing up for multilinks with no key property.
#
# Revision 1.27  2001/10/05 02:23:24  richard
#  . roundup-admin create now prompts for property info if none is supplied
#    on the command-line.
#  . hyperdb Class getprops() method may now return only the mutable
#    properties.
#  . Login now uses cookies, which makes it a whole lot more flexible. We can
#    now support anonymous user access (read-only, unless there's an
#    "anonymous" user, in which case write access is permitted). Login
#    handling has been moved into cgi_client.Client.main()
#  . The "extended" schema is now the default in roundup init.
#  . The schemas have had their page headings modified to cope with the new
#    login handling. Existing installations should copy the interfaces.py
#    file from the roundup lib directory to their instance home.
#  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
#    Ping - has been removed.
#  . Fixed a whole bunch of places in the CGI interface where we should have
#    been returning Not Found instead of throwing an exception.
#  . Fixed a deviation from the spec: trying to modify the 'id' property of
#    an item now throws an exception.
#
# Revision 1.26  2001/09/12 08:31:42  richard
# handle cases where mime type is not guessable
#
# Revision 1.25  2001/08/29 05:30:49  richard
# change messages weren't being saved when there was no-one on the nosy list.
#
# Revision 1.24  2001/08/29 04:49:39  richard
# didn't clean up fully after debugging :(
#
# Revision 1.23  2001/08/29 04:47:18  richard
# Fixed CGI client change messages so they actually include the properties
# changed (again).
#
# Revision 1.22  2001/08/17 00:08:10  richard
# reverted back to sending messages always regardless of who is doing the web
# edit. change notes weren't being saved. bleah. hackish.
#
# Revision 1.21  2001/08/15 23:43:18  richard
# Fixed some isFooTypes that I missed.
# Refactored some code in the CGI code.
#
# Revision 1.20  2001/08/12 06:32:36  richard
# using isinstance(blah, Foo) now instead of isFooType
#
# Revision 1.19  2001/08/07 00:24:42  richard
# stupid typo
#
# Revision 1.18  2001/08/07 00:15:51  richard
# Added the copyright/license notice to (nearly) all files at request of
# Bizar Software.
#
# Revision 1.17  2001/08/02 06:38:17  richard
# Roundupdb now appends "mailing list" information to its messages which
# include the e-mail address and web interface address. Templates may
# override this in their db classes to include specific information (support
# instructions, etc).
#
# Revision 1.16  2001/08/02 05:55:25  richard
# Web edit messages aren't sent to the person who did the edit any more. No
# message is generated if they are the only person on the nosy list.
#
# Revision 1.15  2001/08/02 00:34:10  richard
# bleah syntax error
#
# Revision 1.14  2001/08/02 00:26:16  richard
# Changed the order of the information in the message generated by web edits.
#
# Revision 1.13  2001/07/30 08:12:17  richard
# Added time logging and file uploading to the templates.
#
# Revision 1.12  2001/07/30 06:26:31  richard
# Added some documentation on how the newblah works.
#
# Revision 1.11  2001/07/30 06:17:45  richard
# Features:
#  . Added ability for cgi newblah forms to indicate that the new node
#    should be linked somewhere.
# Fixed:
#  . Fixed the agument handling for the roundup-admin find command.
#  . Fixed handling of summary when no note supplied for newblah. Again.
#  . Fixed detection of no form in htmltemplate Field display.
#
# Revision 1.10  2001/07/30 02:37:34  richard
# Temporary measure until we have decent schema migration...
#
# Revision 1.9  2001/07/30 01:25:07  richard
# Default implementation is now "classic" rather than "extended" as one would
# expect.
#
# Revision 1.8  2001/07/29 08:27:40  richard
# Fixed handling of passed-in values in form elements (ie. during a
# drill-down)
#
# Revision 1.7  2001/07/29 07:01:39  richard
# Added vim command to all source so that we don't get no steenkin' tabs :)
#
# Revision 1.6  2001/07/29 04:04:00  richard
# Moved some code around allowing for subclassing to change behaviour.
#
# Revision 1.5  2001/07/28 08:16:52  richard
# New issue form handles lack of note better now.
#
# Revision 1.4  2001/07/28 00:34:34  richard
# Fixed some non-string node ids.
#
# Revision 1.3  2001/07/23 03:56:30  richard
# oops, missed a config removal
#
# Revision 1.2  2001/07/22 12:09:32  richard
# Final commit of Grande Splite
#
# Revision 1.1  2001/07/22 11:58:35  richard
# More Grande Splite
#
#
# vim: set filetype=python ts=4 sw=4 et si

Roundup Issue Tracker: http://roundup-tracker.org/