diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py
index e6a69c27a0d26db47350001f7068b83b5d8014ed..b471c1b206be9611455fc61719447ae5f0440447 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.113 2002-03-14 23:59:24 richard Exp $
+# $Id: cgi_client.py,v 1.134 2002-07-09 04:19:09 richard Exp $
__doc__ = """
WWW request handler (also used in the stand-alone server).
'''
def __init__(self, instance, request, env, form=None):
+ hyperdb.traceMark()
self.instance = instance
self.request = request
self.env = env
self.debug = 0
def getuid(self):
- return self.db.user.lookup(self.user)
+ try:
+ return self.db.user.lookup(self.user)
+ except KeyError:
+ if self.user is None:
+ # user is not logged in and username 'anonymous' doesn't
+ # exist in the database
+ err = _('anonymous users have read-only access only')
+ else:
+ err = _("sanity check: unknown user name `%s'")%self.user
+ raise Unauthorised, errmsg
def header(self, headers=None):
'''Put up the appropriate header.
d[':group'] = ','.join(map(urllib.quote, spec['GROUP']))
d[':filter'] = ','.join(map(urllib.quote, spec['FILTER']))
d[':columns'] = ','.join(map(urllib.quote, spec['COLUMNS']))
+ d[':pagesize'] = spec.get('PAGESIZE','50')
# snarf the filterspec
filterspec = spec['FILTERSPEC'].copy()
# now format the filterspec
for k, l in filterspec.items():
- # fix up the assignedto if needed
- if k == 'assignedto' and l is None:
+ # fix up the CURRENT USER if needed (handle None too since that's
+ # the old flag value)
+ if l in (None, 'CURRENT USER'):
+ if not self.user:
+ continue
l = [self.db.user.lookup(self.user)]
# add
# skip if we need to fill in the logged-in user id there's
# no user logged in
if (spec['FILTERSPEC'].has_key('assignedto') and
- spec['FILTERSPEC']['assignedto'] is None and
- userid is None):
+ spec['FILTERSPEC']['assignedto'] in ('CURRENT USER',
+ None) and userid is None):
continue
links.append(self.make_index_link(name))
else:
links.append(_('<a href="user">User List</a>'))
links.append(_('<a href="newuser">Add User</a>'))
+ # add the search links
+ if hasattr(self.instance, 'HEADER_SEARCH_LINKS'):
+ classes = self.instance.HEADER_SEARCH_LINKS
+ else:
+ classes = ['issue']
+ l = []
+ for class_name in classes:
+ cap_class = class_name.capitalize()
+ links.append(_('Search <a href="search%(class_name)s">'
+ '%(cap_class)s</a>')%locals())
+
# now we have all the links, join 'em
links = '\n | '.join(links)
<tr class="location-bar">
<td align=left>%(links)s</td>
<td align=right>%(user_info)s</td>
-</table>
+</table><br>
''')%locals())
def pagefoot(self):
return arg.value.split(',')
return []
+ def index_sort(self):
+ # first try query string
+ x = self.index_arg(':sort')
+ if x:
+ return x
+ # nope - get the specs out of the form
+ specs = []
+ for colnm in self.db.getclass(self.classname).getprops().keys():
+ desc = ''
+ try:
+ spec = self.form[':%s_ss' % colnm]
+ except KeyError:
+ continue
+ spec = spec.value
+ if spec:
+ if spec[-1] == '-':
+ desc='-'
+ spec = spec[0]
+ specs.append((int(spec), colnm, desc))
+ specs.sort()
+ x = []
+ for _, colnm, desc in specs:
+ x.append('%s%s' % (desc, colnm))
+ return x
+
def index_filterspec(self, filter):
''' pull the index filter spec from the form
Links and multilinks want to be lists - the rest are straight
strings.
'''
- props = self.db.classes[self.classname].getprops()
- # all the form args not starting with ':' are filters
filterspec = {}
- for key in self.form.keys():
- if key[0] == ':': continue
- if not props.has_key(key): continue
- if key not in filter: continue
- prop = props[key]
- value = self.form[key]
- if (isinstance(prop, hyperdb.Link) or
- isinstance(prop, hyperdb.Multilink)):
- if type(value) == type([]):
- value = [arg.value for arg in value]
+ props = self.db.classes[self.classname].getprops()
+ for colnm in filter:
+ widget = ':%s_fs' % colnm
+ try:
+ val = self.form[widget]
+ except KeyError:
+ try:
+ val = self.form[colnm]
+ except KeyError:
+ # they checked the filter box but didn't enter a value
+ continue
+ propdescr = props.get(colnm, None)
+ if propdescr is None:
+ print "huh? %r is in filter & form, but not in Class!" % colnm
+ raise "butthead programmer"
+ if (isinstance(propdescr, hyperdb.Link) or
+ isinstance(propdescr, hyperdb.Multilink)):
+ if type(val) == type([]):
+ val = [arg.value for arg in val]
else:
- value = value.value.split(',')
- l = filterspec.get(key, [])
- l = l + value
- filterspec[key] = l
+ val = val.value.split(',')
+ l = filterspec.get(colnm, [])
+ l = l + val
+ filterspec[colnm] = l
else:
- filterspec[key] = value.value
+ filterspec[colnm] = val.value
+
return filterspec
-
+
def customization_widget(self):
''' The customization widget is visible by default. The widget
visibility is remembered by show_customization. Visibility
default_index_filter = ['status']
default_index_columns = ['id','activity','title','status','assignedto']
default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
+ default_pagesize = '50'
- def index(self):
- ''' put up an index - no class specified
- '''
+ def _get_customisation_info(self):
# see if the web has supplied us with any customisation info
defaults = 1
- for key in ':sort', ':group', ':filter', ':columns':
+ for key in ':sort', ':group', ':filter', ':columns', ':pagesize':
if self.form.has_key(key):
defaults = 0
break
if defaults:
# try the instance config first
- if hasattr(self.instance, 'DEFAULT_INDEX_CLASS'):
- self.classname = self.instance.DEFAULT_INDEX_CLASS
- sort = self.instance.DEFAULT_INDEX_SORT
- group = self.instance.DEFAULT_INDEX_GROUP
- filter = self.instance.DEFAULT_INDEX_FILTER
- columns = self.instance.DEFAULT_INDEX_COLUMNS
- filterspec = self.instance.DEFAULT_INDEX_FILTERSPEC
+ if hasattr(self.instance, 'DEFAULT_INDEX'):
+ d = self.instance.DEFAULT_INDEX
+ self.classname = d['CLASS']
+ sort = d['SORT']
+ group = d['GROUP']
+ filter = d['FILTER']
+ columns = d['COLUMNS']
+ filterspec = d['FILTERSPEC']
+ pagesize = d.get('PAGESIZE', '50')
else:
# nope - fall back on the old way of doing it
filter = self.default_index_filter
columns = self.default_index_columns
filterspec = self.default_index_filterspec
+ pagesize = self.default_pagesize
else:
# make list() extract the info from the CGI environ
self.classname = 'issue'
- sort = group = filter = columns = filterspec = None
+ sort = group = filter = columns = filterspec = pagesize = None
+ return columns, filter, group, sort, filterspec, pagesize
+
+ def index(self):
+ ''' put up an index - no class specified
+ '''
+ columns, filter, group, sort, filterspec, pagesize = \
+ self._get_customisation_info()
return self.list(columns=columns, filter=filter, group=group,
- sort=sort, filterspec=filterspec)
+ sort=sort, filterspec=filterspec, pagesize=pagesize)
+
+ def searchnode(self):
+ columns, filter, group, sort, filterspec, pagesize = \
+ self._get_customisation_info()
+## show_nodes = 1
+## if len(self.form.keys()) == 0:
+## # get the default search filters from instance_config
+## if hasattr(self.instance, 'SEARCH_FILTERS'):
+## for f in self.instance.SEARCH_FILTERS:
+## spec = getattr(self.instance, f)
+## if spec['CLASS'] == self.classname:
+## filter = spec['FILTER']
+##
+## show_nodes = 0
+## show_customization = 1
+## return self.list(columns=columns, filter=filter, group=group,
+## sort=sort, filterspec=filterspec,
+## show_customization=show_customization, show_nodes=show_nodes,
+## pagesize=pagesize)
+ cn = self.classname
+ self.pagehead(_('%(instancename)s: Index of %(classname)s')%{
+ 'classname': cn, 'instancename': self.instance.INSTANCE_NAME})
+ index = htmltemplate.IndexTemplate(self, self.instance.TEMPLATES, cn)
+ self.write('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
+ all_columns = self.db.getclass(cn).getprops().keys()
+ all_columns.sort()
+ index.filter_section('', filter, columns, group, all_columns, sort,
+ filterspec, pagesize, 0)
+ self.pagefoot()
+ index.db = index.cl = index.properties = None
+ index.clear()
# XXX deviates from spec - loses the '+' (that's a reserved character
# in URLS
def list(self, sort=None, group=None, filter=None, columns=None,
- filterspec=None, show_customization=None):
+ filterspec=None, show_customization=None, show_nodes=1, pagesize=None):
''' call the template index with the args
:sort - sort by prop name, optionally preceeded with '-'
cl = self.db.classes[cn]
self.pagehead(_('%(instancename)s: Index of %(classname)s')%{
'classname': cn, 'instancename': self.instance.INSTANCE_NAME})
- if sort is None: sort = self.index_arg(':sort')
+ if sort is None: sort = self.index_sort()
if group is None: group = self.index_arg(':group')
if filter is None: filter = self.index_arg(':filter')
if columns is None: columns = self.index_arg(':columns')
if filterspec is None: filterspec = self.index_filterspec(filter)
if show_customization is None:
show_customization = self.customization_widget()
+ if self.form.has_key('search_text'):
+ search_text = self.form['search_text'].value
+ else:
+ search_text = ''
+ if pagesize is None:
+ if self.form.has_key(':pagesize'):
+ pagesize = self.form[':pagesize'].value
+ else:
+ pagesize = '50'
+ pagesize = int(pagesize)
+ if self.form.has_key(':startwith'):
+ startwith = int(self.form[':startwith'].value)
+ else:
+ startwith = 0
index = htmltemplate.IndexTemplate(self, self.instance.TEMPLATES, cn)
try:
- index.render(filterspec, filter, columns, sort, group,
- show_customization=show_customization)
+ index.render(filterspec, search_text, filter, columns, sort,
+ group, show_customization=show_customization,
+ show_nodes=show_nodes, pagesize=pagesize, startwith=startwith)
except htmltemplate.MissingTemplateError:
self.basicClassEditPage()
self.pagefoot()
cn = self.form['classname'].value
cl = self.db.classes[cn]
props = self.form['properties'].value.split(',')
+ if cl.labelprop(1) in props:
+ sort = [cl.labelprop(1)]
+ else:
+ sort = props[0]
w('<table border=1 cellspacing=0 cellpaddin=2>')
w('<tr>')
for name in props:
w('<th align=left>%s</th>'%name)
w('</tr>')
- for nodeid in cl.list():
+ for nodeid in cl.filter(None, {}, sort, []): #cl.list():
w('<tr>')
for name in props:
value = cgi.escape(str(cl.get(nodeid, name)))
w('</tr>')
w('</table>')
- def shownode(self, message=None):
+ def shownode(self, message=None, num_re=re.compile('^\d+$')):
''' display an item
'''
cn = self.classname
cl = self.db.classes[cn]
+ if self.form.has_key(':multilink'):
+ link = self.form[':multilink'].value
+ designator, linkprop = link.split(':')
+ xtra = ' for <a href="%s">%s</a>' % (designator, designator)
+ else:
+ xtra = ''
# possibly perform an edit
keys = self.form.keys()
- num_re = re.compile('^\d+$')
# don't try to set properties if the user has just logged in
if keys and not self.form.has_key('__login_name'):
try:
id = self.nodeid
if cl.getkey():
id = cl.get(id, cl.getkey())
- self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
+ self.pagehead('%s: %s %s'%(self.classname.capitalize(), id, xtra), message)
nodeid = self.nodeid
self.pagefoot()
showissue = shownode
showmsg = shownode
-
- def _add_assignedto_to_nosy(self, props):
- ''' add the assignedto value from the props to the nosy list
- '''
- if not props.has_key('assignedto'):
- return
- assignedto_id = props['assignedto']
- if not props.has_key('nosy'):
- # load current nosy
- if self.nodeid:
- cl = self.db.classes[self.classname]
- l = cl.get(self.nodeid, 'nosy')
- if assignedto_id in l:
- return
- props['nosy'] = l
- else:
- props['nosy'] = []
- if assignedto_id not in props['nosy']:
- props['nosy'].append(assignedto_id)
+ searchissue = searchnode
def _changenode(self, props):
''' change the node based on the contents of the form
'''
cl = self.db.classes[self.classname]
- # set status to chatting if 'unread' or 'resolved'
- try:
- # determine the id of 'unread','resolved' and 'chatting'
- unread_id = self.db.status.lookup('unread')
- resolved_id = self.db.status.lookup('resolved')
- chatting_id = self.db.status.lookup('chatting')
- current_status = cl.get(self.nodeid, 'status')
- if props.has_key('status'):
- new_status = props['status']
- else:
- # apparently there's a chance that some browsers don't
- # send status...
- new_status = current_status
- except KeyError:
- pass
- else:
- if new_status == unread_id or (new_status == resolved_id
- and current_status == resolved_id):
- props['status'] = chatting_id
-
- self._add_assignedto_to_nosy(props)
# create the message
message, files = self._handle_message()
cl = self.db.classes[self.classname]
props = parsePropsFromForm(self.db, cl, self.form)
- # set status to 'unread' if not specified - a status of '- no
- # selection -' doesn't make sense
- if not props.has_key('status') and cl.getprops().has_key('status'):
- try:
- unread_id = self.db.status.lookup('unread')
- except KeyError:
- pass
- else:
- props['status'] = unread_id
-
- self._add_assignedto_to_nosy(props)
-
# check for messages and files
message, files = self._handle_message()
if message:
# in a nutshell, don't do anything if there's no note or there's no
# NOSY
if self.form.has_key('__note'):
- note = self.form['__note'].value
+ note = self.form['__note'].value.strip()
+ if not note:
+ return None, files
if not props.has_key('messages'):
return None, files
if not isinstance(props['messages'], hyperdb.Multilink):
return None, files
# handle the note
- if note:
- if '\n' in note:
- summary = re.split(r'\n\r?', note)[0]
- else:
- summary = note
- m = ['%s\n'%note]
- elif not files:
- # don't generate a useless message
- return None, files
+ if '\n' in note:
+ summary = re.split(r'\n\r?', note)[0]
+ else:
+ summary = note
+ m = ['%s\n'%note]
# handle the messageid
# TODO: handle inreplyto
designator, property = value.split(':')
link, nodeid = roundupdb.splitDesignator(designator)
link = self.db.classes[link]
- value = link.get(nodeid, property)
+ # take a dupe of the list so we're not changing the cache
+ value = link.get(nodeid, property)[:]
value.append(nid)
link.set(nodeid, **{property: value})
elif key == ':link':
'''
cn = self.classname
cl = self.db.classes[cn]
+ if self.form.has_key(':multilink'):
+ link = self.form[':multilink'].value
+ designator, linkprop = link.split(':')
+ xtra = ' for <a href="%s">%s</a>' % (designator, designator)
+ else:
+ xtra = ''
# possibly perform a create
keys = self.form.keys()
s = StringIO.StringIO()
traceback.print_exc(None, s)
message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
- self.pagehead(_('New %(classname)s')%{'classname':
- self.classname.capitalize()}, message)
+ self.pagehead(_('New %(classname)s %(xtra)s')%{
+ 'classname': self.classname.capitalize(),
+ 'xtra': xtra }, message)
# call the template
newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES,
'''
cn = self.classname
cl = self.db.classes[cn]
+ props = parsePropsFromForm(self.db, cl, self.form)
+ if self.form.has_key(':multilink'):
+ link = self.form[':multilink'].value
+ designator, linkprop = link.split(':')
+ xtra = ' for <a href="%s">%s</a>' % (designator, designator)
+ else:
+ xtra = ''
# possibly perform a create
keys = self.form.keys()
if not mime_type:
mime_type = "application/octet-stream"
# save the file
- nid = cl.create(content=file.file.read(), type=mime_type,
- name=file.filename)
+ props['type'] = mime_type
+ props['name'] = file.filename
+ props['content'] = file.file.read()
+ nid = cl.create(**props)
# handle linked nodes
self._post_editnode(nid)
# and some nice feedback for the user
traceback.print_exc(None, s)
message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
- self.pagehead(_('New %(classname)s')%{'classname':
- self.classname.capitalize()}, message)
+ self.pagehead(_('New %(classname)s %(xtra)s')%{
+ 'classname': self.classname.capitalize(),
+ 'xtra': xtra }, message)
newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES,
self.classname)
newitem.render(self.form)
self.pagefoot()
- def showuser(self, message=None):
+ def showuser(self, message=None, num_re=re.compile('^\d+$')):
'''Display a user page for editing. Make sure the user is allowed
to edit this node, and also check for password changes.
'''
# perform any editing
#
keys = self.form.keys()
- num_re = re.compile('^\d+$')
if keys:
try:
props = parsePropsFromForm(self.db, user, self.form,
''' display a file
'''
nodeid = self.nodeid
- cl = self.db.file
+ cl = self.db.classes[self.classname]
mime_type = cl.get(nodeid, 'type')
if mime_type == 'message/rfc822':
mime_type = 'text/plain'
self.db.commit()
def do_action(self, action, dre=re.compile(r'([^\d]+)(\d+)'),
- nre=re.compile(r'new(\w+)')):
+ nre=re.compile(r'new(\w+)'), sre=re.compile(r'search(\w+)')):
'''Figure the user's action and do it.
'''
# here be the "normal" functionality
try:
cl = self.db.classes[self.classname]
except KeyError:
- raise NotFound
+ raise NotFound, self.classname
try:
cl.get(self.nodeid, 'id')
except IndexError:
- raise NotFound
+ raise NotFound, self.nodeid
try:
func = getattr(self, 'show%s'%self.classname)
except AttributeError:
- raise NotFound
+ raise NotFound, 'show%s'%self.classname
func()
return
self.classname = m.group(1)
try:
func = getattr(self, 'new%s'%self.classname)
+ except AttributeError:
+ raise NotFound, 'new%s'%self.classname
+ func()
+ return
+
+ # see if we're to put up the new node page
+ m = sre.match(action)
+ if m:
+ self.classname = m.group(1)
+ try:
+ func = getattr(self, 'search%s'%self.classname)
except AttributeError:
raise NotFound
func()
try:
self.db.getclass(self.classname)
except KeyError:
- raise NotFound
+ raise NotFound, self.classname
self.list()
showtimelog = Client.shownode
newsupport = Client.newnode
newtimelog = Client.newnode
+ searchsupport = Client.searchnode
default_index_sort = ['-activity']
default_index_group = ['priority']
default_index_filter = ['status']
default_index_columns = ['activity','status','title','assignedto']
default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
+ default_pagesize = '50'
-def parsePropsFromForm(db, cl, form, nodeid=0):
+def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
'''Pull properties for the given class out of the form.
'''
props = {}
keys = form.keys()
- num_re = re.compile('^\d+$')
for key in keys:
if not cl.properties.has_key(key):
continue
#
# $Log: not supported by cvs2svn $
+# Revision 1.133 2002/07/08 15:32:05 gmcm
+# Pagination of index pages.
+# New search form.
+#
+# Revision 1.132 2002/07/08 07:26:14 richard
+# ehem
+#
+# Revision 1.131 2002/07/08 06:53:57 richard
+# Not sure why the cgi_client had an indexer argument.
+#
+# Revision 1.130 2002/06/27 12:01:53 gmcm
+# If the form has a :multilink, put a back href in the pageheader (back to the linked-to node).
+# Some minor optimizations (only compile regexes once).
+#
+# Revision 1.129 2002/06/20 23:52:11 richard
+# Better handling of unauth attempt to edit stuff
+#
+# Revision 1.128 2002/06/12 21:28:25 gmcm
+# Allow form to set user-properties on a Fileclass.
+# Don't assume that a Fileclass is named "files".
+#
+# Revision 1.127 2002/06/11 06:38:24 richard
+# . #565996 ] The "Attach a File to this Issue" fails
+#
+# Revision 1.126 2002/05/29 01:16:17 richard
+# Sorry about this huge checkin! It's fixing a lot of related stuff in one go
+# though.
+#
+# . #541941 ] changing multilink properties by mail
+# . #526730 ] search for messages capability
+# . #505180 ] split MailGW.handle_Message
+# - also changed cgi client since it was duplicating the functionality
+# . build htmlbase if tests are run using CVS checkout (removed note from
+# installation.txt)
+# . don't create an empty message on email issue creation if the email is empty
+#
+# Revision 1.125 2002/05/25 07:16:24 rochecompaan
+# Merged search_indexing-branch with HEAD
+#
+# Revision 1.124 2002/05/24 02:09:24 richard
+# Nothing like a live demo to show up the bugs ;)
+#
+# Revision 1.123 2002/05/22 05:04:13 richard
+# Oops
+#
+# Revision 1.122 2002/05/22 04:12:05 richard
+# . applied patch #558876 ] cgi client customization
+# ... with significant additions and modifications ;)
+# - extended handling of ML assignedto to all places it's handled
+# - added more NotFound info
+#
+# Revision 1.121 2002/05/21 06:08:10 richard
+# Handle migration
+#
+# Revision 1.120 2002/05/21 06:05:53 richard
+# . #551483 ] assignedto in Client.make_index_link
+#
+# Revision 1.119 2002/05/15 06:21:21 richard
+# . node caching now works, and gives a small boost in performance
+#
+# As a part of this, I cleaned up the DEBUG output and implemented TRACE
+# output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
+# CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
+# (using if __debug__ which is compiled out with -O)
+#
+# Revision 1.118 2002/05/12 23:46:33 richard
+# ehem, part 2
+#
+# Revision 1.117 2002/05/12 23:42:29 richard
+# ehem
+#
+# Revision 1.116 2002/05/02 08:07:49 richard
+# Added the ADD_AUTHOR_TO_NOSY handling to the CGI interface.
+#
+# Revision 1.115 2002/04/02 01:56:10 richard
+# . stop sending blank (whitespace-only) notes
+#
+# Revision 1.114.2.4 2002/05/02 11:49:18 rochecompaan
+# Allow customization of the search filters that should be displayed
+# on the search page.
+#
+# Revision 1.114.2.3 2002/04/20 13:23:31 rochecompaan
+# We now have a separate search page for nodes. Search links for
+# different classes can be customized in instance_config similar to
+# index links.
+#
+# Revision 1.114.2.2 2002/04/19 19:54:42 rochecompaan
+# cgi_client.py
+# removed search link for the time being
+# moved rendering of matches to htmltemplate
+# hyperdb.py
+# filtering of nodes on full text search incorporated in filter method
+# roundupdb.py
+# added paramater to call of filter method
+# roundup_indexer.py
+# added search method to RoundupIndexer class
+#
+# Revision 1.114.2.1 2002/04/03 11:55:57 rochecompaan
+# . Added feature #526730 - search for messages capability
+#
+# Revision 1.114 2002/03/17 23:06:05 richard
+# oops
+#
+# Revision 1.113 2002/03/14 23:59:24 richard
+# . #517734 ] web header customisation is obscure
+#
# Revision 1.112 2002/03/12 22:52:26 richard
# more pychecker warnings removed
#