Roundup Wiki

In our office I wanted a means to edit a number of issues at one time, but only have to edit the actual changes once. The Project Manager Batch Editing was a nice start, but I was finding myself typing the same thing repeatedly, a less-than-ideal situation. After a number of days of work, I came up with a solution that remained true to the usage of Roundup while adding new functionality.

My solution is a two-step process. The first page is divided into two sections. The first section is where the information to be added/changed is to be entered. This section looks virtually identical to the 'item' template, with only the 'title' entry field missing. The second half of the page looks like the 'search' template, with only the information regarding a pagination missing.

To implement this search/edit feature, you will need to create three new files and edit one existing one. The first file you will wish to create is 'issue.batch_search.html', placed in your 'html' directory, and input the following code::

<tal:block metal:use-macro="templates/page/macros/icing">
  <title metal:fill-slot="head_title">Issue Batch Editing - <span tal:replace="config/TRACKER_NAME" /></title>
  <span metal:fill-slot="body_title" tal:omit-tag="true">Issue Batch Editing</span>
  <td class="content" metal:fill-slot="content">
   <form method="GET" name="itemSynopsis" tal:attributes="action request/classname">
    <table class="form">
     <tr><th colspan="4" class="header">Edit Fields</th></tr>
     <tr>
      <th>Priority</th>
      <td>
       <select name="batch_priority">
        <option value="-1">- no selection -</option>
        <tal:block tal:repeat="item db/priority/list">
         <option tal:attributes="value item/id" tal:content="item/name"></option>
        </tal:block>
       </select>
      </td>
      <th>Status</th>
      <td>
       <select name="batch_status">
        <option value="-1">- no selection -</option>
        <tal:block tal:repeat="item db/status/list">
         <option tal:attributes="value item/id" tal:content="item/name"></option>
        </tal:block>
       </select>
      </td>
     </tr>
     <tr>
      <th>Superseder</th>
      <td>
       <input type="text" name="batch_superseder" size="30" />
       <span tal:replace="structure python:db.issue.classhelp('name', property='batch_superseder')" />
      </td>
      <th>Nosy List</th>
      <td>
       <input type="text" name="batch_nosy" size="30" />
       <span tal:replace="structure python:db.user.classhelp('name', property='batch_nosy')" />
      </td>
     </tr>
     <tr>
      <th>Assigned To</th>
      <td>
       <select name="batch_assignedto">
        <option value="-1">- no selection -</option>
        <tal:block tal:repeat="item db/user/list">
         <option tal:attributes="value item/id" tal:content="item/username"></option>
        </tal:block>
       </select>
      </td>
      <th>Topics</th>
      <td>
       <input type="text" name="batch_topics" size="30" />
       <span tal:replace="structure python:db.keyword.classhelp('id,title', property='batch_topics')" />
      </td>
     </tr>
     <tr>
      <th>Change Note</th>
      <td colspan=3>
       <textarea name="batch_note" wrap="hard" rows="5" cols="80"></textarea>
      </td>
     </tr>
     <tr>
      <th>File</th>
      <td colspan=3><input type="file" name="batch_file" size="40"></td>
     </tr>
    </table>
    <br />
    <table class="form" tal:define="
     cols python:request.columns or 'id activity title status assignedto'.split();
     sort_on python:request.sort[1] or 'activity';
     group_on python:request.group[1] or 'status';
     search_input templates/page/macros/search_input;
     column_input templates/page/macros/column_input;
     sort_input templates/page/macros/sort_input;
     group_input templates/page/macros/group_input;
     search_select templates/page/macros/search_select;
     search_multiselect templates/page/macros/search_multiselect;">
     <tr>
      <th class="header">&nbsp;</th>
      <th class="header">Filter on</th>
      <th class="header">Display</th>
      <th class="header">Sort on</th>
      <th class="header">Group on</th>
     </tr>
     <tr tal:define="name string:@search_text">
      <th>All text*:</th>
      <td metal:use-macro="search_input"></td>
      <td>&nbsp;</td>
      <td>&nbsp;</td>
      <td>&nbsp;</td>
     </tr>
     <tr tal:define="name string:title">
      <th>Title:</th>
      <td metal:use-macro="search_input"></td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td>&nbsp;</td>
     </tr>
     <tr tal:define="name string:topic; db_klass string:keyword; db_content string:name;">
      <th>Topic:</th>
      <td metal:use-macro="search_select"></td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr tal:define="name string:id">
      <th>ID:</th>
      <td metal:use-macro="search_input"></td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td>&nbsp;</td>
     </tr>
     <tr tal:define="name string:creation">
      <th>Creation Date:</th>
      <td metal:use-macro="search_input"></td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr tal:define="name string:creator; db_klass string:user; db_content string:username;" 
      tal:condition="db/user/is_view_ok">
      <th>Creator:</th>
      <td metal:use-macro="search_select">
       <option metal:fill-slot="extra_options" tal:attributes="value request/user/id">created by me</option>
      </td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr tal:define="name string:activity">
      <th>Activity:</th>
      <td metal:use-macro="search_input"></td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td>&nbsp;</td>
     </tr>
     <tr tal:define="name string:actor; db_klass string:user; db_content string:username;" 
      tal:condition="db/user/is_view_ok">
      <th>Actor:</th>
      <td metal:use-macro="search_select">
       <option metal:fill-slot="extra_options" tal:attributes="value request/user/id">
        done by me
       </option>
      </td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td>&nbsp;</td>
     </tr>
     <tr tal:define="name string:priority; db_klass string:priority; db_content string:name;">
      <th>Priority:</th>
      <td metal:use-macro="search_select">
       <option metal:fill-slot="extra_options" value="-1" 
         tal:attributes="selected python:value == '-1'">
        not selected
       </option>
      </td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr tal:define="name string:status; db_klass string:status; db_content string:name;">
      <th>Status:</th>
      <td metal:use-macro="search_select">
       <tal:block metal:fill-slot="extra_options">
        <option value="-1,1,2,3,4,5,6,7" 
          tal:attributes="selected python:value == '-1,1,2,3,4,5,6,7'">
         not resolved
        </option>
        <option value="-1" tal:attributes="selected python:value == '-1'">
         not selected
        </option>
       </tal:block>
      </td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr tal:define="name string:assignedto; db_klass string:user; db_content string:username;" tal:condition="db/user/is_view_ok">
      <th>Assigned to:</th>
      <td metal:use-macro="search_select">
       <tal:block metal:fill-slot="extra_options">
        <option tal:attributes="value request/user/id">assigned to me</option>
        <option value="-1" tal:attributes="selected python:value == '-1'">
         unassigned
        </option>
       </tal:block>
      </td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr>
      <th>No Sort or group:</th>
      <td>&nbsp;</td>
      <td>&nbsp;</td>
      <td><input type="radio" name="@sort" value=""></td>
      <td><input type="radio" name="@group" value=""></td>
     </tr>
     <tr>
      <th>Sort Descending:</th>
      <td><input type="checkbox" name="@sortdir" 
         tal:attributes="checked python:request.sort[0] == '-' or request.sort[0] is None"></td>
     </tr>
     <tr>
      <th>Group Descending:</th>
      <td><input type="checkbox" name="@groupdir" 
         tal:attributes="checked python:request.group[0] == '-'"></td>
     </tr>
     <tr tal:condition="python:request.user.hasPermission('Edit', 'query')">
      <th>Query name**:</th>
      <td tal:define="value request/form/@queryname/value | nothing">
       <input name="@queryname" tal:attributes="value value">
       <input type="hidden" name="@old-queryname" tal:attributes="value value">
      </td>
     </tr>
     <tr>
      <td>
       <input type="hidden" name="@action" value="batch_validate" />
       <input type="hidden" name="@template" value="batch" />
      </td>
      <td><input type="submit" value="Submit"></td>
     </tr>
     <tr>
      <td>&nbsp;</td>
      <td colspan="4" class="help">
       <span tal:omit-tag="true">*: The "all text" field will look in message bodies and development ticket titles</span><br>
       <span tal:condition="python:request.user.hasPermission('Edit', 'query')" tal:omit-tag="true">
        **: If you supply a name, the query will be saved off and available as a link in the sidebar
       </span>
      </td>
     </tr>
    </table>
   </form>
  </td>
 </tal:block>

The second page to create is 'issue.batch.html', placed in your 'html' directory, and insert the following code::

 <tal:block metal:use-macro="templates/page/macros/icing">
  <title metal:fill-slot="head_title">Issue Batch Editing - <span tal:replace="config/TRACKER_NAME" /></title>
  <span metal:fill-slot="body_title" tal:omit-tag="true">Issue Batch Editing</span>
  <td class="content" metal:fill-slot="content">
   <form method="GET" name="itemSynopsis" tal:attributes="action request/classname">
    <table class="form">
     <tr><th colspan="4" class="header">Edit Fields</th></tr>
     <tr>
      <th>Priority</th>
      <td>
       <select name="batch_priority">
        <option value="-1">- no selection -</option>
        <tal:block tal:repeat="item db/priority/list">
         <option tal:attributes="value item/id" tal:content="item/name"></option>
        </tal:block>
       </select>
      </td>
      <th>Status</th>
      <td>
       <select name="batch_status">
        <option value="-1">- no selection -</option>
        <tal:block tal:repeat="item db/status/list">
         <option tal:attributes="value item/id" tal:content="item/name"></option>
        </tal:block>
       </select>
      </td>
     </tr>
     <tr>
      <th>Superseder</th>
      <td>
       <input type="text" name="batch_superseder" size="30" />
       <span tal:replace="structure python:db.issue.classhelp('name', property='batch_superseder')" />
      </td>
      <th>Nosy List</th>
      <td>
       <input type="text" name="batch_nosy" size="30" />
       <span tal:replace="structure python:db.user.classhelp('name', property='batch_nosy')" />
      </td>
     </tr>
     <tr>
      <th>Assigned To</th>
      <td>
       <select name="batch_assignedto">
        <option value="-1">- no selection -</option>
        <tal:block tal:repeat="item db/user/list">
         <option tal:attributes="value item/id" tal:content="item/username"></option>
        </tal:block>
       </select>
      </td>
      <th>Topics</th>
      <td>
       <input type="text" name="batch_topics" size="30" />
       <span tal:replace="structure python:db.keyword.classhelp('id,title', property='batch_topics')" />
      </td>
     </tr>
     <tr>
      <th>Change Note</th>
      <td colspan=3>
       <textarea name="batch_note" wrap="hard" rows="5" cols="80"></textarea>
      </td>
     </tr>
     <tr>
      <th>File</th>
      <td colspan=3><input type="file" name="batch_file" size="40"></td>
     </tr>
    </table>
    <br />
    <table class="form" tal:define="
     cols python:request.columns or 'id activity title status assignedto'.split();
     sort_on python:request.sort[1] or 'activity';
     group_on python:request.group[1] or 'status';
     search_input templates/page/macros/search_input;
     column_input templates/page/macros/column_input;
     sort_input templates/page/macros/sort_input;
     group_input templates/page/macros/group_input;
     search_select templates/page/macros/search_select;
     search_multiselect templates/page/macros/search_multiselect;">
     <tr>
      <th class="header">&nbsp;</th>
      <th class="header">Filter on</th>
      <th class="header">Display</th>
      <th class="header">Sort on</th>
      <th class="header">Group on</th>
     </tr>
     <tr tal:define="name string:@search_text">
      <th>All text*:</th>
      <td metal:use-macro="search_input"></td>
      <td>&nbsp;</td>
      <td>&nbsp;</td>
      <td>&nbsp;</td>
     </tr>
     <tr tal:define="name string:title">
      <th>Title:</th>
      <td metal:use-macro="search_input"></td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td>&nbsp;</td>
     </tr>
     <tr tal:define="name string:topic; db_klass string:keyword; db_content string:name;">
      <th>Topic:</th>
      <td metal:use-macro="search_select"></td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr tal:define="name string:id">
      <th>ID:</th>
      <td metal:use-macro="search_input"></td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td>&nbsp;</td>
     </tr>
     <tr tal:define="name string:creation">
      <th>Creation Date:</th>
      <td metal:use-macro="search_input"></td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr tal:define="name string:creator; db_klass string:user; db_content string:username;" 
      tal:condition="db/user/is_view_ok">
      <th>Creator:</th>
      <td metal:use-macro="search_select">
       <option metal:fill-slot="extra_options" tal:attributes="value request/user/id">created by me</option>
      </td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr tal:define="name string:activity">
      <th>Activity:</th>
      <td metal:use-macro="search_input"></td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td>&nbsp;</td>
     </tr>
     <tr tal:define="name string:actor; db_klass string:user; db_content string:username;" 
      tal:condition="db/user/is_view_ok">
      <th>Actor:</th>
      <td metal:use-macro="search_select">
       <option metal:fill-slot="extra_options" tal:attributes="value request/user/id">
        done by me
       </option>
      </td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td>&nbsp;</td>
     </tr>
     <tr tal:define="name string:priority; db_klass string:priority; db_content string:name;">
      <th>Priority:</th>
      <td metal:use-macro="search_select">
       <option metal:fill-slot="extra_options" value="-1" 
         tal:attributes="selected python:value == '-1'">
        not selected
       </option>
      </td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr tal:define="name string:status; db_klass string:status; db_content string:name;">
      <th>Status:</th>
      <td metal:use-macro="search_select">
       <tal:block metal:fill-slot="extra_options">
        <option value="-1,1,2,3,4,5,6,7" 
          tal:attributes="selected python:value == '-1,1,2,3,4,5,6,7'">
         not resolved
        </option>
        <option value="-1" tal:attributes="selected python:value == '-1'">
         not selected
        </option>
       </tal:block>
      </td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr tal:define="name string:assignedto; db_klass string:user; db_content string:username;" tal:condition="db/user/is_view_ok">
      <th>Assigned to:</th>
      <td metal:use-macro="search_select">
       <tal:block metal:fill-slot="extra_options">
        <option tal:attributes="value request/user/id">assigned to me</option>
        <option value="-1" tal:attributes="selected python:value == '-1'">
         unassigned
        </option>
       </tal:block>
      </td>
      <td metal:use-macro="column_input"></td>
      <td metal:use-macro="sort_input"></td>
      <td metal:use-macro="group_input"></td>
     </tr>
     <tr>
      <th>No Sort or group:</th>
      <td>&nbsp;</td>
      <td>&nbsp;</td>
      <td><input type="radio" name="@sort" value=""></td>
      <td><input type="radio" name="@group" value=""></td>
     </tr>
     <tr>
      <th>Sort Descending:</th>
      <td><input type="checkbox" name="@sortdir" 
         tal:attributes="checked python:request.sort[0] == '-' or request.sort[0] is None"></td>
     </tr>
     <tr>
      <th>Group Descending:</th>
      <td><input type="checkbox" name="@groupdir" 
         tal:attributes="checked python:request.group[0] == '-'"></td>
     </tr>
     <tr tal:condition="python:request.user.hasPermission('Edit', 'query')">
      <th>Query name**:</th>
      <td tal:define="value request/form/@queryname/value | nothing">
       <input name="@queryname" tal:attributes="value value">
       <input type="hidden" name="@old-queryname" tal:attributes="value value">
      </td>
     </tr>
     <tr>
      <td>
       <input type="hidden" name="@action" value="batch_validate" />
       <input type="hidden" name="@template" value="batch" />
      </td>
      <td><input type="submit" value="Submit"></td>
     </tr>
     <tr>
      <td>&nbsp;</td>
      <td colspan="4" class="help">
       <span tal:omit-tag="true">*: The "all text" field will look in message bodies and development ticket titles</span><br>
       <span tal:condition="python:request.user.hasPermission('Edit', 'query')" tal:omit-tag="true">
        **: If you supply a name, the query will be saved off and available as a link in the sidebar
       </span>
      </td>
     </tr>
    </table>
   </form>
  </td>
 </tal:block>

Third, find the following line to your 'page.html' file, found in the 'html' directory::

 <a href="issue?@template=search" i18n:translate="">Search</a><br>

Add after it the following line::

 <tal:block tal:condition="python:request.user.hasPermission('View', 'issue')">
  <a href="issue?@template=batch_search">Batch Edit</a><br />
 </tal:block>

The final piece of the puzzle is to create a new file, 'batch_editing.py', place it into the 'extensions' directory, and insert the following code::

   1 from roundup.cgi.actions import SearchAction
   2 
   3 class batch_validate(SearchAction):
   4     def handle(self):
   5         data_entered = False
   6         data_keys = [x for x in self.form.keys() if x.startswith('batch_')]
   7         if len(data_keys) > 1:
   8             for key in data_keys:
   9                 if self.form[key].value != '-1':
  10                     data_entered = True
  11         if not data_entered:
  12             self.client.template = 'batch_search'
  13             self.client.error_message.append('You must enter data in at least one edit field')
  14         else:
  15             SearchAction.handle(self)
  16 
  17 def init(tracker):
  18     tracker.registerAction('batch_validate', batch_validate)

Hopefully this will be as useful to you as it has been to me.

--Lewis Franklin