From e0682d7cfa374ce2f417ddcf0e652f030fb5dcd0 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 29 May 2002 01:16:17 +0000 Subject: [PATCH] Sorry about this huge checkin! It's fixing a lot of related stuff in one go though. . #541941 ] changing multilink properties by mail . #526730 ] search for messages capability . #505180 ] split MailGW.handle_Message - also changed cgi client since it was duplicating the functionality . build htmlbase if tests are run using CVS checkout (removed note from installation.txt) . don't create an empty message on email issue creation if the email is empty git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@766 57a73879-2fb5-44c3-a270-3262357dd7e2 --- CHANGES.txt | 5 + MANIFEST.in | 2 +- MIGRATION.txt | 36 ++- doc/installation.txt | 6 +- doc/user_guide.txt | 19 +- roundup/cgi_client.py | 97 +----- roundup/mailgw.py | 302 +++++++----------- roundup/roundupdb.py | 48 +-- .../classic/detectors/nosyreaction.py | 104 +++++- .../classic/detectors/statusauditor.py | 68 ++++ .../templates/extended/detectors/__init__.py | 8 +- .../extended/detectors/nosyreaction.py | 106 +++++- .../extended/detectors/statusauditor.py | 68 ++++ run_tests | 12 +- setup.py | 108 ++++--- test/__init__.py | 12 +- test/test_mailgw.py | 103 +++--- 17 files changed, 671 insertions(+), 433 deletions(-) create mode 100644 roundup/templates/classic/detectors/statusauditor.py create mode 100644 roundup/templates/extended/detectors/statusauditor.py diff --git a/CHANGES.txt b/CHANGES.txt index ec62d75..e781a45 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -32,7 +32,11 @@ Feature: . applied patch #558876 ] cgi client customization . split instance initialisation into two steps, allowing config changes before the database is initialised. + . don't create an empty message on email issue creation if the email is empty + . #541941 ] changing multilink properties by mail . #526730 ] search for messages capability + . #505180 ] split MailGW.handle_Message + - also changed cgi client since it was duplicating the functionality Fixed: . stop sending blank (whitespace-only) notes @@ -58,6 +62,7 @@ Fixed: . cleaned out the template stylesheets, removing a bunch of junk that really wasn't necessary (font specs, styles never used) and added a style for message content + . build htmlbase if tests are run using CVS checkout 2002-03-25 - 0.4.1 Feature: diff --git a/MANIFEST.in b/MANIFEST.in index 05ca55e..0fc4e8f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,5 +5,5 @@ recursive-include test *.py *.txt recursive-include doc *.html *.png *.txt exclude doc/announcement.txt include roundup-* -include *.txt +include run_tests *.txt diff --git a/MIGRATION.txt b/MIGRATION.txt index 7730c36..ca793cd 100644 --- a/MIGRATION.txt +++ b/MIGRATION.txt @@ -9,18 +9,7 @@ This file contains information for users upgrading from: 0.3.x -> 0.4.x 0.2.x -> 0.3.x -From CVS -======== - -Files storage -------------- - -Messages and files from newly created issues will be put into subdierectories -in thousands e.g. msg123 will be put into files/msg/0/msg123, file2003 -will go into files/file/2/file2003. Previous messages are still found, but -could be put into this structure. - -Migrating from 0.4.0 to 0.4.1 +Migrating from 0.4.1 to 0.4.2 ============================= Configuration @@ -38,9 +27,30 @@ The new configuration variables are: - ADD_RECIPIENTS_TO_NOSY +Mail Gateway +------------ + +You will need to copy the detectors from the distribution into your instance +home detectors directory. The schema-specific code has been removed from the +mail gateway and made into auditors: + +- nosyreactor.py has now got an updatenosy auditor which updates the nosy + list with author, recipient and assignedto information. +- statusauditor.py makes the unread or resolved -> chatting changes and + presets the status of an issue to unread. + + Migrating from 0.4.0 to 0.4.1 ============================= +Files storage +------------- + +Messages and files from newly created issues will be put into subdierectories +in thousands e.g. msg123 will be put into files/msg/0/msg123, file2003 +will go into files/file/2/file2003. Previous messages are still found, but +could be put into this structure. + Configuration ------------- @@ -68,8 +78,6 @@ the appropriate variables in instance_config.py. The extended schema has similar variables added too - see the source for more info. - - Alternate E-Mail Addresses -------------------------- diff --git a/doc/installation.txt b/doc/installation.txt index 908cd59..e61279a 100644 --- a/doc/installation.txt +++ b/doc/installation.txt @@ -2,7 +2,7 @@ Installing Roundup ================== -:Version: $Revision: 1.10 $ +:Version: $Revision: 1.11 $ .. contents:: @@ -69,10 +69,6 @@ Testing your Python Run ``"python ./run_tests"`` and make sure there are no errors. If there are errors, please let us know! -Note: if you're installing from the CVS, you will need to run "python setup.py -build" before the tests will work, as there are modules that must be -generated that are not stored in the CVS. - Getting Roundup =============== diff --git a/doc/user_guide.txt b/doc/user_guide.txt index 1c1cc3a..87c7373 100644 --- a/doc/user_guide.txt +++ b/doc/user_guide.txt @@ -2,7 +2,7 @@ User Guide ========== -:Version: $Revision: 1.3 $ +:Version: $Revision: 1.4 $ .. contents:: @@ -222,7 +222,22 @@ Setting Properties The e-mail interface also provides a simple way to set properties on items. At the end of the subject line, propname=value pairs can be specified in square brackets, using the same conventions as for the roundup set shell command. -explanatory message given in the exception. + +For example, + +- setting the priority of an issue:: + + Subject: Re: [issue1] the coffee machine is broken! [priority=urgent] + +- adding yourself to a nosy list (both of these are equivalent):: + + Subject: Re: [issue2] we're out of widgets [nosy=+richard] + Subject: Re: [issue2] we're out of widgets [nosy=richard] + +- removing yourself from a nosy list:: + + Subject: Re: [issue2] we're out of widgets [nosy=-richard] + Message content ~~~~~~~~~~~~~~~ diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py index 8fb4139..ae013fc 100644 --- a/roundup/cgi_client.py +++ b/roundup/cgi_client.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: cgi_client.py,v 1.125 2002-05-25 07:16:24 rochecompaan Exp $ +# $Id: cgi_client.py,v 1.126 2002-05-29 01:16:17 richard Exp $ __doc__ = """ WWW request handler (also used in the stand-alone server). @@ -604,86 +604,10 @@ function help_window(helpurl, width, height) { showmsg = shownode searchissue = searchnode - def _add_author_to_nosy(self, props): - ''' add the author value from the props to the nosy list - ''' - if not props.has_key('author'): - return - author_id = props['author'] - if not props.has_key('nosy'): - # load current nosy - if self.nodeid: - cl = self.db.classes[self.classname] - l = cl.get(self.nodeid, 'nosy') - if author_id in l: - return - props['nosy'] = l - else: - props['nosy'] = [] - if author_id not in props['nosy']: - props['nosy'].append(author_id) - - def _add_assignedto_to_nosy(self, props): - ''' add the assignedto value from the props to the nosy list - ''' - # get the properties definition and make some checks - if not props.has_key('assignedto'): - return - cl = self.db.classes[self.classname] - propdef = cl.getprops() - if not propdef.has_key('assignedto') or not propdef.has_key('nosy'): - return - - # get the assignedto(s) - if isinstance(propdef['assignedto'], hyperdb.Link): - assignedto_ids = [props['assignedto']] - elif isinstance(propdef['assignedto'], hyperdb.Multilink): - assignedto_ids = props['assignedto'] - else: - return - - # ok, now get the nosy list value - if not props.has_key('nosy'): - # load current nosy - if self.nodeid: - props['nosy'] = cl.get(self.nodeid, 'nosy') - else: - props['nosy'] = [] - - # and update for assignedto id(s) - for assignedto_id in assignedto_ids: - if assignedto_id not in props['nosy']: - props['nosy'].append(assignedto_id) - def _changenode(self, props): ''' change the node based on the contents of the form ''' cl = self.db.classes[self.classname] - # set status to chatting if 'unread' or 'resolved' - try: - # determine the id of 'unread','resolved' and 'chatting' - unread_id = self.db.status.lookup('unread') - resolved_id = self.db.status.lookup('resolved') - chatting_id = self.db.status.lookup('chatting') - current_status = cl.get(self.nodeid, 'status') - if props.has_key('status'): - new_status = props['status'] - else: - # apparently there's a chance that some browsers don't - # send status... - new_status = current_status - except KeyError: - pass - else: - if new_status == unread_id or (new_status == resolved_id - and current_status == resolved_id): - props['status'] = chatting_id - - self._add_assignedto_to_nosy(props) - - # possibly add the author of the change to the nosy list - if self.db.config.ADD_AUTHOR_TO_NOSY == 'yes': - self._add_author_to_nosy(props) # create the message message, files = self._handle_message() @@ -701,22 +625,6 @@ function help_window(helpurl, width, height) { cl = self.db.classes[self.classname] props = parsePropsFromForm(self.db, cl, self.form) - # set status to 'unread' if not specified - a status of '- no - # selection -' doesn't make sense - if not props.has_key('status') and cl.getprops().has_key('status'): - try: - unread_id = self.db.status.lookup('unread') - except KeyError: - pass - else: - props['status'] = unread_id - - self._add_assignedto_to_nosy(props) - - # possibly add the author of the new node to the nosy list - if self.db.config.ADD_AUTHOR_TO_NOSY in ('new', 'yes'): - self._add_author_to_nosy(props) - # check for messages and files message, files = self._handle_message() if message: @@ -1454,6 +1362,9 @@ def parsePropsFromForm(db, cl, form, nodeid=0): # # $Log: not supported by cvs2svn $ +# Revision 1.125 2002/05/25 07:16:24 rochecompaan +# Merged search_indexing-branch with HEAD +# # Revision 1.124 2002/05/24 02:09:24 richard # Nothing like a live demo to show up the bugs ;) # diff --git a/roundup/mailgw.py b/roundup/mailgw.py index 4c99afb..53dc401 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.73 2002-05-22 04:12:05 richard Exp $ +$Id: mailgw.py,v 1.74 2002-05-29 01:16:17 richard Exp $ ''' @@ -344,7 +344,7 @@ Subject was: "%s" title = '' # but we do need either a title or a nodeid... - if not nodeid and not title: + if nodeid is None and not title: raise MailUsageError, ''' I cannot match your message to a node in the database - you need to either supply a full node identifier (with number, eg "[issue123]" or keep the @@ -353,105 +353,135 @@ previous subject title intact so I can match that. Subject was: "%s" '''%subject - # extract the args - subject_args = m.group('args') - # If there's no nodeid, check to see if this is a followup and # maybe someone's responded to the initial mail that created an # entry. Try to find the matching nodes with the same title, and # use the _last_ one matched (since that'll _usually_ be the most # recent...) - if not nodeid and m.group('refwd'): + if nodeid is None and m.group('refwd'): l = cl.stringFind(title=title) if l: nodeid = l[-1] - # start of the props + # if a nodeid was specified, make sure it's valid + if nodeid is not None and not cl.hasnode(nodeid): + raise MailUsageError, ''' +The node specified by the designator in the subject of your message ("%s") +does not exist. + +Subject was: "%s" +'''%(nodeid, subject) + + # + # extract the args + # + subject_args = m.group('args') + + # + # handle the subject argument list + # + # figure what the properties of this Class are properties = cl.getprops() props = {} - - # handle the args args = m.group('args') if args: + errors = [] for prop in string.split(args, ';'): # extract the property name and value try: - key, value = prop.split('=') + propname, value = prop.split('=') except ValueError, message: - raise MailUsageError, ''' -Subject argument list not of form [arg=value,value,...;arg=value,value...] - (specific exception message was "%s") - -Subject was: "%s" -'''%(message, subject) + errors.append('not of form [arg=value,' + 'value,...;arg=value,value...]') + break # ensure it's a valid property name - key = key.strip() + propname = propname.strip() try: - proptype = properties[key] + proptype = properties[propname] except KeyError: - raise MailUsageError, ''' -Subject argument list refers to an invalid property: "%s" - -Subject was: "%s" -'''%(key, subject) + 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[key] = value.strip() + props[propname] = value.strip() if isinstance(proptype, hyperdb.Password): - props[key] = password.Password(value.strip()) + props[propname] = password.Password(value.strip()) elif isinstance(proptype, hyperdb.Date): try: - props[key] = date.Date(value.strip()) + props[propname] = date.Date(value.strip()) except ValueError, message: - raise UsageError, ''' -Subject argument list contains an invalid date for %s. - -Error was: %s -Subject was: "%s" -'''%(key, message, subject) + errors.append('contains an invalid date for ' + '%s.'%propname) elif isinstance(proptype, hyperdb.Interval): try: - props[key] = date.Interval(value) # no strip needed + props[propname] = date.Interval(value) except ValueError, message: - raise UsageError, ''' -Subject argument list contains an invalid date interval for %s. - -Error was: %s -Subject was: "%s" -'''%(key, message, subject) + 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[key] = linkcl.lookup(value) + props[propname] = linkcl.lookup(value) except KeyError, message: - raise MailUsageError, ''' -Subject argument list contains an invalid value for %s. - -Error was: %s -Subject was: "%s" -'''%(key, message, subject) + 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 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:] + + # look up the value try: item = linkcl.lookup(item) except KeyError, message: - raise MailUsageError, ''' -Subject argument list contains an invalid value for %s. + 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: + if item not in curvalue: + curvalue.append(item) + + # that's it, set the new Multilink property value + props[propname] = curvalue + + # handle any errors parsing the argument list + if errors: + errors = '\n- '.join(errors) + raise MailUsageError, ''' +There were problems handling your subject line argument list: +- %s -Error was: %s Subject was: "%s" -'''%(key, message, subject) - if props.has_key(key): - props[key].append(item) - else: - props[key] = [item] +'''%(errors, subject) # # handle the users @@ -624,156 +654,48 @@ not find a text/plain part to use. files.append(self.db.file.create(type=mime_type, name=name, content=data)) + # + # create the message if there's a message body (content) # - # now handle the db stuff - # - if nodeid: - # If an item designator (class name and id number) is found there, - # the newly created "msg" node is added to the "messages" property - # for that item, and any new "file" nodes are added to the "files" - # property for the item. - - # if the message is currently 'unread' or 'resolved', then set - # it to 'chatting' - if properties.has_key('status'): - try: - # determine the id of 'unread', 'resolved' and 'chatting' - unread_id = self.db.status.lookup('unread') - resolved_id = self.db.status.lookup('resolved') - chatting_id = self.db.status.lookup('chatting') - except KeyError: - pass - else: - current_status = cl.get(nodeid, 'status') - if (not props.has_key('status') and - current_status == unread_id or - current_status == resolved_id): - props['status'] = chatting_id - - # update the nosy list - current = {} - for nid in cl.get(nodeid, 'nosy'): - current[nid] = 1 - self.updateNosy(cl, props, author, recipients, current) - - # create the message + if content: message_id = self.db.msg.create(author=author, recipients=recipients, date=date.Date('.'), summary=summary, content=content, files=files, messageid=messageid, inreplyto=inreplyto) - try: + + # attach the message to the node + if nodeid: + # add the message to the node's list messages = cl.get(nodeid, 'messages') - except IndexError: - raise MailUsageError, ''' -The node specified by the designator in the subject of your message ("%s") -does not exist. + messages.append(message_id) + props['messages'] = messages + else: + # pre-load the messages list + props['messages'] = [message_id] -Subject was: "%s" -'''%(nodeid, subject) - messages.append(message_id) - props['messages'] = messages + # set the title to the subject + if properties.has_key('title') and not props.has_key('title'): + props['title'] = title - # now apply the changes - try: + # + # perform the node change / create + # + try: + if nodeid: cl.set(nodeid, **props) - except (TypeError, IndexError, ValueError), message: - raise MailUsageError, ''' -There was a problem with the message you sent: - %s -'''%message - # commit the changes to the DB - self.db.commit() - else: - # If just an item class name is found there, we attempt to create a - # new item of that class with its "messages" property initialized to - # contain the new "msg" node and its "files" property initialized to - # contain any new "file" nodes. - message_id = self.db.msg.create(author=author, - recipients=recipients, date=date.Date('.'), summary=summary, - content=content, files=files, messageid=messageid, - inreplyto=inreplyto) - - # pre-set the issue to unread - if properties.has_key('status') and not props.has_key('status'): - try: - # determine the id of 'unread' - unread_id = self.db.status.lookup('unread') - except KeyError: - pass - else: - props['status'] = '1' - - # set the title to the subject - if properties.has_key('title') and not props.has_key('title'): - props['title'] = title - - # pre-load the messages list - props['messages'] = [message_id] - - # set up (clean) the nosy list - self.updateNosy(cl, props, author, recipients) - - # and attempt to create the new node - try: + else: nodeid = cl.create(**props) - except (TypeError, IndexError, ValueError), message: - raise MailUsageError, ''' + except (TypeError, IndexError, ValueError), message: + raise MailUsageError, ''' There was a problem with the message you sent: %s '''%message - # commit the new node(s) to the DB - self.db.commit() + # commit the changes to the DB + self.db.commit() return nodeid - def updateNosy(self, cl, props, author, recipients, current=None): - '''Determine what the nosy list should be given: - - props: properties specified on the subject line of the message - author: the sender of the message - recipients: the recipients (to, cc) of the message - current: if the issue already exists, this is the current nosy - list, as a dictionary. - ''' - if current is None: - current = {} - ok = ('new', 'yes') - else: - ok = ('yes',) - - # add nosy in arguments to issue's nosy list - nosy = props.get('nosy', []) - for value in nosy: - if not self.db.hasnode('user', value): - continue - if not current.has_key(value): - current[value] = 1 - - # add the author to the nosy list - if getattr(self.instance, 'ADD_AUTHOR_TO_NOSY', 'new') in ok: - if not current.has_key(author): - current[author] = 1 - - # add on the recipients of the message - if getattr(self.instance, 'ADD_RECIPIENTS_TO_NOSY', 'new') in ok: - for recipient in recipients: - if not current.has_key(recipient): - current[recipient] = 1 - - # add assignedto(s) to the nosy list - if props.has_key('assignedto'): - propdef = cl.getprops() - if isinstance(propdef['assignedto'], hyperdb.Link): - assignedto_ids = [props['assignedto']] - elif isinstance(propdef['assignedto'], hyperdb.Multilink): - assignedto_ids = props['assignedto'] - for assignedto_id in assignedto_ids: - if not current.has_key(assignedto_id): - current[assignedto_id] = 1 - - props['nosy'] = current.keys() - def parseContent(content, keep_citations, keep_body, blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'), eol=re.compile(r'[\r\n]+'), @@ -843,6 +765,12 @@ def parseContent(content, keep_citations, keep_body, # # $Log: not supported by cvs2svn $ +# Revision 1.73 2002/05/22 04:12:05 richard +# . applied patch #558876 ] cgi client customization +# ... with significant additions and modifications ;) +# - extended handling of ML assignedto to all places it's handled +# - added more NotFound info +# # Revision 1.72 2002/05/22 01:24:51 richard # Added note to MIGRATION about new config vars. Also made us more resilient # for upgraders. Reinstated list header style (oops) diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py index f874db8..9f1d81d 100644 --- a/roundup/roundupdb.py +++ b/roundup/roundupdb.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundupdb.py,v 1.53 2002-05-25 07:16:24 rochecompaan Exp $ +# $Id: roundupdb.py,v 1.54 2002-05-29 01:16:17 richard Exp $ __doc__ = """ Extending hyperdb with types specific to issue-tracking. @@ -115,11 +115,9 @@ class Class(hyperdb.Class): """ if propvalues.has_key('creation') or propvalues.has_key('activity'): raise KeyError, '"creation" and "activity" are reserved' - for audit in self.auditors['create']: - audit(self.db, self, None, propvalues) + self.fireAuditors('create', None, propvalues) nodeid = hyperdb.Class.create(self, **propvalues) - for react in self.reactors['create']: - react(self.db, self, nodeid, None) + self.fireReactors('create', nodeid, None) return nodeid def set(self, nodeid, **propvalues): @@ -128,8 +126,7 @@ class Class(hyperdb.Class): """ if propvalues.has_key('creation') or propvalues.has_key('activity'): raise KeyError, '"creation" and "activity" are reserved' - for audit in self.auditors['set']: - audit(self.db, self, nodeid, propvalues) + self.fireAuditors('set', nodeid, propvalues) # Take a copy of the node dict so that the subsequent set # operation doesn't modify the oldvalues structure. try: @@ -141,18 +138,15 @@ class Class(hyperdb.Class): # with no intervening commit() oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid)) hyperdb.Class.set(self, nodeid, **propvalues) - for react in self.reactors['set']: - react(self.db, self, nodeid, oldvalues) + self.fireReactors('set', nodeid, oldvalues) def retire(self, nodeid): """These operations trigger detectors and can be vetoed. Attempts to modify the "creation" or "activity" properties cause a KeyError. """ - for audit in self.auditors['retire']: - audit(self.db, self, nodeid, None) + self.fireAuditors('retire', nodeid, None) hyperdb.Class.retire(self, nodeid) - for react in self.reactors['retire']: - react(self.db, self, nodeid, None) + self.fireReactors('retire', nodeid, None) def get(self, nodeid, propname, default=_marker, cache=1): """Attempts to get the "creation" or "activity" properties should @@ -208,6 +202,12 @@ class Class(hyperdb.Class): if detector not in l: self.auditors[event].append(detector) + def fireAuditors(self, action, nodeid, newvalues): + """Fire all registered auditors. + """ + for audit in self.auditors[action]: + audit(self.db, self, nodeid, newvalues) + def react(self, event, detector): """Register a detector """ @@ -215,6 +215,11 @@ class Class(hyperdb.Class): if detector not in l: self.reactors[event].append(detector) + def fireReactors(self, action, nodeid, oldvalues): + """Fire all registered reactors. + """ + for react in self.reactors[action]: + react(self.db, self, nodeid, oldvalues) class FileClass(Class): def create(self, **propvalues): @@ -297,7 +302,7 @@ class IssueClass(Class): appended to the "messages" field of the specified issue. """ - def nosymessage(self, nodeid, msgid, change_note): + def nosymessage(self, nodeid, msgid, oldvalues): """Send a message to the members of an issue's nosy list. The message is sent only to users on the nosy list who are not @@ -318,9 +323,6 @@ class IssueClass(Class): # figure the author's id, and indicate they've received the message authid = messages.get(msgid, 'author') - # get the current nosy list, we'll need it - nosy = self.get(nodeid, 'nosy') - # possibly send the message to the author, as long as they aren't # anonymous if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and @@ -329,6 +331,7 @@ class IssueClass(Class): r[authid] = 1 # now figure the nosy people who weren't recipients + nosy = self.get(nodeid, 'nosy') for nosyid in nosy: # Don't send nosy mail to the anonymous user (that user # shouldn't appear in the nosy list, but just in case they @@ -341,6 +344,12 @@ class IssueClass(Class): sendto.append(nosyid) recipients.append(nosyid) + # generate a change note + if oldvalues: + note = self.generateChangeNote(nodeid, oldvalues) + else: + note = self.generateCreateNote(nodeid) + # we have new recipients if sendto: # map userids to addresses @@ -350,7 +359,7 @@ class IssueClass(Class): messages.set(msgid, recipients=recipients) # send the message - self.send_message(nodeid, msgid, change_note, sendto) + self.send_message(nodeid, msgid, note, sendto) # XXX backwards compatibility - don't remove sendmessage = nosymessage @@ -625,6 +634,9 @@ class IssueClass(Class): # # $Log: not supported by cvs2svn $ +# Revision 1.53 2002/05/25 07:16:24 rochecompaan +# Merged search_indexing-branch with HEAD +# # Revision 1.52 2002/05/15 03:27:16 richard # . fixed SCRIPT_NAME in ZRoundup for instances not at top level of Zope # (thanks dman) diff --git a/roundup/templates/classic/detectors/nosyreaction.py b/roundup/templates/classic/detectors/nosyreaction.py index c5c818e..4490b49 100644 --- a/roundup/templates/classic/detectors/nosyreaction.py +++ b/roundup/templates/classic/detectors/nosyreaction.py @@ -15,9 +15,9 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: nosyreaction.py,v 1.11 2002-01-14 22:21:38 richard Exp $ +#$Id: nosyreaction.py,v 1.12 2002-05-29 01:16:17 richard Exp $ -from roundup import roundupdb +from roundup import roundupdb, hyperdb def nosyreaction(db, cl, nodeid, oldvalues): ''' A standard detector is provided that watches for additions to the @@ -34,12 +34,21 @@ def nosyreaction(db, cl, nodeid, oldvalues): The journal recorded by the hyperdatabase on the "recipients" property then provides a log of when the message was sent to whom. ''' + # send a copy of all new messages to the nosy list + for msgid in determineNewMessages(cl, nodeid, oldvalues): + try: + cl.nosymessage(nodeid, msgid, oldvalues) + except roundupdb.MessageSendError, message: + raise roundupdb.DetectorError, message + +def determineNewMessages(cl, nodeid, oldvalues): + ''' Figure a list of the messages that are being added to the given + node in this transaction. + ''' messages = [] - change_note = '' if oldvalues is None: # the action was a create, so use all the messages in the create messages = cl.get(nodeid, 'messages') - change_note = cl.generateCreateNote(nodeid) elif oldvalues.has_key('messages'): # the action was a set (so adding new messages to an existing issue) m = {} @@ -50,24 +59,91 @@ def nosyreaction(db, cl, nodeid, oldvalues): for msgid in cl.get(nodeid, 'messages'): if not m.has_key(msgid): messages.append(msgid) - if messages: - change_note = cl.generateChangeNote(nodeid, oldvalues) - if not messages: - return + return messages - # send a copy to the nosy list - for msgid in messages: - try: - cl.sendmessage(nodeid, msgid, change_note) - except roundupdb.MessageSendError, message: - raise roundupdb.DetectorError, message +def updatenosy(db, cl, nodeid, newvalues): + '''Update the nosy list for changes to the assignedto + ''' + # nodeid will be None if this is a new node + current = {} + if nodeid is None: + ok = ('new', 'yes') + else: + ok = ('yes',) + # old node, get the current values from the node if they haven't + # changed + if not newvalues.has_key('nosy'): + nosy = cl.get(nodeid, 'nosy') + for value in nosy: + if not current.has_key(value): + current[value] = 1 + + # if the nosy list changed in this transaction, init from the new value + if newvalues.has_key('nosy'): + nosy = newvalues.get('nosy', []) + for value in nosy: + if not db.hasnode('user', value): + continue + if not current.has_key(value): + current[value] = 1 + + # add assignedto(s) to the nosy list + if newvalues.has_key('assignedto'): + propdef = cl.getprops() + if isinstance(propdef['assignedto'], hyperdb.Link): + assignedto_ids = [newvalues['assignedto']] + elif isinstance(propdef['assignedto'], hyperdb.Multilink): + assignedto_ids = newvalues['assignedto'] + for assignedto_id in assignedto_ids: + if not current.has_key(assignedto_id): + current[assignedto_id] = 1 + + # see if there's any new messages - if so, possibly add the author and + # recipient to the nosy + if newvalues.has_key('messages'): + if nodeid is None: + ok = ('new', 'yes') + messages = newvalues['messages'] + else: + ok = ('yes',) + # figure which of the messages now on the issue weren't + # there before - make sure we don't get a cached version! + oldmessages = cl.get(nodeid, 'messages', cache=0) + messages = [] + for msgid in newvalues['messages']: + if msgid not in oldmessages: + messages.append(msgid) + + # configs for nosy modifications + add_author = getattr(db.config, 'ADD_AUTHOR_TO_NOSY', 'new') + add_recips = getattr(db.config, 'ADD_RECIPIENTS_TO_NOSY', 'new') + + # now for each new message: + msg = db.msg + for msgid in messages: + if add_author in ok: + authid = msg.get(msgid, 'author') + current[authid] = 1 + + # add on the recipients of the message + if add_recips in ok: + for recipient in msg.get(msgid, 'recipients'): + current[recipient] = 1 + + # that's it, save off the new nosy list + newvalues['nosy'] = current.keys() def init(db): db.issue.react('create', nosyreaction) db.issue.react('set', nosyreaction) + db.issue.audit('create', updatenosy) + db.issue.audit('set', updatenosy) # #$Log: not supported by cvs2svn $ +#Revision 1.11 2002/01/14 22:21:38 richard +##503353 ] setting properties in initial email +# #Revision 1.10 2002/01/11 23:22:29 richard # . #502437 ] rogue reactor and unittest # in short, the nosy reactor was modifying the nosy list. That code had diff --git a/roundup/templates/classic/detectors/statusauditor.py b/roundup/templates/classic/detectors/statusauditor.py new file mode 100644 index 0000000..b935706 --- /dev/null +++ b/roundup/templates/classic/detectors/statusauditor.py @@ -0,0 +1,68 @@ +# Copyright (c) 2002 ekit.com Inc (http://www.ekit-inc.com/) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +#$Id: statusauditor.py,v 1.1 2002-05-29 01:16:17 richard Exp $ + +def chatty(db, cl, nodeid, newvalues): + ''' If the issue is currently 'unread' or 'resolved', then set + it to 'chatting' + ''' + # don't fire if there's no new message (ie. chat) + if not newvalues.has_key('messages'): + return + if newvalues['messages'] == cl.get(nodeid, 'messages', cache=0): + return + + # determine the id of 'unread', 'resolved' and 'chatting' + unread_id = db.status.lookup('unread') + resolved_id = db.status.lookup('resolved') + chatting_id = db.status.lookup('chatting') + + # get the current value + current_status = cl.get(nodeid, 'status') + + # see if there's an explicit change in this transaction + if newvalues.has_key('status') and newvalues['status'] != current_status: + # yep, skip + return + + # ok, there's no explicit change, so do it manually + if current_status in (unread_id, resolved_id): + newvalues['status'] = chatting_id + + +def presetunread(db, cl, nodeid, newvalues): + ''' Make sure the status is set on new issues + ''' + if newvalues.has_key('status'): + return + + # ok, do it + newvalues['status'] = db.status.lookup('unread') + + +def init(db): + # fire before changes are made + db.issue.audit('set', chatty) + db.issue.audit('create', presetunread) + +# +#$Log: not supported by cvs2svn $ +# diff --git a/roundup/templates/extended/detectors/__init__.py b/roundup/templates/extended/detectors/__init__.py index 2a705cf..78f532f 100644 --- a/roundup/templates/extended/detectors/__init__.py +++ b/roundup/templates/extended/detectors/__init__.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: __init__.py,v 1.3 2001-08-07 00:24:43 richard Exp $ +#$Id: __init__.py,v 1.4 2002-05-29 01:16:17 richard Exp $ def init(db): ''' execute the init functions of all the modules in this directory @@ -35,10 +35,16 @@ def init(db): # #$Log: not supported by cvs2svn $ +#Revision 1.3 2001/08/07 00:24:43 richard +#stupid typo +# #Revision 1.2 2001/08/07 00:15:51 richard #Added the copyright/license notice to (nearly) all files at request of #Bizar Software. # +#Revision 1.1 2001/07/23 23:29:10 richard +#Adding the classic template +# #Revision 1.1 2001/07/23 03:50:47 anthonybaxter #moved templates to proper location # diff --git a/roundup/templates/extended/detectors/nosyreaction.py b/roundup/templates/extended/detectors/nosyreaction.py index b303043..4490b49 100644 --- a/roundup/templates/extended/detectors/nosyreaction.py +++ b/roundup/templates/extended/detectors/nosyreaction.py @@ -15,9 +15,9 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: nosyreaction.py,v 1.11 2002-01-14 22:21:38 richard Exp $ +#$Id: nosyreaction.py,v 1.12 2002-05-29 01:16:17 richard Exp $ -from roundup import roundupdb +from roundup import roundupdb, hyperdb def nosyreaction(db, cl, nodeid, oldvalues): ''' A standard detector is provided that watches for additions to the @@ -34,12 +34,21 @@ def nosyreaction(db, cl, nodeid, oldvalues): The journal recorded by the hyperdatabase on the "recipients" property then provides a log of when the message was sent to whom. ''' + # send a copy of all new messages to the nosy list + for msgid in determineNewMessages(cl, nodeid, oldvalues): + try: + cl.nosymessage(nodeid, msgid, oldvalues) + except roundupdb.MessageSendError, message: + raise roundupdb.DetectorError, message + +def determineNewMessages(cl, nodeid, oldvalues): + ''' Figure a list of the messages that are being added to the given + node in this transaction. + ''' messages = [] - change_note = '' if oldvalues is None: # the action was a create, so use all the messages in the create messages = cl.get(nodeid, 'messages') - change_note = cl.generateCreateNote(nodeid) elif oldvalues.has_key('messages'): # the action was a set (so adding new messages to an existing issue) m = {} @@ -50,25 +59,91 @@ def nosyreaction(db, cl, nodeid, oldvalues): for msgid in cl.get(nodeid, 'messages'): if not m.has_key(msgid): messages.append(msgid) - if messages: - change_note = cl.generateChangeNote(nodeid, oldvalues) - if not messages: - return + return messages - # send a copy to the nosy list - for msgid in messages: - try: - cl.sendmessage(nodeid, msgid, change_note) - except roundupdb.MessageSendError, message: - raise roundupdb.DetectorError, message +def updatenosy(db, cl, nodeid, newvalues): + '''Update the nosy list for changes to the assignedto + ''' + # nodeid will be None if this is a new node + current = {} + if nodeid is None: + ok = ('new', 'yes') + else: + ok = ('yes',) + # old node, get the current values from the node if they haven't + # changed + if not newvalues.has_key('nosy'): + nosy = cl.get(nodeid, 'nosy') + for value in nosy: + if not current.has_key(value): + current[value] = 1 + # if the nosy list changed in this transaction, init from the new value + if newvalues.has_key('nosy'): + nosy = newvalues.get('nosy', []) + for value in nosy: + if not db.hasnode('user', value): + continue + if not current.has_key(value): + current[value] = 1 + + # add assignedto(s) to the nosy list + if newvalues.has_key('assignedto'): + propdef = cl.getprops() + if isinstance(propdef['assignedto'], hyperdb.Link): + assignedto_ids = [newvalues['assignedto']] + elif isinstance(propdef['assignedto'], hyperdb.Multilink): + assignedto_ids = newvalues['assignedto'] + for assignedto_id in assignedto_ids: + if not current.has_key(assignedto_id): + current[assignedto_id] = 1 + + # see if there's any new messages - if so, possibly add the author and + # recipient to the nosy + if newvalues.has_key('messages'): + if nodeid is None: + ok = ('new', 'yes') + messages = newvalues['messages'] + else: + ok = ('yes',) + # figure which of the messages now on the issue weren't + # there before - make sure we don't get a cached version! + oldmessages = cl.get(nodeid, 'messages', cache=0) + messages = [] + for msgid in newvalues['messages']: + if msgid not in oldmessages: + messages.append(msgid) + + # configs for nosy modifications + add_author = getattr(db.config, 'ADD_AUTHOR_TO_NOSY', 'new') + add_recips = getattr(db.config, 'ADD_RECIPIENTS_TO_NOSY', 'new') + + # now for each new message: + msg = db.msg + for msgid in messages: + if add_author in ok: + authid = msg.get(msgid, 'author') + current[authid] = 1 + + # add on the recipients of the message + if add_recips in ok: + for recipient in msg.get(msgid, 'recipients'): + current[recipient] = 1 + + # that's it, save off the new nosy list + newvalues['nosy'] = current.keys() def init(db): db.issue.react('create', nosyreaction) db.issue.react('set', nosyreaction) + db.issue.audit('create', updatenosy) + db.issue.audit('set', updatenosy) # #$Log: not supported by cvs2svn $ +#Revision 1.11 2002/01/14 22:21:38 richard +##503353 ] setting properties in initial email +# #Revision 1.10 2002/01/11 23:22:29 richard # . #502437 ] rogue reactor and unittest # in short, the nosy reactor was modifying the nosy list. That code had @@ -119,6 +194,9 @@ def init(db): #Added the copyright/license notice to (nearly) all files at request of #Bizar Software. # +#Revision 1.1 2001/07/23 23:29:10 richard +#Adding the classic template +# #Revision 1.1 2001/07/23 03:50:47 anthonybaxter #moved templates to proper location # diff --git a/roundup/templates/extended/detectors/statusauditor.py b/roundup/templates/extended/detectors/statusauditor.py new file mode 100644 index 0000000..b935706 --- /dev/null +++ b/roundup/templates/extended/detectors/statusauditor.py @@ -0,0 +1,68 @@ +# Copyright (c) 2002 ekit.com Inc (http://www.ekit-inc.com/) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +#$Id: statusauditor.py,v 1.1 2002-05-29 01:16:17 richard Exp $ + +def chatty(db, cl, nodeid, newvalues): + ''' If the issue is currently 'unread' or 'resolved', then set + it to 'chatting' + ''' + # don't fire if there's no new message (ie. chat) + if not newvalues.has_key('messages'): + return + if newvalues['messages'] == cl.get(nodeid, 'messages', cache=0): + return + + # determine the id of 'unread', 'resolved' and 'chatting' + unread_id = db.status.lookup('unread') + resolved_id = db.status.lookup('resolved') + chatting_id = db.status.lookup('chatting') + + # get the current value + current_status = cl.get(nodeid, 'status') + + # see if there's an explicit change in this transaction + if newvalues.has_key('status') and newvalues['status'] != current_status: + # yep, skip + return + + # ok, there's no explicit change, so do it manually + if current_status in (unread_id, resolved_id): + newvalues['status'] = chatting_id + + +def presetunread(db, cl, nodeid, newvalues): + ''' Make sure the status is set on new issues + ''' + if newvalues.has_key('status'): + return + + # ok, do it + newvalues['status'] = db.status.lookup('unread') + + +def init(db): + # fire before changes are made + db.issue.audit('set', chatty) + db.issue.audit('create', presetunread) + +# +#$Log: not supported by cvs2svn $ +# diff --git a/run_tests b/run_tests index b9976d9..ee73ce7 100755 --- a/run_tests +++ b/run_tests @@ -9,7 +9,14 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # -# $Id: run_tests,v 1.6 2002-05-25 07:24:29 rochecompaan Exp $ +# $Id: run_tests,v 1.7 2002-05-29 01:16:16 richard Exp $ + +# make sure we have the htmlbase +try: + from roundup.templates.classic import htmlbase +except ImportError: + import setup + setup.buildTemplates() from test import go import sys @@ -20,6 +27,9 @@ else: # # $Log: not supported by cvs2svn $ +# Revision 1.6 2002/05/25 07:24:29 rochecompaan +# oops +# # Revision 1.4 2002/02/14 23:38:12 richard # Fixed the unit tests for the mailgw re: the x-roundup-name header. # Also made the test runner more user-friendly: diff --git a/setup.py b/setup.py index ca2832c..7c64ffe 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: setup.py,v 1.33 2002-04-03 05:53:03 richard Exp $ +# $Id: setup.py,v 1.34 2002-05-29 01:16:16 richard Exp $ from distutils.core import setup, Extension from distutils.util import get_platform @@ -112,8 +112,6 @@ def scriptname(path): script = script + ".bat" return script -# build list of scripts from their implementation modules -roundup_scripts = map(scriptname, glob('roundup/scripts/[!_]*.py')) ############################################################################# @@ -124,55 +122,75 @@ def isTemplateDir(dir): return dir[0] != '.' and dir != 'CVS' and os.path.isdir(dir) \ and os.path.isfile(os.path.join(dir, '__init__.py')) +# use that function to list all the templates templates = map(os.path.basename, filter(isTemplateDir, glob(os.path.join('roundup', 'templates', '*')))) -packagelist = [ - 'roundup', - 'roundup.backends', - 'roundup.scripts', - 'roundup.templates' -] -installdatafiles = [ - ('share/roundup/cgi-bin', ['cgi-bin/roundup.cgi']), -] - -for template in templates: - tdir = os.path.join('roundup', 'templates', template) - makeHtmlBase(tdir) - - # add the template package and subpackage - packagelist.append('roundup.templates.%s' % template) - packagelist.append('roundup.templates.%s.detectors' % template) - - # scan for data files - tfiles = glob(os.path.join(tdir, 'html', '*')) - tfiles = filter(os.path.isfile, tfiles) - installdatafiles.append( - ('share/roundup/templates/%s/html' % template, tfiles) - ) - - -setup( - name = "roundup", - version = "0.4.1", - description = "Roundup issue tracking system.", - author = "Richard Jones", - author_email = "richard@users.sourceforge.net", - url = 'http://sourceforge.net/projects/roundup/', - packages = packagelist, - # Override certain command classes with our own ones - cmdclass = { - 'build_scripts': build_scripts_roundup, - }, - scripts = roundup_scripts, - - data_files = installdatafiles -) +def buildTemplates(): + for template in templates: + tdir = os.path.join('roundup', 'templates', template) + makeHtmlBase(tdir) + +if __name__ == '__main__': + # build list of scripts from their implementation modules + roundup_scripts = map(scriptname, glob('roundup/scripts/[!_]*.py')) + + # template munching + templates = map(os.path.basename, filter(isTemplateDir, + glob(os.path.join('roundup', 'templates', '*')))) + packagelist = [ + 'roundup', + 'roundup.backends', + 'roundup.scripts', + 'roundup.templates' + ] + installdatafiles = [ + ('share/roundup/cgi-bin', ['cgi-bin/roundup.cgi']), + ] + + # munge the template HTML into the htmlbase module + buildTemplates() + + # add the templates to the setup packages and data files lists + for template in templates: + tdir = os.path.join('roundup', 'templates', template) + + # add the template package and subpackage + packagelist.append('roundup.templates.%s' % template) + packagelist.append('roundup.templates.%s.detectors' % template) + + # scan for data files + tfiles = glob(os.path.join(tdir, 'html', '*')) + tfiles = filter(os.path.isfile, tfiles) + installdatafiles.append( + ('share/roundup/templates/%s/html' % template, tfiles) + ) + + # perform the setup action + setup( + name = "roundup", + version = "0.4.1", + description = "Roundup issue tracking system.", + author = "Richard Jones", + author_email = "richard@users.sourceforge.net", + url = 'http://sourceforge.net/projects/roundup/', + packages = packagelist, + + # Override certain command classes with our own ones + cmdclass = { + 'build_scripts': build_scripts_roundup, + }, + scripts = roundup_scripts, + + data_files = installdatafiles + ) # # $Log: not supported by cvs2svn $ +# Revision 1.33 2002/04/03 05:53:03 richard +# Didn't get around to committing these after the last release. +# # Revision 1.32 2002/03/27 23:47:58 jhermann # Fix for scripts running under CMD.EXE # diff --git a/test/__init__.py b/test/__init__.py index 749ba67..92d9f6f 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -15,10 +15,11 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: __init__.py,v 1.16 2002-02-14 23:38:12 richard Exp $ +# $Id: __init__.py,v 1.17 2002-05-29 01:16:17 richard Exp $ import os, tempfile, unittest, shutil -os.environ['SENDMAILDEBUG'] = tempfile.mktemp() +import roundup.roundupdb +roundup.roundupdb.SENDMAILDEBUG=os.environ['SENDMAILDEBUG']=tempfile.mktemp() # figure all the modules available dir = os.path.split(__file__)[0] @@ -39,6 +40,13 @@ def go(tests=all_tests): # # $Log: not supported by cvs2svn $ +# Revision 1.16 2002/02/14 23:38:12 richard +# Fixed the unit tests for the mailgw re: the x-roundup-name header. +# Also made the test runner more user-friendly: +# ./run_tests - detect all tests in test/test_.py and run them +# ./run_tests - run only test/test_.py +# eg ./run_tests mailgw - run the mailgw test from test/test_mailgw.py +# # Revision 1.15 2002/01/22 00:12:20 richard # oops # diff --git a/test/test_mailgw.py b/test/test_mailgw.py index 5431488..93a1f8f 100644 --- a/test/test_mailgw.py +++ b/test/test_mailgw.py @@ -8,7 +8,7 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # -# $Id: test_mailgw.py,v 1.19 2002-05-23 04:26:05 richard Exp $ +# $Id: test_mailgw.py,v 1.20 2002-05-29 01:16:17 richard Exp $ import unittest, cStringIO, tempfile, os, shutil, errno, imp, sys, difflib @@ -86,7 +86,7 @@ class MailgwTestCase(unittest.TestCase, DiffHelper): except OSError, error: if error.errno not in (errno.ENOENT, errno.ESRCH): raise - def testNewIssue(self): + def doNewIssue(self): message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" From: Chef +From: mary To: issue_tracker@your.tracker.email.domain.example Message-Id: In-Reply-To: -Subject: [issue1] Testing... [assignedto=mary; nosy=john] +Subject: [issue1] Testing... -This is a followup +This is a second followup ''') handler = self.instance.MailGW(self.instance, self.db) handler.main(message) - self.compareStrings(open(os.environ['SENDMAILDEBUG']).read(), '''FROM: roundup-admin@your.tracker.email.domain.example -TO: chef@bork.bork.bork, john@test, mary@test +TO: chef@bork.bork.bork, richard@test Content-Type: text/plain Subject: [issue1] Testing... -To: chef@bork.bork.bork, john@test, mary@test -From: richard +To: chef@bork.bork.bork, richard@test +From: mary Reply-To: Roundup issue tracker MIME-Version: 1.0 Message-Id: @@ -248,14 +250,12 @@ X-Roundup-Name: Roundup issue tracker Content-Transfer-Encoding: quoted-printable -richard added the comment: +mary added the comment: -This is a followup +This is a second followup ---------- -assignedto: -> mary -nosy: +mary, john status: unread -> chatting _________________________________________________________________________ "Roundup issue tracker" @@ -263,27 +263,32 @@ http://your.tracker.url.example/issue1 _________________________________________________________________________ ''') - def testFollowup2(self): - self.testNewIssue() + def testFollowup(self): + self.doNewIssue() + message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" -From: mary +From: richard To: issue_tracker@your.tracker.email.domain.example Message-Id: In-Reply-To: -Subject: [issue1] Testing... +Subject: [issue1] Testing... [assignedto=mary; nosy=john] -This is a second followup +This is a followup ''') handler = self.instance.MailGW(self.instance, self.db) handler.main(message) + l = self.db.issue.get('1', 'nosy') + l.sort() + self.assertEqual(l, ['2', '3', '4', '5']) + self.compareStrings(open(os.environ['SENDMAILDEBUG']).read(), '''FROM: roundup-admin@your.tracker.email.domain.example -TO: chef@bork.bork.bork, richard@test +TO: chef@bork.bork.bork, john@test, mary@test Content-Type: text/plain Subject: [issue1] Testing... -To: chef@bork.bork.bork, richard@test -From: mary +To: chef@bork.bork.bork, john@test, mary@test +From: richard Reply-To: Roundup issue tracker MIME-Version: 1.0 Message-Id: @@ -292,12 +297,14 @@ X-Roundup-Name: Roundup issue tracker Content-Transfer-Encoding: quoted-printable -mary added the comment: +richard added the comment: -This is a second followup +This is a followup ---------- +assignedto: -> mary +nosy: +mary, john status: unread -> chatting _________________________________________________________________________ "Roundup issue tracker" @@ -306,7 +313,7 @@ _________________________________________________________________________ ''') def testFollowupTitleMatch(self): - self.testNewIssue() + self.doNewIssue() message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" From: richard @@ -351,8 +358,8 @@ _________________________________________________________________________ ''') def testFollowupNosyAuthor(self): - self.testNewIssue() - self.instance.ADD_AUTHOR_TO_NOSY = 'yes' + self.doNewIssue() + self.db.config.ADD_AUTHOR_TO_NOSY = self.instance.ADD_AUTHOR_TO_NOSY = 'yes' message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" From: john@test @@ -397,8 +404,8 @@ _________________________________________________________________________ ''') def testFollowupNosyRecipients(self): - self.testNewIssue() - self.instance.ADD_RECIPIENTS_TO_NOSY = 'yes' + self.doNewIssue() + self.db.config.ADD_RECIPIENTS_TO_NOSY = self.instance.ADD_RECIPIENTS_TO_NOSY = 'yes' message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" From: richard@test @@ -444,8 +451,8 @@ _________________________________________________________________________ ''') def testFollowupNosyAuthorAndCopy(self): - self.testNewIssue() - self.instance.ADD_AUTHOR_TO_NOSY = 'yes' + self.doNewIssue() + self.db.config.ADD_AUTHOR_TO_NOSY = self.instance.ADD_AUTHOR_TO_NOSY = 'yes' self.db.config.MESSAGES_TO_AUTHOR = 'yes' message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" @@ -491,7 +498,7 @@ _________________________________________________________________________ ''') def testFollowupNoNosyAuthor(self): - self.testNewIssue() + self.doNewIssue() self.instance.ADD_AUTHOR_TO_NOSY = 'no' message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" @@ -536,7 +543,7 @@ _________________________________________________________________________ ''') def testFollowupNoNosyRecipients(self): - self.testNewIssue() + self.doNewIssue() self.instance.ADD_RECIPIENTS_TO_NOSY = 'no' message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" @@ -581,8 +588,29 @@ _________________________________________________________________________ ''') + def testNosyRemove(self): + self.doNewIssue() + + message = cStringIO.StringIO('''Content-Type: text/plain; + charset="iso-8859-1" +From: richard +To: issue_tracker@your.tracker.email.domain.example +Message-Id: +In-Reply-To: +Subject: [issue1] Testing... [nosy=-richard] + +''') + handler = self.instance.MailGW(self.instance, self.db) + handler.main(message) + l = self.db.issue.get('1', 'nosy') + l.sort() + self.assertEqual(l, ['2']) + + # NO NOSY MESSAGE SHOULD BE SENT! + self.assert_(not os.path.exists(os.environ['SENDMAILDEBUG'])) + def testEnc01(self): - self.testNewIssue() + self.doNewIssue() message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" From: mary @@ -628,7 +656,7 @@ _________________________________________________________________________ def testMultipartEnc01(self): - self.testNewIssue() + self.doNewIssue() message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" From: mary @@ -691,6 +719,9 @@ def suite(): # # $Log: not supported by cvs2svn $ +# Revision 1.19 2002/05/23 04:26:05 richard +# 'I must run unit tests before committing\n' * 100 +# # Revision 1.18 2002/05/15 03:27:16 richard # . fixed SCRIPT_NAME in ZRoundup for instances not at top level of Zope # (thanks dman) -- 2.30.2