comparison doc/design.txt @ 1711:3c3e44aacdb4

Documentation fixes. doc/customizing.txt, doc/design.txt Documented 'db.curuserid'. doc/design.txt Reflowed to 72 columns (even the layer cake fits :) roundup/mailgw.py Strip '\n' introduced by rfc822.readheaders
author Jean Jordaan <neaj@users.sourceforge.net>
date Tue, 24 Jun 2003 12:39:20 +0000
parents eb3c348676ed
children 84c61e912079
comparison
equal deleted inserted replaced
1708:9595eee0c116 1711:3c3e44aacdb4
24 24
25 Lots of software design documents come with a picture of a cake. 25 Lots of software design documents come with a picture of a cake.
26 Everybody seems to like them. I also like cakes (i think they are 26 Everybody seems to like them. I also like cakes (i think they are
27 tasty). So I, too, shall include a picture of a cake here:: 27 tasty). So I, too, shall include a picture of a cake here::
28 28
29 _________________________________________________________________________ 29 ________________________________________________________________
30 | E-mail Client | Web Browser | Detector Scripts | Shell | 30 | E-mail Client | Web Browser | Detector Scripts | Shell |
31 |------------------+-----------------+----------------------+-------------| 31 |---------------+---------------+--------------------+-----------|
32 | E-mail User | Web User | Detector | Command | 32 | E-mail User | Web User | Detector | Command |
33 |-------------------------------------------------------------------------| 33 |----------------------------------------------------------------|
34 | Roundup Database Layer | 34 | Roundup Database Layer |
35 |-------------------------------------------------------------------------| 35 |----------------------------------------------------------------|
36 | Hyperdatabase Layer | 36 | Hyperdatabase Layer |
37 |-------------------------------------------------------------------------| 37 |----------------------------------------------------------------|
38 | Storage Layer | 38 | Storage Layer |
39 ------------------------------------------------------------------------- 39 ----------------------------------------------------------------
40 40
41 The colourful parts of the cake are part of our system; the faint grey 41 The colourful parts of the cake are part of our system; the faint grey
42 parts of the cake are external components. 42 parts of the cake are external components.
43 43
44 I will now proceed to forgo all table manners and eat from the bottom of 44 I will now proceed to forgo all table manners and eat from the bottom of
120 120
121 Here is an outline of the Date and Interval classes:: 121 Here is an outline of the Date and Interval classes::
122 122
123 class Date: 123 class Date:
124 def __init__(self, spec, offset): 124 def __init__(self, spec, offset):
125 """Construct a date given a specification and a time zone offset. 125 """Construct a date given a specification and a time zone
126 offset.
126 127
127 'spec' is a full date or a partial form, with an optional 128 'spec' is a full date or a partial form, with an optional
128 added or subtracted interval. 'offset' is the local time 129 added or subtracted interval. 'offset' is the local time
129 zone offset from GMT in hours. 130 zone offset from GMT in hours.
130 """ 131 """
131 132
132 def __add__(self, interval): 133 def __add__(self, interval):
133 """Add an interval to this date to produce another date.""" 134 """Add an interval to this date to produce another date."""
134 135
135 def __sub__(self, interval): 136 def __sub__(self, interval):
136 """Subtract an interval from this date to produce another date.""" 137 """Subtract an interval from this date to produce another
138 date.
139 """
137 140
138 def __cmp__(self, other): 141 def __cmp__(self, other):
139 """Compare this date to another date.""" 142 """Compare this date to another date."""
140 143
141 def __str__(self): 144 def __str__(self):
142 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format.""" 145 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss
146 format.
147 """
143 148
144 def local(self, offset): 149 def local(self, offset):
145 """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.""" 150 """Return this date as yyyy-mm-dd.hh:mm:ss in a local time
151 zone.
152 """
146 153
147 class Interval: 154 class Interval:
148 def __init__(self, spec): 155 def __init__(self, spec):
149 """Construct an interval given a specification.""" 156 """Construct an interval given a specification."""
150 157
177 >>> Interval(" 3w 1 d 2:00") 184 >>> Interval(" 3w 1 d 2:00")
178 <Interval 22d 2:00> 185 <Interval 22d 2:00>
179 >>> Date(". + 2d") - Interval("3w") 186 >>> Date(". + 2d") - Interval("3w")
180 <Date 2000-06-07.00:34:02> 187 <Date 2000-06-07.00:34:02>
181 188
189
182 Items and Classes 190 Items and Classes
183 ~~~~~~~~~~~~~~~~~ 191 ~~~~~~~~~~~~~~~~~
184 192
185 Items contain data in properties. To Python, these properties are 193 Items contain data in properties. To Python, these properties are
186 presented as the key-value pairs of a dictionary. Each item belongs to a 194 presented as the key-value pairs of a dictionary. Each item belongs to a
187 class which defines the names and types of its properties. The database 195 class which defines the names and types of its properties. The database
188 permits the creation and modification of classes as well as items. 196 permits the creation and modification of classes as well as items.
197
189 198
190 Identifiers and Designators 199 Identifiers and Designators
191 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 200 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
192 201
193 Each item has a numeric identifier which is unique among items in its 202 Each item has a numeric identifier which is unique among items in its
196 identify an item in the database, and consists of the name of the item's 205 identify an item in the database, and consists of the name of the item's
197 class concatenated with the item's numeric identifier. 206 class concatenated with the item's numeric identifier.
198 207
199 For example, if "spam" and "eggs" are classes, the first item created in 208 For example, if "spam" and "eggs" are classes, the first item created in
200 class "spam" has id 1 and designator "spam1". The first item created in 209 class "spam" has id 1 and designator "spam1". The first item created in
201 class "eggs" also has id 1 but has the distinct designator "eggs1". 210 class "eggs" also has id 1 but has the distinct designator "eggs1". Item
202 Item designators are conventionally enclosed in square brackets when 211 designators are conventionally enclosed in square brackets when
203 mentioned in plain text. This permits a casual mention of, say, 212 mentioned in plain text. This permits a casual mention of, say,
204 "[patch37]" in an e-mail message to be turned into an active hyperlink. 213 "[patch37]" in an e-mail message to be turned into an active hyperlink.
214
205 215
206 Property Names and Types 216 Property Names and Types
207 ~~~~~~~~~~~~~~~~~~~~~~~~ 217 ~~~~~~~~~~~~~~~~~~~~~~~~
208 218
209 Property names must begin with a letter. 219 Property names must begin with a letter.
229 *None* is also a permitted value for any of these property types. An 239 *None* is also a permitted value for any of these property types. An
230 attempt to store None into a Multilink property stores an empty list. 240 attempt to store None into a Multilink property stores an empty list.
231 241
232 A property that is not specified will return as None from a *get* 242 A property that is not specified will return as None from a *get*
233 operation. 243 operation.
244
234 245
235 Hyperdb Interface Specification 246 Hyperdb Interface Specification
236 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 247 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
237 248
238 TODO: replace the Interface Specifications with links to the pydoc 249 TODO: replace the Interface Specifications with links to the pydoc
277 288
278 289
279 Here is the interface provided by the hyperdatabase:: 290 Here is the interface provided by the hyperdatabase::
280 291
281 class Database: 292 class Database:
282 """A database for storing records containing flexible data types.""" 293 """A database for storing records containing flexible data
294 types.
295 """
283 296
284 def __init__(self, config, journaltag=None): 297 def __init__(self, config, journaltag=None):
285 """Open a hyperdatabase given a specifier to some storage. 298 """Open a hyperdatabase given a specifier to some storage.
286 299
287 The 'storagelocator' is obtained from config.DATABASE. The 300 The 'storagelocator' is obtained from config.DATABASE. The
304 """Return a list of the names of all existing classes.""" 317 """Return a list of the names of all existing classes."""
305 318
306 def getclass(self, classname): 319 def getclass(self, classname):
307 """Get the Class object representing a particular class. 320 """Get the Class object representing a particular class.
308 321
309 If 'classname' is not a valid class name, a KeyError is raised. 322 If 'classname' is not a valid class name, a KeyError is
323 raised.
310 """ 324 """
311 325
312 class Class: 326 class Class:
313 """The handle to a particular class of items in a hyperdatabase.""" 327 """The handle to a particular class of items in a hyperdatabase.
328 """
314 329
315 def __init__(self, db, classname, **properties): 330 def __init__(self, db, classname, **properties):
316 """Create a new class with a given name and property specification. 331 """Create a new class with a given name and property
317 332 specification.
318 'classname' must not collide with the name of an existing class, 333
319 or a ValueError is raised. The keyword arguments in 'properties' 334 'classname' must not collide with the name of an existing
320 must map names to property objects, or a TypeError is raised. 335 class, or a ValueError is raised. The keyword arguments in
336 'properties' must map names to property objects, or a
337 TypeError is raised.
338
339 A proxied reference to the database is available as the
340 'db' attribute on instances. For example, in
341 'IssueClass.send_message', the following is used to lookup
342 users, messages and files::
343
344 users = self.db.user
345 messages = self.db.msg
346 files = self.db.file
347
348 The id of the current user is also available on the database
349 as 'self.db.curuserid'.
321 """ 350 """
322 351
323 # Editing items: 352 # Editing items:
324 353
325 def create(self, **propvalues): 354 def create(self, **propvalues):
326 """Create a new item of this class and return its id. 355 """Create a new item of this class and return its id.
327 356
328 The keyword arguments in 'propvalues' map property names to values. 357 The keyword arguments in 'propvalues' map property names to
329 The values of arguments must be acceptable for the types of their 358 values. The values of arguments must be acceptable for the
330 corresponding properties or a TypeError is raised. If this class 359 types of their corresponding properties or a TypeError is
331 has a key property, it must be present and its value must not 360 raised. If this class has a key property, it must be
332 collide with other key strings or a ValueError is raised. Any other 361 present and its value must not collide with other key
333 properties on this class that are missing from the 'propvalues' 362 strings or a ValueError is raised. Any other properties on
334 dictionary are set to None. If an id in a link or multilink 363 this class that are missing from the 'propvalues' dictionary
335 property does not refer to a valid item, an IndexError is raised. 364 are set to None. If an id in a link or multilink property
365 does not refer to a valid item, an IndexError is raised.
336 """ 366 """
337 367
338 def get(self, itemid, propname): 368 def get(self, itemid, propname):
339 """Get the value of a property on an existing item of this class. 369 """Get the value of a property on an existing item of this
340 370 class.
341 'itemid' must be the id of an existing item of this class or an 371
342 IndexError is raised. 'propname' must be the name of a property 372 'itemid' must be the id of an existing item of this class or
343 of this class or a KeyError is raised. 373 an IndexError is raised. 'propname' must be the name of a
374 property of this class or a KeyError is raised.
344 """ 375 """
345 376
346 def set(self, itemid, **propvalues): 377 def set(self, itemid, **propvalues):
347 """Modify a property on an existing item of this class. 378 """Modify a property on an existing item of this class.
348 379
349 'itemid' must be the id of an existing item of this class or an 380 'itemid' must be the id of an existing item of this class or
350 IndexError is raised. Each key in 'propvalues' must be the name 381 an IndexError is raised. Each key in 'propvalues' must be
351 of a property of this class or a KeyError is raised. All values 382 the name of a property of this class or a KeyError is
352 in 'propvalues' must be acceptable types for their corresponding 383 raised. All values in 'propvalues' must be acceptable types
353 properties or a TypeError is raised. If the value of the key 384 for their corresponding properties or a TypeError is raised.
354 property is set, it must not collide with other key strings or a 385 If the value of the key property is set, it must not collide
355 ValueError is raised. If the value of a Link or Multilink 386 with other key strings or a ValueError is raised. If the
356 property contains an invalid item id, a ValueError is raised. 387 value of a Link or Multilink property contains an invalid
388 item id, a ValueError is raised.
357 """ 389 """
358 390
359 def retire(self, itemid): 391 def retire(self, itemid):
360 """Retire an item. 392 """Retire an item.
361 393
362 The properties on the item remain available from the get() method, 394 The properties on the item remain available from the get()
363 and the item's id is never reused. Retired items are not returned 395 method, and the item's id is never reused. Retired items
364 by the find(), list(), or lookup() methods, and other items may 396 are not returned by the find(), list(), or lookup() methods,
365 reuse the values of their key properties. 397 and other items may reuse the values of their key
398 properties.
366 """ 399 """
367 400
368 def restore(self, nodeid): 401 def restore(self, nodeid):
369 '''Restore a retired node. 402 '''Restore a retired node.
370 403
371 Make node available for all operations like it was before retirement. 404 Make node available for all operations like it was before
405 retirement.
372 ''' 406 '''
373 407
374 def history(self, itemid): 408 def history(self, itemid):
375 """Retrieve the journal of edits on a particular item. 409 """Retrieve the journal of edits on a particular item.
376 410
377 'itemid' must be the id of an existing item of this class or an 411 'itemid' must be the id of an existing item of this class or
378 IndexError is raised. 412 an IndexError is raised.
379 413
380 The returned list contains tuples of the form 414 The returned list contains tuples of the form
381 415
382 (date, tag, action, params) 416 (date, tag, action, params)
383 417
384 'date' is a Timestamp object specifying the time of the change and 418 'date' is a Timestamp object specifying the time of the
385 'tag' is the journaltag specified when the database was opened. 419 change and 'tag' is the journaltag specified when the
386 'action' may be: 420 database was opened. 'action' may be:
387 421
388 'create' or 'set' -- 'params' is a dictionary of property values 422 'create' or 'set' -- 'params' is a dictionary of
389 'link' or 'unlink' -- 'params' is (classname, itemid, propname) 423 property values
424 'link' or 'unlink' -- 'params' is (classname, itemid,
425 propname)
390 'retire' -- 'params' is None 426 'retire' -- 'params' is None
391 """ 427 """
392 428
393 # Locating items: 429 # Locating items:
394 430
395 def setkey(self, propname): 431 def setkey(self, propname):
396 """Select a String property of this class to be the key property. 432 """Select a String property of this class to be the key
397 433 property.
398 'propname' must be the name of a String property of this class or 434
399 None, or a TypeError is raised. The values of the key property on 435 'propname' must be the name of a String property of this
400 all existing items must be unique or a ValueError is raised. 436 class or None, or a TypeError is raised. The values of the
437 key property on all existing items must be unique or a
438 ValueError is raised.
401 """ 439 """
402 440
403 def getkey(self): 441 def getkey(self):
404 """Return the name of the key property for this class or None.""" 442 """Return the name of the key property for this class or
443 None.
444 """
405 445
406 def lookup(self, keyvalue): 446 def lookup(self, keyvalue):
407 """Locate a particular item by its key property and return its id. 447 """Locate a particular item by its key property and return
408 448 its id.
409 If this class has no key property, a TypeError is raised. If the 449
410 'keyvalue' matches one of the values for the key property among 450 If this class has no key property, a TypeError is raised.
411 the items in this class, the matching item's id is returned; 451 If the 'keyvalue' matches one of the values for the key
412 otherwise a KeyError is raised. 452 property among the items in this class, the matching item's
453 id is returned; otherwise a KeyError is raised.
413 """ 454 """
414 455
415 def find(self, propname, itemid): 456 def find(self, propname, itemid):
416 """Get the ids of items in this class which link to the given items. 457 """Get the ids of items in this class which link to the
417 458 given items.
418 'propspec' consists of keyword args propname={itemid:1,} 459
419 'propname' must be the name of a property in this class, or a 460 'propspec' consists of keyword args propname={itemid:1,}
420 KeyError is raised. That property must be a Link or Multilink 461 'propname' must be the name of a property in this class, or
421 property, or a TypeError is raised. 462 a KeyError is raised. That property must be a Link or
422 463 Multilink property, or a TypeError is raised.
423 Any item in this class whose 'propname' property links to any of the 464
424 itemids will be returned. Used by the full text indexing, which 465 Any item in this class whose 'propname' property links to
425 knows that "foo" occurs in msg1, msg3 and file7, so we have hits 466 any of the itemids will be returned. Used by the full text
426 on these issues: 467 indexing, which knows that "foo" occurs in msg1, msg3 and
468 file7, so we have hits on these issues:
427 469
428 db.issue.find(messages={'1':1,'3':1}, files={'7':1}) 470 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
429 """ 471 """
430 472
431 def filter(self, search_matches, filterspec, sort, group): 473 def filter(self, search_matches, filterspec, sort, group):
432 ''' Return a list of the ids of the active items in this class that 474 """ Return a list of the ids of the active items in this
433 match the 'filter' spec, sorted by the group spec and then the 475 class that match the 'filter' spec, sorted by the group spec
434 sort spec. 476 and then the sort spec.
435 ''' 477 """
436 478
437 def list(self): 479 def list(self):
438 """Return a list of the ids of the active items in this class.""" 480 """Return a list of the ids of the active items in this
481 class.
482 """
439 483
440 def count(self): 484 def count(self):
441 """Get the number of items in this class. 485 """Get the number of items in this class.
442 486
443 If the returned integer is 'numitems', the ids of all the items 487 If the returned integer is 'numitems', the ids of all the
444 in this class run from 1 to numitems, and numitems+1 will be the 488 items in this class run from 1 to numitems, and numitems+1
445 id of the next item to be created in this class. 489 will be the id of the next item to be created in this class.
446 """ 490 """
447 491
448 # Manipulating properties: 492 # Manipulating properties:
449 493
450 def getprops(self): 494 def getprops(self):
451 """Return a dictionary mapping property names to property objects.""" 495 """Return a dictionary mapping property names to property
496 objects.
497 """
452 498
453 def addprop(self, **properties): 499 def addprop(self, **properties):
454 """Add properties to this class. 500 """Add properties to this class.
455 501
456 The keyword arguments in 'properties' must map names to property 502 The keyword arguments in 'properties' must map names to
457 objects, or a TypeError is raised. None of the keys in 'properties' 503 property objects, or a TypeError is raised. None of the
458 may collide with the names of existing properties, or a ValueError 504 keys in 'properties' may collide with the names of existing
459 is raised before any properties have been added. 505 properties, or a ValueError is raised before any properties
506 have been added.
460 """ 507 """
461 508
462 def getitem(self, itemid, cache=1): 509 def getitem(self, itemid, cache=1):
463 ''' Return a Item convenience wrapper for the item. 510 """ Return a Item convenience wrapper for the item.
464 511
465 'itemid' must be the id of an existing item of this class or an 512 'itemid' must be the id of an existing item of this class or
466 IndexError is raised. 513 an IndexError is raised.
467 514
468 'cache' indicates whether the transaction cache should be queried 515 'cache' indicates whether the transaction cache should be
469 for the item. If the item has been modified and you need to 516 queried for the item. If the item has been modified and you
470 determine what its values prior to modification are, you need to 517 need to determine what its values prior to modification are,
471 set cache=0. 518 you need to set cache=0.
472 ''' 519 """
473 520
474 class Item: 521 class Item:
475 ''' A convenience wrapper for the given item. It provides a mapping 522 """ A convenience wrapper for the given item. It provides a
476 interface to a single item's properties 523 mapping interface to a single item's properties
477 ''' 524 """
478 525
479 Hyperdatabase Implementations 526 Hyperdatabase Implementations
480 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 527 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
481 528
482 Hyperdatabase implementations exist to create the interface described in 529 Hyperdatabase implementations exist to create the interface described in
483 the `hyperdb interface specification`_ over an existing storage 530 the `hyperdb interface specification`_ over an existing storage
484 mechanism. Examples are relational databases, \*dbm key-value databases, 531 mechanism. Examples are relational databases, \*dbm key-value databases,
485 and so on. 532 and so on.
486 533
487 Several implementations are provided - they belong in the 534 Several implementations are provided - they belong in the
488 roundup.backends package. 535 ``roundup.backends`` package.
489 536
490 537
491 Application Example 538 Application Example
492 ~~~~~~~~~~~~~~~~~~~ 539 ~~~~~~~~~~~~~~~~~~~
493 540
528 3 575 3
529 >>> db.issue.create(title="arguments", status=2) 576 >>> db.issue.create(title="arguments", status=2)
530 4 577 4
531 >>> db.issue.create(title="abuse", status=1) 578 >>> db.issue.create(title="abuse", status=1)
532 5 579 5
533 >>> hyperdb.Class(db, "user", username=hyperdb.Key(), password=hyperdb.String()) 580 >>> hyperdb.Class(db, "user", username=hyperdb.Key(),
581 ... password=hyperdb.String())
534 <hyperdb.Class "user"> 582 <hyperdb.Class "user">
535 >>> db.issue.addprop(fixer=hyperdb.Link("user")) 583 >>> db.issue.addprop(fixer=hyperdb.Link("user"))
536 >>> db.issue.getprops() 584 >>> db.issue.getprops()
537 {"title": <hyperdb.String>, "status": <hyperdb.Link to "status">, 585 {"title": <hyperdb.String>, "status": <hyperdb.Link to "status">,
538 "user": <hyperdb.Link to "user">} 586 "user": <hyperdb.Link to "user">}
544 >>> db.issue.get(5, "title") 592 >>> db.issue.get(5, "title")
545 "abuse" 593 "abuse"
546 >>> db.issue.find("status", db.status.lookup("in-progress")) 594 >>> db.issue.find("status", db.status.lookup("in-progress"))
547 [2, 4, 5] 595 [2, 4, 5]
548 >>> db.issue.history(5) 596 >>> db.issue.history(5)
549 [(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse", "status": 1}), 597 [(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse",
598 "status": 1}),
550 (<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})] 599 (<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})]
551 >>> db.status.history(1) 600 >>> db.status.history(1)
552 [(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")), 601 [(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")),
553 (<Date 2000-06-28.19:11:04>, "ping", "unlink", ("issue", 5, "status"))] 602 (<Date 2000-06-28.19:11:04>, "ping", "unlink", ("issue", 5, "status"))]
554 >>> db.status.history(2) 603 >>> db.status.history(2)
568 The Roundup database layer is implemented on top of the hyperdatabase 617 The Roundup database layer is implemented on top of the hyperdatabase
569 and mediates calls to the database. Some of the classes in the Roundup 618 and mediates calls to the database. Some of the classes in the Roundup
570 database are considered issue classes. The Roundup database layer adds 619 database are considered issue classes. The Roundup database layer adds
571 detectors and user items, and on issues it provides mail spools, nosy 620 detectors and user items, and on issues it provides mail spools, nosy
572 lists, and superseders. 621 lists, and superseders.
622
573 623
574 Reserved Classes 624 Reserved Classes
575 ~~~~~~~~~~~~~~~~ 625 ~~~~~~~~~~~~~~~~
576 626
577 Internal to this layer we reserve three special classes of items that 627 Internal to this layer we reserve three special classes of items that
610 The "author" property indicates the author of the message (a "user" item 660 The "author" property indicates the author of the message (a "user" item
611 must exist in the hyperdatabase for any messages that are stored in the 661 must exist in the hyperdatabase for any messages that are stored in the
612 system). The "summary" property contains a summary of the message for 662 system). The "summary" property contains a summary of the message for
613 display in a message index. 663 display in a message index.
614 664
665
615 Files 666 Files
616 """"" 667 """""
617 668
618 Submitted files are represented by hyperdatabase items of class "file". 669 Submitted files are represented by hyperdatabase items of class "file".
619 Like e-mail messages, the file content is stored in files outside the 670 Like e-mail messages, the file content is stored in files outside the
625 type=hyperdb.String()) 676 type=hyperdb.String())
626 677
627 The "user" property indicates the user who submitted the file, the 678 The "user" property indicates the user who submitted the file, the
628 "name" property holds the original name of the file, and the "type" 679 "name" property holds the original name of the file, and the "type"
629 property holds the MIME type of the file as received. 680 property holds the MIME type of the file as received.
681
630 682
631 Issue Classes 683 Issue Classes
632 ~~~~~~~~~~~~~ 684 ~~~~~~~~~~~~~
633 685
634 All issues have the following standard properties: 686 All issues have the following standard properties:
650 made available. The value of the "creation" property is the date when an 702 made available. The value of the "creation" property is the date when an
651 issue was created, and the value of the "activity" property is the date 703 issue was created, and the value of the "activity" property is the date
652 when any property on the issue was last edited (equivalently, these are 704 when any property on the issue was last edited (equivalently, these are
653 the dates on the first and last records in the issue's journal). 705 the dates on the first and last records in the issue's journal).
654 706
707
655 Roundupdb Interface Specification 708 Roundupdb Interface Specification
656 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 709 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
657 710
658 The interface to a Roundup database delegates most method calls to the 711 The interface to a Roundup database delegates most method calls to the
659 hyperdatabase, except for the following changes and additional methods:: 712 hyperdatabase, except for the following changes and additional methods::
721 ~~~~~~~~~~~~~~ 774 ~~~~~~~~~~~~~~
722 775
723 The default schema included with Roundup turns it into a typical 776 The default schema included with Roundup turns it into a typical
724 software bug tracker. The database is set up like this:: 777 software bug tracker. The database is set up like this::
725 778
726 pri = Class(db, "priority", name=hyperdb.String(), order=hyperdb.String()) 779 pri = Class(db, "priority", name=hyperdb.String(),
780 order=hyperdb.String())
727 pri.setkey("name") 781 pri.setkey("name")
728 pri.create(name="critical", order="1") 782 pri.create(name="critical", order="1")
729 pri.create(name="urgent", order="2") 783 pri.create(name="urgent", order="2")
730 pri.create(name="bug", order="3") 784 pri.create(name="bug", order="3")
731 pri.create(name="feature", order="4") 785 pri.create(name="feature", order="4")
732 pri.create(name="wish", order="5") 786 pri.create(name="wish", order="5")
733 787
734 stat = Class(db, "status", name=hyperdb.String(), order=hyperdb.String()) 788 stat = Class(db, "status", name=hyperdb.String(),
789 order=hyperdb.String())
735 stat.setkey("name") 790 stat.setkey("name")
736 stat.create(name="unread", order="1") 791 stat.create(name="unread", order="1")
737 stat.create(name="deferred", order="2") 792 stat.create(name="deferred", order="2")
738 stat.create(name="chatting", order="3") 793 stat.create(name="chatting", order="3")
739 stat.create(name="need-eg", order="4") 794 stat.create(name="need-eg", order="4")
756 first-stage submission, but it could be made just as easy with the 811 first-stage submission, but it could be made just as easy with the
757 addition of a convenience function like Choice for setting up the 812 addition of a convenience function like Choice for setting up the
758 "priority" and "status" classes:: 813 "priority" and "status" classes::
759 814
760 def Choice(name, *options): 815 def Choice(name, *options):
761 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String()) 816 cl = Class(db, name, name=hyperdb.String(),
817 order=hyperdb.String())
762 for i in range(len(options)): 818 for i in range(len(options)):
763 cl.create(name=option[i], order=i) 819 cl.create(name=option[i], order=i)
764 return hyperdb.Link(name) 820 return hyperdb.Link(name)
765 821
766 822
786 842
787 If none of the auditors raises an exception, the database proceeds to 843 If none of the auditors raises an exception, the database proceeds to
788 carry out the operation. After it's done, it then calls all of the 844 carry out the operation. After it's done, it then calls all of the
789 *reactors* that have been registered for the operation. 845 *reactors* that have been registered for the operation.
790 846
847
791 Detector Interface Specification 848 Detector Interface Specification
792 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 849 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
793 850
794 The ``audit()`` and ``react()`` methods register detectors on a given 851 The ``audit()`` and ``react()`` methods register detectors on a given
795 class of items:: 852 class of items::
842 For a ``set()`` operation, ``olddata`` contains the names and previous 899 For a ``set()`` operation, ``olddata`` contains the names and previous
843 values of properties that were changed. 900 values of properties that were changed.
844 901
845 For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of 902 For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of
846 the retired or restored item and ``olddata`` is None. 903 the retired or restored item and ``olddata`` is None.
904
847 905
848 Detector Example 906 Detector Example
849 ~~~~~~~~~~~~~~~~ 907 ~~~~~~~~~~~~~~~~
850 908
851 Here is an example of detectors written for a hypothetical 909 Here is an example of detectors written for a hypothetical
862 "for a project that has already been approved." 920 "for a project that has already been approved."
863 old = cl.get(id, "approvals") 921 old = cl.get(id, "approvals")
864 new = newdata["approvals"] 922 new = newdata["approvals"]
865 for uid in old: 923 for uid in old:
866 if uid not in new and uid != db.getuid(): 924 if uid not in new and uid != db.getuid():
867 raise Reject, "You can't remove other users from the " 925 raise Reject, "You can't remove other users from " \
868 "approvals list; you can only remove yourself." 926 "the approvals list; you can only remove " \
927 "yourself."
869 for uid in new: 928 for uid in new:
870 if uid not in old and uid != db.getuid(): 929 if uid not in old and uid != db.getuid():
871 raise Reject, "You can't add other users to the approvals " 930 raise Reject, "You can't add other users to the " \
872 "list; you can only add yourself." 931 "approvals list; you can only add yourself."
873 932
874 # When three people have approved a project, change its 933 # When three people have approved a project, change its status from
875 # status from "pending" to "approved". 934 # "pending" to "approved".
876 935
877 def approve_project(db, cl, id, olddata): 936 def approve_project(db, cl, id, olddata):
878 if olddata.has_key("approvals") and len(cl.get(id, "approvals")) == 3: 937 if (olddata.has_key("approvals") and
938 len(cl.get(id, "approvals")) == 3):
879 if cl.get(id, "status") == db.status.lookup("pending"): 939 if cl.get(id, "status") == db.status.lookup("pending"):
880 cl.set(id, status=db.status.lookup("approved")) 940 cl.set(id, status=db.status.lookup("approved"))
881 941
882 def init(db): 942 def init(db):
883 db.project.audit("set", check_approval) 943 db.project.audit("set", check_approval)
888 are submitted by sending in e-mail with an attached file, and we want to 948 are submitted by sending in e-mail with an attached file, and we want to
889 ensure that there are text/plain attachments on the message. The 949 ensure that there are text/plain attachments on the message. The
890 maintainer of the package can then apply the patch by setting its status 950 maintainer of the package can then apply the patch by setting its status
891 to "applied":: 951 to "applied"::
892 952
893 # Only accept attempts to create new patches that come with patch files. 953 # Only accept attempts to create new patches that come with patch
954 # files.
894 955
895 def check_new_patch(db, cl, id, newdata): 956 def check_new_patch(db, cl, id, newdata):
896 if not newdata["files"]: 957 if not newdata["files"]:
897 raise Reject, "You can't submit a new patch without " \ 958 raise Reject, "You can't submit a new patch without " \
898 "attaching a patch file." 959 "attaching a patch file."
899 for fileid in newdata["files"]: 960 for fileid in newdata["files"]:
900 if db.file.get(fileid, "type") != "text/plain": 961 if db.file.get(fileid, "type") != "text/plain":
901 raise Reject, "Submitted patch files must be text/plain." 962 raise Reject, "Submitted patch files must be " \
902 963 "text/plain."
903 # When the status is changed from "approved" to "applied", apply the patch. 964
965 # When the status is changed from "approved" to "applied", apply the
966 # patch.
904 967
905 def apply_patch(db, cl, id, olddata): 968 def apply_patch(db, cl, id, olddata):
906 if cl.get(id, "status") == db.status.lookup("applied") and \ 969 if (cl.get(id, "status") == db.status.lookup("applied") and
907 olddata["status"] == db.status.lookup("approved"): 970 olddata["status"] == db.status.lookup("approved")):
908 # ...apply the patch... 971 # ...apply the patch...
909 972
910 def init(db): 973 def init(db):
911 db.patch.audit("create", check_new_patch) 974 db.patch.audit("create", check_new_patch)
912 db.patch.react("set", apply_patch) 975 db.patch.react("set", apply_patch)
918 The command interface is a very simple and minimal interface, intended 981 The command interface is a very simple and minimal interface, intended
919 only for quick searches and checks from the shell prompt. (Anything more 982 only for quick searches and checks from the shell prompt. (Anything more
920 interesting can simply be written in Python using the Roundup database 983 interesting can simply be written in Python using the Roundup database
921 module.) 984 module.)
922 985
986
923 Command Interface Specification 987 Command Interface Specification
924 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 988 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
925 989
926 A single command, roundup, provides basic access to the hyperdatabase 990 A single command, roundup, provides basic access to the hyperdatabase
927 from the command line:: 991 from the command line::
959 listed items. 1023 listed items.
960 1024
961 When multiple results are returned by the roundup get or roundup find 1025 When multiple results are returned by the roundup get or roundup find
962 commands, they are printed one per line (default) or joined by commas 1026 commands, they are printed one per line (default) or joined by commas
963 (with the -list) option. 1027 (with the -list) option.
1028
964 1029
965 Usage Example 1030 Usage Example
966 ~~~~~~~~~~~~~ 1031 ~~~~~~~~~~~~~
967 1032
968 To find all messages regarding in-progress issues that contain the word 1033 To find all messages regarding in-progress issues that contain the word
994 1059
995 The Roundup system must be assigned an e-mail address at which to 1060 The Roundup system must be assigned an e-mail address at which to
996 receive mail. Messages should be piped to the Roundup mail-handling 1061 receive mail. Messages should be piped to the Roundup mail-handling
997 script by the mail delivery system (e.g. using an alias beginning with 1062 script by the mail delivery system (e.g. using an alias beginning with
998 "|" for sendmail). 1063 "|" for sendmail).
1064
999 1065
1000 Message Processing 1066 Message Processing
1001 ~~~~~~~~~~~~~~~~~~ 1067 ~~~~~~~~~~~~~~~~~~
1002 1068
1003 Incoming messages are examined for multiple parts. In a multipart/mixed 1069 Incoming messages are examined for multiple parts. In a multipart/mixed
1047 set() method to add the message to the issue's spool; in the second case 1113 set() method to add the message to the issue's spool; in the second case
1048 we are calling the create() method to create a new item). If an auditor 1114 we are calling the create() method to create a new item). If an auditor
1049 raises an exception, the original message is bounced back to the sender 1115 raises an exception, the original message is bounced back to the sender
1050 with the explanatory message given in the exception. 1116 with the explanatory message given in the exception.
1051 1117
1118
1052 Nosy Lists 1119 Nosy Lists
1053 ~~~~~~~~~~ 1120 ~~~~~~~~~~
1054 1121
1055 A standard detector is provided that watches for additions to the 1122 A standard detector is provided that watches for additions to the
1056 "messages" property. When a new message is added, the detector sends it 1123 "messages" property. When a new message is added, the detector sends it
1059 to the "recipients" property on the message, so multiple copies of a 1126 to the "recipients" property on the message, so multiple copies of a
1060 message are never sent to the same user. The journal recorded by the 1127 message are never sent to the same user. The journal recorded by the
1061 hyperdatabase on the "recipients" property then provides a log of when 1128 hyperdatabase on the "recipients" property then provides a log of when
1062 the message was sent to whom. 1129 the message was sent to whom.
1063 1130
1131
1064 Setting Properties 1132 Setting Properties
1065 ~~~~~~~~~~~~~~~~~~ 1133 ~~~~~~~~~~~~~~~~~~
1066 1134
1067 The e-mail interface also provides a simple way to set properties on 1135 The e-mail interface also provides a simple way to set properties on
1068 issues. At the end of the subject line, ``propname=value`` pairs can be 1136 issues. At the end of the subject line, ``propname=value`` pairs can be
1080 1148
1081 The user interface is constructed from a number of template files 1149 The user interface is constructed from a number of template files
1082 containing mostly HTML. Among the HTML tags in templates are 1150 containing mostly HTML. Among the HTML tags in templates are
1083 interspersed some nonstandard tags, which we use as placeholders to be 1151 interspersed some nonstandard tags, which we use as placeholders to be
1084 replaced by properties and their values. 1152 replaced by properties and their values.
1153
1085 1154
1086 Views and View Specifiers 1155 Views and View Specifiers
1087 ~~~~~~~~~~~~~~~~~~~~~~~~~ 1156 ~~~~~~~~~~~~~~~~~~~~~~~~~
1088 1157
1089 There are two main kinds of views: *index* views and *issue* views. An 1158 There are two main kinds of views: *index* views and *issue* views. An
1096 construct a particular view. It goes after the URL to the Roundup CGI 1165 construct a particular view. It goes after the URL to the Roundup CGI
1097 script or the web server to form the complete URL to a view. When the 1166 script or the web server to form the complete URL to a view. When the
1098 result of selecting a link or submitting a form takes the user to a new 1167 result of selecting a link or submitting a form takes the user to a new
1099 view, the Web browser should be redirected to a canonical location 1168 view, the Web browser should be redirected to a canonical location
1100 containing a complete view specifier so that the view can be bookmarked. 1169 containing a complete view specifier so that the view can be bookmarked.
1170
1101 1171
1102 Displaying Properties 1172 Displaying Properties
1103 ~~~~~~~~~~~~~~~~~~~~~ 1173 ~~~~~~~~~~~~~~~~~~~~~
1104 1174
1105 Properties appear in the user interface in three contexts: in indices, 1175 Properties appear in the user interface in three contexts: in indices,
1149 1219
1150 An index view contains two sections: a filter section and an index 1220 An index view contains two sections: a filter section and an index
1151 section. The filter section provides some widgets for selecting which 1221 section. The filter section provides some widgets for selecting which
1152 issues appear in the index. The index section is a table of issues. 1222 issues appear in the index. The index section is a table of issues.
1153 1223
1224
1154 Index View Specifiers 1225 Index View Specifiers
1155 """"""""""""""""""""" 1226 """""""""""""""""""""
1156 1227
1157 An index view specifier looks like this (whitespace has been added for 1228 An index view specifier looks like this (whitespace has been added for
1158 clarity):: 1229 clarity)::
1248 1319
1249 Here's an example of a basic editor template:: 1320 Here's an example of a basic editor template::
1250 1321
1251 <table> 1322 <table>
1252 <tr> 1323 <tr>
1253 <td colspan=2 tal:content="python:context.title.field(size='60')"></td> 1324 <td colspan=2
1325 tal:content="python:context.title.field(size='60')"></td>
1254 </tr> 1326 </tr>
1255 <tr> 1327 <tr>
1256 <td tal:content="context/fixer/field"></td> 1328 <td tal:content="context/fixer/field"></td>
1257 <td tal:content="context/status/menu"></td> 1329 <td tal:content="context/status/menu"></td>
1258 </tr> 1330 </tr>
1285 If a note is given in the ":note" field, the note is appended to the 1357 If a note is given in the ":note" field, the note is appended to the
1286 description. The message is then added to the issue's message spool 1358 description. The message is then added to the issue's message spool
1287 (thus triggering the standard detector to react by sending out this 1359 (thus triggering the standard detector to react by sending out this
1288 message to the nosy list). 1360 message to the nosy list).
1289 1361
1362
1290 Spool Section 1363 Spool Section
1291 """"""""""""" 1364 """""""""""""
1292 1365
1293 The spool section lists messages in the issue's "messages" property. 1366 The spool section lists messages in the issue's "messages" property.
1294 The index of messages displays the "date", "author", and "summary" 1367 The index of messages displays the "date", "author", and "summary"
1439 to perform some action:: 1512 to perform some action::
1440 1513
1441 if db.security.hasPermission('issue', 'Edit', userid): 1514 if db.security.hasPermission('issue', 'Edit', userid):
1442 # all ok 1515 # all ok
1443 1516
1444 if db.security.hasItemPermission('issue', itemid, assignedto=userid): 1517 if db.security.hasItemPermission('issue', itemid,
1518 assignedto=userid):
1445 # all ok 1519 # all ok
1446 1520
1447 Code in the core will make use of these methods, as should code in 1521 Code in the core will make use of these methods, as should code in
1448 auditors in custom templates. The HTML templating may access the access 1522 auditors in custom templates. The HTML templating may access the access
1449 controls through the *user* attribute of the *request* variable. It 1523 controls through the *user* attribute of the *request* variable. It
1471 1545
1472 1546
1473 Anonymous Users 1547 Anonymous Users
1474 ~~~~~~~~~~~~~~~ 1548 ~~~~~~~~~~~~~~~
1475 1549
1476 The "anonymous" user must always exist, and defines the access permissions for 1550 The "anonymous" user must always exist, and defines the access
1477 anonymous users. Unknown users accessing Roundup through the web or email 1551 permissions for anonymous users. Unknown users accessing Roundup through
1478 interfaces will be logged in as the "anonymous" user. 1552 the web or email interfaces will be logged in as the "anonymous" user.
1479 1553
1480 1554
1481 Use Cases 1555 Use Cases
1482 ~~~~~~~~~ 1556 ~~~~~~~~~
1483 1557
1528 My thanks are due to Christy Heyl for reviewing and contributing 1602 My thanks are due to Christy Heyl for reviewing and contributing
1529 suggestions to this paper and motivating me to get it done, and to Jesse 1603 suggestions to this paper and motivating me to get it done, and to Jesse
1530 Vincent, Mark Miller, Christopher Simons, Jeff Dunmall, Wayne Gramlich, 1604 Vincent, Mark Miller, Christopher Simons, Jeff Dunmall, Wayne Gramlich,
1531 and Dean Tribble for their assistance with the first-round submission. 1605 and Dean Tribble for their assistance with the first-round submission.
1532 1606
1607
1533 Changes to this document 1608 Changes to this document
1534 ------------------------ 1609 ------------------------
1535 1610
1536 - Added Boolean and Number types 1611 - Added Boolean and Number types
1537 - Added section Hyperdatabase Implementations 1612 - Added section Hyperdatabase Implementations

Roundup Issue Tracker: http://roundup-tracker.org/