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 the version manager is opened, it appears in a popup window that looks a bit like the classhelp window. On top there is a selection box which allows you to select the product for which you want to manage the versions. Below this product selector, there is a inline scrollable list of all known version names. Before those names are check boxes. The for the selected product valid versions are checked. Changes can be made and will be updated in the backend as soon as the apply button is hit. This button is below the version list. At the bottom of the window, there are two more buttons. Close to close the window (confirmation will be asked when pressing close while changes are done without applying them) and Add Version to add a new version to the list (default for the selected product).
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
- The database relation seems to be a bit illogical. Each version entry contains a multilink product property. It would be easier to gave each product entry (in the product table) a multilink version property, but we did so for the next reasons:
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:
<span tal:replace="structure python: db.product.classhelp(name,
property=product&@filter=product&product=1, width=600)"></span>
In this example, only the versions for product 1 are showed.
- Note: at least we hoped that it could be used like this, but it seems that the batch method in _generic.help.html ignores filter settings. Not so nice if you ask me.
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
- The next definitions are added to 'dbinit.py':
Product table::
- product = Class(db, 'product',
- name=String())
Version table::
- version = Class(db, 'version',
- name=String(), product=Multilink('product'))
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 product: <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"><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"> <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"> </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::
- body.classhelp {
- font-family: Arial; color: #000000; background-color: #eee;
- border-top: 2px solid #444444; border-bottom: 2px solid #444444;
- font-family: Arial; color: black; margin: 0 0 0 0; background-color: #ddddde;
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::
<a class="navigate" href="javascript:self.open('version?@template=manager', 'VERMAN', 'scrollbars=no,resizable=no,location=no,directories=no,menubar=no,toolbar=no,status=no,height=600,width=400')">
- Version Manager
</a>
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:
<a class="navigate" href="javascript:popup('version?@template=manager', 400, 600)">
- Version Manager
</a>
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::
<html tal:define="selected_product context/product/id | request/form/product/value | nothing"> [snip]
<select name="version" tal:attributes="disabled not: selected_product">
<option value="-1">- no selection -</option> <tal:block tal:repeat="version python: db.version.list()">
<tal:block tal:condition="python: selected_product in version.product">
<option tal:attributes="selected python: context.id and version.id == context.version.id; value version/id"
tal:content="version/name">
</option>
</tal:block>
</tal:block>
</select>
That's it! I hope you like and have need for it. At least my boss did
Best regards, Marlon