diff --git a/doc/customizing.txt b/doc/customizing.txt
index 245ca2a7d517ebdc4a4e32dd8c775818e79ff059..67de904e00585a6267a5f1d8798c51514a72197f 100644 (file)
--- a/doc/customizing.txt
+++ b/doc/customizing.txt
Customising Roundup
===================
-:Version: $Revision: 1.92 $
+:Version: $Revision: 1.104 $
.. 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.
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 ``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.
-
-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
python:db.user
Also, the current id of the current user is available as
-``db.curuserid``. This isn't so useful in templates (where you have
+``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`_.
<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>
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>
``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">
<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>
field to capture a new timelog item's perdiod::
<tr>
- <th nowrap>Time Log</th>
+ <th>Time Log</th>
<td colspan=3><input type="text" name="timelog-1@period" />
<br />(enter as '3y 1m 4d 2:40:02' or parts thereof)
</td>
<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>
<tal:block tal:repeat="msg context/messages">
<tr>
<th tal:content="msg/author">author</th>
- <th nowrap tal:content="msg/date/pretty">date</th>
+ <th class="date" tal:content="msg/date/pretty">date</th>
<th style="text-align: right">
(<a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>)
</th>
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
-------------------