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