Mercurial > p > roundup > code
comparison doc/design.txt @ 1356:83f33642d220 maint-0.5
[[Metadata associated with this commit was garbled during conversion from CVS
to Subversion.]]
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Thu, 09 Jan 2003 22:59:22 +0000 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 1242:3d0158c8c32b | 1356:83f33642d220 |
|---|---|
| 1 ======================================================== | |
| 2 Roundup - An Issue-Tracking System for Knowledge Workers | |
| 3 ======================================================== | |
| 4 | |
| 5 :Authors: Ka-Ping Yee (original__), Richard Jones (implementation) | |
| 6 | |
| 7 __ spec.html | |
| 8 | |
| 9 .. contents:: | |
| 10 | |
| 11 Introduction | |
| 12 --------------- | |
| 13 | |
| 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. | |
| 21 | |
| 22 | |
| 23 The Layer Cake | |
| 24 ----------------- | |
| 25 | |
| 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:: | |
| 30 | |
| 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 ------------------------------------------------------------------------- | |
| 42 | |
| 43 The colourful parts of the cake are part of our system; | |
| 44 the faint grey parts of the cake are external components. | |
| 45 | |
| 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. | |
| 49 | |
| 50 | |
| 51 Hyperdatabase | |
| 52 ------------- | |
| 53 | |
| 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. | |
| 58 | |
| 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. | |
| 64 | |
| 65 Dates and Date Arithmetic | |
| 66 ~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 67 | |
| 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. | |
| 73 | |
| 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. | |
| 86 | |
| 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. | |
| 95 | |
| 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" | |
| 106 | |
| 107 | |
| 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). | |
| 112 | |
| 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 | |
| 120 | |
| 121 | |
| 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). | |
| 130 | |
| 131 Here is an outline of the Date and Interval classes:: | |
| 132 | |
| 133 class Date: | |
| 134 def __init__(self, spec, offset): | |
| 135 """Construct a date given a specification and a time zone offset. | |
| 136 | |
| 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 """ | |
| 141 | |
| 142 def __add__(self, interval): | |
| 143 """Add an interval to this date to produce another date.""" | |
| 144 | |
| 145 def __sub__(self, interval): | |
| 146 """Subtract an interval from this date to produce another date.""" | |
| 147 | |
| 148 def __cmp__(self, other): | |
| 149 """Compare this date to another date.""" | |
| 150 | |
| 151 def __str__(self): | |
| 152 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format.""" | |
| 153 | |
| 154 def local(self, offset): | |
| 155 """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.""" | |
| 156 | |
| 157 class Interval: | |
| 158 def __init__(self, spec): | |
| 159 """Construct an interval given a specification.""" | |
| 160 | |
| 161 def __cmp__(self, other): | |
| 162 """Compare this interval to another interval.""" | |
| 163 | |
| 164 def __str__(self): | |
| 165 """Return this interval as a string.""" | |
| 166 | |
| 167 | |
| 168 | |
| 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:: | |
| 172 | |
| 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> | |
| 191 | |
| 192 Items and Classes | |
| 193 ~~~~~~~~~~~~~~~~~ | |
| 194 | |
| 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. | |
| 200 | |
| 201 Identifiers and Designators | |
| 202 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 203 | |
| 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. | |
| 211 | |
| 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. | |
| 220 | |
| 221 Property Names and Types | |
| 222 ~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 223 | |
| 224 Property names must begin with a letter. | |
| 225 | |
| 226 A property may be one of five basic types: | |
| 227 | |
| 228 - String properties are for storing arbitrary-length strings. | |
| 229 | |
| 230 - Boolean properties are for storing true/false, or yes/no values. | |
| 231 | |
| 232 - Number properties are for storing numeric values. | |
| 233 | |
| 234 - Date properties store date-and-time stamps. | |
| 235 Their values are Timestamp objects. | |
| 236 | |
| 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. | |
| 240 | |
| 241 - A Multilink property refers to possibly many items | |
| 242 in a specified class. The value is a list of integers. | |
| 243 | |
| 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. | |
| 246 | |
| 247 A property that is not specified will return as None from a *get* | |
| 248 operation. | |
| 249 | |
| 250 Hyperdb Interface Specification | |
| 251 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 252 | |
| 253 TODO: replace the Interface Specifications with links to the pydoc | |
| 254 | |
| 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:: | |
| 258 | |
| 259 class String: | |
| 260 def __init__(self, indexme='no'): | |
| 261 """An object designating a String property.""" | |
| 262 | |
| 263 class Boolean: | |
| 264 def __init__(self): | |
| 265 """An object designating a Boolean property.""" | |
| 266 | |
| 267 class Number: | |
| 268 def __init__(self): | |
| 269 """An object designating a Number property.""" | |
| 270 | |
| 271 class Date: | |
| 272 def __init__(self): | |
| 273 """An object designating a Date property.""" | |
| 274 | |
| 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. | |
| 279 | |
| 280 If the do_journal argument is not 'yes' then changes to | |
| 281 the property are not journalled in the linked item. | |
| 282 """ | |
| 283 | |
| 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. | |
| 288 | |
| 289 If the do_journal argument is not 'yes' then changes to | |
| 290 the property are not journalled in the linked item(s). | |
| 291 """ | |
| 292 | |
| 293 | |
| 294 Here is the interface provided by the hyperdatabase:: | |
| 295 | |
| 296 class Database: | |
| 297 """A database for storing records containing flexible data types.""" | |
| 298 | |
| 299 def __init__(self, config, journaltag=None): | |
| 300 """Open a hyperdatabase given a specifier to some storage. | |
| 301 | |
| 302 The 'storagelocator' is obtained from config.DATABASE. | |
| 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. | |
| 307 | |
| 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 """ | |
| 313 | |
| 314 def __getattr__(self, classname): | |
| 315 """A convenient way of calling self.getclass(classname).""" | |
| 316 | |
| 317 def getclasses(self): | |
| 318 """Return a list of the names of all existing classes.""" | |
| 319 | |
| 320 def getclass(self, classname): | |
| 321 """Get the Class object representing a particular class. | |
| 322 | |
| 323 If 'classname' is not a valid class name, a KeyError is raised. | |
| 324 """ | |
| 325 | |
| 326 class Class: | |
| 327 """The handle to a particular class of items in a hyperdatabase.""" | |
| 328 | |
| 329 def __init__(self, db, classname, **properties): | |
| 330 """Create a new class with a given name and property specification. | |
| 331 | |
| 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 """ | |
| 336 | |
| 337 # Editing items: | |
| 338 | |
| 339 def create(self, **propvalues): | |
| 340 """Create a new item of this class and return its id. | |
| 341 | |
| 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 """ | |
| 351 | |
| 352 def get(self, itemid, propname): | |
| 353 """Get the value of a property on an existing item of this class. | |
| 354 | |
| 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 """ | |
| 359 | |
| 360 def set(self, itemid, **propvalues): | |
| 361 """Modify a property on an existing item of this class. | |
| 362 | |
| 363 'itemid' must be the id of an existing item of this class or an | |
| 364 IndexError is raised. Each key in 'propvalues' must be the name | |
| 365 of a property of this class or a KeyError is raised. All values | |
| 366 in 'propvalues' must be acceptable types for their corresponding | |
| 367 properties or a TypeError is raised. If the value of the key | |
| 368 property is set, it must not collide with other key strings or a | |
| 369 ValueError is raised. If the value of a Link or Multilink | |
| 370 property contains an invalid item id, a ValueError is raised. | |
| 371 """ | |
| 372 | |
| 373 def retire(self, itemid): | |
| 374 """Retire an item. | |
| 375 | |
| 376 The properties on the item remain available from the get() method, | |
| 377 and the item's id is never reused. Retired items are not returned | |
| 378 by the find(), list(), or lookup() methods, and other items may | |
| 379 reuse the values of their key properties. | |
| 380 """ | |
| 381 | |
| 382 def history(self, itemid): | |
| 383 """Retrieve the journal of edits on a particular item. | |
| 384 | |
| 385 'itemid' must be the id of an existing item of this class or an | |
| 386 IndexError is raised. | |
| 387 | |
| 388 The returned list contains tuples of the form | |
| 389 | |
| 390 (date, tag, action, params) | |
| 391 | |
| 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: | |
| 395 | |
| 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 """ | |
| 400 | |
| 401 # Locating items: | |
| 402 | |
| 403 def setkey(self, propname): | |
| 404 """Select a String property of this class to be the key property. | |
| 405 | |
| 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 """ | |
| 410 | |
| 411 def getkey(self): | |
| 412 """Return the name of the key property for this class or None.""" | |
| 413 | |
| 414 def lookup(self, keyvalue): | |
| 415 """Locate a particular item by its key property and return its id. | |
| 416 | |
| 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 """ | |
| 422 | |
| 423 def find(self, propname, itemid): | |
| 424 """Get the ids of items in this class which link to the given items. | |
| 425 | |
| 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. | |
| 430 | |
| 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: | |
| 435 | |
| 436 db.issue.find(messages={'1':1,'3':1}, files={'7':1}) | |
| 437 """ | |
| 438 | |
| 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 ''' | |
| 444 | |
| 445 def list(self): | |
| 446 """Return a list of the ids of the active items in this class.""" | |
| 447 | |
| 448 def count(self): | |
| 449 """Get the number of items in this class. | |
| 450 | |
| 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 """ | |
| 455 | |
| 456 # Manipulating properties: | |
| 457 | |
| 458 def getprops(self): | |
| 459 """Return a dictionary mapping property names to property objects.""" | |
| 460 | |
| 461 def addprop(self, **properties): | |
| 462 """Add properties to this class. | |
| 463 | |
| 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 """ | |
| 469 | |
| 470 def getitem(self, itemid, cache=1): | |
| 471 ''' Return a Item convenience wrapper for the item. | |
| 472 | |
| 473 'itemid' must be the id of an existing item of this class or an | |
| 474 IndexError is raised. | |
| 475 | |
| 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 ''' | |
| 481 | |
| 482 class Item: | |
| 483 ''' A convenience wrapper for the given item. It provides a mapping | |
| 484 interface to a single item's properties | |
| 485 ''' | |
| 486 | |
| 487 Hyperdatabase Implementations | |
| 488 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 489 | |
| 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. | |
| 494 | |
| 495 Several implementations are provided - they belong in the roundup.backends | |
| 496 package. | |
| 497 | |
| 498 | |
| 499 Application Example | |
| 500 ~~~~~~~~~~~~~~~~~~~ | |
| 501 | |
| 502 Here is an example of how the hyperdatabase module would work in practice:: | |
| 503 | |
| 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"))] | |
| 563 | |
| 564 | |
| 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. | |
| 572 | |
| 573 | |
| 574 Roundup Database | |
| 575 ---------------- | |
| 576 | |
| 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. | |
| 583 | |
| 584 Reserved Classes | |
| 585 ~~~~~~~~~~~~~~~~ | |
| 586 | |
| 587 Internal to this layer we reserve three special classes | |
| 588 of items that are not issues. | |
| 589 | |
| 590 Users | |
| 591 """"" | |
| 592 | |
| 593 Users are stored in the hyperdatabase as items of | |
| 594 class "user". The "user" class has the definition:: | |
| 595 | |
| 596 hyperdb.Class(db, "user", username=hyperdb.String(), | |
| 597 password=hyperdb.String(), | |
| 598 address=hyperdb.String()) | |
| 599 db.user.setkey("username") | |
| 600 | |
| 601 Messages | |
| 602 """""""" | |
| 603 | |
| 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:: | |
| 613 | |
| 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")) | |
| 619 | |
| 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. | |
| 625 | |
| 626 Files | |
| 627 """"" | |
| 628 | |
| 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:: | |
| 634 | |
| 635 hyperdb.Class(db, "file", user=hyperdb.Link("user"), | |
| 636 name=hyperdb.String(), | |
| 637 type=hyperdb.String()) | |
| 638 | |
| 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. | |
| 642 | |
| 643 Issue Classes | |
| 644 ~~~~~~~~~~~~~ | |
| 645 | |
| 646 All issues have the following standard properties: | |
| 647 | |
| 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 =========== ========================== | |
| 657 | |
| 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). | |
| 667 | |
| 668 Roundupdb Interface Specification | |
| 669 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 670 | |
| 671 The interface to a Roundup database delegates most method | |
| 672 calls to the hyperdatabase, except for the following | |
| 673 changes and additional methods:: | |
| 674 | |
| 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.""" | |
| 679 | |
| 680 class Class: | |
| 681 # Overridden methods: | |
| 682 | |
| 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 """ | |
| 689 | |
| 690 # New methods: | |
| 691 | |
| 692 def audit(self, event, detector): | |
| 693 def react(self, event, detector): | |
| 694 """Register a detector (see below for more details).""" | |
| 695 | |
| 696 class IssueClass(Class): | |
| 697 # Overridden methods: | |
| 698 | |
| 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.""" | |
| 704 | |
| 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.""" | |
| 709 | |
| 710 # New methods: | |
| 711 | |
| 712 def addmessage(self, itemid, summary, text): | |
| 713 """Add a message to an issue's mail spool. | |
| 714 | |
| 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 """ | |
| 722 | |
| 723 def sendmessage(self, itemid, msgid): | |
| 724 """Send a message to the members of an issue's nosy list. | |
| 725 | |
| 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 """ | |
| 730 | |
| 731 | |
| 732 Default Schema | |
| 733 ~~~~~~~~~~~~~~ | |
| 734 | |
| 735 The default schema included with Roundup turns it into a | |
| 736 typical software bug tracker. The database is set up like this:: | |
| 737 | |
| 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") | |
| 745 | |
| 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") | |
| 756 | |
| 757 Class(db, "keyword", name=hyperdb.String()) | |
| 758 | |
| 759 Class(db, "issue", fixer=hyperdb.Multilink("user"), | |
| 760 topic=hyperdb.Multilink("keyword"), | |
| 761 priority=hyperdb.Link("priority"), | |
| 762 status=hyperdb.Link("status")) | |
| 763 | |
| 764 (The "order" property hasn't been explained yet. It | |
| 765 gets used by the Web user interface for sorting.) | |
| 766 | |
| 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:: | |
| 771 | |
| 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) | |
| 777 | |
| 778 | |
| 779 Detector Interface | |
| 780 ------------------ | |
| 781 | |
| 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. | |
| 789 | |
| 790 There are two kinds of detectors: | |
| 791 | |
| 792 1. an auditor is triggered just before modifying an item | |
| 793 2. a reactor is triggered just after an item has been modified | |
| 794 | |
| 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. | |
| 801 | |
| 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. | |
| 806 | |
| 807 Detector Interface Specification | |
| 808 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 809 | |
| 810 The ``audit()`` and ``react()`` methods | |
| 811 register detectors on a given class of items:: | |
| 812 | |
| 813 class Class: | |
| 814 def audit(self, event, detector): | |
| 815 """Register an auditor on this class. | |
| 816 | |
| 817 'event' should be one of "create", "set", or "retire". | |
| 818 'detector' should be a function accepting four arguments. | |
| 819 """ | |
| 820 | |
| 821 def react(self, event, detector): | |
| 822 """Register a reactor on this class. | |
| 823 | |
| 824 'event' should be one of "create", "set", or "retire". | |
| 825 'detector' should be a function accepting four arguments. | |
| 826 """ | |
| 827 | |
| 828 Auditors are called with the arguments:: | |
| 829 | |
| 830 audit(db, cl, itemid, newdata) | |
| 831 | |
| 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. | |
| 835 | |
| 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. | |
| 840 | |
| 841 For a ``set()`` operation, newdata | |
| 842 contains only the names and values of properties that are about | |
| 843 to be changed. | |
| 844 | |
| 845 For a ``retire()`` operation, newdata is None. | |
| 846 | |
| 847 Reactors are called with the arguments:: | |
| 848 | |
| 849 react(db, cl, itemid, olddata) | |
| 850 | |
| 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. | |
| 854 | |
| 855 For a ``create()`` | |
| 856 operation, the ``itemid`` argument is the id of the | |
| 857 newly-created item and ``olddata`` is None. | |
| 858 | |
| 859 For a ``set()`` operation, ``olddata`` | |
| 860 contains the names and previous values of properties that were changed. | |
| 861 | |
| 862 For a ``retire()`` operation, ``itemid`` is the | |
| 863 id of the retired item and ``olddata`` is None. | |
| 864 | |
| 865 Detector Example | |
| 866 ~~~~~~~~~~~~~~~~ | |
| 867 | |
| 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:: | |
| 872 | |
| 873 # Permit users only to add themselves to the "approvals" list. | |
| 874 | |
| 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." | |
| 890 | |
| 891 # When three people have approved a project, change its | |
| 892 # status from "pending" to "approved". | |
| 893 | |
| 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")) | |
| 898 | |
| 899 def init(db): | |
| 900 db.project.audit("set", check_approval) | |
| 901 db.project.react("set", approve_project) | |
| 902 | |
| 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":: | |
| 909 | |
| 910 # Only accept attempts to create new patches that come with patch files. | |
| 911 | |
| 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." | |
| 919 | |
| 920 # When the status is changed from "approved" to "applied", apply the patch. | |
| 921 | |
| 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... | |
| 926 | |
| 927 def init(db): | |
| 928 db.patch.audit("create", check_new_patch) | |
| 929 db.patch.react("set", apply_patch) | |
| 930 | |
| 931 | |
| 932 Command Interface | |
| 933 ----------------- | |
| 934 | |
| 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.) | |
| 939 | |
| 940 Command Interface Specification | |
| 941 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 942 | |
| 943 A single command, roundup, provides basic access to | |
| 944 the hyperdatabase from the command line:: | |
| 945 | |
| 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 ... | |
| 950 | |
| 951 See ``roundup-admin help commands`` for a complete list of commands. | |
| 952 | |
| 953 Property values are represented as strings in command arguments | |
| 954 and in the printed results: | |
| 955 | |
| 956 - Strings are, well, strings. | |
| 957 | |
| 958 - Numbers are displayed the same as strings. | |
| 959 | |
| 960 - Booleans are displayed as 'Yes' or 'No'. | |
| 961 | |
| 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. | |
| 965 | |
| 966 - Link values are printed as item designators. When given as | |
| 967 an argument, item designators and key strings are both accepted. | |
| 968 | |
| 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. | |
| 973 | |
| 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. | |
| 978 | |
| 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. | |
| 982 | |
| 983 Usage Example | |
| 984 ~~~~~~~~~~~~~ | |
| 985 | |
| 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:: | |
| 990 | |
| 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% | |
| 999 | |
| 1000 Or, using the -list option, this can be written as a single command:: | |
| 1001 | |
| 1002 shell% grep -l spam `roundup get \ | |
| 1003 \`roundup find -list issue status=in-progress\` messages` | |
| 1004 msg23 | |
| 1005 msg49 | |
| 1006 msg50 | |
| 1007 msg61 | |
| 1008 shell% | |
| 1009 | |
| 1010 | |
| 1011 E-mail User Interface | |
| 1012 --------------------- | |
| 1013 | |
| 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). | |
| 1018 | |
| 1019 Message Processing | |
| 1020 ~~~~~~~~~~~~~~~~~~ | |
| 1021 | |
| 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. | |
| 1032 | |
| 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. | |
| 1040 | |
| 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. | |
| 1055 | |
| 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). | |
| 1061 | |
| 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. | |
| 1066 | |
| 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. | |
| 1071 | |
| 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. | |
| 1078 | |
| 1079 Nosy Lists | |
| 1080 ~~~~~~~~~~ | |
| 1081 | |
| 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. | |
| 1091 | |
| 1092 Setting Properties | |
| 1093 ~~~~~~~~~~~~~~~~~~ | |
| 1094 | |
| 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. | |
| 1100 | |
| 1101 | |
| 1102 Web User Interface | |
| 1103 ------------------ | |
| 1104 | |
| 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. | |
| 1110 | |
| 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. | |
| 1115 | |
| 1116 Views and View Specifiers | |
| 1117 ~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1118 | |
| 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. | |
| 1124 | |
| 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. | |
| 1133 | |
| 1134 Displaying Properties | |
| 1135 ~~~~~~~~~~~~~~~~~~~~~ | |
| 1136 | |
| 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. | |
| 1143 | |
| 1144 The display of a property is handled by functions in | |
| 1145 the ``cgi.templating`` module. | |
| 1146 | |
| 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:: | |
| 1151 | |
| 1152 tal:content="context/status/plain" | |
| 1153 | |
| 1154 in a template triggers a call to:: | |
| 1155 | |
| 1156 context['status'].plain() | |
| 1157 | |
| 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. | |
| 1161 | |
| 1162 Some of the standard displayer functions include: | |
| 1163 | |
| 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 ========= ==================================================================== | |
| 1176 | |
| 1177 See the `customisation`_ documentation for the complete list. | |
| 1178 | |
| 1179 | |
| 1180 Index Views | |
| 1181 ~~~~~~~~~~~ | |
| 1182 | |
| 1183 XXX The following needs to be clearer | |
| 1184 | |
| 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. | |
| 1190 | |
| 1191 Index View Specifiers | |
| 1192 """"""""""""""""""""" | |
| 1193 | |
| 1194 An index view specifier looks like this (whitespace | |
| 1195 has been added for clarity):: | |
| 1196 | |
| 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 | |
| 1203 | |
| 1204 | |
| 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. | |
| 1213 | |
| 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. | |
| 1218 | |
| 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. | |
| 1224 | |
| 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. | |
| 1236 | |
| 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. | |
| 1242 | |
| 1243 Index Section | |
| 1244 """"""""""""" | |
| 1245 | |
| 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. | |
| 1253 | |
| 1254 Here's a simple example of an index template:: | |
| 1255 | |
| 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> | |
| 1261 | |
| 1262 Sorting | |
| 1263 """"""" | |
| 1264 | |
| 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. | |
| 1271 | |
| 1272 Issue Views | |
| 1273 ~~~~~~~~~~~ | |
| 1274 | |
| 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. | |
| 1278 | |
| 1279 Issue View Specifiers | |
| 1280 """"""""""""""""""""" | |
| 1281 | |
| 1282 An issue view specifier is simply the issue's designator:: | |
| 1283 | |
| 1284 /patch23 | |
| 1285 | |
| 1286 | |
| 1287 Editor Section | |
| 1288 """""""""""""" | |
| 1289 | |
| 1290 The editor section is generated from a template | |
| 1291 containing <display> tags to insert | |
| 1292 the appropriate widgets for editing properties. | |
| 1293 | |
| 1294 Here's an example of a basic editor template:: | |
| 1295 | |
| 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> | |
| 1314 | |
| 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. | |
| 1317 | |
| 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:: | |
| 1323 | |
| 1324 title: Polly Parrot is dead | |
| 1325 priority: critical | |
| 1326 status: unread -> in-progress | |
| 1327 fixer: (none) | |
| 1328 keywords: parrot,plumage,perch,nailed,dead | |
| 1329 | |
| 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). | |
| 1334 | |
| 1335 Spool Section | |
| 1336 """"""""""""" | |
| 1337 | |
| 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. | |
| 1342 | |
| 1343 Access Control | |
| 1344 -------------- | |
| 1345 | |
| 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. | |
| 1349 | |
| 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. | |
| 1356 | |
| 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. | |
| 1361 | |
| 1362 | |
| 1363 Access Control Interface Specification | |
| 1364 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1365 | |
| 1366 The security module defines:: | |
| 1367 | |
| 1368 class Permission: | |
| 1369 ''' Defines a Permission with the attributes | |
| 1370 - name | |
| 1371 - description | |
| 1372 - klass (optional) | |
| 1373 | |
| 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 ''' | |
| 1378 | |
| 1379 class Role: | |
| 1380 ''' Defines a Role with the attributes | |
| 1381 - name | |
| 1382 - description | |
| 1383 - permissions | |
| 1384 ''' | |
| 1385 | |
| 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 ''' | |
| 1391 | |
| 1392 def getPermission(self, permission, classname=None): | |
| 1393 ''' Find the Permission matching the name and for the class, if the | |
| 1394 classname is specified. | |
| 1395 | |
| 1396 Raise ValueError if there is no exact match. | |
| 1397 ''' | |
| 1398 | |
| 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 ''' | |
| 1403 | |
| 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. | |
| 1408 | |
| 1409 'propspec' consists of a set of properties and values that | |
| 1410 must be present on the given item for access to be granted. | |
| 1411 | |
| 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 ''' | |
| 1416 | |
| 1417 def addPermission(self, **propspec): | |
| 1418 ''' Create a new Permission with the properties defined in | |
| 1419 'propspec' | |
| 1420 ''' | |
| 1421 | |
| 1422 def addRole(self, **propspec): | |
| 1423 ''' Create a new Role with the properties defined in 'propspec' | |
| 1424 ''' | |
| 1425 | |
| 1426 def addPermissionToRole(self, rolename, permission): | |
| 1427 ''' Add the permission to the role's permission list. | |
| 1428 | |
| 1429 'rolename' is the name of the role to add permission to. | |
| 1430 ''' | |
| 1431 | |
| 1432 Modules such as ``cgi/client.py`` and ``mailgw.py`` define their own | |
| 1433 permissions like so (this example is ``cgi/client.py``):: | |
| 1434 | |
| 1435 def initialiseSecurity(security): | |
| 1436 ''' Create some Permissions and Roles on the security object | |
| 1437 | |
| 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) | |
| 1444 | |
| 1445 Detectors may also define roles in their init() function:: | |
| 1446 | |
| 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) | |
| 1453 | |
| 1454 The tracker dbinit module then has in ``open()``:: | |
| 1455 | |
| 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) | |
| 1459 | |
| 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) | |
| 1467 | |
| 1468 In the dbinit ``init()``:: | |
| 1469 | |
| 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') | |
| 1474 | |
| 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:: | |
| 1478 | |
| 1479 if db.security.hasPermission('issue', 'Edit', userid): | |
| 1480 # all ok | |
| 1481 | |
| 1482 if db.security.hasItemPermission('issue', itemid, assignedto=userid): | |
| 1483 # all ok | |
| 1484 | |
| 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:: | |
| 1489 | |
| 1490 tal:condition="python:request.user.hasPermission('Edit', 'issue')" | |
| 1491 | |
| 1492 or, if the *context* is *issue*, then the following is the same:: | |
| 1493 | |
| 1494 tal:condition="python:request.user.hasPermission('Edit')" | |
| 1495 | |
| 1496 | |
| 1497 Authentication of Users | |
| 1498 ~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1499 | |
| 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. | |
| 1503 | |
| 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. | |
| 1508 | |
| 1509 | |
| 1510 Anonymous Users | |
| 1511 ~~~~~~~~~~~~~~~ | |
| 1512 | |
| 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. | |
| 1516 | |
| 1517 | |
| 1518 Use Cases | |
| 1519 ~~~~~~~~~ | |
| 1520 | |
| 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. | |
| 1544 | |
| 1545 | |
| 1546 Deployment Scenarios | |
| 1547 -------------------- | |
| 1548 | |
| 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. | |
| 1556 | |
| 1557 | |
| 1558 Acknowledgements | |
| 1559 ---------------- | |
| 1560 | |
| 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. | |
| 1567 | |
| 1568 Changes to this document | |
| 1569 ------------------------ | |
| 1570 | |
| 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 | |
| 1577 | |
| 1578 ------------------ | |
| 1579 | |
| 1580 Back to `Table of Contents`_ | |
| 1581 | |
| 1582 .. _`Table of Contents`: index.html | |
| 1583 .. _customisation: customizing.html | |
| 1584 |
