Code

Added a Zope frontend for roundup.
[roundup.git] / roundup / cgi_client.py
index 66c923a21586cd0b0df3c06df33ae7241437808a..59b587b33aff973c225554a5cf5761a9796485bf 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: cgi_client.py,v 1.72 2001-11-30 20:47:58 rochecompaan Exp $
+# $Id: cgi_client.py,v 1.80 2001-12-12 23:27:14 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
@@ -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))
@@ -313,7 +316,7 @@ class Client:
                     self.nodeid)
 
                 # set status to chatting if 'unread' or 'resolved'
-                if 'status' not in changed:
+                if not changed.has_key('status'):
                     try:
                         # determine the id of 'unread','resolved' and 'chatting'
                         unread_id = self.db.status.lookup('unread')
@@ -326,22 +329,27 @@ class Client:
                                 props['status'] == unread_id or
                                 props['status'] == resolved_id):
                             props['status'] = chatting_id
-                            changed.append('status')
-                note = None
-                if self.form.has_key('__note'):
-                    note = self.form['__note']
-                    note = note.value
-                if changed or note:
-                    p = self._post_editnode(self.nodeid, changed)
-                    props['messages'] = p['messages']
-                    props['files'] = p['files']
-                    cl.set(self.nodeid, **props)
-                    # and some nice feedback for the user
+                            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)
+
+                # and some nice feedback for the user
+                if changed:
                     message = _('%(changes)s edited ok')%{'changes':
-                        ', '.join(changed)}
+                        ', '.join(changed.keys())}
+                elif self.form.has_key('__note') and self.form['__note'].value:
+                    message = _('note added')
                 else:
                     message = _('nothing changed')
             except:
+                self.db.rollback()
                 s = StringIO.StringIO()
                 traceback.print_exc(None, s)
                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
@@ -395,11 +403,12 @@ class Client:
                         del props['password']
                         del changed[changed.index('password')]
                 user.set(self.nodeid, **props)
-                self._post_editnode(self.nodeid, changed)
+                self._post_editnode(self.nodeid)
                 # and some feedback for the user
                 message = _('%(changes)s edited ok')%{'changes':
-                    ', '.join(changed)}
+                    ', '.join(changed.keys())}
             except:
+                self.db.rollback()
                 s = StringIO.StringIO()
                 traceback.print_exc(None, s)
                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
@@ -436,9 +445,19 @@ class Client:
         '''
         cl = self.db.classes[self.classname]
         props, dummy = 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'):
+            try:
+                unread_id = self.db.status.lookup('unread')
+            except KeyError:
+                pass
+            else:
+                props['status'] = unread_id
         return cl.create(**props)
 
-    def _post_editnode(self, nid, changes=None):
+    def _post_editnode(self, nid, change_note=''):
         ''' do the linking and message sending part of the node creation
         '''
         cn = self.classname
@@ -466,7 +485,7 @@ class Client:
                     link.set(nodeid, **{property: nid})
 
         # handle file attachments
-        files = []
+        files = cl.get(nid, 'files')
         if self.form.has_key('__file'):
             file = self.form['__file']
             if file.filename:
@@ -476,40 +495,53 @@ class Client:
                 # 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
-        # don't bother if there's no messages or nosy list 
+        #
+
+        # we don't want to do a message if none of the following is true...
         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]
+            note = self.form['__note'].value
+        if not props.has_key('messages'):
+            return
+        if not isinstance(props['messages'], hyperdb.Multilink):
+            return
+        if not props['messages'].classname == 'msg':
+            return
+        if not (len(cl.get(nid, 'nosy', [])) or note):
+            return
+
+        # handle the note
+        if note:
+            if '\n' in note:
+                summary = re.split(r'\n\r?', note)[0]
             else:
-                summary = _('This %(classname)s has been edited through'
-                    ' the web.\n')%{'classname': cn}
-                m = [summary]
-
-            # 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, files=files)
-            messages = cl.get(nid, 'messages')
-            messages.append(message_id)
-            props = {'messages': messages, 'files': files}
-            return props
+                summary = note
+            m = ['%s\n'%note]
+        else:
+            summary = _('This %(classname)s has been edited through'
+                ' the web.\n')%{'classname': cn}
+            m = [summary]
+
+        # append the change note
+        if change_note:
+            m.append(change_note)
+
+        # 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, files=files)
+
+        # update the messages property
+        messages = cl.get(nid, 'messages')
+        messages.append(message_id)
+        cl.set(nid, messages=messages, files=files)
 
     def newnode(self, message=None):
         ''' Add a new node to the database.
@@ -542,11 +574,12 @@ class Client:
             props = {}
             try:
                 nid = self._createnode()
-                props = self._post_editnode(nid)
-                cl.set(nid, **props)
+                # handle linked nodes and change message generation
+                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())
@@ -579,11 +612,15 @@ class Client:
                 mime_type = mimetypes.guess_type(file.filename)[0]
                 if not mime_type:
                     mime_type = "application/octet-stream"
-                self._post_editnode(cl.create(content=file.file.read(),
-                    type=mime_type, name=file.filename))
+                # save the file
+                nid = cl.create(content=file.file.read(), type=mime_type,
+                    name=file.filename)
+                # 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())
@@ -617,6 +654,8 @@ class Client:
             raise Unauthorised
 
     def login(self, message=None, newuser_form=None, action='index'):
+        '''Display a login page.
+        '''
         self.pagehead(_('Login to roundup'), message)
         self.write(_('''
 <table>
@@ -634,9 +673,10 @@ class Client:
         if self.user is None and self.ANONYMOUS_REGISTER == 'deny':
             self.write('</table>')
             self.pagefoot()
-            return 1
+            return
         values = {'realname': '', 'organisation': '', 'address': '',
-            'phone': '', 'username': '', 'password': '', 'confirm': ''}
+            'phone': '', 'username': '', 'password': '', 'confirm': '',
+            'action': action}
         if newuser_form is not None:
             for key in newuser_form.keys():
                 values[key] = newuser_form[key].value
@@ -645,6 +685,7 @@ class Client:
 <tr><td colspan=2 class="strong-header">New User Registration</td></tr>
 <tr><td colspan=2><em>marked items</em> are optional...</td></tr>
 <form action="newuser_action" method=POST>
+<input type="hidden" name="__destination_url" value="%(action)s">
 <tr><td align=right><em>Name: </em></td>
     <td><input name="realname" value="%(realname)s"></td></tr>
 <tr><td align=right><em>Organisation: </em></td>
@@ -667,8 +708,14 @@ class Client:
         self.pagefoot()
 
     def login_action(self, message=None):
+        '''Attempt to log a user in and set the cookie
+
+        returns 0 if a page is generated as a result of this call, and
+        1 if not (ie. the login is successful
+        '''
         if not self.form.has_key('__login_name'):
-            return self.login(message=_('Username required'))
+            self.login(message=_('Username required'))
+            return 0
         self.user = self.form['__login_name'].value
         if self.form.has_key('__login_password'):
             password = self.form['__login_password'].value
@@ -681,18 +728,43 @@ class Client:
             name = self.user
             self.make_user_anonymous()
             action = self.form['__destination_url'].value
-            return self.login(message=_('No such user "%(name)s"')%locals(),
-                              action=action)
+            self.login(message=_('No such user "%(name)s"')%locals(),
+                action=action)
+            return 0
 
         # and that the password is correct
         pw = self.db.user.get(uid, 'password')
-        if password != self.db.user.get(uid, 'password'):
+        if password != pw:
             self.make_user_anonymous()
             action = self.form['__destination_url'].value
-            return self.login(message=_('Incorrect password'), action=action)
+            self.login(message=_('Incorrect password'), action=action)
+            return 0
 
         self.set_cookie(self.user, password)
-        return None     # make it explicit
+        return 1
+
+    def newuser_action(self, message=None):
+        '''Attempt to create a new user based on the contents of the form
+        and then set the cookie.
+
+        return 1 on successful login
+        '''
+        # re-open the database as "admin"
+        self.db = self.instance.open('admin')
+
+        # TODO: pre-check the required fields and username key property
+        cl = self.db.user
+        try:
+            props, dummy = parsePropsFromForm(self.db, cl, self.form)
+            uid = cl.create(**props)
+        except ValueError, message:
+            action = self.form['__destination_url'].value
+            self.login(message, action=action)
+            return 0
+        self.user = cl.get(uid, 'username')
+        password = cl.get(uid, 'password')
+        self.set_cookie(self.user, self.form['password'].value)
+        return 1
 
     def set_cookie(self, user, password):
         # construct the cookie
@@ -723,29 +795,12 @@ class Client:
         self.header({'Set-Cookie':
             'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now,
             path)})
-        return self.login()
+        self.login()
 
-    def newuser_action(self, message=None):
-        ''' create a new user based on the contents of the form and then
-        set the cookie
-        '''
-        # re-open the database as "admin"
-        self.db.close()
-        self.db = self.instance.open('admin')
-
-        # TODO: pre-check the required fields and username key property
-        cl = self.db.user
-        try:
-            props, dummy = parsePropsFromForm(self.db, cl, self.form)
-            uid = cl.create(**props)
-        except ValueError, message:
-            return self.login(message, newuser_form=self.form)
-        self.user = cl.get(uid, 'username')
-        password = cl.get(uid, 'password')
-        self.set_cookie(self.user, self.form['password'].value)
-        return None    # make the None explicit
 
     def main(self):
+        '''Wrap the database accesses so we can close the database cleanly
+        '''
         # determine the uid to use
         self.db = self.instance.open('admin')
         cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
@@ -775,7 +830,6 @@ class Client:
             self.make_user_anonymous()
         else:
             self.user = user
-        self.db.close()
 
         # re-open the database for real, using the user
         self.db = self.instance.open(self.user)
@@ -796,15 +850,15 @@ class Client:
 
         # everyone is allowed to try to log in
         if action == 'login_action':
-            # do the login
-            ret = self.login_action()
-            if ret is not None:
-                return ret
+            # try to login
+            if not self.login_action():
+                return
             # figure the resulting page
             action = self.form['__destination_url'].value
             if not action:
                 action = 'index'
-            return self.do_action(action)
+            self.do_action(action)
+            return
 
         # allow anonymous people to register
         if action == 'newuser_action':
@@ -812,40 +866,49 @@ class Client:
             # register, then spit up the login form
             if self.ANONYMOUS_REGISTER == 'deny' and self.user is None:
                 if action == 'login':
-                    return self.login()         # go to the index after login
+                    self.login()         # go to the index after login
                 else:
-                    return self.login(action=action)
-            # add the user
-            ret = self.newuser_action()
-            if ret is not None:
-                return ret
+                    self.login(action=action)
+                return
+            # try to add the user
+            if not self.newuser_action():
+                return
             # figure the resulting page
             action = self.form['__destination_url'].value
             if not action:
                 action = 'index'
-            return self.do_action(action)
 
         # no login or registration, make sure totally anonymous access is OK
-        if self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
+        elif self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
             if action == 'login':
-                return self.login()             # go to the index after login
+                self.login()             # go to the index after login
             else:
-                return self.login(action=action)
+                self.login(action=action)
+            return
 
         # just a regular action
-        return self.do_action(action)
+        self.do_action(action)
+
+        # commit all changes to the database
+        self.db.commit()
 
     def do_action(self, action, dre=re.compile(r'([^\d]+)(\d+)'),
             nre=re.compile(r'new(\w+)')):
+        '''Figure the user's action and do it.
+        '''
         # here be the "normal" functionality
         if action == 'index':
-            return self.index()
+            self.index()
+            return
         if action == 'list_classes':
-            return self.classes()
+            self.classes()
+            return
         if action == 'login':
-            return self.login()
+            self.login()
+            return
         if action == 'logout':
-            return self.logout()
+            self.logout()
+            return
         m = dre.match(action)
         if m:
             self.classname = m.group(1)
@@ -862,7 +925,8 @@ class Client:
                 func = getattr(self, 'show%s'%self.classname)
             except AttributeError:
                 raise NotFound
-            return func()
+            func()
+            return
         m = nre.match(action)
         if m:
             self.classname = m.group(1)
@@ -870,16 +934,14 @@ class Client:
                 func = getattr(self, 'new%s'%self.classname)
             except AttributeError:
                 raise NotFound
-            return func()
+            func()
+            return
         self.classname = action
         try:
             self.db.getclass(self.classname)
         except KeyError:
             raise NotFound
-        return self.list()
-
-    def __del__(self):
-        self.db.close()
+        self.list()
 
 
 class ExtendedClient(Client): 
@@ -958,7 +1020,7 @@ def parsePropsFromForm(db, cl, form, nodeid=0):
     '''Pull properties for the given class out of the form.
     '''
     props = {}
-    changed = []
+    changed = {}
     keys = form.keys()
     num_re = re.compile('^\d+$')
     for key in keys:
@@ -998,6 +1060,7 @@ def parsePropsFromForm(db, cl, form, nodeid=0):
             link = cl.properties[key].classname
             l = []
             for entry in map(str, value):
+                if entry == '': continue
                 if not num_re.match(entry):
                     try:
                         entry = db.classes[link].lookup(entry)
@@ -1021,12 +1084,64 @@ def parsePropsFromForm(db, cl, form, nodeid=0):
 
         # if changed, set it
         if nodeid and value != existing:
-            changed.append(key)
+            changed[key] = value
             props[key] = value
     return props, changed
 
 #
 # $Log: not supported by cvs2svn $
+# 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.
+#
+# Revision 1.74  2001/12/02 05:06:16  richard
+# . We now use weakrefs in the Classes to keep the database reference, so
+#   the close() method on the database is no longer needed.
+#   I bumped the minimum python requirement up to 2.1 accordingly.
+# . #487480 ] roundup-server
+# . #487476 ] INSTALL.txt
+#
+# I also cleaned up the change message / post-edit stuff in the cgi client.
+# There's now a clearly marked "TODO: append the change note" where I believe
+# the change note should be added there. The "changes" list will obviously
+# have to be modified to be a dict of the changes, or somesuch.
+#
+# More testing needed.
+#
+# Revision 1.73  2001/12/01 07:17:50  richard
+# . We now have basic transaction support! Information is only written to
+#   the database when the commit() method is called. Only the anydbm
+#   backend is modified in this way - neither of the bsddb backends have been.
+#   The mail, admin and cgi interfaces all use commit (except the admin tool
+#   doesn't have a commit command, so interactive users can't commit...)
+# . Fixed login/registration forwarding the user to the right page (or not,
+#   on a failure)
+#
+# Revision 1.72  2001/11/30 20:47:58  rochecompaan
+# Links in page header are now consistent with default sort order.
+#
+# Fixed bugs:
+#     - When login failed the list of issues were still rendered.
+#     - User was redirected to index page and not to his destination url
+#       if his first login attempt failed.
+#
 # Revision 1.71  2001/11/30 20:28:10  rochecompaan
 # Property changes are now completely traceable, whether changes are
 # made through the web or by email