Roundup Tracker

Introduction

In our tracker we have a version table which has a multilink product (linked to a product table). As long as the version and product tables were small, the _generic.index.html page was suitable to maintain the version table. But in time it became harder and harder to maintain it. What we needed was some easy way to add/edit/remove versions and to select the versions that were valid for a specific product.

What does it do?

When adding a version (or changing one by clicking the version name), an other popup appears to let you set (or change) the version name and the products (multi select) for which the version is valid. By default the product selected in the manager is there. At the bottom of the window are an Add button (will be Update in modify mode) and a Cancel button. Pressing Add (or Update) will automatically refresh the version list in the manager.

When removing a version from a product (by uncheck it and pressing Apply), an action handler checks if there are still other products that use it. If not, the version is retired and not shown again.

The database relation

This way the classlist can be used to select more than one version when a bug is solved in more than one release (e.g. HEAD revision for main release and branch revision for patches). Default the classlist can't filter (it hasn't a parameter for it), but you can trick it by misusing the property parameter like this:

In this example, only the versions for product 1 are showed.

The _generic.item.html (or the below version.item.html) template can be used to change version names with still being able to see which products will be affected. Even so can all the needed products be specified directly when creating a new version. This prevents having to add a new version manual to each product.

The _generic.index.html template will give a quick overview which versions there are, and for which products they are valid. The version/product classes

Product table::

Version table::

Now we have our scheme complete. What's left is adding the HTML templates and the action handler.

The HTML templates

In total we need three HTML templates: 1. version.manager.html This template is the basis for the version manager. It contains an inline frame that will hold the version listing.

version.manager.list.html This template is loaded in the inline frame of version.manager.html.

version.item.html (optional) This template is used to add or edit a version. The template is not really needed. You might let roundup use the default _generic.item.html template.

version.manager.html:

 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html tal:define="selected_product request/form/selected_product/value | nothing">
  <head>
   <title>Version Manager</title>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8;">
   <link rel="stylesheet" type="text/css" href="@@file/style.css">

   <script language="javascript" type="text/javascript">
    applied = true;

    function add_version(product) {
     if (product != 'None') {
      extra = '&product=' + product;
     }
     else {
      extra = '';
     }

     self.frames['VERSIONLIST'].window.popup('version?@template=item' + extra, 720, 180);
    }

    function change_product() {
     if (applied || self.confirm('Changes are not applied! Change product anyway?')) {
      window.document.forms['productForm'].submit();
      applied = true;
     }
    }

    function close_manager() {
     if (applied || self.confirm('Changes are not applied! Close anyway?')) {
      self.close();
     }
    }
   </script>
  </head>

  <body class="classhelp"
   tal:attributes="onload string:javascript:self.frames['VERSIONLIST'].window.location = 'version?@template=manager.list&selected_product=${selected_product}'">

   <span tal:condition="not:context/is_edit_ok">
    You are not allowed to view this page.
   </span>

   <tal:block tal:condition="context/is_edit_ok">
    <table class="gray-form" style="height:100%" cellspacing="0" tal:condition="context/is_view_ok">
     <tr>
      <form method="GET" name="productForm">
       <input type="hidden" name="@template" value="manager">
       <td style="padding-top:10" nowrap>
        For&nbsp;product:&nbsp;
        <select name="selected_product" onChange="javascript: change_product();">
         <option value="" tal:attributes="selected python: not selected_product">- no selection -</option>
         <tal:block tal:repeat="prod db/product/list">
          <tal:block tal:condition="python: not prod.name in ['Other','n/a']">
           <option tal:attributes="selected python: selected_product and selected_product == prod.id;
                 value prod/id"
             tal:content="prod/name">
           </option>
          </tal:block>
         </tal:block>
        </select>
       </td>
      </form>
     </tr>

     <tr tal:condition="python:request.user.hasPermission('Edit', None)">
      <td>
       <input style="margin:10 10 10 10" type="button" name="clear" value=" Clear All "
        onclick="javascript: self.frames['VERSIONLIST'].window.set_all(false); applied = false;"
        tal:attributes="disabled python: not selected_product">
       <input style="margin:10 10 10 10" type="button" name="select" value=" Select All "
        onclick="javascript: self.frames['VERSIONLIST'].window.set_all(true); applied = false;"
        tal:attributes="disabled python: not selected_product">
      </td>
     </tr>

     <tr>
      <td style="padding-top:10" width="100%" height="100%">
       <iframe name="VERSIONLIST" marginwidth="0" marginheight="0"
       width="100%" height="80%" frameborder="0" scrolling="auto">
        Sorry, you need inline frames to fully see this page.
       </iframe><br>

       <input style="margin:10 10 10 10" type="button" name="apply" value=" Apply "
        onclick="javascript: self.frames['VERSIONLIST'].window.document.forms['versionList'].submit(); applied = true;"
        tal:attributes="disabled python: not selected_product">
      </td>
     </tr>

     <tr>
      <td style="padding-top:10"><hr style="width:100%; color:black; height:1pt;"></td>
     </tr>

     <tr>
      <td style="text-align:center">
       <input style="margin:10 10 10 0" type="button" name="close" value=" Close "
              onclick="javascript: close_manager();">
       <input style="margin:10 0 10 10" type="button" name="add" value=" Add Version "
        tal:attributes="onclick string:javascript: add_version('${selected_product}')">
      </td>
     </tr>
    </table>
   </tal:block>
  </body>
 </html>

version.manager.list.html:

 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html tal:define="selected_product request/form/selected_product/value | nothing">
  <head>
   <title></title>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8;">
   <link rel="stylesheet" type="text/css" href="@@file/style.css">

   <script language="javascript" type="text/javascript">
    undefined = self.document.make_it_work; // to make this work with IE 4.x and 5.0

    // prefent being opened outside the main template
    if (self.opener == undefined) {
     if (top.frames.length == 0) {
      window.location = 'version?@template=manager';
     }
    }

    // to make it possible for my popups to reload/refresh my content
    function refresh() {
     self.location.reload();
    }

    // to check or uncheck all versions
    function set_all(value) {
     if (self.document.versionList.version != undefined) {
      for (check = 0; check < self.document.versionList.version.length; check++) {
       self.document.versionList.version[check].checked = value;
      }
     }
    }

    function popup(uri, width, height) {
     if (document.all) {
      // IE
      version = navigator.appVersion.match('IE (([0-9]+)[.][0-9]+)');
      if (((version.length >= 3) && (version[2] == '4')) ||
       ((version.length >= 2) && (version[1] == '5.0'))) {
       // IE 4.x + IE 5.0
       height = height * 1.15;
       extra = ',top=1,left=1';
      }
      else {
       // All other IEs
       extra = '';
      }
     }
     else {
      // Netscape
      extra = ',screenX=1,screenY=1';
     }

     self.open(uri, '',
'scrollbars=no,resizable=no,location=no,directories=no,menubar=no,toolbar=no,status=no,height='+height+',width='+width+extra);
    }

    function add_version(product) {
     if (product != 'None') {
      extra = '&product=' + product;
     }
     else {
      extra = '';
     }

     popup('version?@template=item' + extra, 720, 180);
    }
   </script>
  </head>

  <body class="classhelp">
   <span tal:condition="not:context/is_edit_ok">
    You are not allowed to view this page.
   </span>

   <tal:block tal:condition="context/is_edit_ok">
    <form method="GET" name="versionList">
     <input type="hidden" name="@template" value="manager.list">
     <input type="hidden" name="@action" value="versionmanager">
     <input type="hidden" name="product_id" tal:attributes="value selected_product">

     <table style="background-color:#ddddde; height:100%" width="100%" cellspacing="0"
      tal:condition="context/is_view_ok">
      <tal:block tal:repeat="version db/version/list">
       <tr>
        <td width="100px">
         <input tal:condition="python: selected_product != 'None'" type="checkbox" name="version"
          tal:attributes="checked python: selected_product in version.product._value;
              value version/id"
          onclick="javascript: self.parent.applied = false;">
        </td>
        <td>
         <a tal:attributes="href string:
            javascript:popup('version${version/id}?@template=item', 720, 180)"
            tal:content="version/name"></a>
        </td>
       </tr>
      </tal:block>
     </table>
    </form>
   </tal:block>
  </body>
 </html>

 

version.item.html (optional)::

 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html>
  <head>
   <title tal:condition="not:context/id"
    tal:content="string:Add new version"></title>
   <title tal:condition="context/id"
    tal:content="string:Update version ${context/name}"></title>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8;">
   <link rel="stylesheet" type="text/css" href="@@file/style.css">

   <script tal:replace="structure request/base_javascript"&gt;&lt;disabled /script>
  </head>

  <body class="content">
   <script tal:condition="options/ok_message | nothing"
    language="javascript" type="text/javascript"
    tal:content="string:self.opener.refresh(); self.close();">
   </script>

   <span tal:condition="not:context/is_edit_ok">
    You are not allowed to view this page.
   </span>

   <tal:block tal:condition="context/is_edit_ok">
    <form method="POST" name="itemSynopsis" enctype="multipart/form-data">
     <input type="hidden" name="@template" value="item">
     <input type="hidden" name="@required" value="name,product">

     <table class="form" style="height:100%; width:100%">
      <tr>
       <td colspan="2">
        <tal:block metal:use-macro="templates/page/macros/message_block"></tal:block>
       </td>
      </tr>

      <tr>
       <th nowrap>Version name</th>
       <td width="100%" tal:content="structure python:context.name.field(size=10)"></td>
      </tr>

      <tr>
       <th nowrap>For products</th>
       <td nowrap>
        <span tal:replace="python:context.product.field(size=80)"></span>
        <span tal:replace="structure python:
              db.product.classhelp('name', property='product', width='600')"></span>
       </td>
      </tr>

      <tr>
       <td colspan="2">&nbsp;<br><hr style="width:100%; color:black; height:1pt;"></td>
      </tr>

      <tr>
       <td colspan="2" style="text-align:center">
        <tal:block tal:condition="not:context/id">
         <input type="hidden" name="@action" value="new">
         <input type="submit" name="submit" value=" Add ">
        </tal:block>
        <tal:block tal:condition="context/id">
         <input type="hidden" name="@action" value="edit">
         <input type="submit" name="submit" value=" Update ">
        </tal:block>
        <input style="margin-left:20" type="button" name="cancel" value=" Cancel "
               onclick="javascript: self.close();">
       </td>
      </tr>

      <tr>
       <td colspan="2">&nbsp;</td>
      </tr>

     </table>
    </form>
   </tal:block>
  </body>
 </html>

The style sheet

The manager mainly uses the classhelp styles. Ony some additional body and iframe styles are needed::

The action handler

We need an action handler to update the selections in the database tables.

In interfaces.py we add a VersionManager class::

 from roundup.cgi.exceptions import Redirect
 from roundup.cgi import actions

 class VersionManagerAction(actions.Action):

     def handle(self):
         import types

         if self.form.has_key('product_id'):
             product_id = self.form['product_id'].value

             if self.form.has_key('version'):
                 versions = self.form['version']

                 if not isinstance(versions, types.ListType):
                     versions = [ versions ]

                 version_list = []

                 for version in versions:
                     if not version.value in version_list:
                         version_list.append(version.value)

                 save_changes = False

                 for version_id in self.db.version.list():
                     changed = False

                     product = self.db.version.get(version_id, 'product')

                     if version_id in version_list:
                         if not product_id in product:
                             product.append(product_id)
                             changed = True
                     else:
                         if product_id in product:
                             product.remove(product_id)
                             changed = True

                     if changed:
                         if len(product) > 0:
                             product.sort()
                             self.db.version.set(version_id, product=product)
                         else:
                             self.db.version.retire(version_id)

                     save_changes = save_changes or changed

                 if save_changes:
                     self.db.commit()

                 raise Redirect, '%sversion?@template=%s&selected_product=%s'%(self.base, self.template, product_id)

And finally we make the new handler known to roundup in 'interfaces.py'::

 class Client(client.Client):
      ''' derives basic CGI implementation from the standard module,
          with any specific extensions
      '''
      def __init__(self, instance, request, env, form=None):
          client.Client.__init__(self, instance, request, env, form)
          actions = list(self.actions)
          actions.append(('merge', VersionManagerAction),)
          self.actions = tuple(actions)

Finally

To use the version manager, you have to call it somewhere in page.html. One location could be below the Add User anchor tag in the Administration block. The version manager is supposed to run in a popup window, but that can easily be changed. The advantage of a popup window will be adding a version while the issue.item.html template is still visible.

The call could look like::

We our self placed the javascript function popup function in version.manager.list.html into a separate javascript file and include it where needed. The call then look like:

Unfortunately, roundup doesn't support a direct method of displaying only those versions that are valid for a selected product without some handwork.

There are two handwork methods to do this:

You can show all versions and use an auditor to check if the version is valid for the selected product. This works, but the users will see a long list of versions with only same that are relevant for them.

You can add a product select form to pre-select a product before filling in the issue.item.html. To my opinion, this is the best (and hardest) way. There is an example of such a pre-select form in the customizing document.

After pre-select, the HTML to filter the versions could look like::

That's it! I hope you like and have need for it. At least my boss did ;-)

Best regards, Marlon


CategorySchema