diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py
index d4cdaf5891a4cd802acf8ba8968a44d9bf87e1cd..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.30 2001-10-09 07:38:58 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
'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):
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)
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()
default_index_sort = ['-activity']
default_index_group = ['priority']
- default_index_filter = []
+ default_index_filter = ['status']
default_index_columns = ['activity','status','title','assignedto']
default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
if self.user not in (None, 'anonymous'):
userid = self.db.user.lookup(self.user)
user_info = '''
-<a href="issue?assignedto=%s&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority">My Issues</a> |
-<a href="support?assignedto=%s&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername">My Support</a> |
+<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:
<td align=right valign=bottom>%s</td></tr>
<tr class="location-bar">
<td align=left>All
-<a href="issue?status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority">Issues</a>,
-<a href="support?status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername">Support</a>
+<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=admin&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority">Issues</a>,
-<a href="support?assignedto=admin&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername">Support</a>
+<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>
'''%(title, style, message, title, user_name, add_links, admin_links,
user_info))
-def parsePropsFromForm(cl, form, nodeid=0):
+def parsePropsFromForm(db, cl, form, nodeid=0):
'''Pull properties for the given class out of the form.
'''
props = {}
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.