Code

- Add tests for Interval.pretty().
[roundup.git] / doc / customizing.txt
index 4592a7a63d2e279be9de750cd3d23f93ecdceb8c..0136601910ec8a56f34d249378bd9c36dd0a9db6 100644 (file)
@@ -2,7 +2,7 @@
 Customising Roundup
 ===================
 
-:Version: $Revision: 1.89 $
+:Version: $Revision: 1.105 $
 
 .. This document borrows from the ZopeBook section on ZPT. The original is at:
    http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx
@@ -16,7 +16,7 @@ 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
@@ -301,6 +301,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
 ---------------------------------------------------------
 
@@ -468,6 +503,15 @@ for you are:
   It also provides the ``presetunread`` auditor which pre-sets the
   status to ``unread`` on new items if the status isn't explicitly
   defined.
+**messagesummary.py**
+  Generates the ``summary`` property for new messages based on the message
+  content.
+**userauditor.py**
+  Verifies the content of some of the user fields (email addresses and
+  roles lists).
+
+If you don't want this default behaviour, you're completely free to change
+or remove these detectors.
 
 See the detectors section in the `design document`__ for details of the
 interface for detectors.
@@ -750,7 +794,7 @@ Determining web context
 -----------------------
 
 To determine the "context" of a request, we look at the URL and the
-special request variable ``:template``. The URL path after the tracker
+special request variable ``@template``. The URL path after the tracker
 identifier is examined. Typical URL paths look like:
 
 1.  ``/tracker/issue``
@@ -783,7 +827,7 @@ e. if the path starts with an item designator and is longer than one
 Both b. and e. stop before we bother to determine the template we're
 going to use. That's because they don't actually use templates.
 
-The template used is specified by the ``:template`` CGI variable, which
+The template used is specified by the ``@template`` CGI variable, which
 defaults to:
 
 - only classname suplied:        "index"
@@ -796,7 +840,7 @@ Performing actions in web requests
 When a user requests a web page, they may optionally also request for an
 action to take place. As described in `how requests are processed`_, the
 action is performed before the requested page is generated. Actions are
-triggered by using a ``:action`` CGI variable, where the value is one
+triggered by using a ``@action`` CGI variable, where the value is one
 of:
 
 **login**
@@ -810,31 +854,12 @@ of:
  log them in.
 
 **edit**
- Perform an edit of an item in the database. There are some special form
- elements you may use:
-
- :link=designator:property and :multilink=designator:property
-  The value specifies an item designator and the property on that item
-  to which *this* item should be added, as a link or multilink.
- :note
-  Create a message and attach it to the current item's "messages"
-  property.
- :file
-  Create a file and attach it to the current item's "files" property.
-  Attach the file to the message created from the ``:note`` if it's
-  supplied.
- :required=property,property,...
-  The named properties are required to be filled in the form.
- :remove:<propname>=id(s)
-  The ids will be removed from the multilink property. You may have
-  multiple ``:remove:<propname>`` form elements for a single <propname>.
- :add:<propname>=id(s)
-  The ids will be added to the multilink property. You may have multiple
-  ``:add:<propname>`` form elements for a single <propname>.
+ Perform an edit of an item in the database. There are some `special form
+ variables`_ you may use.
 
 **new**
- Add a new item to the database. You may use the same special form
elements as in the "edit" action.
+ Add a new item to the database. You may use the same `special form
variables`_ as in the "edit" action.
 
 **retire**
  Retire the item in the database.
@@ -893,6 +918,139 @@ are:
  behaviour is to check whether the user may view this class.
 
 
+Special form variables
+----------------------
+
+Item properties and their values are edited with html FORM
+variables and their values. You can:
+
+- Change the value of some property of the current item.
+- Create a new item of any class, and edit the new item's
+  properties,
+- Attach newly created items to a multilink property of the
+  current item.
+- Remove items from a multilink property of the current item.
+- Specify that some properties are required for the edit
+  operation to be successful.
+
+In the following, <bracketed> values are variable, "@" may be
+either ":" or "@", and other text "required" is fixed.
+
+Most properties are specified as form variables:
+
+``<propname>``
+  property on the current context item
+
+``<designator>"@"<propname>``
+  property on the indicated item (for editing related information)
+
+Designators name a specific item of a class.
+
+``<classname><N>``
+    Name an existing item of class <classname>.
+
+``<classname>"-"<N>``
+    Name the <N>th new item of class <classname>. If the form
+    submission is successful, a new item of <classname> is
+    created. Within the submitted form, a particular
+    designator of this form always refers to the same new
+    item.
+
+Once we have determined the "propname", we look at it to see
+if it's special:
+
+``@required``
+    The associated form value is a comma-separated list of
+    property names that must be specified when the form is
+    submitted for the edit operation to succeed.  
+
+    When the <designator> is missing, the properties are
+    for the current context item.  When <designator> is
+    present, they are for the item specified by
+    <designator>.
+
+    The "@required" specifier must come before any of the
+    properties it refers to are assigned in the form.
+
+``@remove@<propname>=id(s)`` or ``@add@<propname>=id(s)``
+    The "@add@" and "@remove@" edit actions apply only to
+    Multilink properties.  The form value must be a
+    comma-separate list of keys for the class specified by
+    the simple form variable.  The listed items are added
+    to (respectively, removed from) the specified
+    property.
+
+``@link@<propname>=<designator>``
+    If the edit action is "@link@", the simple form
+    variable must specify a Link or Multilink property.
+    The form value is a comma-separated list of
+    designators.  The item corresponding to each
+    designator is linked to the property given by simple
+    form variable.
+
+None of the above (ie. just a simple form value)
+    The value of the form variable is converted
+    appropriately, depending on the type of the property.
+
+    For a Link('klass') property, the form value is a
+    single key for 'klass', where the key field is
+    specified in dbinit.py.  
+
+    For a Multilink('klass') property, the form value is a
+    comma-separated list of keys for 'klass', where the
+    key field is specified in dbinit.py.  
+
+    Note that for simple-form-variables specifiying Link
+    and Multilink properties, the linked-to class must
+    have a key field.
+
+    For a String() property specifying a filename, the
+    file named by the form value is uploaded. This means we
+    try to set additional properties "filename" and "type" (if
+    they are valid for the class).  Otherwise, the property
+    is set to the form value.
+
+    For Date(), Interval(), Boolean(), and Number()
+    properties, the form value is converted to the
+    appropriate
+
+Any of the form variables may be prefixed with a classname or
+designator.
+
+Two special form values are supported for backwards compatibility:
+
+@note
+    This is equivalent to::
+
+        @link@messages=msg-1
+        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
+    time, respectively.
+
+@file
+    This is equivalent to::
+
+        @link@files=file-1
+        file-1@content=value
+
+    The String content value is handled as described above for file
+    uploads.
+
+If both the "@note" and "@file" form variables are
+specified, the action::
+
+        @link@msg-1@files=file-1
+
+is also performed.
+
+We also check that FileClass items have a "content" property with
+actual content, otherwise we remove them from all_props before
+returning.
+
+
+
 Default templates
 -----------------
 
@@ -936,9 +1094,9 @@ The *classic* template has a number of additional templates.
 Note: Remember that you can create any template extension you want to,
 so if you just want to play around with the templating for new issues,
 you can copy the current "issue.item" template to "issue.test", and then
-access the test template using the ":template" URL argument::
+access the test template using the "@template" URL argument::
 
-   http://your.tracker.example/tracker/issue?:template=test
+   http://your.tracker.example/tracker/issue?@template=test
 
 and it won't affect your users using the "issue.item" template.
 
@@ -1359,65 +1517,68 @@ _value          the value of the property if any - this is the actual
 
 There are several methods available on these wrapper objects:
 
-========= ================================================================
-Method    Description
-========= ================================================================
-plain     render a "plain" representation of the property. This method
-          may take two arguments:
-
-          escape
-           If true, escape the text so it is HTML safe (default: no). The
-           reason this defaults to off is that text is usually escaped
-           at a later stage by the TAL commands, unless the "structure"
-           option is used in the template. The following are all
-           equivalent::
-
-            <p tal:content="structure python:msg.content.plain(escape=1)" />
-            <p tal:content="python:msg.content.plain()" />
-            <p tal:content="msg/content/plain" />
-            <p tal:content="msg/content" />
-
-           Usually you'll only want to use the escape option in a
-           complex expression.
-
-          hyperlink
-           If true, turn URLs, email addresses and hyperdb item
-           designators in the text into hyperlinks (default: no). Note
-           that you'll need to use the "structure" TAL option if you
-           want to use this::
-
-            <p tal:content="structure python:msg.content.plain(hyperlink=1)" />
-
-           Note also that the text is automatically HTML-escaped before
-           the hyperlinking transformation.
-
-field     render an appropriate form edit field for the property - for
-          most types this is a text entry box, but for Booleans it's a
-          tri-state yes/no/neither selection.
-stext     only on String properties - render the value of the property
-          as StructuredText (requires the StructureText module to be
-          installed separately)
-multiline only on String properties - render a multiline form edit
-          field for the property
-email     only on String properties - render the value of the property
-          as an obscured email address
-confirm   only on Password properties - render a second form edit field
-          for the property, used for confirmation that the user typed
-          the password correctly. Generates a field with name
-          "name:confirm".
-now       only on Date properties - return the current date as a new
-          property
-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
-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
-          list for this property
-reverse   only on Multilink properties - produce a list of the linked
-          items in reverse order
-========= =====================================================================
+=========== ================================================================
+Method      Description
+=========== ================================================================
+plain       render a "plain" representation of the property. This method
+            may take two arguments:
+
+            escape
+             If true, escape the text so it is HTML safe (default: no). The
+             reason this defaults to off is that text is usually escaped
+             at a later stage by the TAL commands, unless the "structure"
+             option is used in the template. The following ``tal:content``
+             expressions are all equivalent::
+              "structure python:msg.content.plain(escape=1)"
+              "python:msg.content.plain()"
+              "msg/content/plain"
+              "msg/content"
+
+             Usually you'll only want to use the escape option in a
+             complex expression.
+
+            hyperlink
+             If true, turn URLs, email addresses and hyperdb item
+             designators in the text into hyperlinks (default: no). Note
+             that you'll need to use the "structure" TAL option if you
+             want to use this ``tal:content`` expression::
+  
+              "structure python:msg.content.plain(hyperlink=1)"
+
+             Note also that the text is automatically HTML-escaped before
+             the hyperlinking transformation.
+hyperlinked The same as msg.content.plain(hyperlink=1), but nicer::
+
+              "structure msg/content/hyperlinked"
+
+field       render an appropriate form edit field for the property - for
+            most types this is a text entry box, but for Booleans it's a
+            tri-state yes/no/neither selection.
+stext       only on String properties - render the value of the property
+            as StructuredText (requires the StructureText module to be
+            installed separately)
+multiline   only on String properties - render a multiline form edit
+            field for the property
+email       only on String properties - render the value of the property
+            as an obscured email address
+confirm     only on Password properties - render a second form edit field
+            for the property, used for confirmation that the user typed
+            the password correctly. Generates a field with name
+            "name:confirm".
+now         only on Date properties - return the current date as a new
+            property
+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
+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
+            list for this property
+reverse     only on Multilink properties - produce a list of the linked
+            items in reverse order
+=========== ================================================================
 
 
 The request variable
@@ -1508,6 +1669,10 @@ you want access to the "user" class, for example, you would use::
   db/user
   python:db.user
 
+Also, the current id of the current user is available as
+``db.getuid()``. This isn't so useful in templates (where you have
+``request/user``), but it can be useful in detectors or interfaces.
+
 The access results in a `hyperdb class wrapper`_.
 
 
@@ -1687,7 +1852,7 @@ Note: if you add a new column to the ``:columns`` form variable
 
 This is one of the class context views. The template used is typically
 "*classname*.search". The form on this page should have "search" as its
-``:action`` variable. The "search" action:
+``@action`` variable. The "search" action:
 
 - sets up additional filtering, as well as performing indexed text
   searching
@@ -1736,19 +1901,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')" />
@@ -1756,7 +1921,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')" />
@@ -1764,7 +1929,7 @@ template)::
  </tr>
  
  <tr>
-  <th nowrap>Assigned To</th>
+  <th>Assigned To</th>
   <td tal:content="structure context/assignedto/menu">
    assignedto menu
   </td>
@@ -1773,14 +1938,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>
  
@@ -1829,11 +1994,11 @@ Properties are specified as form variables:
 Once we have determined the "propname", we check to see if it is one of
 the special form values:
 
-``:required``
+``@required``
   The named property values must be supplied or a ValueError will be
   raised.
 
-``:remove:<propname>=id(s)``
+``@remove@<propname>=id(s)``
   The ids will be removed from the multilink property.
 
 ``:add:<propname>=id(s)``
@@ -1892,13 +2057,13 @@ templating through the "journal" method of the item*::
 Defining new web actions
 ------------------------
 
-You may define new actions to be triggered by the ``:action`` form
+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. 
 
 Adding action methods takes three steps; first you `define the new
 action method`_, then you `register the action method`_ with the cgi
-interface so it may be triggered by the ``:action`` form variable.
+interface so it may be triggered by the ``@action`` form variable.
 Finally you `use the new action`_ in your HTML form.
 
 See "`setting up a "wizard" (or "druid") for controlled adding of
@@ -1946,7 +2111,7 @@ Use the new action
 
 In your HTML form, add a hidden form element like so::
 
-  <input type="hidden" name=":action" value="myaction">
+  <input type="hidden" name="@action" value="myaction">
 
 where "myaction" is the name you registered in the previous step.
 
@@ -2128,7 +2293,7 @@ are going to add the category code::
      tal:condition="python:request.user.hasPermission('View', 'category')">
    <b>Categories</b><br>
    <a tal:condition="python:request.user.hasPermission('Edit', 'category')"
-      href="category?:template=item">New Category<br></a>
+      href="category?@template=item">New Category<br></a>
   </p>
 
 The first two lines is the classblock definition, which sets up a
@@ -2194,7 +2359,7 @@ 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::
 
-    <input type="hidden" name=":required" value="name">
+    <input type="hidden" name="@required" value="name">
 
 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
@@ -2211,7 +2376,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>
@@ -2243,19 +2408,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>
@@ -2284,7 +2450,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>
@@ -2309,48 +2475,53 @@ on the web server could look at all issues in the category "Web".
 
 If you look for "Search Issues" in the 'html/page.html' file, you will
 find that it looks something like 
-``<a href="issue?:template=search">Search Issues</a>``. This shows us
+``<a href="issue?@template=search">Search Issues</a>``. This shows us
 that when you click on "Search Issues" it will be looking for a
 ``issue.search.html`` file to display. So that is the file that we will
 change.
 
-This file should begin to look familiar, by now. It is a simple HTML
-form using a table to define structure. You can add the new category
-search code anywhere you like within that form::
-
-    <tr>
-     <th>Category:</th>
-     <td>
-      <select name="category">
-       <option value="">don't care</option>
-       <option value="">------------</option>
-       <option tal:repeat="s db/category/list"
-               tal:attributes="value s/name"
-               tal:content="s/name">category to filter on</option>
-      </select>
-     </td>
-     <td><input type="checkbox" name=":columns" value="category"
-                checked></td>
-     <td><input type="radio" name=":sort" value="category"></td>
-     <td><input type="radio" name=":group" value="category"></td>
-    </tr>
-
-Most of this is straightforward to anyone who knows HTML. It is just
-setting up a select list followed by a checkbox and a couple of radio
-buttons. 
+If you look at this file it should be starting to seem familiar, although it
+does use some new macros. You can add the new category search code anywhere you
+like within that form::
+
+  <tr tal:define="name string:category;
+                  db_klass string:category;
+                  db_content string:name;">
+    <th>Priority:</th>
+    <td metal:use-macro="search_select"></td>
+    <td metal:use-macro="column_input"></td>
+    <td metal:use-macro="sort_input"></td>
+    <td metal:use-macro="group_input"></td>
+  </tr>
 
-The ``tal:repeat`` part repeats the tag for every item in the "category"
-table and sets "s" to each category in turn.
+The definitions in the <tr> opening tag are used by the macros:
 
-The ``tal:attributes`` part is setting up the ``value=`` part of the
-option tag to be the name part of "s", which is the current category in
-the loop.
+- search_select expands to a drop-down box with all categories using db_klass
+  and db_content.
+- column_input expands to a checkbox for selecting what columns should be
+  displayed.
+- sort_input expands to a radio button for selecting what property should be
+  sorted on.
+- group_input expands to a radio button for selecting what property should be
+  group on.
 
-The ``tal:content`` part is setting the contents of the option tag to be
-the name part of "s" again. For objects more complex than category,
-obviously you would put an id in the value, and the descriptive part in
-the content; but for categories they are the same.
+The category search code above would expand to the following::
 
+  <tr>
+    <th>Category:</th>
+    <td>
+      <select name="category">
+        <option value="">don't care</option>
+        <option value="">------------</option>      
+        <option value="1">scipy</option>
+        <option value="2">chaco</option>
+        <option value="3">weave</option>
+      </select>
+    </td>
+    <td><input type="checkbox" name=":columns" value="category"></td>
+    <td><input type="radio" name=":sort" value="category"></td>
+    <td><input type="radio" name=":group" value="category"></td>
+  </tr>
 
 Adding category to the default view
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2443,12 +2614,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"
@@ -2478,10 +2649,10 @@ 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">
+    <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">
     remove</a>
    </td>
   </tr>
@@ -2556,8 +2727,8 @@ 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="@template" value="add_page1">
+      <input type="hidden" name="@action" value="page1submit">
 
       <strong>Category:</strong>
       <tal:block tal:replace="structure context/category/menu" />
@@ -2572,8 +2743,8 @@ Setting up a "wizard" (or "druid") for controlled adding of issues
           tal:condition="context/is_edit_ok"
           tal:define="cat request/form/category/value">
 
-      <input type="hidden" name=":template" value="add_page2">
-      <input type="hidden" name=":required" value="title">
+      <input type="hidden" name="@template" value="add_page2">
+      <input type="hidden" name="@required" value="title">
       <input type="hidden" name="category" tal:attributes="value cat">
        .
        .
@@ -2585,11 +2756,11 @@ Setting up a "wizard" (or "druid") for controlled adding of issues
 
     <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>
@@ -2618,7 +2789,7 @@ Setting up a "wizard" (or "druid") for controlled adding of issues
         # 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
+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).
 
 
@@ -2787,122 +2958,32 @@ be able to give a summary of the total time spent on a particular issue.
    the "times" property is the new link to the "timelog" class.
 
 3. We'll need to let people add in times to the issue, so in the web
-   interface we'll have a new entry field, just below the change note
-   box::
-
-    <tr>
-     <th nowrap>Time Log</th>
-     <td colspan="3"><input name=":timelog">
-      (enter as "3y 1m 4d 2:40:02" or parts thereof)
-     </td>
-    </tr>
-
-   Note that we've made up a new form variable, but since we place a
-   colon ":" in front of it, it won't clash with any existing property
-   variables. The names you *can't* use are ``:note``, ``:file``,
-   ``:action``, ``:required`` and ``:template``. These variables are
-   described in the section `performing actions in web requests`_.
-
-4. We also need to handle this new field in the CGI interface - the way
-   to do this is through implementing a new form action (see `Setting up
-   a "wizard" (or "druid") for controlled adding of issues`_ for another
-   example where we implemented a new CGI form action).
-
-   In this case, we'll want our action to:
-
-   1. create a new "timelog" entry,
-   2. fake that the issue's "times" property has been edited, and then
-   3. call the normal CGI edit action handler.
-
-   The code to do this is::
-
-    class Client(client.Client): 
-        ''' derives basic CGI implementation from the standard module, 
-            with any specific extensions 
-        ''' 
-        actions = client.Client.actions + (
-            ('edit_with_timelog', 'timelogEditAction'),
-            ('new_with_timelog', 'timelogEditAction'),
-        )
-
-        def timelogEditAction(self):
-            ''' Handle the creation of a new time log entry if
-                necessary.
-
-                If we create a new entry, fake up a CGI form value for
-                the altered "times" property of the issue being edited.
-   
-                Punt to the regular edit action when we're done.
-            '''
-            # if there's a timelog value specified, create an entry
-            if self.form.has_key(':timelog') and \
-                    self.form[':timelog'].value.strip():
-                period = Interval(self.form[':timelog'].value)
-                # create it
-                newid = self.db.timelog.create(period=period)
-
-                # if we're editing an existing item, get the old timelog
-                # value
-                if self.nodeid:
-                    l = self.db.issue.get(self.nodeid, 'times')
-                    l.append(newid)
-                else:
-                    l = [newid]
-
-                # now make the fake CGI form values
-                for entry in l:
-                    self.form.list.append(
-                        MiniFieldStorage('times', entry))
-
-            # punt to the normal edit action
-            if self.nodeid:
-                return self.editItemAction()
-            else:
-                return self.newItemAction()
-   
-   you add this code to your Client class in your tracker's
-   ``interfaces.py`` file. Locate the section that looks like::
-
-    class Client:
-        ''' derives basic CGI implementation from the standard module,
-            with any specific extensions 
-        ''' 
-        pass
-
-   and insert this code in place of the ``pass`` statement.
-
-5. You'll also need to modify your ``issue.item`` form submit action so
-   it calls the time logging action we just created. The current
-   template will look like this::
-
-    <tr>
-     <td>&nbsp;</td>
-     <td colspan="3" tal:content="structure context/submit">
-      submit button will go here
-     </td>
-    </tr>
-
-   replace it with this::
-
-    <tr>
-     <td>&nbsp;</td>
-     <td colspan="3">
-      <tal:block tal:condition="context/id">
-        <input type="hidden" name=":action" value="edit_with_timelog">
-        <input type="submit" name="submit" value="Submit Changes">
-      </tal:block>
-      <tal:block tal:condition="not:context/id">
-        <input type="hidden" name=":action" value="new_with_timelog">
-        <input type="submit" name="submit" value="Submit New Issue">
-      </tal:block>
-     </td>
-    </tr>
-
-   The important change is setting the action to "edit_with_timelog" for
-   edit operations (where the item exists) and "new_with_timelog" for
-   creations operations.
-
-6. We want to display a total of the time log times that have been
+   interface we'll have a new entry field. This is a special field
+   because unlike the other fields in the issue.item 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 `special form variables`_). In particular, we add a
+   field to capture a new timelog item's perdiod::
+
+    <tr> 
+     <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> 
+    </tr> 
+         
+   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::
+
+     <input type="hidden" name="@link@times" value="timelog-1" />
+
+   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.
+
+4. We want to display a total of the time log 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 method to the
@@ -2926,7 +3007,7 @@ be able to give a summary of the total time spent on a particular issue.
    ``totalTimeSpent`` method via the ``utils`` variable in our
    templates.
 
-7. Display the time log for an issue::
+5. Display the time log for an issue::
 
      <table class="otherinfo" tal:condition="context/times">
       <tr><th colspan="3" class="header">Time Log
@@ -3088,6 +3169,42 @@ now do all the work::
 And that's it!
 
 
+Using an LDAP database for user information
+-------------------------------------------
+
+A script that reads users from an LDAP store using
+http://python-ldap.sf.net/ 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 `Using a UN*X passwd file as the user database`_
+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::
+
+    def verifyPassword(self, userid, password):
+        ''' Verify the password that the user has supplied
+        '''
+        stored = self.db.user.get(self.userid, 'password')
+        if password == stored:
+            return 1
+        if not password and not stored:
+            return 1
+        return 0
+
+So you could reimplement this as something like::
+
+    def verifyPassword(self, userid, password):
+        ''' Verify the password that the user has supplied
+        '''
+        # look up some unique LDAP information about the user
+        username = self.db.user.get(self.userid, 'username')
+        # now verify the password supplied against the LDAP store
+
+
 Enabling display of either message summaries or the entire messages
 -------------------------------------------------------------------
 
@@ -3095,39 +3212,39 @@ This is pretty simple - all we need to do is copy the code from the
 example `displaying only message summaries in the issue display`_ into
 our template alongside the summary display, and then introduce a switch
 that shows either one or the other. We'll use a new form variable,
-``:whole_messages`` to achieve this::
+``@whole_messages`` to achieve this::
 
  <table class="messages" tal:condition="context/messages">
-  <tal:block tal:condition="not:request/form/:whole_messages/value | python:0">
+  <tal:block tal:condition="not:request/form/@whole_messages/value | python:0">
    <tr><th colspan="3" class="header">Messages</th>
        <th colspan="2" class="header">
-         <a href="?:whole_messages=yes">show entire messages</a>
+         <a href="?@whole_messages=yes">show entire messages</a>
        </th>
    </tr>
    <tr tal:repeat="msg context/messages">
     <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>
+     <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>
     </td>
    </tr>
   </tal:block>
 
-  <tal:block tal:condition="request/form/:whole_messages/value | python:0">
+  <tal:block tal:condition="request/form/@whole_messages/value | python:0">
    <tr><th colspan="2" class="header">Messages</th>
        <th class="header">
-         <a href="?:whole_messages=">show only summaries</a>
+         <a href="?@whole_messages=">show only summaries</a>
        </th>
    </tr>
    <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>)
+      (<a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>)
      </th>
     </tr>
     <tr><td colspan="3" tal:content="msg/content"></td></tr>
@@ -3161,7 +3278,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)" />
@@ -3268,6 +3385,190 @@ 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
 
 -------------------