X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fcgi_client.py;h=86177bb6b257e0978b35d5d4970cb982d5e404f7;hb=1b8dd00d357bfc90e4f1d57ac8c3dd36aa9a0ccd;hp=06cd0d31e3b72ab28290f4c11538ba0f4581aabd;hpb=6017df9b80fa58974ffc5f2abf67316cea57be77;p=roundup.git diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py index 06cd0d3..86177bb 100644 --- a/roundup/cgi_client.py +++ b/roundup/cgi_client.py @@ -15,14 +15,14 @@ # 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 _ @@ -60,14 +60,17 @@ class Client: 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)) @@ -105,7 +108,8 @@ class Client: user_name = self.user or '' if self.user == 'admin': admin_links = _(' | Class List' \ - ' | User List') + ' | User List' \ + ' | Add User') else: admin_links = '' if self.user not in (None, 'anonymous'): @@ -119,8 +123,7 @@ class Client: if self.user is not None: add_links = _(''' | Add -Issue, -User +Issue ''') else: add_links = '' @@ -311,36 +314,18 @@ class Client: 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: @@ -365,75 +350,52 @@ class Client: 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 = '
%s'%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 @@ -450,67 +412,51 @@ class Client: 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: @@ -519,24 +465,61 @@ class Client: 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. @@ -569,17 +552,28 @@ class Client: 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 = '
%s'%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, @@ -588,7 +582,39 @@ class Client: 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 = '
%s'%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. @@ -615,6 +641,7 @@ class Client: # 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 = '
%s'%cgi.escape(s.getvalue()) @@ -626,6 +653,76 @@ class Client: 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 = '
%s'%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 ''' @@ -967,7 +1064,8 @@ class ExtendedClient(Client): user_name = self.user or '' if self.user == 'admin': admin_links = _(' | Class List' \ - ' | User List') + ' | User List' \ + ' | Add User') else: admin_links = '' if self.user not in (None, 'anonymous'): @@ -984,7 +1082,6 @@ class ExtendedClient(Client): | Add Issue, Support, -User ''') else: add_links = '' @@ -1084,6 +1181,94 @@ def parsePropsFromForm(db, cl, form, nodeid=0): # # $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.