Code

ensured there's no zero-length files in source (sf bug 633622)
[roundup.git] / roundup / cgi / client.py
index df85b0c85d596b753b84e39327bce2c1fa79c2c6..10fd44897bd8e423e252cc1e8304f868725af3e4 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.49 2002-10-03 06:56:29 richard Exp $
+# $Id: client.py,v 1.55 2002-10-18 03:34:58 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
@@ -313,6 +313,12 @@ class Client:
             # with only a class, we default to index view
             self.template = 'index'
 
+        # make sure the classname is valid
+        try:
+            self.db.getclass(self.classname)
+        except KeyError:
+            raise NotFound, self.classname
+
         # see if we have a template override
         if self.form.has_key(':template'):
             self.template = self.form[':template'].value
@@ -375,6 +381,7 @@ class Client:
         ('login',    'loginAction'),
         ('logout',   'logout_action'),
         ('search',   'searchAction'),
+        ('retire',   'retireAction'),
     )
     def handle_action(self):
         ''' Determine whether there should be an _action called.
@@ -388,7 +395,7 @@ class Client:
              "login"     -> self.loginAction
              "logout"    -> self.logout_action
              "search"    -> self.searchAction
-
+             "retire"    -> self.retireAction
         '''
         if not self.form.has_key(':action'):
             return None
@@ -525,7 +532,8 @@ class Client:
         # make sure we're allowed to be here
         if not self.loginPermission():
             self.make_user_anonymous()
-            raise Unauthorised, _("You do not have permission to login")
+            self.error_message.append(_("You do not have permission to login"))
+            return
 
         # now we're OK, re-open the database for real, using the user
         self.opendb(self.user)
@@ -536,7 +544,12 @@ class Client:
     def verifyPassword(self, userid, password):
         ''' Verify the password that the user has supplied
         '''
-        return password == self.db.user.get(self.userid, 'password')
+        stored = self.db.user.get(self.userid, 'password')
+        if password == stored:
+            return 1
+        if not password and not stored:
+            return 1
+        return 0
 
     def loginPermission(self):
         ''' Determine whether the user has permission to log in.
@@ -603,8 +616,8 @@ class Client:
         # re-open the database for real, using the user
         self.opendb(self.user)
 
-        # update the user's session
-        if self.session:
+        # if we have a session, update it
+        if hasattr(self, 'session'):
             self.db.sessions.set(self.session, user=self.user,
                 last_use=time.time())
         else:
@@ -650,6 +663,11 @@ class Client:
             :required=property,property,...
              The named properties are required to be filled in the form.
 
+            :remove:<propname>=id(s)
+             The ids will be removed from the multilink property.
+            :add:<propname>=id(s)
+             The ids will be added to the multilink property.
+
         '''
         cl = self.db.classes[self.classname]
 
@@ -943,33 +961,46 @@ class Client:
             return 0
         return 1
 
-    def remove_action(self,  dre=re.compile(r'([^\d]+)(\d+)')):
-        # XXX I believe this could be handled by a regular edit action that
-        # just sets the multilink...
-        target = self.index_arg(':target')[0]
-        m = dre.match(target)
-        if m:
-            classname = m.group(1)
-            nodeid = m.group(2)
-            cl = self.db.getclass(classname)
-            cl.retire(nodeid)
-            # now take care of the reference
-            parentref =  self.index_arg(':multilink')[0]
-            parent, prop = parentref.split(':')
-            m = dre.match(parent)
-            if m:
-                self.classname = m.group(1)
-                self.nodeid = m.group(2)
-                cl = self.db.getclass(self.classname)
-                value = cl.get(self.nodeid, prop)
-                value.remove(nodeid)
-                cl.set(self.nodeid, **{prop:value})
-                func = getattr(self, 'show%s'%self.classname)
-                return func()
-            else:
-                raise NotFound, parent
-        else:
-            raise NotFound, target
+    def retireAction(self):
+        ''' 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
+        if self.template == 'index':
+            self.nodeid = None
+
+        # generic edit is per-class only
+        if not self.retirePermission():
+            self.error_message.append(
+                _('You do not have permission to retire %s' %self.classname))
+            return
+
+        # make sure we don't try to retire admin or anonymous
+        if self.classname == 'user' and \
+                self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'):
+            self.error_message.append(
+                _('You may not retire the admin or anonymous user'))
+            return
+
+        # do the retire
+        self.db.getclass(self.classname).retire(nodeid)
+        self.db.commit()
+
+        self.ok_message.append(
+            _('%(classname)s %(itemid)s has been retired')%{
+                'classname': self.classname.capitalize(), 'itemid': nodeid})
+
+    def retirePermission(self):
+        ''' Determine whether the user has permission to retire this class.
+
+            Base behaviour is to check the user can edit this class.
+        ''' 
+        if not self.db.security.hasPermission('Edit', self.userid,
+                self.classname):
+            return 0
+        return 1
+
 
     #
     #  Utility methods for editing
@@ -1027,7 +1058,8 @@ class Client:
         # 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'].value.strip()
+            # fix the CRLF/CR -> LF stuff
+            note = fixNewlines(self.form[':note'].value.strip())
         if not note:
             return None, files
         if not props.has_key('messages'):
@@ -1099,12 +1131,27 @@ class Client:
                     link = self.db.classes[link]
                     link.set(nodeid, **{property: nid})
 
+def fixNewlines(text):
+    ''' Homogenise line endings.
+
+        Different web clients send different line ending values, but
+        other systems (eg. email) don't necessarily handle those line
+        endings. Our solution is to convert all line endings to LF.
+    '''
+    text = text.replace('\r\n', '\n')
+    return text.replace('\r', '\n')
 
 def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
     ''' Pull properties for the given class out of the form.
 
         If a ":required" parameter is supplied, then the names property values
         must be supplied or a ValueError will be raised.
+
+        Other special form values:
+         :remove:<propname>=id(s)
+          The ids will be removed from the multilink property.
+         :add:<propname>=id(s)
+          The ids will be added to the multilink property.
     '''
     required = []
     if form.has_key(':required'):
@@ -1118,19 +1165,37 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
     keys = form.keys()
     properties = cl.getprops()
     for key in keys:
-        if not properties.has_key(key):
+        # see if we're performing a special multilink action
+        mlaction = 'set'
+        if key.startswith(':remove:'):
+            propname = key[8:]
+            mlaction = 'remove'
+        elif key.startswith(':add:'):
+            propname = key[5:]
+            mlaction = 'add'
+        else:
+            propname = key
+
+
+        # does the property exist?
+        if not properties.has_key(propname):
+            if mlaction != 'set':
+                raise ValueError, 'You have submitted a remove action for'\
+                    ' the property "%s" which doesn\'t exist'%propname
             continue
-        proptype = properties[key]
+        proptype = properties[propname]
 
         # Get the form value. This value may be a MiniFieldStorage or a list
         # of MiniFieldStorages.
         value = form[key]
 
+        print (mlaction, propname, value)
+
         # make sure non-multilinks only get one value
         if not isinstance(proptype, hyperdb.Multilink):
             if isinstance(value, type([])):
                 raise ValueError, 'You have submitted more than one value'\
-                    ' for the %s property'%key
+                    ' for the %s property'%propname
             # we've got a MiniFieldStorage, so pull out the value and strip
             # surrounding whitespace
             value = value.value.strip()
@@ -1138,27 +1203,29 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
         if isinstance(proptype, hyperdb.String):
             if not value:
                 continue
+            # fix the CRLF/CR -> LF stuff
+            value = fixNewlines(value)
         elif isinstance(proptype, hyperdb.Password):
             if not value:
                 # ignore empty password values
                 continue
-            if not form.has_key('%s:confirm'%key):
+            if not form.has_key('%s:confirm'%propname):
                 raise ValueError, 'Password and confirmation text do not match'
-            confirm = form['%s:confirm'%key]
+            confirm = form['%s:confirm'%propname]
             if isinstance(confirm, type([])):
                 raise ValueError, 'You have submitted more than one value'\
-                    ' for the %s property'%key
+                    ' for the %s property'%propname
             if value != confirm.value:
                 raise ValueError, 'Password and confirmation text do not match'
             value = password.Password(value)
         elif isinstance(proptype, hyperdb.Date):
             if value:
-                value = date.Date(form[key].value.strip())
+                value = date.Date(value.value.strip())
             else:
                 continue
         elif isinstance(proptype, hyperdb.Interval):
             if value:
-                value = date.Interval(form[key].value.strip())
+                value = date.Interval(value.value.strip())
             else:
                 continue
         elif isinstance(proptype, hyperdb.Link):
@@ -1173,12 +1240,13 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
                         value = db.classes[link].lookup(value)
                     except KeyError:
                         raise ValueError, _('property "%(propname)s": '
-                            '%(value)s not a %(classname)s')%{'propname':key, 
-                            'value': value, 'classname': link}
+                            '%(value)s not a %(classname)s')%{
+                            'propname': propname, 'value': value,
+                            'classname': link}
                     except TypeError, message:
                         raise ValueError, _('you may only enter ID values '
                             'for property "%(propname)s": %(message)s')%{
-                            'propname':key, 'message': message}
+                            'propname': propname, 'message': message}
         elif isinstance(proptype, hyperdb.Multilink):
             if isinstance(value, type([])):
                 # it's a list of MiniFieldStorages
@@ -1197,37 +1265,66 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
                     except KeyError:
                         raise ValueError, _('property "%(propname)s": '
                             '"%(value)s" not an entry of %(classname)s')%{
-                            'propname':key, 'value': entry, 'classname': link}
+                            'propname': propname, 'value': entry,
+                            'classname': link}
                     except TypeError, message:
                         raise ValueError, _('you may only enter ID values '
                             'for property "%(propname)s": %(message)s')%{
-                            'propname':key, 'message': message}
+                            'propname': propname, 'message': message}
                 l.append(entry)
             l.sort()
-            value = l
+
+            # now use that list of ids to modify the multilink
+            if mlaction == 'set':
+                value = l
+            else:
+                # we're modifying the list - get the current list of ids
+                try:
+                    existing = cl.get(nodeid, propname)
+                except KeyError:
+                    existing = []
+                if mlaction == 'remove':
+                    # remove - handle situation where the id isn't in the list
+                    for entry in l:
+                        try:
+                            existing.remove(entry)
+                        except ValueError:
+                            raise ValueError, _('property "%(propname)s": '
+                                '"%(value)s" not currently in list')%{
+                                'propname': propname, 'value': entry}
+                else:
+                    # add - easy, just don't dupe
+                    for entry in l:
+                        if entry not in existing:
+                            existing.append(entry)
+                value = existing
+                value.sort()
+
         elif isinstance(proptype, hyperdb.Boolean):
-            props[key] = value = value.lower() in ('yes', 'true', 'on', '1')
+            value = value.lower() in ('yes', 'true', 'on', '1')
+            props[propname] = value
         elif isinstance(proptype, hyperdb.Number):
-            props[key] = value = int(value)
+            props[propname] = value = int(value)
 
         # register this as received if required?
-        if key in required and value is not None:
-            required.remove(key)
+        if propname in required and value is not None:
+            required.remove(propname)
 
         # get the old value
         if nodeid:
             try:
-                existing = cl.get(nodeid, key)
+                existing = cl.get(nodeid, propname)
             except KeyError:
                 # this might be a new property for which there is no existing
                 # value
-                if not properties.has_key(key): raise
+                if not properties.has_key(propname):
+                    raise
 
             # if changed, set it
             if value != existing:
-                props[key] = value
+                props[propname] = value
         else:
-            props[key] = value
+            props[propname] = value
 
     # see if all the required properties have been supplied
     if required: