Code

Oops, missed this before the beta:
[roundup.git] / roundup / cgi_client.py
index 06cd0d31e3b72ab28290f4c11538ba0f4581aabd..86177bb6b257e0978b35d5d4970cb982d5e404f7 100644 (file)
 # 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 = _(' | <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'):
@@ -119,8 +123,7 @@ class Client:
         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 = ''
@@ -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 = '<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
@@ -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 = '<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,
@@ -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 = '<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.
@@ -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 = '<pre>%s</pre>'%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 = '<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
         '''
@@ -967,7 +1064,8 @@ class ExtendedClient(Client):
         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'):
@@ -984,7 +1082,6 @@ class ExtendedClient(Client):
 | Add
 <a href="newissue">Issue</a>,
 <a href="newsupport">Support</a>,
-<a href="newuser">User</a>
 ''')
         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.