Code

support setting of properties on message and file through web and email
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sat, 11 Jan 2003 23:52:28 +0000 (23:52 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sat, 11 Jan 2003 23:52:28 +0000 (23:52 +0000)
interface

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1434 57a73879-2fb5-44c3-a270-3262357dd7e2

CHANGES.txt
roundup/cgi/client.py
roundup/mailgw.py

index dfccac849c11572a62b9f9fbc9e9581783c72418..c27eb53bd3698a6752d0644cca84492a8fc73ea7 100644 (file)
@@ -2,7 +2,10 @@ This file contains the changes to the Roundup system over time. The entries
 are given with the most recent entry first.
 
 2003-??-?? 0.6.0 (?)
-- better hyperlinking
+- better hyperlinking in web message texts
+- support setting of properties on message and file through web and
+  email interface (thanks John Rouillard)
+
 
 2003-01-10 0.5.4
 - key the templates cache off full path, not filename
index da91444bb89afd4c3b172ec9e9fae73305b8e8d0..170d86bc87a4cd03b0e4a35e3fcebc3e4f6cb417 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.65 2003-01-08 04:39:36 richard Exp $
+# $Id: client.py,v 1.66 2003-01-11 23:52:28 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
@@ -1050,14 +1050,28 @@ class Client:
         files = []
         if self.form.has_key(':file'):
             file = self.form[':file']
+
+            # if there's a filename, then we create a file
             if file.filename:
+                # see if there are any file properties we should set
+                file_props={};
+                if self.form.has_key(':file_fields'):
+                    for field in self.form[':file_fields'].value.split(','):
+                        if self.form.has_key(field):
+                            if field.startswith("file_"):
+                                file_props[field[5:]] = self.form[field].value
+                            else :
+                                file_props[field] = self.form[field].value
+
+                # try to determine the file content-type
                 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=filename, content=file.file.read()))
+                    name=filename, content=file.file.read(), **file_props))
 
         # we don't want to do a message if none of the following is true...
         cn = self.classname
@@ -1092,11 +1106,21 @@ class Client:
         messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(),
             self.classname, self.instance.config.MAIL_DOMAIN)
 
+        # see if there are any message properties we should set
+        msg_props={};
+        if self.form.has_key(':msg_fields'):
+            for field in self.form[':msg_fields'].value.split(','):
+                if self.form.has_key(field):
+                    if field.startswith("msg_"):
+                        msg_props[field[4:]] = self.form[field].value
+                    else :
+                        msg_props[field] = self.form[field].value
+
         # now create the message, attaching the files
         content = '\n'.join(m)
         message_id = self.db.msg.create(author=self.userid,
             recipients=[], date=date.Date('.'), summary=summary,
-            content=content, files=files, messageid=messageid)
+            content=content, files=files, messageid=messageid, **msg_props)
 
         # update the messages property
         return message_id, files
index bca88512ca436da354579c4f19f646c1e9cc3a64..8f91118ca8e19f350e1d73bb919258621c303c85 100644 (file)
@@ -73,7 +73,7 @@ are calling the create() method to create a new node). If an auditor raises
 an exception, the original message is bounced back to the sender with the
 explanatory message given in the exception. 
 
-$Id: mailgw.py,v 1.104 2003-01-06 21:28:38 richard Exp $
+$Id: mailgw.py,v 1.105 2003-01-11 23:52:27 richard Exp $
 '''
 
 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
@@ -306,7 +306,7 @@ class MailGW:
 
         # now send the message
         if SENDMAILDEBUG:
-            open(SENDMAILDEBUG, 'w').write('From: %s\nTo: %s\n%s\n'%(
+            open(SENDMAILDEBUG, 'a').write('From: %s\nTo: %s\n%s\n'%(
                 self.instance.config.ADMIN_EMAIL, ', '.join(sendto),
                     m.getvalue()))
         else:
@@ -387,6 +387,23 @@ class MailGW:
         if message.getheader('x-roundup-loop', ''):
             raise MailLoop
 
+        # XXX Don't enable. This doesn't work yet.
+#  "[^A-z.]tracker\+(?P<classname>[^\d\s]+)(?P<nodeid>\d+)\@some.dom.ain[^A-z.]"
+        # handle delivery to addresses like:tracker+issue25@some.dom.ain
+        # use the embedded issue number as our issue
+#        if hasattr(self.instance.config, 'EMAIL_ISSUE_ADDRESS_RE') and \
+#                self.instance.config.EMAIL_ISSUE_ADDRESS_RE:
+#            issue_re = self.instance.config.EMAIL_ISSUE_ADDRESS_RE
+#            for header in ['to', 'cc', 'bcc']:
+#                addresses = message.getheader(header, '')
+#            if addresses:
+#              # FIXME, this only finds the first match in the addresses.
+#                issue = re.search(issue_re, addresses, 'i')
+#                if issue:
+#                    classname = issue.group('classname')
+#                    nodeid = issue.group('nodeid')
+#                    break
+
         # handle the subject line
         subject = message.getheader('subject', '')
 
@@ -479,6 +496,52 @@ does not exist.
 Subject was: "%s"
 '''%(nodeid, subject)
 
+        #
+        # Handle the options specified by the email gateway
+        # command line. I do this by looping over the list of
+        # self.options looking for a -C to tell me what class
+        # I add the -S setting string to.
+        #
+        msg_props = {}
+        user_props = {}
+        file_props = {}
+        issue_props = {}
+        # this should be true if options are set on command
+        # line
+        if hasattr(self, 'options'):
+            current_class = 'msg'
+            for option, propstring in self.options:
+                if option in ( '-C', '--class'):
+                    current_class = propstring.strip()
+                    if current_class not in ('msg', 'file', 'user', 'issue'):
+                        raise MailUsageError, '''
+The mail gateway is not properly set up. Please contact
+%s and have them fix the incorrect class specified as:
+  %s
+'''%(self.instance.config.ADMIN_EMAIL, current_class)
+                if option in ('-S', '--set'):
+                    if current_class == 'issue' :
+                        errors, issue_props = setPropArrayFromString(self,
+                            cl, propstring.strip(), nodeid)
+                    elif current_class == 'file' :
+                        temp_cl = self.db.getclass('file')
+                        errors, file_props = setPropArrayFromString(self,
+                            temp_cl, propstring.strip())
+                    elif current_class == 'msg' :
+                        temp_cl = self.db.getclass('msg')
+                        errors, msg_props = setPropArrayFromString(self,
+                            temp_cl, propstring.strip())
+                    elif current_class == 'user' :
+                        temp_cl = self.db.getclass('user')
+                        errors, user_props = setPropArrayFromString(self,
+                            temp_cl, propstring.strip())
+                    if errors:
+                        raise MailUsageError, '''
+The mail gateway is not properly set up. Please contact
+%s and have them fix the incorrect properties:
+  %s
+'''%(self.instance.config.ADMIN_EMAIL, errors)
+
         #
         # handle the users
         #
@@ -538,14 +601,14 @@ Unknown address: %s
 
             # look up the recipient - create if necessary (and we're
             # allowed to)
-            recipient = uidFromAddress(self.db, recipient, create)
+            recipient = uidFromAddress(self.db, recipient, create, **user_props)
 
             # if all's well, add the recipient to the list
             if recipient:
                 recipients.append(recipient)
 
         #
-        # extract the args
+        # XXX extract the args NOT USED WHY -- rouilj
         #
         subject_args = m.group('args')
 
@@ -557,113 +620,7 @@ Unknown address: %s
         props = {}
         args = m.group('args')
         if args:
-            errors = []
-            for prop in string.split(args, ';'):
-                # extract the property name and value
-                try:
-                    propname, value = prop.split('=')
-                except ValueError, message:
-                    errors.append('not of form [arg=value,'
-                        'value,...;arg=value,value...]')
-                    break
-
-                # ensure it's a valid property name
-                propname = propname.strip()
-                try:
-                    proptype =  properties[propname]
-                except KeyError:
-                    errors.append('refers to an invalid property: '
-                        '"%s"'%propname)
-                    continue
-
-                # convert the string value to a real property value
-                if isinstance(proptype, hyperdb.String):
-                    props[propname] = value.strip()
-                if isinstance(proptype, hyperdb.Password):
-                    props[propname] = password.Password(value.strip())
-                elif isinstance(proptype, hyperdb.Date):
-                    try:
-                        props[propname] = date.Date(value.strip())
-                    except ValueError, message:
-                        errors.append('contains an invalid date for '
-                            '%s.'%propname)
-                elif isinstance(proptype, hyperdb.Interval):
-                    try:
-                        props[propname] = date.Interval(value)
-                    except ValueError, message:
-                        errors.append('contains an invalid date interval'
-                            'for %s.'%propname)
-                elif isinstance(proptype, hyperdb.Link):
-                    linkcl = self.db.classes[proptype.classname]
-                    propkey = linkcl.labelprop(default_to_id=1)
-                    try:
-                        props[propname] = linkcl.lookup(value)
-                    except KeyError, message:
-                        errors.append('"%s" is not a value for %s.'%(value,
-                            propname))
-                elif isinstance(proptype, hyperdb.Multilink):
-                    # get the linked class
-                    linkcl = self.db.classes[proptype.classname]
-                    propkey = linkcl.labelprop(default_to_id=1)
-                    if nodeid:
-                        curvalue = cl.get(nodeid, propname)
-                    else:
-                        curvalue = []
-
-                    # handle each add/remove in turn
-                    # keep an extra list for all items that are
-                    # definitely in the new list (in case of e.g.
-                    # <propname>=A,+B, which should replace the old
-                    # list with A,B)
-                    set = 0
-                    newvalue = []
-                    for item in value.split(','):
-                        item = item.strip()
-
-                        # handle +/-
-                        remove = 0
-                        if item.startswith('-'):
-                            remove = 1
-                            item = item[1:]
-                        elif item.startswith('+'):
-                            item = item[1:]
-                        else:
-                            set = 1
-
-                        # look up the value
-                        try:
-                            item = linkcl.lookup(item)
-                        except KeyError, message:
-                            errors.append('"%s" is not a value for %s.'%(item,
-                                propname))
-                            continue
-
-                        # perform the add/remove
-                        if remove:
-                            try:
-                                curvalue.remove(item)
-                            except ValueError:
-                                errors.append('"%s" is not currently in '
-                                    'for %s.'%(item, propname))
-                                continue
-                        else:
-                            newvalue.append(item)
-                            if item not in curvalue:
-                                curvalue.append(item)
-
-                    # that's it, set the new Multilink property value,
-                    # or overwrite it completely
-                    if set:
-                        props[propname] = newvalue
-                    else:
-                        props[propname] = curvalue
-                elif isinstance(proptype, hyperdb.Boolean):
-                    value = value.strip()
-                    props[propname] = value.lower() in ('yes', 'true', 'on', '1')
-                elif isinstance(proptype, hyperdb.Number):
-                    value = value.strip()
-                    props[propname] = int(value)
-
+            errors, props = setPropArrayFromString(self, cl, args, nodeid)
             # handle any errors parsing the argument list
             if errors:
                 errors = '\n- '.join(errors)
@@ -814,7 +771,7 @@ not find a text/plain part to use.
             if not name:
                 name = "unnamed"
             files.append(self.db.file.create(type=mime_type, name=name,
-                content=data))
+                content=data, **file_props))
 
         # 
         # create the message if there's a message body (content)
@@ -823,7 +780,7 @@ not find a text/plain part to use.
             message_id = self.db.msg.create(author=author,
                 recipients=recipients, date=date.Date('.'), summary=summary,
                 content=content, files=files, messageid=messageid,
-                inreplyto=inreplyto)
+                inreplyto=inreplyto, **msg_props)
 
             # attach the message to the node
             if nodeid:
@@ -843,6 +800,12 @@ not find a text/plain part to use.
         # perform the node change / create
         #
         try:
+            # merge the command line props defined in issue_props into
+            # the props dictionary because function(**props, **issue_props)
+            # is a syntax error.
+            for prop in issue_props.keys() :
+                if not props.has_key(prop) :
+                    props[prop] = issue_props[prop]
             if nodeid:
                 cl.set(nodeid, **props)
             else:
@@ -858,6 +821,119 @@ There was a problem with the message you sent:
 
         return nodeid
 
+def setPropArrayFromString(self, cl, propString, nodeid = None):
+    ''' takes string of form prop=value,value;prop2=value
+        and returns (error, prop[..])
+    '''
+    properties = cl.getprops()
+    props = {}
+    errors = []
+    for prop in string.split(propString, ';'):
+        # extract the property name and value
+        try:
+            propname, value = prop.split('=')
+        except ValueError, message:
+            errors.append('not of form [arg=value,value,...;'
+                'arg=value,value,...]')
+            return (errors, props)
+
+        # ensure it's a valid property name
+        propname = propname.strip()
+        try:
+            proptype =  properties[propname]
+        except KeyError:
+            errors.append('refers to an invalid property: "%s"'%propname)
+            continue
+
+        # convert the string value to a real property value
+        if isinstance(proptype, hyperdb.String):
+            props[propname] = value.strip()
+        if isinstance(proptype, hyperdb.Password):
+            props[propname] = password.Password(value.strip())
+        elif isinstance(proptype, hyperdb.Date):
+            try:
+                props[propname] = date.Date(value.strip())
+            except ValueError, message:
+                errors.append('contains an invalid date for %s.'%propname)
+        elif isinstance(proptype, hyperdb.Interval):
+            try:
+                props[propname] = date.Interval(value)
+            except ValueError, message:
+                errors.append('contains an invalid date interval for %s.'%
+                    propname)
+        elif isinstance(proptype, hyperdb.Link):
+            linkcl = self.db.classes[proptype.classname]
+            propkey = linkcl.labelprop(default_to_id=1)
+            try:
+                props[propname] = linkcl.lookup(value)
+            except KeyError, message:
+                errors.append('"%s" is not a value for %s.'%(value, propname))
+        elif isinstance(proptype, hyperdb.Multilink):
+            # get the linked class
+            linkcl = self.db.classes[proptype.classname]
+            propkey = linkcl.labelprop(default_to_id=1)
+            if nodeid:
+                curvalue = cl.get(nodeid, propname)
+            else:
+                curvalue = []
+
+            # handle each add/remove in turn
+            # keep an extra list for all items that are
+            # definitely in the new list (in case of e.g.
+            # <propname>=A,+B, which should replace the old
+            # list with A,B)
+            set = 0
+            newvalue = []
+            for item in value.split(','):
+                item = item.strip()
+
+                # handle +/-
+                remove = 0
+                if item.startswith('-'):
+                    remove = 1
+                    item = item[1:]
+                elif item.startswith('+'):
+                    item = item[1:]
+                else:
+                    set = 1
+
+                # look up the value
+                try:
+                    item = linkcl.lookup(item)
+                except KeyError, message:
+                    errors.append('"%s" is not a value for %s.'%(item,
+                        propname))
+                    continue
+
+                # perform the add/remove
+                if remove:
+                    try:
+                        curvalue.remove(item)
+                    except ValueError:
+                        errors.append('"%s" is not currently in for %s.'%(item,
+                            propname))
+                        continue
+                else:
+                    newvalue.append(item)
+                    if item not in curvalue:
+                        curvalue.append(item)
+
+            # that's it, set the new Multilink property value,
+            # or overwrite it completely
+            if set:
+                props[propname] = newvalue
+            else:
+                props[propname] = curvalue
+        elif isinstance(proptype, hyperdb.Boolean):
+            value = value.strip()
+            props[propname] = value.lower() in ('yes', 'true', 'on', '1')
+        elif isinstance(proptype, hyperdb.Number):
+            value = value.strip()
+            props[propname] = int(value)
+    return errors, props
+
+
 def extractUserFromList(userClass, users):
     '''Given a list of users, try to extract the first non-anonymous user
        and return that user, otherwise return None
@@ -875,10 +951,12 @@ def extractUserFromList(userClass, users):
         return users[0]
     return None
 
-def uidFromAddress(db, address, create=1):
+
+def uidFromAddress(db, address, create=1, **user_props):
     ''' address is from the rfc822 module, and therefore is (name, addr)
 
         user is created if they don't exist in the db already
+        user_props may supply additional user information
     '''
     (realname, address) = address
 
@@ -900,10 +978,12 @@ def uidFromAddress(db, address, create=1):
     # couldn't match address or username, so create a new user
     if create:
         return db.user.create(username=address, address=address,
-            realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES)
+            realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES,
+            **user_props)
     else:
         return 0
 
+
 def parseContent(content, keep_citations, keep_body,
         blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
         eol=re.compile(r'[\r\n]+'),