Code

minor pre-release / test fixes
[roundup.git] / roundup / cgi / actions.py
index 196ad5d1478ed44255eabc458cdbaa064582419d..33e7281399c16268aa20d34f75ae2ac60994828f 100755 (executable)
@@ -3,7 +3,7 @@ import re, cgi, StringIO, urllib, Cookie, time, random
 from roundup import hyperdb, token, date, password, rcsv
 from roundup.i18n import _
 from roundup.cgi import templating
-from roundup.cgi.exceptions import Redirect, Unauthorised
+from roundup.cgi.exceptions import Redirect, Unauthorised, SeriousError
 from roundup.mailgw import uidFromAddress
 
 __all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
@@ -14,7 +14,7 @@ __all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
 # used by a couple of routines
 chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
 
-class Action:    
+class Action:
     def __init__(self, client):
         self.client = client
         self.form = client.form
@@ -25,7 +25,7 @@ class Action:
         self.userid = client.userid
         self.base = client.base
         self.user = client.user
-        
+
     def execute(self):
         """Execute the action specified by this object."""
         self.permission()
@@ -40,20 +40,20 @@ class Action:
         a simple permission, check whether the user has that permission.
         Subclasses must also define the name attribute if they define
         permissionType.
-        
+
         Despite having this permission, users may still be unauthorised to
-        perform parts of actions. It is up to the subclasses to detect this.        
+        perform parts of actions. It is up to the subclasses to detect this.
         """
         if (self.permissionType and
-            not self.hasPermission(self.permissionType)):
-
-            raise Unauthorised, _('You do not have permission to %s the %s class.' %
-                                  (self.name, self.classname))
+                not self.hasPermission(self.permissionType)):
+            info = {'action': self.name, 'classname': self.classname}
+            raise Unauthorised, _('You do not have permission to '
+                '%(action)s the %(classname)s class.')%info
 
     def hasPermission(self, permission):
         """Check whether the user has 'permission' on the current class."""
         return self.db.security.hasPermission(permission, self.client.userid,
-                                              self.client.classname)
+            self.client.classname)
 
 class ShowAction(Action):
     def handle(self, typere=re.compile('[@:]type'),
@@ -66,7 +66,15 @@ class ShowAction(Action):
             elif numre.match(key):
                 n = self.form[key].value.strip()
         if not t:
-            raise ValueError, 'Invalid %s number'%t
+            raise ValueError, 'No type specified'
+        if not n:
+            raise SeriousError, _('No ID entered')
+        try:
+            int(n)
+        except ValueError:
+            d = {'input': n, 'classname': t}
+            raise SeriousError, _(
+                '"%(input)s" is not an ID (%(classname)s ID required)')%d
         url = '%s%s%s'%(self.db.config.TRACKER_WEB, t, n)
         raise Redirect, url
 
@@ -75,7 +83,7 @@ class RetireAction(Action):
     permissionType = 'Edit'
 
     def handle(self):
-        """Retire the context item."""        
+        """Retire the context item."""
         # if we want to view the index template now, then unset the nodeid
         # context info (a special-case for retire actions on the index page)
         nodeid = self.nodeid
@@ -98,7 +106,7 @@ class RetireAction(Action):
 class SearchAction(Action):
     name = 'search'
     permissionType = 'View'
-    
+
     def handle(self, wcre=re.compile(r'[\s,]+')):
         """Mangle some of the form variables.
 
@@ -113,7 +121,7 @@ class SearchAction(Action):
 
         """
         self.fakeFilterVars()
-        queryname = self.getQueryName()        
+        queryname = self.getQueryName()
 
         # handle saving the query params
         if queryname:
@@ -135,8 +143,9 @@ class SearchAction(Action):
 
             # and add it to the user's query multilink
             queries = self.db.user.get(self.userid, 'queries')
-            queries.append(qid)
-            self.db.user.set(self.userid, queries=queries)
+            if qid not in queries:
+                queries.append(qid)
+                self.db.user.set(self.userid, queries=queries)
 
             # commit the query change to the database
             self.db.commit()
@@ -165,7 +174,7 @@ class SearchAction(Action):
                         # replace the single value with the split list
                         for v in l:
                             self.form.value.append(cgi.MiniFieldStorage(key, v))
-        
+
             self.form.value.append(cgi.MiniFieldStorage('@filter', key))
 
     FV_QUERYNAME = re.compile(r'[@:]queryname')
@@ -178,7 +187,7 @@ class SearchAction(Action):
 class EditCSVAction(Action):
     name = 'edit'
     permissionType = 'Edit'
-    
+
     def handle(self):
         """Performs an edit of all of a class' items in one go.
 
@@ -270,13 +279,13 @@ class EditCSVAction(Action):
         self.db.commit()
 
         self.client.ok_message.append(_('Items edited OK'))
-    
+
 class _EditAction(Action):
     def isEditingSelf(self):
         """Check whether a user is editing his/her own details."""
         return (self.nodeid == self.userid
                 and self.db.user.get(self.nodeid, 'username') != 'anonymous')
-    
+
     def editItemPermission(self, props):
         """Determine whether the user has permission to edit this item.
 
@@ -444,12 +453,12 @@ class EditItemAction(_EditAction):
 
     def handleCollision(self):
         self.client.template = 'collision'
-    
+
     def handle(self):
         """Perform an edit of an item in the database.
 
         See parsePropsFromForm and _editnodes for special variables.
-        
+
         """
         if self.detectCollision(self.lastUserActivity(), self.lastNodeActivity()):
             self.handleCollision()
@@ -480,7 +489,7 @@ class EditItemAction(_EditAction):
             req = templating.HTMLRequest(self)
             url += '&' + req.indexargs_href('', {})[1:]
         raise Redirect, url
-    
+
 class NewItemAction(_EditAction):
     def handle(self):
         ''' Add a new item to the database.
@@ -490,7 +499,7 @@ class NewItemAction(_EditAction):
         '''
         # parse the props from the form
         try:
-            props, links = self.client.parsePropsFromForm(create=True)
+            props, links = self.client.parsePropsFromForm(create=1)
         except (ValueError, KeyError), message:
             self.client.error_message.append(_('Error: ') + str(message))
             return
@@ -512,19 +521,20 @@ class NewItemAction(_EditAction):
         raise Redirect, '%s%s%s?@ok_message=%s&@template=%s'%(self.base,
             self.classname, self.nodeid, urllib.quote(messages),
             urllib.quote(self.template))
-        
+
 class PassResetAction(Action):
     def handle(self):
         """Handle password reset requests.
-    
+
         Presence of either "name" or "address" generates email. Presence of
         "otk" performs the reset.
-    
+
         """
         if self.form.has_key('otk'):
             # pull the rego information out of the otk database
             otk = self.form['otk'].value
-            uid = self.db.otks.get(otk, 'uid')
+            otks = self.db.getOTKManager()
+            uid = otks.get(otk, 'uid')
             if uid is None:
                 self.client.error_message.append("""Invalid One Time Key!
 (a Mozilla bug may cause this message to show up erroneously,
@@ -540,12 +550,12 @@ class PassResetAction(Action):
             newpw = password.generatePassword()
 
             cl = self.db.user
-# XXX we need to make the "default" page be able to display errors!
+            # XXX we need to make the "default" page be able to display errors!
             try:
                 # set the password
                 cl.set(uid, password=password.Password(newpw))
                 # clear the props from the otk database
-                self.db.otks.destroy(otk)
+                otks.destroy(otk)
                 self.db.commit()
             except (ValueError, KeyError), message:
                 self.client.error_message.append(str(message))
@@ -566,8 +576,8 @@ Your password is now: %(password)s
             if not self.client.standard_message([address], subject, body):
                 return
 
-            self.client.ok_message.append('Password reset and email sent to %s' %
-                                          address)
+            self.client.ok_message.append(
+                    'Password reset and email sent to %s'%address)
             return
 
         # no OTK, so now figure the user
@@ -593,7 +603,10 @@ Your password is now: %(password)s
 
         # generate the one-time-key and store the props for later
         otk = ''.join([random.choice(chars) for x in range(32)])
-        self.db.otks.set(otk, uid=uid, __time=time.time())
+        while otks.exists(otk):
+            otk = ''.join([random.choice(chars) for x in range(32)])
+        otks.set(otk, uid=uid)
+        self.db.commit()
 
         # send the email
         tracker_name = self.db.config.TRACKER_NAME
@@ -619,19 +632,17 @@ class ConfRegoAction(Action):
             # pull the rego information out of the otk database
             self.userid = self.db.confirm_registration(self.form['otk'].value)
         except (ValueError, KeyError), message:
-            # XXX: we need to make the "default" page be able to display errors!
             self.client.error_message.append(str(message))
             return
-        
+
         # log the new user in
         self.client.user = self.db.user.get(self.userid, 'username')
         # re-open the database for real, using the user
         self.client.opendb(self.client.user)
-        self.db = client.db
 
         # if we have a session, update it
         if hasattr(self, 'session'):
-            self.db.sessions.set(self.session, user=self.user,
+            self.client.db.sessions.set(self.session, user=self.user,
                 last_use=time.time())
         else:
             # new session cookie
@@ -639,37 +650,44 @@ class ConfRegoAction(Action):
 
         # nice message
         message = _('You are now registered, welcome!')
+        url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
+            urllib.quote(message))
 
-        # redirect to the user's page
-        raise Redirect, '%suser%s?@ok_message=%s'%(self.base,
-                                                   self.userid, urllib.quote(message))
+        # redirect to the user's page (but not 302, as some email clients seem
+        # to want to reload the page, or something)
+        return '''<html><head><title>%s</title></head>
+            <body><p><a href="%s">%s</a></p>
+            <script type="text/javascript">
+            window.setTimeout('window.location = "%s"', 1000);
+            </script>'''%(message, url, message, url)
 
 class RegisterAction(Action):
     name = 'register'
     permissionType = 'Web Registration'
-    
+
     def handle(self):
         """Attempt to create a new user based on the contents of the form
         and then set the cookie.
 
         Return 1 on successful login.
-        """        
-        props = self.client.parsePropsFromForm()[0][('user', None)]
+        """
+        props = self.client.parsePropsFromForm(create=1)[0][('user', None)]
 
         # registration isn't allowed to supply roles
         if props.has_key('roles'):
-            raise Unauthorised, _("It is not permitted to supply roles at registration.")            
+            raise Unauthorised, _("It is not permitted to supply roles "
+                "at registration.")
 
+        username = props['username']
         try:
-            self.db.user.lookup(props['username'])
-            self.client.error_message.append('Error: A user with the username "%s" '
-                'already exists'%props['username'])
+            self.db.user.lookup(username)
+            self.client.error_message.append(_('Error: A user with the '
+                'username "%(username)s" already exists')%props)
             return
         except KeyError:
             pass
 
         # generate the one-time-key and store the props for later
-        otk = ''.join([random.choice(chars) for x in range(32)])
         for propname, proptype in self.db.user.getprops().items():
             value = props.get(propname, None)
             if value is None:
@@ -680,13 +698,15 @@ class RegisterAction(Action):
                 props[propname] = str(value)
             elif isinstance(proptype, hyperdb.Password):
                 props[propname] = str(value)
-        props['__time'] = time.time()
-        self.db.otks.set(otk, **props)
+        otks = self.db.getOTKManager()
+        while otks.exists(otk):
+            otk = ''.join([random.choice(chars) for x in range(32)])
+        otks.set(otk, **props)
 
         # send the email
         tracker_name = self.db.config.TRACKER_NAME
         tracker_email = self.db.config.TRACKER_EMAIL
-        subject = 'Complete your registration to %s -- key %s' % (tracker_name,
+        subject = 'Complete your registration to %s -- key %s'%(tracker_name,
                                                                   otk)
         body = """To complete your registration of the user "%(name)s" with
 %(tracker)s, please do one of the following: