Code

don't display Editing on read-only pages (sf bug 651967)
[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 Items and Classes
193 ~~~~~~~~~~~~~~~~~
195 Items 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".  Item 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 Item 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 Item:
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 search 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 the ``cgi.templating`` module.
1147 Displayer functions are triggered by ``tal:content`` or ``tal:replace``
1148 tag attributes in templates.  The value of the attribute
1149 provides an expression for calling the displayer function.
1150 For example, the occurrence of::
1152     tal:content="context/status/plain"
1154 in a template triggers a call to::
1155     
1156     context['status'].plain()
1158 where the context would be an item of the "issue" class.  The displayer
1159 functions can accept extra arguments to further specify
1160 details about the widgets that should be generated.
1162 Some of the standard displayer functions include:
1164 ========= ====================================================================
1165 Function  Description
1166 ========= ====================================================================
1167 plain     display a String property directly;
1168           display a Date property in a specified time zone with an option
1169           to omit the time from the date stamp; for a Link or Multilink
1170           property, display the key strings of the linked items (or the
1171           ids if the linked class has no key property)
1172 field     display a property like the plain displayer above, but in a text
1173           field to be edited
1174 menu      for a Link property, display a menu of the available choices
1175 ========= ====================================================================
1177 See the `customisation`_ documentation for the complete list.
1180 Index Views
1181 ~~~~~~~~~~~
1183 XXX The following needs to be clearer
1185 An index view contains two sections: a filter section
1186 and an index section.
1187 The filter section provides some widgets for selecting
1188 which issues appear in the index.  The index section is
1189 a table of issues.
1191 Index View Specifiers
1192 """""""""""""""""""""
1194 An index view specifier looks like this (whitespace
1195 has been added for clarity)::
1197     /issue?status=unread,in-progress,resolved&
1198         topic=security,ui&
1199         :group=priority&
1200         :sort=-activity&
1201         :filters=status,topic&
1202         :columns=title,status,fixer
1205 The index view is determined by two parts of the
1206 specifier: the layout part and the filter part.
1207 The layout part consists of the query parameters that
1208 begin with colons, and it determines the way that the
1209 properties of selected items are displayed.
1210 The filter part consists of all the other query parameters,
1211 and it determines the criteria by which items 
1212 are selected for display.
1214 The filter part is interactively manipulated with
1215 the form widgets displayed in the filter section.  The
1216 layout part is interactively manipulated by clicking
1217 on the column headings in the table.
1219 The filter part selects the union of the
1220 sets of issues with values matching any specified Link
1221 properties and the intersection of the sets
1222 of issues with values matching any specified Multilink
1223 properties.
1225 The example specifies an index of "issue" items.
1226 Only issues with a "status" of either
1227 "unread" or "in-progres" or "resolved" are displayed,
1228 and only issues with "topic" values including both
1229 "security" and "ui" are displayed.  The issues
1230 are grouped by priority, arranged in ascending order;
1231 and within groups, sorted by activity, arranged in
1232 descending order.  The filter section shows filters
1233 for the "status" and "topic" properties, and the
1234 table includes columns for the "title", "status", and
1235 "fixer" properties.
1237 Associated with each issue class is a default
1238 layout specifier.  The layout specifier in the above
1239 example is the default layout to be provided with
1240 the default bug-tracker schema described above in
1241 section 4.4.
1243 Index Section
1244 """""""""""""
1246 The template for an index section describes one row of
1247 the index table.
1248 Fragments enclosed in ``<property>...</property>``
1249 tags are included or omitted depending on whether the
1250 view specifier requests a column for a particular property.
1251 The table cells should contain <display> tags
1252 to display the values of the issue's properties.
1254 Here's a simple example of an index template::
1256     <tr>
1257       <td tal:condition="request/show/title" tal:content="contex/title"></td>
1258       <td tal:condition="request/show/status" tal:content="contex/status"></td>
1259       <td tal:condition="request/show/fixer" tal:content="contex/fixer"></td>
1260     </tr>
1262 Sorting
1263 """""""
1265 String and Date values are sorted in the natural way.
1266 Link properties are sorted according to the value of the
1267 "order" property on the linked items if it is present; or
1268 otherwise on the key string of the linked items; or
1269 finally on the item ids.  Multilink properties are
1270 sorted according to how many links are present.
1272 Issue Views
1273 ~~~~~~~~~~~
1275 An issue view contains an editor section and a spool section.
1276 At the top of an issue view, links to superseding and superseded
1277 issues are always displayed.
1279 Issue View Specifiers
1280 """""""""""""""""""""
1282 An issue view specifier is simply the issue's designator::
1284     /patch23
1287 Editor Section
1288 """"""""""""""
1290 The editor section is generated from a template
1291 containing <display> tags to insert
1292 the appropriate widgets for editing properties.
1294 Here's an example of a basic editor template::
1296     <table>
1297     <tr>
1298         <td colspan=2 tal:content="python:context.title.field(size='60')"></td>
1299     </tr>
1300     <tr>
1301         <td tal:content="context/fixer/field"></td>
1302         <td tal:content="context/status/menu"></td>
1303     </tr>
1304     <tr>
1305         <td tal:content="context/nosy/field"></td>
1306         <td tal:content="context/priority/menu"></td>
1307     </tr>
1308     <tr>
1309         <td colspan=2>
1310           <textarea name=":note" rows=5 cols=60></textarea>
1311         </td>
1312     </tr>
1313     </table>
1315 As shown in the example, the editor template can also include a ":note" field,
1316 which is a text area for entering a note to go along with a change.
1318 When a change is submitted, the system automatically
1319 generates a message describing the changed properties.
1320 The message displays all of the property values on the
1321 issue and indicates which ones have changed.
1322 An example of such a message might be this::
1324     title: Polly Parrot is dead
1325     priority: critical
1326     status: unread -> in-progress
1327     fixer: (none)
1328     keywords: parrot,plumage,perch,nailed,dead
1330 If a note is given in the ":note" field, the note is
1331 appended to the description.  The message is then added
1332 to the issue's message spool (thus triggering the standard
1333 detector to react by sending out this message to the nosy list).
1335 Spool Section
1336 """""""""""""
1338 The spool section lists messages in the issue's "messages"
1339 property.  The index of messages displays the "date", "author",
1340 and "summary" properties on the message items, and selecting a
1341 message takes you to its content.
1343 Access Control
1344 --------------
1346 At each point that requires an action to be performed, the security mechanisms
1347 are asked if the current user has permission. This permission is defined as a
1348 Permission.
1350 Individual assignment of Permission to user is unwieldy. The concept of a
1351 Role, which encompasses several Permissions and may be assigned to many Users,
1352 is quite well developed in many projects. Roundup will take this path, and
1353 allow the multiple assignment of Roles to Users, and multiple Permissions to
1354 Roles. These definitions are not persistent - they're defined when the
1355 application initialises.
1357 There will be two levels of Permission. The Class level permissions define
1358 logical permissions associated with all items of a particular class (or all
1359 classes). The Item level permissions define logical permissions associated
1360 with specific items by way of their user-linked properties.
1363 Access Control Interface Specification
1364 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1366 The security module defines::
1368     class Permission:
1369         ''' Defines a Permission with the attributes
1370             - name
1371             - description
1372             - klass (optional)
1374             The klass may be unset, indicating that this permission is not
1375             locked to a particular hyperdb class. There may be multiple
1376             Permissions for the same name for different classes.
1377         '''
1379     class Role:
1380         ''' Defines a Role with the attributes
1381             - name
1382             - description
1383             - permissions
1384         '''
1386     class Security:
1387         def __init__(self, db):
1388             ''' Initialise the permission and role stores, and add in the
1389                 base roles (for admin user).
1390             '''
1392         def getPermission(self, permission, classname=None):
1393             ''' Find the Permission matching the name and for the class, if the
1394                 classname is specified.
1396                 Raise ValueError if there is no exact match.
1397             '''
1399         def hasPermission(self, permission, userid, classname=None):
1400             ''' Look through all the Roles, and hence Permissions, and see if
1401                 "permission" is there for the specified classname.
1402             '''
1404         def hasItemPermission(self, classname, itemid, **propspec):
1405             ''' Check the named properties of the given item to see if the
1406                 userid appears in them. If it does, then the user is granted
1407                 this permission check.
1409                 'propspec' consists of a set of properties and values that
1410                 must be present on the given item for access to be granted.
1412                 If a property is a Link, the value must match the property
1413                 value. If a property is a Multilink, the value must appear
1414                 in the Multilink list.
1415             '''
1417         def addPermission(self, **propspec):
1418             ''' Create a new Permission with the properties defined in
1419                 'propspec'
1420             '''
1422         def addRole(self, **propspec):
1423             ''' Create a new Role with the properties defined in 'propspec'
1424             '''
1426         def addPermissionToRole(self, rolename, permission):
1427             ''' Add the permission to the role's permission list.
1429                 'rolename' is the name of the role to add permission to.
1430             '''
1432 Modules such as ``cgi/client.py`` and ``mailgw.py`` define their own
1433 permissions like so (this example is ``cgi/client.py``)::
1435     def initialiseSecurity(security):
1436         ''' Create some Permissions and Roles on the security object
1438             This function is directly invoked by security.Security.__init__()
1439             as a part of the Security object instantiation.
1440         '''
1441         p = security.addPermission(name="Web Registration",
1442             description="Anonymous users may register through the web")
1443         security.addToRole('Anonymous', p)
1445 Detectors may also define roles in their init() function::
1447     def init(db):
1448         # register an auditor that checks that a user has the "May Resolve"
1449         # Permission before allowing them to set an issue status to "resolved"
1450         db.issue.audit('set', checkresolvedok)
1451         p = db.security.addPermission(name="May Resolve", klass="issue")
1452         security.addToRole('Manager', p)
1454 The tracker dbinit module then has in ``open()``::
1456     # open the database - it must be modified to init the Security class
1457     # from security.py as db.security
1458     db = Database(config, name)
1460     # add some extra permissions and associate them with roles
1461     ei = db.security.addPermission(name="Edit", klass="issue",
1462                     description="User is allowed to edit issues")
1463     db.security.addPermissionToRole('User', ei)
1464     ai = db.security.addPermission(name="View", klass="issue",
1465                     description="User is allowed to access issues")
1466     db.security.addPermissionToRole('User', ai)
1468 In the dbinit ``init()``::
1470     # create the two default users
1471     user.create(username="admin", password=Password(adminpw),
1472                 address=config.ADMIN_EMAIL, roles='Admin')
1473     user.create(username="anonymous", roles='Anonymous')
1475 Then in the code that matters, calls to ``hasPermission`` and
1476 ``hasItemPermission`` are made to determine if the user has permission
1477 to perform some action::
1479     if db.security.hasPermission('issue', 'Edit', userid):
1480         # all ok
1482     if db.security.hasItemPermission('issue', itemid, assignedto=userid):
1483         # all ok
1485 Code in the core will make use of these methods, as should code in auditors in
1486 custom templates. The HTML templating may access the access controls through
1487 the *user* attribute of the *request* variable. It exposes a ``hasPermission()``
1488 method::
1490   tal:condition="python:request.user.hasPermission('Edit', 'issue')"
1492 or, if the *context* is *issue*, then the following is the same::
1494   tal:condition="python:request.user.hasPermission('Edit')"
1497 Authentication of Users
1498 ~~~~~~~~~~~~~~~~~~~~~~~
1500 Users must be authenticated correctly for the above controls to work. This is
1501 not done in the current mail gateway at all. Use of digital signing of
1502 messages could alleviate this problem.
1504 The exact mechanism of registering the digital signature should be flexible,
1505 with perhaps a level of trust. Users who supply their signature through their
1506 first message into the tracker should be at a lower level of trust to those
1507 who supply their signature to an admin for submission to their user details.
1510 Anonymous Users
1511 ~~~~~~~~~~~~~~~
1513 The "anonymous" user must always exist, and defines the access permissions for
1514 anonymous users. Unknown users accessing Roundup through the web or email
1515 interfaces will be logged in as the "anonymous" user.
1518 Use Cases
1519 ~~~~~~~~~
1521 public - end users can submit bugs, request new features, request support
1522     The Users would be given the default "User" Role which gives "View" and
1523     "Edit" Permission to the "issue" class.
1524 developer - developers can fix bugs, implement new features, provide support
1525     A new Role "Developer" is created with the Permission "Fixer" which is
1526     checked for in custom auditors that see whether the issue is being
1527     resolved with a particular resolution ("fixed", "implemented",
1528     "supported") and allows that resolution only if the permission is
1529     available.
1530 manager - approvers/managers can approve new features and signoff bug fixes
1531     A new Role "Manager" is created with the Permission "Signoff" which is
1532     checked for in custom auditors that see whether the issue status is being
1533     changed similar to the developer example.
1534 admin - administrators can add users and set user's roles
1535     The existing Role "Admin" has the Permissions "Edit" for all classes
1536     (including "user") and "Web Roles" which allow the desired actions.
1537 system - automated request handlers running various report/escalation scripts
1538     A combination of existing and new Roles, Permissions and auditors could
1539     be used here.
1540 privacy - issues that are only visible to some users
1541     A new property is added to the issue which marks the user or group of
1542     users who are allowed to view and edit the issue. An auditor will check
1543     for edit access, and the template user object can check for view access.
1546 Deployment Scenarios
1547 --------------------
1549 The design described above should be general enough
1550 to permit the use of Roundup for bug tracking, managing
1551 projects, managing patches, or holding discussions.  By
1552 using items of multiple types, one could deploy a system
1553 that maintains requirement specifications, catalogs bugs,
1554 and manages submitted patches, where patches could be
1555 linked to the bugs and requirements they address.
1558 Acknowledgements
1559 ----------------
1561 My thanks are due to Christy Heyl for 
1562 reviewing and contributing suggestions to this paper
1563 and motivating me to get it done, and to
1564 Jesse Vincent, Mark Miller, Christopher Simons,
1565 Jeff Dunmall, Wayne Gramlich, and Dean Tribble for
1566 their assistance with the first-round submission.
1568 Changes to this document
1569 ------------------------
1571 - Added Boolean and Number types
1572 - Added section Hyperdatabase Implementations
1573 - "Item" has been renamed to "Issue" to account for the more specific nature
1574   of the Class.
1575 - New Templating
1576 - Access Controls
1578 ------------------
1580 Back to `Table of Contents`_
1582 .. _`Table of Contents`: index.html
1583 .. _customisation: customizing.html