Mercurial > p > roundup > code
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 ------------------------
