diff --git a/doc/customizing.txt b/doc/customizing.txt
index 4592a7a63d2e279be9de750cd3d23f93ecdceb8c..0136601910ec8a56f34d249378bd9c36dd0a9db6 100644 (file)
--- a/doc/customizing.txt
+++ b/doc/customizing.txt
Customising Roundup
===================
-:Version: $Revision: 1.89 $
+:Version: $Revision: 1.105 $
.. This document borrows from the ZopeBook section on ZPT. The original is at:
http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx
Before you get too far, it's probably worth having a quick read of the Roundup
`design documentation`_.
-Customisation of Roundup can take one of five forms:
+Customisation of Roundup can take one of six forms:
1. `tracker configuration`_ file changes
2. database, or `tracker schema`_ changes
priority=Link("priority"))
issue.setkey('title')
+
+What you can't do to the schema
+-------------------------------
+
+You must never:
+
+**Remove the users class**
+ This class is the only *required* class in Roundup. Similarly, its
+ username, password and address properties must never be removed.
+
+**Change the type of a property**
+ Property types must *never* be changed - the database simply doesn't take
+ this kind of action into account. Note that you can't just remove a
+ property and re-add it as a new type either. If you wanted to make the
+ assignedto property a Multilink, you'd need to create a new property
+ assignedto_list and remove the old assignedto property.
+
+
+What you can do to the schema
+-----------------------------
+
+Your schema may be changed at any time before or after the tracker has been
+initialised (or used). You may:
+
+**Add new properties to classes, or add whole new classes**
+ This is painless and easy to do - there are generally no repurcussions
+ from adding new information to a tracker's schema.
+
+**Remove properties**
+ Removing properties is a little more tricky - you need to make sure that
+ the property is no longer used in the `web interface`_ *or* by the
+ detectors_.
+
+
+
Classes and Properties - creating a new information store
---------------------------------------------------------
It also provides the ``presetunread`` auditor which pre-sets the
status to ``unread`` on new items if the status isn't explicitly
defined.
+**messagesummary.py**
+ Generates the ``summary`` property for new messages based on the message
+ content.
+**userauditor.py**
+ Verifies the content of some of the user fields (email addresses and
+ roles lists).
+
+If you don't want this default behaviour, you're completely free to change
+or remove these detectors.
See the detectors section in the `design document`__ for details of the
interface for detectors.
-----------------------
To determine the "context" of a request, we look at the URL and the
-special request variable ``:template``. The URL path after the tracker
+special request variable ``@template``. The URL path after the tracker
identifier is examined. Typical URL paths look like:
1. ``/tracker/issue``
Both b. and e. stop before we bother to determine the template we're
going to use. That's because they don't actually use templates.
-The template used is specified by the ``:template`` CGI variable, which
+The template used is specified by the ``@template`` CGI variable, which
defaults to:
- only classname suplied: "index"
When a user requests a web page, they may optionally also request for an
action to take place. As described in `how requests are processed`_, the
action is performed before the requested page is generated. Actions are
-triggered by using a ``:action`` CGI variable, where the value is one
+triggered by using a ``@action`` CGI variable, where the value is one
of:
**login**
log them in.
**edit**
- Perform an edit of an item in the database. There are some special form
- elements you may use:
-
- :link=designator:property and :multilink=designator:property
- The value specifies an item designator and the property on that item
- to which *this* item should be added, as a link or multilink.
- :note
- Create a message and attach it to the current item's "messages"
- property.
- :file
- Create a file and attach it to the current item's "files" property.
- Attach the file to the message created from the ``:note`` if it's
- supplied.
- :required=property,property,...
- The named properties are required to be filled in the form.
- :remove:<propname>=id(s)
- The ids will be removed from the multilink property. You may have
- multiple ``:remove:<propname>`` form elements for a single <propname>.
- :add:<propname>=id(s)
- The ids will be added to the multilink property. You may have multiple
- ``:add:<propname>`` form elements for a single <propname>.
+ Perform an edit of an item in the database. There are some `special form
+ variables`_ you may use.
**new**
- Add a new item to the database. You may use the same special form
- elements as in the "edit" action.
+ Add a new item to the database. You may use the same `special form
+ variables`_ as in the "edit" action.
**retire**
Retire the item in the database.
behaviour is to check whether the user may view this class.
+Special form variables
+----------------------
+
+Item properties and their values are edited with html FORM
+variables and their values. You can:
+
+- Change the value of some property of the current item.
+- Create a new item of any class, and edit the new item's
+ properties,
+- Attach newly created items to a multilink property of the
+ current item.
+- Remove items from a multilink property of the current item.
+- Specify that some properties are required for the edit
+ operation to be successful.
+
+In the following, <bracketed> values are variable, "@" may be
+either ":" or "@", and other text "required" is fixed.
+
+Most properties are specified as form variables:
+
+``<propname>``
+ property on the current context item
+
+``<designator>"@"<propname>``
+ property on the indicated item (for editing related information)
+
+Designators name a specific item of a class.
+
+``<classname><N>``
+ Name an existing item of class <classname>.
+
+``<classname>"-"<N>``
+ Name the <N>th new item of class <classname>. If the form
+ submission is successful, a new item of <classname> is
+ created. Within the submitted form, a particular
+ designator of this form always refers to the same new
+ item.
+
+Once we have determined the "propname", we look at it to see
+if it's special:
+
+``@required``
+ The associated form value is a comma-separated list of
+ property names that must be specified when the form is
+ submitted for the edit operation to succeed.
+
+ When the <designator> is missing, the properties are
+ for the current context item. When <designator> is
+ present, they are for the item specified by
+ <designator>.
+
+ The "@required" specifier must come before any of the
+ properties it refers to are assigned in the form.
+
+``@remove@<propname>=id(s)`` or ``@add@<propname>=id(s)``
+ The "@add@" and "@remove@" edit actions apply only to
+ Multilink properties. The form value must be a
+ comma-separate list of keys for the class specified by
+ the simple form variable. The listed items are added
+ to (respectively, removed from) the specified
+ property.
+
+``@link@<propname>=<designator>``
+ If the edit action is "@link@", the simple form
+ variable must specify a Link or Multilink property.
+ The form value is a comma-separated list of
+ designators. The item corresponding to each
+ designator is linked to the property given by simple
+ form variable.
+
+None of the above (ie. just a simple form value)
+ The value of the form variable is converted
+ appropriately, depending on the type of the property.
+
+ For a Link('klass') property, the form value is a
+ single key for 'klass', where the key field is
+ specified in dbinit.py.
+
+ For a Multilink('klass') property, the form value is a
+ comma-separated list of keys for 'klass', where the
+ key field is specified in dbinit.py.
+
+ Note that for simple-form-variables specifiying Link
+ and Multilink properties, the linked-to class must
+ have a key field.
+
+ For a String() property specifying a filename, the
+ file named by the form value is uploaded. This means we
+ try to set additional properties "filename" and "type" (if
+ they are valid for the class). Otherwise, the property
+ is set to the form value.
+
+ For Date(), Interval(), Boolean(), and Number()
+ properties, the form value is converted to the
+ appropriate
+
+Any of the form variables may be prefixed with a classname or
+designator.
+
+Two special form values are supported for backwards compatibility:
+
+@note
+ This is equivalent to::
+
+ @link@messages=msg-1
+ msg-1@content=value
+
+ except that in addition, the "author" and "date" properties of
+ "msg-1" are set to the userid of the submitter, and the current
+ time, respectively.
+
+@file
+ This is equivalent to::
+
+ @link@files=file-1
+ file-1@content=value
+
+ The String content value is handled as described above for file
+ uploads.
+
+If both the "@note" and "@file" form variables are
+specified, the action::
+
+ @link@msg-1@files=file-1
+
+is also performed.
+
+We also check that FileClass items have a "content" property with
+actual content, otherwise we remove them from all_props before
+returning.
+
+
+
Default templates
-----------------
Note: Remember that you can create any template extension you want to,
so if you just want to play around with the templating for new issues,
you can copy the current "issue.item" template to "issue.test", and then
-access the test template using the ":template" URL argument::
+access the test template using the "@template" URL argument::
- http://your.tracker.example/tracker/issue?:template=test
+ http://your.tracker.example/tracker/issue?@template=test
and it won't affect your users using the "issue.item" template.
There are several methods available on these wrapper objects:
-========= ================================================================
-Method Description
-========= ================================================================
-plain render a "plain" representation of the property. This method
- may take two arguments:
-
- escape
- If true, escape the text so it is HTML safe (default: no). The
- reason this defaults to off is that text is usually escaped
- at a later stage by the TAL commands, unless the "structure"
- option is used in the template. The following are all
- equivalent::
-
- <p tal:content="structure python:msg.content.plain(escape=1)" />
- <p tal:content="python:msg.content.plain()" />
- <p tal:content="msg/content/plain" />
- <p tal:content="msg/content" />
-
- Usually you'll only want to use the escape option in a
- complex expression.
-
- hyperlink
- If true, turn URLs, email addresses and hyperdb item
- designators in the text into hyperlinks (default: no). Note
- that you'll need to use the "structure" TAL option if you
- want to use this::
-
- <p tal:content="structure python:msg.content.plain(hyperlink=1)" />
-
- Note also that the text is automatically HTML-escaped before
- the hyperlinking transformation.
-
-field render an appropriate form edit field for the property - for
- most types this is a text entry box, but for Booleans it's a
- tri-state yes/no/neither selection.
-stext only on String properties - render the value of the property
- as StructuredText (requires the StructureText module to be
- installed separately)
-multiline only on String properties - render a multiline form edit
- field for the property
-email only on String properties - render the value of the property
- as an obscured email address
-confirm only on Password properties - render a second form edit field
- for the property, used for confirmation that the user typed
- the password correctly. Generates a field with name
- "name:confirm".
-now only on Date properties - return the current date as a new
- property
-reldate only on Date properties - render the interval between the date
- and now
-local only on Date properties - return this date as a new property
- with some timezone offset
-pretty only on Interval properties - render the interval in a pretty
- format (eg. "yesterday")
-menu only on Link and Multilink properties - render a form select
- list for this property
-reverse only on Multilink properties - produce a list of the linked
- items in reverse order
-========= =====================================================================
+=========== ================================================================
+Method Description
+=========== ================================================================
+plain render a "plain" representation of the property. This method
+ may take two arguments:
+
+ escape
+ If true, escape the text so it is HTML safe (default: no). The
+ reason this defaults to off is that text is usually escaped
+ at a later stage by the TAL commands, unless the "structure"
+ option is used in the template. The following ``tal:content``
+ expressions are all equivalent::
+
+ "structure python:msg.content.plain(escape=1)"
+ "python:msg.content.plain()"
+ "msg/content/plain"
+ "msg/content"
+
+ Usually you'll only want to use the escape option in a
+ complex expression.
+
+ hyperlink
+ If true, turn URLs, email addresses and hyperdb item
+ designators in the text into hyperlinks (default: no). Note
+ that you'll need to use the "structure" TAL option if you
+ want to use this ``tal:content`` expression::
+
+ "structure python:msg.content.plain(hyperlink=1)"
+
+ Note also that the text is automatically HTML-escaped before
+ the hyperlinking transformation.
+hyperlinked The same as msg.content.plain(hyperlink=1), but nicer::
+
+ "structure msg/content/hyperlinked"
+
+field render an appropriate form edit field for the property - for
+ most types this is a text entry box, but for Booleans it's a
+ tri-state yes/no/neither selection.
+stext only on String properties - render the value of the property
+ as StructuredText (requires the StructureText module to be
+ installed separately)
+multiline only on String properties - render a multiline form edit
+ field for the property
+email only on String properties - render the value of the property
+ as an obscured email address
+confirm only on Password properties - render a second form edit field
+ for the property, used for confirmation that the user typed
+ the password correctly. Generates a field with name
+ "name:confirm".
+now only on Date properties - return the current date as a new
+ property
+reldate only on Date properties - render the interval between the date
+ and now
+local only on Date properties - return this date as a new property
+ with some timezone offset
+pretty only on Interval properties - render the interval in a pretty
+ format (eg. "yesterday")
+menu only on Link and Multilink properties - render a form select
+ list for this property
+reverse only on Multilink properties - produce a list of the linked
+ items in reverse order
+=========== ================================================================
The request variable
db/user
python:db.user
+Also, the current id of the current user is available as
+``db.getuid()``. This isn't so useful in templates (where you have
+``request/user``), but it can be useful in detectors or interfaces.
+
The access results in a `hyperdb class wrapper`_.
This is one of the class context views. The template used is typically
"*classname*.search". The form on this page should have "search" as its
-``:action`` variable. The "search" action:
+``@action`` variable. The "search" action:
- sets up additional filtering, as well as performing indexed text
searching
<table class="form">
<tr>
- <th nowrap>Title</th>
+ <th>Title</th>
<td colspan="3" tal:content="structure python:context.title.field(size=60)">title</td>
</tr>
<tr>
- <th nowrap>Priority</th>
+ <th>Priority</th>
<td tal:content="structure context/priority/menu">priority</td>
- <th nowrap>Status</th>
+ <th>Status</th>
<td tal:content="structure context/status/menu">status</td>
</tr>
<tr>
- <th nowrap>Superseder</th>
+ <th>Superseder</th>
<td>
<span tal:replace="structure python:context.superseder.field(showid=1, size=20)" />
<span tal:replace="structure python:db.issue.classhelp('id,title')" />
<br>View: <span tal:replace="structure python:context.superseder.link(showid=1)" />
</span>
</td>
- <th nowrap>Nosy List</th>
+ <th>Nosy List</th>
<td>
<span tal:replace="structure context/nosy/field" />
<span tal:replace="structure python:db.user.classhelp('username,realname,address,phone')" />
</tr>
<tr>
- <th nowrap>Assigned To</th>
+ <th>Assigned To</th>
<td tal:content="structure context/assignedto/menu">
assignedto menu
</td>
</tr>
<tr>
- <th nowrap>Change Note</th>
+ <th>Change Note</th>
<td colspan="3">
<textarea name=":note" wrap="hard" rows="5" cols="60"></textarea>
</td>
</tr>
<tr>
- <th nowrap>File</th>
+ <th>File</th>
<td colspan="3"><input type="file" name=":file" size="40"></td>
</tr>
Once we have determined the "propname", we check to see if it is one of
the special form values:
-``:required``
+``@required``
The named property values must be supplied or a ValueError will be
raised.
-``:remove:<propname>=id(s)``
+``@remove@<propname>=id(s)``
The ids will be removed from the multilink property.
``:add:<propname>=id(s)``
Defining new web actions
------------------------
-You may define new actions to be triggered by the ``:action`` form
+You may define new actions to be triggered by the ``@action`` form
variable. These are added to the tracker ``interfaces.py`` as methods on
the ``Client`` class.
Adding action methods takes three steps; first you `define the new
action method`_, then you `register the action method`_ with the cgi
-interface so it may be triggered by the ``:action`` form variable.
+interface so it may be triggered by the ``@action`` form variable.
Finally you `use the new action`_ in your HTML form.
See "`setting up a "wizard" (or "druid") for controlled adding of
In your HTML form, add a hidden form element like so::
- <input type="hidden" name=":action" value="myaction">
+ <input type="hidden" name="@action" value="myaction">
where "myaction" is the name you registered in the previous step.
tal:condition="python:request.user.hasPermission('View', 'category')">
<b>Categories</b><br>
<a tal:condition="python:request.user.hasPermission('Edit', 'category')"
- href="category?:template=item">New Category<br></a>
+ href="category?@template=item">New Category<br></a>
</p>
The first two lines is the classblock definition, which sets up a
we require the user to enter. There will be only one field - "name" - so
they better put something in it, otherwise the whole form is pointless::
- <input type="hidden" name=":required" value="name">
+ <input type="hidden" name="@required" value="name">
To get everything to line up properly we will put everything in a table,
and put a nice big header on it so the user has an idea what is
to the form, a new category will be created with that name::
<tr>
- <th nowrap>Name</th>
+ <th>Name</th>
<td tal:content="structure python:context.name.field(size=60)">
name</td>
</tr>
<form method="POST" onSubmit="return submit_once()"
enctype="multipart/form-data">
- <input type="hidden" name=":required" value="name">
-
<table class="form">
<tr><th class="header" colspan="2">Category</th></tr>
<tr>
- <th nowrap>Name</th>
+ <th>Name</th>
<td tal:content="structure python:context.name.field(size=60)">
name</td>
</tr>
<tr>
- <td> </td>
+ <td>
+
+ <input type="hidden" name="@required" value="name">
+ </td>
<td colspan="3" tal:content="structure context/submit">
submit button will go here
</td>
table to lay things out. It doesn't matter where in the table we add new
stuff, it is entirely up to your sense of aesthetics::
- <th nowrap>Category</th>
+ <th>Category</th>
<td><span tal:replace="structure context/category/field" />
<span tal:replace="structure db/category/classhelp" />
</td>
If you look for "Search Issues" in the 'html/page.html' file, you will
find that it looks something like
-``<a href="issue?:template=search">Search Issues</a>``. This shows us
+``<a href="issue?@template=search">Search Issues</a>``. This shows us
that when you click on "Search Issues" it will be looking for a
``issue.search.html`` file to display. So that is the file that we will
change.
-This file should begin to look familiar, by now. It is a simple HTML
-form using a table to define structure. You can add the new category
-search code anywhere you like within that form::
-
- <tr>
- <th>Category:</th>
- <td>
- <select name="category">
- <option value="">don't care</option>
- <option value="">------------</option>
- <option tal:repeat="s db/category/list"
- tal:attributes="value s/name"
- tal:content="s/name">category to filter on</option>
- </select>
- </td>
- <td><input type="checkbox" name=":columns" value="category"
- checked></td>
- <td><input type="radio" name=":sort" value="category"></td>
- <td><input type="radio" name=":group" value="category"></td>
- </tr>
-
-Most of this is straightforward to anyone who knows HTML. It is just
-setting up a select list followed by a checkbox and a couple of radio
-buttons.
+If you look at this file it should be starting to seem familiar, although it
+does use some new macros. You can add the new category search code anywhere you
+like within that form::
+
+ <tr tal:define="name string:category;
+ db_klass string:category;
+ db_content string:name;">
+ <th>Priority:</th>
+ <td metal:use-macro="search_select"></td>
+ <td metal:use-macro="column_input"></td>
+ <td metal:use-macro="sort_input"></td>
+ <td metal:use-macro="group_input"></td>
+ </tr>
-The ``tal:repeat`` part repeats the tag for every item in the "category"
-table and sets "s" to each category in turn.
+The definitions in the <tr> opening tag are used by the macros:
-The ``tal:attributes`` part is setting up the ``value=`` part of the
-option tag to be the name part of "s", which is the current category in
-the loop.
+- search_select expands to a drop-down box with all categories using db_klass
+ and db_content.
+- column_input expands to a checkbox for selecting what columns should be
+ displayed.
+- sort_input expands to a radio button for selecting what property should be
+ sorted on.
+- group_input expands to a radio button for selecting what property should be
+ group on.
-The ``tal:content`` part is setting the contents of the option tag to be
-the name part of "s" again. For objects more complex than category,
-obviously you would put an id in the value, and the descriptive part in
-the content; but for categories they are the same.
+The category search code above would expand to the following::
+ <tr>
+ <th>Category:</th>
+ <td>
+ <select name="category">
+ <option value="">don't care</option>
+ <option value="">------------</option>
+ <option value="1">scipy</option>
+ <option value="2">chaco</option>
+ <option value="3">weave</option>
+ </select>
+ </td>
+ <td><input type="checkbox" name=":columns" value="category"></td>
+ <td><input type="radio" name=":sort" value="category"></td>
+ <td><input type="radio" name=":group" value="category"></td>
+ </tr>
Adding category to the default view
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4. in the ``issue.item.html`` template, change the status editing bit
from::
- <th nowrap>Status</th>
+ <th>Status</th>
<td tal:content="structure context/status/menu">status</td>
to::
- <th nowrap>Status</th>
+ <th>Status</th>
<td>
<select tal:condition="context/id" name="status">
<tal:block tal:define="ok context/status/transitions"
<td><a tal:attributes="href string:msg${msg/id}"
tal:content="string:msg${msg/id}"></a></td>
<td tal:content="msg/author">author</td>
- <td nowrap tal:content="msg/date/pretty">date</td>
+ <td class="date" tal:content="msg/date/pretty">date</td>
<td tal:content="msg/summary">summary</td>
<td>
- <a tal:attributes="href string:?:remove:messages=${msg/id}&:action=edit">
+ <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">
remove</a>
</td>
</tr>
<form method="POST" onSubmit="return submit_once()"
enctype="multipart/form-data">
- <input type="hidden" name=":template" value="add_page1">
- <input type="hidden" name=":action" value="page1submit">
+ <input type="hidden" name="@template" value="add_page1">
+ <input type="hidden" name="@action" value="page1submit">
<strong>Category:</strong>
<tal:block tal:replace="structure context/category/menu" />
tal:condition="context/is_edit_ok"
tal:define="cat request/form/category/value">
- <input type="hidden" name=":template" value="add_page2">
- <input type="hidden" name=":required" value="title">
+ <input type="hidden" name="@template" value="add_page2">
+ <input type="hidden" name="@required" value="title">
<input type="hidden" name="category" tal:attributes="value cat">
.
.
<tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()">
<tr>
- <th nowrap>Operating System</th>
+ <th>Operating System</th>
<td tal:content="structure context/os/field"></td>
</tr>
<tr>
- <th nowrap>Web Browser</th>
+ <th>Web Browser</th>
<td tal:content="structure context/browser/field"></td>
</tr>
</tal:block>
# everything's ok, move on to the next page
self.template = 'add_page2'
-4. Use the usual "new" action as the ``:action`` on the final page, and
+4. Use the usual "new" action as the ``@action`` on the final page, and
you're done (the standard context/submit method can do this for you).
the "times" property is the new link to the "timelog" class.
3. We'll need to let people add in times to the issue, so in the web
- interface we'll have a new entry field, just below the change note
- box::
-
- <tr>
- <th nowrap>Time Log</th>
- <td colspan="3"><input name=":timelog">
- (enter as "3y 1m 4d 2:40:02" or parts thereof)
- </td>
- </tr>
-
- Note that we've made up a new form variable, but since we place a
- colon ":" in front of it, it won't clash with any existing property
- variables. The names you *can't* use are ``:note``, ``:file``,
- ``:action``, ``:required`` and ``:template``. These variables are
- described in the section `performing actions in web requests`_.
-
-4. We also need to handle this new field in the CGI interface - the way
- to do this is through implementing a new form action (see `Setting up
- a "wizard" (or "druid") for controlled adding of issues`_ for another
- example where we implemented a new CGI form action).
-
- In this case, we'll want our action to:
-
- 1. create a new "timelog" entry,
- 2. fake that the issue's "times" property has been edited, and then
- 3. call the normal CGI edit action handler.
-
- The code to do this is::
-
- class Client(client.Client):
- ''' derives basic CGI implementation from the standard module,
- with any specific extensions
- '''
- actions = client.Client.actions + (
- ('edit_with_timelog', 'timelogEditAction'),
- ('new_with_timelog', 'timelogEditAction'),
- )
-
- def timelogEditAction(self):
- ''' Handle the creation of a new time log entry if
- necessary.
-
- If we create a new entry, fake up a CGI form value for
- the altered "times" property of the issue being edited.
-
- Punt to the regular edit action when we're done.
- '''
- # if there's a timelog value specified, create an entry
- if self.form.has_key(':timelog') and \
- self.form[':timelog'].value.strip():
- period = Interval(self.form[':timelog'].value)
- # create it
- newid = self.db.timelog.create(period=period)
-
- # if we're editing an existing item, get the old timelog
- # value
- if self.nodeid:
- l = self.db.issue.get(self.nodeid, 'times')
- l.append(newid)
- else:
- l = [newid]
-
- # now make the fake CGI form values
- for entry in l:
- self.form.list.append(
- MiniFieldStorage('times', entry))
-
- # punt to the normal edit action
- if self.nodeid:
- return self.editItemAction()
- else:
- return self.newItemAction()
-
- you add this code to your Client class in your tracker's
- ``interfaces.py`` file. Locate the section that looks like::
-
- class Client:
- ''' derives basic CGI implementation from the standard module,
- with any specific extensions
- '''
- pass
-
- and insert this code in place of the ``pass`` statement.
-
-5. You'll also need to modify your ``issue.item`` form submit action so
- it calls the time logging action we just created. The current
- template will look like this::
-
- <tr>
- <td> </td>
- <td colspan="3" tal:content="structure context/submit">
- submit button will go here
- </td>
- </tr>
-
- replace it with this::
-
- <tr>
- <td> </td>
- <td colspan="3">
- <tal:block tal:condition="context/id">
- <input type="hidden" name=":action" value="edit_with_timelog">
- <input type="submit" name="submit" value="Submit Changes">
- </tal:block>
- <tal:block tal:condition="not:context/id">
- <input type="hidden" name=":action" value="new_with_timelog">
- <input type="submit" name="submit" value="Submit New Issue">
- </tal:block>
- </td>
- </tr>
-
- The important change is setting the action to "edit_with_timelog" for
- edit operations (where the item exists) and "new_with_timelog" for
- creations operations.
-
-6. We want to display a total of the time log times that have been
+ interface we'll have a new entry field. This is a special field
+ because unlike the other fields in the issue.item template, it
+ affects a different item (a timelog item) and not the template's
+ item, an issue. We have a special syntax for form fields that affect
+ items other than the template default item (see the cgi
+ documentation on `special form variables`_). In particular, we add a
+ field to capture a new timelog item's perdiod::
+
+ <tr>
+ <th>Time Log</th>
+ <td colspan=3><input type="text" name="timelog-1@period" />
+ <br />(enter as '3y 1m 4d 2:40:02' or parts thereof)
+ </td>
+ </tr>
+
+ and another hidden field that links that new timelog item (new
+ because it's marked as having id "-1") to the issue item. It looks
+ like this::
+
+ <input type="hidden" name="@link@times" value="timelog-1" />
+
+ On submission, the "-1" timelog item will be created and assigned a
+ real item id. The "times" property of the issue will have the new id
+ added to it.
+
+4. We want to display a total of the time log times that have been
accumulated for an issue. To do this, we'll need to actually write
some Python code, since it's beyond the scope of PageTemplates to
perform such calculations. We do this by adding a method to the
``totalTimeSpent`` method via the ``utils`` variable in our
templates.
-7. Display the time log for an issue::
+5. Display the time log for an issue::
<table class="otherinfo" tal:condition="context/times">
<tr><th colspan="3" class="header">Time Log
And that's it!
+Using an LDAP database for user information
+-------------------------------------------
+
+A script that reads users from an LDAP store using
+http://python-ldap.sf.net/ and then compares the list to the users in the
+roundup user database would be pretty easy to write. You'd then have it run
+once an hour / day (or on demand if you can work that into your LDAP store
+workflow). See the example `Using a UN*X passwd file as the user database`_
+for more information about doing this.
+
+To authenticate off the LDAP store (rather than using the passwords in the
+roundup user database) you'd use the same python-ldap module inside an
+extension to the cgi interface. You'd do this by adding a method called
+"verifyPassword" to the Client class in your tracker's interfaces.py
+module. The method is implemented by default as::
+
+ def verifyPassword(self, userid, password):
+ ''' Verify the password that the user has supplied
+ '''
+ stored = self.db.user.get(self.userid, 'password')
+ if password == stored:
+ return 1
+ if not password and not stored:
+ return 1
+ return 0
+
+So you could reimplement this as something like::
+
+ def verifyPassword(self, userid, password):
+ ''' Verify the password that the user has supplied
+ '''
+ # look up some unique LDAP information about the user
+ username = self.db.user.get(self.userid, 'username')
+ # now verify the password supplied against the LDAP store
+
+
Enabling display of either message summaries or the entire messages
-------------------------------------------------------------------
example `displaying only message summaries in the issue display`_ into
our template alongside the summary display, and then introduce a switch
that shows either one or the other. We'll use a new form variable,
-``:whole_messages`` to achieve this::
+``@whole_messages`` to achieve this::
<table class="messages" tal:condition="context/messages">
- <tal:block tal:condition="not:request/form/:whole_messages/value | python:0">
+ <tal:block tal:condition="not:request/form/@whole_messages/value | python:0">
<tr><th colspan="3" class="header">Messages</th>
<th colspan="2" class="header">
- <a href="?:whole_messages=yes">show entire messages</a>
+ <a href="?@whole_messages=yes">show entire messages</a>
</th>
</tr>
<tr tal:repeat="msg context/messages">
<td><a tal:attributes="href string:msg${msg/id}"
tal:content="string:msg${msg/id}"></a></td>
<td tal:content="msg/author">author</td>
- <td nowrap tal:content="msg/date/pretty">date</td>
+ <td class="date" tal:content="msg/date/pretty">date</td>
<td tal:content="msg/summary">summary</td>
<td>
- <a tal:attributes="href string:?:remove:messages=${msg/id}&:action=edit">remove</a>
+ <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>
</td>
</tr>
</tal:block>
- <tal:block tal:condition="request/form/:whole_messages/value | python:0">
+ <tal:block tal:condition="request/form/@whole_messages/value | python:0">
<tr><th colspan="2" class="header">Messages</th>
<th class="header">
- <a href="?:whole_messages=">show only summaries</a>
+ <a href="?@whole_messages=">show only summaries</a>
</th>
</tr>
<tal:block tal:repeat="msg context/messages">
<tr>
<th tal:content="msg/author">author</th>
- <th nowrap tal:content="msg/date/pretty">date</th>
+ <th class="date" tal:content="msg/date/pretty">date</th>
<th style="text-align: right">
- (<a tal:attributes="href string:?:remove:messages=${msg/id}&:action=edit">remove</a>)
+ (<a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>)
</th>
</tr>
<tr><td colspan="3" tal:content="msg/content"></td></tr>
2. Add the new "blockers" property to the issue.item edit page, using
something like::
- <th nowrap>Waiting On</th>
+ <th>Waiting On</th>
<td>
<span tal:replace="structure python:context.blockers.field(showid=1,
size=20)" />
history at the bottom of the issue page - look for a "link" event to
another issue's "blockers" property.
+Add users to the nosy list based on the topic
+---------------------------------------------
+
+We need the ability to automatically add users to the nosy list based
+on the occurence of a topic. Every user should be allowed to edit his
+own list of topics for which he wants to be added to the nosy list.
+
+Below will be showed that such a change can be performed with only
+minimal understanding of the roundup system, but with clever use
+of Copy and Paste.
+
+This requires three changes to the tracker: a change in the database to
+allow per-user recording of the lists of topics for which he wants to
+be put on the nosy list, a change in the user view allowing to edit
+this list of topics, and addition of an auditor which updates the nosy
+list when a topic is set.
+
+Adding the nosy topic list
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The change in the database to make is that for any user there should be
+a list of topics for which he wants to be put on the nosy list. Adding
+a ``Multilink`` of ``keyword`` seem to fullfill this (note that within
+the code topics are called ``keywords``.) As such, all what has to be
+done is to add a new field to the definition of ``user`` within the
+file ``dbinit.py``. We will call this new field ``nosy_keywords``, and
+the updated definition of user will be::
+
+ user = Class(db, "user",
+ username=String(), password=Password(),
+ address=String(), realname=String(),
+ phone=String(), organisation=String(),
+ alternate_addresses=String(),
+ queries=Multilink('query'), roles=String(),
+ timezone=String(),
+ nosy_keywords=Multilink('keyword'))
+
+Changing the user view to allow changing the nosy topic list
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We want any user to be able to change the list of topics for which
+he will by default be added to the nosy list. We choose to add this
+to the user view, as is generated by the file ``html/user.item.html``.
+We easily can
+see that the topic field in the issue view has very similar editting
+requirements as our nosy topics, both being a list of topics. As
+such, we search for Topics in ``issue.item.html``, and extract the
+associated parts from there. We add this to ``user.item.html`` at the
+bottom of the list of viewed items (i.e. just below the 'Alternate
+E-mail addresses' in the classic template)::
+
+ <tr>
+ <th>Nosy Topics</th>
+ <td>
+ <span tal:replace="structure context/nosy_keywords/field" />
+ <span tal:replace="structure python:db.keyword.classhelp(property='nosy_keywords')" />
+ </td>
+ </tr>
+
+
+Addition of an auditor to update the nosy list
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The more difficult part is the addition of the logic to actually
+at the users to the nosy list when it is required.
+The choice is made to perform this action when the topics on an
+item are set, including when an item is created.
+Here we choose to start out with a copy of the
+``detectors/nosyreaction.py`` detector, which we copy to the file
+``detectors/nosy_keyword_reaction.py``.
+This looks like a good start as it also adds users
+to the nosy list. A look through the code reveals that the
+``nosyreaction`` function actually is sending the e-mail, which
+we do not need. As such, we can change the init function to::
+
+ def init(db):
+ db.issue.audit('create', update_kw_nosy)
+ db.issue.audit('set', update_kw_nosy)
+
+After that we rename the ``updatenosy`` function to ``update_kw_nosy``.
+The first two blocks of code in that function relate to settings
+``current`` to a combination of the old and new nosy lists. This
+functionality is left in the new auditor. The following block of
+code, which in ``updatenosy`` handled adding the assignedto user(s)
+to the nosy list, should be replaced by a block of code to add the
+interested users to the nosy list. We choose here to loop over all
+new topics, than loop over all users,
+and assign the user to the nosy list when the topic in the user's
+nosy_keywords. The next part in ``updatenosy``, adding the author
+and/or recipients of a message to the nosy list, obviously is not
+relevant here and thus is deleted from the new auditor. The last
+part, copying the new nosy list to newvalues, does not have to be changed.
+This brings the following function::
+
+ def update_kw_nosy(db, cl, nodeid, newvalues):
+ '''Update the nosy list for changes to the topics
+ '''
+ # nodeid will be None if this is a new node
+ current = {}
+ if nodeid is None:
+ ok = ('new', 'yes')
+ else:
+ ok = ('yes',)
+ # old node, get the current values from the node if they haven't
+ # changed
+ if not newvalues.has_key('nosy'):
+ nosy = cl.get(nodeid, 'nosy')
+ for value in nosy:
+ if not current.has_key(value):
+ current[value] = 1
+
+ # if the nosy list changed in this transaction, init from the new value
+ if newvalues.has_key('nosy'):
+ nosy = newvalues.get('nosy', [])
+ for value in nosy:
+ if not db.hasnode('user', value):
+ continue
+ if not current.has_key(value):
+ current[value] = 1
+
+ # add users with topic in nosy_keywords to the nosy list
+ if newvalues.has_key('topic') and newvalues['topic'] is not None:
+ topic_ids = newvalues['topic']
+ for topic in topic_ids:
+ # loop over all users,
+ # and assign user to nosy when topic in nosy_keywords
+ for user_id in db.user.list():
+ nosy_kw = db.user.get(user_id, "nosy_keywords")
+ found = 0
+ for kw in nosy_kw:
+ if kw == topic:
+ found = 1
+ if found:
+ current[user_id] = 1
+
+ # that's it, save off the new nosy list
+ newvalues['nosy'] = current.keys()
+
+and these two function are the only ones needed in the file.
+
+TODO: update this example to use the find() Class method.
+
+Caveats
+~~~~~~~
+
+A few problems with the design here can be noted:
+
+Multiple additions
+ When a user, after automatic selection, is manually removed
+ from the nosy list, he again is added to the nosy list when the
+ topic list of the issue is updated. A better design might be
+ to only check which topics are new compared to the old list
+ of topics, and only add users when they have indicated
+ interest on a new topic.
+
+ The code could also be changed to only trigger on the create() event,
+ rather than also on the set() event, thus only setting the nosy list
+ when the issue is created.
+
+Scalability
+ In the auditor there is a loop over all users. For a site with
+ only few users this will pose no serious problem, however, with
+ many users this will be a serious performance bottleneck.
+ A way out will be to link from the topics to the users which
+ selected these topics a nosy topics. This will eliminate the
+ loop over all users.
+
+
+Adding action links to the index page
+-------------------------------------
+
+Add a column to the item.index.html template.
+
+Resolving the issue::
+
+ <a tal:attributes="href
+ string:issue${i/id}?:status=resolved&:action=edit">resolve</a>
+
+"Take" the issue::
+
+ <a tal:attributes="href
+ string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a>
+
+... and so on
-------------------