From 14ccca8ea95dd75eb4001ca4059c743b0b10a7a8 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 11 Jan 2003 23:52:28 +0000 Subject: [PATCH] support setting of properties on message and file through web and email interface git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1434 57a73879-2fb5-44c3-a270-3262357dd7e2 --- CHANGES.txt | 5 +- roundup/cgi/client.py | 30 +++- roundup/mailgw.py | 310 ++++++++++++++++++++++++++---------------- 3 files changed, 226 insertions(+), 119 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index dfccac8..c27eb53 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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 diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py index da91444..170d86b 100644 --- a/roundup/cgi/client.py +++ b/roundup/cgi/client.py @@ -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 diff --git a/roundup/mailgw.py b/roundup/mailgw.py index bca8851..8f91118 100644 --- a/roundup/mailgw.py +++ b/roundup/mailgw.py @@ -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[^\d\s]+)(?P\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. - # =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. + # =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]+'), -- 2.30.2