Roundup Tracker

The Roundup mail gateway automatically transforms incoming email messages into issues to be tracked. This is A Good Thing (TM). However, as the famous Johan Cruyff said: every upside has its downside. More specifically, you'll find that some people are reply-button-challenged. They will not use reply. They will compose new emails without In-Reply-To references, without issue id in the subject. They'll happily invent new subject lines for existing issues.

As a consequence, Roundup will be unable to recognize such mail for what it is, a followup to an existing issue. It will open a new issue instead. So now you've got two issue threads in Roundup for what is a single issue in real life.

The solution: a new Merge action that takes a 'source' issue, copies all messages and files from it to a 'target' issue, then retires the 'source' issue. So you'll have everything relevant in the consolidated 'target' issue. Meanwhile, the 'source' issue is retired and will not accept any followup changes. Neat.

Credits to Marlon van den Berg for prototyping this.

Change schema.py:

Here we go. First, create a new property 'merged' on 'IssueClass'.

 issue = IssueClass(db, "issue",
     assignedto=Link("user"), topic=Multilink("keyword"),
     priority=Link("priority"), status=Link("status"),
     merged=Link("issue"))

We'll use this 'merged' property to store a reference to the 'target' issue on retiring the 'source' issue.

Create a merge action class:

In 'interfaces.py':

 from roundup.cgi.exceptions import Redirect
 from roundup.cgi import actions
 
 class MergeAction(actions.Action):
     def handle(self):
         source_issue = self.nodeid
 
         # find out if the form has a target issue edit field
         if self.form.has_key('target_issue'):
             # yes it does. Get the value.
             target_issue = self.form['target_issue'].value.strip()
         else:
             # nope
             target_issue = None
         if not target_issue or target_issue == '':
             self.client.error_message.append('Unknown target issue')
             return
         elif target_issue == source_issue:        
             self.client.error_message.append('Cannot merge issue %s into myself (%s)' % (target_issue, source_issue))
             return
 
         # get the message lists of the two issues
         source_messages = self.db.issue.get(source_issue, 'messages')
         target_messages = self.db.issue.get(target_issue, 'messages')
         # merge them
         for msg in source_messages:
             target_messages.append(msg)
         # update the target issue message list
         self.db.issue.set(target_issue, messages=target_messages)
 
         # get the file lists of the two issues
         source_files = self.db.issue.get(source_issue, 'files')
         target_files = self.db.issue.get(target_issue, 'files')
         # merge them
         for file in source_files:
             target_files.append(file)
         # update the target issue file list
         self.db.issue.set(target_issue, files=target_files)
 
 # uncomment this if you're using timelogs
 #        # get the timelog lists of the two issues
 #        source_timelog = self.db.issue.get(source_issue, 'timelog')
 #        target_timelog = self.db.issue.get(target_issue, 'timelog')
 #        # merge them
 #        for time in source_timelog:
 #            target_timelog.append(time)
 #        # update the target issue time list
 #        self.db.issue.set(target_issue, timelog=target_timelog)
 
         # store the 'merged-into' issue id
         self.db.issue.set(source_issue, merged=target_issue)
         # retire the source issue
         self.db.issue.retire(source_issue)
         # commit all database changes
         self.db.commit()
         # confirm the merge and redirect to the target issue      
         raise Redirect, """%sissue%s?@ok_message=Merging of issue %s into issue
         %s successful""" % (self.base, target_issue, source_issue, target_issue)

Put a merge form in your issue template:

Finally, you'll need a way to plug all this logic into your webinterface. In your template file $TRACKER_HOME/html/issue.item.html you'll add a new form _below_ the main editing form. Make sure you locate the correct '/form' tag.

After that tag (_outside_ the main form) add a new form::

 <form method=POST tal:condition="python: context.merged == None">
  <input type="hidden" name="@action" value="merge">
  <table class="form">
   <tr>
    <th>Merge into</th>
    <td>
      <tal:block tal:content="structure context/issue/menu" />
    </td>
    <td>
     <input type="submit" value="  Merge  ">
    </td>
   </tr>
  </table>
 </form>

You can of course replace the very boring issue selector you get by::

      <tal:block metal:use-macro="templates/lib/macros/issue_menu" />

Which does suppose you have defined a custom macro in $TRACKER_HOME/html/lib.html for generating the issue list. YMMV.

This should work. Test.

To be continued...

we'll not only be able to 'merge into target issue', but also to 'supersede from older issue' :-)

Superseding differs from merging, in that superseding handles related, but still different issues (brothers and sisters) whereas merging handles identical split personality issues (clones).

Stay tuned.

From wiki Sat Feb 3 20:42:27 +1100 2007 From: wiki Date: Sat, 03 Feb 2007 20:42:27 +1100 Subject: Make it work with roundup 1.x Message-ID: <20070203204227+1100@www.mechanicalcat.net>

With some small changes, this works with Roundup 1.x, too:

- the "issue" class is changed in schema.py rather than dbinit.py

- the "MergeAction" class is put in an existing or separate Python file in the "extensions" subdirectory

Restart the Roundup server to recognise the schema change and the new extension.

- to put the form for the merge action in the "issue.item.html" template, I did as follows:

- added a slot to the "searchbox" div in page.html
  •      <div id="searchbox">
           <form method="GET" action="issue" metal:define-slot="search-form">
             <!-- ... -->
           </form>
           <metal:slot define-slot="more-forms"/>
         </div>
- filled this new slot in "issue.item.html"
  •      <tal:if condition="context/is_edit_ok">
           <form metal:fill-slot="more-forms" tal:condition="not:context/merged"
                 class="additional"
                 method="POST"
                 name="mergeForm">
             <input type="hidden" name="@action" value="merge">
             <label for="merge_into" i18n:translate="">Merge into</label>
             <input type="text" id="merge_into" name="merge_into" size="5">
             <span tal:replace="structure python:db.issue.classhelp('id,title', property='merge_into', inputtype='radio', form='mergeForm')" />
             <input type="submit" value="  Merge  " i18n:attributes="value">
           </form>
         </tal:if>

Of course, this can be improved further; there is no GUI method so far to undo a merge. You can restore and unmerge issueXXXX using the "roundup-admin" script, using the following commands::

 restore issueXXXX
 set issueXXXX merged=
 commit

The file attachments and messages will stay double-linked, but this might be ok and can be changed TTW.


CategorySchema