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 (?)
 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
 
 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).
 
 __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']
         files = []
         if self.form.has_key(':file'):
             file = self.form[':file']
+
+            # if there's a filename, then we create a file
             if file.filename:
             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"
                 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,
                 # 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
 
         # 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)
 
         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,
         # 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
 
         # 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. 
 
 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
 '''
 
 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
@@ -306,7 +306,7 @@ class MailGW:
 
         # now send the message
         if SENDMAILDEBUG:
 
         # 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:
                 self.instance.config.ADMIN_EMAIL, ', '.join(sendto),
                     m.getvalue()))
         else:
@@ -387,6 +387,23 @@ class MailGW:
         if message.getheader('x-roundup-loop', ''):
             raise MailLoop
 
         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', '')
 
         # handle the subject line
         subject = message.getheader('subject', '')
 
@@ -479,6 +496,52 @@ does not exist.
 Subject was: "%s"
 '''%(nodeid, subject)
 
 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
         #
         #
         # handle the users
         #
@@ -538,14 +601,14 @@ Unknown address: %s
 
             # look up the recipient - create if necessary (and we're
             # allowed to)
 
             # 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)
 
         #
 
             # 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')
 
         #
         subject_args = m.group('args')
 
@@ -557,113 +620,7 @@ Unknown address: %s
         props = {}
         args = m.group('args')
         if args:
         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)
             # 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,
             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)
 
         # 
         # 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,
             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:
 
             # 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:
         # 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:
             if nodeid:
                 cl.set(nodeid, **props)
             else:
@@ -858,6 +821,119 @@ There was a problem with the message you sent:
 
         return nodeid
 
 
         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
 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
 
         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
     ''' 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
 
     '''
     (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,
     # 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
 
     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]+'), 
 def parseContent(content, keep_citations, keep_body,
         blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
         eol=re.compile(r'[\r\n]+'),