Code

oops
[roundup.git] / doc / customizing.txt
index 9cdf143c0aa69f82dfe9692b665fb901f889b0b3..0136601910ec8a56f34d249378bd9c36dd0a9db6 100644 (file)
@@ -2,7 +2,7 @@
 Customising Roundup
 ===================
 
-:Version: $Revision: 1.93 $
+:Version: $Revision: 1.105 $
 
 .. This document borrows from the ZopeBook section on ZPT. The original is at:
    http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx
@@ -16,7 +16,7 @@ What You Can Do
 Before you get too far, it's probably worth having a quick read of the Roundup
 `design documentation`_.
 
-Customisation of Roundup can take one of five forms:
+Customisation of Roundup can take one of six forms:
 
 1. `tracker configuration`_ file changes
 2. database, or `tracker schema`_ changes
@@ -301,6 +301,41 @@ of ``'setkey'``)::
         priority=Link("priority"))
     issue.setkey('title')
 
+
+What you can't do to the schema
+-------------------------------
+
+You must never:
+
+**Remove the users class**
+  This class is the only *required* class in Roundup. Similarly, its
+  username, password and address properties must never be removed.
+
+**Change the type of a property**
+  Property types must *never* be changed - the database simply doesn't take
+  this kind of action into account. Note that you can't just remove a
+  property and re-add it as a new type either. If you wanted to make the
+  assignedto property a Multilink, you'd need to create a new property
+  assignedto_list and remove the old assignedto property.
+
+
+What you can do to the schema
+-----------------------------
+
+Your schema may be changed at any time before or after the tracker has been
+initialised (or used). You may:
+
+**Add new properties to classes, or add whole new classes**
+  This is painless and easy to do - there are generally no repurcussions
+  from adding new information to a tracker's schema.
+
+**Remove properties**
+  Removing properties is a little more tricky - you need to make sure that
+  the property is no longer used in the `web interface`_ *or* by the
+  detectors_.
+
+
+
 Classes and Properties - creating a new information store
 ---------------------------------------------------------
 
@@ -468,6 +503,15 @@ for you are:
   It also provides the ``presetunread`` auditor which pre-sets the
   status to ``unread`` on new items if the status isn't explicitly
   defined.
+**messagesummary.py**
+  Generates the ``summary`` property for new messages based on the message
+  content.
+**userauditor.py**
+  Verifies the content of some of the user fields (email addresses and
+  roles lists).
+
+If you don't want this default behaviour, you're completely free to change
+or remove these detectors.
 
 See the detectors section in the `design document`__ for details of the
 interface for detectors.
@@ -979,7 +1023,7 @@ Two special form values are supported for backwards compatibility:
     This is equivalent to::
 
         @link@messages=msg-1
-        @msg-1@content=value
+        msg-1@content=value
 
     except that in addition, the "author" and "date" properties of
     "msg-1" are set to the userid of the submitter, and the current
@@ -989,7 +1033,7 @@ Two special form values are supported for backwards compatibility:
     This is equivalent to::
 
         @link@files=file-1
-        @file-1@content=value
+        file-1@content=value
 
     The String content value is handled as described above for file
     uploads.
@@ -1473,65 +1517,68 @@ _value          the value of the property if any - this is the actual
 
 There are several methods available on these wrapper objects:
 
-========= ================================================================
-Method    Description
-========= ================================================================
-plain     render a "plain" representation of the property. This method
-          may take two arguments:
-
-          escape
-           If true, escape the text so it is HTML safe (default: no). The
-           reason this defaults to off is that text is usually escaped
-           at a later stage by the TAL commands, unless the "structure"
-           option is used in the template. The following ``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
@@ -1623,7 +1670,7 @@ you want access to the "user" class, for example, you would use::
   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`_.
@@ -1854,19 +1901,19 @@ template)::
 
  <table class="form">
  <tr>
-  <th nowrap>Title</th>
+  <th>Title</th>
   <td colspan="3" tal:content="structure python:context.title.field(size=60)">title</td>
  </tr>
  
  <tr>
-  <th nowrap>Priority</th>
+  <th>Priority</th>
   <td tal:content="structure context/priority/menu">priority</td>
-  <th nowrap>Status</th>
+  <th>Status</th>
   <td tal:content="structure context/status/menu">status</td>
  </tr>
  
  <tr>
-  <th nowrap>Superseder</th>
+  <th>Superseder</th>
   <td>
    <span tal:replace="structure python:context.superseder.field(showid=1, size=20)" />
    <span tal:replace="structure python:db.issue.classhelp('id,title')" />
@@ -1874,7 +1921,7 @@ template)::
     <br>View: <span tal:replace="structure python:context.superseder.link(showid=1)" />
    </span>
   </td>
-  <th nowrap>Nosy List</th>
+  <th>Nosy List</th>
   <td>
    <span tal:replace="structure context/nosy/field" />
    <span tal:replace="structure python:db.user.classhelp('username,realname,address,phone')" />
@@ -1882,7 +1929,7 @@ template)::
  </tr>
  
  <tr>
-  <th nowrap>Assigned To</th>
+  <th>Assigned To</th>
   <td tal:content="structure context/assignedto/menu">
    assignedto menu
   </td>
@@ -1891,14 +1938,14 @@ template)::
  </tr>
  
  <tr>
-  <th nowrap>Change Note</th>
+  <th>Change Note</th>
   <td colspan="3">
    <textarea name=":note" wrap="hard" rows="5" cols="60"></textarea>
   </td>
  </tr>
  
  <tr>
-  <th nowrap>File</th>
+  <th>File</th>
   <td colspan="3"><input type="file" name=":file" size="40"></td>
  </tr>
  
@@ -2329,7 +2376,7 @@ will be the "name" variable of the current context (which is
 to the form, a new category will be created with that name::
 
     <tr>
-     <th nowrap>Name</th>
+     <th>Name</th>
      <td tal:content="structure python:context.name.field(size=60)">
      name</td>
     </tr>
@@ -2361,19 +2408,20 @@ So putting it all together, and closing the table and form we get::
    <form method="POST" onSubmit="return submit_once()"
          enctype="multipart/form-data">
 
-    <input type="hidden" name="@required" value="name">
-
     <table class="form">
      <tr><th class="header" colspan="2">Category</th></tr>
 
      <tr>
-      <th nowrap>Name</th>
+      <th>Name</th>
       <td tal:content="structure python:context.name.field(size=60)">
       name</td>
      </tr>
 
      <tr>
-      <td>&nbsp;</td>
+      <td>
+        &nbsp;
+        <input type="hidden" name="@required" value="name"> 
+      </td>
       <td colspan="3" tal:content="structure context/submit">
        submit button will go here
       </td>
@@ -2402,7 +2450,7 @@ Just like ``category.issue.html`` this file defines a form which has a
 table to lay things out. It doesn't matter where in the table we add new
 stuff, it is entirely up to your sense of aesthetics::
 
-   <th nowrap>Category</th>
+   <th>Category</th>
    <td><span tal:replace="structure context/category/field" />
        <span tal:replace="structure db/category/classhelp" />
    </td>
@@ -2566,12 +2614,12 @@ issues to. You can do this by following these steps:
 4. in the ``issue.item.html`` template, change the status editing bit
    from::
 
-    <th nowrap>Status</th>
+    <th>Status</th>
     <td tal:content="structure context/status/menu">status</td>
 
    to::
 
-    <th nowrap>Status</th>
+    <th>Status</th>
     <td>
      <select tal:condition="context/id" name="status">
       <tal:block tal:define="ok context/status/transitions"
@@ -2601,7 +2649,7 @@ Alter the issue.item template section for messages to::
    <td><a tal:attributes="href string:msg${msg/id}"
           tal:content="string:msg${msg/id}"></a></td>
    <td tal:content="msg/author">author</td>
-   <td nowrap tal:content="msg/date/pretty">date</td>
+   <td class="date" tal:content="msg/date/pretty">date</td>
    <td tal:content="msg/summary">summary</td>
    <td>
     <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">
@@ -2708,11 +2756,11 @@ Setting up a "wizard" (or "druid") for controlled adding of issues
 
     <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()">
      <tr>
-      <th nowrap>Operating System</th>
+      <th>Operating System</th>
       <td tal:content="structure context/os/field"></td>
      </tr>
      <tr>
-      <th nowrap>Web Browser</th>
+      <th>Web Browser</th>
       <td tal:content="structure context/browser/field"></td>
      </tr>
     </tal:block>
@@ -2919,7 +2967,7 @@ be able to give a summary of the total time spent on a particular issue.
    field to capture a new timelog item's perdiod::
 
     <tr> 
-     <th nowrap>Time Log</th> 
+     <th>Time Log</th> 
      <td colspan=3><input type="text" name="timelog-1@period" /> 
       <br />(enter as '3y 1m 4d 2:40:02' or parts thereof) 
      </td> 
@@ -3177,7 +3225,7 @@ that shows either one or the other. We'll use a new form variable,
     <td><a tal:attributes="href string:msg${msg/id}"
            tal:content="string:msg${msg/id}"></a></td>
     <td tal:content="msg/author">author</td>
-    <td nowrap tal:content="msg/date/pretty">date</td>
+    <td class="date" tal:content="msg/date/pretty">date</td>
     <td tal:content="msg/summary">summary</td>
     <td>
      <a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>
@@ -3194,7 +3242,7 @@ that shows either one or the other. We'll use a new form variable,
    <tal:block tal:repeat="msg context/messages">
     <tr>
      <th tal:content="msg/author">author</th>
-     <th nowrap tal:content="msg/date/pretty">date</th>
+     <th class="date" tal:content="msg/date/pretty">date</th>
      <th style="text-align: right">
       (<a tal:attributes="href string:?@remove@messages=${msg/id}&@action=edit">remove</a>)
      </th>
@@ -3230,7 +3278,7 @@ resolved. To achieve this:
 2. Add the new "blockers" property to the issue.item edit page, using
    something like::
 
-    <th nowrap>Waiting On</th>
+    <th>Waiting On</th>
     <td>
      <span tal:replace="structure python:context.blockers.field(showid=1,
                                   size=20)" />
@@ -3337,6 +3385,190 @@ on it (i.e. it's in their blockers list) you can look at the journal
 history at the bottom of the issue page - look for a "link" event to
 another issue's "blockers" property.
 
+Add users to the nosy list based on the topic
+---------------------------------------------
+
+We need the ability to automatically add users to the nosy list based
+on the occurence of a topic. Every user should be allowed to edit his
+own list of topics for which he wants to be added to the nosy list.
+
+Below will be showed that such a change can be performed with only
+minimal understanding of the roundup system, but with clever use
+of Copy and Paste.
+
+This requires three changes to the tracker: a change in the database to
+allow per-user recording of the lists of topics for which he wants to
+be put on the nosy list, a change in the user view allowing to edit
+this list of topics, and addition of an auditor which updates the nosy
+list when a topic is set.
+
+Adding the nosy topic list
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The change in the database to make is that for any user there should be
+a list of topics for which he wants to be put on the nosy list. Adding
+a ``Multilink`` of ``keyword`` seem to fullfill this (note that within
+the code topics are called ``keywords``.) As such, all what has to be
+done is to add a new field to the definition of ``user`` within the
+file ``dbinit.py``.  We will call this new field ``nosy_keywords``, and
+the updated definition of user will be::
+
+    user = Class(db, "user", 
+                    username=String(),   password=Password(),
+                    address=String(),    realname=String(), 
+                    phone=String(),      organisation=String(),
+                    alternate_addresses=String(),
+                    queries=Multilink('query'), roles=String(),
+                    timezone=String(),
+                    nosy_keywords=Multilink('keyword'))
+Changing the user view to allow changing the nosy topic list
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We want any user to be able to change the list of topics for which
+he will by default be added to the nosy list. We choose to add this
+to the user view, as is generated by the file ``html/user.item.html``.
+We easily can
+see that the topic field in the issue view has very similar editting
+requirements as our nosy topics, both being a list of topics. As
+such, we search for Topics in ``issue.item.html``, and extract the
+associated parts from there. We add this to ``user.item.html`` at the 
+bottom of the list of viewed items (i.e. just below the 'Alternate
+E-mail addresses' in the classic template)::
+
+ <tr>
+  <th>Nosy Topics</th>
+  <td>
+  <span tal:replace="structure context/nosy_keywords/field" />
+  <span tal:replace="structure python:db.keyword.classhelp(property='nosy_keywords')" />
+  </td>
+ </tr>
+  
+
+Addition of an auditor to update the nosy list
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The more difficult part is the addition of the logic to actually
+at the users to the nosy list when it is required. 
+The choice is made to perform this action when the topics on an
+item are set, including when an item is created.
+Here we choose to start out with a copy of the 
+``detectors/nosyreaction.py`` detector, which we copy to the file
+``detectors/nosy_keyword_reaction.py``. 
+This looks like a good start as it also adds users
+to the nosy list. A look through the code reveals that the
+``nosyreaction`` function actually is sending the e-mail, which
+we do not need. As such, we can change the init function to::
+
+    def init(db):
+        db.issue.audit('create', update_kw_nosy)
+        db.issue.audit('set', update_kw_nosy)
+
+After that we rename the ``updatenosy`` function to ``update_kw_nosy``.
+The first two blocks of code in that function relate to settings
+``current`` to a combination of the old and new nosy lists. This
+functionality is left in the new auditor. The following block of
+code, which in ``updatenosy`` handled adding the assignedto user(s)
+to the nosy list, should be replaced by a block of code to add the
+interested users to the nosy list. We choose here to loop over all
+new topics, than loop over all users,
+and assign the user to the nosy list when the topic in the user's
+nosy_keywords. The next part in ``updatenosy``, adding the author
+and/or recipients of a message to the nosy list, obviously is not
+relevant here and thus is deleted from the new auditor. The last
+part, copying the new nosy list to newvalues, does not have to be changed.
+This brings the following function::
+
+    def update_kw_nosy(db, cl, nodeid, newvalues):
+        '''Update the nosy list for changes to the topics
+        '''
+        # nodeid will be None if this is a new node
+        current = {}
+        if nodeid is None:
+            ok = ('new', 'yes')
+        else:
+            ok = ('yes',)
+            # old node, get the current values from the node if they haven't
+            # changed
+            if not newvalues.has_key('nosy'):
+                nosy = cl.get(nodeid, 'nosy')
+                for value in nosy:
+                    if not current.has_key(value):
+                        current[value] = 1
+
+        # if the nosy list changed in this transaction, init from the new value
+        if newvalues.has_key('nosy'):
+            nosy = newvalues.get('nosy', [])
+            for value in nosy:
+                if not db.hasnode('user', value):
+                    continue
+                if not current.has_key(value):
+                    current[value] = 1
+
+        # add users with topic in nosy_keywords to the nosy list
+        if newvalues.has_key('topic') and newvalues['topic'] is not None:
+            topic_ids = newvalues['topic']
+            for topic in topic_ids:
+                # loop over all users,
+                # and assign user to nosy when topic in nosy_keywords
+                for user_id in db.user.list():
+                    nosy_kw = db.user.get(user_id, "nosy_keywords")
+                    found = 0
+                    for kw in nosy_kw:
+                        if kw == topic:
+                            found = 1
+                    if found:
+                        current[user_id] = 1
+
+        # that's it, save off the new nosy list
+        newvalues['nosy'] = current.keys()
+
+and these two function are the only ones needed in the file.
+
+TODO: update this example to use the find() Class method.
+
+Caveats
+~~~~~~~
+
+A few problems with the design here can be noted:
+
+Multiple additions
+    When a user, after automatic selection, is manually removed
+    from the nosy list, he again is added to the nosy list when the
+    topic list of the issue is updated. A better design might be
+    to only check which topics are new compared to the old list
+    of topics, and only add users when they have indicated
+    interest on a new topic.
+
+    The code could also be changed to only trigger on the create() event,
+    rather than also on the set() event, thus only setting the nosy list
+    when the issue is created.
+
+Scalability
+    In the auditor there is a loop over all users. For a site with
+    only few users this will pose no serious problem, however, with
+    many users this will be a serious performance bottleneck.
+    A way out will be to link from the topics to the users which
+    selected these topics a nosy topics. This will eliminate the
+    loop over all users.
+
+
+Adding action links to the index page
+-------------------------------------
+
+Add a column to the item.index.html template.
+
+Resolving the issue::
+
+  <a tal:attributes="href
+     string:issue${i/id}?:status=resolved&:action=edit">resolve</a>
+
+"Take" the issue::
+
+  <a tal:attributes="href
+     string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a>
+
+... and so on
 
 -------------------