Code

instance -> tracker, node -> item
[roundup.git] / doc / design.txt
1 ========================================================
2 Roundup - An Issue-Tracking System for Knowledge Workers
3 ========================================================
5 :Authors: Ka-Ping Yee (original__), Richard Jones (implementation)
7 __ spec.html
9 .. contents::
11 Introduction
12 ---------------
14 This document presents a description of the components
15 of the Roundup system and specifies their interfaces and
16 behaviour in sufficient detail to guide an implementation.
17 For the philosophy and rationale behind the Roundup design,
18 see the first-round Software Carpentry submission for Roundup.
19 This document fleshes out that design as well as specifying
20 interfaces so that the components can be developed separately.
23 The Layer Cake
24 -----------------
26 Lots of software design documents come with a picture of
27 a cake.  Everybody seems to like them.  I also like cakes
28 (i think they are tasty).  So i, too, shall include
29 a picture of a cake here::
31      _________________________________________________________________________
32     |  E-mail Client   |   Web Browser   |   Detector Scripts   |    Shell    |
33     |------------------+-----------------+----------------------+-------------|
34     |   E-mail User    |    Web User     |      Detector        |   Command   | 
35     |-------------------------------------------------------------------------|
36     |                         Roundup Database Layer                          |
37     |-------------------------------------------------------------------------|
38     |                          Hyperdatabase Layer                            |
39     |-------------------------------------------------------------------------|
40     |                             Storage Layer                               |
41      -------------------------------------------------------------------------
43 The colourful parts of the cake are part of our system;
44 the faint grey parts of the cake are external components.
46 I will now proceed to forgo all table manners and
47 eat from the bottom of the cake to the top.  You may want
48 to stand back a bit so you don't get covered in crumbs.
51 Hyperdatabase
52 -------------
54 The lowest-level component to be implemented is the hyperdatabase.
55 The hyperdatabase is intended to be
56 a flexible data store that can hold configurable data in
57 records which we call items.
59 The hyperdatabase is implemented on top of the storage layer,
60 an external module for storing its data.  The storage layer could
61 be a third-party RDBMS; for a "batteries-included" distribution,
62 implementing the hyperdatabase on the standard bsddb
63 module is suggested.
65 Dates and Date Arithmetic
66 ~~~~~~~~~~~~~~~~~~~~~~~~~
68 Before we get into the hyperdatabase itself, we need a
69 way of handling dates.  The hyperdatabase module provides
70 Timestamp objects for
71 representing date-and-time stamps and Interval objects for
72 representing date-and-time intervals.
74 As strings, date-and-time stamps are specified with
75 the date in international standard format
76 (``yyyy-mm-dd``)
77 joined to the time (``hh:mm:ss``)
78 by a period "``.``".  Dates in
79 this form can be easily compared and are fairly readable
80 when printed.  An example of a valid stamp is
81 "``2000-06-24.13:03:59``".
82 We'll call this the "full date format".  When Timestamp objects are
83 printed as strings, they appear in the full date format with
84 the time always given in GMT.  The full date format is always
85 exactly 19 characters long.
87 For user input, some partial forms are also permitted:
88 the whole time or just the seconds may be omitted; and the whole date
89 may be omitted or just the year may be omitted.  If the time is given,
90 the time is interpreted in the user's local time zone.
91 The Date constructor takes care of these conversions.
92 In the following examples, suppose that ``yyyy`` is the current year,
93 ``mm`` is the current month, and ``dd`` is the current
94 day of the month; and suppose that the user is on Eastern Standard Time.
96 -   "2000-04-17" means <Date 2000-04-17.00:00:00>
97 -   "01-25" means <Date yyyy-01-25.00:00:00>
98 -   "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
99 -   "08-13.22:13" means <Date yyyy-08-14.03:13:00>
100 -   "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
101 -   "14:25" means
102 -   <Date yyyy-mm-dd.19:25:00>
103 -   "8:47:11" means
104 -   <Date yyyy-mm-dd.13:47:11>
105 -   the special date "." means "right now"
108 Date intervals are specified using the suffixes
109 "y", "m", and "d".  The suffix "w" (for "week") means 7 days.
110 Time intervals are specified in hh:mm:ss format (the seconds
111 may be omitted, but the hours and minutes may not).
113 -   "3y" means three years
114 -   "2y 1m" means two years and one month
115 -   "1m 25d" means one month and 25 days
116 -   "2w 3d" means two weeks and three days
117 -   "1d 2:50" means one day, two hours, and 50 minutes
118 -   "14:00" means 14 hours
119 -   "0:04:33" means four minutes and 33 seconds
122 The Date class should understand simple date expressions of the form 
123 *stamp* ``+`` *interval* and *stamp* ``-`` *interval*.
124 When adding or subtracting intervals involving months or years, the
125 components are handled separately.  For example, when evaluating
126 "``2000-06-25 + 1m 10d``", we first add one month to
127 get 2000-07-25, then add 10 days to get
128 2000-08-04 (rather than trying to decide whether
129 1m 10d means 38 or 40 or 41 days).
131 Here is an outline of the Date and Interval classes::
133     class Date:
134         def __init__(self, spec, offset):
135             """Construct a date given a specification and a time zone offset.
137             'spec' is a full date or a partial form, with an optional
138             added or subtracted interval.  'offset' is the local time
139             zone offset from GMT in hours.
140             """
142         def __add__(self, interval):
143             """Add an interval to this date to produce another date."""
145         def __sub__(self, interval):
146             """Subtract an interval from this date to produce another date."""
148         def __cmp__(self, other):
149             """Compare this date to another date."""
151         def __str__(self):
152             """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
154         def local(self, offset):
155             """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone."""
157     class Interval:
158         def __init__(self, spec):
159             """Construct an interval given a specification."""
161         def __cmp__(self, other):
162             """Compare this interval to another interval."""
163             
164         def __str__(self):
165             """Return this interval as a string."""
169 Here are some examples of how these classes would behave in practice.
170 For the following examples, assume that we are on Eastern Standard
171 Time and the current local time is 19:34:02 on 25 June 2000::
173     >>> Date(".")
174     <Date 2000-06-26.00:34:02>
175     >>> _.local(-5)
176     "2000-06-25.19:34:02"
177     >>> Date(". + 2d")
178     <Date 2000-06-28.00:34:02>
179     >>> Date("1997-04-17", -5)
180     <Date 1997-04-17.00:00:00>
181     >>> Date("01-25", -5)
182     <Date 2000-01-25.00:00:00>
183     >>> Date("08-13.22:13", -5)
184     <Date 2000-08-14.03:13:00>
185     >>> Date("14:25", -5)
186     <Date 2000-06-25.19:25:00>
187     >>> Interval("  3w  1  d  2:00")
188     <Interval 22d 2:00>
189     >>> Date(". + 2d") - Interval("3w")
190     <Date 2000-06-07.00:34:02>
192 Nodes and Classes
193 ~~~~~~~~~~~~~~~~~
195 Nodes contain data in properties.  To Python, these
196 properties are presented as the key-value pairs of a dictionary.
197 Each item belongs to a class which defines the names
198 and types of its properties.  The database permits the creation
199 and modification of classes as well as items.
201 Identifiers and Designators
202 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
204 Each item has a numeric identifier which is unique among
205 items in its class.  The items are numbered sequentially
206 within each class in order of creation, starting from 1.
207 The designator
208 for an item is a way to identify an item in the database, and
209 consists of the name of the item's class concatenated with
210 the item's numeric identifier.
212 For example, if "spam" and "eggs" are classes, the first
213 item created in class "spam" has id 1 and designator "spam1".
214 The first item created in class "eggs" also has id 1 but has
215 the distinct designator "eggs1".  Node designators are
216 conventionally enclosed in square brackets when mentioned
217 in plain text.  This permits a casual mention of, say,
218 "[patch37]" in an e-mail message to be turned into an active
219 hyperlink.
221 Property Names and Types
222 ~~~~~~~~~~~~~~~~~~~~~~~~
224 Property names must begin with a letter.
226 A property may be one of five basic types:
228 - String properties are for storing arbitrary-length strings.
230 - Boolean properties are for storing true/false, or yes/no values.
232 - Number properties are for storing numeric values.
234 - Date properties store date-and-time stamps.
235   Their values are Timestamp objects.
237 - A Link property refers to a single other item
238   selected from a specified class.  The class is part of the property;
239   the value is an integer, the id of the chosen item.
241 - A Multilink property refers to possibly many items
242   in a specified class.  The value is a list of integers.
244 *None* is also a permitted value for any of these property
245 types.  An attempt to store None into a Multilink property stores an empty list.
247 A property that is not specified will return as None from a *get*
248 operation.
250 Hyperdb Interface Specification
251 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
253 TODO: replace the Interface Specifications with links to the pydoc
255 The hyperdb module provides property objects to designate
256 the different kinds of properties.  These objects are used when
257 specifying what properties belong in classes::
259     class String:
260         def __init__(self, indexme='no'):
261             """An object designating a String property."""
263     class Boolean:
264         def __init__(self):
265             """An object designating a Boolean property."""
267     class Number:
268         def __init__(self):
269             """An object designating a Number property."""
271     class Date:
272         def __init__(self):
273             """An object designating a Date property."""
275     class Link:
276         def __init__(self, classname, do_journal='yes'):
277             """An object designating a Link property that links to
278             items in a specified class.
280             If the do_journal argument is not 'yes' then changes to
281             the property are not journalled in the linked item.
282             """
284     class Multilink:
285         def __init__(self, classname, do_journal='yes'):
286             """An object designating a Multilink property that links
287             to items in a specified class.
289             If the do_journal argument is not 'yes' then changes to
290             the property are not journalled in the linked item(s).
291             """
294 Here is the interface provided by the hyperdatabase::
296     class Database:
297         """A database for storing records containing flexible data types."""
299         def __init__(self, config, journaltag=None):
300             """Open a hyperdatabase given a specifier to some storage.
302             The 'storagelocator' is obtained from config.DATABASE.
303             The meaning of 'storagelocator' depends on the particular
304             implementation of the hyperdatabase.  It could be a file name,
305             a directory path, a socket descriptor for a connection to a
306             database over the network, etc.
308             The 'journaltag' is a token that will be attached to the journal
309             entries for any edits done on the database.  If 'journaltag' is
310             None, the database is opened in read-only mode: the Class.create(),
311             Class.set(), and Class.retire() methods are disabled.
312             """
314         def __getattr__(self, classname):
315             """A convenient way of calling self.getclass(classname)."""
317         def getclasses(self):
318             """Return a list of the names of all existing classes."""
320         def getclass(self, classname):
321             """Get the Class object representing a particular class.
323             If 'classname' is not a valid class name, a KeyError is raised.
324             """
326     class Class:
327         """The handle to a particular class of items in a hyperdatabase."""
329         def __init__(self, db, classname, **properties):
330             """Create a new class with a given name and property specification.
332             'classname' must not collide with the name of an existing class,
333             or a ValueError is raised.  The keyword arguments in 'properties'
334             must map names to property objects, or a TypeError is raised.
335             """
337         # Editing items:
339         def create(self, **propvalues):
340             """Create a new item of this class and return its id.
342             The keyword arguments in 'propvalues' map property names to values.
343             The values of arguments must be acceptable for the types of their
344             corresponding properties or a TypeError is raised.  If this class
345             has a key property, it must be present and its value must not
346             collide with other key strings or a ValueError is raised.  Any other
347             properties on this class that are missing from the 'propvalues'
348             dictionary are set to None.  If an id in a link or multilink
349             property does not refer to a valid item, an IndexError is raised.
350             """
352         def get(self, itemid, propname):
353             """Get the value of a property on an existing item of this class.
355             'itemid' must be the id of an existing item of this class or an
356             IndexError is raised.  'propname' must be the name of a property
357             of this class or a KeyError is raised.
358             """
360         def set(self, itemid, **propvalues):
361             """Modify a property on an existing item of this class.
362             
363             'itemid' must be the id of an existing item of this class or an
364             IndexError is raised.  Each key in 'propvalues' must be the name
365             of a property of this class or a KeyError is raised.  All values
366             in 'propvalues' must be acceptable types for their corresponding
367             properties or a TypeError is raised.  If the value of the key
368             property is set, it must not collide with other key strings or a
369             ValueError is raised.  If the value of a Link or Multilink
370             property contains an invalid item id, a ValueError is raised.
371             """
373         def retire(self, itemid):
374             """Retire an item.
375             
376             The properties on the item remain available from the get() method,
377             and the item's id is never reused.  Retired items are not returned
378             by the find(), list(), or lookup() methods, and other items may
379             reuse the values of their key properties.
380             """
382         def history(self, itemid):
383             """Retrieve the journal of edits on a particular item.
385             'itemid' must be the id of an existing item of this class or an
386             IndexError is raised.
388             The returned list contains tuples of the form
390                 (date, tag, action, params)
392             'date' is a Timestamp object specifying the time of the change and
393             'tag' is the journaltag specified when the database was opened.
394             'action' may be:
396                 'create' or 'set' -- 'params' is a dictionary of property values
397                 'link' or 'unlink' -- 'params' is (classname, itemid, propname)
398                 'retire' -- 'params' is None
399             """
401         # Locating items:
403         def setkey(self, propname):
404             """Select a String property of this class to be the key property.
406             'propname' must be the name of a String property of this class or
407             None, or a TypeError is raised.  The values of the key property on
408             all existing items must be unique or a ValueError is raised.
409             """
411         def getkey(self):
412             """Return the name of the key property for this class or None."""
414         def lookup(self, keyvalue):
415             """Locate a particular item by its key property and return its id.
417             If this class has no key property, a TypeError is raised.  If the
418             'keyvalue' matches one of the values for the key property among
419             the items in this class, the matching item's id is returned;
420             otherwise a KeyError is raised.
421             """
423         def find(self, propname, itemid):
424             """Get the ids of items in this class which link to the given items.
426             'propspec' consists of keyword args propname={itemid:1,}   
427             'propname' must be the name of a property in this class, or a
428             KeyError is raised.  That property must be a Link or Multilink
429             property, or a TypeError is raised.
431             Any item in this class whose 'propname' property links to any of the
432             itemids will be returned. Used by the full text indexing, which
433             knows that "foo" occurs in msg1, msg3 and file7, so we have hits
434             on these issues:
436                 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
437             """
439         def filter(self, search_matches, filterspec, sort, group):
440             ''' Return a list of the ids of the active items in this class that
441                 match the 'filter' spec, sorted by the group spec and then the
442                 sort spec.
443             '''
445         def list(self):
446             """Return a list of the ids of the active items in this class."""
448         def count(self):
449             """Get the number of items in this class.
451             If the returned integer is 'numitems', the ids of all the items
452             in this class run from 1 to numitems, and numitems+1 will be the
453             id of the next item to be created in this class.
454             """
456         # Manipulating properties:
458         def getprops(self):
459             """Return a dictionary mapping property names to property objects."""
461         def addprop(self, **properties):
462             """Add properties to this class.
464             The keyword arguments in 'properties' must map names to property
465             objects, or a TypeError is raised.  None of the keys in 'properties'
466             may collide with the names of existing properties, or a ValueError
467             is raised before any properties have been added.
468             """
470         def getitem(self, itemid, cache=1):
471             ''' Return a Node convenience wrapper for the item.
473             'itemid' must be the id of an existing item of this class or an
474             IndexError is raised.
476             'cache' indicates whether the transaction cache should be queried
477             for the item. If the item has been modified and you need to
478             determine what its values prior to modification are, you need to
479             set cache=0.
480             '''
482     class Node:
483         ''' A convenience wrapper for the given item. It provides a mapping
484             interface to a single item's properties
485         '''
487 Hyperdatabase Implementations
488 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
490 Hyperdatabase implementations exist to create the interface described in the
491 `hyperdb interface specification`_
492 over an existing storage mechanism. Examples are relational databases,
493 \*dbm key-value databases, and so on.
495 Several implementations are provided - they belong in the roundup.backends
496 package.
499 Application Example
500 ~~~~~~~~~~~~~~~~~~~
502 Here is an example of how the hyperdatabase module would work in practice::
504     >>> import hyperdb
505     >>> db = hyperdb.Database("foo.db", "ping")
506     >>> db
507     <hyperdb.Database "foo.db" opened by "ping">
508     >>> hyperdb.Class(db, "status", name=hyperdb.String())
509     <hyperdb.Class "status">
510     >>> _.setkey("name")
511     >>> db.status.create(name="unread")
512     1
513     >>> db.status.create(name="in-progress")
514     2
515     >>> db.status.create(name="testing")
516     3
517     >>> db.status.create(name="resolved")
518     4
519     >>> db.status.count()
520     4
521     >>> db.status.list()
522     [1, 2, 3, 4]
523     >>> db.status.lookup("in-progress")
524     2
525     >>> db.status.retire(3)
526     >>> db.status.list()
527     [1, 2, 4]
528     >>> hyperdb.Class(db, "issue", title=hyperdb.String(), status=hyperdb.Link("status"))
529     <hyperdb.Class "issue">
530     >>> db.issue.create(title="spam", status=1)
531     1
532     >>> db.issue.create(title="eggs", status=2)
533     2
534     >>> db.issue.create(title="ham", status=4)
535     3
536     >>> db.issue.create(title="arguments", status=2)
537     4
538     >>> db.issue.create(title="abuse", status=1)
539     5
540     >>> hyperdb.Class(db, "user", username=hyperdb.Key(), password=hyperdb.String())
541     <hyperdb.Class "user">
542     >>> db.issue.addprop(fixer=hyperdb.Link("user"))
543     >>> db.issue.getprops()
544     {"title": <hyperdb.String>, "status": <hyperdb.Link to "status">,
545      "user": <hyperdb.Link to "user">}
546     >>> db.issue.set(5, status=2)
547     >>> db.issue.get(5, "status")
548     2
549     >>> db.status.get(2, "name")
550     "in-progress"
551     >>> db.issue.get(5, "title")
552     "abuse"
553     >>> db.issue.find("status", db.status.lookup("in-progress"))
554     [2, 4, 5]
555     >>> db.issue.history(5)
556     [(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse", "status": 1}),
557      (<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})]
558     >>> db.status.history(1)
559     [(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")),
560      (<Date 2000-06-28.19:11:04>, "ping", "unlink", ("issue", 5, "status"))]
561     >>> db.status.history(2)
562     [(<Date 2000-06-28.19:11:04>, "ping", "link", ("issue", 5, "status"))]
565 For the purposes of journalling, when a Multilink property is
566 set to a new list of items, the hyperdatabase compares the old
567 list to the new list.
568 The journal records "unlink" events for all the items that appear
569 in the old list but not the new list,
570 and "link" events for
571 all the items that appear in the new list but not in the old list.
574 Roundup Database
575 ----------------
577 The Roundup database layer is implemented on top of the
578 hyperdatabase and mediates calls to the database.
579 Some of the classes in the Roundup database are considered
580 issue classes.
581 The Roundup database layer adds detectors and user items,
582 and on issues it provides mail spools, nosy lists, and superseders.
584 Reserved Classes
585 ~~~~~~~~~~~~~~~~
587 Internal to this layer we reserve three special classes
588 of items that are not issues.
590 Users
591 """""
593 Users are stored in the hyperdatabase as items of
594 class "user".  The "user" class has the definition::
596     hyperdb.Class(db, "user", username=hyperdb.String(),
597                               password=hyperdb.String(),
598                               address=hyperdb.String())
599     db.user.setkey("username")
601 Messages
602 """"""""
604 E-mail messages are represented by hyperdatabase items of class "msg".
605 The actual text content of the messages is stored in separate files.
606 (There's no advantage to be gained by stuffing them into the
607 hyperdatabase, and if messages are stored in ordinary text files,
608 they can be grepped from the command line.)  The text of a message is
609 saved in a file named after the message item designator (e.g. "msg23")
610 for the sake of the command interface (see below).  Attachments are
611 stored separately and associated with "file" items.
612 The "msg" class has the definition::
614     hyperdb.Class(db, "msg", author=hyperdb.Link("user"),
615                              recipients=hyperdb.Multilink("user"),
616                              date=hyperdb.Date(),
617                              summary=hyperdb.String(),
618                              files=hyperdb.Multilink("file"))
620 The "author" property indicates the author of the message
621 (a "user" item must exist in the hyperdatabase for any messages
622 that are stored in the system).
623 The "summary" property contains a summary of the message for display
624 in a message index.
626 Files
627 """""
629 Submitted files are represented by hyperdatabase
630 items of class "file".  Like e-mail messages, the file content
631 is stored in files outside the database,
632 named after the file item designator (e.g. "file17").
633 The "file" class has the definition::
635     hyperdb.Class(db, "file", user=hyperdb.Link("user"),
636                               name=hyperdb.String(),
637                               type=hyperdb.String())
639 The "user" property indicates the user who submitted the
640 file, the "name" property holds the original name of the file,
641 and the "type" property holds the MIME type of the file as received.
643 Issue Classes
644 ~~~~~~~~~~~~~
646 All issues have the following standard properties:
648 =========== ==========================
649 Property    Definition
650 =========== ==========================
651 title       hyperdb.String()
652 messages    hyperdb.Multilink("msg")
653 files       hyperdb.Multilink("file")
654 nosy        hyperdb.Multilink("user")
655 superseder  hyperdb.Multilink("issue")
656 =========== ==========================
658 Also, two Date properties named "creation" and "activity" are
659 fabricated by the Roundup database layer.  By "fabricated" we
660 mean that no such properties are actually stored in the
661 hyperdatabase, but when properties on issues are requested, the
662 "creation" and "activity" properties are made available.
663 The value of the "creation" property is the date when an issue was
664 created, and the value of the "activity" property is the
665 date when any property on the issue was last edited (equivalently,
666 these are the dates on the first and last records in the issue's journal).
668 Roundupdb Interface Specification
669 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
671 The interface to a Roundup database delegates most method
672 calls to the hyperdatabase, except for the following
673 changes and additional methods::
675     class Database:
676         def getuid(self):
677             """Return the id of the "user" item associated with the user
678             that owns this connection to the hyperdatabase."""
680     class Class:
681         # Overridden methods:
683         def create(self, **propvalues):
684         def set(self, **propvalues):
685         def retire(self, itemid):
686             """These operations trigger detectors and can be vetoed.  Attempts
687             to modify the "creation" or "activity" properties cause a KeyError.
688             """
690         # New methods:
692         def audit(self, event, detector):
693         def react(self, event, detector):
694             """Register a detector (see below for more details)."""
696     class IssueClass(Class):
697         # Overridden methods:
699         def __init__(self, db, classname, **properties):
700             """The newly-created class automatically includes the "messages",
701             "files", "nosy", and "superseder" properties.  If the 'properties'
702             dictionary attempts to specify any of these properties or a
703             "creation" or "activity" property, a ValueError is raised."""
705         def get(self, itemid, propname):
706         def getprops(self):
707             """In addition to the actual properties on the item, these
708             methods provide the "creation" and "activity" properties."""
710         # New methods:
712         def addmessage(self, itemid, summary, text):
713             """Add a message to an issue's mail spool.
715             A new "msg" item is constructed using the current date, the
716             user that owns the database connection as the author, and
717             the specified summary text.  The "files" and "recipients"
718             fields are left empty.  The given text is saved as the body
719             of the message and the item is appended to the "messages"
720             field of the specified issue.
721             """
723         def sendmessage(self, itemid, msgid):
724             """Send a message to the members of an issue's nosy list.
726             The message is sent only to users on the nosy list who are not
727             already on the "recipients" list for the message.  These users
728             are then added to the message's "recipients" list.
729             """
732 Default Schema
733 ~~~~~~~~~~~~~~
735 The default schema included with Roundup turns it into a
736 typical software bug tracker.  The database is set up like this::
738     pri = Class(db, "priority", name=hyperdb.String(), order=hyperdb.String())
739     pri.setkey("name")
740     pri.create(name="critical", order="1")
741     pri.create(name="urgent", order="2")
742     pri.create(name="bug", order="3")
743     pri.create(name="feature", order="4")
744     pri.create(name="wish", order="5")
746     stat = Class(db, "status", name=hyperdb.String(), order=hyperdb.String())
747     stat.setkey("name")
748     stat.create(name="unread", order="1")
749     stat.create(name="deferred", order="2")
750     stat.create(name="chatting", order="3")
751     stat.create(name="need-eg", order="4")
752     stat.create(name="in-progress", order="5")
753     stat.create(name="testing", order="6")
754     stat.create(name="done-cbb", order="7")
755     stat.create(name="resolved", order="8")
757     Class(db, "keyword", name=hyperdb.String())
759     Class(db, "issue", fixer=hyperdb.Multilink("user"),
760                        topic=hyperdb.Multilink("keyword"),
761                        priority=hyperdb.Link("priority"),
762                        status=hyperdb.Link("status"))
764 (The "order" property hasn't been explained yet.  It
765 gets used by the Web user interface for sorting.)
767 The above isn't as pretty-looking as the schema specification
768 in the first-stage submission, but it could be made just as easy
769 with the addition of a convenience function like Choice
770 for setting up the "priority" and "status" classes::
772     def Choice(name, *options):
773         cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
774         for i in range(len(options)):
775             cl.create(name=option[i], order=i)
776         return hyperdb.Link(name)
779 Detector Interface
780 ------------------
782 Detectors are Python functions that are triggered on certain
783 kinds of events.  The definitions of the
784 functions live in Python modules placed in a directory set aside
785 for this purpose.  Importing the Roundup database module also
786 imports all the modules in this directory, and the ``init()``
787 function of each module is called when a database is opened to
788 provide it a chance to register its detectors.
790 There are two kinds of detectors:
792 1. an auditor is triggered just before modifying an item
793 2. a reactor is triggered just after an item has been modified
795 When the Roundup database is about to perform a
796 ``create()``, ``set()``, or ``retire()``
797 operation, it first calls any *auditors* that
798 have been registered for that operation on that class.
799 Any auditor may raise a *Reject* exception
800 to abort the operation.
802 If none of the auditors raises an exception, the database
803 proceeds to carry out the operation.  After it's done, it
804 then calls all of the *reactors* that have been registered
805 for the operation.
807 Detector Interface Specification
808 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
810 The ``audit()`` and ``react()`` methods
811 register detectors on a given class of items::
813     class Class:
814         def audit(self, event, detector):
815             """Register an auditor on this class.
817             'event' should be one of "create", "set", or "retire".
818             'detector' should be a function accepting four arguments.
819             """
821         def react(self, event, detector):
822             """Register a reactor on this class.
824             'event' should be one of "create", "set", or "retire".
825             'detector' should be a function accepting four arguments.
826             """
828 Auditors are called with the arguments::
830     audit(db, cl, itemid, newdata)
832 where ``db`` is the database, ``cl`` is an
833 instance of Class or IssueClass within the database, and ``newdata``
834 is a dictionary mapping property names to values.
836 For a ``create()``
837 operation, the ``itemid`` argument is None and newdata
838 contains all of the initial property values with which the item
839 is about to be created.
841 For a ``set()`` operation, newdata
842 contains only the names and values of properties that are about
843 to be changed.
845 For a ``retire()`` operation, newdata is None.
847 Reactors are called with the arguments::
849     react(db, cl, itemid, olddata)
851 where ``db`` is the database, ``cl`` is an
852 instance of Class or IssueClass within the database, and ``olddata``
853 is a dictionary mapping property names to values.
855 For a ``create()``
856 operation, the ``itemid`` argument is the id of the
857 newly-created item and ``olddata`` is None.
859 For a ``set()`` operation, ``olddata``
860 contains the names and previous values of properties that were changed.
862 For a ``retire()`` operation, ``itemid`` is the
863 id of the retired item and ``olddata`` is None.
865 Detector Example
866 ~~~~~~~~~~~~~~~~
868 Here is an example of detectors written for a hypothetical
869 project-management application, where users can signal approval
870 of a project by adding themselves to an "approvals" list, and
871 a project proceeds when it has three approvals::
873     # Permit users only to add themselves to the "approvals" list.
875     def check_approvals(db, cl, id, newdata):
876         if newdata.has_key("approvals"):
877             if cl.get(id, "status") == db.status.lookup("approved"):
878                 raise Reject, "You can't modify the approvals list " \
879                     "for a project that has already been approved."
880             old = cl.get(id, "approvals")
881             new = newdata["approvals"]
882             for uid in old:
883                 if uid not in new and uid != db.getuid():
884                     raise Reject, "You can't remove other users from the "
885                         "approvals list; you can only remove yourself."
886             for uid in new:
887                 if uid not in old and uid != db.getuid():
888                     raise Reject, "You can't add other users to the approvals "
889                         "list; you can only add yourself."
891     # When three people have approved a project, change its
892     # status from "pending" to "approved".
894     def approve_project(db, cl, id, olddata):
895         if olddata.has_key("approvals") and len(cl.get(id, "approvals")) == 3:
896             if cl.get(id, "status") == db.status.lookup("pending"):
897                 cl.set(id, status=db.status.lookup("approved"))
899     def init(db):
900         db.project.audit("set", check_approval)
901         db.project.react("set", approve_project)
903 Here is another example of a detector that can allow or prevent
904 the creation of new items.  In this scenario, patches for a software
905 project are submitted by sending in e-mail with an attached file,
906 and we want to ensure that there are text/plain attachments on
907 the message.  The maintainer of the package can then apply the
908 patch by setting its status to "applied"::
910     # Only accept attempts to create new patches that come with patch files.
912     def check_new_patch(db, cl, id, newdata):
913         if not newdata["files"]:
914             raise Reject, "You can't submit a new patch without " \
915                           "attaching a patch file."
916         for fileid in newdata["files"]:
917             if db.file.get(fileid, "type") != "text/plain":
918                 raise Reject, "Submitted patch files must be text/plain."
920     # When the status is changed from "approved" to "applied", apply the patch.
922     def apply_patch(db, cl, id, olddata):
923         if cl.get(id, "status") == db.status.lookup("applied") and \
924             olddata["status"] == db.status.lookup("approved"):
925             # ...apply the patch...
927     def init(db):
928         db.patch.audit("create", check_new_patch)
929         db.patch.react("set", apply_patch)
932 Command Interface
933 -----------------
935 The command interface is a very simple and minimal interface,
936 intended only for quick searches and checks from the shell prompt.
937 (Anything more interesting can simply be written in Python using
938 the Roundup database module.)
940 Command Interface Specification
941 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
943 A single command, roundup, provides basic access to
944 the hyperdatabase from the command line::
946     roundup-admin help
947     roundup-admin get [-list] designator[, designator,...] propname
948     roundup-admin set designator[, designator,...] propname=value ...
949     roundup-admin find [-list] classname propname=value ...
951 See ``roundup-admin help commands`` for a complete list of commands.
953 Property values are represented as strings in command arguments
954 and in the printed results:
956 - Strings are, well, strings.
958 - Numbers are displayed the same as strings.
960 - Booleans are displayed as 'Yes' or 'No'.
962 - Date values are printed in the full date format in the local
963   time zone, and accepted in the full format or any of the partial
964   formats explained above.
966 - Link values are printed as item designators.  When given as
967   an argument, item designators and key strings are both accepted.
969 - Multilink values are printed as lists of item designators
970   joined by commas.  When given as an argument, item designators
971   and key strings are both accepted; an empty string, a single item,
972   or a list of items joined by commas is accepted.
974 When multiple items are specified to the
975 roundup get or roundup set
976 commands, the specified properties are retrieved or set
977 on all the listed items.
979 When multiple results are returned by the roundup get
980 or roundup find commands, they are printed one per
981 line (default) or joined by commas (with the -list) option.
983 Usage Example
984 ~~~~~~~~~~~~~
986 To find all messages regarding in-progress issues that
987 contain the word "spam", for example, you could execute the
988 following command from the directory where the database
989 dumps its files::
991     shell% for issue in `roundup find issue status=in-progress`; do
992     > grep -l spam `roundup get $issue messages`
993     > done
994     msg23
995     msg49
996     msg50
997     msg61
998     shell%
1000 Or, using the -list option, this can be written as a single command::
1002     shell% grep -l spam `roundup get \
1003         \`roundup find -list issue status=in-progress\` messages`
1004     msg23
1005     msg49
1006     msg50
1007     msg61
1008     shell%
1009     
1011 E-mail User Interface
1012 ---------------------
1014 The Roundup system must be assigned an e-mail address
1015 at which to receive mail.  Messages should be piped to
1016 the Roundup mail-handling script by the mail delivery
1017 system (e.g. using an alias beginning with "|" for sendmail).
1019 Message Processing
1020 ~~~~~~~~~~~~~~~~~~
1022 Incoming messages are examined for multiple parts.
1023 In a multipart/mixed message or part, each subpart is
1024 extracted and examined.  In a multipart/alternative
1025 message or part, we look for a text/plain subpart and
1026 ignore the other parts.  The text/plain subparts are
1027 assembled to form the textual body of the message, to
1028 be stored in the file associated with a "msg" class item.
1029 Any parts of other types are each stored in separate
1030 files and given "file" class items that are linked to
1031 the "msg" item.
1033 The "summary" property on message items is taken from
1034 the first non-quoting section in the message body.
1035 The message body is divided into sections by blank lines.
1036 Sections where the second and all subsequent lines begin
1037 with a ">" or "|" character are considered "quoting
1038 sections".  The first line of the first non-quoting 
1039 section becomes the summary of the message.
1041 All of the addresses in the To: and Cc: headers of the
1042 incoming message are looked up among the user items, and
1043 the corresponding users are placed in the "recipients"
1044 property on the new "msg" item.  The address in the From:
1045 header similarly determines the "author" property of the
1046 new "msg" item.
1047 The default handling for
1048 addresses that don't have corresponding users is to create
1049 new users with no passwords and a username equal to the
1050 address.  (The web interface does not permit logins for
1051 users with no passwords.)  If we prefer to reject mail from
1052 outside sources, we can simply register an auditor on the
1053 "user" class that prevents the creation of user items with
1054 no passwords.
1056 The subject line of the incoming message is examined to
1057 determine whether the message is an attempt to create a new
1058 issue or to discuss an existing issue.  A designator enclosed
1059 in square brackets is sought as the first thing on the
1060 subject line (after skipping any "Fwd:" or "Re:" prefixes).
1062 If an issue designator (class name and id number) is found
1063 there, the newly created "msg" item is added to the "messages"
1064 property for that issue, and any new "file" items are added to
1065 the "files" property for the issue.
1067 If just an issue class name is found there, we attempt to
1068 create a new issue of that class with its "messages" property
1069 initialized to contain the new "msg" item and its "files"
1070 property initialized to contain any new "file" items.
1072 Both cases may trigger detectors (in the first case we
1073 are calling the set() method to add the message to the
1074 issue's spool; in the second case we are calling the
1075 create() method to create a new item).  If an auditor
1076 raises an exception, the original message is bounced back to
1077 the sender with the explanatory message given in the exception.
1079 Nosy Lists
1080 ~~~~~~~~~~
1082 A standard detector is provided that watches for additions
1083 to the "messages" property.  When a new message is added, the
1084 detector sends it to all the users on the "nosy" list for the
1085 issue that are not already on the "recipients" list of the
1086 message.  Those users are then appended to the "recipients"
1087 property on the message, so multiple copies of a message
1088 are never sent to the same user.  The journal recorded by
1089 the hyperdatabase on the "recipients" property then provides
1090 a log of when the message was sent to whom.
1092 Setting Properties
1093 ~~~~~~~~~~~~~~~~~~
1095 The e-mail interface also provides a simple way to set
1096 properties on issues.  At the end of the subject line,
1097 ``propname=value`` pairs can be
1098 specified in square brackets, using the same conventions
1099 as for the roundup ``set`` shell command.
1102 Web User Interface
1103 ------------------
1105 The web interface is provided by a CGI script that can be
1106 run under any web server.  A simple web server can easily be
1107 built on the standard CGIHTTPServer module, and
1108 should also be included in the distribution for quick
1109 out-of-the-box deployment.
1111 The user interface is constructed from a number of template
1112 files containing mostly HTML.  Among the HTML tags in templates
1113 are interspersed some nonstandard tags, which we use as
1114 placeholders to be replaced by properties and their values.
1116 Views and View Specifiers
1117 ~~~~~~~~~~~~~~~~~~~~~~~~~
1119 There are two main kinds of views: *index* views and *issue* views.
1120 An index view displays a list of issues of a particular class,
1121 optionally sorted and filtered as requested.  An issue view
1122 presents the properties of a particular issue for editing
1123 and displays the message spool for the issue.
1125 A view specifier is a string that specifies
1126 all the options needed to construct a particular view.
1127 It goes after the URL to the Roundup CGI script or the
1128 web server to form the complete URL to a view.  When the
1129 result of selecting a link or submitting a form takes
1130 the user to a new view, the Web browser should be redirected
1131 to a canonical location containing a complete view specifier
1132 so that the view can be bookmarked.
1134 Displaying Properties
1135 ~~~~~~~~~~~~~~~~~~~~~
1137 Properties appear in the user interface in three contexts:
1138 in indices, in editors, and as filters.  For each type of
1139 property, there are several display possibilities.  For example,
1140 in an index view, a string property may just be printed as
1141 a plain string, but in an editor view, that property should
1142 be displayed in an editable field.
1144 The display of a property is handled by functions in
1145 a displayers module.  Each function accepts at
1146 least three standard arguments -- the database, class name,
1147 and item id -- and returns a chunk of HTML.
1149 Displayer functions are triggered by <display>
1150 tags in templates.  The call attribute of the tag
1151 provides a Python expression for calling the displayer
1152 function.  The three standard arguments are inserted in
1153 front of the arguments given.  For example, the occurrence of::
1155     <display call="plain('status', max=30)">
1157 in a template triggers a call to::
1158     
1159     plain(db, "issue", 13, "status", max=30)
1162 when displaying issue 13 in the "issue" class.  The displayer
1163 functions can accept extra arguments to further specify
1164 details about the widgets that should be generated.  By defining new
1165 displayer functions, the user interface can be highly customized.
1167 Some of the standard displayer functions include:
1169 ========= ====================================================================
1170 Function  Description
1171 ========= ====================================================================
1172 plain     display a String property directly;
1173           display a Date property in a specified time zone with an option
1174           to omit the time from the date stamp; for a Link or Multilink
1175           property, display the key strings of the linked items (or the
1176           ids if the linked class has no key property)
1177 field     display a property like the
1178           plain displayer above, but in a text field
1179           to be edited
1180 menu      for a Link property, display
1181           a menu of the available choices
1182 link      for a Link or Multilink property,
1183           display the names of the linked items, hyperlinked to the
1184           issue views on those items
1185 count     for a Multilink property, display
1186           a count of the number of links in the list
1187 reldate   display a Date property in terms
1188           of an interval relative to the current date (e.g. "+ 3w", "- 2d").
1189 download  show a Link("file") or Multilink("file")
1190           property using links that allow you to download files
1191 checklist for a Link or Multilink property,
1192           display checkboxes for the available choices to permit filtering
1193 ========= ====================================================================
1195 TODO: See the htmltemplate pydoc for a complete list of the functions
1198 Index Views
1199 ~~~~~~~~~~~
1201 An index view contains two sections: a filter section
1202 and an index section.
1203 The filter section provides some widgets for selecting
1204 which issues appear in the index.  The index section is
1205 a table of issues.
1207 Index View Specifiers
1208 """""""""""""""""""""
1210 An index view specifier looks like this (whitespace
1211 has been added for clarity)::
1213     /issue?status=unread,in-progress,resolved&amp;
1214         topic=security,ui&amp;
1215         :group=priority&amp;
1216         :sort=-activity&amp;
1217         :filters=status,topic&amp;
1218         :columns=title,status,fixer
1221 The index view is determined by two parts of the
1222 specifier: the layout part and the filter part.
1223 The layout part consists of the query parameters that
1224 begin with colons, and it determines the way that the
1225 properties of selected items are displayed.
1226 The filter part consists of all the other query parameters,
1227 and it determines the criteria by which items 
1228 are selected for display.
1230 The filter part is interactively manipulated with
1231 the form widgets displayed in the filter section.  The
1232 layout part is interactively manipulated by clicking
1233 on the column headings in the table.
1235 The filter part selects the union of the
1236 sets of issues with values matching any specified Link
1237 properties and the intersection of the sets
1238 of issues with values matching any specified Multilink
1239 properties.
1241 The example specifies an index of "issue" items.
1242 Only issues with a "status" of either
1243 "unread" or "in-progres" or "resolved" are displayed,
1244 and only issues with "topic" values including both
1245 "security" and "ui" are displayed.  The issues
1246 are grouped by priority, arranged in ascending order;
1247 and within groups, sorted by activity, arranged in
1248 descending order.  The filter section shows filters
1249 for the "status" and "topic" properties, and the
1250 table includes columns for the "title", "status", and
1251 "fixer" properties.
1253 Associated with each issue class is a default
1254 layout specifier.  The layout specifier in the above
1255 example is the default layout to be provided with
1256 the default bug-tracker schema described above in
1257 section 4.4.
1259 Filter Section
1260 """"""""""""""
1262 The template for a filter section provides the
1263 filtering widgets at the top of the index view.
1264 Fragments enclosed in ``<property>...</property>``
1265 tags are included or omitted depending on whether the
1266 view specifier requests a filter for a particular property.
1268 Here's a simple example of a filter template::
1270     <property name=status>
1271         <display call="checklist('status')">
1272     </property>
1273     <br>
1274     <property name=priority>
1275         <display call="checklist('priority')">
1276     </property>
1277     <br>
1278     <property name=fixer>
1279         <display call="menu('fixer')">
1280     </property>
1282 Index Section
1283 """""""""""""
1285 The template for an index section describes one row of
1286 the index table.
1287 Fragments enclosed in ``<property>...</property>``
1288 tags are included or omitted depending on whether the
1289 view specifier requests a column for a particular property.
1290 The table cells should contain <display> tags
1291 to display the values of the issue's properties.
1293 Here's a simple example of an index template::
1295     <tr>
1296         <property name=title>
1297             <td><display call="plain('title', max=50)"></td>
1298         </property>
1299         <property name=status>
1300             <td><display call="plain('status')"></td>
1301         </property>
1302         <property name=fixer>
1303             <td><display call="plain('fixer')"></td>
1304         </property>
1305     </tr>
1307 Sorting
1308 """""""
1310 String and Date values are sorted in the natural way.
1311 Link properties are sorted according to the value of the
1312 "order" property on the linked items if it is present; or
1313 otherwise on the key string of the linked items; or
1314 finally on the item ids.  Multilink properties are
1315 sorted according to how many links are present.
1317 Issue Views
1318 ~~~~~~~~~~~
1320 An issue view contains an editor section and a spool section.
1321 At the top of an issue view, links to superseding and superseded
1322 issues are always displayed.
1324 Issue View Specifiers
1325 """""""""""""""""""""
1327 An issue view specifier is simply the issue's designator::
1329     /patch23
1332 Editor Section
1333 """"""""""""""
1335 The editor section is generated from a template
1336 containing <display> tags to insert
1337 the appropriate widgets for editing properties.
1339 Here's an example of a basic editor template::
1341     <table>
1342     <tr>
1343         <td colspan=2>
1344             <display call="field('title', size=60)">
1345         </td>
1346     </tr>
1347     <tr>
1348         <td>
1349             <display call="field('fixer', size=30)">
1350         </td>
1351         <td>
1352             <display call="menu('status')>
1353         </td>
1354     </tr>
1355     <tr>
1356         <td>
1357             <display call="field('nosy', size=30)">
1358         </td>
1359         <td>
1360             <display call="menu('priority')>
1361         </td>
1362     </tr>
1363     <tr>
1364         <td colspan=2>
1365             <display call="note()">
1366         </td>
1367     </tr>
1368     </table>
1370 As shown in the example, the editor template can also
1371 request the display of a "note" field, which is a
1372 text area for entering a note to go along with a change.
1374 When a change is submitted, the system automatically
1375 generates a message describing the changed properties.
1376 The message displays all of the property values on the
1377 issue and indicates which ones have changed.
1378 An example of such a message might be this::
1380     title: Polly Parrot is dead
1381     priority: critical
1382     status: unread -> in-progress
1383     fixer: (none)
1384     keywords: parrot,plumage,perch,nailed,dead
1386 If a note is given in the "note" field, the note is
1387 appended to the description.  The message is then added
1388 to the issue's message spool (thus triggering the standard
1389 detector to react by sending out this message to the nosy list).
1391 Spool Section
1392 """""""""""""
1394 The spool section lists messages in the issue's "messages"
1395 property.  The index of messages displays the "date", "author",
1396 and "summary" properties on the message items, and selecting a
1397 message takes you to its content.
1399 Access Control
1400 --------------
1402 At each point that requires an action to be performed, the security mechanisms
1403 are asked if the current user has permission. This permission is defined as a
1404 Permission.
1406 Individual assignment of Permission to user is unwieldy. The concept of a
1407 Role, which encompasses several Permissions and may be assigned to many Users,
1408 is quite well developed in many projects. Roundup will take this path, and
1409 allow the multiple assignment of Roles to Users, and multiple Permissions to
1410 Roles. These definitions are not persistent - they're defined when the
1411 application initialises.
1413 There will be two levels of Permission. The Class level permissions define
1414 logical permissions associated with all items of a particular class (or all
1415 classes). The Node level permissions define logical permissions associated
1416 with specific items by way of their user-linked properties.
1419 Access Control Interface Specification
1420 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1422 The security module defines::
1424     class Permission:
1425         ''' Defines a Permission with the attributes
1426             - name
1427             - description
1428             - klass (optional)
1430             The klass may be unset, indicating that this permission is not
1431             locked to a particular hyperdb class. There may be multiple
1432             Permissions for the same name for different classes.
1433         '''
1435     class Role:
1436         ''' Defines a Role with the attributes
1437             - name
1438             - description
1439             - permissions
1440         '''
1442     class Security:
1443         def __init__(self, db):
1444             ''' Initialise the permission and role stores, and add in the
1445                 base roles (for admin user).
1446             '''
1448         def getPermission(self, permission, classname=None):
1449             ''' Find the Permission matching the name and for the class, if the
1450                 classname is specified.
1452                 Raise ValueError if there is no exact match.
1453             '''
1455         def hasPermission(self, permission, userid, classname=None):
1456             ''' Look through all the Roles, and hence Permissions, and see if
1457                 "permission" is there for the specified classname.
1458             '''
1460         def hasNodePermission(self, classname, itemid, **propspec):
1461             ''' Check the named properties of the given item to see if the
1462                 userid appears in them. If it does, then the user is granted
1463                 this permission check.
1465                 'propspec' consists of a set of properties and values that
1466                 must be present on the given item for access to be granted.
1468                 If a property is a Link, the value must match the property
1469                 value. If a property is a Multilink, the value must appear
1470                 in the Multilink list.
1471             '''
1473         def addPermission(self, **propspec):
1474             ''' Create a new Permission with the properties defined in
1475                 'propspec'
1476             '''
1478         def addRole(self, **propspec):
1479             ''' Create a new Role with the properties defined in 'propspec'
1480             '''
1482         def addPermissionToRole(self, rolename, permission):
1483             ''' Add the permission to the role's permission list.
1485                 'rolename' is the name of the role to add permission to.
1486             '''
1488 Modules such as ``cgi/client.py`` and ``mailgw.py`` define their own
1489 permissions like so (this example is ``cgi/client.py``)::
1491     def initialiseSecurity(security):
1492         ''' Create some Permissions and Roles on the security object
1494             This function is directly invoked by security.Security.__init__()
1495             as a part of the Security object instantiation.
1496         '''
1497         p = security.addPermission(name="Web Registration",
1498             description="Anonymous users may register through the web")
1499         security.addToRole('Anonymous', p)
1501 Detectors may also define roles in their init() function::
1503     def init(db):
1504         # register an auditor that checks that a user has the "May Resolve"
1505         # Permission before allowing them to set an issue status to "resolved"
1506         db.issue.audit('set', checkresolvedok)
1507         p = db.security.addPermission(name="May Resolve", klass="issue")
1508         security.addToRole('Manager', p)
1510 The tracker dbinit module then has in ``open()``::
1512     # open the database - it must be modified to init the Security class
1513     # from security.py as db.security
1514     db = Database(config, name)
1516     # add some extra permissions and associate them with roles
1517     ei = db.security.addPermission(name="Edit", klass="issue",
1518                     description="User is allowed to edit issues")
1519     db.security.addPermissionToRole('User', ei)
1520     ai = db.security.addPermission(name="View", klass="issue",
1521                     description="User is allowed to access issues")
1522     db.security.addPermissionToRole('User', ai)
1524 In the dbinit ``init()``::
1526     # create the two default users
1527     user.create(username="admin", password=Password(adminpw),
1528                 address=config.ADMIN_EMAIL, roles='Admin')
1529     user.create(username="anonymous", roles='Anonymous')
1531 Then in the code that matters, calls to ``hasPermission`` and
1532 ``hasNodePermission`` are made to determine if the user has permission
1533 to perform some action::
1535     if db.security.hasPermission('issue', 'Edit', userid):
1536         # all ok
1538     if db.security.hasNodePermission('issue', itemid, assignedto=userid):
1539         # all ok
1541 Code in the core will make use of these methods, as should code in auditors in
1542 custom templates. The htmltemplate will implement a new tag, ``<require>``
1543 which has the form::
1545   <require permission="name,name,name" assignedto="$userid" status="open">
1546    HTML to display if the user has the permission.
1547   <else>
1548    HTML to display if the user does not have the permission.
1549   </require>
1551 where:
1553 - the permission attribute gives a comma-separated list of permission names.
1554   These are checked in turn using ``hasPermission`` and requires one to
1555   be OK.
1556 - the other attributes are lookups on the item using ``hasNodePermission``. If
1557   the attribute value is "$userid" then the current user's userid is tested.
1559 Any of these tests must pass or the ``<require>`` check will fail. The section
1560 of html within the side of the ``<else>`` that fails is remove from processing.
1562 Authentication of Users
1563 ~~~~~~~~~~~~~~~~~~~~~~~
1565 Users must be authenticated correctly for the above controls to work. This is
1566 not done in the current mail gateway at all. Use of digital signing of
1567 messages could alleviate this problem.
1569 The exact mechanism of registering the digital signature should be flexible,
1570 with perhaps a level of trust. Users who supply their signature through their
1571 first message into the tracker should be at a lower level of trust to those
1572 who supply their signature to an admin for submission to their user details.
1575 Anonymous Users
1576 ~~~~~~~~~~~~~~~
1578 The "anonymous" user must always exist, and defines the access permissions for
1579 anonymous users. Unknown users accessing Roundup through the web or email
1580 interfaces will be logged in as the "anonymous" user.
1583 Use Cases
1584 ~~~~~~~~~
1586 public - end users can submit bugs, request new features, request support
1587     The Users would be given the default "User" Role which gives "View" and
1588     "Edit" Permission to the "issue" class.
1589 developer - developers can fix bugs, implement new features, provide support
1590     A new Role "Developer" is created with the Permission "Fixer" which is
1591     checked for in custom auditors that see whether the issue is being
1592     resolved with a particular resolution ("fixed", "implemented",
1593     "supported") and allows that resolution only if the permission is
1594     available.
1595 manager - approvers/managers can approve new features and signoff bug fixes
1596     A new Role "Manager" is created with the Permission "Signoff" which is
1597     checked for in custom auditors that see whether the issue status is being
1598     changed similar to the developer example.
1599 admin - administrators can add users and set user's roles
1600     The existing Role "Admin" has the Permissions "Edit" for all classes
1601     (including "user") and "Web Roles" which allow the desired actions.
1602 system - automated request handlers running various report/escalation scripts
1603     A combination of existing and new Roles, Permissions and auditors could
1604     be used here.
1605 privacy - issues that are only visible to some users
1606     A new property is added to the issue which marks the user or group of
1607     users who are allowed to view and edit the issue. An auditor will check
1608     for edit access, and the htmltemplate <require> tag can check for view
1609     access.
1612 Deployment Scenarios
1613 --------------------
1615 The design described above should be general enough
1616 to permit the use of Roundup for bug tracking, managing
1617 projects, managing patches, or holding discussions.  By
1618 using items of multiple types, one could deploy a system
1619 that maintains requirement specifications, catalogs bugs,
1620 and manages submitted patches, where patches could be
1621 linked to the bugs and requirements they address.
1624 Acknowledgements
1625 ----------------
1627 My thanks are due to Christy Heyl for 
1628 reviewing and contributing suggestions to this paper
1629 and motivating me to get it done, and to
1630 Jesse Vincent, Mark Miller, Christopher Simons,
1631 Jeff Dunmall, Wayne Gramlich, and Dean Tribble for
1632 their assistance with the first-round submission.
1634 Changes to this document
1635 ------------------------
1637 - Added Boolean and Number types
1638 - Added section Hyperdatabase Implementations
1639 - "Item" has been renamed to "Issue" to account for the more specific nature
1640   of the Class.