diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py
index 33d2381fcc9f64840f1abee93563bdc19f3550a7..c978b3d94fcd147abdbb582819b0b793b9fba0d7 100644 (file)
--- a/roundup/cgi_client.py
+++ b/roundup/cgi_client.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: cgi_client.py,v 1.27 2001-10-05 02:23:24 richard Exp $
+# $Id: cgi_client.py,v 1.40 2001-10-23 23:06:39 richard Exp $
import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
import base64, Cookie, time
-import roundupdb, htmltemplate, date, hyperdb
+import roundupdb, htmltemplate, date, hyperdb, password
class Unauthorised(ValueError):
pass
class Client:
'''
-
A note about login
------------------
'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, out, env):
self.instance = instance
return arg.value.split(',')
return []
- def index_filterspec(self):
+ def index_filterspec(self, filter):
''' pull the index filter spec from the form
Links and multilinks want to be lists - the rest are straight
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
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 = []
+ 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'
- if self.form.has_key(':sort'): sort = self.index_arg(':sort')
- else: sort = self.default_index_sort
- if self.form.has_key(':group'): group = self.index_arg(':group')
- else: group = self.default_index_group
- if self.form.has_key(':filter'): filter = self.index_arg(':filter')
- else: filter = self.default_index_filter
- if self.form.has_key(':columns'): columns = self.index_arg(':columns')
- else: columns = self.default_index_columns
- filterspec = self.index_filterspec()
- if not filterspec:
+ # 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):
+ filterspec=None, show_customization=None):
''' call the template index with the args
:sort - sort by prop name, optionally preceeded with '-'
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()
+ if filterspec is None: filterspec = self.index_filterspec(filter)
+ 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()
def shownode(self, message=None):
num_re = re.compile('^\d+$')
if keys:
try:
- props, changed = parsePropsFromForm(cl, self.form, self.nodeid)
+ 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
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
''' create a node based on the contents of the form
'''
cl = self.db.classes[self.classname]
- props, dummy = parsePropsFromForm(cl, self.form)
+ props, dummy = parsePropsFromForm(self.db, cl, self.form)
return cl.create(**props)
def _post_editnode(self, nid, changes=None):
key = link.labelprop(default_to_id=1)
for entry in value:
if key:
- l.append(link.get(entry, link.getkey()))
+ l.append(link.get(entry, key))
else:
l.append(entry)
value = ', '.join(l)
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
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):
<tr><td></td>
<td><input type="submit" value="Log In"></td></tr>
</form>
-
+''')
+ if self.user is None and not self.ANONYMOUS_REGISTER == 'deny':
+ self.write('</table')
+ return
+ 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>
''')
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
- password = self.form['__login_password'].value
+ if self.form.has_key('__login_password'):
+ 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)
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')
# construct the cookie
user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
''))
- cookie = Cookie.SmartCookie()
- cookie['roundup_user'] = user
- cookie['roundup_user']['path'] = path
- self.header({'Set-Cookie': str(cookie)})
+ self.header({'Set-Cookie': 'roundup_user=%s; Path=%s;'%(user, path)})
return self.index()
def make_user_anonymous(self):
# construct the logout cookie
path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
''))
- cookie = Cookie.SmartCookie()
- cookie['roundup_user'] = 'deleted'
- cookie['roundup_user']['path'] = path
- cookie['roundup_user']['expires'] = 0
- cookie['roundup_user']['max-age'] = 0
- self.header({'Set-Cookie': str(cookie)})
+ now = Cookie._getdate()
+ self.header({'Set-Cookie':
+ 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
return self.index()
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.classes['user']
- props, dummy = parsePropsFromForm(cl, self.form)
+ props, dummy = parsePropsFromForm(self.db, cl, self.form)
uid = cl.create(**props)
self.user = self.db.user.get(uid, 'username')
password = self.db.user.get(uid, 'password')
user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
''))
- cookie = Cookie.SmartCookie()
- cookie['roundup_user'] = user
- cookie['roundup_user']['path'] = path
- self.header({'Set-Cookie': str(cookie)})
+ self.header({'Set-Cookie': 'roundup_user=%s; Path=%s;'%(user, path)})
return self.index()
def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
# now figure which function to call
path = self.split_path
if not path or path[0] in ('', 'index'):
- self.index()
- elif len(path) == 1:
- if path[0] == 'list_classes':
- self.classes()
- return
- if path[0] == 'login':
- self.login()
- return
- if path[0] == 'login_action':
- self.login_action()
- return
- if path[0] == 'newuser_action':
- self.newuser_action()
- return
- if path[0] == 'logout':
- self.logout()
- return
- m = dre.match(path[0])
- 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:
- getattr(self, 'show%s'%self.classname)()
- except AttributeError:
- raise NotFound
- return
- m = nre.match(path[0])
- if m:
- self.classname = m.group(1)
- try:
- getattr(self, 'new%s'%self.classname)()
- except AttributeError:
- raise NotFound
- return
- self.classname = path[0]
+ return self.index()
+ elif not path:
+ raise 'ValueError', 'Path not understood'
+
+ #
+ # 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.
+ action = path[0]
+ if action == 'login_action':
+ return self.login_action()
+
+ # make sure anonymous are allowed to register
+ if self.ANONYMOUS_REGISTER == 'deny' and self.user is None:
+ return self.login()
+
+ if action == 'newuser_action':
+ return self.newuser_action()
+
+ # make sure totally anonymous access is OK
+ if self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
+ return self.login()
+
+ 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:
- self.db.getclass(self.classname)
+ cl = self.db.classes[self.classname]
except KeyError:
raise NotFound
- self.list()
- else:
- raise 'ValueError', 'Path not understood'
+ 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()
-def parsePropsFromForm(cl, form, nodeid=0):
+
+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 = {}
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()
- # handle key values
- link = cl.properties[key].classname
- if not num_re.match(value):
- try:
- value = self.db.classes[link].lookup(value)
- except:
- raise ValueError, 'property "%s": %s not a %s'%(
- key, value, link)
+ # 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([]):
for entry in map(str, value):
if not num_re.match(entry):
try:
- entry = self.db.classes[link].lookup(entry)
- except:
+ entry = db.classes[link].lookup(entry)
+ except KeyError:
raise ValueError, \
- 'property "%s": %s not a %s'%(key,
- entry, link)
+ 'property "%s": "%s" not an entry of %s'%(key,
+ entry, link.capitalize())
l.append(entry)
l.sort()
value = l
#
# $Log: not supported by cvs2svn $
+# 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
#