diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py
index 1c000403a418b316e517b61f92a963ae0a7e6264..1cb3113307c71a1dfc8ddd60af2eb571941bbe54 100644 (file)
--- a/roundup/cgi_client.py
+++ b/roundup/cgi_client.py
-# $Id: cgi_client.py,v 1.8 2001-07-29 08:27:40 richard Exp $
+#
+# 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.26 2001-09-12 08:31:42 richard Exp $
-import os, cgi, pprint, StringIO, urlparse, re, traceback
+import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
-import roundupdb, htmltemplate, date
+import roundupdb, htmltemplate, date, hyperdb
class Unauthorised(ValueError):
pass
self.headers_done = 0
self.debug = 0
+ def getuid(self):
+ return self.db.user.lookup(self.user)
+
def header(self, headers={'Content-Type':'text/html'}):
if not headers.has_key('Content-Type'):
headers['Content-Type'] = 'text/html'
message = ''
style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
userid = self.db.user.lookup(self.user)
- if self.user == 'admin':
- extras = ' | <a href="list_classes">Class List</a>'
- else:
- extras = ''
self.write('''<html><head>
<title>%s</title>
<style type="text/css">%s</style>
<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><a href="issue?status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=activity,status,title&:group=priority">All issues</a> |
-<a href="issue?priority=fatal-bug,bug">Bugs</a> |
-<a href="issue?priority=usability">Support</a> |
-<a href="issue?priority=feature">Wishlist</a> |
-<a href="newissue">New Issue</a>
-%s</td>
-<td align=right><a href="user%s">Your Details</a></td>
+<tr class="location-bar"><td><big><strong>%s</strong></big>
+(login: <a href="user%s">%s</a>)</td></tr>
</table>
-'''%(title, style, message, title, self.user, extras, userid))
+'''%(title, style, message, title, userid, self.user))
def pagefoot(self):
if self.debug:
if key[0] == ':': continue
prop = props[key]
value = self.form[key]
- if prop.isLinkType or prop.isMultilinkType:
+ if (isinstance(prop, hyperdb.Link) or
+ isinstance(prop, hyperdb.Multilink)):
if type(value) == type([]):
value = [arg.value for arg in value]
else:
default_index_sort = ['-activity']
default_index_group = ['priority']
default_index_filter = []
- default_index_columns = ['activity','status','title']
+ 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
filter, columns, sort, group)
self.pagefoot()
- def showitem(self, message=None):
+ def shownode(self, message=None):
''' display an item
'''
cn = self.classname
keys = self.form.keys()
num_re = re.compile('^\d+$')
if keys:
- changed = []
- props = {}
try:
- keys = self.form.keys()
- for key in keys:
- if not cl.properties.has_key(key):
- continue
- proptype = cl.properties[key]
- if proptype.isStringType:
- value = str(self.form[key].value).strip()
- elif proptype.isDateType:
- value = date.Date(str(self.form[key].value))
- elif proptype.isIntervalType:
- value = date.Interval(str(self.form[key].value))
- elif proptype.isLinkType:
- value = str(self.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)
- elif proptype.isMultilinkType:
- value = self.form[key]
- if type(value) != type([]):
- value = [i.strip() for i in str(value.value).split(',')]
- else:
- value = [str(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 = self.db.classes[link].lookup(entry)
- except:
- raise ValueError, \
- 'property "%s": %s not a %s'%(key,
- entry, link)
- l.append(entry)
- l.sort()
- value = l
- # if changed, set it
- if value != cl.get(self.nodeid, key):
- changed.append(key)
- props[key] = value
+ props, changed = parsePropsFromForm(cl, self.form, self.nodeid)
cl.set(self.nodeid, **props)
-
- # if this item has messages, generate an edit message
- # TODO: don't send the edit message to the person who
- # performed the edit
- if (cl.getprops().has_key('messages') and
- cl.getprops()['messages'].isMultilinkType and
- cl.getprops()['messages'].classname == 'msg'):
- nid = self.nodeid
- m = []
- for name, prop in cl.getprops().items():
- value = cl.get(nid, name)
- if prop.isLinkType:
- link = self.db.classes[prop.classname]
- key = link.getkey()
- if value is not None and key:
- value = link.get(value, key)
- else:
- value = '-'
- elif prop.isMultilinkType:
- l = []
- link = self.db.classes[prop.classname]
- for entry in value:
- key = link.getkey()
- if key:
- l.append(link.get(entry, link.getkey()))
- else:
- l.append(entry)
- value = ', '.join(l)
- if name in changed:
- chg = '*'
- else:
- chg = ' '
- m.append('%s %s: %s'%(chg, name, value))
-
- # handle the note
- if self.form.has_key('__note'):
- note = self.form['__note'].value
- if '\n' in note:
- summary = re.split(r'\n\r?', note)[0]
- else:
- summary = note
- m.insert(0, '%s\n\n'%note)
- else:
- if len(changed) > 1:
- plural = 's were'
- else:
- plural = ' was'
- summary = 'This %s has been edited through the web '\
- 'and the %s value%s changed.'%(cn,
- ', '.join(changed), plural)
- m.insert(0, '%s\n\n'%summary)
-
- # now create the message
- content = '\n'.join(m)
- message_id = self.db.msg.create(author='1', recipients=[],
- date=date.Date('.'), summary=summary, content=content)
- messages = cl.get(nid, 'messages')
- messages.append(message_id)
- props = {'messages': messages}
- cl.set(nid, **props)
-
+ self._post_editnode(self.nodeid, changed)
# and some nice feedback for the user
message = '%s edited ok'%', '.join(changed)
except:
# use the template to display the item
htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
self.pagefoot()
- showissue = showitem
- showmsg = showitem
+ showissue = shownode
+ showmsg = shownode
+
+ def showuser(self, message=None):
+ ''' display an item
+ '''
+ if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
+ self.shownode(message)
+ else:
+ raise Unauthorised
+
+ 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 newissue(self, message=None):
- ''' add an issue
+ def _createnode(self):
+ ''' create a node based on the contents of the form
+ '''
+ cl = self.db.classes[self.classname]
+ props, dummy = parsePropsFromForm(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, link.getkey()))
+ 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()
- num_re = re.compile('^\d+$')
- if keys:
+ if [i for i in keys if i[0] != ':']:
props = {}
try:
- keys = self.form.keys()
- for key in keys:
- if not cl.properties.has_key(key):
- continue
- proptype = cl.properties[key]
- if proptype.isStringType:
- value = self.form[key].value.strip()
- elif proptype.isDateType:
- value = date.Date(self.form[key].value.strip())
- elif proptype.isIntervalType:
- value = date.Interval(self.form[key].value.strip())
- elif proptype.isLinkType:
- value = self.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)
- elif proptype.isMultilinkType:
- value = self.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 = self.db.classes[link].lookup(entry)
- except:
- raise ValueError, \
- 'property "%s": %s not a %s'%(key,
- entry, link)
- l.append(entry)
- l.sort()
- value = l
- props[key] = value
- nid = cl.create(**props)
-
- # if this item has messages,
- if (cl.getprops().has_key('messages') and
- cl.getprops()['messages'].isMultilinkType and
- cl.getprops()['messages'].classname == 'msg'):
- # generate an edit message - nosyreactor will send it
- m = []
- for name, prop in cl.getprops().items():
- value = cl.get(nid, name)
- if prop.isLinkType:
- link = self.db.classes[prop.classname]
- key = link.getkey()
- if value is not None and key:
- value = link.get(value, key)
- else:
- value = '-'
- elif prop.isMultilinkType:
- l = []
- link = self.db.classes[prop.classname]
- for entry in value:
- key = link.getkey()
- if key:
- l.append(link.get(entry, link.getkey()))
- else:
- l.append(entry)
- value = ', '.join(l)
- m.append('%s: %s'%(name, value))
-
- # handle the note
- note = self.form.get('__note', None)
- if note and note.value:
- note = note.value
- if '\n' in note:
- summary = re.split(r'\n\r?', note)[0]
- else:
- summary = note
- m.append('\n%s\n'%note)
- else:
- m.append('\nThis %s has been created through '
- 'the web.\n'%cn)
-
- # now create the message
- content = '\n'.join(m)
- message_id = self.db.msg.create(author='1', recipients=[],
- date=date.Date('.'), summary=summary, content=content)
- messages = cl.get(nid, 'messages')
- messages.append(message_id)
- props = {'messages': messages}
- cl.set(nid, **props)
-
+ nid = self._createnode()
+ self._post_editnode(nid)
# and some nice feedback for the user
message = '%s created ok'%cn
except:
htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
self.form)
self.pagefoot()
-
- def showuser(self, message=None):
- ''' display an item
+ 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.
'''
- if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
- self.showitem(message)
- else:
- raise Unauthorised
+ cn = self.classname
+ cl = self.db.classes[cn]
- 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'))
+ # 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)
+ htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
+ self.form)
+ self.pagefoot()
def classes(self, message=None):
''' display a list of all the classes in the database
def __del__(self):
self.db.close()
+def parsePropsFromForm(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.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)
+ 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 = self.db.classes[link].lookup(entry)
+ except:
+ raise ValueError, \
+ 'property "%s": %s not a %s'%(key,
+ entry, link)
+ 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.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 :)
#