Code

*** empty log message ***
[roundup.git] / doc / customizing.txt
index 120eaff055afd73b2b4aaa85597bfd8dd01c04f2..7205a2fcfd5a275952848d42d87c34743ded61a3 100644 (file)
@@ -2,7 +2,7 @@
 Customising Roundup
 ===================
 
-:Version: $Revision: 1.97 $
+:Version: $Revision: 1.117 $
 
 .. This document borrows from the ZopeBook section on ZPT. The original is at:
    http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx
@@ -16,13 +16,13 @@ What You Can Do
 Before you get too far, it's probably worth having a quick read of the Roundup
 `design documentation`_.
 
-Customisation of Roundup can take one of five forms:
+Customisation of Roundup can take one of six forms:
 
 1. `tracker configuration`_ file changes
 2. database, or `tracker schema`_ changes
 3. "definition" class `database content`_ changes
 4. behavioural changes, through detectors_
-5. `access controls`_
+5. `security / access controls`_
 6. change the `web interface`_
 
 The third case is special because it takes two distinctly different forms
@@ -169,17 +169,30 @@ The configuration variables available are:
 
 **EMAIL_KEEP_QUOTED_TEXT** - ``'yes'`` or ``'no'``
  Keep email citations. Citations are the part of e-mail which the sender has
- quoted in their reply to previous e-mail.
+ quoted in their reply to previous e-mail with ``>`` or ``|`` characters at
+ the start of the line.
 
 **EMAIL_LEAVE_BODY_UNCHANGED** - ``'no'``
  Preserve the email body as is. Enabiling this will cause the entire message
- body to be stored, including all citations and signatures. It should be
- either ``'yes'`` or ``'no'``.
+ body to be stored, including all citations, signatures and Outlook-quoted
+ sections (ie. "Original Message" blocks). It should be either ``'yes'``
+ or ``'no'``.
 
 **MAIL_DEFAULT_CLASS** - ``'issue'`` or ``''``
  Default class to use in the mailgw if one isn't supplied in email
  subjects. To disable, comment out the variable below or leave it blank.
 
+**HTML_VERSION** -  ``'html4'`` or ``'xhtml'``
+ HTML version to generate. The templates are html4 by default. If you
+ wish to make them xhtml, then you'll need to change this var to 'xhtml'
+ too so all auto-generated HTML is compliant.
+
+**EMAIL_CHARSET** - ``utf-8`` (or ``iso-8859-1`` for Eudora users)
+ Character set to encode email headers with. We use utf-8 by default, as
+ it's the most flexible. Some mail readers (eg. Eudora) can't cope with
+ that, so you might need to specify a more limited character set (eg.
+ 'iso-8859-1'.
+
 The default config.py is given below - as you
 can see, the MAIL_DOMAIN must be edited before any interaction with the
 tracker is attempted.::
@@ -255,6 +268,18 @@ tracker is attempted.::
     MAIL_DEFAULT_CLASS = 'issue'   # use "issue" class by default
     #MAIL_DEFAULT_CLASS = ''        # disable (or just comment the var out)
 
+    # HTML version to generate. The templates are html4 by default. If you
+    # wish to make them xhtml, then you'll need to change this var to 'xhtml'
+    # too so all auto-generated HTML is compliant.
+    HTML_VERSION = 'html4'         # either 'html4' or 'xhtml'
+
+    # Character set to encode email headers with. We use utf-8 by default, as
+    # it's the most flexible. Some mail readers (eg. Eudora) can't cope with
+    # that, so you might need to specify a more limited character set (eg.
+    # 'iso-8859-1'.
+    EMAIL_CHARSET = 'utf-8'
+    #EMAIL_CHARSET = 'iso-8859-1'   # use this instead for Eudora users
+
     # 
     # SECURITY DEFINITIONS
     #
@@ -301,6 +326,41 @@ of ``'setkey'``)::
         priority=Link("priority"))
     issue.setkey('title')
 
+
+What you can't do to the schema
+-------------------------------
+
+You must never:
+
+**Remove the users class**
+  This class is the only *required* class in Roundup. Similarly, its
+  username, password and address properties must never be removed.
+
+**Change the type of a property**
+  Property types must *never* be changed - the database simply doesn't take
+  this kind of action into account. Note that you can't just remove a
+  property and re-add it as a new type either. If you wanted to make the
+  assignedto property a Multilink, you'd need to create a new property
+  assignedto_list and remove the old assignedto property.
+
+
+What you can do to the schema
+-----------------------------
+
+Your schema may be changed at any time before or after the tracker has been
+initialised (or used). You may:
+
+**Add new properties to classes, or add whole new classes**
+  This is painless and easy to do - there are generally no repurcussions
+  from adding new information to a tracker's schema.
+
+**Remove properties**
+  Removing properties is a little more tricky - you need to make sure that
+  the property is no longer used in the `web interface`_ *or* by the
+  detectors_.
+
+
+
 Classes and Properties - creating a new information store
 ---------------------------------------------------------
 
@@ -545,8 +605,8 @@ See "`adding a new field to the classic schema`_" for an example that
 requires database content changes.
 
 
-Access Controls
-===============
+Security / Access Controls
+==========================
 
 A set of Permissions is built into the security module by default:
 
@@ -845,15 +905,15 @@ of:
  - Also handle the ":queryname" variable and save off the query to the
    user's query list.
 
-Each of the actions is implemented by a corresponding ``*actionAction*``
-(where "action" is the name of the action) method on the
-``roundup.cgi.Client`` class, which also happens to be available in your
-tracker instance as ``interfaces.Client``. So if you need to define new
-actions, you may add them there (see `defining new web actions`_).
+Each of the actions is implemented by a corresponding ``*XxxAction*`` (where
+"Xxx" is the name of the action) class in the ``roundup.cgi.actions`` module.
+These classes are registered with ``roundup.cgi.client.Client`` which also
+happens to be available in your tracker instance as ``interfaces.Client``. So
+if you need to define new actions, you may add them there (see `defining new
+web actions`_).
 
-Each action also has a corresponding ``*actionPermission*`` (where
-"action" is the name of the action) method which determines whether the
-action is permissible given the current user. The base permission checks
+Each action class also has a ``*permission*`` method which determines whether
+the action is permissible given the current user. The base permission checks
 are:
 
 **login**
@@ -988,7 +1048,7 @@ Two special form values are supported for backwards compatibility:
     This is equivalent to::
 
         @link@messages=msg-1
-        @msg-1@content=value
+        msg-1@content=value
 
     except that in addition, the "author" and "date" properties of
     "msg-1" are set to the userid of the submitter, and the current
@@ -998,7 +1058,7 @@ Two special form values are supported for backwards compatibility:
     This is equivalent to::
 
         @link@files=file-1
-        @file-1@content=value
+        file-1@content=value
 
     The String content value is handled as described above for file
     uploads.
@@ -1019,6 +1079,10 @@ returning.
 Default templates
 -----------------
 
+The default templates are html4 compliant. If you wish to change them to be
+xhtml compliant, you'll need to change the ``HTML_VERSION`` configuration
+variable in ``config.py`` to ``'xhtml'`` instead of ``'html4'``.
+
 Most customisation of the web view can be done by modifying the
 templates in the tracker ``'html'`` directory. There are several types
 of files in there. The *minimal* template includes:
@@ -1536,7 +1600,11 @@ now         only on Date properties - return the current date as a new
 reldate     only on Date properties - render the interval between the date
             and now
 local       only on Date properties - return this date as a new property
-            with some timezone offset
+            with some timezone offset, for example::
+            
+                python:context.creation.local(10)
+
+            will render the date with a +10 hour offset.
 pretty      only on Interval properties - render the interval in a pretty
             format (eg. "yesterday")
 menu        only on Link and Multilink properties - render a form select
@@ -1866,19 +1934,19 @@ template)::
 
  <table class="form">
  <tr>
-  <th nowrap>Title</th>
+  <th>Title</th>
   <td colspan="3" tal:content="structure python:context.title.field(size=60)">title</td>
  </tr>
  
  <tr>
-  <th nowrap>Priority</th>
+  <th>Priority</th>
   <td tal:content="structure context/priority/menu">priority</td>
-  <th nowrap>Status</th>
+  <th>Status</th>
   <td tal:content="structure context/status/menu">status</td>
  </tr>
  
  <tr>
-  <th nowrap>Superseder</th>
+  <th>Superseder</th>
   <td>
    <span tal:replace="structure python:context.superseder.field(showid=1, size=20)" />
    <span tal:replace="structure python:db.issue.classhelp('id,title')" />
@@ -1886,7 +1954,7 @@ template)::
     <br>View: <span tal:replace="structure python:context.superseder.link(showid=1)" />
    </span>
   </td>
-  <th nowrap>Nosy List</th>
+  <th>Nosy List</th>
   <td>
    <span tal:replace="structure context/nosy/field" />
    <span tal:replace="structure python:db.user.classhelp('username,realname,address,phone')" />
@@ -1894,7 +1962,7 @@ template)::
  </tr>
  
  <tr>
-  <th nowrap>Assigned To</th>
+  <th>Assigned To</th>
   <td tal:content="structure context/assignedto/menu">
    assignedto menu
   </td>
@@ -1903,14 +1971,14 @@ template)::
  </tr>
  
  <tr>
-  <th nowrap>Change Note</th>
+  <th>Change Note</th>
   <td colspan="3">
    <textarea name=":note" wrap="hard" rows="5" cols="60"></textarea>
   </td>
  </tr>
  
  <tr>
-  <th nowrap>File</th>
+  <th>File</th>
   <td colspan="3"><input type="file" name=":file" size="40"></td>
  </tr>
  
@@ -2022,12 +2090,12 @@ templating through the "journal" method of the item*::
 Defining new web actions
 ------------------------
 
-You may define new actions to be triggered by the ``@action`` form
-variable. These are added to the tracker ``interfaces.py`` as methods on
-the ``Client`` class. 
+You may define new actions to be triggered by the ``@action`` form variable.
+These are added to the tracker ``interfaces.py`` as ``Action`` classes that get
+called by the the ``Client`` class.
 
-Adding action methods takes three steps; first you `define the new
-action method`_, then you `register the action method`_ with the cgi
+Adding action classes takes three steps; first you `define the new
+action class`_, then you `register the action class`_ with the cgi
 interface so it may be triggered by the ``@action`` form variable.
 Finally you `use the new action`_ in your HTML form.
 
@@ -2035,41 +2103,41 @@ See "`setting up a "wizard" (or "druid") for controlled adding of
 issues`_" for an example.
 
 
-Define the new action method
+Define the new action class
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-The action methods have the following interface::
+The action classes have the following interface::
 
-    def myActionMethod(self):
-        ''' Perform some action. No return value is required.
-        '''
+ class MyAction(Action):
+     def handle(self):
+         ''' Perform some action. No return value is required.
+         '''
 
-The *self* argument is an instance of your tracker ``instance.Client``
-class - thus it's mostly implemented by ``roundup.cgi.Client``. See the
+The *self.client* attribute is an instance of your tracker ``instance.Client``
+class - thus it's mostly implemented by ``roundup.cgi.client.Client``. See the
 docstring of that class for details of what it can do.
 
 The method will typically check the ``self.form`` variable's contents.
 It may then:
 
-- add information to ``self.ok_message`` or ``self.error_message``
-- change the ``self.template`` variable to alter what the user will see
+- add information to ``self.client.ok_message`` or ``self.client.error_message``
+- change the ``self.client.template`` variable to alter what the user will see
   next
 - raise Unauthorised, SendStaticFile, SendFile, NotFound or Redirect
-  exceptions
+  exceptions (import them from roundup.cgi.exceptions)
 
 
-Register the action method
+Register the action class
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-The method is now written, but isn't available to the user until you add
-it to the `instance.Client`` class ``actions`` variable, like so::
+The class is now written, but isn't available to the user until you add it to
+the ``instance.Client`` class ``actions`` variable, like so::
 
-    actions = client.Class.actions + (
-        ('myaction', 'myActionMethod'),
+    actions = client.Client.actions + (
+        ('myaction', myActionClass),
     )
 
-This maps the action name "myaction" to the action method we defined.
-
+This maps the action name "myaction" to the action class we defined.
 
 Use the new action
 ~~~~~~~~~~~~~~~~~~
@@ -2080,6 +2148,23 @@ In your HTML form, add a hidden form element like so::
 
 where "myaction" is the name you registered in the previous step.
 
+Actions may return content to the user
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Actions generally perform some database manipulation and then pass control
+on to the rendering of a template in the current context (see `Determining
+web context`_ for how that works.) Some actions will want to generate the
+actual content returned to the user. Action methods may return their own
+content string to be displayed to the user, overriding the templating step.
+In this situation, we assume that the content is HTML by default. You may
+override the content type indicated to the user by calling ``setHeader``::
+
+   self.client.setHeader('Content-Type', 'text/csv')
+
+This example indicates that the value sent back to the user is actually
+comma-separated value content (eg. something to be loaded into a
+spreadsheet or database).
+
 
 Examples
 ========
@@ -2341,7 +2426,7 @@ will be the "name" variable of the current context (which is
 to the form, a new category will be created with that name::
 
     <tr>
-     <th nowrap>Name</th>
+     <th>Name</th>
      <td tal:content="structure python:context.name.field(size=60)">
      name</td>
     </tr>
@@ -2373,19 +2458,20 @@ So putting it all together, and closing the table and form we get::
    <form method="POST" onSubmit="return submit_once()"
          enctype="multipart/form-data">
 
-    <input type="hidden" name="@required" value="name">
-
     <table class="form">
      <tr><th class="header" colspan="2">Category</th></tr>
 
      <tr>
-      <th nowrap>Name</th>
+      <th>Name</th>
       <td tal:content="structure python:context.name.field(size=60)">
       name</td>
      </tr>
 
      <tr>
-      <td>&nbsp;</td>
+      <td>
+        &nbsp;
+        <input type="hidden" name="@required" value="name"> 
+      </td>
       <td colspan="3" tal:content="structure context/submit">
        submit button will go here
       </td>
@@ -2414,7 +2500,7 @@ Just like ``category.issue.html`` 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::
 
-   <th nowrap>Category</th>
+   <th>Category</th>
    <td><span tal:replace="structure context/category/field" />
        <span tal:replace="structure db/category/classhelp" />
    </td>
@@ -2578,12 +2664,12 @@ issues to. You can do this by following these steps:
 4. in the ``issue.item.html`` template, change the status editing bit
    from::
 
-    <th nowrap>Status</th>
+    <th>Status</th>
     <td tal:content="structure context/status/menu">status</td>
 
    to::
 
-    <th nowrap>Status</th>
+    <th>Status</th>
     <td>
      <select tal:condition="context/id" name="status">
       <tal:block tal:define="ok context/status/transitions"
@@ -2613,7 +2699,7 @@ Alter the issue.item template section for messages to::
    <td><a tal:attributes="href string:msg${msg/id}"
           tal:content="string:msg${msg/id}"></a></td>
    <td tal:content="msg/author">author</td>
-   <td nowrap tal:content="msg/date/pretty">date</td>
+   <td class="date" tal:content="msg/date/pretty">date</td>
    <td tal:content="msg/summary">summary</td>
    <td>
     <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">
@@ -2692,7 +2778,7 @@ Setting up a "wizard" (or "druid") for controlled adding of issues
     <form method="POST" onSubmit="return submit_once()"
           enctype="multipart/form-data">
       <input type="hidden" name="@template" value="add_page1">
-      <input type="hidden" name="@action" value="page1submit">
+      <input type="hidden" name="@action" value="page1_submit">
 
       <strong>Category:</strong>
       <tal:block tal:replace="structure context/category/menu" />
@@ -2716,15 +2802,15 @@ Setting up a "wizard" (or "druid") for controlled adding of issues
     </form>
 
    Note that later in the form, I test the value of "cat" include form
-   elements that are appropriate. For example::
+   elements that are appropriate. For exsample::
 
     <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()">
      <tr>
-      <th nowrap>Operating System</th>
+      <th>Operating System</th>
       <td tal:content="structure context/os/field"></td>
      </tr>
      <tr>
-      <th nowrap>Web Browser</th>
+      <th>Web Browser</th>
       <td tal:content="structure context/browser/field"></td>
      </tr>
     </tal:block>
@@ -2733,26 +2819,27 @@ Setting up a "wizard" (or "druid") for controlled adding of issues
    of 6, 10, 13, 14, 15, 16 or 17.
 
 3. 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 methods on the ``interfaces.Client`` class
-   and insert hooks to those actions in the "actions" attribute on that
-   class, like so::
+   usually to validate user choices and determine what page is next. Now encode
+   those actions in a new ``Action`` class and insert hooks to those actions in
+   the "actions" attribute on on the ``interfaces.Client`` class, like so (see 
+   `defining new web actions`_)::
+
+    class Page1SubmitAction(Action):
+        def handle(self):
+            ''' Verify that the user has selected a category, and then move
+                on to page 2.
+            '''
+            category = self.form['category'].value
+            if category == '-1':
+                self.error_message.append('You must select a category of report')
+                return
+            # everything's ok, move on to the next page
+            self.template = 'add_page2'
 
     actions = client.Client.actions + (
-        ('page1_submit', 'page1SubmitAction'),
+        ('page1_submit', Page1SubmitAction),
     )
 
-    def page1SubmitAction(self):
-        ''' Verify that the user has selected a category, and then move
-            on to page 2.
-        '''
-        category = self.form['category'].value
-        if category == '-1':
-            self.error_message.append('You must select a category of report')
-            return
-        # everything's ok, move on to the next page
-        self.template = 'add_page2'
-
 4. Use the usual "new" action as the ``@action`` on the final page, and
    you're done (the standard context/submit method can do this for you).
 
@@ -2769,27 +2856,36 @@ would be::
 
     admin:aamrgyQfDFSHw
 
-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 add the following code to our ``Client`` class in the
-tracker home ``interfaces.py`` module::
+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 ``verifyPassword`` method defined in
+``roundup.cgi.actions.LoginAction`` and register the new class with our
+``Client`` class in the tracker home ``interfaces.py`` module::
 
-    def verifyPassword(self, userid, password):
-        # get the user's username
-        username = self.db.user.get(userid, 'username')
+    from roundup.cgi.actions import LoginAction    
 
-        # the passwords are stored in the "passwd.txt" file in the
-        # tracker home
-        file = os.path.join(self.db.config.TRACKER_HOME, 'passwd.txt')
+    class ExternalPasswordLoginAction(LoginAction):
+        def verifyPassword(self, userid, password):
+            # get the user's username
+            username = self.db.user.get(userid, 'username')
 
-        # see if we can find a match
-        for ent in [line.strip().split(':') for line in
-                                            open(file).readlines()]:
-            if ent[0] == username:
-                return crypt.crypt(password, ent[1][:2]) == ent[1]
+            # the passwords are stored in the "passwd.txt" file in the
+            # tracker home
+            file = os.path.join(self.db.config.TRACKER_HOME, 'passwd.txt')
 
-        # user doesn't exist in the file
-        return 0
+            # see if we can find a match
+            for ent in [line.strip().split(':') for line in
+                                                open(file).readlines()]:
+                if ent[0] == username:
+                    return crypt.crypt(password, ent[1][:2]) == ent[1]
+
+            # user doesn't exist in the file
+            return 0
+
+    class Client(client.Client):
+        actions = client.Client.actions + (
+            ('login', ExternalPasswordLoginAction)
+        )
 
 What this does is look through the file, line by line, looking for a
 name that matches.
@@ -2931,7 +3027,7 @@ be able to give a summary of the total time spent on a particular issue.
    field to capture a new timelog item's perdiod::
 
     <tr> 
-     <th nowrap>Time Log</th> 
+     <th>Time Log</th> 
      <td colspan=3><input type="text" name="timelog-1@period" /> 
       <br />(enter as '3y 1m 4d 2:40:02' or parts thereof) 
      </td> 
@@ -2961,15 +3057,14 @@ be able to give a summary of the total time spent on a particular issue.
             ''' Call me with a list of timelog items (which have an
                 Interval "period" property)
             '''
-            total = Interval('')
+            total = Interval('0d')
             for time in times:
                 total += time.period._value
             return total
 
-   Replace the ``pass`` line as we did in step 4 above with the Client
+   Replace the ``pass`` line if one appears in your TemplatingUtils
    class. As indicated in the docstrings, we will be able to access the
-   ``totalTimeSpent`` method via the ``utils`` variable in our
-   templates.
+   ``totalTimeSpent`` method via the ``utils`` variable in our templates.
 
 5. Display the time log for an issue::
 
@@ -3145,9 +3240,10 @@ for more information about doing this.
 
 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 adding a method called
-"verifyPassword" to the Client class in your tracker's interfaces.py
-module. The method is implemented by default as::
+extension to the cgi interface. You'd do this by overriding the method called
+"verifyPassword" on the LoginAction class in your tracker's interfaces.py
+module (see `using an external password validation source`_). The method is
+implemented by default as::
 
     def verifyPassword(self, userid, password):
         ''' Verify the password that the user has supplied
@@ -3189,7 +3285,7 @@ that shows either one or the other. We'll use a new form variable,
     <td><a tal:attributes="href string:msg${msg/id}"
            tal:content="string:msg${msg/id}"></a></td>
     <td tal:content="msg/author">author</td>
-    <td nowrap tal:content="msg/date/pretty">date</td>
+    <td class="date" tal:content="msg/date/pretty">date</td>
     <td tal:content="msg/summary">summary</td>
     <td>
      <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>
@@ -3206,7 +3302,7 @@ that shows either one or the other. We'll use a new form variable,
    <tal:block tal:repeat="msg context/messages">
     <tr>
      <th tal:content="msg/author">author</th>
-     <th nowrap tal:content="msg/date/pretty">date</th>
+     <th class="date" tal:content="msg/date/pretty">date</th>
      <th style="text-align: right">
       (<a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>)
      </th>
@@ -3242,7 +3338,7 @@ resolved. To achieve this:
 2. Add the new "blockers" property to the issue.item edit page, using
    something like::
 
-    <th nowrap>Waiting On</th>
+    <th>Waiting On</th>
     <td>
      <span tal:replace="structure python:context.blockers.field(showid=1,
                                   size=20)" />
@@ -3349,6 +3445,329 @@ 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.
 
+Add users to the nosy list based on the topic
+---------------------------------------------
+
+We need the ability to automatically add users to the nosy list based
+on the occurence of a topic. Every user should be allowed to edit his
+own list of topics for which he wants to be added to the nosy list.
+
+Below will be showed that such a change can be performed with only
+minimal understanding of the roundup system, but with clever use
+of Copy and Paste.
+
+This requires three changes to the tracker: a change in the database to
+allow per-user recording of the lists of topics for which he wants to
+be put on the nosy list, a change in the user view allowing to edit
+this list of topics, and addition of an auditor which updates the nosy
+list when a topic is set.
+
+Adding the nosy topic list
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The change in the database to make is that for any user there should be
+a list of topics for which he wants to be put on the nosy list. Adding
+a ``Multilink`` of ``keyword`` seem to fullfill this (note that within
+the code topics are called ``keywords``.) As such, all what has to be
+done is to add a new field to the definition of ``user`` within the
+file ``dbinit.py``.  We will call this new field ``nosy_keywords``, and
+the updated definition of user will be::
+
+    user = Class(db, "user", 
+                    username=String(),   password=Password(),
+                    address=String(),    realname=String(), 
+                    phone=String(),      organisation=String(),
+                    alternate_addresses=String(),
+                    queries=Multilink('query'), roles=String(),
+                    timezone=String(),
+                    nosy_keywords=Multilink('keyword'))
+Changing the user view to allow changing the nosy topic list
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We want any user to be able to change the list of topics 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 ``html/user.item.html``.
+We easily can
+see that the topic field in the issue view has very similar editting
+requirements as our nosy topics, both being a list of topics. As
+such, we search for Topics in ``issue.item.html``, and extract the
+associated parts from there. We add this to ``user.item.html`` at the 
+bottom of the list of viewed items (i.e. just below the 'Alternate
+E-mail addresses' in the classic template)::
+
+ <tr>
+  <th>Nosy Topics</th>
+  <td>
+  <span tal:replace="structure context/nosy_keywords/field" />
+  <span tal:replace="structure python:db.keyword.classhelp(property='nosy_keywords')" />
+  </td>
+ </tr>
+  
+
+Addition of an auditor to update the nosy list
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The more difficult part is the addition of the logic to actually
+at the users to the nosy list when it is required. 
+The choice is made to perform this action when the topics on an
+item are set, including when an item is created.
+Here we choose to start out with a copy of the 
+``detectors/nosyreaction.py`` detector, which we copy to the file
+``detectors/nosy_keyword_reaction.py``. 
+This looks like a good start as it also adds users
+to the nosy list. A look through the code reveals that the
+``nosyreaction`` function actually is sending the e-mail, which
+we do not need. As such, we can change the init function to::
+
+    def init(db):
+        db.issue.audit('create', update_kw_nosy)
+        db.issue.audit('set', update_kw_nosy)
+
+After that we rename the ``updatenosy`` function to ``update_kw_nosy``.
+The first two blocks of code in that function relate to settings
+``current`` to a combination of the old and new nosy lists. This
+functionality is left in the new auditor. The following block of
+code, which in ``updatenosy`` handled adding the assignedto user(s)
+to the nosy list, 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 topics, than loop over all users,
+and assign the user to the nosy list when the topic in the user's
+nosy_keywords. The next part in ``updatenosy``, adding the author
+and/or recipients of a message to the nosy list, obviously is not
+relevant here and thus is deleted from the new auditor. The last
+part, copying the new nosy list to newvalues, does not have to be changed.
+This brings the following function::
+
+    def update_kw_nosy(db, cl, nodeid, newvalues):
+        '''Update the nosy list for changes to the topics
+        '''
+        # nodeid will be None if this is a new node
+        current = {}
+        if nodeid is None:
+            ok = ('new', 'yes')
+        else:
+            ok = ('yes',)
+            # old node, get the current values from the node if they haven't
+            # changed
+            if not newvalues.has_key('nosy'):
+                nosy = cl.get(nodeid, 'nosy')
+                for value in nosy:
+                    if not current.has_key(value):
+                        current[value] = 1
+
+        # if the nosy list changed in this transaction, init from the new value
+        if newvalues.has_key('nosy'):
+            nosy = newvalues.get('nosy', [])
+            for value in nosy:
+                if not db.hasnode('user', value):
+                    continue
+                if not current.has_key(value):
+                    current[value] = 1
+
+        # add users with topic in nosy_keywords to the nosy list
+        if newvalues.has_key('topic') and newvalues['topic'] is not None:
+            topic_ids = newvalues['topic']
+            for topic in topic_ids:
+                # loop over all users,
+                # and assign user to nosy when topic in nosy_keywords
+                for user_id in db.user.list():
+                    nosy_kw = db.user.get(user_id, "nosy_keywords")
+                    found = 0
+                    for kw in nosy_kw:
+                        if kw == topic:
+                            found = 1
+                    if found:
+                        current[user_id] = 1
+
+        # that's it, save off the new nosy list
+        newvalues['nosy'] = current.keys()
+
+and these two function are the only ones needed in the file.
+
+TODO: update this example to use the find() Class method.
+
+Caveats
+~~~~~~~
+
+A few problems with the design here can be noted:
+
+Multiple additions
+    When a user, after automatic selection, is manually removed
+    from the nosy list, he again is added to the nosy list when the
+    topic list of the issue is updated. A better design might be
+    to only check which topics are new compared to the old list
+    of topics, and only add users when they have indicated
+    interest on a new topic.
+
+    The code could also be changed to only trigger on the create() event,
+    rather than also on the set() event, thus only setting the nosy list
+    when the issue is created.
+
+Scalability
+    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 will be to link from the topics to the users which
+    selected these topics a nosy topics. This will eliminate the
+    loop over all users.
+
+
+Adding action links to the index page
+-------------------------------------
+
+Add a column to the item.index.html template.
+
+Resolving the issue::
+
+  <a tal:attributes="href
+     string:issue${i/id}?:status=resolved&:action=edit">resolve</a>
+
+"Take" the issue::
+
+  <a tal:attributes="href
+     string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a>
+
+... and so on
+
+Users may only edit their issues
+--------------------------------
+
+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".) We back up the permissions with
+an auditor.
+
+First up, we create the new Role and Permission structure in
+``dbinit.py``::
+
+    # New users not approved by the admin
+    db.security.addRole(name='Provisional User',
+        description='New user registered via web or email')
+    p = db.security.addPermission(name='Edit Own', klass='issue',
+        description='Can only edit own issues')
+    db.security.addPermissionToRole('Provisional User', p)
+
+    # Assign the access and edit Permissions for issue to new users now
+    p = db.security.getPermission('View', 'issue')
+    db.security.addPermissionToRole('Provisional User', p)
+    p = db.security.getPermission('Edit', 'issue')
+    db.security.addPermissionToRole('Provisional User', p)
+
+    # and give the new users access to the web and email interface
+    p = db.security.getPermission('Web Access')
+    db.security.addPermissionToRole('Provisional User', p)
+    p = db.security.getPermission('Email Access')
+    db.security.addPermissionToRole('Provisional User', p)
+
+
+Then in the ``config.py`` we change the Role assigned to newly-registered
+users, replacing the existing ``'User'`` values::
+
+    NEW_WEB_USER_ROLES = 'Provisional User'
+    NEW_EMAIL_USER_ROLES = 'Provisional User'
+
+Finally we add a new *auditor* to the ``detectors`` directory called
+``provisional_user_auditor.py``::
+
+ def audit_provisionaluser(db, cl, nodeid, newvalues):
+     ''' New users are only allowed to modify their own issues.
+     '''
+     if (db.getuid() != cl.get(nodeid, 'creator')
+         and db.security.hasPermission('Edit Own', db.getuid(), cl.classname)):
+         raise ValueError, ('You are only allowed to edit your own %s'
+                            % cl.classname)
+
+ def init(db):
+     # fire before changes are made
+     db.issue.audit('set', audit_provisionaluser)
+     db.issue.audit('retire', audit_provisionaluser)
+     db.issue.audit('restore', audit_provisionaluser)
+
+Note that some older trackers might also want to change the ``page.html``
+template as follows::
+
+ <p class="classblock"
+ -       tal:condition="python:request.user.username != 'anonymous'">
+ +       tal:condition="python:request.user.hasPermission('View', 'user')">
+     <b>Administration</b><br>
+     <tal:block tal:condition="python:request.user.hasPermission('Edit', None)">
+      <a href="home?:template=classlist">Class List</a><br>
+
+(note that the "-" indicates a removed line, and the "+" indicates an added
+line).
+
+
+Colouring the rows in the issue index according to priority
+-----------------------------------------------------------
+
+A simple ``tal:attributes`` statement will do the bulk of the work here. In
+the ``issue.index.html`` template, add to the ``<tr>`` that displays the
+actual rows of data::
+
+   <tr tal:attributes="class string:priority-${i/priority/plain}">
+
+and then in your stylesheet (``style.css``) specify the colouring for the
+different priorities, like::
+
+   tr.priority-critical td {
+       background-color: red;
+   }
+
+   tr.priority-urgent td {
+       background-color: orange;
+   }
+
+and so on, with far less offensive colours :)
+
+Editing multiple items in an index view
+---------------------------------------
+
+To edit the status of all items in the item index view, edit the
+``issue.item.html``:
+
+1. add a form around the listing table, so at the top it reads::
+
+    <form method="POST" tal:attributes="action request/classname">
+     <table class="list">
+
+   and at the bottom of that table::
+
+     </table>
+    </form
+
+   making sure you match the ``</table>`` from the list table, not the
+   navigation table or the subsequent form table.
+
+2. in the display for the issue property, change::
+
+    <td tal:condition="request/show/status"
+        tal:content="python:i.status.plain() or default">&nbsp;</td>
+
+   to::
+
+    <td tal:condition="request/show/status"
+        tal:content="structure i/status/field">&nbsp;</td>
+
+   this will result in an edit field for the status property.
+
+3. after the ``tal:block`` which lists the actual index items (marked by
+   ``tal:repeat="i batch"``) add a new table row::
+
+    <tr>
+     <td tal:attributes="colspan python:len(request.columns)">
+      <input type="submit" value=" Save Changes ">
+      <input type="hidden" name="@action" value="edit">
+      <tal:block replace="structure request/indexargs_form" />
+     </td>
+    </tr>
+
+   which gives us a submit button, indicates that we are performing an edit
+   on any changed statuses and the final block 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).
 
 -------------------