Mercurial > p > roundup > code
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 |
