Code

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