Code

- document experience from release(s)
[roundup.git] / doc / design.txt
index 67f82f1bdeb9cba5e1c2c4d339582c286c8e40ce..0748fa29183de290805d49dcb868ba150f0cd292 100644 (file)
@@ -2,9 +2,7 @@
 Roundup - An Issue-Tracking System for Knowledge Workers
 ========================================================
 
-:Authors: Ka-Ping Yee (original__), Richard Jones (implementation)
-
-__ spec.html
+:Authors: Ka-Ping Yee (original), Richard Jones (implementation)
 
 .. contents::
 
@@ -14,10 +12,12 @@ Introduction
 This document presents a description of the components of the Roundup
 system and specifies their interfaces and behaviour in sufficient detail
 to guide an implementation. For the philosophy and rationale behind the
-Roundup design, see the first-round Software Carpentry submission for
-Roundup. This document fleshes out that design as well as specifying
+Roundup design, see the first-round Software Carpentry `submission for
+Roundup`__. This document fleshes out that design as well as specifying
 interfaces so that the components can be developed separately.
 
+__ spec.html
+
 
 The Layer Cake
 -----------------
@@ -26,17 +26,17 @@ Lots of software design documents come with a picture of a cake.
 Everybody seems to like them.  I also like cakes (i think they are
 tasty).  So I, too, shall include a picture of a cake here::
 
-     _________________________________________________________________________
-    |  E-mail Client   |   Web Browser   |   Detector Scripts   |    Shell    |
-    |------------------+-----------------+----------------------+-------------|
-    |   E-mail User    |    Web User     |      Detector        |   Command   | 
-    |-------------------------------------------------------------------------|
-    |                         Roundup Database Layer                          |
-    |-------------------------------------------------------------------------|
-    |                          Hyperdatabase Layer                            |
-    |-------------------------------------------------------------------------|
-    |                             Storage Layer                               |
-     -------------------------------------------------------------------------
+     ________________________________________________________________
+    | E-mail Client |  Web Browser  |  Detector Scripts  |   Shell   |
+    |---------------+---------------+--------------------+-----------|
+    |  E-mail User  |   Web User    |     Detector       |  Command  | 
+    |----------------------------------------------------------------|
+    |                    Roundup Database Layer                      |
+    |----------------------------------------------------------------|
+    |                     Hyperdatabase Layer                        |
+    |----------------------------------------------------------------|
+    |                        Storage Layer                           |
+     ----------------------------------------------------------------
 
 The colourful parts of the cake are part of our system; the faint grey
 parts of the cake are external components.
@@ -50,13 +50,15 @@ Hyperdatabase
 -------------
 
 The lowest-level component to be implemented is the hyperdatabase. The
-hyperdatabase is intended to be a flexible data store that can hold
-configurable data in records which we call items.
+hyperdatabase is a flexible data store that can hold configurable data
+in records which we call items.
 
 The hyperdatabase is implemented on top of the storage layer, an
-external module for storing its data.  The storage layer could be a
-third-party RDBMS; for a "batteries-included" distribution, implementing
-the hyperdatabase on the standard bsddb module is suggested.
+external module for storing its data. The "batteries-includes" distribution
+implements the hyperdatabase on the standard anydbm module.  The storage
+layer could be a third-party RDBMS; for a low-maintenance solution,
+implementing the hyperdatabase on the SQLite RDBMS is suggested.
+
 
 Dates and Date Arithmetic
 ~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -122,7 +124,8 @@ Here is an outline of the Date and Interval classes::
 
     class Date:
         def __init__(self, spec, offset):
-            """Construct a date given a specification and a time zone offset.
+            """Construct a date given a specification and a time zone
+            offset.
 
             'spec' is a full date or a partial form, with an optional
             added or subtracted interval.  'offset' is the local time
@@ -133,16 +136,22 @@ Here is an outline of the Date and Interval classes::
             """Add an interval to this date to produce another date."""
 
         def __sub__(self, interval):
-            """Subtract an interval from this date to produce another date."""
+            """Subtract an interval from this date to produce another
+            date.
+            """
 
         def __cmp__(self, other):
             """Compare this date to another date."""
 
         def __str__(self):
-            """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
+            """Return this date as a string in the yyyy-mm-dd.hh:mm:ss
+            format.
+            """
 
         def local(self, offset):
-            """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone."""
+            """Return this date as yyyy-mm-dd.hh:mm:ss in a local time
+            zone.
+            """
 
     class Interval:
         def __init__(self, spec):
@@ -179,6 +188,7 @@ and the current local time is 19:34:02 on 25 June 2000::
     >>> Date(". + 2d") - Interval("3w")
     <Date 2000-06-07.00:34:02>
 
+
 Items and Classes
 ~~~~~~~~~~~~~~~~~
 
@@ -187,6 +197,7 @@ presented as the key-value pairs of a dictionary. Each item belongs to a
 class which defines the names and types of its properties.  The database
 permits the creation and modification of classes as well as items.
 
+
 Identifiers and Designators
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -198,11 +209,12 @@ class concatenated with the item's numeric identifier.
 
 For example, if "spam" and "eggs" are classes, the first item created in
 class "spam" has id 1 and designator "spam1". The first item created in
-class "eggs" also has id 1 but has the distinct designator "eggs1".
-Item designators are conventionally enclosed in square brackets when
+class "eggs" also has id 1 but has the distinct designator "eggs1". Item
+designators are conventionally enclosed in square brackets when
 mentioned in plain text.  This permits a casual mention of, say,
 "[patch37]" in an e-mail message to be turned into an active hyperlink.
 
+
 Property Names and Types
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -232,6 +244,7 @@ attempt to store None into a Multilink property stores an empty list.
 A property that is not specified will return as None from a *get*
 operation.
 
+
 Hyperdb Interface Specification
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -279,7 +292,9 @@ properties belong in classes::
 Here is the interface provided by the hyperdatabase::
 
     class Database:
-        """A database for storing records containing flexible data types."""
+        """A database for storing records containing flexible data
+        types.
+        """
 
         def __init__(self, config, journaltag=None):
             """Open a hyperdatabase given a specifier to some storage.
@@ -306,18 +321,31 @@ Here is the interface provided by the hyperdatabase::
         def getclass(self, classname):
             """Get the Class object representing a particular class.
 
-            If 'classname' is not a valid class name, a KeyError is raised.
+            If 'classname' is not a valid class name, a KeyError is
+            raised.
             """
 
     class Class:
-        """The handle to a particular class of items in a hyperdatabase."""
+        """The handle to a particular class of items in a hyperdatabase.
+        """
 
         def __init__(self, db, classname, **properties):
-            """Create a new class with a given name and property specification.
-
-            'classname' must not collide with the name of an existing class,
-            or a ValueError is raised.  The keyword arguments in 'properties'
-            must map names to property objects, or a TypeError is raised.
+            """Create a new class with a given name and property
+            specification.
+
+            'classname' must not collide with the name of an existing
+            class, or a ValueError is raised.  The keyword arguments in
+            'properties' must map names to property objects, or a
+            TypeError is raised.
+
+            A proxied reference to the database is available as the
+            'db' attribute on instances. For example, in
+            'IssueClass.send_message', the following is used to lookup
+            users, messages and files::
+
+                users = self.db.user
+                messages = self.db.msg
+                files = self.db.file
             """
 
         # Editing items:
@@ -325,156 +353,197 @@ Here is the interface provided by the hyperdatabase::
         def create(self, **propvalues):
             """Create a new item of this class and return its id.
 
-            The keyword arguments in 'propvalues' map property names to values.
-            The values of arguments must be acceptable for the types of their
-            corresponding properties or a TypeError is raised.  If this class
-            has a key property, it must be present and its value must not
-            collide with other key strings or a ValueError is raised.  Any other
-            properties on this class that are missing from the 'propvalues'
-            dictionary are set to None.  If an id in a link or multilink
-            property does not refer to a valid item, an IndexError is raised.
+            The keyword arguments in 'propvalues' map property names to
+            values. The values of arguments must be acceptable for the
+            types of their corresponding properties or a TypeError is
+            raised.  If this class has a key property, it must be
+            present and its value must not collide with other key
+            strings or a ValueError is raised.  Any other properties on
+            this class that are missing from the 'propvalues' dictionary
+            are set to None.  If an id in a link or multilink property
+            does not refer to a valid item, an IndexError is raised.
             """
 
         def get(self, itemid, propname):
-            """Get the value of a property on an existing item of this class.
+            """Get the value of a property on an existing item of this
+            class.
 
-            'itemid' must be the id of an existing item of this class or an
-            IndexError is raised.  'propname' must be the name of a property
-            of this class or a KeyError is raised.
+            'itemid' must be the id of an existing item of this class or
+            an IndexError is raised.  'propname' must be the name of a
+            property of this class or a KeyError is raised.
             """
 
         def set(self, itemid, **propvalues):
             """Modify a property on an existing item of this class.
             
-            'itemid' must be the id of an existing item of this class or an
-            IndexError is raised.  Each key in 'propvalues' must be the name
-            of a property of this class or a KeyError is raised.  All values
-            in 'propvalues' must be acceptable types for their corresponding
-            properties or a TypeError is raised.  If the value of the key
-            property is set, it must not collide with other key strings or a
-            ValueError is raised.  If the value of a Link or Multilink
-            property contains an invalid item id, a ValueError is raised.
+            'itemid' must be the id of an existing item of this class or
+            an IndexError is raised.  Each key in 'propvalues' must be
+            the name of a property of this class or a KeyError is
+            raised.  All values in 'propvalues' must be acceptable types
+            for their corresponding properties or a TypeError is raised.
+            If the value of the key property is set, it must not collide
+            with other key strings or a ValueError is raised.  If the
+            value of a Link or Multilink property contains an invalid
+            item id, a ValueError is raised.
             """
 
         def retire(self, itemid):
             """Retire an item.
             
-            The properties on the item remain available from the get() method,
-            and the item's id is never reused.  Retired items are not returned
-            by the find(), list(), or lookup() methods, and other items may
-            reuse the values of their key properties.
+            The properties on the item remain available from the get()
+            method, and the item's id is never reused.  Retired items
+            are not returned by the find(), list(), or lookup() methods,
+            and other items may reuse the values of their key
+            properties.
             """
 
         def restore(self, nodeid):
         '''Restore a retired node.
 
-        Make node available for all operations like it was before retirement.
+        Make node available for all operations like it was before
+        retirement.
         '''
 
         def history(self, itemid):
             """Retrieve the journal of edits on a particular item.
 
-            'itemid' must be the id of an existing item of this class or an
-            IndexError is raised.
+            'itemid' must be the id of an existing item of this class or
+            an IndexError is raised.
 
             The returned list contains tuples of the form
 
                 (date, tag, action, params)
 
-            'date' is a Timestamp object specifying the time of the change and
-            'tag' is the journaltag specified when the database was opened.
-            'action' may be:
+            'date' is a Timestamp object specifying the time of the
+            change and 'tag' is the journaltag specified when the
+            database was opened. 'action' may be:
 
-                'create' or 'set' -- 'params' is a dictionary of property values
-                'link' or 'unlink' -- 'params' is (classname, itemid, propname)
+                'create' or 'set' -- 'params' is a dictionary of
+                    property values
+                'link' or 'unlink' -- 'params' is (classname, itemid,
+                    propname)
                 'retire' -- 'params' is None
             """
 
         # Locating items:
 
         def setkey(self, propname):
-            """Select a String property of this class to be the key property.
+            """Select a String property of this class to be the key
+            property.
 
-            'propname' must be the name of a String property of this class or
-            None, or a TypeError is raised.  The values of the key property on
-            all existing items must be unique or a ValueError is raised.
+            'propname' must be the name of a String property of this
+            class or None, or a TypeError is raised.  The values of the
+            key property on all existing items must be unique or a
+            ValueError is raised.
             """
 
         def getkey(self):
-            """Return the name of the key property for this class or None."""
+            """Return the name of the key property for this class or
+            None.
+            """
 
         def lookup(self, keyvalue):
-            """Locate a particular item by its key property and return its id.
+            """Locate a particular item by its key property and return
+            its id.
 
-            If this class has no key property, a TypeError is raised.  If the
-            'keyvalue' matches one of the values for the key property among
-            the items in this class, the matching item's id is returned;
-            otherwise a KeyError is raised.
+            If this class has no key property, a TypeError is raised.
+            If the 'keyvalue' matches one of the values for the key
+            property among the items in this class, the matching item's
+            id is returned; otherwise a KeyError is raised.
             """
 
-        def find(self, propname, itemid):
-            """Get the ids of items in this class which link to the given items.
+        def find(self, **propspec):
+            """Get the ids of items in this class which link to the
+            given items.
 
-            'propspec' consists of keyword args propname={itemid:1,}   
-            'propname' must be the name of a property in this class, or a
-            KeyError is raised.  That property must be a Link or Multilink
-            property, or a TypeError is raised.
+            'propspec' consists of keyword args propname=itemid or
+                       propname={<itemid 1>:1, <itemid 2>: 1, ...}
+            'propname' must be the name of a property in this class,
+                       or a KeyError is raised.  That property must
+                       be a Link or Multilink property, or a TypeError
+                       is raised.
 
-            Any item in this class whose 'propname' property links to any of the
-            itemids will be returned. Used by the full text indexing, which
-            knows that "foo" occurs in msg1, msg3 and file7, so we have hits
-            on these issues:
+            Any item in this class whose 'propname' property links to
+            any of the itemids will be returned. Examples::
 
+                db.issue.find(messages='1')
                 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
             """
 
         def filter(self, search_matches, filterspec, sort, group):
-            ''' Return a list of the ids of the active items in this class that
-                match the 'filter' spec, sorted by the group spec and then the
-                sort spec.
-            '''
+            """Return a list of the ids of the active nodes in this class that
+            match the 'filter' spec, sorted by the group spec and then the
+            sort spec.
+
+            "search_matches" is a container type
+
+            "filterspec" is {propname: value(s)}
+
+            "sort" and "group" are [(dir, prop), ...] where dir is '+', '-'
+            or None and prop is a prop name or None. Note that for
+            backward-compatibility reasons a single (dir, prop) tuple is
+            also allowed.
+
+            The filter must match all properties specificed. If the property
+            value to match is a list:
+
+            1. String properties must match all elements in the list, and
+            2. Other properties must match any of the elements in the list.
+
+            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, e.g. you can search for all issues where
+            a message was added by a certain user in the last week with a
+            filterspec of
+            {'messages.author' : '42', 'messages.creation' : '.-1w;'}
+            """
 
         def list(self):
-            """Return a list of the ids of the active items in this class."""
+            """Return a list of the ids of the active items in this
+            class.
+            """
 
         def count(self):
             """Get the number of items in this class.
 
-            If the returned integer is 'numitems', the ids of all the items
-            in this class run from 1 to numitems, and numitems+1 will be the
-            id of the next item to be created in this class.
+            If the returned integer is 'numitems', the ids of all the
+            items in this class run from 1 to numitems, and numitems+1
+            will be the id of the next item to be created in this class.
             """
 
         # Manipulating properties:
 
         def getprops(self):
-            """Return a dictionary mapping property names to property objects."""
+            """Return a dictionary mapping property names to property
+            objects.
+            """
 
         def addprop(self, **properties):
             """Add properties to this class.
 
-            The keyword arguments in 'properties' must map names to property
-            objects, or a TypeError is raised.  None of the keys in 'properties'
-            may collide with the names of existing properties, or a ValueError
-            is raised before any properties have been added.
+            The keyword arguments in 'properties' must map names to
+            property objects, or a TypeError is raised.  None of the
+            keys in 'properties' may collide with the names of existing
+            properties, or a ValueError is raised before any properties
+            have been added.
             """
 
         def getitem(self, itemid, cache=1):
-            ''' Return a Item convenience wrapper for the item.
+            """ Return a Item convenience wrapper for the item.
 
-            'itemid' must be the id of an existing item of this class or an
-            IndexError is raised.
+            'itemid' must be the id of an existing item of this class or
+            an IndexError is raised.
 
-            'cache' indicates whether the transaction cache should be queried
-            for the item. If the item has been modified and you need to
-            determine what its values prior to modification are, you need to
-            set cache=0.
-            '''
+            'cache' indicates whether the transaction cache should be
+            queried for the item. If the item has been modified and you
+            need to determine what its values prior to modification are,
+            you need to set cache=0.
+            """
 
     class Item:
-        ''' A convenience wrapper for the given item. It provides a mapping
-            interface to a single item's properties
-        '''
+        """ A convenience wrapper for the given item. It provides a
+        mapping interface to a single item's properties
+        """
 
 Hyperdatabase Implementations
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -485,7 +554,7 @@ mechanism. Examples are relational databases, \*dbm key-value databases,
 and so on.
 
 Several implementations are provided - they belong in the
-roundup.backends package.
+``roundup.backends`` package.
 
 
 Application Example
@@ -530,7 +599,8 @@ practice::
     4
     >>> db.issue.create(title="abuse", status=1)
     5
-    >>> hyperdb.Class(db, "user", username=hyperdb.Key(), password=hyperdb.String())
+    >>> hyperdb.Class(db, "user", username=hyperdb.String(),
+    ... password=hyperdb.String())
     <hyperdb.Class "user">
     >>> db.issue.addprop(fixer=hyperdb.Link("user"))
     >>> db.issue.getprops()
@@ -546,7 +616,8 @@ practice::
     >>> db.issue.find("status", db.status.lookup("in-progress"))
     [2, 4, 5]
     >>> db.issue.history(5)
-    [(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse", "status": 1}),
+    [(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse",
+    "status": 1}),
      (<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})]
     >>> db.status.history(1)
     [(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")),
@@ -571,6 +642,7 @@ database are considered issue classes. The Roundup database layer adds
 detectors and user items, and on issues it provides mail spools, nosy
 lists, and superseders.
 
+
 Reserved Classes
 ~~~~~~~~~~~~~~~~
 
@@ -612,6 +684,7 @@ must exist in the hyperdatabase for any messages that are stored in the
 system). The "summary" property contains a summary of the message for
 display in a message index.
 
+
 Files
 """""
 
@@ -628,6 +701,7 @@ The "user" property indicates the user who submitted the file, the
 "name" property holds the original name of the file, and the "type"
 property holds the MIME type of the file as received.
 
+
 Issue Classes
 ~~~~~~~~~~~~~
 
@@ -644,13 +718,16 @@ superseder  hyperdb.Multilink("issue")
 =========== ==========================
 
 Also, two Date properties named "creation" and "activity" are fabricated
-by the Roundup database layer.  By "fabricated" we mean that no such
+by the Roundup database layer. Two user Link properties, "creator" and
+"actor" are also fabricated. By "fabricated" we mean that no such
 properties are actually stored in the hyperdatabase, but when properties
-on issues are requested, the "creation" and "activity" properties are
-made available. The value of the "creation" property is the date when an
-issue was created, and the value of the "activity" property is the date
-when any property on the issue was last edited (equivalently, these are
-the dates on the first and last records in the issue's journal).
+on issues are requested, the "creation"/"creator" and "activity"/"actor"
+properties are made available. The value of the "creation"/"creator"
+properties relate to issue creation, and the value of the "activity"/
+"actor" properties relate to the last editing of any property on the issue
+(equivalently, these are the dates on the first and last records in the
+issue's journal).
+
 
 Roundupdb Interface Specification
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -670,16 +747,10 @@ hyperdatabase, except for the following changes and additional methods::
         def set(self, **propvalues):
         def retire(self, itemid):
             """These operations trigger detectors and can be vetoed.
-            Attempts to modify the "creation" or "activity" properties
-            cause a KeyError.
+            Attempts to modify the "creation", "creator", "activity"
+            properties or "actor" cause a KeyError.
             """
 
-        # New methods:
-
-        def audit(self, event, detector):
-        def react(self, event, detector):
-            """Register a detector (see below for more details)."""
-
     class IssueClass(Class):
         # Overridden methods:
 
@@ -687,13 +758,14 @@ hyperdatabase, except for the following changes and additional methods::
             """The newly-created class automatically includes the
             "messages", "files", "nosy", and "superseder" properties.
             If the 'properties' dictionary attempts to specify any of
-            these properties or a "creation" or "activity" property, a
-            ValueError is raised."""
+            these properties or a "creation", "creator", "activity" or
+            "actor" property, a ValueError is raised."""
 
         def get(self, itemid, propname):
         def getprops(self):
             """In addition to the actual properties on the item, these
-            methods provide the "creation" and "activity" properties."""
+            methods provide the "creation", "creator", "activity" and
+            "actor" properties."""
 
         # New methods:
 
@@ -723,7 +795,8 @@ Default Schema
 The default schema included with Roundup turns it into a typical
 software bug tracker.  The database is set up like this::
 
-    pri = Class(db, "priority", name=hyperdb.String(), order=hyperdb.String())
+    pri = Class(db, "priority", name=hyperdb.String(),
+                order=hyperdb.String())
     pri.setkey("name")
     pri.create(name="critical", order="1")
     pri.create(name="urgent", order="2")
@@ -731,7 +804,8 @@ software bug tracker.  The database is set up like this::
     pri.create(name="feature", order="4")
     pri.create(name="wish", order="5")
 
-    stat = Class(db, "status", name=hyperdb.String(), order=hyperdb.String())
+    stat = Class(db, "status", name=hyperdb.String(),
+                 order=hyperdb.String())
     stat.setkey("name")
     stat.create(name="unread", order="1")
     stat.create(name="deferred", order="2")
@@ -745,7 +819,7 @@ software bug tracker.  The database is set up like this::
     Class(db, "keyword", name=hyperdb.String())
 
     Class(db, "issue", fixer=hyperdb.Multilink("user"),
-                       topic=hyperdb.Multilink("keyword"),
+                       keyword=hyperdb.Multilink("keyword"),
                        priority=hyperdb.Link("priority"),
                        status=hyperdb.Link("status"))
 
@@ -758,7 +832,8 @@ addition of a convenience function like Choice for setting up the
 "priority" and "status" classes::
 
     def Choice(name, *options):
-        cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
+        cl = Class(db, name, name=hyperdb.String(),
+                   order=hyperdb.String())
         for i in range(len(options)):
             cl.create(name=option[i], order=i)
         return hyperdb.Link(name)
@@ -788,6 +863,7 @@ If none of the auditors raises an exception, the database proceeds to
 carry out the operation.  After it's done, it then calls all of the
 *reactors* that have been registered for the operation.
 
+
 Detector Interface Specification
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -795,20 +871,22 @@ The ``audit()`` and ``react()`` methods register detectors on a given
 class of items::
 
     class Class:
-        def audit(self, event, detector):
+        def audit(self, event, detector, priority=100):
             """Register an auditor on this class.
 
             'event' should be one of "create", "set", "retire", or
             "restore". 'detector' should be a function accepting four
-            arguments.
+            arguments. Detectors are called in priority order, execution
+            order is undefined for detectors with the same priority.
             """
 
-        def react(self, event, detector):
+        def react(self, event, detector, priority=100):
             """Register a reactor on this class.
 
             'event' should be one of "create", "set", "retire", or
             "restore". 'detector' should be a function accepting four
-            arguments.
+            arguments. Detectors are called in priority order, execution
+            order is undefined for detectors with the same priority.
             """
 
 Auditors are called with the arguments::
@@ -845,6 +923,7 @@ 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.
 
+
 Detector Example
 ~~~~~~~~~~~~~~~~
 
@@ -864,18 +943,20 @@ proceeds when it has three approvals::
             new = newdata["approvals"]
             for uid in old:
                 if uid not in new and uid != db.getuid():
-                    raise Reject, "You can't remove other users from the "
-                        "approvals list; you can only remove yourself."
+                    raise Reject, "You can't remove other users from " \
+                        "the approvals list; you can only remove " \
+                        "yourself."
             for uid in new:
                 if uid not in old and uid != db.getuid():
-                    raise Reject, "You can't add other users to the approvals "
-                        "list; you can only add yourself."
+                    raise Reject, "You can't add other users to the " \
+                        "approvals list; you can only add yourself."
 
-    # When three people have approved a project, change its
-    # status from "pending" to "approved".
+    # When three people have approved a project, change its status from
+    # "pending" to "approved".
 
     def approve_project(db, cl, id, olddata):
-        if olddata.has_key("approvals") and len(cl.get(id, "approvals")) == 3:
+        if (olddata.has_key("approvals") and 
+            len(cl.get(id, "approvals")) == 3):
             if cl.get(id, "status") == db.status.lookup("pending"):
                 cl.set(id, status=db.status.lookup("approved"))
 
@@ -890,7 +971,8 @@ ensure that there are text/plain attachments on the message.  The
 maintainer of the package can then apply the patch by setting its status
 to "applied"::
 
-    # Only accept attempts to create new patches that come with patch files.
+    # Only accept attempts to create new patches that come with patch
+    # files.
 
     def check_new_patch(db, cl, id, newdata):
         if not newdata["files"]:
@@ -898,13 +980,15 @@ to "applied"::
                           "attaching a patch file."
         for fileid in newdata["files"]:
             if db.file.get(fileid, "type") != "text/plain":
-                raise Reject, "Submitted patch files must be text/plain."
+                raise Reject, "Submitted patch files must be " \
+                              "text/plain."
 
-    # When the status is changed from "approved" to "applied", apply the patch.
+    # When the status is changed from "approved" to "applied", apply the
+    # patch.
 
     def apply_patch(db, cl, id, olddata):
-        if cl.get(id, "status") == db.status.lookup("applied") and \
-            olddata["status"] == db.status.lookup("approved"):
+        if (cl.get(id, "status") == db.status.lookup("applied") and 
+            olddata["status"] == db.status.lookup("approved")):
             # ...apply the patch...
 
     def init(db):
@@ -920,10 +1004,11 @@ only for quick searches and checks from the shell prompt. (Anything more
 interesting can simply be written in Python using the Roundup database
 module.)
 
+
 Command Interface Specification
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-A single command, roundup, provides basic access to the hyperdatabase
+A single command, ``roundup-admin``, provides basic access to the hyperdatabase
 from the command line::
 
     roundup-admin help
@@ -954,14 +1039,16 @@ the printed results:
   are both accepted; an empty string, a single item, or a list of items
   joined by commas is accepted.
 
-When multiple items are specified to the roundup get or roundup set
+When multiple items are specified to the roundup-admin get or roundup-admin set
 commands, the specified properties are retrieved or set on all the
 listed items.
 
-When multiple results are returned by the roundup get or roundup find
+When multiple results are returned by the roundup-admin get or
+roundup-admin find
 commands, they are printed one per line (default) or joined by commas
 (with the -list) option.
 
+
 Usage Example
 ~~~~~~~~~~~~~
 
@@ -969,8 +1056,8 @@ To find all messages regarding in-progress issues that contain the word
 "spam", for example, you could execute the following command from the
 directory where the database dumps its files::
 
-    shell% for issue in `roundup find issue status=in-progress`; do
-    > grep -l spam `roundup get $issue messages`
+    shell% for issue in `roundup-admin find issue status=in-progress`; do
+    > grep -l spam `roundup-admin get $issue messages`
     > done
     msg23
     msg49
@@ -980,8 +1067,8 @@ directory where the database dumps its files::
 
 Or, using the -list option, this can be written as a single command::
 
-    shell% grep -l spam `roundup get \
-        \`roundup find -list issue status=in-progress\` messages`
+    shell% grep -l spam `roundup-admin get \
+        \`roundup-admin find -list issue status=in-progress\` messages`
     msg23
     msg49
     msg50
@@ -997,6 +1084,7 @@ receive mail.  Messages should be piped to the Roundup mail-handling
 script by the mail delivery system (e.g. using an alias beginning with
 "|" for sendmail).
 
+
 Message Processing
 ~~~~~~~~~~~~~~~~~~
 
@@ -1049,6 +1137,7 @@ we are calling the create() method to create a new item).  If an auditor
 raises an exception, the original message is bounced back to the sender
 with the explanatory message given in the exception.
 
+
 Nosy Lists
 ~~~~~~~~~~
 
@@ -1061,13 +1150,14 @@ message are never sent to the same user.  The journal recorded by the
 hyperdatabase on the "recipients" property then provides a log of when
 the message was sent to whom.
 
+
 Setting Properties
 ~~~~~~~~~~~~~~~~~~
 
 The e-mail interface also provides a simple way to set properties on
 issues.  At the end of the subject line, ``propname=value`` pairs can be
 specified in square brackets, using the same conventions as for the
-roundup ``set`` shell command.
+roundup-admin ``set`` shell command.
 
 
 Web User Interface
@@ -1083,6 +1173,7 @@ containing mostly HTML.  Among the HTML tags in templates are
 interspersed some nonstandard tags, which we use as placeholders to be
 replaced by properties and their values.
 
+
 Views and View Specifiers
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1099,6 +1190,7 @@ result of selecting a link or submitting a form takes the user to a new
 view, the Web browser should be redirected to a canonical location
 containing a complete view specifier so that the view can be bookmarked.
 
+
 Displaying Properties
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -1151,6 +1243,7 @@ An index view contains two sections: a filter section and an index
 section. The filter section provides some widgets for selecting which
 issues appear in the index.  The index section is a table of issues.
 
+
 Index View Specifiers
 """""""""""""""""""""
 
@@ -1158,10 +1251,10 @@ An index view specifier looks like this (whitespace has been added for
 clarity)::
 
     /issue?status=unread,in-progress,resolved&
-        topic=security,ui&
-        :group=priority&
+        keyword=security,ui&
+        :group=priority,-status&
         :sort=-activity&
-        :filters=status,topic&
+        :filters=status,keyword&
         :columns=title,status,fixer
 
 
@@ -1182,12 +1275,12 @@ of issues with values matching any specified Multilink properties.
 
 The example specifies an index of "issue" items. Only issues with a
 "status" of either "unread" or "in-progres" or "resolved" are displayed,
-and only issues with "topic" values including both "security" and "ui"
-are displayed.  The issues 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.
+and only issues 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.
 
 Associated with each issue class is a default layout specifier.  The
 layout specifier in the above example is the default layout to be
@@ -1250,7 +1343,8 @@ Here's an example of a basic editor template::
 
     <table>
     <tr>
-        <td colspan=2 tal:content="python:context.title.field(size='60')"></td>
+        <td colspan=2
+            tal:content="python:context.title.field(size='60')"></td>
     </tr>
     <tr>
         <td tal:content="context/fixer/field"></td>
@@ -1287,6 +1381,7 @@ description.  The message is then added to the issue's message spool
 (thus triggering the standard detector to react by sending out this
 message to the nosy list).
 
+
 Spool Section
 """""""""""""
 
@@ -1309,11 +1404,12 @@ this path, and allow the multiple assignment of Roles to Users, and
 multiple Permissions to Roles. These definitions are not persistent -
 they're defined when the application initialises.
 
-There will be two levels of Permission. The Class level permissions
+There will be three levels of Permission. The Class level permissions
 define logical permissions associated with all items of a particular
 class (or all classes). The Item level permissions define logical
 permissions associated with specific items by way of their user-linked
-properties.
+properties. The Property level permissions define logical permissions
+associated with a specific property of an item.
 
 
 Access Control Interface Specification
@@ -1326,11 +1422,20 @@ The security module defines::
             - name
             - description
             - klass (optional)
+            - properties (optional)
+            - check function (optional)
 
             The klass may be unset, indicating that this permission is
             not locked to a particular hyperdb class. There may be
             multiple Permissions for the same name for different
             classes.
+
+            If property names are set, permission is restricted to those
+            properties only.
+
+            If check function is set, permission is granted only when
+            the function returns value interpreted as boolean true.
+            The function is called with arguments db, userid, itemid.
         '''
 
     class Role:
@@ -1346,36 +1451,41 @@ The security module defines::
                 the base roles (for admin user).
             '''
 
-        def getPermission(self, permission, classname=None):
-            ''' Find the Permission matching the name and for the class,
-                if the classname is specified.
+        def getPermission(self, permission, classname=None, properties=None,
+                check=None):
+            ''' Find the Permission exactly matching the name, class,
+                properties list and check function.
 
                 Raise ValueError if there is no exact match.
             '''
 
-        def hasPermission(self, permission, userid, classname=None):
+        def hasPermission(self, permission, userid, classname=None,
+                property=None, itemid=None):
             ''' Look through all the Roles, and hence Permissions, and
-                see if "permission" is there for the specified
-                classname.
-            '''
+                see if "permission" exists given the constraints of
+                classname, property and itemid.
 
-        def hasItemPermission(self, classname, itemid, **propspec):
-            ''' Check the named properties of the given item to see if
-                the userid appears in them. If it does, then the user is
-                granted this permission check.
+                If classname is specified (and only classname) then the
+                search will match if there is *any* Permission for that
+                classname, even if the Permission has additional
+                constraints.
 
-                'propspec' consists of a set of properties and values
-                that must be present on the given item for access to be
-                granted.
+                If property is specified, the Permission matched must have
+                either no properties listed or the property must appear in
+                the list.
 
-                If a property is a Link, the value must match the
-                property value. If a property is a Multilink, the value
-                must appear in the Multilink list.
+                If itemid is specified, the Permission matched must have
+                either no check function defined or the check function,
+                when invoked, must return a True value.
+
+                Note that this functionality is actually implemented by the
+                Permission.test() method.
             '''
 
         def addPermission(self, **propspec):
             ''' Create a new Permission with the properties defined in
-                'propspec'
+                'propspec'. See the Permission class for the possible
+                keyword args.
             '''
 
         def addRole(self, **propspec):
@@ -1441,7 +1551,8 @@ to perform some action::
     if db.security.hasPermission('issue', 'Edit', userid):
         # all ok
 
-    if db.security.hasItemPermission('issue', itemid, assignedto=userid):
+    if db.security.hasItemPermission('issue', itemid,
+                                     assignedto=userid):
         # all ok
 
 Code in the core will make use of these methods, as should code in
@@ -1473,9 +1584,9 @@ submission to their user details.
 Anonymous Users
 ~~~~~~~~~~~~~~~
 
-The "anonymous" user must always exist, and defines the access permissions for
-anonymous users. Unknown users accessing Roundup through the web or email
-interfaces will be logged in as the "anonymous" user.
+The "anonymous" user must always exist, and defines the access
+permissions for anonymous users. Unknown users accessing Roundup through
+the web or email interfaces will be logged in as the "anonymous" user.
 
 
 Use Cases
@@ -1530,6 +1641,7 @@ suggestions to this paper and motivating me to get it done, and to Jesse
 Vincent, Mark Miller, Christopher Simons, Jeff Dunmall, Wayne Gramlich,
 and Dean Tribble for their assistance with the first-round submission.
 
+
 Changes to this document
 ------------------------
 
@@ -1539,11 +1651,7 @@ Changes to this document
   nature of the Class.
 - New Templating
 - Access Controls
+- Added "actor" property
 
-------------------
-
-Back to `Table of Contents`_
-
-.. _`Table of Contents`: index.html
 .. _customisation: customizing.html