diff --git a/doc/design.txt b/doc/design.txt
index 67f82f1bdeb9cba5e1c2c4d339582c286c8e40ce..0748fa29183de290805d49dcb868ba150f0cd292 100644 (file)
--- a/doc/design.txt
+++ b/doc/design.txt
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::
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
-----------------
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.
-------------
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
~~~~~~~~~~~~~~~~~~~~~~~~~
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
"""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):
>>> Date(". + 2d") - Interval("3w")
<Date 2000-06-07.00:34:02>
+
Items and Classes
~~~~~~~~~~~~~~~~~
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~
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
~~~~~~~~~~~~~~~~~~~~~~~~
A property that is not specified will return as None from a *get*
operation.
+
Hyperdb Interface Specification
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
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:
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
and so on.
Several implementations are provided - they belong in the
-roundup.backends package.
+``roundup.backends`` package.
Application Example
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()
>>> 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")),
detectors and user items, and on issues it provides mail spools, nosy
lists, and superseders.
+
Reserved Classes
~~~~~~~~~~~~~~~~
system). The "summary" property contains a summary of the message for
display in a message index.
+
Files
"""""
"name" property holds the original name of the file, and the "type"
property holds the MIME type of the file as received.
+
Issue Classes
~~~~~~~~~~~~~
=========== ==========================
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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:
"""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:
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")
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")
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"))
"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)
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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::
For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of
the retired or restored item and ``olddata`` is None.
+
Detector Example
~~~~~~~~~~~~~~~~
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"))
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"]:
"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):
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
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
~~~~~~~~~~~~~
"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
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
script by the mail delivery system (e.g. using an alias beginning with
"|" for sendmail).
+
Message Processing
~~~~~~~~~~~~~~~~~~
raises an exception, the original message is bounced back to the sender
with the explanatory message given in the exception.
+
Nosy Lists
~~~~~~~~~~
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
interspersed some nonstandard tags, which we use as placeholders to be
replaced by properties and their values.
+
Views and View Specifiers
~~~~~~~~~~~~~~~~~~~~~~~~~
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
~~~~~~~~~~~~~~~~~~~~~
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
"""""""""""""""""""""
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
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
<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>
(thus triggering the standard detector to react by sending out this
message to the nosy list).
+
Spool Section
"""""""""""""
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
- 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:
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):
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
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
Vincent, Mark Miller, Christopher Simons, Jeff Dunmall, Wayne Gramlich,
and Dean Tribble for their assistance with the first-round submission.
+
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