diff doc/design.txt @ 1711:3c3e44aacdb4

Documentation fixes. doc/customizing.txt, doc/design.txt Documented 'db.curuserid'. doc/design.txt Reflowed to 72 columns (even the layer cake fits :) roundup/mailgw.py Strip '\n' introduced by rfc822.readheaders
author Jean Jordaan <neaj@users.sourceforge.net>
date Tue, 24 Jun 2003 12:39:20 +0000
parents eb3c348676ed
children 84c61e912079
line wrap: on
line diff
--- a/doc/design.txt	Tue Jun 24 08:07:34 2003 +0000
+++ b/doc/design.txt	Tue Jun 24 12:39:20 2003 +0000
@@ -26,17 +26,17 @@
 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.
@@ -122,7 +122,8 @@
 
     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 +134,22 @@
             """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 +186,7 @@
     >>> Date(". + 2d") - Interval("3w")
     <Date 2000-06-07.00:34:02>
 
+
 Items and Classes
 ~~~~~~~~~~~~~~~~~
 
@@ -187,6 +195,7 @@
 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 +207,12 @@
 
 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 +242,7 @@
 A property that is not specified will return as None from a *get*
 operation.
 
+
 Hyperdb Interface Specification
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -279,7 +290,9 @@
 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 +319,34 @@
         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.
+            """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.
 
-            '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
+
+            The id of the current user is also available on the database
+            as 'self.db.curuserid'.
             """
 
         # Editing items:
@@ -325,156 +354,174 @@
         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.
+            """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: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. Used by the full text
+            indexing, which knows that "foo" occurs in msg1, msg3 and
+            file7, so we have hits on these issues:
 
                 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 items in this
+            class that match the 'filter' spec, sorted by the group spec
+            and then the sort spec.
+            """
 
         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 +532,7 @@
 and so on.
 
 Several implementations are provided - they belong in the
-roundup.backends package.
+``roundup.backends`` package.
 
 
 Application Example
@@ -530,7 +577,8 @@
     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.Key(),
+    ... password=hyperdb.String())
     <hyperdb.Class "user">
     >>> db.issue.addprop(fixer=hyperdb.Link("user"))
     >>> db.issue.getprops()
@@ -546,7 +594,8 @@
     >>> 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 +620,7 @@
 detectors and user items, and on issues it provides mail spools, nosy
 lists, and superseders.
 
+
 Reserved Classes
 ~~~~~~~~~~~~~~~~
 
@@ -612,6 +662,7 @@
 system). The "summary" property contains a summary of the message for
 display in a message index.
 
+
 Files
 """""
 
@@ -628,6 +679,7 @@
 "name" property holds the original name of the file, and the "type"
 property holds the MIME type of the file as received.
 
+
 Issue Classes
 ~~~~~~~~~~~~~
 
@@ -652,6 +704,7 @@
 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).
 
+
 Roundupdb Interface Specification
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -723,7 +776,8 @@
 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 +785,8 @@
     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")
@@ -758,7 +813,8 @@
 "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 +844,7 @@
 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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -845,6 +902,7 @@
 For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of
 the retired or restored item and ``olddata`` is None.
 
+
 Detector Example
 ~~~~~~~~~~~~~~~~
 
@@ -864,18 +922,20 @@
             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 +950,8 @@
 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 +959,15 @@
                           "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,6 +983,7 @@
 interesting can simply be written in Python using the Roundup database
 module.)
 
+
 Command Interface Specification
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -962,6 +1026,7 @@
 commands, they are printed one per line (default) or joined by commas
 (with the -list) option.
 
+
 Usage Example
 ~~~~~~~~~~~~~
 
@@ -997,6 +1062,7 @@
 script by the mail delivery system (e.g. using an alias beginning with
 "|" for sendmail).
 
+
 Message Processing
 ~~~~~~~~~~~~~~~~~~
 
@@ -1049,6 +1115,7 @@
 raises an exception, the original message is bounced back to the sender
 with the explanatory message given in the exception.
 
+
 Nosy Lists
 ~~~~~~~~~~
 
@@ -1061,6 +1128,7 @@
 hyperdatabase on the "recipients" property then provides a log of when
 the message was sent to whom.
 
+
 Setting Properties
 ~~~~~~~~~~~~~~~~~~
 
@@ -1083,6 +1151,7 @@
 interspersed some nonstandard tags, which we use as placeholders to be
 replaced by properties and their values.
 
+
 Views and View Specifiers
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1099,6 +1168,7 @@
 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 +1221,7 @@
 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
 """""""""""""""""""""
 
@@ -1250,7 +1321,8 @@
 
     <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 +1359,7 @@
 (thus triggering the standard detector to react by sending out this
 message to the nosy list).
 
+
 Spool Section
 """""""""""""
 
@@ -1441,7 +1514,8 @@
     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 +1547,9 @@
 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 +1604,7 @@
 Vincent, Mark Miller, Christopher Simons, Jeff Dunmall, Wayne Gramlich,
 and Dean Tribble for their assistance with the first-round submission.
 
+
 Changes to this document
 ------------------------
 

Roundup Issue Tracker: http://roundup-tracker.org/