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