CINXE.COM

Customising Roundup - Roundup 2.4.0 documentation

<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Customising Roundup - Roundup 2.4.0 documentation</title> <script type="text/javascript"> var DOCUMENTATION_OPTIONS = { URL_ROOT: '../', VERSION: '2.4.0', COLLAPSE_MODINDEX: false, FILE_SUFFIX: '.html' }; </script> <link rel="stylesheet" href="../_static/style.css" type="text/css" /> <!-- https://github.com/sphinx-doc/sphinx/issues/11699 means a duplicate viewport tag --> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta content="How to customise and extend the Roundup Issue Tracker. Includes many cookbook and how-to examples." name="description" /> <meta property="og:title" content="Customising Roundup" /> <meta property="og:type" content="website" /> <meta property="og:url" content="https://www.roundup-tracker.org/docs/customizing.html" /> <meta property="og:site_name" content="Roundup" /> <meta property="og:description" content="What You Can Do, Examples- Changing what’s stored in the database- Adding a new field to the classic schema, Adding a new constrained field to the classic schema, Adding a time log to your issues, ..." /> <meta property="og:image" content="https://www.roundup-tracker.org/_images/index_logged_out.png" /> <meta property="og:image:alt" content="The front page of a tracker showing a table of issues and their properties." /> <link rel="canonical" href="https://www.roundup-tracker.org/docs/customizing.html" /> <link rel="search" type="application/opensearchdescription+xml" title="Search within Roundup 2.4.0 documentation" href="../_static/opensearch.xml"/> <link rel="index" title="Index" href="../genindex.html" /> <link rel="search" title="Search" href="../search.html" /> <link rel="top" title="Roundup 2.4.0 documentation" href="../index.html" /> <link rel="up" title="Docs" href="../docs.html" /> <link rel="next" title="REST API for Roundup" href="rest.html" /> <link rel="prev" title="User Guide" href="user_guide.html" /> </head> <body> <div id="skiplink"><a href="#main">Skip to main content</a></div> <header class="header"> <div class="label non_mobile">Roundup</div> <div class="label mobile"><a href="#main">Roundup <span class="jumplabel">jump to Customising Roundup</span></a></div> <div id="searchbox" style="display: none"> <form class="search" action="../search.html" method="get"> <input type="text" aria-label="Enter search terms" name="q" size="18" autocomplete="on" /> <input type="submit" value="Search" /> <input type="hidden" name="check_keywords" value="yes" /> <input type="hidden" name="area" value="default" /> </form> </div> </header> <div class="navigation"> <nav aria-label="primary navigation"> <div class="menu"> <ul class="current"> <li class="toctree-l1"><a class="reference internal" href="../index.html">Home</a></li> <li class="toctree-l1"><a class="reference external" href="https://pypi.org/project/roundup/">Download</a></li> <li class="toctree-l1 current"><a class="reference internal" href="../docs.html">Docs</a><ul class="current"> <li class="toctree-l2"><a class="reference internal" href="features.html">Features</a></li> <li class="toctree-l2"><a class="reference internal" href="installation.html">Installation</a></li> <li class="toctree-l2"><a class="reference internal" href="upgrading.html">Upgrading to Newer Versions</a></li> <li class="toctree-l2"><a class="reference internal" href="security.html">Security Issues</a></li> <li class="toctree-l2"><a class="reference internal" href="FAQ.html">FAQ</a></li> <li class="toctree-l2"><a class="reference internal" href="user_guide.html">User's Guide</a></li> <li class="toctree-l2 current"><a class="current reference internal" href="#">Customising</a></li> <li class="toctree-l2"><a class="reference internal" href="rest.html">Rest API</a></li> <li class="toctree-l2"><a class="reference internal" href="xmlrpc.html">XML-RPC API</a></li> <li class="toctree-l2"><a class="reference internal" href="reference.html">Reference</a></li> <li class="toctree-l2"><a class="reference internal" href="glossary.html">Glossary</a></li> <li class="toctree-l2"><a class="reference internal" href="admin_guide.html">Administration Guide</a></li> <li class="toctree-l2"><a class="reference internal" href="man_pages.html">Roundup Manual Pages</a></li> <li class="toctree-l2"><a class="reference internal" href="license.html">License</a></li> <li class="toctree-l2"><a class="reference internal" href="acknowledgements.html">Acknowledgements</a></li> <li class="toctree-l2"><a class="reference internal" href="../olderdocs.html">Other Docs</a></li> </ul> </li> <li class="toctree-l1"><a class="reference external" href="https://issues.roundup-tracker.org">Issues</a></li> <li class="toctree-l1"><a class="reference internal" href="../contact.html">Contact</a></li> <li class="toctree-l1"><a class="reference external" href="https://wiki.roundup-tracker.org">Wiki</a></li> <li class="toctree-l1"><a class="reference internal" href="../code.html">Code</a></li> </ul> </div> </nav> </div> <div class="content"> <nav id="subnav" aria-label="sub navigation"> <a title="User Guide" href="user_guide.html"> Prev</a> <a title="REST API for Roundup" href="rest.html"> Next</a> <a title="Index" href="../genindex.html"> Index</a> </nav> <main id="main" role="main" tabindex="-1"> <section id="customising-roundup"> <h1>Customising Roundup</h1> <div class="admonition-welcome admonition"> <p class="admonition-title">Welcome</p> <p>This document used to be much larger and include a lot of reference material. That has been moved to the <a class="reference external" href="reference.html">reference document</a>.</p> <p>The documentation is slowly being reorganized using the <a class="reference external" href="https://diataxis.fr/">Diataxis framework</a>. Help with the reorganization is welcome.</p> </div> <nav class="contents local" id="contents"> <ul class="simple"> <li><p><a class="reference internal" href="#what-you-can-do" id="id1">What You Can Do</a></p></li> <li><p><a class="reference internal" href="#examples" id="id2">Examples</a></p> <ul> <li><p><a class="reference internal" href="#changing-what-s-stored-in-the-database" id="id3">Changing what’s stored in the database</a></p> <ul> <li><p><a class="reference internal" href="#adding-a-new-field-to-the-classic-schema" id="id4">Adding a new field to the classic schema</a></p></li> <li><p><a class="reference internal" href="#adding-a-new-constrained-field-to-the-classic-schema" id="id5">Adding a new constrained field to the classic schema</a></p></li> <li><p><a class="reference internal" href="#adding-a-time-log-to-your-issues" id="id6">Adding a time log to your issues</a></p></li> <li><p><a class="reference internal" href="#tracking-different-types-of-issues" id="id7">Tracking different types of issues</a></p></li> </ul> </li> <li><p><a class="reference internal" href="#using-external-user-databases" id="id8">Using External User Databases</a></p> <ul> <li><p><a class="reference internal" href="#using-an-external-password-validation-source" id="id9">Using an external password validation source</a></p></li> <li><p><a class="reference internal" href="#using-a-un-x-passwd-file-as-the-user-database" id="id10">Using a UN*X passwd file as the user database</a></p></li> <li><p><a class="reference internal" href="#using-an-ldap-database-for-user-information" id="id11">Using an LDAP database for user information</a></p></li> <li><p><a class="reference internal" href="#other-external-databases" id="id12">Other External Databases</a></p></li> </ul> </li> <li><p><a class="reference internal" href="#changes-to-tracker-behaviour" id="id13">Changes to Tracker Behaviour</a></p> <ul> <li><p><a class="reference internal" href="#preventing-spam" id="id14">Preventing SPAM</a></p></li> <li><p><a class="reference internal" href="#stop-nosy-messages-going-to-people-on-vacation" id="id15">Stop “nosy” messages going to people on vacation</a></p></li> <li><p><a class="reference internal" href="#adding-in-state-transition-control" id="id16">Adding in state transition control</a></p></li> <li><p><a class="reference internal" href="#blocking-issues-that-depend-on-other-issues" id="id17">Blocking issues that depend on other issues</a></p></li> <li><p><a class="reference internal" href="#add-users-to-the-nosy-list-based-on-the-keyword" id="id18">Add users to the nosy list based on the keyword</a></p></li> <li><p><a class="reference internal" href="#restricting-updates-that-arrive-by-email" id="id19">Restricting updates that arrive by email</a></p></li> </ul> </li> <li><p><a class="reference internal" href="#changes-to-security-and-permissions" id="id20">Changes to Security and Permissions</a></p> <ul> <li><p><a class="reference internal" href="#restricting-the-list-of-users-that-are-assignable-to-a-task" id="id21">Restricting the list of users that are assignable to a task</a></p></li> <li><p><a class="reference internal" href="#users-may-only-edit-their-issues" id="id22">Users may only edit their issues</a></p></li> <li><p><a class="reference internal" href="#all-users-may-only-view-and-edit-issues-files-and-messages-they-create" id="id23">All users may only view and edit issues, files and messages they create</a></p></li> <li><p><a class="reference internal" href="#moderating-user-registration" id="id24">Moderating user registration</a></p></li> </ul> </li> <li><p><a class="reference internal" href="#changes-to-the-web-user-interface" id="id25">Changes to the Web User Interface</a></p> <ul> <li><p><a class="reference internal" href="#adding-action-links-to-the-index-page" id="id26">Adding action links to the index page</a></p></li> <li><p><a class="reference internal" href="#colouring-the-rows-in-the-issue-index-according-to-priority" id="id27">Colouring the rows in the issue index according to priority</a></p></li> <li><p><a class="reference internal" href="#editing-multiple-items-in-an-index-view" id="id28">Editing multiple items in an index view</a></p></li> <li><p><a class="reference internal" href="#displaying-only-message-summaries-in-the-issue-display" id="id29">Displaying only message summaries in the issue display</a></p></li> <li><p><a class="reference internal" href="#enabling-display-of-either-message-summaries-or-the-entire-messages" id="id30">Enabling display of either message summaries or the entire messages</a></p></li> <li><p><a class="reference internal" href="#setting-up-a-wizard-or-druid-for-controlled-adding-of-issues" id="id31">Setting up a “wizard” (or “druid”) for controlled adding of issues</a></p></li> <li><p><a class="reference internal" href="#silent-submit" id="id32">Silent Submit</a></p></li> </ul> </li> <li><p><a class="reference internal" href="#changing-how-the-core-code-works" id="id33">Changing How the Core Code Works</a></p> <ul> <li><p><a class="reference internal" href="#changing-cache-control-headers" id="id34">Changing Cache-Control Headers</a></p></li> <li><p><a class="reference internal" href="#implement-password-complexity-checking" id="id35">Implement Password Complexity Checking</a></p></li> <li><p><a class="reference internal" href="#enhance-time-intervals-forms" id="id36">Enhance Time Intervals Forms</a></p></li> <li><p><a class="reference internal" href="#modifying-the-mail-gateway" id="id37">Modifying the Mail Gateway</a></p></li> </ul> </li> </ul> </li> <li><p><a class="reference internal" href="#other-examples" id="id38">Other Examples</a></p></li> <li><p><a class="reference internal" href="#examples-on-the-wiki" id="id39">Examples on the Wiki</a></p></li> </ul> </nav> <section id="what-you-can-do"> <h2><a class="toc-backref" href="#id1" role="doc-backlink">What You Can Do</a></h2> <p>Before you get too far, it’s probably worth having a quick read of the Roundup <a class="reference external" href="design.html">design documentation</a>.</p> <p>Customisation of Roundup can take one of six forms:</p> <ol class="arabic simple"> <li><p><a class="reference external" href="reference.html#tracker-configuration">tracker configuration</a> changes</p></li> <li><p>database, or <a class="reference external" href="reference.html#tracker-schema">tracker schema</a> changes</p></li> <li><p>“definition” class <a class="reference external" href="reference.html#database-content">database content</a> changes</p></li> <li><p>behavioural changes through <a class="reference external" href="reference.html#detectors">detectors</a>, <a class="reference external" href="reference.html#extensions">extensions</a> and <a class="reference external" href="reference.html#interfaces-py">interfaces.py</a></p></li> <li><p><a class="reference external" href="reference.html#security-access-controls">security / access controls</a></p></li> <li><p>change the <a class="reference external" href="reference.html#web-interface">web interface</a></p></li> </ol> <p>The third case is special because it takes two distinctly different forms depending upon whether the tracker has been initialised or not. The other two may be done at any time, before or after tracker initialisation. Yes, this includes adding or removing properties from classes.</p> </section> <section id="examples"> <span id="customexamples"></span><h2><a class="toc-backref" href="#id2" role="doc-backlink">Examples</a></h2> <section id="changing-what-s-stored-in-the-database"> <h3><a class="toc-backref" href="#id3" role="doc-backlink">Changing what’s stored in the database</a></h3> <p>The following examples illustrate ways to change the information stored in the database.</p> <section id="adding-a-new-field-to-the-classic-schema"> <h4><a class="toc-backref" href="#id4" role="doc-backlink">Adding a new field to the classic schema</a></h4> <p>This example shows how to add a simple field (a due date) to the default classic schema. It does not add any additional behaviour, such as enforcing the due date, or causing automatic actions to fire if the due date passes.</p> <p>You add new fields by editing the <code class="docutils literal notranslate"><span class="pre">schema.py</span></code> file in you tracker’s home. Schema changes are automatically applied to the database on the next tracker access (note that roundup-server would need to be restarted as it caches the schema).</p> <ol class="arabic" id="index-0"> <li><p>Modify the <code class="docutils literal notranslate"><span class="pre">schema.py</span></code>:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">issue</span> <span class="o">=</span> <span class="n">IssueClass</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;issue&quot;</span><span class="p">,</span> <span class="n">assignedto</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;user&quot;</span><span class="p">),</span> <span class="n">keyword</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s2">&quot;keyword&quot;</span><span class="p">),</span> <span class="n">priority</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;priority&quot;</span><span class="p">),</span> <span class="n">status</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;status&quot;</span><span class="p">),</span> <span class="n">due_date</span><span class="o">=</span><span class="n">Date</span><span class="p">())</span> </pre></div> </div> </li> <li><p>Add an edit field to the <code class="docutils literal notranslate"><span class="pre">issue.item.html</span></code> template:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Due</span> <span class="n">Date</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;structure context/due_date/field&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> <p>If you want to show only the date part of due_date then do this instead:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Due</span> <span class="n">Date</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;structure python:context.due_date.field(format=&#39;%Y-%m-</span><span class="si">%d</span><span class="s2">&#39;)&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> </li> <li><p>Add the property to the <code class="docutils literal notranslate"><span class="pre">issue.index.html</span></code> page:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">(</span><span class="ow">in</span> <span class="n">the</span> <span class="n">heading</span> <span class="n">row</span><span class="p">)</span> <span class="o">&lt;</span><span class="n">th</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;request/show/due_date&quot;</span><span class="o">&gt;</span><span class="n">Due</span> <span class="n">Date</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="p">(</span><span class="ow">in</span> <span class="n">the</span> <span class="n">data</span> <span class="n">row</span><span class="p">)</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;request/show/due_date&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;i/due_date&quot;</span> <span class="o">/&gt;</span> </pre></div> </div> <p>If you want format control of the display of the due date you can enter the following in the data row to show only the actual due date:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;request/show/due_date&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;python:i.due_date.pretty(&#39;%Y-%m-</span><span class="si">%d</span><span class="s2">&#39;)&quot;</span><span class="o">&gt;&amp;</span><span class="n">nbsp</span><span class="p">;</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> </pre></div> </div> </li> <li><p>Add the property to the <code class="docutils literal notranslate"><span class="pre">issue.search.html</span></code> page:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span> <span class="n">tal</span><span class="p">:</span><span class="n">define</span><span class="o">=</span><span class="s2">&quot;name string:due_date&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span> <span class="n">i18n</span><span class="p">:</span><span class="n">translate</span><span class="o">=</span><span class="s2">&quot;&quot;</span><span class="o">&gt;</span><span class="n">Due</span> <span class="n">Date</span><span class="p">:</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">metal</span><span class="p">:</span><span class="n">use</span><span class="o">-</span><span class="n">macro</span><span class="o">=</span><span class="s2">&quot;search_input&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">metal</span><span class="p">:</span><span class="n">use</span><span class="o">-</span><span class="n">macro</span><span class="o">=</span><span class="s2">&quot;column_input&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">metal</span><span class="p">:</span><span class="n">use</span><span class="o">-</span><span class="n">macro</span><span class="o">=</span><span class="s2">&quot;sort_input&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">metal</span><span class="p">:</span><span class="n">use</span><span class="o">-</span><span class="n">macro</span><span class="o">=</span><span class="s2">&quot;group_input&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> </li> <li><p>If you wish for the due date to appear in the standard views listed in the sidebar of the web interface then you’ll need to add “due_date” to the columns and columns_showall lists in your <code class="docutils literal notranslate"><span class="pre">page.html</span></code>:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">columns</span> <span class="n">string</span><span class="p">:</span><span class="nb">id</span><span class="p">,</span><span class="n">activity</span><span class="p">,</span><span class="n">due_date</span><span class="p">,</span><span class="n">title</span><span class="p">,</span><span class="n">creator</span><span class="p">,</span><span class="n">status</span><span class="p">;</span> <span class="n">columns_showall</span> <span class="n">string</span><span class="p">:</span><span class="nb">id</span><span class="p">,</span><span class="n">activity</span><span class="p">,</span><span class="n">due_date</span><span class="p">,</span><span class="n">title</span><span class="p">,</span><span class="n">creator</span><span class="p">,</span><span class="n">assignedto</span><span class="p">,</span><span class="n">status</span><span class="p">;</span> </pre></div> </div> </li> </ol> </section> <section id="adding-a-new-constrained-field-to-the-classic-schema"> <h4><a class="toc-backref" href="#id5" role="doc-backlink">Adding a new constrained field to the classic schema</a></h4> <p>This example shows how to add a new constrained property (i.e. a selection of distinct values) to your tracker.</p> <section id="introduction"> <h5>Introduction</h5> <p>To make the classic schema of Roundup useful as a TODO tracking system for a group of systems administrators, it needs an extra data field per issue: a category.</p> <p>This would let sysadmins quickly list all TODOs in their particular area of interest without having to do complex queries, and without relying on the spelling capabilities of other sysadmins (a losing proposition at best).</p> </section> <section id="adding-a-field-to-the-database"> <h5>Adding a field to the database</h5> <p>This is the easiest part of the change. The category would just be a plain string, nothing fancy. To change what is in the database you need to add some lines to the <code class="docutils literal notranslate"><span class="pre">schema.py</span></code> file of your tracker instance. Under the comment:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># add any additional database schema configuration here</span> </pre></div> </div> <p>add:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">category</span> <span class="o">=</span> <span class="n">Class</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;category&quot;</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="n">String</span><span class="p">())</span> <span class="n">category</span><span class="o">.</span><span class="n">setkey</span><span class="p">(</span><span class="s2">&quot;name&quot;</span><span class="p">)</span> </pre></div> </div> <p>Here we are setting up a chunk of the database which we are calling “category”. It contains a string, which we are refering to as “name” for lack of a more imaginative title. (Since “name” is one of the properties that Roundup looks for on items if you do not set a key for them, it’s probably a good idea to stick with it for new classes if at all appropriate.) Then we are setting the key of this chunk of the database to be that “name”. This is equivalent to an index for database types. This also means that there can only be one category with a given name.</p> <p>Adding the above lines allows us to create categories, but they’re not tied to the issues that we are going to be creating. It’s just a list of categories off on its own, which isn’t much use. We need to link it in with the issues. To do that, find the lines in <code class="docutils literal notranslate"><span class="pre">schema.py</span></code> which set up the “issue” class, and then add a link to the category:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">issue</span> <span class="o">=</span> <span class="n">IssueClass</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;issue&quot;</span><span class="p">,</span> <span class="o">...</span> <span class="p">,</span> <span class="n">category</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s2">&quot;category&quot;</span><span class="p">),</span> <span class="o">...</span> <span class="p">)</span> </pre></div> </div> <p>The <code class="docutils literal notranslate"><span class="pre">Multilink()</span></code> means that each issue can have many categories. If you were adding something with a one-to-one relationship to issues (such as the “assignedto” property), use <code class="docutils literal notranslate"><span class="pre">Link()</span></code> instead.</p> <p>That is all you need to do to change the schema. The rest of the effort is fiddling around so you can actually use the new category.</p> </section> <section id="populating-the-new-category-class"> <h5>Populating the new category class</h5> <p>If you haven’t initialised the database with the “<code class="docutils literal notranslate"><span class="pre">roundup-admin</span> <span class="pre">initialise</span></code>” command, then you can add the following to the tracker <code class="docutils literal notranslate"><span class="pre">initial_data.py</span></code> under the comment:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># add any additional database creation steps here - but only if you</span> <span class="c1"># haven&#39;t initialised the database with the admin &quot;initialise&quot; command</span> </pre></div> </div> <p>Add:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">category</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">getclass</span><span class="p">(</span><span class="s1">&#39;category&#39;</span><span class="p">)</span> <span class="n">category</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">&quot;scipy&quot;</span><span class="p">)</span> <span class="n">category</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">&quot;chaco&quot;</span><span class="p">)</span> <span class="n">category</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">&quot;weave&quot;</span><span class="p">)</span> </pre></div> </div> <p id="index-1">If the database has already been initalised, then you need to use the <code class="docutils literal notranslate"><span class="pre">roundup-admin</span></code> tool:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span>% roundup-admin -i &lt;tracker home&gt; Roundup &lt;version&gt; ready for input. Type &quot;help&quot; for help. roundup&gt; create category name=scipy 1 roundup&gt; create category name=chaco 2 roundup&gt; create category name=weave 3 roundup&gt; exit... There are unsaved changes. Commit them (y/N)? y </pre></div> </div> </section> <section id="setting-up-security-on-the-new-objects"> <h5>Setting up security on the new objects</h5> <p>By default only the admin user can look at and change objects. This doesn’t suit us, as we want any user to be able to create new categories as required, and obviously everyone needs to be able to view the categories of issues for it to be useful.</p> <p>We therefore need to change the security of the category objects. This is also done in <code class="docutils literal notranslate"><span class="pre">schema.py</span></code>.</p> <p>There are currently two loops which set up permissions and then assign them to various roles. Simply add the new “category” to both lists:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Assign the access and edit permissions for issue, file and message</span> <span class="c1"># to regular users now</span> <span class="k">for</span> <span class="n">cl</span> <span class="ow">in</span> <span class="s1">&#39;issue&#39;</span><span class="p">,</span> <span class="s1">&#39;file&#39;</span><span class="p">,</span> <span class="s1">&#39;msg&#39;</span><span class="p">,</span> <span class="s1">&#39;category&#39;</span><span class="p">:</span> <span class="n">p</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">getPermission</span><span class="p">(</span><span class="s1">&#39;View&#39;</span><span class="p">,</span> <span class="n">cl</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="s1">&#39;View&#39;</span><span class="p">,</span> <span class="n">cl</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="s1">&#39;Edit&#39;</span><span class="p">,</span> <span class="n">cl</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="s1">&#39;Create&#39;</span><span class="p">,</span> <span class="n">cl</span><span class="p">)</span> </pre></div> </div> <p>These lines assign the “View” and “Edit” Permissions to the “User” role, so that normal users can view and edit “category” objects.</p> <p>This is all the work that needs to be done for the database. It will store categories, and let users view and edit them. Now on to the interface stuff.</p> </section> <section id="changing-the-web-left-hand-frame"> <h5>Changing the web left hand frame</h5> <p>We need to give the users the ability to create new categories, and the place to put the link to this functionality is in the left hand function bar, under the “Issues” area. The file that defines how this area looks is <code class="docutils literal notranslate"><span class="pre">html/page.html</span></code>, which is what we are going to be editing next.</p> <p>If you look at this file you can see that it contains a lot of “classblock” sections which are chunks of HTML that will be included or excluded in the output depending on whether the condition in the classblock is met. We are going to add the category code at the end of the classblock for the <em>issue</em> class:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">p</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;classblock&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;python:request.user.hasPermission(&#39;View&#39;, &#39;category&#39;)&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">b</span><span class="o">&gt;</span><span class="n">Categories</span><span class="o">&lt;/</span><span class="n">b</span><span class="o">&gt;&lt;</span><span class="n">br</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">a</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;python:request.user.hasPermission(&#39;Edit&#39;, &#39;category&#39;)&quot;</span> <span class="n">href</span><span class="o">=</span><span class="s2">&quot;category?@template=item&quot;</span><span class="o">&gt;</span><span class="n">New</span> <span class="n">Category</span><span class="o">&lt;</span><span class="n">br</span><span class="o">&gt;&lt;/</span><span class="n">a</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">p</span><span class="o">&gt;</span> </pre></div> </div> <p>The first two lines is the classblock definition, which sets up a condition that only users who have “View” permission for the “category” object will have this section included in their output. Next comes a plain “Categories” header in bold. Everyone who can view categories will get that.</p> <p>Next comes the link to the editing area of categories. This link will only appear if the condition - that the user has “Edit” permissions for the “category” objects - is matched. If they do have permission then they will get a link to another page which will let the user add new categories.</p> <p>Note that if you have permission to <em>view</em> but not to <em>edit</em> categories, then all you will see is a “Categories” header with nothing underneath it. This is obviously not very good interface design, but will do for now. I just claim that it is so I can add more links in this section later on. However, to fix the problem you could change the condition in the classblock statement, so that only users with “Edit” permission would see the “Categories” stuff.</p> </section> <section id="setting-up-a-page-to-edit-categories"> <h5>Setting up a page to edit categories</h5> <p>We defined code in the previous section which let users with the appropriate permissions see a link to a page which would let them edit conditions. Now we have to write that page.</p> <p>The link was for the <em>item</em> template of the <em>category</em> object. This translates into Roundup looking for a file called <code class="docutils literal notranslate"><span class="pre">category.item.html</span></code> in the <code class="docutils literal notranslate"><span class="pre">html</span></code> tracker directory. This is the file that we are going to write now.</p> <p>First, we add an info tag in a comment which doesn’t affect the outcome of the code at all, but is useful for debugging. If you load a page in a browser and look at the page source, you can see which sections come from which files by looking for these comments:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span>&lt;!-- category.item --&gt; </pre></div> </div> <p>Next we need to add in the METAL macro stuff so we get the normal page trappings:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span> <span class="n">metal</span><span class="p">:</span><span class="n">use</span><span class="o">-</span><span class="n">macro</span><span class="o">=</span><span class="s2">&quot;templates/page/macros/icing&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">title</span> <span class="n">metal</span><span class="p">:</span><span class="n">fill</span><span class="o">-</span><span class="n">slot</span><span class="o">=</span><span class="s2">&quot;head_title&quot;</span><span class="o">&gt;</span><span class="n">Category</span> <span class="n">editing</span><span class="o">&lt;/</span><span class="n">title</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;page-header-top&quot;</span> <span class="n">metal</span><span class="p">:</span><span class="n">fill</span><span class="o">-</span><span class="n">slot</span><span class="o">=</span><span class="s2">&quot;body_title&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">h2</span><span class="o">&gt;</span><span class="n">Category</span> <span class="n">editing</span><span class="o">&lt;/</span><span class="n">h2</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;content&quot;</span> <span class="n">metal</span><span class="p">:</span><span class="n">fill</span><span class="o">-</span><span class="n">slot</span><span class="o">=</span><span class="s2">&quot;content&quot;</span><span class="o">&gt;</span> </pre></div> </div> <p>Next we need to setup up a standard HTML form, which is the whole purpose of this file. We link to some handy javascript which sends the form through only once. This is to stop users hitting the send button multiple times when they are impatient and thus having the form sent multiple times:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">form</span> <span class="n">method</span><span class="o">=</span><span class="s2">&quot;POST&quot;</span> <span class="n">onSubmit</span><span class="o">=</span><span class="s2">&quot;return submit_once()&quot;</span> <span class="n">enctype</span><span class="o">=</span><span class="s2">&quot;multipart/form-data&quot;</span><span class="o">&gt;</span> </pre></div> </div> <p>Next we define some code which sets up the minimum list of fields that we require the user to enter. There will be only one field - “name” - so they better put something in it, otherwise the whole form is pointless:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@required&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;name&quot;</span><span class="o">&gt;</span> </pre></div> </div> <p>To get everything to line up properly we will put everything in a table, and put a nice big header on it so the user has an idea what is happening:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">table</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;form&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;&lt;</span><span class="n">th</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;header&quot;</span> <span class="n">colspan</span><span class="o">=</span><span class="s2">&quot;2&quot;</span><span class="o">&gt;</span><span class="n">Category</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> <p>Next, we need the field into which the user is going to enter the new category. The <code class="docutils literal notranslate"><span class="pre">context.name.field(size=60)</span></code> bit tells Roundup to generate a normal HTML field of size 60, and the contents of that field will be the “name” variable of the current context (namely “category”). The upshot of this is that when the user types something in to the form, a new category will be created with that name:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Name</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;structure python:context.name.field(size=60)&quot;</span><span class="o">&gt;</span> <span class="n">name</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> <p>Then a submit button so that the user can submit the new category:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;&amp;</span><span class="n">nbsp</span><span class="p">;</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">colspan</span><span class="o">=</span><span class="s2">&quot;3&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;structure context/submit&quot;</span><span class="o">&gt;</span> <span class="n">submit</span> <span class="n">button</span> <span class="n">will</span> <span class="n">go</span> <span class="n">here</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> <p>The <code class="docutils literal notranslate"><span class="pre">context/submit</span></code> bit generates the submit button but also generates the &#64;action and &#64;csrf hidden fields. The &#64;action field is used to tell Roundup how to process the form. The &#64;csrf field provides a unique single use token to defend against CSRF attacks. (More about anti-csrf measures can be found in <code class="docutils literal notranslate"><span class="pre">upgrading.txt</span></code>.)</p> <p>Finally we finish off the tags we used at the start to do the METAL stuff:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span><span class="o">&gt;</span> </pre></div> </div> <p>So putting it all together, and closing the table and form we get:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span>&lt;!-- category.item --&gt; &lt;tal:block metal:use-macro=&quot;templates/page/macros/icing&quot;&gt; &lt;title metal:fill-slot=&quot;head_title&quot;&gt;Category editing&lt;/title&gt; &lt;td class=&quot;page-header-top&quot; metal:fill-slot=&quot;body_title&quot;&gt; &lt;h2&gt;Category editing&lt;/h2&gt; &lt;/td&gt; &lt;td class=&quot;content&quot; metal:fill-slot=&quot;content&quot;&gt; &lt;form method=&quot;POST&quot; onSubmit=&quot;return submit_once()&quot; enctype=&quot;multipart/form-data&quot;&gt; &lt;table class=&quot;form&quot;&gt; &lt;tr&gt;&lt;th class=&quot;header&quot; colspan=&quot;2&quot;&gt;Category&lt;/th&gt;&lt;/tr&gt; &lt;tr&gt; &lt;th&gt;Name&lt;/th&gt; &lt;td tal:content=&quot;structure python:context.name.field(size=60)&quot;&gt; name&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt; &amp;nbsp; &lt;input type=&quot;hidden&quot; name=&quot;@required&quot; value=&quot;name&quot;&gt; &lt;/td&gt; &lt;td colspan=&quot;3&quot; tal:content=&quot;structure context/submit&quot;&gt; submit button will go here &lt;/td&gt; &lt;/tr&gt; &lt;/table&gt; &lt;/form&gt; &lt;/td&gt; &lt;/tal:block&gt; </pre></div> </div> <p>This is quite a lot to just ask the user one simple question, but there is a lot of setup for basically one line (the form line) to do its work. To add another field to “category” would involve one more line (well, maybe a few extra to get the formatting correct).</p> </section> <section id="adding-the-category-to-the-issue"> <h5>Adding the category to the issue</h5> <p>We now have the ability to create issues to our heart’s content, but that is pointless unless we can assign categories to issues. Just like the <code class="docutils literal notranslate"><span class="pre">html/category.item.html</span></code> file was used to define how to add a new category, the <code class="docutils literal notranslate"><span class="pre">html/issue.item.html</span></code> is used to define how a new issue is created.</p> <p>Just like <code class="docutils literal notranslate"><span class="pre">category.issue.html</span></code>, this file defines a form which has a table to lay things out. It doesn’t matter where in the table we add new stuff, it is entirely up to your sense of aesthetics:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Category</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">span</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;structure context/category/field&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="n">span</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;structure python:db.category.classhelp(&#39;name&#39;,</span> <span class="nb">property</span><span class="o">=</span><span class="s1">&#39;category&#39;</span><span class="p">,</span> <span class="n">width</span><span class="o">=</span><span class="s1">&#39;200&#39;</span><span class="p">)</span><span class="s2">&quot; /&gt;</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> </pre></div> </div> <p>First, we define a nice header so that the user knows what the next section is, then the middle line does what we are most interested in. This <code class="docutils literal notranslate"><span class="pre">context/category/field</span></code> gets replaced by a field which contains the category in the current context (the current context being the new issue).</p> <p>The classhelp lines generate a link (labelled “list”) to a popup window which contains the list of currently known categories.</p> </section> <section id="searching-on-categories"> <h5>Searching on categories</h5> <p>Now we can add categories, and create issues with categories. The next obvious thing that we would like to be able to do, would be to search for issues based on their category, so that, for example, anyone working on the web server could look at all issues in the category “Web”.</p> <p>If you look for “Search Issues” in the <code class="docutils literal notranslate"><span class="pre">html/page.html</span></code> file, you will find that it looks something like <code class="docutils literal notranslate"><span class="pre">&lt;a</span> <span class="pre">href=&quot;issue?&#64;template=search&quot;&gt;Search</span> <span class="pre">Issues&lt;/a&gt;</span></code>. This shows us that when you click on “Search Issues” it will be looking for a <code class="docutils literal notranslate"><span class="pre">issue.search.html</span></code> file to display. So that is the file that we will change.</p> <p>If you look at this file it should begin to seem familiar, although it does use some new macros. You can add the new category search code anywhere you like within that form:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span> <span class="n">tal</span><span class="p">:</span><span class="n">define</span><span class="o">=</span><span class="s2">&quot;name string:category;</span> <span class="n">db_klass</span> <span class="n">string</span><span class="p">:</span><span class="n">category</span><span class="p">;</span> <span class="n">db_content</span> <span class="n">string</span><span class="p">:</span><span class="n">name</span><span class="p">;</span><span class="s2">&quot;&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Priority</span><span class="p">:</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">metal</span><span class="p">:</span><span class="n">use</span><span class="o">-</span><span class="n">macro</span><span class="o">=</span><span class="s2">&quot;search_select&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">metal</span><span class="p">:</span><span class="n">use</span><span class="o">-</span><span class="n">macro</span><span class="o">=</span><span class="s2">&quot;column_input&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">metal</span><span class="p">:</span><span class="n">use</span><span class="o">-</span><span class="n">macro</span><span class="o">=</span><span class="s2">&quot;sort_input&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">metal</span><span class="p">:</span><span class="n">use</span><span class="o">-</span><span class="n">macro</span><span class="o">=</span><span class="s2">&quot;group_input&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> <p>The definitions in the <code class="docutils literal notranslate"><span class="pre">&lt;tr&gt;</span></code> opening tag are used by the macros:</p> <ul class="simple"> <li><p><code class="docutils literal notranslate"><span class="pre">search_select</span></code> expands to a drop-down box with all categories using <code class="docutils literal notranslate"><span class="pre">db_klass</span></code> and <code class="docutils literal notranslate"><span class="pre">db_content</span></code>.</p></li> <li><p><code class="docutils literal notranslate"><span class="pre">column_input</span></code> expands to a checkbox for selecting what columns should be displayed.</p></li> <li><p><code class="docutils literal notranslate"><span class="pre">sort_input</span></code> expands to a radio button for selecting what property should be sorted on.</p></li> <li><p><code class="docutils literal notranslate"><span class="pre">group_input</span></code> expands to a radio button for selecting what property should be grouped on.</p></li> </ul> <p>The category search code above would expand to the following:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Category</span><span class="p">:</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">select</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;category&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">option</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;&quot;</span><span class="o">&gt;</span><span class="n">don</span><span class="s1">&#39;t care&lt;/option&gt;</span> <span class="o">&lt;</span><span class="n">option</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;&quot;</span><span class="o">&gt;------------&lt;/</span><span class="n">option</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">option</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;1&quot;</span><span class="o">&gt;</span><span class="n">scipy</span><span class="o">&lt;/</span><span class="n">option</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">option</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;2&quot;</span><span class="o">&gt;</span><span class="n">chaco</span><span class="o">&lt;/</span><span class="n">option</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">option</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;3&quot;</span><span class="o">&gt;</span><span class="n">weave</span><span class="o">&lt;/</span><span class="n">option</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">select</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;checkbox&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;:columns&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;category&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;radio&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;:sort0&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;category&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;radio&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;:group0&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;category&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> </section> <section id="adding-category-to-the-default-view"> <h5>Adding category to the default view</h5> <p>We can now add categories, add issues with categories, and search for issues based on categories. This is everything that we need to do; however, there is some more icing that we would like. I think the category of an issue is important enough that it should be displayed by default when listing all the issues.</p> <p>Unfortunately, this is a bit less obvious than the previous steps. The code defining how the issues look is in <code class="docutils literal notranslate"><span class="pre">html/issue.index.html</span></code>. This is a large table with a form down at the bottom for redisplaying and so forth.</p> <p>Firstly we need to add an appropriate header to the start of the table:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">th</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;request/show/category&quot;</span><span class="o">&gt;</span><span class="n">Category</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> </pre></div> </div> <p>The <em>condition</em> part of this statement is to avoid displaying the Category column if the user has selected not to see it.</p> <p>The rest of the table is a loop which will go through every issue that matches the display criteria. The loop variable is “i” - which means that every issue gets assigned to “i” in turn.</p> <p>The new part of code to display the category will look like this:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;request/show/category&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;i/category&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> </pre></div> </div> <p>The condition is the same as above: only display the condition when the user hasn’t asked for it to be hidden. The next part is to set the content of the cell to be the category part of “i” - the current issue.</p> <p>Finally we have to edit <code class="docutils literal notranslate"><span class="pre">html/page.html</span></code> again. This time, we need to tell it that when the user clicks on “Unassigned Issues” or “All Issues”, the category column should be included in the resulting list. If you scroll down the page file, you can see the links with lots of options. The option that we are interested in is the <code class="docutils literal notranslate"><span class="pre">:columns=</span></code> one which tells Roundup which fields of the issue to display. Simply add “category” to that list and it all should work.</p> </section> </section> <section id="adding-a-time-log-to-your-issues"> <h4><a class="toc-backref" href="#id6" role="doc-backlink">Adding a time log to your issues</a></h4> <p>We want to log the dates and amount of time spent working on issues, and be able to give a summary of the total time spent on a particular issue.</p> <ol class="arabic"> <li><p>Add a new class to your tracker <code class="docutils literal notranslate"><span class="pre">schema.py</span></code>:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># storage for time logging</span> <span class="n">timelog</span> <span class="o">=</span> <span class="n">Class</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;timelog&quot;</span><span class="p">,</span> <span class="n">period</span><span class="o">=</span><span class="n">Interval</span><span class="p">())</span> </pre></div> </div> <p>Note that we automatically get the date of the time log entry creation through the standard property “creation”.</p> <p>You will need to grant “Creation” permission to the users who are allowed to add timelog entries. You may do this with:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="s1">&#39;Create&#39;</span><span class="p">,</span> <span class="s1">&#39;timelog&#39;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="s1">&#39;View&#39;</span><span class="p">,</span> <span class="s1">&#39;timelog&#39;</span><span class="p">)</span> </pre></div> </div> <p>If users are also able to <em>edit</em> timelog entries, then also include:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="s1">&#39;Edit&#39;</span><span class="p">,</span> <span class="s1">&#39;timelog&#39;</span><span class="p">)</span> </pre></div> </div> </li> </ol> <ol class="arabic" id="index-2" start="2"> <li><p>Link to the new class from your issue class (again, in <code class="docutils literal notranslate"><span class="pre">schema.py</span></code>):</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">issue</span> <span class="o">=</span> <span class="n">IssueClass</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;issue&quot;</span><span class="p">,</span> <span class="n">assignedto</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;user&quot;</span><span class="p">),</span> <span class="n">keyword</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s2">&quot;keyword&quot;</span><span class="p">),</span> <span class="n">priority</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;priority&quot;</span><span class="p">),</span> <span class="n">status</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;status&quot;</span><span class="p">),</span> <span class="n">times</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s2">&quot;timelog&quot;</span><span class="p">))</span> </pre></div> </div> <p>the “times” property is the new link to the “timelog” class.</p> </li> <li><p>We’ll need to let people add in times to the issue, so in the web interface we’ll have a new entry field. This is a special field because unlike the other fields in the <code class="docutils literal notranslate"><span class="pre">issue.item</span></code> template, it affects a different item (a timelog item) and not the template’s item (an issue). We have a special syntax for form fields that affect items other than the template default item (see the cgi documentation on <a class="reference external" href="reference.html#special-form-variables">special form variables</a>). In particular, we add a field to capture a new timelog item’s period:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Time</span> <span class="n">Log</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">colspan</span><span class="o">=</span><span class="mi">3</span><span class="o">&gt;&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;text&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;timelog-1@period&quot;</span> <span class="o">/&gt;</span> <span class="p">(</span><span class="n">enter</span> <span class="k">as</span> <span class="s1">&#39;3y 1m 4d 2:40:02&#39;</span> <span class="ow">or</span> <span class="n">parts</span> <span class="n">thereof</span><span class="p">)</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> <p>and another hidden field that links that new timelog item (new because it’s marked as having id “-1”) to the issue item. It looks like this:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@link@times&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;timelog-1&quot;</span> <span class="o">/&gt;</span> </pre></div> </div> <p>On submission, the “-1” timelog item will be created and assigned a real item id. The “times” property of the issue will have the new id added to it.</p> <p>The full entry will now look like this:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Time</span> <span class="n">Log</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">colspan</span><span class="o">=</span><span class="mi">3</span><span class="o">&gt;&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;text&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;timelog-1@period&quot;</span> <span class="o">/&gt;</span> <span class="p">(</span><span class="n">enter</span> <span class="k">as</span> <span class="s1">&#39;3y 1m 4d 2:40:02&#39;</span> <span class="ow">or</span> <span class="n">parts</span> <span class="n">thereof</span><span class="p">)</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@link@times&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;timelog-1&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> </li> </ol> <ol class="arabic" id="adding-a-time-log-to-your-issues-4" start="4"> <li><p>We want to display a total of the timelog times that have been accumulated for an issue. To do this, we’ll need to actually write some Python code, since it’s beyond the scope of PageTemplates to perform such calculations. We do this by adding a module <code class="docutils literal notranslate"><span class="pre">timespent.py</span></code> to the <code class="docutils literal notranslate"><span class="pre">extensions</span></code> directory in our tracker. The contents of this file is as follows:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">roundup</span> <span class="kn">import</span> <span class="n">date</span> <span class="k">def</span> <span class="nf">totalTimeSpent</span><span class="p">(</span><span class="n">times</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39; Call me with a list of timelog items (which have an</span> <span class="sd"> Interval &quot;period&quot; property)</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="n">total</span> <span class="o">=</span> <span class="n">date</span><span class="o">.</span><span class="n">Interval</span><span class="p">(</span><span class="s1">&#39;0d&#39;</span><span class="p">)</span> <span class="k">for</span> <span class="n">time</span> <span class="ow">in</span> <span class="n">times</span><span class="p">:</span> <span class="n">total</span> <span class="o">+=</span> <span class="n">time</span><span class="o">.</span><span class="n">period</span><span class="o">.</span><span class="n">_value</span> <span class="k">return</span> <span class="n">total</span> <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">instance</span><span class="p">):</span> <span class="n">instance</span><span class="o">.</span><span class="n">registerUtil</span><span class="p">(</span><span class="s1">&#39;totalTimeSpent&#39;</span><span class="p">,</span> <span class="n">totalTimeSpent</span><span class="p">)</span> </pre></div> </div> <p>We will now be able to access the <code class="docutils literal notranslate"><span class="pre">totalTimeSpent</span></code> function via the <code class="docutils literal notranslate"><span class="pre">utils</span></code> variable in our templates, as shown in the next step.</p> </li> <li><p>Display the timelog for an issue:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">table</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;otherinfo&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;context/times&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;&lt;</span><span class="n">th</span> <span class="n">colspan</span><span class="o">=</span><span class="s2">&quot;3&quot;</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;header&quot;</span><span class="o">&gt;</span><span class="n">Time</span> <span class="n">Log</span> <span class="o">&lt;</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;python:utils.totalTimeSpent(context.times)&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Date</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Period</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Logged</span> <span class="n">By</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span> <span class="n">tal</span><span class="p">:</span><span class="n">repeat</span><span class="o">=</span><span class="s2">&quot;time context/times&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;time/creation&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;time/period&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;time/creator&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">table</span><span class="o">&gt;</span> </pre></div> </div> <p>I put this just above the Messages log in my issue display. Note our use of the <code class="docutils literal notranslate"><span class="pre">totalTimeSpent</span></code> method which will total up the times for the issue and return a new Interval. That will be automatically displayed in the template as text like “+ 1y 2:40” (1 year, 2 hours and 40 minutes).</p> </li> <li><p>If you’re using a persistent web server - <code class="docutils literal notranslate"><span class="pre">roundup-server</span></code> or <code class="docutils literal notranslate"><span class="pre">mod_wsgi</span></code> for example - then you’ll need to restart that to pick up the code changes. When that’s done, you’ll be able to use the new time logging interface.</p></li> </ol> <p>An extension of this modification attaches the timelog entries to any change message entered at the time of the timelog entry:</p> <ol class="upperalpha"> <li><p>Add a link to the timelog to the msg class in <code class="docutils literal notranslate"><span class="pre">schema.py</span></code>:</p> <blockquote> <div><dl class="simple"> <dt>msg = FileClass(db, “msg”,</dt><dd><p>author=Link(“user”, do_journal=’no’), recipients=Multilink(“user”, do_journal=’no’), date=Date(), summary=String(), files=Multilink(“file”), messageid=String(), inreplyto=String(), times=Multilink(“timelog”))</p> </dd> </dl> </div></blockquote> </li> <li><p>Add a new hidden field that links that new timelog item (new because it’s marked as having id “-1”) to the new message. The link is placed in <code class="docutils literal notranslate"><span class="pre">issue.item.html</span></code> in the same section that handles the timelog entry.</p> <p>It looks like this after this addition:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Time</span> <span class="n">Log</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">colspan</span><span class="o">=</span><span class="mi">3</span><span class="o">&gt;&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;text&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;timelog-1@period&quot;</span> <span class="o">/&gt;</span> <span class="p">(</span><span class="n">enter</span> <span class="k">as</span> <span class="s1">&#39;3y 1m 4d 2:40:02&#39;</span> <span class="ow">or</span> <span class="n">parts</span> <span class="n">thereof</span><span class="p">)</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@link@times&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;timelog-1&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;msg-1@link@times&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;timelog-1&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> <p>The “times” property of the message will have the new id added to it.</p> </li> <li><p>Add the timelog listing from step 5. to the <code class="docutils literal notranslate"><span class="pre">msg.item.html</span></code> template so that the timelog entry appears on the message view page. Note that the call to totalTimeSpent is not used here since there will only be one single timelog entry for each message.</p> <p>I placed it after the Date entry like this:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span> <span class="n">i18n</span><span class="p">:</span><span class="n">translate</span><span class="o">=</span><span class="s2">&quot;&quot;</span><span class="o">&gt;</span><span class="n">Date</span><span class="p">:</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;context/date&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">table</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">table</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;otherinfo&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;context/times&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;&lt;</span><span class="n">th</span> <span class="n">colspan</span><span class="o">=</span><span class="s2">&quot;3&quot;</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;header&quot;</span><span class="o">&gt;</span><span class="n">Time</span> <span class="n">Log</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Date</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Period</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Logged</span> <span class="n">By</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span> <span class="n">tal</span><span class="p">:</span><span class="n">repeat</span><span class="o">=</span><span class="s2">&quot;time context/times&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;time/creation&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;time/period&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;time/creator&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">table</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">table</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;messages&quot;</span><span class="o">&gt;</span> </pre></div> </div> </li> </ol> <p>The wiki includes an <a class="reference external" href="https://wiki.roundup-tracker.org/TimelogAuditor">auditor that extracts specially formatted timelog entries from emails</a> sent to the tracker.</p> </section> <section id="tracking-different-types-of-issues"> <h4><a class="toc-backref" href="#id7" role="doc-backlink">Tracking different types of issues</a></h4> <p>Sometimes you will want to track different types of issues - developer, customer support, systems, sales leads, etc. A single Roundup tracker is able to support multiple types of issues. This example demonstrates adding a system support issue class to a tracker.</p> <ol class="arabic"> <li><p>Figure out what information you’re going to want to capture. OK, so this is obvious, but sometimes it’s better to actually sit down for a while and think about the schema you’re going to implement.</p></li> <li><p>Add the new issue class to your tracker’s <code class="docutils literal notranslate"><span class="pre">schema.py</span></code>. Just after the “issue” class definition, add:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># list our systems</span> <span class="n">system</span> <span class="o">=</span> <span class="n">Class</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;system&quot;</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">order</span><span class="o">=</span><span class="n">Number</span><span class="p">())</span> <span class="n">system</span><span class="o">.</span><span class="n">setkey</span><span class="p">(</span><span class="s2">&quot;name&quot;</span><span class="p">)</span> <span class="c1"># store issues related to those systems</span> <span class="n">support</span> <span class="o">=</span> <span class="n">IssueClass</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;support&quot;</span><span class="p">,</span> <span class="n">assignedto</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;user&quot;</span><span class="p">),</span> <span class="n">keyword</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s2">&quot;keyword&quot;</span><span class="p">),</span> <span class="n">status</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;status&quot;</span><span class="p">),</span> <span class="n">deadline</span><span class="o">=</span><span class="n">Date</span><span class="p">(),</span> <span class="n">affects</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s2">&quot;system&quot;</span><span class="p">))</span> </pre></div> </div> </li> <li><p>Copy the existing <code class="docutils literal notranslate"><span class="pre">issue.*</span></code> (item, search and index) templates in the tracker’s <code class="docutils literal notranslate"><span class="pre">html</span></code> to <code class="docutils literal notranslate"><span class="pre">support.*</span></code>. Edit them so they use the properties defined in the <code class="docutils literal notranslate"><span class="pre">support</span></code> class. Be sure to check for hidden form variables like “required” to make sure they have the correct set of required properties.</p></li> <li><p>Edit the modules in the <code class="docutils literal notranslate"><span class="pre">detectors</span></code>, adding lines to their <code class="docutils literal notranslate"><span class="pre">init</span></code> functions where appropriate. Look for <code class="docutils literal notranslate"><span class="pre">audit</span></code> and <code class="docutils literal notranslate"><span class="pre">react</span></code> registrations on the <code class="docutils literal notranslate"><span class="pre">issue</span></code> class, and duplicate them for <code class="docutils literal notranslate"><span class="pre">support</span></code>.</p></li> <li><p>Create a new sidebar box for the new support class. Duplicate the existing issues one, changing the <code class="docutils literal notranslate"><span class="pre">issue</span></code> class name to <code class="docutils literal notranslate"><span class="pre">support</span></code>.</p></li> <li><p>Re-start your tracker and start using the new <code class="docutils literal notranslate"><span class="pre">support</span></code> class.</p></li> </ol> <p>Optionally, you might want to restrict the users able to access this new class to just the users with a new “SysAdmin” Role. To do this, we add some security declarations:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;SysAdmin&#39;</span><span class="p">,</span> <span class="s1">&#39;View&#39;</span><span class="p">,</span> <span class="s1">&#39;support&#39;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;SysAdmin&#39;</span><span class="p">,</span> <span class="s1">&#39;Create&#39;</span><span class="p">,</span> <span class="s1">&#39;support&#39;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;SysAdmin&#39;</span><span class="p">,</span> <span class="s1">&#39;Edit&#39;</span><span class="p">,</span> <span class="s1">&#39;support&#39;</span><span class="p">)</span> </pre></div> </div> <p>You would then (as an “admin” user) edit the details of the appropriate users, and add “SysAdmin” to their Roles list.</p> <p>Alternatively, you might want to change the Edit/View permissions granted for the <code class="docutils literal notranslate"><span class="pre">issue</span></code> class so that it’s only available to users with the “System” or “Developer” Role, and then the new class you’re adding is available to all with the “User” Role.</p> </section> </section> <section id="using-external-user-databases"> <span id="external-authentication"></span><h3><a class="toc-backref" href="#id8" role="doc-backlink">Using External User Databases</a></h3> <section id="using-an-external-password-validation-source"> <h4><a class="toc-backref" href="#id9" role="doc-backlink">Using an external password validation source</a></h4> <div class="admonition note"> <p class="admonition-title">Note</p> <p>You will need to either have an “admin” user in your external password source <em>or</em> have one of your regular users have the Admin Role assigned. If you need to assign the Role <em>after</em> making the changes below, you may use the <code class="docutils literal notranslate"><span class="pre">roundup-admin</span></code> program to edit a user’s details.</p> </div> <p>We have a centrally-managed password changing system for our users. This results in a UN*X passwd-style file that we use for verification of users. Entries in the file consist of <code class="docutils literal notranslate"><span class="pre">name:password</span></code> where the password is encrypted using the standard UN*X <code class="docutils literal notranslate"><span class="pre">crypt()</span></code> function (see the <code class="docutils literal notranslate"><span class="pre">crypt</span></code> module in your Python distribution). An example entry would be:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">admin</span><span class="p">:</span><span class="n">aamrgyQfDFSHw</span> </pre></div> </div> <p>Each user of Roundup must still have their information stored in the Roundup database - we just use the passwd file to check their password. To do this, we need to override the standard <code class="docutils literal notranslate"><span class="pre">verifyPassword</span></code> method defined in <code class="docutils literal notranslate"><span class="pre">roundup.cgi.actions.LoginAction</span></code> and register the new class. The following is added as <code class="docutils literal notranslate"><span class="pre">externalpassword.py</span></code> in the tracker <code class="docutils literal notranslate"><span class="pre">extensions</span></code> directory:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">os</span><span class="o">,</span> <span class="nn">crypt</span> <span class="kn">from</span> <span class="nn">roundup.cgi.actions</span> <span class="kn">import</span> <span class="n">LoginAction</span> <span class="k">class</span> <span class="nc">ExternalPasswordLoginAction</span><span class="p">(</span><span class="n">LoginAction</span><span class="p">):</span> <span class="k">def</span> <span class="nf">verifyPassword</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">userid</span><span class="p">,</span> <span class="n">password</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39;Look through the file, line by line, looking for a</span> <span class="sd"> name that matches.</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="c1"># get the user&#39;s username</span> <span class="n">username</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">userid</span><span class="p">,</span> <span class="s1">&#39;username&#39;</span><span class="p">)</span> <span class="c1"># the passwords are stored in the &quot;passwd.txt&quot; file in the</span> <span class="c1"># tracker home</span> <span class="n">file</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">TRACKER_HOME</span><span class="p">,</span> <span class="s1">&#39;passwd.txt&#39;</span><span class="p">)</span> <span class="c1"># see if we can find a match</span> <span class="k">for</span> <span class="n">ent</span> <span class="ow">in</span> <span class="p">[</span><span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">&#39;:&#39;</span><span class="p">)</span> <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="nb">open</span><span class="p">(</span><span class="n">file</span><span class="p">)</span><span class="o">.</span><span class="n">readlines</span><span class="p">()]:</span> <span class="k">if</span> <span class="n">ent</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="n">username</span><span class="p">:</span> <span class="k">return</span> <span class="n">crypt</span><span class="o">.</span><span class="n">crypt</span><span class="p">(</span><span class="n">password</span><span class="p">,</span> <span class="n">ent</span><span class="p">[</span><span class="mi">1</span><span class="p">][:</span><span class="mi">2</span><span class="p">])</span> <span class="o">==</span> <span class="n">ent</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="c1"># user doesn&#39;t exist in the file</span> <span class="k">return</span> <span class="mi">0</span> <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">instance</span><span class="p">):</span> <span class="n">instance</span><span class="o">.</span><span class="n">registerAction</span><span class="p">(</span><span class="s1">&#39;login&#39;</span><span class="p">,</span> <span class="n">ExternalPasswordLoginAction</span><span class="p">)</span> </pre></div> </div> <p>You should also remove the redundant password fields from the <code class="docutils literal notranslate"><span class="pre">user.item</span></code> template.</p> </section> <section id="using-a-un-x-passwd-file-as-the-user-database"> <h4><a class="toc-backref" href="#id10" role="doc-backlink">Using a UN*X passwd file as the user database</a></h4> <p>On some systems the primary store of users is the UN*X passwd file. It holds information on users such as their username, real name, password and primary user group.</p> <p>Roundup can use this store as its primary source of user information, but it needs additional information too - email address(es), Roundup Roles, vacation flags, Roundup hyperdb item ids, etc. Also, “retired” users must still exist in the user database, unlike some passwd files in which the users are removed when they no longer have access to a system.</p> <p>To make use of the passwd file, we therefore synchronise between the two user stores. We also use the passwd file to validate the user logins, as described in the previous example, <a class="reference internal" href="#using-an-external-password-validation-source">using an external password validation source</a>. We keep the user lists in sync using a fairly simple script that runs once a day, or several times an hour if more immediate access is needed. In short, it:</p> <ol class="arabic simple"> <li><p>parses the passwd file, finding usernames, passwords and real names,</p></li> <li><p>compares that list to the current Roundup user list:</p> <ol class="loweralpha simple"> <li><p>entries no longer in the passwd file are <em>retired</em></p></li> <li><p>entries with mismatching real names are <em>updated</em></p></li> <li><p>entries only exist in the passwd file are <em>created</em></p></li> </ol> </li> <li><p>send an email to administrators to let them know what’s been done.</p></li> </ol> <p>The retiring and updating are simple operations, requiring only a call to <code class="docutils literal notranslate"><span class="pre">retire()</span></code> or <code class="docutils literal notranslate"><span class="pre">set()</span></code>. The creation operation requires more information though - the user’s email address and their Roundup Roles. We’re going to assume that the user’s email address is the same as their login name, so we just append the domain name to that. The Roles are determined using the passwd group identifier - mapping their UN*X group to an appropriate set of Roles.</p> <p>The script to perform all this, broken up into its main components, is as follows. Firstly, we import the necessary modules and open the tracker we’re to work on:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">sys</span><span class="o">,</span> <span class="nn">os</span><span class="o">,</span> <span class="nn">smtplib</span> <span class="kn">from</span> <span class="nn">roundup</span> <span class="kn">import</span> <span class="n">instance</span><span class="p">,</span> <span class="n">date</span> <span class="c1"># open the tracker</span> <span class="n">tracker_home</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="n">tracker</span> <span class="o">=</span> <span class="n">instance</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="n">tracker_home</span><span class="p">)</span> </pre></div> </div> <p>Next we read in the <em>passwd</em> file from the tracker home:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># read in the users from the &quot;passwd.txt&quot; file</span> <span class="n">file</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">tracker_home</span><span class="p">,</span> <span class="s1">&#39;passwd.txt&#39;</span><span class="p">)</span> <span class="n">users</span> <span class="o">=</span> <span class="p">[</span><span class="n">x</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">&#39;:&#39;</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">open</span><span class="p">(</span><span class="n">file</span><span class="p">)</span><span class="o">.</span><span class="n">readlines</span><span class="p">()]</span> </pre></div> </div> <p>Handle special users (those to ignore in the file, and those who don’t appear in the file):</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># users to not keep ever, pre-load with the users I know aren&#39;t</span> <span class="c1"># &quot;real&quot; users</span> <span class="n">ignore</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;ekmmon&#39;</span><span class="p">,</span> <span class="s1">&#39;bfast&#39;</span><span class="p">,</span> <span class="s1">&#39;csrmail&#39;</span><span class="p">]</span> <span class="c1"># users to keep - pre-load with the roundup-specific users</span> <span class="n">keep</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;comment_pool&#39;</span><span class="p">,</span> <span class="s1">&#39;network_pool&#39;</span><span class="p">,</span> <span class="s1">&#39;admin&#39;</span><span class="p">,</span> <span class="s1">&#39;dev-team&#39;</span><span class="p">,</span> <span class="s1">&#39;cs_pool&#39;</span><span class="p">,</span> <span class="s1">&#39;anonymous&#39;</span><span class="p">,</span> <span class="s1">&#39;system_pool&#39;</span><span class="p">,</span> <span class="s1">&#39;automated&#39;</span><span class="p">]</span> </pre></div> </div> <p>Now we map the UN*X group numbers to the Roles that users should have:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">roles</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">&#39;501&#39;</span><span class="p">:</span> <span class="s1">&#39;User,Tech&#39;</span><span class="p">,</span> <span class="c1"># tech</span> <span class="s1">&#39;502&#39;</span><span class="p">:</span> <span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="c1"># finance</span> <span class="s1">&#39;503&#39;</span><span class="p">:</span> <span class="s1">&#39;User,CSR&#39;</span><span class="p">,</span> <span class="c1"># customer service reps</span> <span class="s1">&#39;504&#39;</span><span class="p">:</span> <span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="c1"># sales</span> <span class="s1">&#39;505&#39;</span><span class="p">:</span> <span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="c1"># marketing</span> <span class="p">}</span> </pre></div> </div> <p>Now we do all the work. Note that the body of the script (where we have the tracker database open) is wrapped in a <code class="docutils literal notranslate"><span class="pre">try</span></code> / <code class="docutils literal notranslate"><span class="pre">finally</span></code> clause, so that we always close the database cleanly when we’re finished. So, we now do all the work:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># open the database</span> <span class="n">db</span> <span class="o">=</span> <span class="n">tracker</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s1">&#39;admin&#39;</span><span class="p">)</span> <span class="k">try</span><span class="p">:</span> <span class="c1"># store away messages to send to the tracker admins</span> <span class="n">msg</span> <span class="o">=</span> <span class="p">[]</span> <span class="c1"># loop over the users list read in from the passwd file</span> <span class="k">for</span> <span class="n">user</span><span class="p">,</span><span class="n">passw</span><span class="p">,</span><span class="n">uid</span><span class="p">,</span><span class="n">gid</span><span class="p">,</span><span class="n">real</span><span class="p">,</span><span class="n">home</span><span class="p">,</span><span class="n">shell</span> <span class="ow">in</span> <span class="n">users</span><span class="p">:</span> <span class="k">if</span> <span class="n">user</span> <span class="ow">in</span> <span class="n">ignore</span><span class="p">:</span> <span class="c1"># this user shouldn&#39;t appear in our tracker</span> <span class="k">continue</span> <span class="n">keep</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="k">try</span><span class="p">:</span> <span class="c1"># see if the user exists in the tracker</span> <span class="n">uid</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">lookup</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="c1"># yes, they do - now check the real name for correctness</span> <span class="k">if</span> <span class="n">real</span> <span class="o">!=</span> <span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">uid</span><span class="p">,</span> <span class="s1">&#39;realname&#39;</span><span class="p">):</span> <span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">uid</span><span class="p">,</span> <span class="n">realname</span><span class="o">=</span><span class="n">real</span><span class="p">)</span> <span class="n">msg</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s1">&#39;FIX </span><span class="si">%s</span><span class="s1"> - </span><span class="si">%s</span><span class="s1">&#39;</span><span class="o">%</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">real</span><span class="p">))</span> <span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span> <span class="c1"># nope, the user doesn&#39;t exist</span> <span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="n">user</span><span class="p">,</span> <span class="n">realname</span><span class="o">=</span><span class="n">real</span><span class="p">,</span> <span class="n">address</span><span class="o">=</span><span class="s1">&#39;</span><span class="si">%s</span><span class="s1">@ekit-inc.com&#39;</span><span class="o">%</span><span class="n">user</span><span class="p">,</span> <span class="n">roles</span><span class="o">=</span><span class="n">roles</span><span class="p">[</span><span class="n">gid</span><span class="p">])</span> <span class="n">msg</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s1">&#39;ADD </span><span class="si">%s</span><span class="s1"> - </span><span class="si">%s</span><span class="s1"> (</span><span class="si">%s</span><span class="s1">)&#39;</span><span class="o">%</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">real</span><span class="p">,</span> <span class="n">roles</span><span class="p">[</span><span class="n">gid</span><span class="p">]))</span> <span class="c1"># now check that all the users in the tracker are also in our</span> <span class="c1"># &quot;keep&quot; list - retire those who aren&#39;t</span> <span class="k">for</span> <span class="n">uid</span> <span class="ow">in</span> <span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">list</span><span class="p">():</span> <span class="n">user</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">uid</span><span class="p">,</span> <span class="s1">&#39;username&#39;</span><span class="p">)</span> <span class="k">if</span> <span class="n">user</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">keep</span><span class="p">:</span> <span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">retire</span><span class="p">(</span><span class="n">uid</span><span class="p">)</span> <span class="n">msg</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s1">&#39;RET </span><span class="si">%s</span><span class="s1">&#39;</span><span class="o">%</span><span class="n">user</span><span class="p">)</span> <span class="c1"># if we did work, then send email to the tracker admins</span> <span class="k">if</span> <span class="n">msg</span><span class="p">:</span> <span class="c1"># create the email</span> <span class="n">msg</span> <span class="o">=</span> <span class="s1">&#39;&#39;&#39;Subject: </span><span class="si">%s</span><span class="s1"> user database maintenance</span> <span class="s1"> </span><span class="si">%s</span> <span class="s1"> &#39;&#39;&#39;</span><span class="o">%</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">TRACKER_NAME</span><span class="p">,</span> <span class="s1">&#39;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">msg</span><span class="p">))</span> <span class="c1"># send the email</span> <span class="n">smtp</span> <span class="o">=</span> <span class="n">smtplib</span><span class="o">.</span><span class="n">SMTP</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">MAILHOST</span><span class="p">)</span> <span class="n">addr</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">ADMIN_EMAIL</span> <span class="n">smtp</span><span class="o">.</span><span class="n">sendmail</span><span class="p">(</span><span class="n">addr</span><span class="p">,</span> <span class="n">addr</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span> <span class="c1"># now we&#39;re done - commit the changes</span> <span class="n">db</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span> <span class="k">finally</span><span class="p">:</span> <span class="c1"># always close the database cleanly</span> <span class="n">db</span><span class="o">.</span><span class="n">close</span><span class="p">()</span> </pre></div> </div> <p>And that’s it!</p> </section> <section id="using-an-ldap-database-for-user-information"> <h4><a class="toc-backref" href="#id11" role="doc-backlink">Using an LDAP database for user information</a></h4> <p>A script that reads users from an LDAP store using <a class="reference external" href="https://pypi.org/project/python-ldap/">https://pypi.org/project/python-ldap/</a> and then compares the list to the users in the Roundup user database would be pretty easy to write. You’d then have it run once an hour / day (or on demand if you can work that into your LDAP store workflow). See the example <a class="reference internal" href="#using-a-un-x-passwd-file-as-the-user-database">Using a UN*X passwd file as the user database</a> for more information about doing this.</p> <p>To authenticate off the LDAP store (rather than using the passwords in the Roundup user database) you’d use the same python-ldap module inside an extension to the cgi interface. You’d do this by overriding the method called <code class="docutils literal notranslate"><span class="pre">verifyPassword</span></code> on the <code class="docutils literal notranslate"><span class="pre">LoginAction</span></code> class in your tracker’s <code class="docutils literal notranslate"><span class="pre">extensions</span></code> directory (see <a class="reference internal" href="#using-an-external-password-validation-source">using an external password validation source</a>). The method is implemented by default as:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">verifyPassword</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">userid</span><span class="p">,</span> <span class="n">password</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39; Verify the password that the user has supplied</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="n">stored</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">userid</span><span class="p">,</span> <span class="s1">&#39;password&#39;</span><span class="p">)</span> <span class="k">if</span> <span class="n">password</span> <span class="o">==</span> <span class="n">stored</span><span class="p">:</span> <span class="k">return</span> <span class="mi">1</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">password</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">stored</span><span class="p">:</span> <span class="k">return</span> <span class="mi">1</span> <span class="k">return</span> <span class="mi">0</span> </pre></div> </div> <p>So you could reimplement this as something like:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">verifyPassword</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">userid</span><span class="p">,</span> <span class="n">password</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39; Verify the password that the user has supplied</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="c1"># look up some unique LDAP information about the user</span> <span class="n">username</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">userid</span><span class="p">,</span> <span class="s1">&#39;username&#39;</span><span class="p">)</span> <span class="c1"># now verify the password supplied against the LDAP store</span> </pre></div> </div> </section> <section id="other-external-databases"> <h4><a class="toc-backref" href="#id12" role="doc-backlink">Other External Databases</a></h4> <p>See examples for <a class="reference external" href="https://wiki.roundup-tracker.org/ShibbolethLogin">Shibboleth</a> and info about using <a class="reference external" href="https://wiki.roundup-tracker.org/OauthAuthentication">OAUTH</a> in the Roundup Wiki.</p> </section> </section> <section id="changes-to-tracker-behaviour"> <h3><a class="toc-backref" href="#id13" role="doc-backlink">Changes to Tracker Behaviour</a></h3> <section id="preventing-spam"> <span id="index-3"></span><h4><a class="toc-backref" href="#id14" role="doc-backlink">Preventing SPAM</a></h4> <p>The following detector code may be installed in your tracker’s <code class="docutils literal notranslate"><span class="pre">detectors</span></code> directory. It will block any messages being created that have HTML attachments (a very common vector for spam and phishing) and any messages that have more than 2 HTTP URLs in them. Just copy the following into <code class="docutils literal notranslate"><span class="pre">detectors/anti_spam.py</span></code> in your tracker:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">roundup.exceptions</span> <span class="kn">import</span> <span class="n">Reject</span> <span class="k">def</span> <span class="nf">reject_html</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">cl</span><span class="p">,</span> <span class="n">nodeid</span><span class="p">,</span> <span class="n">newvalues</span><span class="p">):</span> <span class="k">if</span> <span class="n">newvalues</span><span class="p">[</span><span class="s1">&#39;type&#39;</span><span class="p">]</span> <span class="o">==</span> <span class="s1">&#39;text/html&#39;</span><span class="p">:</span> <span class="k">raise</span> <span class="n">Reject</span><span class="p">(</span><span class="s1">&#39;not allowed&#39;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">reject_manylinks</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">cl</span><span class="p">,</span> <span class="n">nodeid</span><span class="p">,</span> <span class="n">newvalues</span><span class="p">):</span> <span class="n">content</span> <span class="o">=</span> <span class="n">newvalues</span><span class="p">[</span><span class="s1">&#39;content&#39;</span><span class="p">]</span> <span class="k">if</span> <span class="n">content</span><span class="o">.</span><span class="n">count</span><span class="p">(</span><span class="s1">&#39;http://&#39;</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">2</span><span class="p">:</span> <span class="k">raise</span> <span class="n">Reject</span><span class="p">(</span><span class="s1">&#39;not allowed&#39;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">db</span><span class="p">):</span> <span class="n">db</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">audit</span><span class="p">(</span><span class="s1">&#39;create&#39;</span><span class="p">,</span> <span class="n">reject_html</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">msg</span><span class="o">.</span><span class="n">audit</span><span class="p">(</span><span class="s1">&#39;create&#39;</span><span class="p">,</span> <span class="n">reject_manylinks</span><span class="p">)</span> </pre></div> </div> <p>You may also wish to block image attachments if your tracker does not need that ability:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">if</span> <span class="n">newvalues</span><span class="p">[</span><span class="s1">&#39;type&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s1">&#39;image/&#39;</span><span class="p">):</span> <span class="k">raise</span> <span class="n">Reject</span><span class="p">(</span><span class="s1">&#39;not allowed&#39;</span><span class="p">)</span> </pre></div> </div> </section> <section id="stop-nosy-messages-going-to-people-on-vacation"> <h4><a class="toc-backref" href="#id15" role="doc-backlink">Stop “nosy” messages going to people on vacation</a></h4> <p>When users go on vacation and set up vacation email bouncing, you’ll start to see a lot of messages come back through Roundup “Fred is on vacation”. Not very useful, and relatively easy to stop.</p> <ol class="arabic"> <li><p>add a “vacation” flag to your users:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">user</span> <span class="o">=</span> <span class="n">Class</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;user&quot;</span><span class="p">,</span> <span class="n">username</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">password</span><span class="o">=</span><span class="n">Password</span><span class="p">(),</span> <span class="n">address</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">realname</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">phone</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">organisation</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">alternate_addresses</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">roles</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">queries</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s2">&quot;query&quot;</span><span class="p">),</span> <span class="n">vacation</span><span class="o">=</span><span class="n">Boolean</span><span class="p">())</span> </pre></div> </div> </li> <li><p>So that users may edit the vacation flags, add something like the following to your <code class="docutils literal notranslate"><span class="pre">user.item</span></code> template:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">On</span> <span class="n">Vacation</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;structure context/vacation/field&quot;</span><span class="o">&gt;</span><span class="n">vacation</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> </li> <li><p>edit your detector <code class="docutils literal notranslate"><span class="pre">nosyreactor.py</span></code> so that the <code class="docutils literal notranslate"><span class="pre">nosyreaction()</span></code> consists of:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">nosyreaction</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">cl</span><span class="p">,</span> <span class="n">nodeid</span><span class="p">,</span> <span class="n">oldvalues</span><span class="p">):</span> <span class="n">users</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">user</span> <span class="n">messages</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">msg</span> <span class="c1"># send a copy of all new messages to the nosy list</span> <span class="k">for</span> <span class="n">msgid</span> <span class="ow">in</span> <span class="n">determineNewMessages</span><span class="p">(</span><span class="n">cl</span><span class="p">,</span> <span class="n">nodeid</span><span class="p">,</span> <span class="n">oldvalues</span><span class="p">):</span> <span class="k">try</span><span class="p">:</span> <span class="c1"># figure the recipient ids</span> <span class="n">sendto</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">seen_message</span> <span class="o">=</span> <span class="p">{}</span> <span class="n">recipients</span> <span class="o">=</span> <span class="n">messages</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">msgid</span><span class="p">,</span> <span class="s1">&#39;recipients&#39;</span><span class="p">)</span> <span class="k">for</span> <span class="n">recipid</span> <span class="ow">in</span> <span class="n">messages</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">msgid</span><span class="p">,</span> <span class="s1">&#39;recipients&#39;</span><span class="p">):</span> <span class="n">seen_message</span><span class="p">[</span><span class="n">recipid</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span> <span class="c1"># figure the author&#39;s id, and indicate they&#39;ve received</span> <span class="c1"># the message</span> <span class="n">authid</span> <span class="o">=</span> <span class="n">messages</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">msgid</span><span class="p">,</span> <span class="s1">&#39;author&#39;</span><span class="p">)</span> <span class="c1"># possibly send the message to the author, as long as</span> <span class="c1"># they aren&#39;t anonymous</span> <span class="k">if</span> <span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">MESSAGES_TO_AUTHOR</span> <span class="o">==</span> <span class="s1">&#39;yes&#39;</span> <span class="ow">and</span> <span class="n">users</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">authid</span><span class="p">,</span> <span class="s1">&#39;username&#39;</span><span class="p">)</span> <span class="o">!=</span> <span class="s1">&#39;anonymous&#39;</span><span class="p">):</span> <span class="n">sendto</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">authid</span><span class="p">)</span> <span class="n">seen_message</span><span class="p">[</span><span class="n">authid</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span> <span class="c1"># now figure the nosy people who weren&#39;t recipients</span> <span class="n">nosy</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">nodeid</span><span class="p">,</span> <span class="s1">&#39;nosy&#39;</span><span class="p">)</span> <span class="k">for</span> <span class="n">nosyid</span> <span class="ow">in</span> <span class="n">nosy</span><span class="p">:</span> <span class="c1"># Don&#39;t send nosy mail to the anonymous user (that</span> <span class="c1"># user shouldn&#39;t appear in the nosy list, but just</span> <span class="c1"># in case they do...)</span> <span class="k">if</span> <span class="n">users</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">nosyid</span><span class="p">,</span> <span class="s1">&#39;username&#39;</span><span class="p">)</span> <span class="o">==</span> <span class="s1">&#39;anonymous&#39;</span><span class="p">:</span> <span class="k">continue</span> <span class="c1"># make sure they haven&#39;t seen the message already</span> <span class="k">if</span> <span class="n">nosyid</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">seen_message</span><span class="p">:</span> <span class="c1"># send it to them</span> <span class="n">sendto</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">nosyid</span><span class="p">)</span> <span class="n">recipients</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">nosyid</span><span class="p">)</span> <span class="c1"># generate a change note</span> <span class="k">if</span> <span class="n">oldvalues</span><span class="p">:</span> <span class="n">note</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="n">generateChangeNote</span><span class="p">(</span><span class="n">nodeid</span><span class="p">,</span> <span class="n">oldvalues</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="n">note</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="n">generateCreateNote</span><span class="p">(</span><span class="n">nodeid</span><span class="p">)</span> <span class="c1"># we have new recipients</span> <span class="k">if</span> <span class="n">sendto</span><span class="p">:</span> <span class="c1"># filter out the people on vacation</span> <span class="n">sendto</span> <span class="o">=</span> <span class="p">[</span><span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">sendto</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">users</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="s1">&#39;vacation&#39;</span><span class="p">,</span> <span class="mi">0</span><span class="p">)]</span> <span class="c1"># map userids to addresses</span> <span class="n">sendto</span> <span class="o">=</span> <span class="p">[</span><span class="n">users</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="s1">&#39;address&#39;</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">sendto</span><span class="p">]</span> <span class="c1"># update the message&#39;s recipients list</span> <span class="n">messages</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">msgid</span><span class="p">,</span> <span class="n">recipients</span><span class="o">=</span><span class="n">recipients</span><span class="p">)</span> <span class="c1"># send the message</span> <span class="n">cl</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span><span class="n">nodeid</span><span class="p">,</span> <span class="n">msgid</span><span class="p">,</span> <span class="n">note</span><span class="p">,</span> <span class="n">sendto</span><span class="p">)</span> <span class="k">except</span> <span class="n">roundupdb</span><span class="o">.</span><span class="n">MessageSendError</span> <span class="k">as</span> <span class="n">message</span><span class="p">:</span> <span class="k">raise</span> <span class="n">roundupdb</span><span class="o">.</span><span class="n">DetectorError</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> </pre></div> </div> <p>Note that this is the standard nosy reaction code, with the small addition of:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># filter out the people on vacation</span> <span class="n">sendto</span> <span class="o">=</span> <span class="p">[</span><span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">sendto</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">users</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="s1">&#39;vacation&#39;</span><span class="p">,</span> <span class="mi">0</span><span class="p">)]</span> </pre></div> </div> <p>which filters out the users that have the vacation flag set to true.</p> </li> </ol> </section> <section id="adding-in-state-transition-control"> <h4><a class="toc-backref" href="#id16" role="doc-backlink">Adding in state transition control</a></h4> <p>Sometimes tracker admins want to control the states to which users may move issues. You can do this by following these steps:</p> <ol class="arabic"> <li><p>make “status” a required variable. This is achieved by adding the following to the top of the form in the <code class="docutils literal notranslate"><span class="pre">issue.item.html</span></code> template:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@required&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;status&quot;</span><span class="o">&gt;</span> </pre></div> </div> <p>This will force users to select a status.</p> </li> <li><p>add a Multilink property to the status class:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">stat</span> <span class="o">=</span> <span class="n">Class</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;status&quot;</span><span class="p">,</span> <span class="o">...</span> <span class="p">,</span> <span class="n">transitions</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s1">&#39;status&#39;</span><span class="p">),</span> <span class="o">...</span><span class="p">)</span> </pre></div> </div> <p>and then edit the statuses already created, either:</p> <ol class="loweralpha simple"> <li><p>through the web using the class list -&gt; status class editor, or</p></li> <li><p>using the <code class="docutils literal notranslate"><span class="pre">roundup-admin</span></code> “set” command.</p></li> </ol> </li> <li><p>add an auditor module <code class="docutils literal notranslate"><span class="pre">checktransition.py</span></code> in your tracker’s <code class="docutils literal notranslate"><span class="pre">detectors</span></code> directory, for example:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">checktransition</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">cl</span><span class="p">,</span> <span class="n">nodeid</span><span class="p">,</span> <span class="n">newvalues</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39; Check that the desired transition is valid for the &quot;status&quot;</span> <span class="sd"> property.</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="k">if</span> <span class="s1">&#39;status&#39;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">newvalues</span><span class="p">:</span> <span class="k">return</span> <span class="n">current</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">nodeid</span><span class="p">,</span> <span class="s1">&#39;status&#39;</span><span class="p">)</span> <span class="n">new</span> <span class="o">=</span> <span class="n">newvalues</span><span class="p">[</span><span class="s1">&#39;status&#39;</span><span class="p">]</span> <span class="k">if</span> <span class="n">new</span> <span class="o">==</span> <span class="n">current</span><span class="p">:</span> <span class="k">return</span> <span class="n">ok</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">status</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">current</span><span class="p">,</span> <span class="s1">&#39;transitions&#39;</span><span class="p">)</span> <span class="k">if</span> <span class="n">new</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">ok</span><span class="p">:</span> <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s1">&#39;Status not allowed to move from &quot;</span><span class="si">%s</span><span class="s1">&quot; to &quot;</span><span class="si">%s</span><span class="s1">&quot;&#39;</span><span class="o">%</span><span class="p">(</span> <span class="n">db</span><span class="o">.</span><span class="n">status</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">current</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">),</span> <span class="n">db</span><span class="o">.</span><span class="n">status</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">new</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">)))</span> <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">db</span><span class="p">):</span> <span class="n">db</span><span class="o">.</span><span class="n">issue</span><span class="o">.</span><span class="n">audit</span><span class="p">(</span><span class="s1">&#39;set&#39;</span><span class="p">,</span> <span class="n">checktransition</span><span class="p">)</span> </pre></div> </div> </li> <li><p>in the <code class="docutils literal notranslate"><span class="pre">issue.item.html</span></code> template, change the status editing bit from:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Status</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;structure context/status/menu&quot;</span><span class="o">&gt;</span><span class="n">status</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> </pre></div> </div> <p>to:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Status</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">select</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;context/id&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;status&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span> <span class="n">tal</span><span class="p">:</span><span class="n">define</span><span class="o">=</span><span class="s2">&quot;ok context/status/transitions&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">repeat</span><span class="o">=</span><span class="s2">&quot;state db/status/list&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">option</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;python:state.id in ok&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;</span> <span class="n">value</span> <span class="n">state</span><span class="o">/</span><span class="nb">id</span><span class="p">;</span> <span class="n">selected</span> <span class="n">python</span><span class="p">:</span><span class="n">state</span><span class="o">.</span><span class="n">id</span> <span class="o">==</span> <span class="n">context</span><span class="o">.</span><span class="n">status</span><span class="o">.</span><span class="n">id</span><span class="s2">&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;state/name&quot;</span><span class="o">&gt;&lt;/</span><span class="n">option</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">select</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;not:context/id&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;structure context/status/menu&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> </pre></div> </div> <p>which displays only the allowed status to transition to.</p> </li> </ol> </section> <section id="blocking-issues-that-depend-on-other-issues"> <h4><a class="toc-backref" href="#id17" role="doc-backlink">Blocking issues that depend on other issues</a></h4> <p>We needed the ability to mark certain issues as “blockers” - that is, they can’t be resolved until another issue (the blocker) they rely on is resolved. To achieve this:</p> <ol class="arabic"> <li><p>Create a new property on the <code class="docutils literal notranslate"><span class="pre">issue</span></code> class: <code class="docutils literal notranslate"><span class="pre">blockers=Multilink(&quot;issue&quot;)</span></code>. To do this, edit the definition of this class in your tracker’s <code class="docutils literal notranslate"><span class="pre">schema.py</span></code> file. Change this:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">issue</span> <span class="o">=</span> <span class="n">IssueClass</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;issue&quot;</span><span class="p">,</span> <span class="n">assignedto</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;user&quot;</span><span class="p">),</span> <span class="n">keyword</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s2">&quot;keyword&quot;</span><span class="p">),</span> <span class="n">priority</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;priority&quot;</span><span class="p">),</span> <span class="n">status</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;status&quot;</span><span class="p">))</span> </pre></div> </div> <p>to this, adding the blockers entry:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">issue</span> <span class="o">=</span> <span class="n">IssueClass</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;issue&quot;</span><span class="p">,</span> <span class="n">blockers</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s2">&quot;issue&quot;</span><span class="p">),</span> <span class="n">assignedto</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;user&quot;</span><span class="p">),</span> <span class="n">keyword</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s2">&quot;keyword&quot;</span><span class="p">),</span> <span class="n">priority</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;priority&quot;</span><span class="p">),</span> <span class="n">status</span><span class="o">=</span><span class="n">Link</span><span class="p">(</span><span class="s2">&quot;status&quot;</span><span class="p">))</span> </pre></div> </div> </li> <li><p>Add the new <code class="docutils literal notranslate"><span class="pre">blockers</span></code> property to the <code class="docutils literal notranslate"><span class="pre">issue.item.html</span></code> edit page, using something like:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Waiting</span> <span class="n">On</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">span</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;structure python:context.blockers.field(showid=1,</span> <span class="n">size</span><span class="o">=</span><span class="mi">20</span><span class="p">)</span><span class="s2">&quot; /&gt;</span> <span class="o">&lt;</span><span class="n">span</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;structure python:db.issue.classhelp(&#39;id,title&#39;,</span> <span class="nb">property</span><span class="o">=</span><span class="s1">&#39;blockers&#39;</span><span class="p">)</span><span class="s2">&quot; /&gt;</span> <span class="o">&lt;</span><span class="n">span</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;context/blockers&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">repeat</span><span class="o">=</span><span class="s2">&quot;blk context/blockers&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">br</span><span class="o">&gt;</span><span class="n">View</span><span class="p">:</span> <span class="o">&lt;</span><span class="n">a</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;href string:issue${blk/id}&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;blk/id&quot;</span><span class="o">&gt;&lt;/</span><span class="n">a</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">span</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> </pre></div> </div> <p>You’ll need to fiddle with your item page layout to find an appropriate place to put it - I’ll leave that fun part up to you. Just make sure it appears in the first table, possibly somewhere near the “superseders” field.</p> </li> <li><p>Create a new detector module (see below) which enforces the rules:</p> <ul class="simple"> <li><p>issues may not be resolved if they have blockers</p></li> <li><p>when a blocker is resolved, it’s removed from issues it blocks</p></li> </ul> <p>The contents of the detector should be something like this:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">blockresolution</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">cl</span><span class="p">,</span> <span class="n">nodeid</span><span class="p">,</span> <span class="n">newvalues</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39; If the issue has blockers, don&#39;t allow it to be resolved.</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="k">if</span> <span class="n">nodeid</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span> <span class="n">blockers</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">else</span><span class="p">:</span> <span class="n">blockers</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">nodeid</span><span class="p">,</span> <span class="s1">&#39;blockers&#39;</span><span class="p">)</span> <span class="n">blockers</span> <span class="o">=</span> <span class="n">newvalues</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;blockers&#39;</span><span class="p">,</span> <span class="n">blockers</span><span class="p">)</span> <span class="c1"># don&#39;t do anything if there&#39;s no blockers or the status hasn&#39;t</span> <span class="c1"># changed</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">blockers</span> <span class="ow">or</span> <span class="s1">&#39;status&#39;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">newvalues</span><span class="p">:</span> <span class="k">return</span> <span class="c1"># get the resolved state ID</span> <span class="n">resolved_id</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">status</span><span class="o">.</span><span class="n">lookup</span><span class="p">(</span><span class="s1">&#39;resolved&#39;</span><span class="p">)</span> <span class="c1"># format the info</span> <span class="n">u</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">TRACKER_WEB</span> <span class="n">s</span> <span class="o">=</span> <span class="s1">&#39;, &#39;</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="s1">&#39;&lt;a href=&quot;</span><span class="si">%s</span><span class="s1">issue</span><span class="si">%s</span><span class="s1">&quot;&gt;</span><span class="si">%s</span><span class="s1">&lt;/a&gt;&#39;</span><span class="o">%</span><span class="p">(</span> <span class="n">u</span><span class="p">,</span><span class="nb">id</span><span class="p">,</span><span class="nb">id</span><span class="p">)</span> <span class="k">for</span> <span class="nb">id</span> <span class="ow">in</span> <span class="n">blockers</span><span class="p">])</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">blockers</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span> <span class="n">s</span> <span class="o">=</span> <span class="s1">&#39;issue </span><span class="si">%s</span><span class="s1"> is&#39;</span><span class="o">%</span><span class="n">s</span> <span class="k">else</span><span class="p">:</span> <span class="n">s</span> <span class="o">=</span> <span class="s1">&#39;issues </span><span class="si">%s</span><span class="s1"> are&#39;</span><span class="o">%</span><span class="n">s</span> <span class="c1"># ok, see if we&#39;re trying to resolve</span> <span class="k">if</span> <span class="n">newvalues</span><span class="p">[</span><span class="s1">&#39;status&#39;</span><span class="p">]</span> <span class="o">==</span> <span class="n">resolved_id</span><span class="p">:</span> <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&quot;This issue can&#39;t be resolved until </span><span class="si">%s</span><span class="s2"> resolved.&quot;</span><span class="o">%</span><span class="n">s</span><span class="p">)</span> <span class="k">def</span> <span class="nf">resolveblockers</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">cl</span><span class="p">,</span> <span class="n">nodeid</span><span class="p">,</span> <span class="n">oldvalues</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39; When we resolve an issue that&#39;s a blocker, remove it from the</span> <span class="sd"> blockers list of the issue(s) it blocks.</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="n">newstatus</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">nodeid</span><span class="p">,</span><span class="s1">&#39;status&#39;</span><span class="p">)</span> <span class="c1"># no change?</span> <span class="k">if</span> <span class="n">oldvalues</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;status&#39;</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span> <span class="o">==</span> <span class="n">newstatus</span><span class="p">:</span> <span class="k">return</span> <span class="n">resolved_id</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">status</span><span class="o">.</span><span class="n">lookup</span><span class="p">(</span><span class="s1">&#39;resolved&#39;</span><span class="p">)</span> <span class="c1"># interesting?</span> <span class="k">if</span> <span class="n">newstatus</span> <span class="o">!=</span> <span class="n">resolved_id</span><span class="p">:</span> <span class="k">return</span> <span class="c1"># yes - find all the blocked issues, if any, and remove me from</span> <span class="c1"># their blockers list</span> <span class="n">issues</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">blockers</span><span class="o">=</span><span class="n">nodeid</span><span class="p">)</span> <span class="k">for</span> <span class="n">issueid</span> <span class="ow">in</span> <span class="n">issues</span><span class="p">:</span> <span class="n">blockers</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">issueid</span><span class="p">,</span> <span class="s1">&#39;blockers&#39;</span><span class="p">)</span> <span class="k">if</span> <span class="n">nodeid</span> <span class="ow">in</span> <span class="n">blockers</span><span class="p">:</span> <span class="n">blockers</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">nodeid</span><span class="p">)</span> <span class="n">cl</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">issueid</span><span class="p">,</span> <span class="n">blockers</span><span class="o">=</span><span class="n">blockers</span><span class="p">)</span> <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">db</span><span class="p">):</span> <span class="c1"># might, in an obscure situation, happen in a create</span> <span class="n">db</span><span class="o">.</span><span class="n">issue</span><span class="o">.</span><span class="n">audit</span><span class="p">(</span><span class="s1">&#39;create&#39;</span><span class="p">,</span> <span class="n">blockresolution</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">issue</span><span class="o">.</span><span class="n">audit</span><span class="p">(</span><span class="s1">&#39;set&#39;</span><span class="p">,</span> <span class="n">blockresolution</span><span class="p">)</span> <span class="c1"># can only happen on a set</span> <span class="n">db</span><span class="o">.</span><span class="n">issue</span><span class="o">.</span><span class="n">react</span><span class="p">(</span><span class="s1">&#39;set&#39;</span><span class="p">,</span> <span class="n">resolveblockers</span><span class="p">)</span> </pre></div> </div> <p>Put the above code in a file called “blockers.py” in your tracker’s “detectors” directory.</p> </li> <li><p>Finally, and this is an optional step, modify the tracker web page URLs so they filter out issues with any blockers. You do this by adding an additional filter on “blockers” for the value “-1”. For example, the existing “Show All” link in the “page” template (in the tracker’s “html” directory) looks like this:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">a</span> <span class="n">href</span><span class="o">=</span><span class="s2">&quot;#&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;href python:request.indexargs_url(&#39;issue&#39;, {</span> <span class="s1">&#39;@sort&#39;</span><span class="p">:</span> <span class="s1">&#39;-activity&#39;</span><span class="p">,</span> <span class="s1">&#39;@group&#39;</span><span class="p">:</span> <span class="s1">&#39;priority&#39;</span><span class="p">,</span> <span class="s1">&#39;@filter&#39;</span><span class="p">:</span> <span class="s1">&#39;status&#39;</span><span class="p">,</span> <span class="s1">&#39;@columns&#39;</span><span class="p">:</span> <span class="n">columns_showall</span><span class="p">,</span> <span class="s1">&#39;@search_text&#39;</span><span class="p">:</span> <span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="s1">&#39;status&#39;</span><span class="p">:</span> <span class="n">status_notresolved</span><span class="p">,</span> <span class="s1">&#39;@dispname&#39;</span><span class="p">:</span> <span class="n">i18n</span><span class="o">.</span><span class="n">gettext</span><span class="p">(</span><span class="s1">&#39;Show All&#39;</span><span class="p">),</span> <span class="p">})</span><span class="s2">&quot;</span> <span class="n">i18n</span><span class="p">:</span><span class="n">translate</span><span class="o">=</span><span class="s2">&quot;&quot;</span><span class="o">&gt;</span><span class="n">Show</span> <span class="n">All</span><span class="o">&lt;/</span><span class="n">a</span><span class="o">&gt;&lt;</span><span class="n">br</span><span class="o">&gt;</span> </pre></div> </div> <p>modify it to add the “blockers” info to the URL (note, both the “&#64;filter” <em>and</em> “blockers” values must be specified):</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">a</span> <span class="n">href</span><span class="o">=</span><span class="s2">&quot;#&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;href python:request.indexargs_url(&#39;issue&#39;, {</span> <span class="s1">&#39;@sort&#39;</span><span class="p">:</span> <span class="s1">&#39;-activity&#39;</span><span class="p">,</span> <span class="s1">&#39;@group&#39;</span><span class="p">:</span> <span class="s1">&#39;priority&#39;</span><span class="p">,</span> <span class="s1">&#39;@filter&#39;</span><span class="p">:</span> <span class="s1">&#39;status,blockers&#39;</span><span class="p">,</span> <span class="s1">&#39;@columns&#39;</span><span class="p">:</span> <span class="n">columns_showall</span><span class="p">,</span> <span class="s1">&#39;@search_text&#39;</span><span class="p">:</span> <span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="s1">&#39;status&#39;</span><span class="p">:</span> <span class="n">status_notresolved</span><span class="p">,</span> <span class="s1">&#39;blockers&#39;</span><span class="p">:</span> <span class="s1">&#39;-1&#39;</span><span class="p">,</span> <span class="s1">&#39;@dispname&#39;</span><span class="p">:</span> <span class="n">i18n</span><span class="o">.</span><span class="n">gettext</span><span class="p">(</span><span class="s1">&#39;Show All&#39;</span><span class="p">),</span> <span class="p">})</span><span class="s2">&quot;</span> <span class="n">i18n</span><span class="p">:</span><span class="n">translate</span><span class="o">=</span><span class="s2">&quot;&quot;</span><span class="o">&gt;</span><span class="n">Show</span> <span class="n">All</span><span class="o">&lt;/</span><span class="n">a</span><span class="o">&gt;&lt;</span><span class="n">br</span><span class="o">&gt;</span> </pre></div> </div> <p>The above examples are line-wrapped on the trailing &amp; and should be unwrapped.</p> </li> </ol> <p>That’s it. You should now be able to set blockers on your issues. Note that if you want to know whether an issue has any other issues dependent on it (i.e. it’s in their blockers list) you can look at the journal history at the bottom of the issue page - look for a “link” event to another issue’s “blockers” property.</p> </section> <section id="add-users-to-the-nosy-list-based-on-the-keyword"> <h4><a class="toc-backref" href="#id18" role="doc-backlink">Add users to the nosy list based on the keyword</a></h4> <p>Let’s say we need the ability to automatically add users to the nosy list based on the occurance of a keyword. Every user should be allowed to edit their own list of keywords for which they want to be added to the nosy list.</p> <p>Below, we’ll show that this change can be done with minimal understanding of the Roundup system, using only copy and paste.</p> <p>This requires three changes to the tracker: a change in the database to allow per-user recording of the lists of keywords for which he wants to be put on the nosy list, a change in the user view allowing them to edit this list of keywords, and addition of an auditor which updates the nosy list when a keyword is set.</p> <section id="adding-the-nosy-keyword-list"> <h5>Adding the nosy keyword list</h5> <p>The change to make in the database, is that for any user there should be a list of keywords for which he wants to be put on the nosy list. Adding a <code class="docutils literal notranslate"><span class="pre">Multilink</span></code> of <code class="docutils literal notranslate"><span class="pre">keyword</span></code> seems to fullfill this. As such, all that has to be done is to add a new field to the definition of <code class="docutils literal notranslate"><span class="pre">user</span></code> within the file <code class="docutils literal notranslate"><span class="pre">schema.py</span></code>. We will call this new field <code class="docutils literal notranslate"><span class="pre">nosy_keywords</span></code>, and the updated definition of user will be:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">user</span> <span class="o">=</span> <span class="n">Class</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s2">&quot;user&quot;</span><span class="p">,</span> <span class="n">username</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">password</span><span class="o">=</span><span class="n">Password</span><span class="p">(),</span> <span class="n">address</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">realname</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">phone</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">organisation</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">alternate_addresses</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">queries</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s1">&#39;query&#39;</span><span class="p">),</span> <span class="n">roles</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">timezone</span><span class="o">=</span><span class="n">String</span><span class="p">(),</span> <span class="n">nosy_keywords</span><span class="o">=</span><span class="n">Multilink</span><span class="p">(</span><span class="s1">&#39;keyword&#39;</span><span class="p">))</span> </pre></div> </div> </section> <section id="changing-the-user-view-to-allow-changing-the-nosy-keyword-list"> <h5>Changing the user view to allow changing the nosy keyword list</h5> <p>We want any user to be able to change the list of keywords for which he will by default be added to the nosy list. We choose to add this to the user view, as is generated by the file <code class="docutils literal notranslate"><span class="pre">html/user.item.html</span></code>. We can easily see that the keyword field in the issue view has very similar editing requirements as our nosy keywords, both being lists of keywords. As such, we look for Keywords in <code class="docutils literal notranslate"><span class="pre">issue.item.html</span></code>, and extract the associated parts from there. We add this to <code class="docutils literal notranslate"><span class="pre">user.item.html</span></code> at the bottom of the list of viewed items (i.e. just below the ‘Alternate E-mail addresses’ in the classic template):</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Nosy</span> <span class="n">Keywords</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">span</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;structure context/nosy_keywords/field&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="n">span</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;structure python:db.keyword.classhelp(property=&#39;nosy_keywords&#39;)&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> </pre></div> </div> </section> <section id="addition-of-an-auditor-to-update-the-nosy-list"> <h5>Addition of an auditor to update the nosy list</h5> <p>The more difficult part is the logic to add the users to the nosy list when required. We choose to perform this action whenever the keywords on an item are set (this includes the creation of items). Here we choose to start out with a copy of the <code class="docutils literal notranslate"><span class="pre">detectors/nosyreaction.py</span></code> detector, which we copy to the file <code class="docutils literal notranslate"><span class="pre">detectors/nosy_keyword_reaction.py</span></code>. This looks like a good start as it also adds users to the nosy list. A look through the code reveals that the <code class="docutils literal notranslate"><span class="pre">nosyreaction</span></code> function actually sends the e-mail. We don’t need this. Therefore, we can change the <code class="docutils literal notranslate"><span class="pre">init</span></code> function to:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">db</span><span class="p">):</span> <span class="n">db</span><span class="o">.</span><span class="n">issue</span><span class="o">.</span><span class="n">audit</span><span class="p">(</span><span class="s1">&#39;create&#39;</span><span class="p">,</span> <span class="n">update_kw_nosy</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">issue</span><span class="o">.</span><span class="n">audit</span><span class="p">(</span><span class="s1">&#39;set&#39;</span><span class="p">,</span> <span class="n">update_kw_nosy</span><span class="p">)</span> </pre></div> </div> <p>After that, we rename the <code class="docutils literal notranslate"><span class="pre">updatenosy</span></code> function to <code class="docutils literal notranslate"><span class="pre">update_kw_nosy</span></code>. The first two blocks of code in that function relate to setting <code class="docutils literal notranslate"><span class="pre">current</span></code> to a combination of the old and new nosy lists. This functionality is left in the new auditor. The following block of code, which handled adding the assignedto user(s) to the nosy list in <code class="docutils literal notranslate"><span class="pre">updatenosy</span></code>, should be replaced by a block of code to add the interested users to the nosy list. We choose here to loop over all new keywords, than looping over all users, and assign the user to the nosy list when the keyword occurs in the user’s <code class="docutils literal notranslate"><span class="pre">nosy_keywords</span></code>. The next part in <code class="docutils literal notranslate"><span class="pre">updatenosy</span></code> – adding the author and/or recipients of a message to the nosy list – is obviously not relevant here and is thus deleted from the new auditor. The last part, copying the new nosy list to <code class="docutils literal notranslate"><span class="pre">newvalues</span></code>, can stay as is. This results in the following function:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">update_kw_nosy</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">cl</span><span class="p">,</span> <span class="n">nodeid</span><span class="p">,</span> <span class="n">newvalues</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39;Update the nosy list for changes to the keywords</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="c1"># nodeid will be None if this is a new node</span> <span class="n">current</span> <span class="o">=</span> <span class="p">{}</span> <span class="k">if</span> <span class="n">nodeid</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span> <span class="n">ok</span> <span class="o">=</span> <span class="p">(</span><span class="s1">&#39;new&#39;</span><span class="p">,</span> <span class="s1">&#39;yes&#39;</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="n">ok</span> <span class="o">=</span> <span class="p">(</span><span class="s1">&#39;yes&#39;</span><span class="p">,)</span> <span class="c1"># old node, get the current values from the node if they haven&#39;t</span> <span class="c1"># changed</span> <span class="k">if</span> <span class="s1">&#39;nosy&#39;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">newvalues</span><span class="p">:</span> <span class="n">nosy</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">nodeid</span><span class="p">,</span> <span class="s1">&#39;nosy&#39;</span><span class="p">)</span> <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">nosy</span><span class="p">:</span> <span class="k">if</span> <span class="n">value</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">current</span><span class="p">:</span> <span class="n">current</span><span class="p">[</span><span class="n">value</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span> <span class="c1"># if the nosy list changed in this transaction, init from the new value</span> <span class="k">if</span> <span class="s1">&#39;nosy&#39;</span> <span class="ow">in</span> <span class="n">newvalues</span><span class="p">:</span> <span class="n">nosy</span> <span class="o">=</span> <span class="n">newvalues</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;nosy&#39;</span><span class="p">,</span> <span class="p">[])</span> <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">nosy</span><span class="p">:</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">db</span><span class="o">.</span><span class="n">hasnode</span><span class="p">(</span><span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span> <span class="k">continue</span> <span class="k">if</span> <span class="n">value</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">current</span><span class="p">:</span> <span class="n">current</span><span class="p">[</span><span class="n">value</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span> <span class="c1"># add users with keyword in nosy_keywords to the nosy list</span> <span class="k">if</span> <span class="s1">&#39;keyword&#39;</span> <span class="ow">in</span> <span class="n">newvalues</span> <span class="ow">and</span> <span class="n">newvalues</span><span class="p">[</span><span class="s1">&#39;keyword&#39;</span><span class="p">]</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span> <span class="n">keyword_ids</span> <span class="o">=</span> <span class="n">newvalues</span><span class="p">[</span><span class="s1">&#39;keyword&#39;</span><span class="p">]</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">keyword_ids</span><span class="p">:</span> <span class="c1"># loop over all users,</span> <span class="c1"># and assign user to nosy when keyword in nosy_keywords</span> <span class="k">for</span> <span class="n">user_id</span> <span class="ow">in</span> <span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">list</span><span class="p">():</span> <span class="n">nosy_kw</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="s2">&quot;nosy_keywords&quot;</span><span class="p">)</span> <span class="n">found</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">for</span> <span class="n">kw</span> <span class="ow">in</span> <span class="n">nosy_kw</span><span class="p">:</span> <span class="k">if</span> <span class="n">kw</span> <span class="o">==</span> <span class="n">keyword</span><span class="p">:</span> <span class="n">found</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">if</span> <span class="n">found</span><span class="p">:</span> <span class="n">current</span><span class="p">[</span><span class="n">user_id</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span> <span class="c1"># that&#39;s it, save off the new nosy list</span> <span class="n">newvalues</span><span class="p">[</span><span class="s1">&#39;nosy&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">current</span><span class="o">.</span><span class="n">keys</span><span class="p">())</span> </pre></div> </div> <p>These two function are the only ones needed in the file.</p> <p>TODO: update this example to use the <code class="docutils literal notranslate"><span class="pre">find()</span></code> Class method.</p> </section> <section id="caveats"> <h5>Caveats</h5> <p>A few problems with the design here can be noted:</p> <dl> <dt>Multiple additions</dt><dd><p>When a user, after automatic selection, is manually removed from the nosy list, he is added to the nosy list again when the keyword list of the issue is updated. A better design might be to only check which keywords are new compared to the old list of keywords, and only add users when they have indicated interest on a new keyword.</p> <p>The code could also be changed to only trigger on the <code class="docutils literal notranslate"><span class="pre">create()</span></code> event, rather than also on the <code class="docutils literal notranslate"><span class="pre">set()</span></code> event, thus only setting the nosy list when the issue is created.</p> </dd> <dt>Scalability</dt><dd><p>In the auditor, there is a loop over all users. For a site with only few users this will pose no serious problem; however, with many users this will be a serious performance bottleneck. A way out would be to link from the keywords to the users who selected these keywords as nosy keywords. This will eliminate the loop over all users. See the <code class="docutils literal notranslate"><span class="pre">rev_multilink</span></code> attribute to make this easier.</p> </dd> </dl> </section> </section> <section id="restricting-updates-that-arrive-by-email"> <h4><a class="toc-backref" href="#id19" role="doc-backlink">Restricting updates that arrive by email</a></h4> <p>Roundup supports multiple update methods:</p> <ol class="arabic simple"> <li><p>command line</p></li> <li><p>plain email</p></li> <li><p>pgp signed email</p></li> <li><p>web access</p></li> </ol> <p>in some cases you may need to prevent changes to properties by some of these methods. For example you can set up issues that are viewable only by people on the nosy list. So you must prevent unauthenticated changes to the nosy list.</p> <p>Since plain email can be easily forged, it does not provide sufficient authentication in this senario.</p> <p>To prevent this we can add a detector that audits the source of the transaction and rejects the update if it changes the nosy list.</p> <p>Create the detector (auditor) module and add it to the detectors directory of your tracker:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">roundup</span> <span class="kn">import</span> <span class="n">roundupdb</span><span class="p">,</span> <span class="n">hyperdb</span> <span class="kn">from</span> <span class="nn">roundup.mailgw</span> <span class="kn">import</span> <span class="n">Unauthorized</span> <span class="k">def</span> <span class="nf">restrict_nosy_changes</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">cl</span><span class="p">,</span> <span class="n">nodeid</span><span class="p">,</span> <span class="n">newvalues</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39;Do not permit changes to nosy via email.&#39;&#39;&#39;</span> <span class="k">if</span> <span class="s1">&#39;nosy&#39;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">newvalues</span><span class="p">:</span> <span class="c1"># the nosy field has not changed so no need to check.</span> <span class="k">return</span> <span class="k">if</span> <span class="n">db</span><span class="o">.</span><span class="n">tx_Source</span> <span class="ow">in</span> <span class="p">[</span><span class="s1">&#39;web&#39;</span><span class="p">,</span> <span class="s1">&#39;rest&#39;</span><span class="p">,</span> <span class="s1">&#39;xmlrpc&#39;</span><span class="p">,</span> <span class="s1">&#39;email-sig-openpgp&#39;</span><span class="p">,</span> <span class="s1">&#39;cli&#39;</span> <span class="p">]:</span> <span class="c1"># if the source of the transaction is from an authenticated</span> <span class="c1"># source or a privileged process allow the transaction.</span> <span class="c1"># Other possible sources: &#39;email&#39;</span> <span class="k">return</span> <span class="c1"># otherwise raise an error</span> <span class="k">raise</span> <span class="n">Unauthorized</span><span class="p">(</span> \ <span class="s1">&#39;Changes to nosy property not allowed via </span><span class="si">%s</span><span class="s1"> for this issue.&#39;</span><span class="o">%</span>\ <span class="n">tx_Source</span><span class="p">)</span> <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">db</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39; Install restrict_nosy_changes to run after other auditors.</span> <span class="sd"> Allow initial creation email to set nosy.</span> <span class="sd"> So don&#39;t execute: db.issue.audit(&#39;create&#39;, requestedbyauditor)</span> <span class="sd"> Set priority to 110 to run this auditor after other auditors</span> <span class="sd"> that can cause nosy to change.</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="n">db</span><span class="o">.</span><span class="n">issue</span><span class="o">.</span><span class="n">audit</span><span class="p">(</span><span class="s1">&#39;set&#39;</span><span class="p">,</span> <span class="n">restrict_nosy_changes</span><span class="p">,</span> <span class="mi">110</span><span class="p">)</span> </pre></div> </div> <p>This detector (auditor) will prevent updates to the nosy field if it arrives by email. Since it runs after other auditors (due to the priority of 110), it will also prevent changes to the nosy field that are done by other auditors if triggered by an email.</p> <p>Note that db.tx_Source was not present in roundup versions before 1.4.22, so you must be running a newer version to use this detector. Read the CHANGES.txt document in the roundup source code for further details on tx_Source.</p> </section> </section> <section id="changes-to-security-and-permissions"> <h3><a class="toc-backref" href="#id20" role="doc-backlink">Changes to Security and Permissions</a></h3> <section id="restricting-the-list-of-users-that-are-assignable-to-a-task"> <h4><a class="toc-backref" href="#id21" role="doc-backlink">Restricting the list of users that are assignable to a task</a></h4> <ol class="arabic"> <li><p>In your tracker’s <code class="docutils literal notranslate"><span class="pre">schema.py</span></code>, create a new Role, say “Developer”:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addRole</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;Developer&#39;</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s1">&#39;A developer&#39;</span><span class="p">)</span> </pre></div> </div> </li> <li><p>Just after that, create a new Permission, say “Fixer”, specific to “issue”:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">p</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermission</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;Fixer&#39;</span><span class="p">,</span> <span class="n">klass</span><span class="o">=</span><span class="s1">&#39;issue&#39;</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s1">&#39;User is allowed to be assigned to fix issues&#39;</span><span class="p">)</span> </pre></div> </div> </li> <li><p>Then assign the new Permission to your “Developer” Role:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Developer&#39;</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> </pre></div> </div> </li> <li><p>In the issue item edit page (<code class="docutils literal notranslate"><span class="pre">html/issue.item.html</span></code> in your tracker directory), use the new Permission in restricting the “assignedto” list:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">select</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;assignedto&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">option</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;-1&quot;</span><span class="o">&gt;-</span> <span class="n">no</span> <span class="n">selection</span> <span class="o">-&lt;/</span><span class="n">option</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span> <span class="n">tal</span><span class="p">:</span><span class="n">repeat</span><span class="o">=</span><span class="s2">&quot;user db/user/list&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">option</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;python:user.hasPermission(</span> <span class="s1">&#39;Fixer&#39;</span><span class="p">,</span> <span class="n">context</span><span class="o">.</span><span class="n">_classname</span><span class="p">)</span><span class="s2">&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;</span> <span class="n">value</span> <span class="n">user</span><span class="o">/</span><span class="nb">id</span><span class="p">;</span> <span class="n">selected</span> <span class="n">python</span><span class="p">:</span><span class="n">user</span><span class="o">.</span><span class="n">id</span> <span class="o">==</span> <span class="n">context</span><span class="o">.</span><span class="n">assignedto</span><span class="s2">&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;user/realname&quot;</span><span class="o">&gt;&lt;/</span><span class="n">option</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">select</span><span class="o">&gt;</span> </pre></div> </div> </li> </ol> <p>For extra security, you may wish to setup an auditor to enforce the Permission requirement (install this as <code class="docutils literal notranslate"><span class="pre">assignedtoFixer.py</span></code> in your tracker <code class="docutils literal notranslate"><span class="pre">detectors</span></code> directory):</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">assignedtoMustBeFixer</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">cl</span><span class="p">,</span> <span class="n">nodeid</span><span class="p">,</span> <span class="n">newvalues</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39; Ensure the assignedto value in newvalues is used with the</span> <span class="sd"> Fixer Permission</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="k">if</span> <span class="s1">&#39;assignedto&#39;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">newvalues</span><span class="p">:</span> <span class="c1"># don&#39;t care</span> <span class="k">return</span> <span class="c1"># get the userid</span> <span class="n">userid</span> <span class="o">=</span> <span class="n">newvalues</span><span class="p">[</span><span class="s1">&#39;assignedto&#39;</span><span class="p">]</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">hasPermission</span><span class="p">(</span><span class="s1">&#39;Fixer&#39;</span><span class="p">,</span> <span class="n">userid</span><span class="p">,</span> <span class="n">cl</span><span class="o">.</span><span class="n">classname</span><span class="p">):</span> <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s1">&#39;You do not have permission to edit </span><span class="si">%s</span><span class="s1">&#39;</span><span class="o">%</span><span class="n">cl</span><span class="o">.</span><span class="n">classname</span><span class="p">)</span> <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">db</span><span class="p">):</span> <span class="n">db</span><span class="o">.</span><span class="n">issue</span><span class="o">.</span><span class="n">audit</span><span class="p">(</span><span class="s1">&#39;set&#39;</span><span class="p">,</span> <span class="n">assignedtoMustBeFixer</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">issue</span><span class="o">.</span><span class="n">audit</span><span class="p">(</span><span class="s1">&#39;create&#39;</span><span class="p">,</span> <span class="n">assignedtoMustBeFixer</span><span class="p">)</span> </pre></div> </div> <p>So now, if an edit action attempts to set “assignedto” to a user that doesn’t have the “Fixer” Permission, the error will be raised.</p> </section> <section id="users-may-only-edit-their-issues"> <h4><a class="toc-backref" href="#id22" role="doc-backlink">Users may only edit their issues</a></h4> <p>In this case, users registering themselves are granted Provisional access, meaning they have access to edit the issues they submit, but not others. We create a new Role called “Provisional User” which is granted to newly-registered users, and has limited access. One of the Permissions they have is the new “Edit Own” on issues (regular users have “Edit”.)</p> <p>First up, we create the new Role and Permission structure in <code class="docutils literal notranslate"><span class="pre">schema.py</span></code>:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">#</span> <span class="c1"># New users not approved by the admin</span> <span class="c1">#</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addRole</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s1">&#39;New user registered via web or email&#39;</span><span class="p">)</span> <span class="c1"># These users need to be able to view and create issues but only edit</span> <span class="c1"># and view their own</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="s1">&#39;Create&#39;</span><span class="p">,</span> <span class="s1">&#39;issue&#39;</span><span class="p">)</span> <span class="k">def</span> <span class="nf">own_issue</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">userid</span><span class="p">,</span> <span class="n">itemid</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39;Determine whether the userid matches the creator of the issue.&#39;&#39;&#39;</span> <span class="k">return</span> <span class="n">userid</span> <span class="o">==</span> <span class="n">db</span><span class="o">.</span><span class="n">issue</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">itemid</span><span class="p">,</span> <span class="s1">&#39;creator&#39;</span><span class="p">)</span> <span class="n">p</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermission</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;Edit&#39;</span><span class="p">,</span> <span class="n">klass</span><span class="o">=</span><span class="s1">&#39;issue&#39;</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="n">own_issue</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s1">&#39;Can only edit own issues&#39;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> <span class="n">p</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermission</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;View&#39;</span><span class="p">,</span> <span class="n">klass</span><span class="o">=</span><span class="s1">&#39;issue&#39;</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="n">own_issue</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s1">&#39;Can only view own issues&#39;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> <span class="c1"># This allows the interface to get the names of the properties</span> <span class="c1"># in the issue. Used for selecting sorting and grouping</span> <span class="c1"># on the index page.</span> <span class="n">p</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermission</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;Search&#39;</span><span class="p">,</span> <span class="n">klass</span><span class="o">=</span><span class="s1">&#39;issue&#39;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span> <span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> <span class="c1"># Assign the Permissions for issue-related classes</span> <span class="k">for</span> <span class="n">cl</span> <span class="ow">in</span> <span class="s1">&#39;file&#39;</span><span class="p">,</span> <span class="s1">&#39;msg&#39;</span><span class="p">,</span> <span class="s1">&#39;query&#39;</span><span class="p">,</span> <span class="s1">&#39;keyword&#39;</span><span class="p">:</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="s1">&#39;View&#39;</span><span class="p">,</span> <span class="n">cl</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="s1">&#39;Edit&#39;</span><span class="p">,</span> <span class="n">cl</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="s1">&#39;Create&#39;</span><span class="p">,</span> <span class="n">cl</span><span class="p">)</span> <span class="k">for</span> <span class="n">cl</span> <span class="ow">in</span> <span class="s1">&#39;priority&#39;</span><span class="p">,</span> <span class="s1">&#39;status&#39;</span><span class="p">:</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="s1">&#39;View&#39;</span><span class="p">,</span> <span class="n">cl</span><span class="p">)</span> <span class="c1"># and give the new users access to the web and email interface</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="s1">&#39;Web Access&#39;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="s1">&#39;Email Access&#39;</span><span class="p">)</span> <span class="c1"># make sure they can view &amp; edit their own user record</span> <span class="k">def</span> <span class="nf">own_record</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">userid</span><span class="p">,</span> <span class="n">itemid</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39;Determine whether the userid matches the item being accessed.&#39;&#39;&#39;</span> <span class="k">return</span> <span class="n">userid</span> <span class="o">==</span> <span class="n">itemid</span> <span class="n">p</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermission</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;View&#39;</span><span class="p">,</span> <span class="n">klass</span><span class="o">=</span><span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="n">own_record</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&quot;User is allowed to view their own user details&quot;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> <span class="n">p</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermission</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;Edit&#39;</span><span class="p">,</span> <span class="n">klass</span><span class="o">=</span><span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="n">own_record</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&quot;User is allowed to edit their own user details&quot;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;Provisional User&#39;</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> </pre></div> </div> <p>Then, in <code class="docutils literal notranslate"><span class="pre">config.ini</span></code>, we change the Role assigned to newly-registered users, replacing the existing <code class="docutils literal notranslate"><span class="pre">'User'</span></code> values:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[</span><span class="n">main</span><span class="p">]</span> <span class="o">...</span> <span class="n">new_web_user_roles</span> <span class="o">=</span> <span class="n">Provisional</span> <span class="n">User</span> <span class="n">new_email_user_roles</span> <span class="o">=</span> <span class="n">Provisional</span> <span class="n">User</span> </pre></div> </div> </section> <section id="all-users-may-only-view-and-edit-issues-files-and-messages-they-create"> <h4><a class="toc-backref" href="#id23" role="doc-backlink">All users may only view and edit issues, files and messages they create</a></h4> <p>Replace the standard “classic” tracker View and Edit Permission assignments for the “issue”, “file” and “msg” classes with the following:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">checker</span><span class="p">(</span><span class="n">klass</span><span class="p">):</span> <span class="k">def</span> <span class="nf">check</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">userid</span><span class="p">,</span> <span class="n">itemid</span><span class="p">,</span> <span class="n">klass</span><span class="o">=</span><span class="n">klass</span><span class="p">):</span> <span class="k">return</span> <span class="n">db</span><span class="o">.</span><span class="n">getclass</span><span class="p">(</span><span class="n">klass</span><span class="p">)</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">itemid</span><span class="p">,</span> <span class="s1">&#39;creator&#39;</span><span class="p">)</span> <span class="o">==</span> <span class="n">userid</span> <span class="k">return</span> <span class="n">check</span> <span class="k">for</span> <span class="n">cl</span> <span class="ow">in</span> <span class="s1">&#39;issue&#39;</span><span class="p">,</span> <span class="s1">&#39;file&#39;</span><span class="p">,</span> <span class="s1">&#39;msg&#39;</span><span class="p">:</span> <span class="n">p</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermission</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;View&#39;</span><span class="p">,</span> <span class="n">klass</span><span class="o">=</span><span class="n">cl</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="n">checker</span><span class="p">(</span><span class="n">cl</span><span class="p">),</span> <span class="n">description</span><span class="o">=</span><span class="s1">&#39;User can view only if creator.&#39;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> <span class="n">p</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermission</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;Edit&#39;</span><span class="p">,</span> <span class="n">klass</span><span class="o">=</span><span class="n">cl</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="n">checker</span><span class="p">(</span><span class="n">cl</span><span class="p">),</span> <span class="n">description</span><span class="o">=</span><span class="s1">&#39;User can edit only if creator.&#39;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span><span class="p">(</span><span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="s1">&#39;Create&#39;</span><span class="p">,</span> <span class="n">cl</span><span class="p">)</span> <span class="c1"># This allows the interface to get the names of the properties</span> <span class="c1"># in the issue. Used for selecting sorting and grouping</span> <span class="c1"># on the index page.</span> <span class="n">p</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermission</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">&#39;Search&#39;</span><span class="p">,</span> <span class="n">klass</span><span class="o">=</span><span class="s1">&#39;issue&#39;</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">addPermissionToRole</span> <span class="p">(</span><span class="s1">&#39;User&#39;</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> </pre></div> </div> </section> <section id="moderating-user-registration"> <h4><a class="toc-backref" href="#id24" role="doc-backlink">Moderating user registration</a></h4> <p>You could set up new-user moderation in a public tracker by:</p> <ol class="arabic simple"> <li><p>creating a new highly-restricted user role “Pending”,</p></li> <li><p>set the config new_web_user_roles and/or new_email_user_roles to that role,</p></li> <li><p>have an auditor that emails you when new users are created with that role using roundup.mailer</p></li> <li><p>edit the role to “User” for valid users.</p></li> </ol> <p>Some simple javascript might help in the last step. If you have high volume you could search for all currently-Pending users and do a bulk edit of all their roles at once (again probably with some simple javascript help).</p> </section> </section> <section id="changes-to-the-web-user-interface"> <h3><a class="toc-backref" href="#id25" role="doc-backlink">Changes to the Web User Interface</a></h3> <section id="adding-action-links-to-the-index-page"> <h4><a class="toc-backref" href="#id26" role="doc-backlink">Adding action links to the index page</a></h4> <p>Add a column to the <code class="docutils literal notranslate"><span class="pre">item.index.html</span></code> template. In that column add a form to trigger the action. Note: the form must use the POST method for security.</p> <p>Resolving the issue:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">form</span> <span class="n">method</span><span class="o">=</span><span class="s2">&quot;POST&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;action string:issue${i/id}&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">button</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;structure</span> <span class="n">python</span><span class="p">:</span><span class="n">context</span><span class="o">.</span><span class="n">submit</span><span class="p">(</span><span class="n">label</span><span class="o">=</span><span class="s1">&#39;resolve&#39;</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s1">&#39;edit&#39;</span><span class="p">)</span><span class="s2">&quot; /&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;status&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;resolved&quot;</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">form</span><span class="o">&gt;</span> </pre></div> </div> <p>“Take” the issue:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">form</span> <span class="n">method</span><span class="o">=</span><span class="s2">&quot;POST&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;action string:issue${i/id}&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">button</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;structure</span> <span class="n">python</span><span class="p">:</span><span class="n">context</span><span class="o">.</span><span class="n">submit</span><span class="p">(</span><span class="n">label</span><span class="o">=</span><span class="s1">&#39;take&#39;</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s1">&#39;edit&#39;</span><span class="p">)</span><span class="s2">&quot; /&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;assignedto&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;value request/user/id&quot;</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">form</span><span class="o">&gt;</span> </pre></div> </div> <p>… and so on.</p> </section> <section id="colouring-the-rows-in-the-issue-index-according-to-priority"> <h4><a class="toc-backref" href="#id27" role="doc-backlink">Colouring the rows in the issue index according to priority</a></h4> <p>A simple <code class="docutils literal notranslate"><span class="pre">tal:attributes</span></code> statement will do the bulk of the work here. In the <code class="docutils literal notranslate"><span class="pre">issue.index.html</span></code> template, add this to the <code class="docutils literal notranslate"><span class="pre">&lt;tr&gt;</span></code> that displays the rows of data:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tr</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;class string:priority-${i/priority/plain}&quot;</span><span class="o">&gt;</span> </pre></div> </div> <p>and then in your stylesheet (<code class="docutils literal notranslate"><span class="pre">style.css</span></code>) specify the colouring for the different priorities, as follows:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">tr</span><span class="o">.</span><span class="n">priority</span><span class="o">-</span><span class="n">critical</span> <span class="n">td</span> <span class="p">{</span> <span class="n">background</span><span class="o">-</span><span class="n">color</span><span class="p">:</span> <span class="n">red</span><span class="p">;</span> <span class="p">}</span> <span class="n">tr</span><span class="o">.</span><span class="n">priority</span><span class="o">-</span><span class="n">urgent</span> <span class="n">td</span> <span class="p">{</span> <span class="n">background</span><span class="o">-</span><span class="n">color</span><span class="p">:</span> <span class="n">orange</span><span class="p">;</span> <span class="p">}</span> </pre></div> </div> <p>and so on, with far less offensive colours :)</p> </section> <section id="editing-multiple-items-in-an-index-view"> <h4><a class="toc-backref" href="#id28" role="doc-backlink">Editing multiple items in an index view</a></h4> <p>To edit the status of all items in the item index view, edit the <code class="docutils literal notranslate"><span class="pre">issue.index.html</span></code>:</p> <ol class="arabic"> <li><p>add a form around the listing table (separate from the existing index-page form), so at the top it reads:</p> <div class="sphinx-tabs docutils container"> <div aria-label="Tabbed content" role="tablist"><button aria-controls="panel-0-VEFM" aria-selected="true" class="sphinx-tabs-tab code-tab group-tab" id="tab-0-VEFM" name="VEFM" role="tab" tabindex="0">TAL</button><button aria-controls="panel-0-SmluamEy" aria-selected="false" class="sphinx-tabs-tab code-tab group-tab" id="tab-0-SmluamEy" name="SmluamEy" role="tab" tabindex="-1">Jinja2</button></div><div aria-labelledby="tab-0-VEFM" class="sphinx-tabs-panel code-tab group-tab" id="panel-0-VEFM" name="VEFM" role="tabpanel" tabindex="0"><div class="highlight-html notranslate"><div class="highlight"><pre><span></span><span class="p">&lt;</span><span class="nt">form</span> <span class="na">method</span><span class="o">=</span><span class="s">&quot;POST&quot;</span> <span class="na">tal:attributes</span><span class="o">=</span><span class="s">&quot;action request/classname&quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">table</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;list&quot;</span><span class="p">&gt;</span> </pre></div> </div> </div><div aria-labelledby="tab-0-SmluamEy" class="sphinx-tabs-panel code-tab group-tab" hidden="true" id="panel-0-SmluamEy" name="SmluamEy" role="tabpanel" tabindex="0"><div class="highlight-html notranslate"><div class="highlight"><pre><span></span><span class="p">&lt;</span><span class="nt">form</span> <span class="na">method</span><span class="o">=</span><span class="s">&quot;POST&quot;</span> <span class="na">action</span><span class="o">=</span><span class="s">&#39;issue{{ context.id }}&#39;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#39;form-inline&#39;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">table</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;list&quot;</span><span class="p">&gt;</span> </pre></div> </div> </div></div> <p>and at the bottom of that table add:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span> <span class="o">&lt;/</span><span class="n">table</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">form</span> </pre></div> </div> <p>making sure you match the <code class="docutils literal notranslate"><span class="pre">&lt;/table&gt;</span></code> from the list table, not the navigation table or the subsequent form table.</p> </li> <li><p>in the display for the issue property, change:</p> <div class="sphinx-tabs docutils container"> <div aria-label="Tabbed content" role="tablist"><button aria-controls="panel-1-VEFM" aria-selected="true" class="sphinx-tabs-tab code-tab group-tab" id="tab-1-VEFM" name="VEFM" role="tab" tabindex="0">TAL</button><button aria-controls="panel-1-SmluamEy" aria-selected="false" class="sphinx-tabs-tab code-tab group-tab" id="tab-1-SmluamEy" name="SmluamEy" role="tab" tabindex="-1">Jinja2</button></div><div aria-labelledby="tab-1-VEFM" class="sphinx-tabs-panel code-tab group-tab" id="panel-1-VEFM" name="VEFM" role="tabpanel" tabindex="0"><div class="highlight-html notranslate"><div class="highlight"><pre><span></span><span class="p">&lt;</span><span class="nt">td</span> <span class="na">tal:condition</span><span class="o">=</span><span class="s">&quot;request/show/status&quot;</span> <span class="na">tal:content</span><span class="o">=</span><span class="s">&quot;python:i.status.plain() or default&quot;</span><span class="p">&gt;</span><span class="ni">&amp;nbsp;</span><span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span> </pre></div> </div> </div><div aria-labelledby="tab-1-SmluamEy" class="sphinx-tabs-panel code-tab group-tab" hidden="true" id="panel-1-SmluamEy" name="SmluamEy" role="tabpanel" tabindex="0"><div class="highlight-html notranslate"><div class="highlight"><pre><span></span>{% if request.show.status %} <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>{{ issue.status.plain()|u }}<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span> {% endif %} </pre></div> </div> </div></div> <p>to:</p> <div class="sphinx-tabs docutils container"> <div aria-label="Tabbed content" role="tablist"><button aria-controls="panel-2-VEFM" aria-selected="true" class="sphinx-tabs-tab code-tab group-tab" id="tab-2-VEFM" name="VEFM" role="tab" tabindex="0">TAL</button><button aria-controls="panel-2-SmluamEy" aria-selected="false" class="sphinx-tabs-tab code-tab group-tab" id="tab-2-SmluamEy" name="SmluamEy" role="tab" tabindex="-1">Jinja2</button></div><div aria-labelledby="tab-2-VEFM" class="sphinx-tabs-panel code-tab group-tab" id="panel-2-VEFM" name="VEFM" role="tabpanel" tabindex="0"><div class="highlight-html notranslate"><div class="highlight"><pre><span></span><span class="p">&lt;</span><span class="nt">td</span> <span class="na">tal:condition</span><span class="o">=</span><span class="s">&quot;request/show/status&quot;</span> <span class="na">tal:content</span><span class="o">=</span><span class="s">&quot;structure i/status/field&quot;</span><span class="p">&gt;</span><span class="ni">&amp;nbsp;</span><span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span> </pre></div> </div> </div><div aria-labelledby="tab-2-SmluamEy" class="sphinx-tabs-panel code-tab group-tab" hidden="true" id="panel-2-SmluamEy" name="SmluamEy" role="tabpanel" tabindex="0"><div class="highlight-html notranslate"><div class="highlight"><pre><span></span>{% if request.show.status %} <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>{{ issue.status.menu()|u|safe }}<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span> {% endif %} <span class="cm">&lt;!-- untested --&gt;</span> </pre></div> </div> </div></div> <p>this will result in an edit field for the status property.</p> </li> <li><p>after the <code class="docutils literal notranslate"><span class="pre">tal:block</span></code> which lists the index items (marked by <code class="docutils literal notranslate"><span class="pre">tal:repeat=&quot;i</span> <span class="pre">batch&quot;</span></code>) add a new table row:</p> <div class="sphinx-tabs docutils container"> <div aria-label="Tabbed content" role="tablist"><button aria-controls="panel-3-VEFM" aria-selected="true" class="sphinx-tabs-tab code-tab group-tab" id="tab-3-VEFM" name="VEFM" role="tab" tabindex="0">TAL</button><button aria-controls="panel-3-SmluamEy" aria-selected="false" class="sphinx-tabs-tab code-tab group-tab" id="tab-3-SmluamEy" name="SmluamEy" role="tab" tabindex="-1">Jinja2</button></div><div aria-labelledby="tab-3-VEFM" class="sphinx-tabs-panel code-tab group-tab" id="panel-3-VEFM" name="VEFM" role="tabpanel" tabindex="0"><div class="highlight-html notranslate"><div class="highlight"><pre><span></span><span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">td</span> <span class="na">tal:attributes</span><span class="o">=</span><span class="s">&quot;colspan python:len(request.columns)&quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">name</span><span class="o">=</span><span class="s">&quot;@csrf&quot;</span> <span class="na">type</span><span class="o">=</span><span class="s">&quot;hidden&quot;</span> <span class="na">tal:attributes</span><span class="o">=</span><span class="s">&quot;value python:utils.anti_csrf_nonce()&quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&quot;submit&quot;</span> <span class="na">value</span><span class="o">=</span><span class="s">&quot; Save Changes &quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&quot;hidden&quot;</span> <span class="na">name</span><span class="o">=</span><span class="s">&quot;@action&quot;</span> <span class="na">value</span><span class="o">=</span><span class="s">&quot;edit&quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">tal:block</span> <span class="na">replace</span><span class="o">=</span><span class="s">&quot;structure request/indexargs_form&quot;</span> <span class="p">/&gt;</span> <span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span> </pre></div> </div> </div><div aria-labelledby="tab-3-SmluamEy" class="sphinx-tabs-panel code-tab group-tab" hidden="true" id="panel-3-SmluamEy" name="SmluamEy" role="tabpanel" tabindex="0"><div class="highlight-html notranslate"><div class="highlight"><pre><span></span>To Be Written </pre></div> </div> </div></div> <p>which gives us a submit button, indicates that we are performing an edit on any changed statuses, and provides a defense against cross site request forgery attacks.</p> <p>The final <code class="docutils literal notranslate"><span class="pre">tal:block</span></code> will make sure that the current index view parameters (filtering, columns, etc) will be used in rendering the next page (the results of the editing).</p> </li> </ol> </section> <section id="displaying-only-message-summaries-in-the-issue-display"> <h4><a class="toc-backref" href="#id29" role="doc-backlink">Displaying only message summaries in the issue display</a></h4> <p>Alter the <code class="docutils literal notranslate"><span class="pre">issue.item</span></code> template section for messages to:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">table</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;messages&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;context/messages&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;&lt;</span><span class="n">th</span> <span class="n">colspan</span><span class="o">=</span><span class="s2">&quot;5&quot;</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;header&quot;</span><span class="o">&gt;</span><span class="n">Messages</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span> <span class="n">tal</span><span class="p">:</span><span class="n">repeat</span><span class="o">=</span><span class="s2">&quot;msg context/messages&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;&lt;</span><span class="n">a</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;href string:msg${msg/id}&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;string:msg${msg/id}&quot;</span><span class="o">&gt;&lt;/</span><span class="n">a</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;msg/author&quot;</span><span class="o">&gt;</span><span class="n">author</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;date&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;msg/date/pretty&quot;</span><span class="o">&gt;</span><span class="n">date</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;msg/summary&quot;</span><span class="o">&gt;</span><span class="n">summary</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">a</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;href string:?@remove@messages=${msg/id}&amp;@action=edit&quot;</span><span class="o">&gt;</span> <span class="n">remove</span><span class="o">&lt;/</span><span class="n">a</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">table</span><span class="o">&gt;</span> </pre></div> </div> </section> <section id="enabling-display-of-either-message-summaries-or-the-entire-messages"> <h4><a class="toc-backref" href="#id30" role="doc-backlink">Enabling display of either message summaries or the entire messages</a></h4> <p>This is pretty simple - all we need to do is copy the code from the example <a class="reference internal" href="#displaying-only-message-summaries-in-the-issue-display">displaying only message summaries in the issue display</a> into our template alongside the summary display, and then introduce a switch that shows either the one or the other. We’ll use a new form variable, <code class="docutils literal notranslate"><span class="pre">&#64;whole_messages</span></code> to achieve this:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">table</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;messages&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;context/messages&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;not:request/form/@whole_messages/value | python:0&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;&lt;</span><span class="n">th</span> <span class="n">colspan</span><span class="o">=</span><span class="s2">&quot;3&quot;</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;header&quot;</span><span class="o">&gt;</span><span class="n">Messages</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span> <span class="n">colspan</span><span class="o">=</span><span class="s2">&quot;2&quot;</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;header&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">a</span> <span class="n">href</span><span class="o">=</span><span class="s2">&quot;?@whole_messages=yes&quot;</span><span class="o">&gt;</span><span class="n">show</span> <span class="n">entire</span> <span class="n">messages</span><span class="o">&lt;/</span><span class="n">a</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span> <span class="n">tal</span><span class="p">:</span><span class="n">repeat</span><span class="o">=</span><span class="s2">&quot;msg context/messages&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;&lt;</span><span class="n">a</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;href string:msg${msg/id}&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;string:msg${msg/id}&quot;</span><span class="o">&gt;&lt;/</span><span class="n">a</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;msg/author&quot;</span><span class="o">&gt;</span><span class="n">author</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;date&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;msg/date/pretty&quot;</span><span class="o">&gt;</span><span class="n">date</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;msg/summary&quot;</span><span class="o">&gt;</span><span class="n">summary</span><span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">a</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;href string:?@remove@messages=${msg/id}&amp;@action=edit&quot;</span><span class="o">&gt;</span><span class="n">remove</span><span class="o">&lt;/</span><span class="n">a</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;request/form/@whole_messages/value | python:0&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;&lt;</span><span class="n">th</span> <span class="n">colspan</span><span class="o">=</span><span class="s2">&quot;2&quot;</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;header&quot;</span><span class="o">&gt;</span><span class="n">Messages</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;header&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">a</span> <span class="n">href</span><span class="o">=</span><span class="s2">&quot;?@whole_messages=&quot;</span><span class="o">&gt;</span><span class="n">show</span> <span class="n">only</span> <span class="n">summaries</span><span class="o">&lt;/</span><span class="n">a</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span> <span class="n">tal</span><span class="p">:</span><span class="n">repeat</span><span class="o">=</span><span class="s2">&quot;msg context/messages&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;msg/author&quot;</span><span class="o">&gt;</span><span class="n">author</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span> <span class="n">class</span><span class="o">=</span><span class="s2">&quot;date&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;msg/date/pretty&quot;</span><span class="o">&gt;</span><span class="n">date</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span> <span class="n">style</span><span class="o">=</span><span class="s2">&quot;text-align: right&quot;</span><span class="o">&gt;</span> <span class="p">(</span><span class="o">&lt;</span><span class="n">a</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;href string:?@remove@messages=${msg/id}&amp;@action=edit&quot;</span><span class="o">&gt;</span><span class="n">remove</span><span class="o">&lt;/</span><span class="n">a</span><span class="o">&gt;</span><span class="p">)</span> <span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;&lt;</span><span class="n">td</span> <span class="n">colspan</span><span class="o">=</span><span class="s2">&quot;3&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;msg/content&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">table</span><span class="o">&gt;</span> </pre></div> </div> </section> <section id="setting-up-a-wizard-or-druid-for-controlled-adding-of-issues"> <h4><a class="toc-backref" href="#id31" role="doc-backlink">Setting up a “wizard” (or “druid”) for controlled adding of issues</a></h4> <ol class="arabic"> <li><p>Set up the page templates you wish to use for data input. My wizard is going to be a two-step process: first figuring out what category of issue the user is submitting, and then getting details specific to that category. The first page includes a table of help, explaining what the category names mean, and then the core of the form:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">form</span> <span class="n">method</span><span class="o">=</span><span class="s2">&quot;POST&quot;</span> <span class="n">onSubmit</span><span class="o">=</span><span class="s2">&quot;return submit_once()&quot;</span> <span class="n">enctype</span><span class="o">=</span><span class="s2">&quot;multipart/form-data&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@csrf&quot;</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;value python:utils.anti_csrf_nonce()&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@template&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;add_page1&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@action&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;page1_submit&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">strong</span><span class="o">&gt;</span><span class="n">Category</span><span class="p">:</span><span class="o">&lt;/</span><span class="n">strong</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span> <span class="n">tal</span><span class="p">:</span><span class="n">replace</span><span class="o">=</span><span class="s2">&quot;structure context/category/menu&quot;</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;submit&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;Continue&quot;</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">form</span><span class="o">&gt;</span> </pre></div> </div> <p>The next page has the usual issue entry information, with the addition of the following form fragments:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">form</span> <span class="n">method</span><span class="o">=</span><span class="s2">&quot;POST&quot;</span> <span class="n">onSubmit</span><span class="o">=</span><span class="s2">&quot;return submit_once()&quot;</span> <span class="n">enctype</span><span class="o">=</span><span class="s2">&quot;multipart/form-data&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;context/is_edit_ok&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">define</span><span class="o">=</span><span class="s2">&quot;cat request/form/category/value&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@csrf&quot;</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;value python:utils.anti_csrf_nonce()&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@template&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;add_page2&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;@required&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;title&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;hidden&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;category&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">attributes</span><span class="o">=</span><span class="s2">&quot;value cat&quot;</span><span class="o">&gt;</span> <span class="o">.</span> <span class="o">.</span> <span class="o">.</span> <span class="o">&lt;/</span><span class="n">form</span><span class="o">&gt;</span> </pre></div> </div> <p>Note that later in the form, I use the value of “cat” to decide which form elements should be displayed. For example:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;python:cat in &#39;6 10 13 14 15 16 17&#39;.split()&quot;</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Operating</span> <span class="n">System</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;structure context/os/field&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">th</span><span class="o">&gt;</span><span class="n">Web</span> <span class="n">Browser</span><span class="o">&lt;/</span><span class="n">th</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">td</span> <span class="n">tal</span><span class="p">:</span><span class="n">content</span><span class="o">=</span><span class="s2">&quot;structure context/browser/field&quot;</span><span class="o">&gt;&lt;/</span><span class="n">td</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tr</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">tal</span><span class="p">:</span><span class="n">block</span><span class="o">&gt;</span> </pre></div> </div> <p>… the above section will only be displayed if the category is one of 6, 10, 13, 14, 15, 16 or 17.</p> </li> </ol> <ol class="arabic" start="3"> <li><p>Determine what actions need to be taken between the pages - these are usually to validate user choices and determine what page is next. Now encode those actions in a new <code class="docutils literal notranslate"><span class="pre">Action</span></code> class (see <a class="reference external" href="reference.html#defining-new-web-actions">defining new web actions</a>):</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">roundup.cgi.actions</span> <span class="kn">import</span> <span class="n">Action</span> <span class="k">class</span> <span class="nc">Page1SubmitAction</span><span class="p">(</span><span class="n">Action</span><span class="p">):</span> <span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39; Verify that the user has selected a category, and then move</span> <span class="sd"> on to page 2.</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="n">category</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">form</span><span class="p">[</span><span class="s1">&#39;category&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">value</span> <span class="k">if</span> <span class="n">category</span> <span class="o">==</span> <span class="s1">&#39;-1&#39;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">add_error_message</span><span class="p">(</span><span class="s1">&#39;You must select a category of report&#39;</span><span class="p">)</span> <span class="k">return</span> <span class="c1"># everything&#39;s ok, move on to the next page</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">template</span> <span class="o">=</span> <span class="s1">&#39;add_page2&#39;</span> <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">instance</span><span class="p">):</span> <span class="n">instance</span><span class="o">.</span><span class="n">registerAction</span><span class="p">(</span><span class="s1">&#39;page1_submit&#39;</span><span class="p">,</span> <span class="n">Page1SubmitAction</span><span class="p">)</span> </pre></div> </div> </li> <li><p>Use the usual “new” action as the <code class="docutils literal notranslate"><span class="pre">&#64;action</span></code> on the final page, and you’re done (the standard context/submit method can do this for you).</p></li> </ol> </section> <section id="silent-submit"> <h4><a class="toc-backref" href="#id32" role="doc-backlink">Silent Submit</a></h4> <p>When working on an issue, most of the time the people on the nosy list need to be notified of changes. There are cases where a user wants to add a comment to an issue and not bother other users on the nosy list. This feature is called Silent Submit because it allows the user to silently modify an issue and not tell anyone.</p> <p>There are several parts to this change. The main activity part involves editing the stock detectors/nosyreaction.py file in your tracker. Insert the following lines near the top of the nosyreaction function:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Did user click button to do a silent change?</span> <span class="k">try</span><span class="p">:</span> <span class="k">if</span> <span class="n">db</span><span class="o">.</span><span class="n">web</span><span class="p">[</span><span class="s1">&#39;submit&#39;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&quot;silent_change&quot;</span><span class="p">:</span> <span class="k">return</span> <span class="k">except</span> <span class="p">(</span><span class="ne">AttributeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">)</span> <span class="k">as</span> <span class="n">err</span><span class="p">:</span> <span class="c1"># The web attribute or submit key don&#39;t exist.</span> <span class="c1"># That&#39;s fine. We were probably triggered by an email</span> <span class="c1"># or cli based change.</span> <span class="k">pass</span> </pre></div> </div> <p>This checks the submit button to see if it is the silent type. If there are exceptions trying to make that determination they are ignored and processing continues. You may wonder how db.web gets set. This is done by creating an extension. Add the file extensions/edit.py with this content:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">roundup.cgi.actions</span> <span class="kn">import</span> <span class="n">EditItemAction</span> <span class="k">class</span> <span class="nc">Edit2Action</span><span class="p">(</span><span class="n">EditItemAction</span><span class="p">):</span> <span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">web</span> <span class="o">=</span> <span class="p">{}</span> <span class="c1"># create the dict</span> <span class="c1"># populate the dict by getting the value of the submit_button</span> <span class="c1"># element from the form.</span> <span class="bp">self</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">web</span><span class="p">[</span><span class="s1">&#39;submit&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">form</span><span class="p">[</span><span class="s1">&#39;submit_button&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">value</span> <span class="c1"># call the core EditItemAction to process the edit.</span> <span class="n">EditItemAction</span><span class="o">.</span><span class="n">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">instance</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39;Override the default edit action with this new version&#39;&#39;&#39;</span> <span class="n">instance</span><span class="o">.</span><span class="n">registerAction</span><span class="p">(</span><span class="s1">&#39;edit&#39;</span><span class="p">,</span> <span class="n">Edit2Action</span><span class="p">)</span> </pre></div> </div> <p>This code is a wrapper for the Roundup EditItemAction. It checks the form’s submit button to save the value element. The rest of the changes needed for the Silent Submit feature involves editing html/issue.item.html to add the silent submit button. In the stock issue.item.html the submit button is on a line that contains “submit button”. Replace that line with something like the following:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">&lt;</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;submit&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;submit_button&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;context/is_edit_ok&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;Submit Changes&quot;</span><span class="o">&gt;&amp;</span><span class="n">nbsp</span><span class="p">;</span> <span class="o">&lt;</span><span class="n">button</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;submit&quot;</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;submit_button&quot;</span> <span class="n">tal</span><span class="p">:</span><span class="n">condition</span><span class="o">=</span><span class="s2">&quot;context/is_edit_ok&quot;</span> <span class="n">title</span><span class="o">=</span><span class="s2">&quot;Click this to submit but not send nosy email.&quot;</span> <span class="n">value</span><span class="o">=</span><span class="s2">&quot;silent_change&quot;</span> <span class="n">i18n</span><span class="p">:</span><span class="n">translate</span><span class="o">=</span><span class="s2">&quot;&quot;</span><span class="o">&gt;</span> <span class="n">Silent</span> <span class="n">Change</span><span class="o">&lt;/</span><span class="n">button</span><span class="o">&gt;</span> </pre></div> </div> <p>Note the difference in the value attribute for the two submit buttons. The value “silent_change” in the button specification must match the string in the nosy reaction function.</p> </section> </section> <section id="changing-how-the-core-code-works"> <h3><a class="toc-backref" href="#id33" role="doc-backlink">Changing How the Core Code Works</a></h3> <section id="changing-cache-control-headers"> <span id="index-4"></span><h4><a class="toc-backref" href="#id34" role="doc-backlink">Changing Cache-Control Headers</a></h4> <p>The Client class in cgi/client.py has a lookup table that is used to set the Cache-Control headers for static files. The entries in this table are set from interfaces.py using:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">roundup.cgi.client</span> <span class="kn">import</span> <span class="n">Client</span> <span class="n">Client</span><span class="o">.</span><span class="n">Cache_Control</span><span class="p">[</span><span class="s1">&#39;text/css&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&quot;public, max-age=3600&quot;</span> <span class="n">Client</span><span class="o">.</span><span class="n">Cache_Control</span><span class="p">[</span><span class="s1">&#39;application/javascript&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&quot;public, max-age=30&quot;</span> <span class="n">Client</span><span class="o">.</span><span class="n">Cache_Control</span><span class="p">[</span><span class="s1">&#39;rss.xml&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&quot;public, max-age=900&quot;</span> <span class="n">Client</span><span class="o">.</span><span class="n">Cache_Control</span><span class="p">[</span><span class="s1">&#39;local.js&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&quot;public, max-age=7200&quot;</span> </pre></div> </div> <p>In this case static files delivered using &#64;&#64;file will have cache headers set. These files are searched for along the <cite>static_files</cite> path in the tracker’s <cite>config.ini</cite>. In the example above:</p> <ul class="simple"> <li><p>a css file (e.g. &#64;&#64;file/style.css) will be cached for an hour</p></li> <li><p>javascript files (e.g. &#64;&#64;file/libraries/jquery.js) will be cached for 30 seconds</p></li> <li><p>a file named rss.xml will be cached for 15 minutes</p></li> <li><p>a file named local.js will be cached for 2 hours</p></li> </ul> <p>Note that a file name match overrides the mime type settings.</p> </section> <section id="implement-password-complexity-checking"> <span id="index-5"></span><h4><a class="toc-backref" href="#id35" role="doc-backlink">Implement Password Complexity Checking</a></h4> <p id="index-6">This example uses the <a class="reference external" href="https://github.com/dwolfhub/zxcvbn-python">zxcvbn</a> module that you can place in the zxcvbn subdirectory of your tracker’s lib directory.</p> <p>If you add this to the interfaces.py file in the root directory of your tracker (same place as schema.py):</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">roundup.password</span> <span class="k">as</span> <span class="nn">password</span> <span class="kn">from</span> <span class="nn">roundup.exceptions</span> <span class="kn">import</span> <span class="n">Reject</span> <span class="kn">from</span> <span class="nn">zxcvbn</span> <span class="kn">import</span> <span class="n">zxcvbn</span> <span class="c1"># monkey patch the setPassword method with this method</span> <span class="c1"># that checks password strength.</span> <span class="n">origPasswordFunc</span> <span class="o">=</span> <span class="n">password</span><span class="o">.</span><span class="n">Password</span><span class="o">.</span><span class="n">setPassword</span> <span class="k">def</span> <span class="nf">mpPasswordFunc</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">plaintext</span><span class="p">,</span> <span class="n">scheme</span><span class="p">,</span> <span class="n">config</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span> <span class="w"> </span><span class="sd">&quot;&quot;&quot; Replace the password set function with one that</span> <span class="sd"> verifies that the password is complex enough. It</span> <span class="sd"> has to be done at this point and not in an auditor</span> <span class="sd"> as the auditor only sees the encrypted password.</span> <span class="sd"> &quot;&quot;&quot;</span> <span class="n">results</span> <span class="o">=</span> <span class="n">zxcvbn</span><span class="p">(</span><span class="n">plaintext</span><span class="p">)</span> <span class="k">if</span> <span class="n">results</span><span class="p">[</span><span class="s1">&#39;score&#39;</span><span class="p">]</span> <span class="o">&lt;</span> <span class="mi">3</span><span class="p">:</span> <span class="n">l</span> <span class="o">=</span> <span class="p">[]</span> <span class="nb">map</span><span class="p">(</span><span class="n">l</span><span class="o">.</span><span class="n">extend</span><span class="p">,</span> <span class="p">[[</span><span class="n">results</span><span class="p">[</span><span class="s1">&#39;feedback&#39;</span><span class="p">][</span><span class="s1">&#39;warning&#39;</span><span class="p">]],</span> <span class="n">results</span><span class="p">[</span><span class="s1">&#39;feedback&#39;</span><span class="p">][</span><span class="s1">&#39;suggestions&#39;</span><span class="p">]])</span> <span class="n">errormsg</span> <span class="o">=</span> <span class="s2">&quot; &quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">l</span><span class="p">)</span> <span class="k">raise</span> <span class="n">Reject</span> <span class="p">(</span><span class="s2">&quot;Password is too easy to guess. &quot;</span> <span class="o">+</span> <span class="n">errormsg</span><span class="p">)</span> <span class="k">return</span> <span class="n">origPasswordFunc</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">plaintext</span><span class="p">,</span> <span class="n">scheme</span><span class="p">,</span> <span class="n">config</span><span class="o">=</span><span class="n">config</span><span class="p">)</span> <span class="n">password</span><span class="o">.</span><span class="n">Password</span><span class="o">.</span><span class="n">setPassword</span> <span class="o">=</span> <span class="n">mpPasswordFunc</span> </pre></div> </div> <p>it replaces the setPassword method in the Password class. The new version validates that the password is sufficiently complex. Then it passes off the setting of password to the original method.</p> </section> <section id="enhance-time-intervals-forms"> <span id="index-7"></span><h4><a class="toc-backref" href="#id36" role="doc-backlink">Enhance Time Intervals Forms</a></h4> <p>To make the user interface easier to use, you may want to support other forms for intervals. For example you can support an interval like 1.5 by interpreting it the same as 1:30 (1 hour 30 minutes). Also you can allow a bare integer (e.g. 45) as a number of minutes.</p> <p>To do this we intercept the from_raw method of the Interval class in hyperdb.py with:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">roundup.hyperdb</span> <span class="k">as</span> <span class="nn">hyperdb</span> <span class="n">origFrom_Raw</span> <span class="o">=</span> <span class="n">hyperdb</span><span class="o">.</span><span class="n">Interval</span><span class="o">.</span><span class="n">from_raw</span> <span class="k">def</span> <span class="nf">normalizeperiod</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="o">**</span><span class="n">kw</span><span class="p">):</span> <span class="w"> </span><span class="sd">&#39;&#39;&#39; Convert alternate time forms into standard interval format</span> <span class="sd"> [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]</span> <span class="sd"> if value is float, it&#39;s hour and fractional hours</span> <span class="sd"> if value is integer, it&#39;s number of minutes</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="k">if</span> <span class="s2">&quot;:&quot;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">value</span><span class="p">:</span> <span class="c1"># Not a specified interval</span> <span class="c1"># if int consider number of minutes</span> <span class="k">try</span><span class="p">:</span> <span class="n">isMinutes</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="n">minutes</span> <span class="o">=</span> <span class="n">isMinutes</span><span class="o">%</span><span class="mi">60</span> <span class="n">hours</span> <span class="o">=</span> <span class="p">(</span><span class="n">isMinutes</span> <span class="o">-</span> <span class="n">minutes</span><span class="p">)</span> <span class="o">/</span> <span class="mi">60</span> <span class="n">value</span> <span class="o">=</span> <span class="s2">&quot;</span><span class="si">%d</span><span class="s2">:</span><span class="si">%d</span><span class="s2">&quot;</span><span class="o">%</span><span class="p">(</span><span class="n">hours</span><span class="p">,</span><span class="n">minutes</span><span class="p">)</span> <span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span> <span class="k">pass</span> <span class="c1"># if float, consider it number of hours and fractional hours.</span> <span class="kn">import</span> <span class="nn">math</span> <span class="k">try</span><span class="p">:</span> <span class="n">afterdecimal</span><span class="p">,</span> <span class="n">beforedecimal</span> <span class="o">=</span> <span class="n">math</span><span class="o">.</span><span class="n">modf</span><span class="p">(</span><span class="nb">float</span><span class="p">(</span><span class="n">value</span><span class="p">))</span> <span class="n">value</span> <span class="o">=</span> <span class="s2">&quot;</span><span class="si">%d</span><span class="s2">:</span><span class="si">%d</span><span class="s2">&quot;</span><span class="o">%</span><span class="p">(</span><span class="n">beforedecimal</span><span class="p">,</span><span class="mi">60</span><span class="o">*</span><span class="n">afterdecimal</span><span class="p">)</span> <span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span> <span class="k">pass</span> <span class="k">return</span> <span class="n">origFrom_Raw</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="o">**</span><span class="n">kw</span><span class="p">)</span> <span class="n">hyperdb</span><span class="o">.</span><span class="n">Interval</span><span class="o">.</span><span class="n">from_raw</span> <span class="o">=</span> <span class="n">normalizeperiod</span> </pre></div> </div> <p>any call to convert an interval from raw form now has two simpler (and more friendly) ways to specify common time intervals.</p> </section> <section id="modifying-the-mail-gateway"> <h4><a class="toc-backref" href="#id37" role="doc-backlink">Modifying the Mail Gateway</a></h4> <p>One site receives email on a main gateway. The virtual alias delivery table on the postfix server is configured with:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">test</span><span class="o">-</span><span class="n">issues</span><span class="nd">@example</span><span class="o">.</span><span class="n">com</span> <span class="n">roundup</span><span class="o">-</span><span class="n">test</span><span class="nd">@roundup</span><span class="o">-</span><span class="n">vm</span><span class="o">.</span><span class="n">example</span><span class="o">.</span><span class="n">com</span> <span class="n">test</span><span class="o">-</span><span class="n">support</span><span class="nd">@example</span><span class="o">.</span><span class="n">com</span> <span class="n">roundup</span><span class="o">-</span><span class="n">test</span><span class="o">+</span><span class="n">support</span><span class="o">-</span><span class="n">a</span><span class="nd">@roundup</span><span class="o">-</span><span class="n">vm</span><span class="o">.</span><span class="n">example</span><span class="o">.</span><span class="n">com</span> <span class="n">test</span><span class="nd">@support</span><span class="o">.</span><span class="n">example</span><span class="o">.</span><span class="n">com</span> <span class="n">roundup</span><span class="o">-</span><span class="n">test</span><span class="o">+</span><span class="n">support</span><span class="o">-</span><span class="n">b</span><span class="nd">@roundup</span><span class="o">-</span><span class="n">vm</span><span class="o">.</span><span class="n">example</span><span class="o">.</span><span class="n">com</span> </pre></div> </div> <p>These modifications to the mail gateway for Roundup allows anonymous submissions. It hides all of the requesters under the “support” user. It also makes some other modifications to the mail parser allowing keywords to be set and prefixes to be defined based on the delivery alias.</p> <p>This is the entry in interfaces.py:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">roundup.mailgw</span> <span class="kn">import</span> <span class="nn">email.utils</span> <span class="k">class</span> <span class="nc">SupportTracker</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span> <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">keyword</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">prefix</span> <span class="o">=</span> <span class="n">prefix</span> <span class="bp">self</span><span class="o">.</span><span class="n">keyword</span> <span class="o">=</span> <span class="n">keyword</span> <span class="c1"># Define new prefixes and keywords based on local address.</span> <span class="n">support_trackers</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">### production instances ###</span> <span class="c1">### test instances ###</span> <span class="s1">&#39;roundup-test+support-a&#39;</span><span class="p">:</span> <span class="n">SupportTracker</span><span class="p">(</span><span class="n">prefix</span><span class="o">=</span><span class="s1">&#39;Support 1&#39;</span><span class="p">,</span> <span class="n">keyword</span><span class="o">=</span><span class="s1">&#39;support1&#39;</span><span class="p">),</span> <span class="s1">&#39;roundup-test+support-b&#39;</span><span class="p">:</span> <span class="n">SupportTracker</span><span class="p">(</span><span class="n">prefix</span><span class="o">=</span><span class="s1">&#39;Support 2&#39;</span><span class="p">,</span> <span class="n">keyword</span><span class="o">=</span><span class="s1">&#39;support2&#39;</span><span class="p">),</span> <span class="s1">&#39;roundup-test2+support-a&#39;</span><span class="p">:</span> <span class="n">SupportTracker</span><span class="p">(</span><span class="n">prefix</span><span class="o">=</span><span class="s1">&#39;Support 1&#39;</span><span class="p">,</span> <span class="n">keyword</span><span class="o">=</span><span class="s1">&#39;support1&#39;</span><span class="p">),</span> <span class="s1">&#39;roundup-test2+support-b&#39;</span><span class="p">:</span> <span class="n">SupportTracker</span><span class="p">(</span><span class="n">prefix</span><span class="o">=</span><span class="s1">&#39;Support 2&#39;</span><span class="p">,</span> <span class="n">keyword</span><span class="o">=</span><span class="s1">&#39;support2&#39;</span><span class="p">),</span> <span class="p">}</span> <span class="k">class</span> <span class="nc">parsedMessage</span><span class="p">(</span><span class="n">roundup</span><span class="o">.</span><span class="n">mailgw</span><span class="o">.</span><span class="n">parsedMessage</span><span class="p">):</span> <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">mailgw</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">support_tracker</span><span class="p">):</span> <span class="n">roundup</span><span class="o">.</span><span class="n">mailgw</span><span class="o">.</span><span class="n">parsedMessage</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">mailgw</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> <span class="k">if</span> <span class="n">support_tracker</span><span class="o">.</span><span class="n">prefix</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">prefix</span> <span class="o">=</span> <span class="s1">&#39;</span><span class="si">%s</span><span class="s1">: &#39;</span> <span class="o">%</span> <span class="n">support_tracker</span><span class="o">.</span><span class="n">prefix</span> <span class="k">else</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">prefix</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">if</span> <span class="n">support_tracker</span><span class="o">.</span><span class="n">keyword</span><span class="p">:</span> <span class="k">try</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span> <span class="o">=</span> <span class="p">[</span> <span class="bp">self</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">keyword</span><span class="o">.</span><span class="n">lookup</span><span class="p">(</span><span class="n">support_tracker</span><span class="o">.</span><span class="n">keyword</span><span class="p">)]</span> <span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span> <span class="k">pass</span> <span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">ADD_AUTHOR_TO_NOSY</span> <span class="o">=</span> <span class="s1">&#39;no&#39;</span> <span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">ADD_RECIPIENTS_TO_NOSY</span> <span class="o">=</span> <span class="s1">&#39;no&#39;</span> <span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">MAILGW_KEEP_QUOTED_TEXT</span> <span class="o">=</span> <span class="s1">&#39;yes&#39;</span> <span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">MAILGW_LEAVE_BODY_UNCHANGED</span> <span class="o">=</span> <span class="s1">&#39;yes&#39;</span> <span class="bp">self</span><span class="o">.</span><span class="n">classname</span> <span class="o">=</span> <span class="s1">&#39;issue&#39;</span> <span class="bp">self</span><span class="o">.</span><span class="n">pfxmode</span> <span class="o">=</span> <span class="s1">&#39;loose&#39;</span> <span class="bp">self</span><span class="o">.</span><span class="n">sfxmode</span> <span class="o">=</span> <span class="s1">&#39;none&#39;</span> <span class="c1"># set the support user id</span> <span class="bp">self</span><span class="o">.</span><span class="n">fixed_author</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">lookup</span><span class="p">(</span><span class="s1">&#39;support&#39;</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">fixed_props</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">&#39;nosy&#39;</span><span class="p">:</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">fixed_author</span><span class="p">],</span> <span class="s1">&#39;keyword&#39;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">,</span> <span class="p">}</span> <span class="k">def</span> <span class="nf">handle_help</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">pass</span> <span class="k">def</span> <span class="nf">check_subject</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">subject</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">subject</span> <span class="o">=</span> <span class="s1">&#39;no subject&#39;</span> <span class="k">def</span> <span class="nf">rego_confirm</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">pass</span> <span class="k">def</span> <span class="nf">get_author_id</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># force the support user to be the author</span> <span class="bp">self</span><span class="o">.</span><span class="n">author</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">fixed_author</span> <span class="k">def</span> <span class="nf">get_props</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">props</span> <span class="o">=</span> <span class="p">{}</span> <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">nodeid</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">props</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">fixed_props</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">props</span><span class="p">[</span><span class="s1">&#39;title&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&quot;</span><span class="si">%s%s</span><span class="s2">&quot;</span> <span class="o">%</span> <span class="p">(</span> <span class="bp">self</span><span class="o">.</span><span class="n">prefix</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">subject</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s1">&#39;[&#39;</span><span class="p">,</span> <span class="s1">&#39;(&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s1">&#39;]&#39;</span><span class="p">,</span> <span class="s1">&#39;)&#39;</span><span class="p">)))</span> <span class="k">def</span> <span class="nf">get_content_and_attachments</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">roundup</span><span class="o">.</span><span class="n">mailgw</span><span class="o">.</span><span class="n">parsedMessage</span><span class="o">.</span><span class="n">get_content_and_attachments</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">content</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="s1">&#39;no text&#39;</span> <span class="n">intro</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">for</span> <span class="n">header</span> <span class="ow">in</span> <span class="p">[</span><span class="s1">&#39;From&#39;</span><span class="p">,</span> <span class="s1">&#39;To&#39;</span><span class="p">,</span> <span class="s1">&#39;Cc&#39;</span><span class="p">]:</span> <span class="k">for</span> <span class="n">addr</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">getaddrlist</span><span class="p">(</span><span class="n">header</span><span class="p">):</span> <span class="n">intro</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s1">&#39;</span><span class="si">%s</span><span class="s1">: </span><span class="si">%s</span><span class="s1">&#39;</span> <span class="o">%</span> <span class="p">(</span><span class="n">header</span><span class="p">,</span> <span class="n">email</span><span class="o">.</span><span class="n">utils</span><span class="o">.</span><span class="n">formataddr</span><span class="p">(</span><span class="n">addr</span><span class="p">)))</span> <span class="n">intro</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s1">&#39;Subject: </span><span class="si">%s</span><span class="s1">&#39;</span> <span class="o">%</span> <span class="bp">self</span><span class="o">.</span><span class="n">subject</span><span class="p">)</span> <span class="n">intro</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s1">&#39;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="s1">&#39;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">intro</span><span class="p">)</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">content</span> <span class="k">class</span> <span class="nc">MailGW</span><span class="p">(</span><span class="n">roundup</span><span class="o">.</span><span class="n">mailgw</span><span class="o">.</span><span class="n">MailGW</span><span class="p">):</span> <span class="k">def</span> <span class="nf">parsed_message_class</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">mailgw</span><span class="p">,</span> <span class="n">message</span><span class="p">):</span> <span class="n">support_tracker</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># The delivered-to header is unique to postfix</span> <span class="c1"># it is the target address:</span> <span class="c1"># roundup-test+support-a@roundup-vm.example.com</span> <span class="c1"># rather than</span> <span class="c1"># test-support@example.com</span> <span class="n">recipients</span> <span class="o">=</span> <span class="n">message</span><span class="o">.</span><span class="n">getaddrlist</span><span class="p">(</span><span class="s1">&#39;delivered-to&#39;</span><span class="p">)</span> <span class="k">if</span> <span class="n">recipients</span><span class="p">:</span> <span class="n">localpart</span> <span class="o">=</span> <span class="n">recipients</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">rpartition</span><span class="p">(</span><span class="s1">&#39;@&#39;</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="n">support_tracker</span> <span class="o">=</span> <span class="n">support_trackers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">localpart</span><span class="p">)</span> <span class="k">if</span> <span class="n">support_tracker</span><span class="p">:</span> <span class="c1"># parse the mesage using the parsedMessage class</span> <span class="c1"># defined above.</span> <span class="k">return</span> <span class="n">parsedMessage</span><span class="p">(</span><span class="n">mailgw</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">support_tracker</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="c1"># parse the message normally</span> <span class="k">return</span> <span class="n">roundup</span><span class="o">.</span><span class="n">mailgw</span><span class="o">.</span><span class="n">parsedMessage</span><span class="p">(</span><span class="n">mailgw</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> </pre></div> </div> <p>This is the most complex example section. The mail gateway is also one of the more complex subsystems in Roundup, and modifying it is not trivial.</p> </section> </section> </section> <section id="other-examples"> <h2><a class="toc-backref" href="#id38" role="doc-backlink">Other Examples</a></h2> <p>See the <a class="reference external" href="rest.html#programming-the-rest-api">rest interface documentation</a> for instructions on how to add new rest endpoints or <a class="reference external" href="rest.html#creating-custom-rate-limits">change the rate limiting method</a> using interfaces.py.</p> <p>The <a class="reference external" href="reference.html">reference document</a> also has examples:</p> <ul class="simple"> <li><p><a class="reference external" href="reference.html#extending-the-configuration-file">Extending the configuration file</a>.</p></li> <li><p><a class="reference external" href="reference.html#adding-a-new-permission">Adding a new Permission</a></p></li> </ul> </section> <section id="examples-on-the-wiki"> <h2><a class="toc-backref" href="#id39" role="doc-backlink">Examples on the Wiki</a></h2> <p>Even more examples of customisation have been contributed by users. They can be found on the <a class="reference external" href="https://wiki.roundup-tracker.org/CustomisationExamples">wiki</a>.</p> </section> </section> </main> </div> <footer class="footer"> <div> <span class="source">[<a href="../_sources/docs/customizing.txt" rel="nofollow">page source</a>]</span> &copy; Copyright 2009-2024, Richard Jones, Roundup-Team. </div> <div> Last updated on Jul 13, 2024. <span>Hosted by <a href="https://sourceforge.net"><img src="https://sflogo.sourceforge.net/sflogo.php?group_id=31577&amp;type=1" width="88" height="31" alt="SourceForge.net Logo" /></a></span> </div> </footer> <link rel="stylesheet" href="../_static/pygments.css" type="text/css" /> <!-- loading css_files --> <link rel="stylesheet" href="../_static/pygments.css" type="text/css" /> <!-- loading css_files --> <link rel="stylesheet" href="../_static/" type="text/css" /> <!-- loading css_files --> <link rel="stylesheet" href="../_static/tabs.css" type="text/css" /> <script> /* locally hosted goatcounter https://www.goatcounter.com/ */ /* include site info in path url to allow multiple sites to be tracked together */ window.goatcounter = { path: function(p) { return location.host + p } } </script> <script data-goatcounter="https://stats.rouilj.dynamic-dns.net/count" integrity="sha384-QGgNMMRFTi8ul5kHJ+vXysPe8gySvSA/Y3rpXZiRLzKPIw8CWY+a3ObKmQsyDr+a" async="" src="../_static/goatcounter_count.v3.js"> </script> <script id="documentation_options" data-url_root="" src="../_static/documentation_options.js"> </script> <script type="text/javascript" src="../_static/jquery.js"></script> <script type="text/javascript" src="../_static/doctools.js"></script> <script type="text/javascript" src="../_static/language_data.js"></script> <script type="text/javascript" src="../_static/searchtools.js"></script> <script type="text/javascript" src="../_static/sphinx_highlight.js"></script> <script type="text/javascript" src="../_static/tabs.js"></script> <script type="text/javascript">$('#searchbox').show(0);</script> </body> </html>

Pages: 1 2 3 4 5 6 7 8 9 10