Code

Fixed bug in filter_iter refactoring (lazy multilinks), in rare cases
[roundup.git] / doc / customizing.txt
index 6568c6a9c3de0b54ab265c4c19ddf967b384f611..58a8ec342f7376b324258a16f8179177a49e79aa 100644 (file)
@@ -1,9 +1,9 @@
+:tocdepth: 2
+
 ===================
 Customising Roundup
 ===================
 
-:Version: $Revision: 1.110 $
-
 .. This document borrows from the ZopeBook section on ZPT. The original is at:
    http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx
 
@@ -18,11 +18,11 @@ Before you get too far, it's probably worth having a quick read of the Roundup
 
 Customisation of Roundup can take one of six forms:
 
-1. `tracker configuration`_ file changes
+1. `tracker configuration`_ 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
@@ -39,254 +39,487 @@ Trackers have the following structure:
 =================== ========================================================
 Tracker File        Description
 =================== ========================================================
-config.py           Holds the basic `tracker configuration`_                 
-dbinit.py           Holds the `tracker schema`_                              
-interfaces.py       Defines the Web and E-Mail interfaces for the tracker    
-select_db.py        Selects the database back-end for the tracker            
+config.ini          Holds the basic `tracker configuration`_                 
+schema.py           Holds the `tracker schema`_                              
+initial_data.py     Holds any data to be entered into the database when the
+                    tracker is initialised.
 db/                 Holds the tracker's database                             
 db/files/           Holds the tracker's upload files and messages            
+db/backend_name     Names the database back-end for the tracker            
 detectors/          Auditors and reactors for this tracker                   
+extensions/         Additional web actions and templating utilities.
 html/               Web interface templates, images and style sheets         
+lib/                optional common imports for detectors and extensions
 =================== ======================================================== 
 
+
 Tracker Configuration
 =====================
 
-The ``config.py`` located in your tracker home contains the basic
+The ``config.ini`` located in your tracker home contains the basic
 configuration for the web and e-mail components of roundup's interfaces.
-As the name suggests, this file is a Python module. This means that any
-valid python expression may be used in the file. Mostly though, you'll
-be setting the configuration variables to string values. Python string
-values must be quoted with either single or double quotes::
-
-   'this is a string'
-   "this is also a string - use it when the value has 'single quotes'"
-   this is not a string - it's not quoted
-
-Python strings may use formatting that's almost identical to C string
-formatting. The ``%`` operator is used to perform the formatting, like
-so::
-
-    'roundup-admin@%s'%MAIL_DOMAIN
-
-this will create a string ``'roundup-admin@tracker.domain.example'`` if
-MAIL_DOMAIN is set to ``'tracker.domain.example'``.
-
-You'll also note some values are set to::
 
-   os.path.join(TRACKER_HOME, 'db')
-
-or similar. This creates a new string which holds the path to the
-``'db'`` directory in the TRACKER_HOME directory. This is just a
-convenience so if the TRACKER_HOME changes you don't have to edit
-multiple valoues.
-
-The configuration variables available are:
+Changes to the data captured by your tracker is controlled by the `tracker
+schema`_.  Some configuration is also performed using permissions - see the 
+`security / access controls`_ section. For example, to allow users to
+automatically register through the email interface, you must grant the
+"Anonymous" Role the "Email Access" Permission.
+
+The following is taken from the `Python Library Reference`__ (May 20, 2004)
+section "ConfigParser -- Configuration file parser":
+
+ The configuration file consists of sections, led by a "[section]" header
+ and followed by "name = value" entries, with line continuations on a
+ newline with leading whitespace. Note that leading whitespace is removed
+ from values. The optional values can contain format strings which
+ refer to other values in the same section. Lines beginning with "#" or ";"
+ are ignored and may be used to provide comments. 
+
+ For example::
+
+   [My Section]
+   foodir = %(dir)s/whatever
+   dir = frob
+
+ would resolve the "%(dir)s" to the value of "dir" ("frob" in this case)
+ resulting in "foodir" being "frob/whatever".
+
+__ http://docs.python.org/lib/module-ConfigParser.html
+
+Section **main**
+ database -- ``db``
+  Database directory path. The path may be either absolute or relative
+  to the directory containig this config file.
+
+ templates -- ``html``
+  Path to the HTML templates directory. The path may be either absolute
+  or relative to the directory containig this config file.
+
+ static_files -- default *blank*
+  Path to directory holding additional static files available via Web
+  UI.  This directory may contain sitewide images, CSS stylesheets etc.
+  and is searched for these files prior to the TEMPLATES directory
+  specified above.  If this option is not set, all static files are
+  taken from the TEMPLATES directory The path may be either absolute or
+  relative to the directory containig this config file.
+
+ admin_email -- ``roundup-admin``
+  Email address that roundup will complain to if it runs into trouble. If
+  the email address doesn't contain an ``@`` part, the MAIL_DOMAIN defined
+  below is used.
+
+ dispatcher_email -- ``roundup-admin``
+  The 'dispatcher' is a role that can get notified of new items to the
+  database. It is used by the ERROR_MESSAGES_TO config setting. If the
+  email address doesn't contain an ``@`` part, the MAIL_DOMAIN defined
+  below is used.
+
+ email_from_tag -- default *blank*
+  Additional text to include in the "name" part of the From: address used
+  in nosy messages. If the sending user is "Foo Bar", the From: line
+  is usually: ``"Foo Bar" <issue_tracker@tracker.example>``
+  the EMAIL_FROM_TAG goes inside the "Foo Bar" quotes like so:
+  ``"Foo Bar EMAIL_FROM_TAG" <issue_tracker@tracker.example>``
+
+ new_web_user_roles -- ``User``
+  Roles that a user gets when they register with Web User Interface.
+  This is a comma-separated list of role names (e.g. ``Admin,User``).
+
+ new_email_user_roles -- ``User``
+  Roles that a user gets when they register with Email Gateway.
+  This is a comma-separated string of role names (e.g. ``Admin,User``).
+
+ error_messages_to -- ``user``
+  Send error message emails to the ``dispatcher``, ``user``, or ``both``?
+  The dispatcher is configured using the DISPATCHER_EMAIL setting.
+  Allowed values: ``dispatcher``, ``user``, or ``both``
+
+ html_version -- ``html4``
+  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.
+  Allowed values: ``html4``, ``xhtml``
+
+ timezone -- ``0``
+  Numeric timezone offset used when users do not choose their own
+  in their settings.
+
+ instant_registration -- ``yes``
+  Register new users instantly, or require confirmation via
+  email?
+  Allowed values: ``yes``, ``no``
+
+ email_registration_confirmation -- ``yes``
+  Offer registration confirmation by email or only through the web?
+  Allowed values: ``yes``, ``no``
+
+ indexer_stopwords -- default *blank*
+  Additional stop-words for the full-text indexer specific to
+  your tracker. See the indexer source for the default list of
+  stop-words (e.g. ``A,AND,ARE,AS,AT,BE,BUT,BY, ...``).
+
+ umask -- ``02``
+  Defines the file creation mode mask.
+
+Section **tracker**
+ name -- ``Roundup issue tracker``
+  A descriptive name for your roundup instance.
+
+ web -- ``http://host.example/demo/``
+  The web address that the tracker is viewable at.
+  This will be included in information sent to users of the tracker.
+  The URL MUST include the cgi-bin part or anything else
+  that is required to get to the home page of the tracker.
+  You MUST include a trailing '/' in the URL.
+
+ email -- ``issue_tracker``
+  Email address that mail to roundup should go to.
+
+ language -- default *blank*
+  Default locale name for this tracker. If this option is not set, the
+  language is determined by the environment variable LANGUAGE, LC_ALL,
+  LC_MESSAGES, or LANG, in that order of preference.
+
+Section **web**
+ allow_html_file -- ``no``
+  Setting this option enables Roundup to serve uploaded HTML
+  file content *as HTML*. This is a potential security risk
+  and is therefore disabled by default. Set to 'yes' if you
+  trust *all* users uploading content to your tracker.
+
+ http_auth -- ``yes``
+  Whether to use HTTP Basic Authentication, if present.
+  Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION
+  variables supplied by your web server (in that order).
+  Set this option to 'no' if you do not wish to use HTTP Basic
+  Authentication in your web interface.
+
+ use_browser_language -- ``yes``
+  Whether to use HTTP Accept-Language, if present.
+  Browsers send a language-region preference list.
+  It's usually set in the client's browser or in their
+  Operating System.
+  Set this option to 'no' if you want to ignore it.
+
+ debug -- ``no``
+  Setting this option makes Roundup display error tracebacks
+  in the user's browser rather than emailing them to the
+  tracker admin."),
+
+Section **rdbms**
+ Settings in this section are used by Postgresql and MySQL backends only
+
+ name -- ``roundup``
+  Name of the database to use.
+
+ host -- ``localhost``
+  Database server host.
+
+ port -- default *blank*
+  TCP port number of the database server. Postgresql usually resides on
+  port 5432 (if any), for MySQL default port number is 3306. Leave this
+  option empty to use backend default.
+
+ user -- ``roundup``
+  Database user name that Roundup should use.
+
+ password -- ``roundup``
+  Database user password.
+
+ read_default_file -- ``~/.my.cnf``
+  Name of the MySQL defaults file. Only used in MySQL connections.
+
+ read_default_group -- ``roundup``
+  Name of the group to use in the MySQL defaults file. Only used in
+  MySQL connections.
+
+Section **logging**
+ config -- default *blank*
+  Path to configuration file for standard Python logging module. If this
+  option is set, logging configuration is loaded from specified file;
+  options 'filename' and 'level' in this section are ignored. The path may
+  be either absolute or relative to the directory containig this config file.
+
+ filename -- default *blank*
+  Log file name for minimal logging facility built into Roundup.  If no file
+  name specified, log messages are written on stderr. If above 'config'
+  option is set, this option has no effect. The path may be either absolute
+  or relative to the directory containig this config file.
+
+ level -- ``ERROR``
+  Minimal severity level of messages written to log file. If above 'config'
+  option is set, this option has no effect.
+  Allowed values: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``
+
+Section **mail**
+ Outgoing email options. Used for nosy messages, password reset and
+ registration approval requests.
+
+ domain -- ``localhost``
+  Domain name used for email addresses.
+
+ host -- default *blank*
+  SMTP mail host that roundup will use to send mail
+
+ username -- default *blank*
+  SMTP login name. Set this if your mail host requires authenticated access.
+  If username is not empty, password (below) MUST be set!
+
+ password -- default *blank*
+  SMTP login password.
+  Set this if your mail host requires authenticated access.
+
+ port -- default *25*
+  SMTP port on mail host.
+  Set this if your mail host runs on a different port.
+
+ local_hostname -- default *blank*
+  The fully qualified domain name (FQDN) to use during SMTP sessions. If left
+  blank, the underlying SMTP library will attempt to detect your FQDN. If your
+  mail host requires something specific, specify the FQDN to use.
+
+ tls -- ``no``
+  If your SMTP mail host provides or requires TLS (Transport Layer Security)
+  then you may set this option to 'yes'.
+  Allowed values: ``yes``, ``no``
+
+ tls_keyfile -- default *blank*
+  If TLS is used, you may set this option to the name of a PEM formatted
+  file that contains your private key. The path may be either absolute or
+  relative to the directory containig this config file.
+
+ tls_certfile -- default *blank*
+  If TLS is used, you may set this option to the name of a PEM formatted
+  certificate chain file. The path may be either absolute or relative
+  to the directory containig this config file.
+
+ charset -- utf-8
+  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).
+
+ debug -- default *blank*
+  Setting this option makes Roundup to write all outgoing email messages
+  to this file *instead* of sending them. This option has the same effect
+  as environment variable SENDMAILDEBUG. Environment variable takes
+  precedence. The path may be either absolute or relative to the directory
+  containig this config file.
+
+ add_authorinfo -- ``yes``
+  Add a line with author information at top of all messages send by
+  roundup.
+
+ add_authoremail -- ``yes``
+  Add the mail address of the author to the author information at the
+  top of all messages.  If this is false but add_authorinfo is true,
+  only the name of the actor is added which protects the mail address
+  of the actor from being exposed at mail archives, etc.
+
+Section **mailgw**
+ Roundup Mail Gateway options
+
+ keep_quoted_text -- ``yes``
+  Keep email citations when accepting messages. Setting this to ``no`` strips
+  out "quoted" text from the message. Signatures are also stripped.
+  Allowed values: ``yes``, ``no``
+
+ leave_body_unchanged -- ``no``
+  Preserve the email body as is - that is, keep the citations *and*
+  signatures.
+  Allowed values: ``yes``, ``no``
+
+ default_class -- ``issue``
+  Default class to use in the mailgw if one isn't supplied in email subjects.
+  To disable, leave the value blank.
+
+ language -- default *blank*
+  Default locale name for the tracker mail gateway.  If this option is
+  not set, mail gateway will use the language of the tracker instance.
+
+ subject_prefix_parsing -- ``strict``
+  Controls the parsing of the [prefix] on subject lines in incoming emails.
+  ``strict`` will return an error to the sender if the [prefix] is not
+  recognised. ``loose`` will attempt to parse the [prefix] but just
+  pass it through as part of the issue title if not recognised. ``none``
+  will always pass any [prefix] through as part of the issue title.
+
+ subject_suffix_parsing -- ``strict``
+  Controls the parsing of the [suffix] on subject lines in incoming emails.
+  ``strict`` will return an error to the sender if the [suffix] is not
+  recognised. ``loose`` will attempt to parse the [suffix] but just
+  pass it through as part of the issue title if not recognised. ``none``
+  will always pass any [suffix] through as part of the issue title.
+
+ subject_suffix_delimiters -- ``[]``
+  Defines the brackets used for delimiting the commands suffix in a subject
+  line.
+
+ subject_content_match -- ``always``
+  Controls matching of the incoming email subject line against issue titles
+  in the case where there is no designator [prefix]. ``never`` turns off
+  matching. ``creation + interval`` or ``activity + interval`` will match
+  an issue for the interval after the issue's creation or last activity.
+  The interval is a standard Roundup interval.
+
+ subject_updates_title -- ``yes``
+  Update issue title if incoming subject of email is different.
+  Setting this to ``no`` will ignore the title part of
+  the subject of incoming email messages.
+
+ refwd_re -- ``(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+``
+  Regular expression matching a single reply or forward prefix
+  prepended by the mailer. This is explicitly stripped from the
+  subject during parsing.  Value is Python Regular Expression
+  (UTF8-encoded).
+
+ origmsg_re -- `` ^[>|\s]*-----\s?Original Message\s?-----$``
+  Regular expression matching start of an original message if quoted
+  the in body.  Value is Python Regular Expression (UTF8-encoded).
+
+ sign_re -- ``^[>|\s]*-- ?$``
+  Regular expression matching the start of a signature in the message
+  body.  Value is Python Regular Expression (UTF8-encoded).
+
+ eol_re -- ``[\r\n]+``
+  Regular expression matching end of line.  Value is Python Regular
+  Expression (UTF8-encoded).
+
+ blankline_re -- ``[\r\n]+\s*[\r\n]+``
+  Regular expression matching a blank line.  Value is Python Regular
+  Expression (UTF8-encoded).
+
+ ignore_alternatives -- ``no``
+  When parsing incoming mails, roundup uses the first
+  text/plain part it finds. If this part is inside a
+  multipart/alternative, and this option is set, all other
+  parts of the multipart/alternative are ignored. The default
+  is to keep all parts and attach them to the issue.
+
+Section **pgp**
+ OpenPGP mail processing options
+
+ enable -- ``no``
+  Enable PGP processing. Requires pyme.
+
+ roles -- default *blank*
+  If specified, a comma-separated list of roles to perform PGP
+  processing on. If not specified, it happens for all users.
+
+ homedir -- default *blank*
+  Location of PGP directory. Defaults to $HOME/.gnupg if not
+  specified.
+
+Section **nosy**
+ Nosy messages sending
+
+ messages_to_author -- ``no``
+  Send nosy messages to the author of the message.
+  Allowed values: ``yes``, ``no``, ``new``
+
+ signature_position -- ``bottom``
+  Where to place the email signature.
+  Allowed values: ``top``, ``bottom``, ``none``
+
+ add_author -- ``new``
+  Does the author of a message get placed on the nosy list automatically?
+  If ``new`` is used, then the author will only be added when a message
+  creates a new issue. If ``yes``, then the author will be added on
+  followups too. If ``no``, they're never added to the nosy.
+  Allowed values: ``yes``, ``no``, ``new``
+  
+ add_recipients -- ``new``
+  Do the recipients (``To:``, ``Cc:``) of a message get placed on the nosy
+  list?  If ``new`` is used, then the recipients will only be added when a
+  message creates a new issue. If ``yes``, then the recipients will be added
+  on followups too. If ``no``, they're never added to the nosy.
+  Allowed values: ``yes``, ``no``, ``new``
 
-**TRACKER_HOME** - ``os.path.split(__file__)[0]``
- The tracker home directory. The above default code will automatically
- determine the tracker home for you, so you can just leave it alone.
+ email_sending -- ``single``
+  Controls the email sending from the nosy reactor. If ``multiple`` then
+  a separate email is sent to each recipient. If ``single`` then a single
+  email is sent with each recipient as a CC address.
 
-**MAILHOST** - ``'localhost'``
- The SMTP mail host that roundup will use to send e-mail.
+ max_attachment_size -- ``2147483647``
+  Attachments larger than the given number of bytes won't be attached
+  to nosy mails. They will be replaced by a link to the tracker's
+  download page for the file.
 
-**MAILUSER** - ``()``
- If your SMTP mail host requires a username and password for access, then
- specify them here. eg. ``MAILUSER = ('username', 'password')``
 
-**MAILHOST_TLS** - ``'no'``
- If your SMTP mail host provides or requires TLS (Transport Layer
- Security) then set ``MAILHOST_TLS = 'yes'``
+You may generate a new default config file using the ``roundup-admin
+genconfig`` command.
 
-**MAILHOST_TLS_KEYFILE** - ``''``
- If you're using TLS, you may also set MAILHOST_TLS_KEYFILE to the name of
- a PEM formatted file that contains your private key.
+Configuration variables may be referred to in lower or upper case. In code,
+variables not in the "main" section are referred to using their section and
+name, so "domain" in the section "mail" becomes MAIL_DOMAIN. The
+configuration variables available are:
 
-**MAILHOST_TLS_CERTFILE** - ``''``
- If you're using TLS and have specified a MAILHOST_TLS_KEYFILE, you may
- also set MAILHOST_TLS_CERTFILE to the name of a PEM formatted certificate
- chain file.
+Extending the configuration file
+--------------------------------
 
-**MAIL_DOMAIN** - ``'tracker.domain.example'``
- The domain name used for email addresses.
+You can't add new variables to the config.ini file in the tracker home but
+you can add two new config.ini files:
 
-**DATABASE** - ``os.path.join(TRACKER_HOME, 'db')``
- This is the directory that the database is going to be stored in. By default
- it is in the tracker home.
+- a config.ini in the ``extensions`` directory will be loaded and attached
+  to the config variable as "ext".
+- a config.ini in the ``detectors`` directory will be loaded and attached
+  to the config variable as "detectors".
 
-**TEMPLATES** - ``os.path.join(TRACKER_HOME, 'html')``
- This is the directory that the HTML templates reside in. By default they are
- in the tracker home.
+For example, the following in ``detectors/config.ini``::
 
-**TRACKER_NAME** - ``'Roundup issue tracker'``
- A descriptive name for your roundup tracker. This is sent out in e-mails and
- appears in the heading of CGI pages.
+    [main]
+    qa_recipients = email@example.com
 
-**TRACKER_EMAIL** - ``'issue_tracker@%s'%MAIL_DOMAIN``
- The email address that e-mail sent to roundup should go to. Think of it as the
- tracker's personal e-mail address.
+is accessible as::
 
-**TRACKER_WEB** - ``'http://tracker.example/cgi-bin/roundup.cgi/bugs/'``
- The web address that the tracker is viewable at. This will be included in
- information sent to users of the tracker. The URL **must** include the
- cgi-bin part or anything else that is required to get to the home page of
- the tracker. You **must** include a trailing '/' in the URL.
+    db.config.detectors['QA_RECIPIENTS']
 
-**ADMIN_EMAIL** - ``'roundup-admin@%s'%MAIL_DOMAIN``
- The email address that roundup will complain to if it runs into trouble.
+Note that the name grouping applied to the main configuration file is
+applied to the extension config files, so if you instead have::
 
-**EMAIL_FROM_TAG** - ``''``
- Additional text to include in the "name" part of the ``From:`` address used
- in nosy messages. If the sending user is "Foo Bar", the ``From:`` line is
- usually::
+    [qa]
+    recipients = email@example.com
 
-    "Foo Bar" <issue_tracker@tracker.example>
+then the above ``db.config.detectors['QA_RECIPIENTS']`` will still work.
 
- The EMAIL_FROM_TAG goes inside the "Foo Bar" quotes like so::
 
-    "Foo Bar EMAIL_FROM_TAG" <issue_tracker@tracker.example>
-
-**MESSAGES_TO_AUTHOR** - ``'new'``, ``'yes'`` or``'no'``
- Send nosy messages to the author of the message?
- If 'new' is used, then the author will only be sent the message when the
- message creates a new issue. If 'yes' then the author will always be sent
- a copy of the message they wrote.
+Tracker Schema
+==============
 
-**ADD_AUTHOR_TO_NOSY** - ``'new'``, ``'yes'`` or ``'no'``
- Does the author of a message get placed on the nosy list automatically?
- If ``'new'`` is used, then the author will only be added when a message
- creates a new issue. If ``'yes'``, then the author will be added on followups
- too. If ``'no'``, they're never added to the nosy.
+.. note::
+   if you modify the schema, you'll most likely need to edit the
+   `web interface`_ HTML template files and `detectors`_ to reflect
+   your changes.
 
-**ADD_RECIPIENTS_TO_NOSY** - ``'new'``, ``'yes'`` or ``'no'``
- Do the recipients (To:, Cc:) of a message get placed on the nosy list?
- If ``'new'`` is used, then the recipients will only be added when a message
- creates a new issue. If ``'yes'``, then the recipients will be added on
- followups too. If ``'no'``, they're never added to the nosy.
+A tracker schema defines what data is stored in the tracker's database.
+Schemas are defined using Python code in the ``schema.py`` module of your
+tracker.
 
-**EMAIL_SIGNATURE_POSITION** - ``'top'``, ``'bottom'`` or ``'none'``
- Where to place the email signature in messages that Roundup generates.
+The ``schema.py`` module
+------------------------
 
-**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 with ``>`` or ``|`` characters at
- the start of the line.
+The ``schema.py`` module contains two functions:
 
-**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, 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.
-
-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.::
-
-    # roundup home is this package's directory
-    TRACKER_HOME=os.path.split(__file__)[0]
-
-    # The SMTP mail host that roundup will use to send mail
-    MAILHOST = 'localhost'
-
-    # The domain name used for email addresses.
-    MAIL_DOMAIN = 'your.tracker.email.domain.example'
-
-    # This is the directory that the database is going to be stored in
-    DATABASE = os.path.join(TRACKER_HOME, 'db')
-
-    # This is the directory that the HTML templates reside in
-    TEMPLATES = os.path.join(TRACKER_HOME, 'html')
-
-    # A descriptive name for your roundup tracker
-    TRACKER_NAME = 'Roundup issue tracker'
-
-    # The email address that mail to roundup should go to
-    TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN
-
-    # The web address that the tracker is viewable at. This will be
-    # included in information sent to users of the tracker. The URL MUST
-    # include the cgi-bin part or anything else that is required to get
-    # to the home page of the tracker. You MUST include a trailing '/'
-    # in the URL.
-    TRACKER_WEB = 'http://tracker.example/cgi-bin/roundup.cgi/bugs/'
-
-    # The email address that roundup will complain to if it runs into
-    # trouble
-    ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN
-
-    # Additional text to include in the "name" part of the From: address
-    # used in nosy messages. If the sending user is "Foo Bar", the From:
-    # line is usually: "Foo Bar" <issue_tracker@tracker.example>
-    # the EMAIL_FROM_TAG goes inside the "Foo Bar" quotes like so:
-    #    "Foo Bar EMAIL_FROM_TAG" <issue_tracker@tracker.example>
-    EMAIL_FROM_TAG = ""
-
-    # Send nosy messages to the author of the message
-    MESSAGES_TO_AUTHOR = 'no'           # either 'yes' or 'no'
-
-    # Does the author of a message get placed on the nosy list
-    # automatically? If 'new' is used, then the author will only be
-    # added when a message creates a new issue. If 'yes', then the
-    # author will be added on followups too. If 'no', they're never
-    # added to the nosy.
-    ADD_AUTHOR_TO_NOSY = 'new'          # one of 'yes', 'no', 'new'
-
-    # Do the recipients (To:, Cc:) of a message get placed on the nosy
-    # list? If 'new' is used, then the recipients will only be added
-    # when a message creates a new issue. If 'yes', then the recipients
-    # will be added on followups too. If 'no', they're never added to
-    # the nosy.
-    ADD_RECIPIENTS_TO_NOSY = 'new'      # either 'yes', 'no', 'new'
-
-    # Where to place the email signature
-    EMAIL_SIGNATURE_POSITION = 'bottom' # one of 'top', 'bottom', 'none'
-
-    # Keep email citations
-    EMAIL_KEEP_QUOTED_TEXT = 'no'       # either 'yes' or 'no'
-
-    # Preserve the email body as is
-    EMAIL_LEAVE_BODY_UNCHANGED = 'no'   # either 'yes' or 'no'
-
-    # 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. Examples:
-    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'
-
-    # 
-    # SECURITY DEFINITIONS
-    #
-    # define the Roles that a user gets when they register with the
-    # tracker these are a comma-separated string of role names (e.g.
-    # 'Admin,User')
-    NEW_WEB_USER_ROLES = 'User'
-    NEW_EMAIL_USER_ROLES = 'User'
+**open**
+  This function defines what your tracker looks like on the inside, the
+  **schema** of the tracker. It defines the **Classes** and **properties**
+  on each class. It also defines the **security** for those Classes. The
+  next few sections describe how schemas work and what you can do with
+  them.
+**init**
+  This function is responsible for setting up the initial state of your
+  tracker. It's called exactly once - by the ``roundup-admin initialise``
+  command.  See the start of the section on `database content`_ for more
+  info about how this works.
 
-Tracker Schema
-==============
 
-Note: if you modify the schema, you'll most likely need to edit the
-      `web interface`_ HTML template files and `detectors`_ to reflect
-      your changes.
+The "classic" schema
+--------------------
 
-A tracker schema defines what data is stored in the tracker's database.
-Schemas are defined using Python code in the ``dbinit.py`` module of your
-tracker. The "classic" schema looks like this (see below for the meaning
-of ``'setkey'``)::
+The "classic" schema looks like this (see section `setkey(property)`_
+below for the meaning of ``'setkey'`` -- you may also want to look into
+the sections `setlabelprop(property)`_ and `setorderprop(property)`_ for
+specifying (default) labelling and ordering of classes.)::
 
     pri = Class(db, "priority", name=String(), order=String())
     pri.setkey("name")
@@ -299,16 +532,17 @@ of ``'setkey'``)::
 
     user = Class(db, "user", username=String(), organisation=String(),
         password=String(), address=String(), realname=String(),
-        phone=String())
+        phone=String(), alternate_addresses=String(),
+        queries=Multilink('query'), roles=String(), timezone=String())
     user.setkey("username")
 
     msg = FileClass(db, "msg", author=Link("user"), summary=String(),
         date=Date(), recipients=Multilink("user"),
-        files=Multilink("file"))
+        files=Multilink("file"), messageid=String(), inreplyto=String())
 
-    file = FileClass(db, "file", name=String(), type=String())
+    file = FileClass(db, "file", name=String())
 
-    issue = IssueClass(db, "issue", topic=Multilink("keyword"),
+    issue = IssueClass(db, "issue", keyword=Multilink("keyword"),
         status=Link("status"), assignedto=Link("user"),
         priority=Link("priority"))
     issue.setkey('title')
@@ -320,8 +554,10 @@ 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.
+  This class is the only *required* class in Roundup.
+
+**Remove the "username", "address", "password" or "realname" user properties**
+  Various parts of Roundup require these properties. Don't remove them.
 
 **Change the type of a property**
   Property types must *never* be changed - the database simply doesn't take
@@ -415,6 +651,17 @@ A Class is comprised of one or more properties of the following types:
 * A Multilink property refers to possibly many items in a specified
   class. The value is a list of integers.
 
+All Classes automatically have a number of properties by default:
+
+*creator*
+  Link to the user that created the item.
+*creation*
+  Date the item was created.
+*actor*
+  Link to the user that last modified the item.
+*activity*
+  Date the item was last modified.
+
 
 FileClass
 ~~~~~~~~~
@@ -424,7 +671,8 @@ the rest of the database. This reduces the number of large entries in
 the database, which generally makes databases more efficient, and also
 allows us to use command-line tools to operate on the files. They are
 stored in the files sub-directory of the ``'db'`` directory in your
-tracker.
+tracker. FileClasses also have a "type" attribute to store the MIME
+type of the file.
 
 
 IssueClass
@@ -468,12 +716,37 @@ or::
 
 Note, the same thing can be done in the web and e-mail interfaces. 
 
-If a class does not have an "order" property, the key is also used to
-sort instances of the class when it is rendered in the user interface.
-(If a class has no "order" property, sorting is by the labelproperty of
-the class. This is computed, in order of precedence, as the key, the
-"name", the "title", or the first property alphabetically.)
+setlabelprop(property)
+~~~~~~~~~~~~~~~~~~~~~~
+
+Select a property of the class to be the label property. The label
+property is used whereever an item should be uniquely identified, e.g.,
+when displaying a link to an item. If setlabelprop is not specified for
+a class, the following values are tried for the label: 
+
+ * the key of the class (see the `setkey(property)`_ section above)
+ * the "name" property
+ * the "title" property
+ * the first property from the sorted property name list
+
+So in most cases you can get away without specifying setlabelprop
+explicitly.
+
+setorderprop(property)
+~~~~~~~~~~~~~~~~~~~~~~
+
+Select a property of the class to be the order property. The order
+property is used whenever using a default sort order for the class,
+e.g., when grouping or sorting class A by a link to class B in the user
+interface, the order property of class B is used for sorting.  If
+setorderprop is not specified for a class, the following values are tried
+for the order property:
 
+ * the property named "order"
+ * the label property (see `setlabelprop(property)`_ above)
+
+So in most cases you can get away without specifying setorderprop
+explicitly.
 
 create(information)
 ~~~~~~~~~~~~~~~~~~~
@@ -482,10 +755,30 @@ Create an item in the database. This is generally used to create items
 in the "definitional" classes like "priority" and "status".
 
 
+A note about ordering
+~~~~~~~~~~~~~~~~~~~~~
+
+When we sort items in the hyperdb, we use one of a number of methods,
+depending on the properties being sorted on:
+
+1. If it's a String, Number, Date or Interval property, we just sort the
+   scalar value of the property. Strings are sorted case-sensitively.
+2. If it's a Link property, we sort by either the linked item's "order"
+   property (if it has one) or the linked item's "id".
+3. Mulitlinks sort similar to #2, but we start with the first Multilink
+   list item, and if they're the same, we sort by the second item, and
+   so on.
+
+Note that if an "order" property is defined on a Class that is used for
+sorting, all items of that Class *must* have a value against the "order"
+property, or sorting will result in random ordering.
+
+
 Examples of adding to your schema
 ---------------------------------
 
-TODO
+The Roundup wiki has examples of how schemas can be customised to add
+new functionality.
 
 
 Detectors - adding behaviour to your tracker
@@ -494,7 +787,7 @@ Detectors - adding behaviour to your tracker
 
 Detectors are initialised every time you open your tracker database, so
 you're free to add and remove them any time, even after the database is
-initialised via the "roundup-admin initialise" command.
+initialised via the ``roundup-admin initialise`` command.
 
 The detectors in your tracker fire *before* (**auditors**) and *after*
 (**reactors**) changes to the contents of your database. They are Python
@@ -530,6 +823,48 @@ interface for detectors.
 
 __ design.html
 
+
+Detector API
+------------
+
+Auditors are called with the arguments::
+
+    audit(db, cl, itemid, newdata)
+
+where ``db`` is the database, ``cl`` is an instance of Class or
+IssueClass within the database, and ``newdata`` is a dictionary mapping
+property names to values.
+
+For a ``create()`` operation, the ``itemid`` argument is None and
+newdata contains all of the initial property values with which the item
+is about to be created.
+
+For a ``set()`` operation, newdata contains only the names and values of
+properties that are about to be changed.
+
+For a ``retire()`` or ``restore()`` operation, newdata is None.
+
+Reactors are called with the arguments::
+
+    react(db, cl, itemid, olddata)
+
+where ``db`` is the database, ``cl`` is an instance of Class or
+IssueClass within the database, and ``olddata`` is a dictionary mapping
+property names to values.
+
+For a ``create()`` operation, the ``itemid`` argument is the id of the
+newly-created item and ``olddata`` is None.
+
+For a ``set()`` operation, ``olddata`` contains the names and previous
+values of properties that were changed.
+
+For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of
+the retired or restored item and ``olddata`` is None.
+
+
+Additional Detectors Ready For Use
+----------------------------------
+
 Sample additional detectors that have been found useful will appear in
 the ``'detectors'`` directory of the Roundup distribution. If you want
 to use one, copy it to the ``'detectors'`` of your tracker instance:
@@ -539,36 +874,71 @@ to use one, copy it to the ``'detectors'`` of your tracker instance:
   created. The address is hard-coded into the detector, so edit it
   before you use it (look for the text 'team@team.host') or you'll get
   email errors!
+**creator_resolution.py**
+  Catch attempts to set the status to "resolved" - if the assignedto
+  user isn't the creator, then set the status to "confirm-done". Note that
+  "classic" Roundup doesn't have that status, so you'll have to add it. If
+  you don't want to though, it'll just use "in-progress" instead.
+**email_auditor.py**
+  If a file added to an issue is of type message/rfc822, we tack on the
+  extension .eml.
+  The reason for this is that Microsoft Internet Explorer will not open
+  things with a .eml attachment, as they deem it 'unsafe'. Worse yet,
+  they'll just give you an incomprehensible error message. For more 
+  information, see the detector code - it has a length explanation.
+
+
+Auditor or Reactor?
+-------------------
 
-  The detector code::
+Generally speaking, the following rules should be observed:
 
-    from roundup import roundupdb
+**Auditors**
+  Are used for `vetoing creation of or changes to items`_. They might
+  also make automatic changes to item properties.
+**Reactors**
+  Detect changes in the database and react accordingly. They should avoid
+  making changes to the database where possible, as this could create
+  detector loops.
 
-    def newissuecopy(db, cl, nodeid, oldvalues):
-        ''' Copy a message about new issues to a team address.
-        '''
-        # so use all the messages in the create
-        change_note = cl.generateCreateNote(nodeid)
 
-        # send a copy to the nosy list
-        for msgid in cl.get(nodeid, 'messages'):
-            try:
-                # note: last arg must be a list
-                cl.send_message(nodeid, msgid, change_note,
-                    ['team@team.host'])
-            except roundupdb.MessageSendError, message:
-                raise roundupdb.DetectorError, message
+Vetoing creation of or changes to items
+---------------------------------------
 
-    def init(db):
-        db.issue.react('create', newissuecopy)
+Auditors may raise the ``Reject`` exception to prevent the creation of
+or changes to items in the database.  The mail gateway, for example, will
+not attach files or messages to issues when the creation of those files or
+messages are prevented through the ``Reject`` exception. It'll also not create
+users if that creation is ``Reject``'ed too.
+
+To use, simply add at the top of your auditor::
+
+   from roundup.exceptions import Reject
+
+And then when your rejection criteria have been detected, simply::
+
+   raise Reject
+
+
+Generating email from Roundup
+-----------------------------
+
+The module ``roundup.mailer`` contains most of the nuts-n-bolts required
+to generate email messages from Roundup.
+
+In addition, the ``IssueClass`` methods ``nosymessage()`` and
+``send_message()`` are used to generate nosy messages, and may generate
+messages which only consist of a change note (ie. the message id parameter
+is not required - this is referred to as a "System Message" because it
+comes from "the system" and not a user).
 
 
 Database Content
 ================
 
-Note: if you modify the content of definitional classes, you'll most
-       likely need to edit the tracker `detectors`_ to reflect your
-       changes.
+.. note::
+   If you modify the content of definitional classes, you'll most
+   likely need to edit the tracker `detectors`_ to reflect your changes.
 
 Customisation of the special "definitional" classes (eg. status,
 priority, resolution, ...) may be done either before or after the
@@ -576,8 +946,8 @@ tracker is initialised. The actual method of doing so is completely
 different in each case though, so be careful to use the right one.
 
 **Changing content before tracker initialisation**
-    Edit the dbinit module in your tracker to alter the items created in
-    using the ``create()`` methods.
+    Edit the initial_data.py module in your tracker to alter the items
+    created using the ``create( ... )`` methods.
 
 **Changing content after tracker initialisation**
     As the "admin" user, click on the "class list" link in the web
@@ -592,71 +962,150 @@ 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:
 
+- Create (everything)
 - Edit (everything)
 - View (everything)
-
-The default interfaces define:
-
-- Web Registration
-- Web Access
-- Web Roles
-- Email Registration
-- Email Access
+- Register (User class only)
+
+These are assigned to the "Admin" Role by default, and allow a user to do
+anything. Every Class you define in your `tracker schema`_ also gets an
+Create, Edit and View Permission of its own. The web and email interfaces
+also define:
+
+*Email Access*
+  If defined, the user may use the email interface. Used by default to deny
+  Anonymous users access to the email interface. When granted to the
+  Anonymous user, they will be automatically registered by the email
+  interface (see also the ``new_email_user_roles`` configuration option).
+*Web Access*
+  If defined, the user may use the web interface. All users are able to see
+  the login form, regardless of this setting (thus enabling logging in).
+*Web Roles*
+  Controls user access to editing the "roles" property of the "user" class.
+  TODO: deprecate in favour of a property-based control.
 
 These are hooked into the default Roles:
 
-- Admin (Edit everything, View everything, Web Roles)
-- User (Web Access, Email Access)
-- Anonymous (Web Registration, Email Registration)
+- Admin (Create, Edit, View and everything; Web Roles)
+- User (Web Access; Email Access)
+- Anonymous (Web Access)
 
 And finally, the "admin" user gets the "Admin" Role, and the "anonymous"
-user gets "Anonymous" assigned when the database is initialised on
-installation. The two default schemas then define:
+user gets "Anonymous" assigned when the tracker is installed.
+
+For the "User" Role, the "classic" tracker defines:
 
-- Edit issue, View issue (both)
-- Edit file, View file (both)
-- Edit msg, View msg (both)
-- Edit support, View support (extended only)
+- Create, Edit and View issue, file, msg, query, keyword 
+- View priority, status
+- View user
+- Edit their own user record
 
-and assign those Permissions to the "User" Role. Put together, these
-settings appear in the ``open()`` function of the tracker ``dbinit.py``
-(the following is taken from the "minimal" template's ``dbinit.py``)::
+And the "Anonymous" Role is defined as:
+
+- Web interface access
+- Register user (for registration)
+- View issue, file, msg, query, keyword, priority, status
+
+Put together, these settings appear in the tracker's ``schema.py`` file::
 
     #
-    # SECURITY SETTINGS
+    # TRACKER SECURITY SETTINGS
     #
-    # new permissions for this schema
-    for cl in ('user', ):
-        db.security.addPermission(name="Edit", klass=cl,
-            description="User is allowed to edit "+cl)
-        db.security.addPermission(name="View", klass=cl,
-            description="User is allowed to access "+cl)
-
-    # and give the regular users access to the web and email interface
-    p = db.security.getPermission('Web Access')
-    db.security.addPermissionToRole('User', p)
-    p = db.security.getPermission('Email Access')
-    db.security.addPermissionToRole('User', p)
+    # See the configuration and customisation document for information
+    # about security setup.
+
+    #
+    # REGULAR USERS
+    #
+    # Give the regular users access to the web and email interface
+    db.security.addPermissionToRole('User', 'Web Access')
+    db.security.addPermissionToRole('User', 'Email Access')
+
+    # Assign the access and edit Permissions for issue, file and message
+    # to regular users now
+    for cl in 'issue', 'file', 'msg', 'query', 'keyword':
+        db.security.addPermissionToRole('User', 'View', cl)
+        db.security.addPermissionToRole('User', 'Edit', cl)
+        db.security.addPermissionToRole('User', 'Create', cl)
+    for cl in 'priority', 'status':
+        db.security.addPermissionToRole('User', 'View', cl)
 
     # May users view other user information? Comment these lines out
     # if you don't want them to
-    p = db.security.getPermission('View', 'user')
+    db.security.addPermissionToRole('User', 'View', 'user')
+
+    # Users should be able to edit their own details -- this permission
+    # is limited to only the situation where the Viewed or Edited item
+    # is their own.
+    def own_record(db, userid, itemid):
+        '''Determine whether the userid matches the item being accessed.'''
+        return userid == itemid
+    p = db.security.addPermission(name='View', klass='user', check=own_record,
+        description="User is allowed to view their own user details")
+    db.security.addPermissionToRole('User', p)
+    p = db.security.addPermission(name='Edit', klass='user', check=own_record,
+        description="User is allowed to edit their own user details")
     db.security.addPermissionToRole('User', p)
 
-    # Assign the appropriate permissions to the anonymous user's
-    # Anonymous role. Choices here are:
-    # - Allow anonymous users to register through the web
-    p = db.security.getPermission('Web Registration')
-    db.security.addPermissionToRole('Anonymous', p)
-    # - Allow anonymous (new) users to register through the email
-    #   gateway
-    p = db.security.getPermission('Email Registration')
-    db.security.addPermissionToRole('Anonymous', p)
+    #
+    # ANONYMOUS USER PERMISSIONS
+    #
+    # Let anonymous users access the web interface. Note that almost all
+    # trackers will need this Permission. The only situation where it's not
+    # required is in a tracker that uses an HTTP Basic Authenticated front-end.
+    db.security.addPermissionToRole('Anonymous', 'Web Access')
+
+    # Let anonymous users access the email interface (note that this implies
+    # that they will be registered automatically, hence they will need the
+    # "Create" user Permission below)
+    # This is disabled by default to stop spam from auto-registering users on
+    # public trackers.
+    #db.security.addPermissionToRole('Anonymous', 'Email Access')
+
+    # Assign the appropriate permissions to the anonymous user's Anonymous
+    # Role. Choices here are:
+    # - Allow anonymous users to register
+    db.security.addPermissionToRole('Anonymous', 'Create', 'user')
+
+    # Allow anonymous users access to view issues (and the related, linked
+    # information)
+    for cl in 'issue', 'file', 'msg', 'keyword', 'priority', 'status':
+        db.security.addPermissionToRole('Anonymous', 'View', cl)
+
+    # [OPTIONAL]
+    # Allow anonymous users access to create or edit "issue" items (and the
+    # related file and message items)
+    #for cl in 'issue', 'file', 'msg':
+    #   db.security.addPermissionToRole('Anonymous', 'Create', cl)
+    #   db.security.addPermissionToRole('Anonymous', 'Edit', cl)
+
+
+Automatic Permission Checks
+---------------------------
+
+Permissions are automatically checked when information is rendered
+through the web. This includes:
+
+1. View checks for properties when being rendered via the ``plain()`` or
+   similar methods. If the check fails, the text "[hidden]" will be
+   displayed.
+2. Edit checks for properties when the edit field is being rendered via
+   the ``field()`` or similar methods. If the check fails, the property
+   will be rendered via the ``plain()`` method (see point 1. for subsequent
+   checking performed)
+3. View checks are performed in index pages for each item being displayed
+   such that if the user does not have permission, the row is not rendered.
+4. View checks are performed at the top of item pages for the Item being
+   displayed. If the user does not have permission, the text "You are not
+   allowed to view this page." will be displayed.
+5. View checks are performed at the top of index pages for the Class being
+   displayed. If the user does not have permission, the text "You are not
+   allowed to view this page." will be displayed.
 
 
 New User Roles
@@ -667,6 +1116,9 @@ New users are assigned the Roles defined in the config file as:
 - NEW_WEB_USER_ROLES
 - NEW_EMAIL_USER_ROLES
 
+The `users may only edit their issues`_ example shows customisation of
+these parameters.
+
 
 Changing Access Controls
 ------------------------
@@ -684,33 +1136,57 @@ Adding a new Permission
 
 When adding a new Permission, you will need to:
 
-1. add it to your tracker's dbinit so it is created
+1. add it to your tracker's ``schema.py`` so it is created, using
+   ``security.addPermission``, for example::
+
+    self.security.addPermission(name="View", klass='frozzle',
+        description="User is allowed to access frozzles")
+
+   will set up a new "View" permission on the Class "frozzle".
 2. enable it for the Roles that should have it (verify with
    "``roundup-admin security``")
 3. add it to the relevant HTML interface templates
 4. add it to the appropriate xxxPermission methods on in your tracker
    interfaces module
 
+The ``addPermission`` method takes a couple of optional parameters:
+
+**properties**
+  A sequence of property names that are the only properties to apply the
+  new Permission to (eg. ``... klass='user', properties=('name',
+  'email') ...``)
+**check**
+  A function to be execute which returns boolean determining whether the
+  Permission is allowed. The function has the signature ``check(db, userid,
+  itemid)`` where ``db`` is a handle on the open database, ``userid`` is
+  the user attempting access and ``itemid`` is the specific item being
+  accessed.
 
 Example Scenarios
 ~~~~~~~~~~~~~~~~~
 
-**automatic registration of users in the e-mail gateway**
- By giving the "anonymous" user the "Email Registration" Role, any
- unidentified user will automatically be registered with the tracker
- (with no password, so they won't be able to log in through the web
- until an admin sets their password). Note: this is the default
- behaviour in the tracker templates that ship with Roundup.
+See the `examples`_ section for longer examples of customisation.
 
 **anonymous access through the e-mail gateway**
- Give the "anonymous" user the "Email Access" and ("Edit", "issue")
- Roles but do not not give them the "Email Registration" Role. This
- means that when an unknown user sends email into the tracker, they're
- automatically logged in as "anonymous". Since they don't have the
- "Email Registration" Role, they won't be automatically registered, but
- since "anonymous" has permission to use the gateway, they'll still be
- able to submit issues. Note that the Sender information - their email
- address - will not be available - they're *anonymous*.
+ Give the "anonymous" user the "Email Access", ("Edit", "issue") and
+ ("Create", "msg") Permissions but do not not give them the ("Create",
+ "user") Permission. This means that when an unknown user sends email
+ into the tracker, they're automatically logged in as "anonymous".
+ Since they don't have the ("Create", "user") Permission, they won't
+ be automatically registered, but since "anonymous" has permission to
+ use the gateway, they'll still be able to submit issues. Note that
+ the Sender information - their email address - will not be available
+ - they're *anonymous*.
+
+**automatic registration of users in the e-mail gateway**
+ By giving the "anonymous" user the ("Register", "user") Permission, any
+ unidentified user will automatically be registered with the tracker
+ (with no password, so they won't be able to log in through
+ the web until an admin sets their password). By default new Roundup
+ trackers don't allow this as it opens them up to spam. It may be enabled
+ by uncommenting the appropriate addPermissionToRole in your tracker's
+ ``schema.py`` file. The new user is given the Roles list defined in the
+ "new_email_user_roles" config variable. 
 
 **only developers may be assigned issues**
  Create a new Permission called "Fixer" for the "issue" class. Create a
@@ -753,17 +1229,17 @@ Web Interface
 
 .. contents::
    :local:
-   :depth: 1
 
 The web interface is provided by the ``roundup.cgi.client`` module and
 is used by ``roundup.cgi``, ``roundup-server`` and ``ZRoundup``
 (``ZRoundup``  is broken, until further notice). In all cases, we
 determine which tracker is being accessed (the first part of the URL
 path inside the scope of the CGI handler) and pass control on to the
-tracker ``interfaces.Client`` class - which uses the ``Client`` class
-from ``roundup.cgi.client`` - which handles the rest of the access
-through its ``main()`` method. This means that you can do pretty much
-anything you want as a web interface to your tracker.
+``roundup.cgi.client.Client`` class - which handles the rest of the
+access through its ``main()`` method. This means that you can do pretty
+much anything you want as a web interface to your tracker.
+
+
 
 Repercussions of changing the tracker schema
 ---------------------------------------------
@@ -776,6 +1252,7 @@ the web interface knows about it:
 2. The "page" template may require links to be changed, as might the
    "home" page's content arguments.
 
+
 How requests are processed
 --------------------------
 
@@ -802,6 +1279,7 @@ In some situations, exceptions occur:
     this exception percolates up to the CGI interface that called the
     client
 
+
 Determining web context
 -----------------------
 
@@ -811,20 +1289,23 @@ identifier is examined. Typical URL paths look like:
 
 1.  ``/tracker/issue``
 2.  ``/tracker/issue1``
-3.  ``/tracker/_file/style.css``
+3.  ``/tracker/@@file/style.css``
 4.  ``/cgi-bin/roundup.cgi/tracker/file1``
 5.  ``/cgi-bin/roundup.cgi/tracker/file1/kitten.png``
 
 where the "tracker identifier" is "tracker" in the above cases. That means
-we're looking at "issue", "issue1", "_file/style.css", "file1" and
+we're looking at "issue", "issue1", "@@file/style.css", "file1" and
 "file1/kitten.png" in the cases above. The path is generally only one
 entry long - longer paths are handled differently.
 
-a. if there is no path, then we are in the "home" context.
-b. if the path starts with "_file" (as in example 3,
-   "/tracker/_file/style.css"), then the additional path entry,
+a. if there is no path, then we are in the "home" context. See `the "home"
+   context`_ below for more information about how it may be used.
+b. if the path starts with "@@file" (as in example 3,
+   "/tracker/@@file/style.css"), then the additional path entry,
    "style.css" specifies the filename of a static file we're to serve up
-   from the tracker "html" directory. Raises a SendStaticFile exception.
+   from the tracker TEMPLATES (or STATIC_FILES, if configured) directory.
+   This is usually the tracker's "html" directory. Raises a SendStaticFile
+   exception.
 c. if there is something in the path (as in example 1, "issue"), it
    identifies the tracker class we're to display.
 d. if the path is an item designator (as in examples 2 and 4, "issue1"
@@ -846,6 +1327,32 @@ defaults to:
 - full item designator supplied: "item"
 
 
+The "home" Context
+------------------
+
+The "home" context is special because it allows you to add templated
+pages to your tracker that don't rely on a class or item (ie. an issues
+list or specific issue).
+
+Let's say you wish to add frames to control the layout of your tracker's
+interface. You'd probably have:
+
+- A top-level frameset page. This page probably wouldn't be templated, so
+  it could be served as a static file (see `serving static content`_)
+- A sidebar frame that is templated. Let's call this page
+  "home.navigation.html" in your tracker's "html" directory. To load that
+  page up, you use the URL:
+
+    <tracker url>/home?@template=navigation
+
+
+Serving static content
+----------------------
+
+See the previous section `determining web context`_ where it describes
+``@@file`` paths.
+
+
 Performing actions in web requests
 ----------------------------------
 
@@ -892,42 +1399,35 @@ 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``. 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
-are:
+Each action class also has a ``*permission*`` method which determines whether
+the action is permissible given the current user. The base permission checks
+for each action are:
 
 **login**
- Determine whether the user has permission to log in. Base behaviour is
- to check the user has "Web Access".
+ Determine whether the user has the "Web Access" Permission.
 **logout**
  No permission checks are made.
 **register**
- Determine whether the user has permission to register. Base behaviour
- is to check the user has the "Web Registration" Permission.
+ Determine whether the user has the ("Create", "user") Permission.
 **edit**
- Determine whether the user has permission to edit this item. Base
- behaviour is to check whether the user can edit this class. If we're
+ Determine whether the user has permission to edit this item. If we're
  editing the "user" class, users are allowed to edit their own details -
  unless they try to edit the "roles" property, which requires the
  special Permission "Web Roles".
 **new**
- Determine whether the user has permission to create (or edit) this
- item. Base behaviour is to check the user can edit this class. No
+ Determine whether the user has permission to create this item. No
  additional property checks are made. Additionally, new user items may
- be created if the user has the "Web Registration" Permission.
+ be created if the user has the ("Create", "user") Permission.
 **editCSV**
- Determine whether the user has permission to edit this class. Base
- behaviour is to check whether the user may edit this class.
+ Determine whether the user has permission to edit this class.
 **search**
- Determine whether the user has permission to search this class. Base
- behaviour is to check whether the user may view this class.
+ Determine whether the user has permission to view this class.
 
 
 Special form variables
@@ -944,10 +1444,32 @@ variables and their values. You can:
 - Remove items from a multilink property of the current item.
 - Specify that some properties are required for the edit
   operation to be successful.
+- Set up user interface locale.
+
+These operations will only take place if the form action (the
+``@action`` variable) is "edit" or "new".
 
 In the following, <bracketed> values are variable, "@" may be
 either ":" or "@", and other text "required" is fixed.
 
+Two special form variables are used to specify user language preferences:
+
+``@language``
+  value may be locale name or ``none``. If this variable is set to
+  locale name, web interface language is changed to given value
+  (provided that appropriate translation is available), the value
+  is stored in the browser cookie and will be used for all following
+  requests.  If value is ``none`` the cookie is removed and the
+  language is changed to the tracker default, set up in the tracker
+  configuration or OS environment.
+
+``@charset``
+  value may be character set name or ``none``.  Character set name
+  is stored in the browser cookie and sets output encoding for all
+  HTML pages generated by Roundup.  If value is ``none`` the cookie
+  is removed and HTML output is reset to Roundup internal encoding
+  (UTF-8).
+
 Most properties are specified as form variables:
 
 ``<propname>``
@@ -1006,11 +1528,11 @@ None of the above (ie. just a simple form value)
 
     For a Link('klass') property, the form value is a
     single key for 'klass', where the key field is
-    specified in dbinit.py.  
+    specified in schema.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.  
+    key field is specified in schema.py.  
 
     Note that for simple-form-variables specifiying Link
     and Multilink properties, the linked-to class must
@@ -1062,13 +1584,12 @@ actual content, otherwise we remove them from all_props before
 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'``.
+xhtml compliant, you'll need to change the ``html_version`` configuration
+variable in ``config.ini`` 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
@@ -1102,12 +1623,12 @@ of files in there. The *minimal* template includes:
 **user.register.html**
   a special page just for the user class, that renders the registration
   page
-**style.css.html**
+**style.css**
   a static file that is served up as-is
 
 The *classic* template has a number of additional templates.
 
-Note: Remember that you can create any template extension you want to,
+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::
@@ -1125,8 +1646,8 @@ Basic Templating Actions
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
 Roundup's templates consist of special attributes on the HTML tags.
-These attributes form the Template Attribute Language, or TAL. The basic
-TAL commands are:
+These attributes form the `Template Attribute Language`_, or TAL.
+The basic TAL commands are:
 
 **tal:define="variable expression; variable expression; ..."**
    Define a new variable that is local to this tag and its contents. For
@@ -1167,7 +1688,10 @@ TAL commands are:
      </tr>
 
    The example would iterate over the sequence of users returned by
-   "user/list" and define the local variable "u" for each entry.
+   "user/list" and define the local variable "u" for each entry. Using
+   the repeat command creates a new variable called "repeat" which you
+   may access to gather information about the iteration. See the section
+   below on `the repeat variable`_.
 
 **tal:replace="expression"**
    Replace this tag with the result of the expression. For example::
@@ -1219,12 +1743,17 @@ making arbitrary blocks of HTML conditional or repeatable (very handy
 for repeating multiple table rows, which would othewise require an
 illegal tag placement to effect the repeat).
 
+.. _TAL:
+.. _Template Attribute Language:
+   http://dev.zope.org/Wikis/DevSite/Projects/ZPT/TAL%20Specification%201.4
+
 
 Templating Expressions
 ~~~~~~~~~~~~~~~~~~~~~~
 
-The expressions you may use in the attribute values may be one of the
-following forms:
+Templating Expressions are covered by `Template Attribute Language
+Expression Syntax`_, or TALES. The expressions you may use in the
+attribute values may be one of the following forms:
 
 **Path Expressions** - eg. ``item/status/checklist``
    These are object attribute / item accesses. Roughly speaking, the
@@ -1281,6 +1810,10 @@ Modifiers:
    This simply inverts the logical true/false value of another
    expression.
 
+.. _TALES:
+.. _Template Attribute Language Expression Syntax:
+   http://dev.zope.org/Wikis/DevSite/Projects/ZPT/TALES%20Specification%201.3
+
 
 Template Macros
 ~~~~~~~~~~~~~~~
@@ -1291,8 +1824,8 @@ you'll use is the "icing" macro defined in the "page" template.
 
 Macros are generated and used inside your templates using special
 attributes similar to the `basic templating actions`_. In this case,
-though, the attributes belong to the Macro Expansion Template Attribute
-Language, or METAL. The macro commands are:
+though, the attributes belong to the `Macro Expansion Template
+Attribute Language`_, or METAL. The macro commands are:
 
 **metal:define-macro="macro name"**
   Define that the tag and its contents are now a macro that may be
@@ -1338,16 +1871,18 @@ Language, or METAL. The macro commands are:
   where the tag that fills the slot completely replaces the one defined
   as the slot in the macro.
 
-Note that you may not mix METAL and TAL commands on the same tag, but
+Note that you may not mix `METAL`_ and `TAL`_ commands on the same tag, but
 TAL commands may be used freely inside METAL-using tags (so your
 *fill-slots* tags may have all manner of TAL inside them).
 
+.. _METAL:
+.. _Macro Expansion Template Attribute Language:
+   http://dev.zope.org/Wikis/DevSite/Projects/ZPT/METAL%20Specification%201.0
 
 Information available to templates
 ----------------------------------
 
-Note: this is implemented by
-``roundup.cgi.templating.RoundupPageTemplate``
+This is implemented by ``roundup.cgi.templating.RoundupPageTemplate``
 
 The following variables are available to templates.
 
@@ -1359,12 +1894,17 @@ The following variables are available to templates.
    - the current index information (``filterspec``, ``filter`` args,
      ``properties``, etc) parsed out of the form. 
    - methods for easy filterspec link generation
-   - *user*, the current user item as an HTMLItem instance
-   - *form*
+   - "form"
      The current CGI form information as a mapping of form argument name
-     to value
+     to value (specifically a cgi.FieldStorage)
+   - "env" the CGI environment variables
+   - "base" the base URL for this instance
+   - "user" a HTMLItem instance for the current user
+   - "language" as determined by the browser or config
+   - "classname" the current classname (possibly None)
+   - "template" the current template (suffix, also possibly None)
 **config**
-  This variable holds all the values defined in the tracker config.py
+  This variable holds all the values defined in the tracker config.ini
   file (eg. TRACKER_NAME, etc.)
 **db**
   The current database, used to access arbitrary database items.
@@ -1397,6 +1937,23 @@ The following variables are available to templates.
 
     <span>Hello, World!</span>
 
+**true**, **false**
+  Boolean constants that may be used in `templating expressions`_
+  instead of ``python:1`` and ``python:0``.
+**i18n**
+  Internationalization service, providing two string translation methods:
+
+  **gettext** (*message*)
+    Return the localized translation of message
+  **ngettext** (*singular*, *plural*, *number*)
+    Like ``gettext()``, but consider plural forms. If a translation
+    is found, apply the plural formula to *number*, and return the
+    resulting message (some languages have more than two plural forms).
+    If no translation is found, return singular if *number* is 1;
+    return plural otherwise.
+
+    This function requires python2.3; in earlier python versions
+    may not work as expected.
 
 The context variable
 ~~~~~~~~~~~~~~~~~~~~
@@ -1424,10 +1981,10 @@ item. The only real difference between cases 2 and 3 above are:
 Hyperdb class wrapper
 :::::::::::::::::::::
 
-Note: this is implemented by the ``roundup.cgi.templating.HTMLClass``
+This is implemented by the ``roundup.cgi.templating.HTMLClass``
 class.
 
-This wrapper object provides access to a hyperb class. It is used
+This wrapper object provides access to a hyperdb class. It is used
 primarily in both index view and new item views, but it's also usable
 anywhere else that you wish to access information about a class, or the
 items of a class, when you don't have a specific item of that class in
@@ -1447,10 +2004,64 @@ properties  return a `hyperdb property wrapper`_ for all of this class's
 list        lists all of the active (not retired) items in the class.
 csv         return the items of this class as a chunk of CSV text.
 propnames   lists the names of the properties of this class.
-filter      lists of items from this class, filtered and sorted by the
-            current *request* filterspec/filter/sort/group args
+filter      lists of items from this class, filtered and sorted. Two
+            options are avaible for sorting:
+
+            1. by the current *request* filterspec/filter/sort/group args
+            2. by the "filterspec", "sort" and "group" keyword args.
+               "filterspec" is ``{propname: value(s)}``. "sort" and
+               "group" are an optionally empty list ``[(dir, prop)]``
+               where dir is '+', '-' or None
+               and prop is a prop name or None.
+
+               The propname in filterspec and prop in a sort/group spec
+               may be transitive, i.e., it may contain properties of
+               the form link.link.link.name.
+
+            eg. All issues with a priority of "1" with messages added in
+            the last week, sorted by activity date:
+            ``issue.filter(filterspec={"priority": "1",
+            'messages.creation' : '.-1w;'}, sort=[('activity', '+')])``
+
+filter_sql  **Only in SQL backends**
+
+            Lists the items that match the SQL provided. The SQL is a
+            complete "select" statement.
+
+            The SQL select must include the item id as the first column.
+
+            This function **does not** filter out retired items, add
+            on a where clause "__retired__ <> 1" if you don't want
+            retired nodes.
+
 classhelp   display a link to a javascript popup containing this class'
             "help" template.
+
+            This generates a link to a popup window which displays the
+            properties indicated by "properties" of the class named by
+            "classname". The "properties" should be a comma-separated list
+            (eg. 'id,name,description'). Properties defaults to all the
+            properties of a class (excluding id, creator, created and
+            activity).
+
+            You may optionally override the "label" displayed, the "width",
+            the "height", the number of items per page ("pagesize") and
+            the field on which the list is sorted ("sort").
+
+            With the "filter" arg it is possible to specify a filter for
+            which items are supposed to be displayed. It has to be of
+            the format "<field>=<values>;<field>=<values>;...".
+
+            The popup window will be resizable and scrollable.
+
+            If the "property" arg is given, it's passed through to the
+            javascript help_window function. This allows updating of a
+            property in the calling HTML page.
+
+            If the "form" arg is given, it's passed through to the
+            javascript help_window function - it's the name of the form
+            the "property" belongs to.
+
 submit      generate a submit button (and action hidden element)
 renderWith  render this class with the given template.
 history     returns 'New node - no history' :)
@@ -1470,10 +2081,10 @@ will access the "list" property, rather than the list method.
 Hyperdb item wrapper
 ::::::::::::::::::::
 
-Note: this is implemented by the ``roundup.cgi.templating.HTMLItem``
+This is implemented by the ``roundup.cgi.templating.HTMLItem``
 class.
 
-This wrapper object provides access to a hyperb item.
+This wrapper object provides access to a hyperdb item.
 
 We allow access to properties. There will be no "id" property. The value
 accessed through the property will be the current value of the same name
@@ -1491,9 +2102,30 @@ history         render the journal of the current item as HTML
 renderQueryForm specific to the "query" class - render the search form
                 for the query
 hasPermission   specific to the "user" class - determine whether the
-                user has a Permission
+                user has a Permission. The signature is::
+
+                    hasPermission(self, permission, [classname=],
+                        [property=], [itemid=])
+
+                where the classname defaults to the current context.
+hasRole         specific to the "user" class - determine whether the
+                user has a Role. The signature is::
+
+                    hasRole(self, rolename)
+
 is_edit_ok      is the user allowed to Edit the current item?
 is_view_ok      is the user allowed to View the current item?
+is_retired      is the item retired?
+download_url    generate a url-quoted link for download of FileClass
+                item contents (ie. file<id>/<name>)
+copy_url        generate a url-quoted link for creating a copy
+                of this item.  By default, the copy will acquire
+                all properties of the current item except for
+                ``messages`` and ``files``.  This can be overridden
+                by passing ``exclude`` argument which contains a list
+                (or any iterable) of property names that shall not be
+                copied.  Database-driven properties like ``id`` or
+                ``activity`` cannot be copied.
 =============== ========================================================
 
 Note that if you have a property of the same name as one of the above
@@ -1508,7 +2140,7 @@ will access the "journal" property, rather than the journal method.
 Hyperdb property wrapper
 ::::::::::::::::::::::::
 
-Note: this is implemented by subclasses of the
+This is implemented by subclasses of the
 ``roundup.cgi.templating.HTMLProperty`` class (``HTMLStringProperty``,
 ``HTMLNumberProperty``, and so on).
 
@@ -1562,15 +2194,30 @@ plain       render a "plain" representation of the property. This method
   
               "structure python:msg.content.plain(hyperlink=1)"
 
-             Note also that the text is automatically HTML-escaped before
-             the hyperlinking transformation.
+             The text is automatically HTML-escaped before the hyperlinking
+             transformation done in the plain() method.
+
 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.
+            tri-state yes/no/neither selection. This method may take some
+            arguments:
+
+            size
+              Sets the width in characters of the edit field
+
+            format (Date properties only)
+              Sets the format of the date in the field - uses the same
+              format string argument as supplied to the ``pretty`` method
+              below.
+
+            popcal (Date properties only)
+              Include the Javascript-based popup calendar for date
+              selection. Defaults to on.
+
 stext       only on String properties - render the value of the property
             as StructuredText (requires the StructureText module to be
             installed separately)
@@ -1587,20 +2234,92 @@ 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
-pretty      only on Interval properties - render the interval in a pretty
-            format (eg. "yesterday")
+            with some timezone offset, for example::
+            
+                python:context.creation.local(10)
+
+            will render the date with a +10 hour offset.
+pretty      Date properties - render the date as "dd Mon YYYY" (eg. "19
+            Mar 2004"). Takes an optional format argument, for example::
+
+                python:context.activity.pretty('%Y-%m-%d')
+
+            Will format as "2004-03-19" instead.
+
+            Interval properties - render the interval in a pretty
+            format (eg. "yesterday"). The format arguments are those used
+            in the standard ``strftime`` call (see the `Python Library
+            Reference: time module`__)
+popcal      Generate a link to a popup calendar which may be used to
+            edit the date field, for example::
+
+              <span tal:replace="structure context/due/popcal" />
+
+            you still need to include the ``field`` for the property, so
+            typically you'd have::
+
+              <span tal:replace="structure context/due/field" />
+              <span tal:replace="structure context/due/popcal" />
+
 menu        only on Link and Multilink properties - render a form select
-            list for this property
+            list for this property. Takes a number of optional arguments
+
+            size
+               is used to limit the length of the list labels
+            height
+               is used to set the <select> tag's "size" attribute
+            showid
+               includes the item ids in the list labels
+            additional
+               lists properties which should be included in the label
+            sort_on
+                indicates the property to sort the list on as (direction,
+                (direction, property) where direction is '+' or '-'. A
+                single string with the direction prepended may be used.
+                For example: ('-', 'order'), '+name'.
+            value
+                gives a default value to preselect in the menu
+
+            The remaining keyword arguments are used as conditions for
+            filtering the items in the list - they're passed as the
+            "filterspec" argument to a Class.filter() call. For example::
+
+             <span tal:replace="structure context/status/menu" />
+
+             <span tal:replace="python:context.status.menu(order='+name",
+                                   value='chatting', 
+                                   filterspec={'status': '1,2,3,4'}" />
+
+sorted      only on Multilink properties - produce a list of the linked
+            items sorted by some property, for example::
+            
+                python:context.files.sorted('creation')
+
+            Will list the files by upload date.
 reverse     only on Multilink properties - produce a list of the linked
             items in reverse order
+isset       returns True if the property has been set to a value
 =========== ================================================================
 
+__ http://docs.python.org/lib/module-time.html
+
+All of the above functions perform checks for permissions required to
+display or edit the data they are manipulating. The simplest case is
+editing an issue title. Including the expression::
+
+   context/title/field
+
+Will present the user with an edit field, if they have edit permission. If
+not, then they will be presented with a static display if they have view
+permission. If they don't even have view permission, then an error message
+is raised, preventing the display of the page, indicating that they don't
+have permission to view the information.
+
 
 The request variable
 ~~~~~~~~~~~~~~~~~~~~
 
-Note: this is implemented by the ``roundup.cgi.templating.HTMLRequest``
+This is implemented by the ``roundup.cgi.templating.HTMLRequest``
 class.
 
 The request variable is packed with information about the current
@@ -1628,10 +2347,11 @@ Variable    Holds
 columns     dictionary of the columns to display in an index page
 show        a convenience access to columns - request/show/colname will
             be true if the columns should be displayed, false otherwise
-sort        index sort column (direction, column name)
-group       index grouping property (direction, column name)
+sort        index sort columns [(direction, column name)]
+group       index grouping properties [(direction, column name)]
 filter      properties to filter the index on
-filterspec  values to filter the index on
+filterspec  values to filter the index on (property=value, eg
+            ``priority=1`` or ``messages.author=42``
 search_text text to perform a full-text search on for an index
 =========== ============================================================
 
@@ -1676,7 +2396,7 @@ best to know beforehand what you're dealing with.
 The db variable
 ~~~~~~~~~~~~~~~
 
-Note: this is implemented by the ``roundup.cgi.templating.HTMLDatabase``
+This is implemented by the ``roundup.cgi.templating.HTMLDatabase``
 class.
 
 Allows access to all hyperdb classes as attributes of this variable. If
@@ -1695,7 +2415,7 @@ The access results in a `hyperdb class wrapper`_.
 The templates variable
 ~~~~~~~~~~~~~~~~~~~~~~
 
-Note: this is implemented by the ``roundup.cgi.templating.Templates``
+This is implemented by the ``roundup.cgi.templating.Templates``
 class.
 
 This variable doesn't have any useful methods defined. It supports being
@@ -1719,11 +2439,39 @@ or the python expression::
 
    templates[name].macros[macro_name]
 
+The repeat variable
+~~~~~~~~~~~~~~~~~~~
+
+The repeat variable holds an entry for each active iteration. That is, if
+you have a ``tal:repeat="user db/users"`` command, then there will be a
+repeat variable entry called "user". This may be accessed as either::
+
+    repeat/user
+    python:repeat['user']
+
+The "user" entry has a number of methods available for information:
+
+=============== =========================================================
+Method          Description
+=============== =========================================================
+first           True if the current item is the first in the sequence.
+last            True if the current item is the last in the sequence.
+even            True if the current item is an even item in the sequence.
+odd             True if the current item is an odd item in the sequence.
+number          Current position in the sequence, starting from 1.
+letter          Current position in the sequence as a letter, a through
+                z, then aa through zz, and so on.
+Letter          Same as letter(), except uppercase.
+roman           Current position in the sequence as lowercase roman
+                numerals.
+Roman           Same as roman(), except uppercase.
+=============== =========================================================
+
 
 The utils variable
 ~~~~~~~~~~~~~~~~~~
 
-Note: this is implemented by the
+This is implemented by the
 ``roundup.cgi.templating.TemplatingUtils`` class, but it may be extended
 as described below.
 
@@ -1731,13 +2479,18 @@ as described below.
 Method          Description
 =============== ========================================================
 Batch           return a batch object using the supplied list
+url_quote       quote some text as safe for a URL (ie. space, %, ...)
+html_quote      quote some text as safe in HTML (ie. <, >, ...)
+html_calendar   renders an HTML calendar used by the
+                ``_generic.calendar.html`` template (itself invoked by
+                the popupCalendar DateHTMLProperty method
 =============== ========================================================
 
 You may add additional utility methods by writing them in your tracker
-``interfaces.py`` module's ``TemplatingUtils`` class. See `adding a time
-log to your issues`_ for an example. The TemplatingUtils class itself
-will have a single attribute, ``client``, which may be used to access
-the ``client.db`` when you need to perform arbitrary database queries.
+``extensions`` directory and registering them with the templating system
+using ``instance.registerUtil`` (see `adding a time log to your issues`_ for
+an example of this).
+
 
 Batching
 ::::::::
@@ -1772,7 +2525,7 @@ addition, it has several more attributes:
 =============== ========================================================
 Attribute       Description
 =============== ========================================================
-start           indicates the start index of the batch. *Note: unlike
+start           indicates the start index of the batch. *Unlike
                 the argument, is a 1-based index (I know, lame)*
 first           indicates the start index of the batch *as a 0-based
                 index*
@@ -1806,6 +2559,16 @@ An example of batching::
 ... which will produce a table with four columns containing the items of
 the "keyword" class (well, their "name" anyway).
 
+
+Translations
+~~~~~~~~~~~~
+
+Should you wish to enable multiple languages in template content that you
+create you'll need to add new locale files in the tracker home under a
+``locale`` directory. Use the instructions in the ``developer's guide`` to
+create the locale files.
+
+
 Displaying Properties
 ---------------------
 
@@ -1829,12 +2592,12 @@ Index View Specifiers
 An index view specifier (URL fragment) looks like this (whitespace has
 been added for clarity)::
 
-     /issue?status=unread,in-progress,resolved&
-            topic=security,ui&
-            :group=+priority&
-            :sort==activity&
-            :filters=status,topic&
-            :columns=title,status,fixer
+    /issue?status=unread,in-progress,resolved&
+        keyword=security,ui&
+        @group=priority,-status&
+        @sort=-activity&
+        @filters=status,keyword&
+        @columns=title,status,fixer
 
 The index view is determined by two parts of the specifier: the layout
 part and the filter part. The layout part consists of the query
@@ -1852,19 +2615,49 @@ of items with values matching any specified Multilink properties.
 
 The example specifies an index of "issue" items. Only items with a
 "status" of either "unread" or "in-progress" or "resolved" are
-displayed, and only items with "topic" values including both "security"
-and "ui" are displayed. The items are grouped by priority, arranged in
-ascending order; and within groups, sorted by activity, arranged in
-descending order. The filter section shows filters for the "status" and
-"topic" properties, and the table includes columns for the "title",
-"status", and "fixer" properties.
+displayed, and only items with "keyword" values including both "security"
+and "ui" are displayed. The items are grouped by priority arranged in
+ascending order and in descending order by status; and within
+groups, sorted by activity, arranged in descending order. The filter
+section shows filters for the "status" and "keyword" properties, and the
+table includes columns for the "title", "status", and "fixer"
+properties.
+
+============ =============================================================
+Argument     Description
+============ =============================================================
+@sort        sort by prop name, optionally preceeded with '-' to give
+             descending or nothing for ascending sorting. Several
+             properties can be specified delimited with comma.
+             Internally a search-page using several sort properties may
+             use @sort0, @sort1 etc. with option @sortdir0, @sortdir1
+             etc. for the direction of sorting (a non-empty value of
+             sortdir0 specifies reverse order).
+@group       group by prop name, optionally preceeded with '-' or to sort
+             in descending or nothing for ascending order. Several
+             properties can be specified delimited with comma.
+             Internally a search-page using several grouping properties may
+             use @group0, @group1 etc. with option @groupdir0, @groupdir1
+             etc. for the direction of grouping (a non-empty value of
+             groupdir0 specifies reverse order).
+@columns     selects the columns that should be displayed. Default is
+             all.                     
+@filter      indicates which properties are being used in filtering.
+             Default is none.
+propname     selects the values the item properties given by propname must
+             have (very basic search/filter).
+@search_text if supplied, performs a full-text search (message bodies,
+             issue titles, etc)
+============ =============================================================
+
 
 Searching Views
 ---------------
 
-Note: if you add a new column to the ``:columns`` form variable
-      potentials then you will need to add the column to the appropriate
-      `index views`_ template so that it is actually displayed.
+.. note::
+   if you add a new column to the ``@columns`` form variable potentials
+   then you will need to add the column to the appropriate `index views`_
+   template so that it is actually displayed.
 
 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
@@ -1872,8 +2665,8 @@ This is one of the class context views. The template used is typically
 
 - sets up additional filtering, as well as performing indexed text
   searching
-- sets the ``:filter`` variable correctly
-- saves the query off if ``:query_name`` is set.
+- sets the ``@filter`` variable correctly
+- saves the query off if ``@query_name`` is set.
 
 The search page should lay out any fields that you wish to allow the
 user to search on. If your schema contains a large number of properties,
@@ -1883,18 +2676,18 @@ Strings, consider having their value indexed, and then they will be
 searchable using the full text indexed search. This is both faster, and
 more useful for the end user.
 
-The two special form values on search pages which are handled by the
-"search" action are:
+If the search view does specify the "search" ``@action``, then it may also
+provide an additional argument:
 
-:search_text
-  Text with which to perform a search of the text index. Results from
-  that search will be used to limit the results of other filters (using
-  an intersection operation)
-:query_name
-  If supplied, the search parameters (including :search_text) will be
-  saved off as a the query item and registered against the user's
-  queries property. Note that the *classic* template schema has this
-  ability, but the *minimal* template schema does not.
+============ =============================================================
+Argument     Description
+============ =============================================================
+@query_name  if supplied, the index parameters (including @search_text)
+             will be saved off as a the query item and registered against
+             the user's queries property. Note that the *classic* template
+             schema has this ability, but the *minimal* template schema
+             does not.
+============ =============================================================
 
 
 Item Views
@@ -2070,15 +2863,18 @@ templating through the "journal" method of the item*::
 
 *where each journal entry is an HTMLJournalEntry.*
 
+
 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 ``extensions`` directory and registered
+using ``instance.registerAction``.
 
-Adding action methods takes three steps; first you `define the new
-action method`_, then you `register the action method`_ with the cgi
+All the existing Actions are defined in ``roundup.cgi.actions``.
+
+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.
 
@@ -2086,40 +2882,42 @@ 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::
+Create a new action class in your tracker's ``extensions`` directory, for
+example ``myaction.py``::
 
-    def myActionMethod(self):
-        ''' Perform some action. No return value is required.
-        '''
+ from roundup.cgi.actions import Action
+
+ 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
-docstring of that class for details of what it can do.
+The *self.client* attribute is an instance of ``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 register
+it with the following code appended to your ``myaction.py`` file::
 
-    actions = client.Class.actions + (
-        ('myaction', 'myActionMethod'),
-    )
+    def init(instance):
+        instance.registerAction('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
@@ -2131,27 +2929,154 @@ 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).
+
+
+8-bit character set support in Web interface
+--------------------------------------------
+
+The web interface uses UTF-8 default. It may be overridden in both forms
+and a browser cookie.
+
+- In forms, use the ``@charset`` variable.
+- To use the cookie override, have the ``roundup_charset`` cookie set.
+
+In both cases, the value is a valid charset name (eg. ``utf-8`` or
+``kio8-r``).
+
+Inside Roundup, all strings are stored and processed in utf-8.
+Unfortunately, some older browsers do not work properly with
+utf-8-encoded pages (e.g. Netscape Navigator 4 displays wrong
+characters in form fields).  This version allows one to change
+the character set for http transfers.  To do so, you may add
+the following code to your ``page.html`` template::
+
+ <tal:block define="uri string:${request/base}${request/env/PATH_INFO}">
+  <a tal:attributes="href python:request.indexargs_url(uri,
+   {'@charset':'utf-8'})">utf-8</a>
+  <a tal:attributes="href python:request.indexargs_url(uri,
+   {'@charset':'koi8-r'})">koi8-r</a>
+ </tal:block>
+
+(substitute ``koi8-r`` with appropriate charset for your language).
+Charset preference is kept in the browser cookie ``roundup_charset``.
+
+``meta http-equiv`` lines added to the tracker templates in version 0.6.0
+should be changed to include actual character set name::
+
+ <meta http-equiv="Content-Type"
+  tal:attributes="content string:text/html;; charset=${request/client/charset}"
+ />
+
+The charset is also sent in the http header.
+
 
 Examples
 ========
 
 .. contents::
    :local:
-   :depth: 1
+   :depth: 2
+
+
+Changing what's stored in the database
+--------------------------------------
+
+The following examples illustrate ways to change the information stored in
+the database.
 
 
 Adding a new field to the classic schema
-----------------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+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.
+
+You add new fields by editing the ``schema.py`` 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).
+
+1. Modify the ``schema.py``::
+
+    issue = IssueClass(db, "issue", 
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
+                    priority=Link("priority"), status=Link("status"),
+                    due_date=Date())
+
+2. Add an edit field to the ``issue.item.html`` template::
+
+    <tr> 
+     <th>Due Date</th> 
+     <td tal:content="structure context/due_date/field" /> 
+    </tr>
+    
+   If you want to show only the date part of due_date then do this instead::
+   
+    <tr> 
+     <th>Due Date</th> 
+     <td tal:content="structure python:context.due_date.field(format='%Y-%m-%d')" /> 
+    </tr>
+
+3. Add the property to the ``issue.index.html`` page::
+
+    (in the heading row)
+      <th tal:condition="request/show/due_date">Due Date</th>
+    (in the data row)
+      <td tal:condition="request/show/due_date" 
+          tal:content="i/due_date" />
+          
+   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::
+    
+      <td tal:condition="request/show/due_date" 
+          tal:content="python:i.due_date.pretty('%Y-%m-%d')">&nbsp;</td>
+
+4. Add the property to the ``issue.search.html`` page::
+
+     <tr tal:define="name string:due_date">
+       <th i18n:translate="">Due Date:</th>
+       <td metal:use-macro="search_input"></td>
+       <td metal:use-macro="column_input"></td>
+       <td metal:use-macro="sort_input"></td>
+       <td metal:use-macro="group_input"></td>
+     </tr>
+
+5. 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 ``page.html``::
+    
+    columns string:id,activity,due_date,title,creator,status;
+    columns_showall string:id,activity,due_date,title,creator,assignedto,status;
+
+Adding a new constrained field to the classic schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This example shows how to add a new constrained property (i.e. a
 selection of distinct values) to your tracker.
 
 
 Introduction
-~~~~~~~~~~~~
+::::::::::::
 
-To make the classic schema of roundup useful as a TODO tracking system
-for a group of systems administrators, it needed an extra data field per
+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.
 
 This would let sysadmins quickly list all TODOs in their particular area
@@ -2161,12 +3086,12 @@ best).
 
 
 Adding a field to the database
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+::::::::::::::::::::::::::::::
 
 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 ``open()`` function in ``dbinit.py``. Under the
-comment::
+to add some lines to the ``schema.py`` file of your tracker instance.
+Under the comment::
 
     # add any additional database schema configuration here
 
@@ -2187,8 +3112,8 @@ This also means that there can only be one category with a given name.
 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 the ``open()`` function
-in ``dbinit.py`` which set up the "issue" class, and then add a link to
+with the issues. To do that, find the lines 
+in ``schema.py`` which set up the "issue" class, and then add a link to
 the category::
 
     issue = IssueClass(db, "issue", ... ,
@@ -2203,21 +3128,21 @@ is fiddling around so you can actually use the new category.
 
 
 Populating the new category class
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+:::::::::::::::::::::::::::::::::
 
-If you haven't initialised the database with the roundup-admin
+If you haven't initialised the database with the ``roundup-admin``
 "initialise" command, then you can add the following to the tracker
-``dbinit.py`` in the ``init()`` function under the comment::
+``initial_data.py`` under the comment::
 
-    # add any additional database create steps here - but only if you
+    # add any additional database creation steps here - but only if you
     # haven't initialised the database with the admin "initialise" command
 
 Add::
 
      category = db.getclass('category')
-     category.create(name="scipy", order="1")
-     category.create(name="chaco", order="2")
-     category.create(name="weave", order="3")
+     category.create(name="scipy")
+     category.create(name="chaco")
+     category.create(name="weave")
 
 If the database has already been initalised, then you need to use the
 ``roundup-admin`` tool::
@@ -2225,21 +3150,18 @@ If the database has already been initalised, then you need to use the
      % roundup-admin -i <tracker home>
      Roundup <version> ready for input.
      Type "help" for help.
-     roundup> create category name=scipy order=1
+     roundup> create category name=scipy
      1
-     roundup> create category name=chaco order=1
+     roundup> create category name=chaco
      2
-     roundup> create category name=weave order=1
+     roundup> create category name=weave
      3
      roundup> exit...
      There are unsaved changes. Commit them (y/N)? y
 
-TODO: explain why order=1 in each case. Also, does key get set to "name"
-automatically when added via roundup-admin?
-
 
 Setting up security on the new objects
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+::::::::::::::::::::::::::::::::::::::
 
 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
@@ -2247,44 +3169,21 @@ as required, and obviously everyone needs to be able to view the
 categories of issues for it to be useful.
 
 We therefore need to change the security of the category objects. This
-is also done in the ``open()`` function of ``dbinit.py``.
+is also done in ``schema.py``.
 
 There are currently two loops which set up permissions and then assign
 them to various roles. Simply add the new "category" to both lists::
 
-    # new permissions for this schema
-    for cl in 'issue', 'file', 'msg', 'user', 'category':
-        db.security.addPermission(name="Edit", klass=cl,
-            description="User is allowed to edit "+cl)
-        db.security.addPermission(name="View", klass=cl,
-            description="User is allowed to access "+cl)
-
     # Assign the access and edit permissions for issue, file and message
     # to regular users now
     for cl in 'issue', 'file', 'msg', 'category':
         p = db.security.getPermission('View', cl)
-        db.security.addPermissionToRole('User', p)
-        p = db.security.getPermission('Edit', cl)
-        db.security.addPermissionToRole('User', p)
-
-So you are in effect doing the following (with 'cl' substituted by its
-value)::
-
-    db.security.addPermission(name="Edit", klass='category',
-        description="User is allowed to edit "+'category')
-    db.security.addPermission(name="View", klass='category',
-        description="User is allowed to access "+'category')
-
-which is creating two permission types; that of editing and viewing
-"category" objects respectively. Then the following lines assign those
-new permissions to the "User" role, so that normal users can view and
-edit "category" objects::
-
-    p = db.security.getPermission('View', 'category')
-    db.security.addPermissionToRole('User', p)
+        db.security.addPermissionToRole('User', 'View', cl)
+        db.security.addPermissionToRole('User', 'Edit', cl)
+        db.security.addPermissionToRole('User', 'Create', cl)
 
-    p = db.security.getPermission('Edit', 'category')
-    db.security.addPermissionToRole('User', p)
+These lines assign the "View" and "Edit" Permissions to the "User" role,
+so that normal users can view and edit "category" objects.
 
 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
@@ -2292,18 +3191,18 @@ interface stuff.
 
 
 Changing the web left hand frame
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+::::::::::::::::::::::::::::::::
 
 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 ``html/page``, which is what we are going to be editing next.
+is ``html/page.html``, which is what we are going to be editing next.
 
 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. Under the end of the classblock for issue is where we
-are going to add the category code::
+classblock is met. We are going to add the category code at the end of
+the classblock for the *issue* class::
 
   <p class="classblock"
      tal:condition="python:request.user.hasPermission('View', 'category')">
@@ -2328,13 +3227,13 @@ Note that if you have permission to *view* but not to *edit* 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
+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.
 
 
 Setting up a page to edit categories
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+::::::::::::::::::::::::::::::::::::
 
 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
@@ -2345,7 +3244,7 @@ translates into Roundup looking for a file called ``category.item.html``
 in the ``html`` tracker directory. This is the file that we are going to
 write now.
 
-First we add an info tag in a comment which doesn't affect the outcome
+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::
@@ -2385,10 +3284,10 @@ happening::
      <tr><th class="header" colspan="2">Category</th></tr>
 
 Next, we need the field into which the user is going to enter the new
-category. The "context.name.field(size=60)" bit tells Roundup to
+category. The ``context.name.field(size=60)`` 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 (which is
-"category"). The upshot of this is that when the user types something in
+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::
 
     <tr>
@@ -2454,7 +3353,7 @@ maybe a few extra to get the formatting correct).
 
 
 Adding the category to the issue
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+::::::::::::::::::::::::::::::::
 
 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
@@ -2462,13 +3361,15 @@ the ``html/category.item.html`` file was used to define how to add a new
 category, the ``html/issue.item.html`` is used to define how a new issue
 is created.
 
-Just like ``category.issue.html`` this file defines a form which has a
+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>Category</th>
-   <td><span tal:replace="structure context/category/field" />
-       <span tal:replace="structure db/category/classhelp" />
+   <td>
+    <span tal:replace="structure context/category/field" />
+    <span tal:replace="structure python:db.category.classhelp('name',
+                property='category', width='200')" />
    </td>
 
 First, we define a nice header so that the user knows what the next
@@ -2482,21 +3383,21 @@ which contains the list of currently known categories.
 
 
 Searching on categories
-~~~~~~~~~~~~~~~~~~~~~~~
+:::::::::::::::::::::::
 
-We can add categories, and create issues with categories. The next
+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".
 
-If you look for "Search Issues" in the 'html/page.html' file, you will
+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
 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.
 
-If you look at this file it should be starting to seem familiar, although it
+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::
 
@@ -2510,16 +3411,16 @@ like within that form::
     <td metal:use-macro="group_input"></td>
   </tr>
 
-The definitions in the <tr> opening tag are used by the macros:
+The definitions in the ``<tr>`` opening tag are used by the macros:
 
-- 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.
+- ``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 grouped on.
 
 The category search code above would expand to the following::
 
@@ -2535,12 +3436,12 @@ The category search code above would expand to the following::
       </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>
+    <td><input type="radio" name=":sort0" value="category"></td>
+    <td><input type="radio" name=":group0" value="category"></td>
   </tr>
 
 Adding category to the default view
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+:::::::::::::::::::::::::::::::::::
 
 We can now add categories, add issues with categories, and search for
 issues based on categories. This is everything that we need to do;
@@ -2574,418 +3475,60 @@ 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.
 
 Finally we have to edit ``html/page.html`` again. This time, we need to
-tell it that when the user clicks on "Unasigned Issues" or "All Issues",
+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 ``:columns=`` one which
 tells roundup which fields of the issue to display. Simply add
 "category" to that list and it all should work.
 
+Adding a time log to your issues
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Adding in state transition control
-----------------------------------
+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.
 
-Sometimes tracker admins want to control the states that users may move
-issues to. You can do this by following these steps:
+1. Add a new class to your tracker ``schema.py``::
 
-1. make "status" a required variable. This is achieved by adding the
-   following to the top of the form in the ``issue.item.html``
-   template::
+    # storage for time logging
+    timelog = Class(db, "timelog", period=Interval())
 
-     <input type="hidden" name="@required" value="status">
+   Note that we automatically get the date of the time log entry
+   creation through the standard property "creation".
 
-   this will force users to select a status.
+   You will need to grant "Creation" permission to the users who are
+   allowed to add timelog entries. You may do this with::
 
-2. add a Multilink property to the status class::
+    db.security.addPermissionToRole('User', 'Create', 'timelog')
+    db.security.addPermissionToRole('User', 'View', 'timelog')
 
-     stat = Class(db, "status", ... , transitions=Multilink('status'),
-                  ...)
+   If users are also able to *edit* timelog entries, then also include::
 
-   and then edit the statuses already created, either:
+    db.security.addPermissionToRole('User', 'Edit', 'timelog')
 
-   a. through the web using the class list -> status class editor, or
-   b. using the roundup-admin "set" command.
+2. Link to the new class from your issue class (again, in
+   ``schema.py``)::
 
-3. add an auditor module ``checktransition.py`` in your tracker's
-   ``detectors`` directory, for example::
-
-     def checktransition(db, cl, nodeid, newvalues):
-         ''' Check that the desired transition is valid for the "status"
-             property.
-         '''
-         if not newvalues.has_key('status'):
-             return
-         current = cl.get(nodeid, 'status')
-         new = newvalues['status']
-         if new == current:
-             return
-         ok = db.status.get(current, 'transitions')
-         if new not in ok:
-             raise ValueError, 'Status not allowed to move from "%s" to "%s"'%(
-                 db.status.get(current, 'name'), db.status.get(new, 'name'))
-
-     def init(db):
-         db.issue.audit('set', checktransition)
-
-4. in the ``issue.item.html`` template, change the status editing bit
-   from::
-
-    <th>Status</th>
-    <td tal:content="structure context/status/menu">status</td>
-
-   to::
-
-    <th>Status</th>
-    <td>
-     <select tal:condition="context/id" name="status">
-      <tal:block tal:define="ok context/status/transitions"
-                 tal:repeat="state db/status/list">
-       <option tal:condition="python:state.id in ok"
-               tal:attributes="
-                    value state/id;
-                    selected python:state.id == context.status.id"
-               tal:content="state/name"></option>
-      </tal:block>
-     </select>
-     <tal:block tal:condition="not:context/id"
-                tal:replace="structure context/status/menu" />
-    </td>
-
-   which displays only the allowed status to transition to.
-
-
-Displaying only message summaries in the issue display
-------------------------------------------------------
-
-Alter the issue.item template section for messages to::
-
- <table class="messages" tal:condition="context/messages">
-  <tr><th colspan="5" class="header">Messages</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 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>
-   </td>
-  </tr>
- </table>
-
-Restricting the list of users that are assignable to a task
------------------------------------------------------------
-
-1. In your tracker's "dbinit.py", create a new Role, say "Developer"::
-
-     db.security.addRole(name='Developer', description='A developer')
-
-2. Just after that, create a new Permission, say "Fixer", specific to
-   "issue"::
-
-     p = db.security.addPermission(name='Fixer', klass='issue',
-         description='User is allowed to be assigned to fix issues')
-
-3. Then assign the new Permission to your "Developer" Role::
-
-     db.security.addPermissionToRole('Developer', p)
-
-4. In the issue item edit page ("html/issue.item.html" in your tracker
-   directory), use the new Permission in restricting the "assignedto"
-   list::
-
-    <select name="assignedto">
-     <option value="-1">- no selection -</option>
-     <tal:block tal:repeat="user db/user/list">
-     <option tal:condition="python:user.hasPermission(
-                                'Fixer', context._classname)"
-             tal:attributes="
-                value user/id;
-                selected python:user.id == context.assignedto"
-             tal:content="user/realname"></option>
-     </tal:block>
-    </select>
-
-For extra security, you may wish to setup an auditor to enforce the
-Permission requirement (install this as "assignedtoFixer.py" in your
-tracker "detectors" directory)::
-
-  def assignedtoMustBeFixer(db, cl, nodeid, newvalues):
-      ''' Ensure the assignedto value in newvalues is a used with the
-          Fixer Permission
-      '''
-      if not newvalues.has_key('assignedto'):
-          # don't care
-          return
-  
-      # get the userid
-      userid = newvalues['assignedto']
-      if not db.security.hasPermission('Fixer', userid, cl.classname):
-          raise ValueError, 'You do not have permission to edit %s'%cl.classname
-
-  def init(db):
-      db.issue.audit('set', assignedtoMustBeFixer)
-      db.issue.audit('create', assignedtoMustBeFixer)
-
-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.
-
-
-Setting up a "wizard" (or "druid") for controlled adding of issues
-------------------------------------------------------------------
-
-1. 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::
-
-    <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">
-
-      <strong>Category:</strong>
-      <tal:block tal:replace="structure context/category/menu" />
-      <input type="submit" value="Continue">
-    </form>
-
-   The next page has the usual issue entry information, with the
-   addition of the following form fragments::
-
-    <form method="POST" onSubmit="return submit_once()"
-          enctype="multipart/form-data"
-          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="category" tal:attributes="value cat">
-       .
-       .
-       .
-    </form>
-
-   Note that later in the form, I test the value of "cat" include form
-   elements that are appropriate. For example::
-
-    <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()">
-     <tr>
-      <th>Operating System</th>
-      <td tal:content="structure context/os/field"></td>
-     </tr>
-     <tr>
-      <th>Web Browser</th>
-      <td tal:content="structure context/browser/field"></td>
-     </tr>
-    </tal:block>
-
-   ... the above section will only be displayed if the category is one
-   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::
-
-    actions = client.Client.actions + (
-        ('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).
-
-
-Using an external password validation source
---------------------------------------------
-
-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 ``name:password`` where the
-password is encrypted using the standard UN*X ``crypt()`` function (see
-the ``crypt`` module in your Python distribution). An example entry
-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::
-
-    def verifyPassword(self, userid, password):
-        # get the user's username
-        username = self.db.user.get(userid, 'username')
-
-        # the passwords are stored in the "passwd.txt" file in the
-        # tracker home
-        file = os.path.join(self.db.config.TRACKER_HOME, 'passwd.txt')
-
-        # 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
-
-What this does is look through the file, line by line, looking for a
-name that matches.
-
-We also remove the redundant password fields from the ``user.item``
-template.
-
-
-Adding a "vacation" flag to users for stopping nosy messages
-------------------------------------------------------------
-
-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.
-
-1. add a "vacation" flag to your users::
-
-         user = Class(db, "user",
-                    username=String(),   password=Password(),
-                    address=String(),    realname=String(),
-                    phone=String(),      organisation=String(),
-                    alternate_addresses=String(),
-                    roles=String(), queries=Multilink("query"),
-                    vacation=Boolean())
-
-2. So that users may edit the vacation flags, add something like the
-   following to your ``user.item`` template::
-
-     <tr>
-      <th>On Vacation</th> 
-      <td tal:content="structure context/vacation/field">vacation</td> 
-     </tr> 
-
-3. edit your detector ``nosyreactor.py`` so that the ``nosyreaction()``
-   consists of::
-
-    def nosyreaction(db, cl, nodeid, oldvalues):
-        # send a copy of all new messages to the nosy list
-        for msgid in determineNewMessages(cl, nodeid, oldvalues):
-            try:
-                users = db.user
-                messages = db.msg
-
-                # figure the recipient ids
-                sendto = []
-                r = {}
-                recipients = messages.get(msgid, 'recipients')
-                for recipid in messages.get(msgid, 'recipients'):
-                    r[recipid] = 1
-
-                # figure the author's id, and indicate they've received
-                # the message
-                authid = messages.get(msgid, 'author')
-
-                # possibly send the message to the author, as long as
-                # they aren't anonymous
-                if (db.config.MESSAGES_TO_AUTHOR == 'yes' and
-                        users.get(authid, 'username') != 'anonymous'):
-                    sendto.append(authid)
-                r[authid] = 1
-
-                # now figure the nosy people who weren't recipients
-                nosy = cl.get(nodeid, 'nosy')
-                for nosyid in nosy:
-                    # Don't send nosy mail to the anonymous user (that
-                    # user shouldn't appear in the nosy list, but just
-                    # in case they do...)
-                    if users.get(nosyid, 'username') == 'anonymous':
-                        continue
-                    # make sure they haven't seen the message already
-                    if not r.has_key(nosyid):
-                        # send it to them
-                        sendto.append(nosyid)
-                        recipients.append(nosyid)
-
-                # generate a change note
-                if oldvalues:
-                    note = cl.generateChangeNote(nodeid, oldvalues)
-                else:
-                    note = cl.generateCreateNote(nodeid)
-
-                # we have new recipients
-                if sendto:
-                    # filter out the people on vacation
-                    sendto = [i for i in sendto 
-                              if not users.get(i, 'vacation', 0)]
-
-                    # map userids to addresses
-                    sendto = [users.get(i, 'address') for i in sendto]
-
-                    # update the message's recipients list
-                    messages.set(msgid, recipients=recipients)
-
-                    # send the message
-                    cl.send_message(nodeid, msgid, note, sendto)
-            except roundupdb.MessageSendError, message:
-                raise roundupdb.DetectorError, message
-
-   Note that this is the standard nosy reaction code, with the small
-   addition of::
-
-    # filter out the people on vacation
-    sendto = [i for i in sendto if not users.get(i, 'vacation', 0)]
-
-   which filters out the users that have the vacation flag set to true.
-
-
-Adding a time log to your issues
---------------------------------
-
-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.
-
-1. Add a new class to your tracker ``dbinit.py``::
-
-    # storage for time logging
-    timelog = Class(db, "timelog", period=Interval())
-
-   Note that we automatically get the date of the time log entry
-   creation through the standard property "creation".
-
-2. Link to the new class from your issue class (again, in
-   ``dbinit.py``)::
-
-    issue = IssueClass(db, "issue", 
-                    assignedto=Link("user"), topic=Multilink("keyword"),
-                    priority=Link("priority"), status=Link("status"),
-                    times=Multilink("timelog"))
+    issue = IssueClass(db, "issue", 
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
+                    priority=Link("priority"), status=Link("status"),
+                    times=Multilink("timelog"))
 
    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. This is a special field
-   because unlike the other fields in the issue.item template, it
+   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
+   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::
+   field to capture a new timelog item's period::
 
     <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) 
+      (enter as '3y 1m 4d 2:40:02' or parts thereof) 
      </td> 
     </tr> 
          
@@ -2998,31 +3541,43 @@ be able to give a summary of the total time spent on a particular issue.
    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.
+   
+   The full entry will now look like this::
+   
+    <tr> 
+     <th>Time Log</th> 
+     <td colspan=3><input type="text" name="timelog-1@period" /> 
+      (enter as '3y 1m 4d 2:40:02' or parts thereof)
+      <input type="hidden" name="@link@times" value="timelog-1" /> 
+     </td> 
+    </tr> 
+   
 
-4. We want to display a total of the time log times that have been
+4. 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 method to the
-   TemplatingUtils class in our tracker ``interfaces.py`` module::
+   perform such calculations. We do this by adding a module ``timespent.py``
+   to the ``extensions`` directory in our tracker. The contents of this
+   file is as follows::
 
-    class TemplatingUtils:
-        ''' Methods implemented on this class will be available to HTML
-            templates through the 'utils' variable.
+    from roundup import date
+
+    def totalTimeSpent(times):
+        ''' Call me with a list of timelog items (which have an
+            Interval "period" property)
         '''
-        def totalTimeSpent(self, times):
-            ''' Call me with a list of timelog items (which have an
-                Interval "period" property)
-            '''
-            total = Interval('0d')
-            for time in times:
-                total += time.period._value
-            return total
+        total = date.Interval('0d')
+        for time in times:
+            total += time.period._value
+        return total
 
-   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.
+    def init(instance):
+        instance.registerUtil('totalTimeSpent', totalTimeSpent)
 
-5. Display the time log for an issue::
+   We will now be able to access the ``totalTimeSpent`` function via the
+   ``utils`` variable in our templates, as shown in the next step.
+
+5. Display the timelog for an issue::
 
      <table class="otherinfo" tal:condition="context/times">
       <tr><th colspan="3" class="header">Time Log
@@ -3043,13 +3598,189 @@ be able to give a summary of the total time spent on a particular issue.
    displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours
    and 40 minutes).
 
-8. If you're using a persistent web server - roundup-server or
-   mod_python for example - then you'll need to restart that to pick up
+6. If you're using a persistent web server - ``roundup-server`` or
+   ``mod_python`` 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.
 
+An extension of this modification attaches the timelog entries to any
+change message entered at the time of the timelog entry:
+
+A. Add a link to the timelog to the msg class in ``schema.py``:
+
+    msg = FileClass(db, "msg",
+                    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"))
+
+B. 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 ``issue.item.html`` in the same section that
+   handles the timelog entry.
+   
+   It looks like this after this addition::
+
+    <tr> 
+     <th>Time Log</th> 
+     <td colspan=3><input type="text" name="timelog-1@period" /> 
+      (enter as '3y 1m 4d 2:40:02' or parts thereof)
+      <input type="hidden" name="@link@times" value="timelog-1" />
+      <input type="hidden" name="msg-1@link@times" value="timelog-1" /> 
+     </td> 
+    </tr> 
+   The "times" property of the message will have the new id added to it.
+
+C. Add the timelog listing from step 5. to the ``msg.item.html`` 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.
+   
+   I placed it after the Date entry like this::
+   
+    <tr>
+     <th i18n:translate="">Date:</th>
+     <td tal:content="context/date"></td>
+    </tr>
+    </table>
+    
+    <table class="otherinfo" tal:condition="context/times">
+     <tr><th colspan="3" class="header">Time Log</th></tr>
+     <tr><th>Date</th><th>Period</th><th>Logged By</th></tr>
+     <tr tal:repeat="time context/times">
+      <td tal:content="time/creation"></td>
+      <td tal:content="time/period"></td>
+      <td tal:content="time/creator"></td>
+     </tr>
+    </table>
+    
+    <table class="messages">
+
+
+Tracking different types of issues
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+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.
+
+1. 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.
+
+2. Add the new issue class to your tracker's ``schema.py``. Just after the
+   "issue" class definition, add::
+
+    # list our systems
+    system = Class(db, "system", name=String(), order=Number())
+    system.setkey("name")
+
+    # store issues related to those systems
+    support = IssueClass(db, "support", 
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
+                    status=Link("status"), deadline=Date(),
+                    affects=Multilink("system"))
+
+3. Copy the existing ``issue.*`` (item, search and index) templates in the
+   tracker's ``html`` to ``support.*``. Edit them so they use the properties
+   defined in the ``support`` class. Be sure to check for hidden form
+   variables like "required" to make sure they have the correct set of
+   required properties.
+
+4. Edit the modules in the ``detectors``, adding lines to their ``init``
+   functions where appropriate. Look for ``audit`` and ``react`` registrations
+   on the ``issue`` class, and duplicate them for ``support``.
+
+5. Create a new sidebar box for the new support class. Duplicate the
+   existing issues one, changing the ``issue`` class name to ``support``.
+
+6. Re-start your tracker and start using the new ``support`` class.
+
+
+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::
+
+    db.security.addPermissionToRole('SysAdmin', 'View', 'support')
+    db.security.addPermissionToRole('SysAdmin', 'Create', 'support')
+    db.security.addPermissionToRole('SysAdmin', 'Edit', 'support')
+
+You would then (as an "admin" user) edit the details of the appropriate
+users, and add "SysAdmin" to their Roles list.
+
+Alternatively, you might want to change the Edit/View permissions granted
+for the ``issue`` 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.
+
+
+Using External User Databases
+-----------------------------
+
+Using an external password validation source
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. note:: You will need to either have an "admin" user in your external
+          password source *or* have one of your regular users have
+          the Admin Role assigned. If you need to assign the Role *after*
+          making the changes below, you may use the ``roundup-admin``
+          program to edit a user's details.
+
+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 ``name:password`` where the
+password is encrypted using the standard UN*X ``crypt()`` function (see
+the ``crypt`` module in your Python distribution). An example entry
+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
+need to override the standard ``verifyPassword`` method defined in
+``roundup.cgi.actions.LoginAction`` and register the new class. The
+following is added as ``externalpassword.py`` in the tracker ``extensions``
+directory::
+
+    import os, crypt
+    from roundup.cgi.actions import LoginAction    
+
+    class ExternalPasswordLoginAction(LoginAction):
+        def verifyPassword(self, userid, password):
+            '''Look through the file, line by line, looking for a
+            name that matches.
+            '''
+            # get the user's username
+            username = self.db.user.get(userid, 'username')
+
+            # the passwords are stored in the "passwd.txt" file in the
+            # tracker home
+            file = os.path.join(self.db.config.TRACKER_HOME, 'passwd.txt')
+
+            # 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
+
+    def init(instance):
+        instance.registerAction('login', ExternalPasswordLoginAction)
+
+You should also remove the redundant password fields from the ``user.item``
+template.
+
+
 Using a UN*X passwd file as the user database
----------------------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 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
@@ -3064,7 +3795,7 @@ which the users are removed when they no longer have access to a system.
 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, `using an external password
-validation source`_. We keep the users lists in sync using a fairly
+validation source`_. 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:
 
@@ -3079,7 +3810,7 @@ immediate access is needed. In short, it:
 
 The retiring and updating are simple operations, requiring only a call
 to ``retire()`` or ``set()``. The creation operation requires more
-information though - the user's email address and their roundup Roles.
+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
@@ -3098,8 +3829,8 @@ tracker we're to work on::
 
 Next we read in the *passwd* file from the tracker home::
 
-    # read in the users
-    file = os.path.join(tracker_home, 'users.passwd')
+    # read in the users from the "passwd.txt" file
+    file = os.path.join(tracker_home, 'passwd.txt')
     users = [x.strip().split(':') for x in open(file).readlines()]
 
 Handle special users (those to ignore in the file, and those who don't
@@ -3185,7 +3916,7 @@ 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
@@ -3195,10 +3926,11 @@ 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::
+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
+``verifyPassword`` on the ``LoginAction`` class in your tracker's
+``extensions`` directory (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
@@ -3220,102 +3952,260 @@ So you could reimplement this as something like::
         # now verify the password supplied against the LDAP store
 
 
-Enabling display of either message summaries or the entire messages
--------------------------------------------------------------------
+Changes to Tracker Behaviour
+----------------------------
 
-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::
+Preventing SPAM
+~~~~~~~~~~~~~~~
 
- <table class="messages" tal:condition="context/messages">
-  <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>
-       </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 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>
-    </td>
-   </tr>
-  </tal:block>
+The following detector code may be installed in your tracker's
+``detectors`` 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 ``detectors/anti_spam.py`` in your tracker::
 
-  <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>
-       </th>
-   </tr>
-   <tal:block tal:repeat="msg context/messages">
-    <tr>
-     <th tal:content="msg/author">author</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>
-    </tr>
-    <tr><td colspan="3" tal:content="msg/content"></td></tr>
-   </tal:block>
-  </tal:block>
- </table>
+    from roundup.exceptions import Reject
+
+    def reject_html(db, cl, nodeid, newvalues):
+        if newvalues['type'] == 'text/html':
+        raise Reject, 'not allowed'
+
+    def reject_manylinks(db, cl, nodeid, newvalues):
+        content = newvalues['content']
+        if content.count('http://') > 2:
+        raise Reject, 'not allowed'
+
+    def init(db):
+        db.file.audit('create', reject_html)
+        db.msg.audit('create', reject_manylinks)
+
+You may also wish to block image attachments if your tracker does not
+need that ability::
+
+    if newvalues['type'].startswith('image/'):
+        raise Reject, 'not allowed'
+
+
+Stop "nosy" messages going to people on vacation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+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.
+
+1. add a "vacation" flag to your users::
+
+         user = Class(db, "user",
+                    username=String(),   password=Password(),
+                    address=String(),    realname=String(),
+                    phone=String(),      organisation=String(),
+                    alternate_addresses=String(),
+                    roles=String(), queries=Multilink("query"),
+                    vacation=Boolean())
+
+2. So that users may edit the vacation flags, add something like the
+   following to your ``user.item`` template::
+
+     <tr>
+      <th>On Vacation</th> 
+      <td tal:content="structure context/vacation/field">vacation</td> 
+     </tr> 
+
+3. edit your detector ``nosyreactor.py`` so that the ``nosyreaction()``
+   consists of::
+
+    def nosyreaction(db, cl, nodeid, oldvalues):
+        users = db.user
+        messages = db.msg
+        # send a copy of all new messages to the nosy list
+        for msgid in determineNewMessages(cl, nodeid, oldvalues):
+            try:
+                # figure the recipient ids
+                sendto = []
+                seen_message = {}
+                recipients = messages.get(msgid, 'recipients')
+                for recipid in messages.get(msgid, 'recipients'):
+                    seen_message[recipid] = 1
+
+                # figure the author's id, and indicate they've received
+                # the message
+                authid = messages.get(msgid, 'author')
+
+                # possibly send the message to the author, as long as
+                # they aren't anonymous
+                if (db.config.MESSAGES_TO_AUTHOR == 'yes' and
+                        users.get(authid, 'username') != 'anonymous'):
+                    sendto.append(authid)
+                seen_message[authid] = 1
+
+                # now figure the nosy people who weren't recipients
+                nosy = cl.get(nodeid, 'nosy')
+                for nosyid in nosy:
+                    # Don't send nosy mail to the anonymous user (that
+                    # user shouldn't appear in the nosy list, but just
+                    # in case they do...)
+                    if users.get(nosyid, 'username') == 'anonymous':
+                        continue
+                    # make sure they haven't seen the message already
+                    if not seen_message.has_key(nosyid):
+                        # send it to them
+                        sendto.append(nosyid)
+                        recipients.append(nosyid)
+
+                # generate a change note
+                if oldvalues:
+                    note = cl.generateChangeNote(nodeid, oldvalues)
+                else:
+                    note = cl.generateCreateNote(nodeid)
+
+                # we have new recipients
+                if sendto:
+                    # filter out the people on vacation
+                    sendto = [i for i in sendto 
+                              if not users.get(i, 'vacation', 0)]
+
+                    # map userids to addresses
+                    sendto = [users.get(i, 'address') for i in sendto]
+
+                    # update the message's recipients list
+                    messages.set(msgid, recipients=recipients)
+
+                    # send the message
+                    cl.send_message(nodeid, msgid, note, sendto)
+            except roundupdb.MessageSendError, message:
+                raise roundupdb.DetectorError, message
+
+   Note that this is the standard nosy reaction code, with the small
+   addition of::
+
+    # filter out the people on vacation
+    sendto = [i for i in sendto if not users.get(i, 'vacation', 0)]
+
+   which filters out the users that have the vacation flag set to true.
+
+Adding in state transition control
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes tracker admins want to control the states to which users may
+move issues. You can do this by following these steps:
+
+1. make "status" a required variable. This is achieved by adding the
+   following to the top of the form in the ``issue.item.html``
+   template::
+
+     <input type="hidden" name="@required" value="status">
+
+   This will force users to select a status.
+
+2. add a Multilink property to the status class::
+
+     stat = Class(db, "status", ... , transitions=Multilink('status'),
+                  ...)
+
+   and then edit the statuses already created, either:
+
+   a. through the web using the class list -> status class editor, or
+   b. using the ``roundup-admin`` "set" command.
+
+3. add an auditor module ``checktransition.py`` in your tracker's
+   ``detectors`` directory, for example::
+
+     def checktransition(db, cl, nodeid, newvalues):
+         ''' Check that the desired transition is valid for the "status"
+             property.
+         '''
+         if not newvalues.has_key('status'):
+             return
+         current = cl.get(nodeid, 'status')
+         new = newvalues['status']
+         if new == current:
+             return
+         ok = db.status.get(current, 'transitions')
+         if new not in ok:
+             raise ValueError, 'Status not allowed to move from "%s" to "%s"'%(
+                 db.status.get(current, 'name'), db.status.get(new, 'name'))
+
+     def init(db):
+         db.issue.audit('set', checktransition)
+
+4. in the ``issue.item.html`` template, change the status editing bit
+   from::
+
+    <th>Status</th>
+    <td tal:content="structure context/status/menu">status</td>
+
+   to::
+
+    <th>Status</th>
+    <td>
+     <select tal:condition="context/id" name="status">
+      <tal:block tal:define="ok context/status/transitions"
+                 tal:repeat="state db/status/list">
+       <option tal:condition="python:state.id in ok"
+               tal:attributes="
+                    value state/id;
+                    selected python:state.id == context.status.id"
+               tal:content="state/name"></option>
+      </tal:block>
+     </select>
+     <tal:block tal:condition="not:context/id"
+                tal:replace="structure context/status/menu" />
+    </td>
+
+   which displays only the allowed status to transition to.
 
 
 Blocking issues that depend on other issues
--------------------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 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:
 
-1. Create a new property on the issue Class,
-   ``blockers=Multilink("issue")``. Edit your tracker's dbinit.py file.
-   Where the "issue" class is defined, something like::
+1. Create a new property on the ``issue`` class:
+   ``blockers=Multilink("issue")``. To do this, edit the definition of
+   this class in your tracker's ``schema.py`` file. Change this::
 
     issue = IssueClass(db, "issue", 
-                    assignedto=Link("user"), topic=Multilink("keyword"),
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
                     priority=Link("priority"), status=Link("status"))
 
-   add the blockers entry like so::
+   to this, adding the blockers entry::
 
     issue = IssueClass(db, "issue", 
                     blockers=Multilink("issue"),
-                    assignedto=Link("user"), topic=Multilink("keyword"),
+                    assignedto=Link("user"), keyword=Multilink("keyword"),
                     priority=Link("priority"), status=Link("status"))
 
-2. Add the new "blockers" property to the issue.item edit page, using
-   something like::
+2. Add the new ``blockers`` property to the ``issue.item.html`` edit
+   page, using something like::
 
     <th>Waiting On</th>
     <td>
      <span tal:replace="structure python:context.blockers.field(showid=1,
                                   size=20)" />
-     <span tal:replace="structure python:db.issue.classhelp('id,title')" />
+     <span tal:replace="structure python:db.issue.classhelp('id,title',
+                                  property='blockers')" />
      <span tal:condition="context/blockers"
            tal:repeat="blk context/blockers">
       <br>View: <a tal:attributes="href string:issue${blk/id}"
                    tal:content="blk/id"></a>
      </span>
+    </td>
 
    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.
 
-3. Create a new detector module (attached) which enforces the rules:
+3. Create a new detector module (see below) which enforces the rules:
 
    - issues may not be resolved if they have blockers
    - when a blocker is resolved, it's removed from issues it blocks
 
    The contents of the detector should be something like this::
 
+
     def blockresolution(db, cl, nodeid, newvalues):
         ''' If the issue has blockers, don't allow it to be resolved.
         '''
@@ -3346,18 +4236,21 @@ resolved. To achieve this:
         if newvalues['status'] == resolved_id:
             raise ValueError, "This issue can't be resolved until %s resolved."%s
 
-    def resolveblockers(db, cl, nodeid, newvalues):
+
+    def resolveblockers(db, cl, nodeid, oldvalues):
         ''' When we resolve an issue that's a blocker, remove it from the
             blockers list of the issue(s) it blocks.
         '''
-        if not newvalues.has_key('status'):
+        newstatus = cl.get(nodeid,'status')
+
+        # no change?
+        if oldvalues.get('status', None) == newstatus:
             return
 
-        # get the resolved state ID
         resolved_id = db.status.lookup('resolved')
 
         # interesting?
-        if newvalues['status'] != resolved_id:
+        if newstatus != resolved_id:
             return
 
         # yes - find all the blocked issues, if any, and remove me from
@@ -3369,7 +4262,6 @@ resolved. To achieve this:
                 blockers.remove(nodeid)
                 cl.set(issueid, blockers=blockers)
 
-
     def init(db):
         # might, in an obscure situation, happen in a create
         db.issue.audit('create', blockresolution)
@@ -3387,12 +4279,36 @@ resolved. To achieve this:
    example, the existing "Show All" link in the "page" template (in the
    tracker's "html" directory) looks like this::
 
-     <a href="issue?:sort=-activity&:group=priority&:filter=status&:columns=id,activity,title,creator,assignedto,status&status=-1,1,2,3,4,5,6,7">Show All</a><br>
+    <a href="#"
+       tal:attributes="href python:request.indexargs_url('issue', {
+      '@sort': '-activity',
+      '@group': 'priority',
+      '@filter': 'status',
+      '@columns': columns_showall,
+      '@search_text': '',
+      'status': status_notresolved,
+      '@dispname': i18n.gettext('Show All'),
+     })"
+       i18n:translate="">Show All</a><br>
 
    modify it to add the "blockers" info to the URL (note, both the
-   ":filter" *and* "blockers" values must be specified)::
-
-     <a href="issue?:sort=-activity&:group=priority&:filter=status,blockers&blockers=-1&:columns=id,activity,title,creator,assignedto,status&status=-1,1,2,3,4,5,6,7">Show All</a><br>
+   "@filter" *and* "blockers" values must be specified)::
+
+    <a href="#"
+       tal:attributes="href python:request.indexargs_url('issue', {
+      '@sort': '-activity',
+      '@group': 'priority',
+      '@filter': 'status,blockers',
+      '@columns': columns_showall,
+      '@search_text': '',
+      'status': status_notresolved,
+      'blockers': '-1',
+      '@dispname': i18n.gettext('Show All'),
+     })"
+       i18n:translate="">Show All</a><br>
+
+   The above examples are line-wrapped on the trailing & and should
+   be unwrapped.
 
 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
@@ -3400,33 +4316,32 @@ 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
----------------------------------------------
+Add users to the nosy list based on the keyword
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-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.
+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.
 
-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.
+Below, we'll show that this change can be done with minimal
+understanding of the Roundup system, using only 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.
+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.
 
-Adding the nosy topic list
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+Adding the nosy keyword 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::
+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
+``Multilink`` of ``keyword`` seems to fullfill this. As such, all that has to
+be done is to add a new field to the definition of ``user`` within the file
+``schema.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(),
@@ -3437,22 +4352,22 @@ the updated definition of user will be::
                     timezone=String(),
                     nosy_keywords=Multilink('keyword'))
  
-Changing the user view to allow changing the nosy topic list
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Changing the user view to allow changing the nosy keyword list
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 
-We want any user to be able to change the list of topics for which
+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 ``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
+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 ``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>
+  <th>Nosy Keywords</th>
   <td>
   <span tal:replace="structure context/nosy_keywords/field" />
   <span tal:replace="structure python:db.keyword.classhelp(property='nosy_keywords')" />
@@ -3461,41 +4376,41 @@ E-mail addresses' in the classic template)::
   
 
 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.
+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 
 ``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::
+``nosyreaction`` function actually sends the e-mail. 
+We don't need this. Therefore, 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
+After that, we rename the ``updatenosy`` function to ``update_kw_nosy``.
+The first two blocks of code in that function relate to setting
 ``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
+code, which handled adding the assignedto user(s) to the nosy list in
+``updatenosy``, 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::
+new keywords, than looping over all users,
+and assign the user to the nosy list when the keyword occurs in the user's
+``nosy_keywords``. The next part in ``updatenosy`` -- 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 ``newvalues``, can stay as is.
+This results in the following function::
 
     def update_kw_nosy(db, cl, nodeid, newvalues):
-        '''Update the nosy list for changes to the topics
+        '''Update the nosy list for changes to the keywords
         '''
         # nodeid will be None if this is a new node
         current = {}
@@ -3520,17 +4435,17 @@ This brings the following function::
                 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:
+        # add users with keyword in nosy_keywords to the nosy list
+        if newvalues.has_key('keyword') and newvalues['keyword'] is not None:
+            keyword_ids = newvalues['keyword']
+            for keyword in keyword_ids:
                 # loop over all users,
-                # and assign user to nosy when topic in nosy_keywords
+                # and assign user to nosy when keyword 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:
+                        if kw == keyword:
                             found = 1
                     if found:
                         current[user_id] = 1
@@ -3538,134 +4453,228 @@ This brings the following function::
         # 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.
+These two function are the only ones needed in the file.
 
-TODO: update this example to use the find() Class method.
+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.
+    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.
 
-    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.
+    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
+    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
+    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.
 
+Changes to Security and Permissions
+-----------------------------------
 
-Adding action links to the index page
--------------------------------------
+Restricting the list of users that are assignable to a task
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Add a column to the item.index.html template.
+1. In your tracker's ``schema.py``, create a new Role, say "Developer"::
 
-Resolving the issue::
+     db.security.addRole(name='Developer', description='A developer')
 
-  <a tal:attributes="href
-     string:issue${i/id}?:status=resolved&:action=edit">resolve</a>
+2. Just after that, create a new Permission, say "Fixer", specific to
+   "issue"::
 
-"Take" the issue::
+     p = db.security.addPermission(name='Fixer', klass='issue',
+         description='User is allowed to be assigned to fix issues')
 
-  <a tal:attributes="href
-     string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a>
+3. Then assign the new Permission to your "Developer" Role::
+
+     db.security.addPermissionToRole('Developer', p)
+
+4. In the issue item edit page (``html/issue.item.html`` in your tracker
+   directory), use the new Permission in restricting the "assignedto"
+   list::
+
+    <select name="assignedto">
+     <option value="-1">- no selection -</option>
+     <tal:block tal:repeat="user db/user/list">
+     <option tal:condition="python:user.hasPermission(
+                                'Fixer', context._classname)"
+             tal:attributes="
+                value user/id;
+                selected python:user.id == context.assignedto"
+             tal:content="user/realname"></option>
+     </tal:block>
+    </select>
+
+For extra security, you may wish to setup an auditor to enforce the
+Permission requirement (install this as ``assignedtoFixer.py`` in your
+tracker ``detectors`` directory)::
+
+  def assignedtoMustBeFixer(db, cl, nodeid, newvalues):
+      ''' Ensure the assignedto value in newvalues is used with the
+          Fixer Permission
+      '''
+      if not newvalues.has_key('assignedto'):
+          # don't care
+          return
+  
+      # get the userid
+      userid = newvalues['assignedto']
+      if not db.security.hasPermission('Fixer', userid, cl.classname):
+          raise ValueError, 'You do not have permission to edit %s'%cl.classname
+
+  def init(db):
+      db.issue.audit('set', assignedtoMustBeFixer)
+      db.issue.audit('create', assignedtoMustBeFixer)
+
+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.
 
-... and so on
 
 Users may only edit their issues
---------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Users registering themselves are granted Provisional access - meaning they
+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".) We back up the permissions with
-an auditor.
+Own" on issues (regular users have "Edit".)
 
 First up, we create the new Role and Permission structure in
-``dbinit.py``::
+``schema.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')
+    # These users need to be able to view and create issues but only edit
+    # and view their own
+    db.security.addPermissionToRole('Provisional User', 'Create', 'issue')
+    def own_issue(db, userid, itemid):
+        '''Determine whether the userid matches the creator of the issue.'''
+        return userid == db.issue.get(itemid, 'creator')
+    p = db.security.addPermission(name='Edit', klass='issue',
+        check=own_issue, description='Can only edit own issues')
     db.security.addPermissionToRole('Provisional User', p)
-    p = db.security.getPermission('Edit', 'issue')
+    p = db.security.addPermission(name='View', klass='issue',
+        check=own_issue, description='Can only view own issues')
     db.security.addPermissionToRole('Provisional User', p)
 
+    # Assign the Permissions for issue-related classes
+    for cl in 'file', 'msg', 'query', 'keyword':
+        db.security.addPermissionToRole('Provisional User', 'View', cl)
+        db.security.addPermissionToRole('Provisional User', 'Edit', cl)
+        db.security.addPermissionToRole('Provisional User', 'Create', cl)
+    for cl in 'priority', 'status':
+        db.security.addPermissionToRole('Provisional User', 'View', cl)
+
     # and give the new users access to the web and email interface
-    p = db.security.getPermission('Web Access')
+    db.security.addPermissionToRole('Provisional User', 'Web Access')
+    db.security.addPermissionToRole('Provisional User', 'Email Access')
+
+    # make sure they can view & edit their own user record
+    def own_record(db, userid, itemid):
+        '''Determine whether the userid matches the item being accessed.'''
+        return userid == itemid
+    p = db.security.addPermission(name='View', klass='user', check=own_record,
+        description="User is allowed to view their own user details")
     db.security.addPermissionToRole('Provisional User', p)
-    p = db.security.getPermission('Email Access')
+    p = db.security.addPermission(name='Edit', klass='user', check=own_record,
+        description="User is allowed to edit their own user details")
     db.security.addPermissionToRole('Provisional User', p)
 
-
-Then in the ``config.py`` we change the Role assigned to newly-registered
+Then, in ``config.ini``, 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'
+    [main]
+    ...
+    new_web_user_roles = 'Provisional User'
+    new_email_user_roles = 'Provisional User'
+
+
+All users may only view and edit issues, files and messages they create
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Replace the standard "classic" tracker View and Edit Permission assignments
+for the "issue", "file" and "msg" classes with the following::
+
+    def checker(klass):
+        def check(db, userid, itemid, klass=klass):
+            return db.getclass(klass).get(itemid, 'creator') == userid
+        return check
+    for cl in 'issue', 'file', 'msg':
+        p = db.security.addPermission(name='View', klass=cl,
+            check=checker(cl))
+        db.security.addPermissionToRole('User', p)
+        p = db.security.addPermission(name='Edit', klass=cl,
+            check=checker(cl))
+        db.security.addPermissionToRole('User', p)
+        db.security.addPermissionToRole('User', 'Create', cl)
+
+
+Moderating user registration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You could set up new-user moderation in a public tracker by:
+
+1. creating a new highly-restricted user role "Pending",
+2. set the config new_web_user_roles and/or new_email_user_roles to that
+   role,
+3. have an auditor that emails you when new users are created with that
+   role using roundup.mailer
+4. edit the role to "User" for valid users.
+
+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).
 
-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)
+Changes to the Web User Interface
+---------------------------------
+
+Adding action links to the index page
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Add a column to the ``item.index.html`` template.
 
- 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)
+Resolving the issue::
 
-Note that some older trackers might also want to change the ``page.html``
-template as follows::
+  <a tal:attributes="href
+     string:issue${i/id}?:status=resolved&:action=edit">resolve</a>
 
- <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>
+"Take" the issue::
 
-(note that the "-" indicates a removed line, and the "+" indicates an added
-line).
+  <a tal:attributes="href
+     string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a>
 
+... and so on.
 
 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::
+the ``issue.index.html`` template, add this to the ``<tr>`` that
+displays the 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::
+different priorities, as follows::
 
    tr.priority-critical td {
        background-color: red;
@@ -3677,10 +4686,218 @@ different priorities, like::
 
 and so on, with far less offensive colours :)
 
--------------------
+Editing multiple items in an index view
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Back to `Table of Contents`_
+To edit the status of all items in the item index view, edit the
+``issue.item.html``:
 
-.. _`Table of Contents`: index.html
-.. _`design documentation`: design.html
+1. add a form around the listing table (separate from the existing
+   index-page form), 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 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. The final ``tal: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).
+
+
+Displaying only message summaries in the issue display
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
+Alter the ``issue.item`` template section for messages to::
+
+ <table class="messages" tal:condition="context/messages">
+  <tr><th colspan="5" class="header">Messages</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 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>
+   </td>
+  </tr>
+ </table>
+
+
+Enabling display of either message summaries or the entire messages
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+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 the one or the other. We'll use a new form variable,
+``@whole_messages`` to achieve this::
+
+ <table class="messages" tal:condition="context/messages">
+  <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>
+       </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 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>
+    </td>
+   </tr>
+  </tal:block>
+
+  <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>
+       </th>
+   </tr>
+   <tal:block tal:repeat="msg context/messages">
+    <tr>
+     <th tal:content="msg/author">author</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>
+    </tr>
+    <tr><td colspan="3" tal:content="msg/content"></td></tr>
+   </tal:block>
+  </tal:block>
+ </table>
+
+
+Setting up a "wizard" (or "druid") for controlled adding of issues
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+1. 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::
+
+    <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="page1_submit">
+
+      <strong>Category:</strong>
+      <tal:block tal:replace="structure context/category/menu" />
+      <input type="submit" value="Continue">
+    </form>
+
+   The next page has the usual issue entry information, with the
+   addition of the following form fragments::
+
+    <form method="POST" onSubmit="return submit_once()"
+          enctype="multipart/form-data"
+          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="category" tal:attributes="value cat">
+       .
+       .
+       .
+    </form>
+
+   Note that later in the form, I use the value of "cat" to decide which
+   form elements should be displayed. For example::
+
+    <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()">
+     <tr>
+      <th>Operating System</th>
+      <td tal:content="structure context/os/field"></td>
+     </tr>
+     <tr>
+      <th>Web Browser</th>
+      <td tal:content="structure context/browser/field"></td>
+     </tr>
+    </tal:block>
+
+   ... the above section will only be displayed if the category is one
+   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 a new ``Action`` class (see `defining new web actions`_)::
+
+    from roundup.cgi.actions import Action
+
+    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.client.error_message.append('You must select a category of report')
+                return
+            # everything's ok, move on to the next page
+            self.client.template = 'add_page2'
+
+    def init(instance):
+        instance.registerAction('page1_submit', Page1SubmitAction)
+
+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).
+
+
+Debugging Trackers
+==================
+
+There are three switches in tracker configs that turn on debugging in
+Roundup:
+
+1. web :: debug
+2. mail :: debug
+3. logging :: level
+
+See the config.ini file or the `tracker configuration`_ section above for
+more information.
+
+Additionally, the ``roundup-server.py`` script has its own debugging mode
+in which it reloads edited templates immediately when they are changed,
+rather than requiring a web server restart.
+
+
+.. _`design documentation`: design.html
+.. _`developer's guide`: developers.html