diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py
index 06cd0d31e3b72ab28290f4c11538ba0f4581aabd..86177bb6b257e0978b35d5d4970cb982d5e404f7 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.76 2001-12-05 14:26:44 rochecompaan Exp $
+# $Id: cgi_client.py,v 1.90 2002-01-08 03:56:55 richard Exp $
__doc__ = """
WWW request handler (also used in the stand-alone server).
"""
import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
-import binascii, Cookie, time
+import binascii, Cookie, time, random
import roundupdb, htmltemplate, date, hyperdb, password
from roundup.i18n import _
ANONYMOUS_ACCESS = 'deny' # one of 'deny', 'allow'
ANONYMOUS_REGISTER = 'deny' # one of 'deny', 'allow'
- def __init__(self, instance, request, env):
+ def __init__(self, instance, request, env, form=None):
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)
+ if form is None:
+ self.form = cgi.FieldStorage(environ=env)
+ else:
+ self.form = form
self.headers_done = 0
try:
self.debug = int(env.get("ROUNDUP_DEBUG", 0))
user_name = self.user or ''
if self.user == 'admin':
admin_links = _(' | <a href="list_classes">Class List</a>' \
- ' | <a href="user">User List</a>')
+ ' | <a href="user">User List</a>' \
+ ' | <a href="newuser">Add User</a>')
else:
admin_links = ''
if self.user not in (None, 'anonymous'):
if self.user is not None:
add_links = _('''
| Add
-<a href="newissue">Issue</a>,
-<a href="newuser">User</a>
+<a href="newissue">Issue</a>
''')
else:
add_links = ''
try:
props, changed = parsePropsFromForm(self.db, cl, self.form,
self.nodeid)
-
- # set status to chatting if 'unread' or 'resolved'
- if 'status' not in changed.keys():
- 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')
- except KeyError:
- pass
- else:
- if (not props.has_key('status') or
- props['status'] == unread_id or
- props['status'] == resolved_id):
- props['status'] = chatting_id
- changed['status'] = chatting_id
-
- # get the change note
- change_note = cl.generateChangeNote(self.nodeid, changed)
-
- # make the changes
- cl.set(self.nodeid, **props)
-
- # handle linked nodes and change message generation
- self._post_editnode(self.nodeid, change_note)
-
+ # make changes to the node
+ self._changenode(props)
+ # handle linked nodes
+ self._post_editnode(self.nodeid)
# and some nice feedback for the user
if changed:
message = _('%(changes)s edited ok')%{'changes':
', '.join(changed.keys())}
+ elif self.form.has_key('__note') and self.form['__note'].value:
+ message = _('note added')
+ elif self.form.has_key('__file'):
+ message = _('file added')
else:
message = _('nothing changed')
except:
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.
+ def _add_assignedto_to_nosy(self, props):
+ ''' add the assignedto value from the props to the nosy list
'''
- 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)
- set_cookie = 0
- if self.nodeid == self.getuid() and 'password' in changed:
- password = self.form['password'].value.strip()
- if password:
- set_cookie = password
- else:
- del props['password']
- del changed[changed.index('password')]
- user.set(self.nodeid, **props)
- self._post_editnode(self.nodeid)
- # and some feedback for the user
- message = _('%(changes)s edited ok')%{'changes':
- ', '.join(changed.keys())}
- except:
- self.db.rollback()
- s = StringIO.StringIO()
- traceback.print_exc(None, s)
- message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
+ if not props.has_key('assignedto'):
+ return
+ assignedto_id = props['assignedto']
+ if props.has_key('nosy') and assignedto_id not in props['nosy']:
+ props['nosy'].append(assignedto_id)
else:
- set_cookie = 0
+ props['nosy'] = cl.get(self.nodeid, 'nosy')
+ props['nosy'].append(assignedto_id)
- # fix the cookie if the password has changed
- if set_cookie:
- self.set_cookie(self.user, set_cookie)
-
- #
- # now the display
- #
- self.pagehead(_('User: %(user)s')%{'user': node_user}, message)
+ 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
- # use the template to display the item
- item = htmltemplate.ItemTemplate(self, self.TEMPLATES, 'user')
- item.render(self.nodeid)
- self.pagefoot()
+ self._add_assignedto_to_nosy(props)
- def showfile(self):
- ''' display a file
- '''
- nodeid = self.nodeid
- cl = self.db.file
- mime_type = cl.get(nodeid, 'type')
- if mime_type == 'message/rfc822':
- mime_type = 'text/plain'
- self.header(headers={'Content-Type': mime_type})
- self.write(cl.get(nodeid, 'content'))
+ # create the message
+ message, files = self._handle_message()
+ if message:
+ props['messages'] = cl.get(self.nodeid, 'messages') + [message]
+ if files:
+ props['files'] = cl.get(self.nodeid, 'files') + files
+ # make the changes
+ cl.set(self.nodeid, **props)
def _createnode(self):
''' create a node based on the contents of the form
pass
else:
props['status'] = unread_id
+
+ self._add_assignedto_to_nosy(props)
+
+ # check for messages and files
+ message, files = self._handle_message()
+ if message:
+ props['messages'] = [message]
+ if files:
+ props['files'] = files
+ # create the node and return it's id
return cl.create(**props)
- def _post_editnode(self, nid, change_note=None):
- ''' do the linking and message sending part of the node creation
+ def _handle_message(self):
+ ''' generate and edit message
'''
- 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})
-
- # handle file attachments
+ # handle file attachments
files = []
if self.form.has_key('__file'):
file = self.form['__file']
if file.filename:
- mime_type = mimetypes.guess_type(file.filename)[0]
+ filename = file.filename.split('\\')[-1]
+ mime_type = mimetypes.guess_type(filename)[0]
if not mime_type:
mime_type = "application/octet-stream"
# create the new file entry
files.append(self.db.file.create(type=mime_type,
- name=file.filename, content=file.file.read()))
- # and save the reference
- cl.set(nid, files=files)
-
- #
- # generate an edit message
- #
+ name=filename, content=file.file.read()))
# we don't want to do a message if none of the following is true...
+ cn = self.classname
+ cl = self.db.classes[self.classname]
props = cl.getprops()
note = None
+ # 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']
- note = note.value
+ note = self.form['__note'].value
if not props.has_key('messages'):
- return
+ return None, files
if not isinstance(props['messages'], hyperdb.Multilink):
- return
+ return None, files
if not props['messages'].classname == 'msg':
- return
- if not (len(cl.get(nid, 'nosy', [])) or note):
- return
+ return None, files
+ if not (self.form.has_key('nosy') or note):
+ return None, files
# handle the note
if note:
else:
summary = note
m = ['%s\n'%note]
- else:
- summary = _('This %(classname)s has been edited through'
- ' the web.\n')%{'classname': cn}
- m = [summary]
+ elif not files:
+ # don't generate a useless message
+ return None, files
- # append the change note
- m.append(change_note)
+ # handle the messageid
+ # TODO: handle inreplyto
+ messageid = "%s.%s.%s-%s"%(time.time(), random.random(),
+ self.classname, self.MAIL_DOMAIN)
- # now create the message
+ # now create the message, attaching the files
content = '\n'.join(m)
message_id = self.db.msg.create(author=self.getuid(),
recipients=[], date=date.Date('.'), summary=summary,
- content=content, files=files)
+ content=content, files=files, messageid=messageid)
# update the messages property
- messages = cl.get(nid, 'messages')
- messages.append(message_id)
- cl.set(nid, messages=messages, files=files)
+ return message_id, files
+
+ def _post_editnode(self, nid):
+ '''Do the linking part of the node creation.
+
+ If a form element has :link or :multilink appended to it, its
+ value specifies a node designator and the property on that node
+ to add _this_ node to as a link or multilink.
+
+ This is typically used on, eg. the file upload page to indicated
+ which issue to link the file to.
+
+ TODO: I suspect that this and newfile will go away now that
+ there's the ability to upload a file using the issue __file form
+ element!
+ '''
+ 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})
def newnode(self, message=None):
''' Add a new node to the database.
props = {}
try:
nid = self._createnode()
- # handle linked nodes and change message generation
+ # handle linked nodes
self._post_editnode(nid)
# and some nice feedback for the user
message = _('%(classname)s created ok')%{'classname': cn}
+
+ # render the newly created issue
+ self.db.commit()
+ self.nodeid = nid
+ self.pagehead('%s: %s'%(self.classname.capitalize(), nid),
+ message)
+ item = htmltemplate.ItemTemplate(self, self.TEMPLATES,
+ self.classname)
+ item.render(nid)
+ self.pagefoot()
+ return
except:
self.db.rollback()
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.classname.capitalize()}, message)
# call the template
newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
self.pagefoot()
newissue = newnode
- newuser = newnode
+
+ def newuser(self, message=None):
+ ''' Add a new user to the database.
+
+ Don't do any of the message or file handling, just create the node.
+ '''
+ 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:
+ props, dummy = parsePropsFromForm(self.db, cl, self.form)
+ nid = cl.create(**props)
+ # handle linked nodes
+ self._post_editnode(nid)
+ # and some nice feedback for the user
+ message = _('%(classname)s created ok')%{'classname': cn}
+ except:
+ self.db.rollback()
+ 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)
+
+ # call the template
+ newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
+ self.classname)
+ newitem.render(self.form)
+
+ self.pagefoot()
def newfile(self, message=None):
''' Add a new file to the database.
# and some nice feedback for the user
message = _('%(classname)s created ok')%{'classname': cn}
except:
+ self.db.rollback()
s = StringIO.StringIO()
traceback.print_exc(None, s)
message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
newitem.render(self.form)
self.pagefoot()
+ 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)
+ set_cookie = 0
+ if self.nodeid == self.getuid() and changed.has_key('password'):
+ password = self.form['password'].value.strip()
+ if password:
+ set_cookie = password
+ else:
+ # no password was supplied - don't change it
+ del props['password']
+ del changed['password']
+ user.set(self.nodeid, **props)
+ # and some feedback for the user
+ message = _('%(changes)s edited ok')%{'changes':
+ ', '.join(changed.keys())}
+ except:
+ self.db.rollback()
+ 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: %(user)s')%{'user': 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
+ mime_type = cl.get(nodeid, 'type')
+ if mime_type == 'message/rfc822':
+ mime_type = 'text/plain'
+ self.header(headers={'Content-Type': mime_type})
+ self.write(cl.get(nodeid, 'content'))
+
def classes(self, message=None):
''' display a list of all the classes in the database
'''
user_name = self.user or ''
if self.user == 'admin':
admin_links = _(' | <a href="list_classes">Class List</a>' \
- ' | <a href="user">User List</a>')
+ ' | <a href="user">User List</a>' \
+ ' | <a href="newuser">Add User</a>')
else:
admin_links = ''
if self.user not in (None, 'anonymous'):
| Add
<a href="newissue">Issue</a>,
<a href="newsupport">Support</a>,
-<a href="newuser">User</a>
''')
else:
add_links = ''
#
# $Log: not supported by cvs2svn $
+# Revision 1.89 2002/01/07 20:24:45 richard
+# *mutter* stupid cutnpaste
+#
+# Revision 1.88 2002/01/02 02:31:38 richard
+# Sorry for the huge checkin message - I was only intending to implement #496356
+# but I found a number of places where things had been broken by transactions:
+# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
+# for _all_ roundup-generated smtp messages to be sent to.
+# . the transaction cache had broken the roundupdb.Class set() reactors
+# . newly-created author users in the mailgw weren't being committed to the db
+#
+# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
+# on when I found that stuff :):
+# . #496356 ] Use threading in messages
+# . detectors were being registered multiple times
+# . added tests for mailgw
+# . much better attaching of erroneous messages in the mail gateway
+#
+# Revision 1.87 2001/12/23 23:18:49 richard
+# We already had an admin-specific section of the web heading, no need to add
+# another one :)
+#
+# Revision 1.86 2001/12/20 15:43:01 rochecompaan
+# Features added:
+# . Multilink properties are now displayed as comma separated values in
+# a textbox
+# . The add user link is now only visible to the admin user
+# . Modified the mail gateway to reject submissions from unknown
+# addresses if ANONYMOUS_ACCESS is denied
+#
+# Revision 1.85 2001/12/20 06:13:24 rochecompaan
+# Bugs fixed:
+# . Exception handling in hyperdb for strings-that-look-like numbers got
+# lost somewhere
+# . Internet Explorer submits full path for filename - we now strip away
+# the path
+# Features added:
+# . Link and multilink properties are now displayed sorted in the cgi
+# interface
+#
+# Revision 1.84 2001/12/18 15:30:30 rochecompaan
+# Fixed bugs:
+# . Fixed file creation and retrieval in same transaction in anydbm
+# backend
+# . Cgi interface now renders new issue after issue creation
+# . Could not set issue status to resolved through cgi interface
+# . Mail gateway was changing status back to 'chatting' if status was
+# omitted as an argument
+#
+# Revision 1.83 2001/12/15 23:51:01 richard
+# Tested the changes and fixed a few problems:
+# . files are now attached to the issue as well as the message
+# . newuser is a real method now since we don't want to do the message/file
+# stuff for it
+# . added some documentation
+# The really big changes in the diff are a result of me moving some code
+# around to keep like methods together a bit better.
+#
+# Revision 1.82 2001/12/15 19:24:39 rochecompaan
+# . Modified cgi interface to change properties only once all changes are
+# collected, files created and messages generated.
+# . Moved generation of change note to nosyreactors.
+# . We now check for changes to "assignedto" to ensure it's added to the
+# nosy list.
+#
+# Revision 1.81 2001/12/12 23:55:00 richard
+# Fixed some problems with user editing
+#
+# Revision 1.80 2001/12/12 23:27:14 richard
+# Added a Zope frontend for roundup.
+#
+# Revision 1.79 2001/12/10 22:20:01 richard
+# Enabled transaction support in the bsddb backend. It uses the anydbm code
+# where possible, only replacing methods where the db is opened (it uses the
+# btree opener specifically.)
+# Also cleaned up some change note generation.
+# Made the backends package work with pydoc too.
+#
+# Revision 1.78 2001/12/07 05:59:27 rochecompaan
+# Fixed small bug that prevented adding issues through the web.
+#
+# Revision 1.77 2001/12/06 22:48:29 richard
+# files multilink was being nuked in post_edit_node
+#
+# Revision 1.76 2001/12/05 14:26:44 rochecompaan
+# Removed generation of change note from "sendmessage" in roundupdb.py.
+# The change note is now generated when the message is created.
+#
# Revision 1.75 2001/12/04 01:25:08 richard
# Added some rollbacks where we were catching exceptions that would otherwise
# have stopped committing.