Mercurial > p > roundup > code
comparison doc/spec.html @ 71:5147b4c51fd5
Added the Roundup spec to the new documentation directory.
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Wed, 25 Jul 2001 01:23:07 +0000 |
| parents | |
| children | 944bb5255eaf |
comparison
equal
deleted
inserted
replaced
| 70:d95a03480fd1 | 71:5147b4c51fd5 |
|---|---|
| 1 <html> | |
| 2 <head> | |
| 3 <title>Software Carpentry Track: Roundup</title> | |
| 4 </head> | |
| 5 <body bgcolor=white> | |
| 6 | |
| 7 <table width="100%"> | |
| 8 <tr> | |
| 9 | |
| 10 <td align="left"> | |
| 11 <a href="http://www.software-carpentry.com"><img src="images/logo-software-carpentry-standard.gif" alt="[Software Carpentry logo]" border="0"></a> | |
| 12 </td> | |
| 13 | |
| 14 <td align="right"> | |
| 15 <table> | |
| 16 <tr><td> | |
| 17 <a href="http://www.acl.lanl.gov"><img src="images//logo-acl-medium.gif" alt="[ACL Logo]" border="0"></a> | |
| 18 </td></tr> | |
| 19 <tr><td><hr></td></tr> | |
| 20 <tr><td> | |
| 21 <a href="http://www.codesourcery.com"><img src="images/logo-codesourcery-medium.gif" alt="[CodeSourcery Logo]" border="0"></a> | |
| 22 </td></tr> | |
| 23 </table> | |
| 24 </td> | |
| 25 | |
| 26 </tr> | |
| 27 </table> | |
| 28 | |
| 29 <hr><p> | |
| 30 | |
| 31 <h1 align=center>Roundup</h1> | |
| 32 <h3 align=center>An Issue-Tracking System for Knowledge Workers</h3> | |
| 33 <h4 align=center><a href="http://www.lfw.org/ping/">Ka-Ping Yee</a><br> | |
| 34 <a href="mailto:ping@lfw.org">ping@lfw.org</a></h4> | |
| 35 <h3 align=center>Implementation Guide</h3> | |
| 36 | |
| 37 <h2>Contents</h2> | |
| 38 | |
| 39 <ol> | |
| 40 <li>Introduction | |
| 41 <li>The Layer Cake | |
| 42 <li>Hyperdatabase | |
| 43 <ol> | |
| 44 <li>Dates and Date Arithmetic | |
| 45 <li>Nodes and Classes | |
| 46 <li>Identifiers and Designators | |
| 47 <li>Property Names and Types | |
| 48 <li>Interface Specification | |
| 49 <li>Application Example | |
| 50 </ol> | |
| 51 <li>Roundup Database | |
| 52 <ol> | |
| 53 <li>Reserved Classes | |
| 54 <ol> | |
| 55 <li>Users | |
| 56 <li>Messages | |
| 57 <li>Files | |
| 58 </ol> | |
| 59 <li>Item Classes | |
| 60 <li>Interface Specification | |
| 61 <li>Default Schema | |
| 62 </ol> | |
| 63 <li>Detector Interface | |
| 64 <ol> | |
| 65 <li>Interface Specification | |
| 66 <li>Detector Example | |
| 67 </ol> | |
| 68 <li>Command Interface | |
| 69 <ol> | |
| 70 <li>Interface Specification | |
| 71 <li>Usage Example | |
| 72 </ol> | |
| 73 <li>E-mail User Interface | |
| 74 <ol> | |
| 75 <li>Message Processing | |
| 76 <li>Nosy Lists | |
| 77 <li>Setting Properties | |
| 78 <li>Workflow Example | |
| 79 </ol> | |
| 80 <li>Web User Interface | |
| 81 <ol> | |
| 82 <li>Views and View Specifiers | |
| 83 <li>Displaying Properties | |
| 84 <li>Index Views | |
| 85 <ol> | |
| 86 <li>Index View Specifiers | |
| 87 <li>Filter Section | |
| 88 <li>Index Section | |
| 89 <li>Sorting | |
| 90 </ol> | |
| 91 <li>Item Views | |
| 92 <ol> | |
| 93 <li>Item View Specifiers | |
| 94 <li>Editor Section | |
| 95 <li>Spool Section | |
| 96 </ol> | |
| 97 </ol> | |
| 98 <li>Deployment Scenarios | |
| 99 <li>Acknowledgements | |
| 100 </ol> | |
| 101 | |
| 102 <p><hr> | |
| 103 <h2>1. Introduction</h2> | |
| 104 | |
| 105 <p>This document presents a description of the components | |
| 106 of the Roundup system and specifies their interfaces and | |
| 107 behaviour in sufficient detail to guide an implementation. | |
| 108 For the philosophy and rationale behind the Roundup design, | |
| 109 see the first-round Software Carpentry submission for Roundup. | |
| 110 This document fleshes out that design as well as specifying | |
| 111 interfaces so that the components can be developed separately. | |
| 112 | |
| 113 <p><hr> | |
| 114 <h2>2. The Layer Cake</h2> | |
| 115 | |
| 116 <p>Lots of software design documents come with a picture of | |
| 117 a cake. Everybody seems to like them. I also like cakes | |
| 118 (i think they are tasty). So i, too, shall include | |
| 119 a picture of a cake here. | |
| 120 | |
| 121 <p align=center><table cellspacing=0 cellpadding=10 border=0 align=center> | |
| 122 <tr> | |
| 123 <td bgcolor="#e8e8e8" align=center> | |
| 124 <p><font face="helvetica, arial"><small> | |
| 125 E-mail Client | |
| 126 </small></font> | |
| 127 </td> | |
| 128 <td bgcolor="#e0e0e0" align="center"> | |
| 129 <p><font face="helvetica, arial"><small> | |
| 130 Web Browser | |
| 131 </small></font> | |
| 132 </td> | |
| 133 <td bgcolor="#e8e8e8" align=center> | |
| 134 <p><font face="helvetica, arial"><small> | |
| 135 Detector Scripts | |
| 136 </small></font> | |
| 137 </td> | |
| 138 <td bgcolor="#e0e0e0" align="center"> | |
| 139 <p><font face="helvetica, arial"><small> | |
| 140 Shell | |
| 141 </small></font> | |
| 142 </td> | |
| 143 <tr> | |
| 144 <td bgcolor="#d0d0f0" align=center> | |
| 145 <p><font face="helvetica, arial"><small> | |
| 146 E-mail User Interface | |
| 147 </small></font> | |
| 148 </td> | |
| 149 <td bgcolor="#f0d0d0" align=center> | |
| 150 <p><font face="helvetica, arial"><small> | |
| 151 Web User Interface | |
| 152 </small></font> | |
| 153 </td> | |
| 154 <td bgcolor="#d0f0d0" align=center> | |
| 155 <p><font face="helvetica, arial"><small> | |
| 156 Detector Interface | |
| 157 </small></font> | |
| 158 </td> | |
| 159 <td bgcolor="#f0d0f0" align=center> | |
| 160 <p><font face="helvetica, arial"><small> | |
| 161 Command Interface | |
| 162 </small></font> | |
| 163 </td> | |
| 164 <tr> | |
| 165 <td bgcolor="#f0f0d0" colspan=4 align=center> | |
| 166 <p><font face="helvetica, arial"><small> | |
| 167 Roundup Database Layer | |
| 168 </small></font> | |
| 169 </td> | |
| 170 <tr> | |
| 171 <td bgcolor="#d0f0f0" colspan=4 align=center> | |
| 172 <p><font face="helvetica, arial"><small> | |
| 173 Hyperdatabase Layer | |
| 174 </small></font> | |
| 175 </td> | |
| 176 <tr> | |
| 177 <td bgcolor="#e8e8e8" colspan=4 align=center> | |
| 178 <p><font face="helvetica, arial"><small> | |
| 179 Storage Layer | |
| 180 </small></font> | |
| 181 </td> | |
| 182 </table> | |
| 183 | |
| 184 <p>The colourful parts of the cake are part of our system; | |
| 185 the faint grey parts of the cake are external components. | |
| 186 | |
| 187 <p>I will now proceed to forgo all table manners and | |
| 188 eat from the bottom of the cake to the top. You may want | |
| 189 to stand back a bit so you don't get covered in crumbs. | |
| 190 | |
| 191 <p><hr> | |
| 192 <h2>3. Hyperdatabase</h2> | |
| 193 | |
| 194 <p>The lowest-level component to be implemented is the hyperdatabase. | |
| 195 The hyperdatabase is intended to be | |
| 196 a flexible data store that can hold configurable data in | |
| 197 records which we call <em>nodes</em>. | |
| 198 | |
| 199 <p>The hyperdatabase is implemented on top of the storage layer, | |
| 200 an external module for storing its data. The storage layer could | |
| 201 be a third-party RDBMS; for a "batteries-included" distribution, | |
| 202 implementing the hyperdatabase on the standard <tt>bsddb</tt> | |
| 203 module is suggested. | |
| 204 | |
| 205 <h3>3.1. Dates and Date Arithmetic</h3> | |
| 206 | |
| 207 <p>Before we get into the hyperdatabase itself, we need a | |
| 208 way of handling dates. The hyperdatabase module provides | |
| 209 Timestamp objects for | |
| 210 representing date-and-time stamps and Interval objects for | |
| 211 representing date-and-time intervals. | |
| 212 | |
| 213 <p>As strings, date-and-time stamps are specified with | |
| 214 the date in international standard format | |
| 215 (<em>yyyy</em>-<em>mm</em>-<em>dd</em>) | |
| 216 joined to the time (<em>hh</em>:<em>mm</em>:<em>ss</em>) | |
| 217 by a period ("."). Dates in | |
| 218 this form can be easily compared and are fairly readable | |
| 219 when printed. An example of a valid stamp is | |
| 220 "<strong>2000-06-24.13:03:59</strong>". | |
| 221 We'll call this the "full date format". When Timestamp objects are | |
| 222 printed as strings, they appear in the full date format with | |
| 223 the time always given in GMT. The full date format is always | |
| 224 exactly 19 characters long. | |
| 225 | |
| 226 <p>For user input, some partial forms are also permitted: | |
| 227 the whole time or just the seconds may be omitted; and the whole date | |
| 228 may be omitted or just the year may be omitted. If the time is given, | |
| 229 the time is interpreted in the user's local time zone. | |
| 230 The <tt>Date</tt> constructor takes care of these conversions. | |
| 231 In the following examples, suppose that <em>yyyy</em> is the current year, | |
| 232 <em>mm</em> is the current month, and <em>dd</em> is the current | |
| 233 day of the month; and suppose that the user is on Eastern Standard Time. | |
| 234 | |
| 235 <ul> | |
| 236 <li>"<strong>2000-04-17</strong>" means <Date 2000-04-17.00:00:00> | |
| 237 <li>"<strong>01-25</strong>" means <Date <em>yyyy</em>-01-25.00:00:00> | |
| 238 <li>"<strong>2000-04-17.03:45</strong>" means <Date 2000-04-17.08:45:00> | |
| 239 <li>"<strong>08-13.22:13</strong>" means <Date <em>yyyy</em>-08-14.03:13:00> | |
| 240 <li>"<strong>11-07.09:32:43</strong>" means <Date <em>yyyy</em>-11-07.14:32:43> | |
| 241 <li>"<strong>14:25</strong>" means | |
| 242 <Date <em>yyyy</em>-<em>mm</em>-<em>dd</em>.19:25:00> | |
| 243 <li>"<strong>8:47:11</strong>" means | |
| 244 <Date <em>yyyy</em>-<em>mm</em>-<em>dd</em>.13:47:11> | |
| 245 <li>the special date "<strong>.</strong>" means "right now" | |
| 246 </ul> | |
| 247 | |
| 248 <p>Date intervals are specified using the suffixes | |
| 249 "y", "m", and "d". The suffix "w" (for "week") means 7 days. | |
| 250 Time intervals are specified in hh:mm:ss format (the seconds | |
| 251 may be omitted, but the hours and minutes may not). | |
| 252 | |
| 253 <ul> | |
| 254 <li>"<strong>3y</strong>" means three years | |
| 255 <li>"<strong>2y 1m</strong>" means two years and one month | |
| 256 <li>"<strong>1m 25d</strong>" means one month and 25 days | |
| 257 <li>"<strong>2w 3d</strong>" means two weeks and three days | |
| 258 <li>"<strong>1d 2:50</strong>" means one day, two hours, and 50 minutes | |
| 259 <li>"<strong>14:00</strong>" means 14 hours | |
| 260 <li>"<strong>0:04:33</strong>" means four minutes and 33 seconds | |
| 261 </ul> | |
| 262 | |
| 263 <p>The Date class should understand simple date expressions of the form | |
| 264 <em>stamp</em> + <em>interval</em> and <em>stamp</em> - <em>interval</em>. | |
| 265 When adding or subtracting intervals involving months or years, the | |
| 266 components are handled separately. For example, when evaluating | |
| 267 "<strong>2000-06-25 + 1m 10d</strong>", we first add one month to | |
| 268 get <strong>2000-07-25</strong>, then add 10 days to get | |
| 269 <strong>2000-08-04</strong> (rather than trying to decide whether | |
| 270 <strong>1m 10d</strong> means 38 or 40 or 41 days). | |
| 271 | |
| 272 <p>Here is an outline of the Date and Interval classes. | |
| 273 | |
| 274 <blockquote> | |
| 275 <pre><small>class <strong>Date</strong>: | |
| 276 def <strong>__init__</strong>(self, spec, offset): | |
| 277 """Construct a date given a specification and a time zone offset. | |
| 278 | |
| 279 'spec' is a full date or a partial form, with an optional | |
| 280 added or subtracted interval. 'offset' is the local time | |
| 281 zone offset from GMT in hours. | |
| 282 """ | |
| 283 | |
| 284 def <strong>__add__</strong>(self, interval): | |
| 285 """Add an interval to this date to produce another date.""" | |
| 286 | |
| 287 def <strong>__sub__</strong>(self, interval): | |
| 288 """Subtract an interval from this date to produce another date.""" | |
| 289 | |
| 290 def <strong>__cmp__</strong>(self, other): | |
| 291 """Compare this date to another date.""" | |
| 292 | |
| 293 def <strong>__str__</strong>(self): | |
| 294 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format.""" | |
| 295 | |
| 296 def <strong>local</strong>(self, offset): | |
| 297 """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.""" | |
| 298 | |
| 299 class <strong>Interval</strong>: | |
| 300 def <strong>__init__</strong>(self, spec): | |
| 301 """Construct an interval given a specification.""" | |
| 302 | |
| 303 def <strong>__cmp__</strong>(self, other): | |
| 304 """Compare this interval to another interval.""" | |
| 305 | |
| 306 def <strong>__str__</strong>(self): | |
| 307 """Return this interval as a string.""" | |
| 308 </small></pre> | |
| 309 </blockquote> | |
| 310 | |
| 311 <p>Here are some examples of how these classes would behave in practice. | |
| 312 For the following examples, assume that we are on Eastern Standard | |
| 313 Time and the current local time is 19:34:02 on 25 June 2000. | |
| 314 | |
| 315 <blockquote><pre><small | |
| 316 >>>> <span class="input">Date(".")</span> | |
| 317 <span class="output"><Date 2000-06-26.00:34:02></span> | |
| 318 >>> <span class="input">_.local(-5)</span> | |
| 319 <span class="output">"2000-06-25.19:34:02"</span> | |
| 320 >>> <span class="input">Date(". + 2d")</span> | |
| 321 <span class="output"><Date 2000-06-28.00:34:02></span> | |
| 322 >>> <span class="input">Date("1997-04-17", -5)</span> | |
| 323 <span class="output"><Date 1997-04-17.00:00:00></span> | |
| 324 >>> <span class="input">Date("01-25", -5)</span> | |
| 325 <span class="output"><Date 2000-01-25.00:00:00></span> | |
| 326 >>> <span class="input">Date("08-13.22:13", -5)</span> | |
| 327 <span class="output"><Date 2000-08-14.03:13:00></span> | |
| 328 >>> <span class="input">Date("14:25", -5)</span> | |
| 329 <span class="output"><Date 2000-06-25.19:25:00></span> | |
| 330 >>> <span class="input">Interval(" 3w 1 d 2:00")</span> | |
| 331 <span class="output"><Interval 22d 2:00></span> | |
| 332 >>> <span class="input">Date(". + 2d") - Interval("3w")</span> | |
| 333 <span class="output"><Date 2000-06-07.00:34:02></span | |
| 334 ></small></pre></blockquote> | |
| 335 | |
| 336 <h3>3.2. Nodes and Classes</h3> | |
| 337 | |
| 338 <p>Nodes contain data in <em>properties</em>. To Python, these | |
| 339 properties are presented as the key-value pairs of a dictionary. | |
| 340 Each node belongs to a <em>class</em> which defines the names | |
| 341 and types of its properties. The database permits the creation | |
| 342 and modification of classes as well as nodes. | |
| 343 | |
| 344 <h3>3.3. Identifiers and Designators</h3> | |
| 345 | |
| 346 <p>Each node has a numeric identifier which is unique among | |
| 347 nodes in its class. The nodes are numbered sequentially | |
| 348 within each class in order of creation, starting from 1. | |
| 349 The <em>designator</em> | |
| 350 for a node is a way to identify a node in the database, and | |
| 351 consists of the name of the node's class concatenated with | |
| 352 the node's numeric identifier. | |
| 353 | |
| 354 <p>For example, if "spam" and "eggs" are classes, the first | |
| 355 node created in class "spam" has id 1 and designator "spam1". | |
| 356 The first node created in class "eggs" also has id 1 but has | |
| 357 the distinct designator "eggs1". Node designators are | |
| 358 conventionally enclosed in square brackets when mentioned | |
| 359 in plain text. This permits a casual mention of, say, | |
| 360 "[patch37]" in an e-mail message to be turned into an active | |
| 361 hyperlink. | |
| 362 | |
| 363 <h3>3.4. Property Names and Types</h3> | |
| 364 | |
| 365 <p>Property names must begin with a letter. | |
| 366 | |
| 367 <p>A property may be one of five <em>basic types</em>: | |
| 368 | |
| 369 <ul> | |
| 370 <li><em>String</em> properties are for storing arbitrary-length | |
| 371 strings. | |
| 372 | |
| 373 <li><em>Date</em> properties store date-and-time stamps. | |
| 374 Their values are Timestamp objects. | |
| 375 | |
| 376 <li>A <em>Link</em> property refers to a single other node | |
| 377 selected from a specified class. The class is part of the property; | |
| 378 the value is an integer, the id of the chosen node. | |
| 379 | |
| 380 <li>A <em>Multilink</em> property refers to possibly many nodes | |
| 381 in a specified class. The value is a list of integers. | |
| 382 </ul> | |
| 383 | |
| 384 <p><tt>None</tt> is also a permitted value for any of these property | |
| 385 types. An attempt to store <tt>None</tt> into a String property | |
| 386 stores the empty string; an attempt to store <tt>None</tt> | |
| 387 into a Multilink property stores an empty list. | |
| 388 | |
| 389 <h3>3.5. Interface Specification</h3> | |
| 390 | |
| 391 <p>The hyperdb module provides property objects to designate | |
| 392 the different kinds of properties. These objects are used when | |
| 393 specifying what properties belong in classes. | |
| 394 | |
| 395 <blockquote><pre><small | |
| 396 >class <strong>String</strong>: | |
| 397 def <strong>__init__</strong>(self): | |
| 398 """An object designating a String property.""" | |
| 399 | |
| 400 class <strong>Date</strong>: | |
| 401 def <strong>__init__</strong>(self): | |
| 402 """An object designating a Date property.""" | |
| 403 | |
| 404 class <strong>Link</strong>: | |
| 405 def <strong>__init__</strong>(self, classname): | |
| 406 """An object designating a Link property that links to | |
| 407 nodes in a specified class.""" | |
| 408 | |
| 409 class <strong>Multilink</strong>: | |
| 410 def <strong>__init__</strong>(self, classname): | |
| 411 """An object designating a Multilink property that links | |
| 412 to nodes in a specified class.""" | |
| 413 </small></pre></blockquote> | |
| 414 | |
| 415 <p>Here is the interface provided by the hyperdatabase. | |
| 416 | |
| 417 <blockquote><pre><small | |
| 418 >class <strong>Database</strong>: | |
| 419 """A database for storing records containing flexible data types.""" | |
| 420 | |
| 421 def <strong>__init__</strong>(self, storagelocator, journaltag): | |
| 422 """Open a hyperdatabase given a specifier to some storage. | |
| 423 | |
| 424 The meaning of 'storagelocator' depends on the particular | |
| 425 implementation of the hyperdatabase. It could be a file name, | |
| 426 a directory path, a socket descriptor for a connection to a | |
| 427 database over the network, etc. | |
| 428 | |
| 429 The 'journaltag' is a token that will be attached to the journal | |
| 430 entries for any edits done on the database. If 'journaltag' is | |
| 431 None, the database is opened in read-only mode: the Class.create(), | |
| 432 Class.set(), and Class.retire() methods are disabled. | |
| 433 """ | |
| 434 | |
| 435 def <strong>__getattr__</strong>(self, classname): | |
| 436 """A convenient way of calling self.getclass(classname).""" | |
| 437 | |
| 438 def <strong>getclasses</strong>(self): | |
| 439 """Return a list of the names of all existing classes.""" | |
| 440 | |
| 441 def <strong>getclass</strong>(self, classname): | |
| 442 """Get the Class object representing a particular class. | |
| 443 | |
| 444 If 'classname' is not a valid class name, a KeyError is raised. | |
| 445 """ | |
| 446 | |
| 447 class <strong>Class</strong>: | |
| 448 """The handle to a particular class of nodes in a hyperdatabase.""" | |
| 449 | |
| 450 def <strong>__init__</strong>(self, db, classname, **properties): | |
| 451 """Create a new class with a given name and property specification. | |
| 452 | |
| 453 'classname' must not collide with the name of an existing class, | |
| 454 or a ValueError is raised. The keyword arguments in 'properties' | |
| 455 must map names to property objects, or a TypeError is raised. | |
| 456 """ | |
| 457 | |
| 458 # Editing nodes: | |
| 459 | |
| 460 def <strong>create</strong>(self, **propvalues): | |
| 461 """Create a new node of this class and return its id. | |
| 462 | |
| 463 The keyword arguments in 'propvalues' map property names to values. | |
| 464 The values of arguments must be acceptable for the types of their | |
| 465 corresponding properties or a TypeError is raised. If this class | |
| 466 has a key property, it must be present and its value must not | |
| 467 collide with other key strings or a ValueError is raised. Any other | |
| 468 properties on this class that are missing from the 'propvalues' | |
| 469 dictionary are set to None. If an id in a link or multilink | |
| 470 property does not refer to a valid node, an IndexError is raised. | |
| 471 """ | |
| 472 | |
| 473 def <strong>get</strong>(self, nodeid, propname): | |
| 474 """Get the value of a property on an existing node of this class. | |
| 475 | |
| 476 'nodeid' must be the id of an existing node of this class or an | |
| 477 IndexError is raised. 'propname' must be the name of a property | |
| 478 of this class or a KeyError is raised. | |
| 479 """ | |
| 480 | |
| 481 def <strong>set</strong>(self, nodeid, **propvalues): | |
| 482 """Modify a property on an existing node of this class. | |
| 483 | |
| 484 'nodeid' must be the id of an existing node of this class or an | |
| 485 IndexError is raised. Each key in 'propvalues' must be the name | |
| 486 of a property of this class or a KeyError is raised. All values | |
| 487 in 'propvalues' must be acceptable types for their corresponding | |
| 488 properties or a TypeError is raised. If the value of the key | |
| 489 property is set, it must not collide with other key strings or a | |
| 490 ValueError is raised. If the value of a Link or Multilink | |
| 491 property contains an invalid node id, a ValueError is raised. | |
| 492 """ | |
| 493 | |
| 494 def <strong>retire</strong>(self, nodeid): | |
| 495 """Retire a node. | |
| 496 | |
| 497 The properties on the node remain available from the get() method, | |
| 498 and the node's id is never reused. Retired nodes are not returned | |
| 499 by the find(), list(), or lookup() methods, and other nodes may | |
| 500 reuse the values of their key properties. | |
| 501 """ | |
| 502 | |
| 503 def <strong>history</strong>(self, nodeid): | |
| 504 """Retrieve the journal of edits on a particular node. | |
| 505 | |
| 506 'nodeid' must be the id of an existing node of this class or an | |
| 507 IndexError is raised. | |
| 508 | |
| 509 The returned list contains tuples of the form | |
| 510 | |
| 511 (date, tag, action, params) | |
| 512 | |
| 513 'date' is a Timestamp object specifying the time of the change and | |
| 514 'tag' is the journaltag specified when the database was opened. | |
| 515 'action' may be: | |
| 516 | |
| 517 'create' or 'set' -- 'params' is a dictionary of property values | |
| 518 'link' or 'unlink' -- 'params' is (classname, nodeid, propname) | |
| 519 'retire' -- 'params' is None | |
| 520 """ | |
| 521 | |
| 522 # Locating nodes: | |
| 523 | |
| 524 def <strong>setkey</strong>(self, propname): | |
| 525 """Select a String property of this class to be the key property. | |
| 526 | |
| 527 'propname' must be the name of a String property of this class or | |
| 528 None, or a TypeError is raised. The values of the key property on | |
| 529 all existing nodes must be unique or a ValueError is raised. | |
| 530 """ | |
| 531 | |
| 532 def <strong>getkey</strong>(self): | |
| 533 """Return the name of the key property for this class or None.""" | |
| 534 | |
| 535 def <strong>lookup</strong>(self, keyvalue): | |
| 536 """Locate a particular node by its key property and return its id. | |
| 537 | |
| 538 If this class has no key property, a TypeError is raised. If the | |
| 539 'keyvalue' matches one of the values for the key property among | |
| 540 the nodes in this class, the matching node's id is returned; | |
| 541 otherwise a KeyError is raised. | |
| 542 """ | |
| 543 | |
| 544 def <strong>find</strong>(self, propname, nodeid): | |
| 545 """Get the ids of nodes in this class which link to a given node. | |
| 546 | |
| 547 'propname' must be the name of a property in this class, or a | |
| 548 KeyError is raised. That property must be a Link or Multilink | |
| 549 property, or a TypeError is raised. 'nodeid' must be the id of | |
| 550 an existing node in the class linked to by the given property, | |
| 551 or an IndexError is raised. | |
| 552 """ | |
| 553 | |
| 554 def <strong>list</strong>(self): | |
| 555 """Return a list of the ids of the active nodes in this class.""" | |
| 556 | |
| 557 def <strong>count</strong>(self): | |
| 558 """Get the number of nodes in this class. | |
| 559 | |
| 560 If the returned integer is 'numnodes', the ids of all the nodes | |
| 561 in this class run from 1 to numnodes, and numnodes+1 will be the | |
| 562 id of the next node to be created in this class. | |
| 563 """ | |
| 564 | |
| 565 # Manipulating properties: | |
| 566 | |
| 567 def <strong>getprops</strong>(self): | |
| 568 """Return a dictionary mapping property names to property objects.""" | |
| 569 | |
| 570 def <strong>addprop</strong>(self, **properties): | |
| 571 """Add properties to this class. | |
| 572 | |
| 573 The keyword arguments in 'properties' must map names to property | |
| 574 objects, or a TypeError is raised. None of the keys in 'properties' | |
| 575 may collide with the names of existing properties, or a ValueError | |
| 576 is raised before any properties have been added. | |
| 577 """</small></pre></blockquote> | |
| 578 | |
| 579 <h3>3.6. Application Example</h3> | |
| 580 | |
| 581 <p>Here is an example of how the hyperdatabase module would work in practice. | |
| 582 | |
| 583 <blockquote><pre><small | |
| 584 >>>> <span class="input">import hyperdb</span> | |
| 585 >>> <span class="input">db = hyperdb.Database("foo.db", "ping")</span> | |
| 586 >>> <span class="input">db</span> | |
| 587 <span class="output"><hyperdb.Database "foo.db" opened by "ping"></span> | |
| 588 >>> <span class="input">hyperdb.Class(db, "status", name=hyperdb.String())</span> | |
| 589 <span class="output"><hyperdb.Class "status"></span> | |
| 590 >>> <span class="input">_.setkey("name")</span> | |
| 591 >>> <span class="input">db.status.create(name="unread")</span> | |
| 592 <span class="output">1</span> | |
| 593 >>> <span class="input">db.status.create(name="in-progress")</span> | |
| 594 <span class="output">2</span> | |
| 595 >>> <span class="input">db.status.create(name="testing")</span> | |
| 596 <span class="output">3</span> | |
| 597 >>> <span class="input">db.status.create(name="resolved")</span> | |
| 598 <span class="output">4</span> | |
| 599 >>> <span class="input">db.status.count()</span> | |
| 600 <span class="output">4</span> | |
| 601 >>> <span class="input">db.status.list()</span> | |
| 602 <span class="output">[1, 2, 3, 4]</span> | |
| 603 >>> <span class="input">db.status.lookup("in-progress")</span> | |
| 604 <span class="output">2</span> | |
| 605 >>> <span class="input">db.status.retire(3)</span> | |
| 606 >>> <span class="input">db.status.list()</span> | |
| 607 <span class="output">[1, 2, 4]</span> | |
| 608 >>> <span class="input">hyperdb.Class(db, "issue", title=hyperdb.String(), status=hyperdb.Link("status"))</span> | |
| 609 <span class="output"><hyperdb.Class "issue"></span> | |
| 610 >>> <span class="input">db.issue.create(title="spam", status=1)</span> | |
| 611 <span class="output">1</span> | |
| 612 >>> <span class="input">db.issue.create(title="eggs", status=2)</span> | |
| 613 <span class="output">2</span> | |
| 614 >>> <span class="input">db.issue.create(title="ham", status=4)</span> | |
| 615 <span class="output">3</span> | |
| 616 >>> <span class="input">db.issue.create(title="arguments", status=2)</span> | |
| 617 <span class="output">4</span> | |
| 618 >>> <span class="input">db.issue.create(title="abuse", status=1)</span> | |
| 619 <span class="output">5</span> | |
| 620 >>> <span class="input">hyperdb.Class(db, "user", username=hyperdb.Key(), password=hyperdb.String())</span> | |
| 621 <span class="output"><hyperdb.Class "user"></span> | |
| 622 >>> <span class="input">db.issue.addprop(fixer=hyperdb.Link("user"))</span> | |
| 623 >>> <span class="input">db.issue.getprops()</span> | |
| 624 <span class="output" | |
| 625 >{"title": <hyperdb.String>, "status": <hyperdb.Link to "status">, | |
| 626 "user": <hyperdb.Link to "user">}</span> | |
| 627 >>> <span class="input">db.issue.set(5, status=2)</span> | |
| 628 >>> <span class="input">db.issue.get(5, "status")</span> | |
| 629 <span class="output">2</span> | |
| 630 >>> <span class="input">db.status.get(2, "name")</span> | |
| 631 <span class="output">"in-progress"</span> | |
| 632 >>> <span class="input">db.issue.get(5, "title")</span> | |
| 633 <span class="output">"abuse"</span> | |
| 634 >>> <span class="input">db.issue.find("status", db.status.lookup("in-progress"))</span> | |
| 635 <span class="output">[2, 4, 5]</span> | |
| 636 >>> <span class="input">db.issue.history(5)</span> | |
| 637 <span class="output" | |
| 638 >[(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse", "status": 1}), | |
| 639 (<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})]</span> | |
| 640 >>> <span class="input">db.status.history(1)</span> | |
| 641 <span class="output" | |
| 642 >[(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")), | |
| 643 (<Date 2000-06-28.19:11:04>, "ping", "unlink", ("issue", 5, "status"))]</span> | |
| 644 >>> <span class="input">db.status.history(2)</span> | |
| 645 <span class="output" | |
| 646 >[(<Date 2000-06-28.19:11:04>, "ping", "link", ("issue", 5, "status"))]</span> | |
| 647 </small></pre></blockquote> | |
| 648 | |
| 649 <p>For the purposes of journalling, when a Multilink property is | |
| 650 set to a new list of nodes, the hyperdatabase compares the old | |
| 651 list to the new list. | |
| 652 The journal records "unlink" events for all the nodes that appear | |
| 653 in the old list but not the new list, | |
| 654 and "link" events for | |
| 655 all the nodes that appear in the new list but not in the old list. | |
| 656 | |
| 657 <p><hr> | |
| 658 <h2>4. Roundup Database</h2> | |
| 659 | |
| 660 <p>The Roundup database layer is implemented on top of the | |
| 661 hyperdatabase and mediates calls to the database. | |
| 662 Some of the classes in the Roundup database are considered | |
| 663 <em>item classes</em>. | |
| 664 The Roundup database layer adds detectors and user nodes, | |
| 665 and on items it provides mail spools, nosy lists, and superseders. | |
| 666 | |
| 667 <h3>4.1. Reserved Classes</h3> | |
| 668 | |
| 669 <p>Internal to this layer we reserve three special classes | |
| 670 of nodes that are not items. | |
| 671 | |
| 672 <h4>4.1.1. Users</h4> | |
| 673 | |
| 674 <p>Users are stored in the hyperdatabase as nodes of | |
| 675 class "user". The "user" class has the definition: | |
| 676 | |
| 677 <blockquote><pre><small | |
| 678 >hyperdb.Class(db, "user", username=hyperdb.String(), | |
| 679 password=hyperdb.String(), | |
| 680 address=hyperdb.String()) | |
| 681 db.user.setkey("username")</small></pre></blockquote> | |
| 682 | |
| 683 <h4>4.1.2. Messages</h4> | |
| 684 | |
| 685 <p>E-mail messages are represented by hyperdatabase nodes of class "msg". | |
| 686 The actual text content of the messages is stored in separate files. | |
| 687 (There's no advantage to be gained by stuffing them into the | |
| 688 hyperdatabase, and if messages are stored in ordinary text files, | |
| 689 they can be grepped from the command line.) The text of a message is | |
| 690 saved in a file named after the message node designator (e.g. "msg23") | |
| 691 for the sake of the command interface (see below). Attachments are | |
| 692 stored separately and associated with "file" nodes. | |
| 693 The "msg" class has the definition: | |
| 694 | |
| 695 <blockquote><pre><small | |
| 696 >hyperdb.Class(db, "msg", author=hyperdb.Link("user"), | |
| 697 recipients=hyperdb.Multilink("user"), | |
| 698 date=hyperdb.Date(), | |
| 699 summary=hyperdb.String(), | |
| 700 files=hyperdb.Multilink("file"))</small | |
| 701 ></pre></blockquote> | |
| 702 | |
| 703 <p>The "author" property indicates the author of the message | |
| 704 (a "user" node must exist in the hyperdatabase for any messages | |
| 705 that are stored in the system). | |
| 706 The "summary" property contains a summary of the message for display | |
| 707 in a message index. | |
| 708 | |
| 709 <h4>4.1.3. Files</h4> | |
| 710 | |
| 711 <p>Submitted files are represented by hyperdatabase | |
| 712 nodes of class "file". Like e-mail messages, the file content | |
| 713 is stored in files outside the database, | |
| 714 named after the file node designator (e.g. "file17"). | |
| 715 The "file" class has the definition: | |
| 716 | |
| 717 <blockquote><pre><small | |
| 718 >hyperdb.Class(db, "file", user=hyperdb.Link("user"), | |
| 719 name=hyperdb.String(), | |
| 720 type=hyperdb.String())</small></pre></blockquote> | |
| 721 | |
| 722 <p>The "user" property indicates the user who submitted the | |
| 723 file, the "name" property holds the original name of the file, | |
| 724 and the "type" property holds the MIME type of the file as received. | |
| 725 | |
| 726 <h3>4.2. Item Classes</h3> | |
| 727 | |
| 728 <p>All items have the following standard properties: | |
| 729 | |
| 730 <blockquote><pre><small | |
| 731 >title=hyperdb.String() | |
| 732 messages=hyperdb.Multilink("msg") | |
| 733 files=hyperdb.Multilink("file") | |
| 734 nosy=hyperdb.Multilink("user") | |
| 735 superseder=hyperdb.Multilink("item")</small></pre></blockquote> | |
| 736 | |
| 737 <p>Also, two Date properties named "creation" and "activity" are | |
| 738 fabricated by the Roundup database layer. By "fabricated" we | |
| 739 mean that no such properties are actually stored in the | |
| 740 hyperdatabase, but when properties on items are requested, the | |
| 741 "creation" and "activity" properties are made available. | |
| 742 The value of the "creation" property is the date when an item was | |
| 743 created, and the value of the "activity" property is the | |
| 744 date when any property on the item was last edited (equivalently, | |
| 745 these are the dates on the first and last records in the item's journal). | |
| 746 | |
| 747 <h3>4.3. Interface Specification</h3> | |
| 748 | |
| 749 <p>The interface to a Roundup database delegates most method | |
| 750 calls to the hyperdatabase, except for the following | |
| 751 changes and additional methods. | |
| 752 | |
| 753 <blockquote><pre><small | |
| 754 >class <strong>Database</strong>: | |
| 755 # Overridden methods: | |
| 756 | |
| 757 def <strong>__init__</strong>(self, storagelocator, journaltag): | |
| 758 """When the Roundup database is opened by a particular user, | |
| 759 the 'journaltag' is the id of the user's "user" node.""" | |
| 760 | |
| 761 def <strong>getclass</strong>(self, classname): | |
| 762 """This method now returns an instance of either Class or | |
| 763 ItemClass depending on whether an item class is specified.""" | |
| 764 | |
| 765 # New methods: | |
| 766 | |
| 767 def <strong>getuid</strong>(self): | |
| 768 """Return the id of the "user" node associated with the user | |
| 769 that owns this connection to the hyperdatabase.""" | |
| 770 | |
| 771 class <strong>Class</strong>: | |
| 772 # Overridden methods: | |
| 773 | |
| 774 def <strong>create</strong>(self, **propvalues): | |
| 775 def <strong>set</strong>(self, **propvalues): | |
| 776 def <strong>retire</strong>(self, nodeid): | |
| 777 """These operations trigger detectors and can be vetoed. Attempts | |
| 778 to modify the "creation" or "activity" properties cause a KeyError. | |
| 779 """ | |
| 780 | |
| 781 # New methods: | |
| 782 | |
| 783 def <strong>audit</strong>(self, event, detector): | |
| 784 def <strong>react</strong>(self, event, detector): | |
| 785 """Register a detector (see below for more details).""" | |
| 786 | |
| 787 class <strong>ItemClass</strong>(Class): | |
| 788 # Overridden methods: | |
| 789 | |
| 790 def <strong>__init__</strong>(self, db, classname, **properties): | |
| 791 """The newly-created class automatically includes the "messages", | |
| 792 "files", "nosy", and "superseder" properties. If the 'properties' | |
| 793 dictionary attempts to specify any of these properties or a | |
| 794 "creation" or "activity" property, a ValueError is raised.""" | |
| 795 | |
| 796 def <strong>get</strong>(self, nodeid, propname): | |
| 797 def <strong>getprops</strong>(self): | |
| 798 """In addition to the actual properties on the node, these | |
| 799 methods provide the "creation" and "activity" properties.""" | |
| 800 | |
| 801 # New methods: | |
| 802 | |
| 803 def <strong>addmessage</strong>(self, nodeid, summary, text): | |
| 804 """Add a message to an item's mail spool. | |
| 805 | |
| 806 A new "msg" node is constructed using the current date, the | |
| 807 user that owns the database connection as the author, and | |
| 808 the specified summary text. The "files" and "recipients" | |
| 809 fields are left empty. The given text is saved as the body | |
| 810 of the message and the node is appended to the "messages" | |
| 811 field of the specified item. | |
| 812 """ | |
| 813 | |
| 814 def <strong>sendmessage</strong>(self, nodeid, msgid): | |
| 815 """Send a message to the members of an item's nosy list. | |
| 816 | |
| 817 The message is sent only to users on the nosy list who are not | |
| 818 already on the "recipients" list for the message. These users | |
| 819 are then added to the message's "recipients" list. | |
| 820 """ | |
| 821 </small></pre></blockquote> | |
| 822 | |
| 823 <h3>4.4. Default Schema</h3> | |
| 824 | |
| 825 <p>The default schema included with Roundup turns it into a | |
| 826 typical software bug tracker. The database is set up like this: | |
| 827 | |
| 828 <blockquote><pre><small | |
| 829 >pri = Class(db, "priority", name=hyperdb.String(), order=hyperdb.String()) | |
| 830 pri.setkey("name") | |
| 831 pri.create(name="critical", order="1") | |
| 832 pri.create(name="urgent", order="2") | |
| 833 pri.create(name="bug", order="3") | |
| 834 pri.create(name="feature", order="4") | |
| 835 pri.create(name="wish", order="5") | |
| 836 | |
| 837 stat = Class(db, "status", name=hyperdb.String(), order=hyperdb.String()) | |
| 838 stat.setkey("name") | |
| 839 stat.create(name="unread", order="1") | |
| 840 stat.create(name="deferred", order="2") | |
| 841 stat.create(name="chatting", order="3") | |
| 842 stat.create(name="need-eg", order="4") | |
| 843 stat.create(name="in-progress", order="5") | |
| 844 stat.create(name="testing", order="6") | |
| 845 stat.create(name="done-cbb", order="7") | |
| 846 stat.create(name="resolved", order="8") | |
| 847 | |
| 848 Class(db, "keyword", name=hyperdb.String()) | |
| 849 | |
| 850 Class(db, "issue", fixer=hyperdb.Multilink("user"), | |
| 851 topic=hyperdb.Multilink("keyword"), | |
| 852 priority=hyperdb.Link("priority"), | |
| 853 status=hyperdb.Link("status")) | |
| 854 </small></pre></blockquote> | |
| 855 | |
| 856 <p>(The "order" property hasn't been explained yet. It | |
| 857 gets used by the Web user interface for sorting.) | |
| 858 | |
| 859 <p>The above isn't as pretty-looking as the schema specification | |
| 860 in the first-stage submission, but it could be made just as easy | |
| 861 with the addition of a convenience function like <tt>Choice</tt> | |
| 862 for setting up the "priority" and "status" classes: | |
| 863 | |
| 864 <blockquote><pre><small | |
| 865 >def Choice(name, *options): | |
| 866 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String()) | |
| 867 for i in range(len(options)): | |
| 868 cl.create(name=option[i], order=i) | |
| 869 return hyperdb.Link(name) | |
| 870 </small></pre></blockquote> | |
| 871 | |
| 872 <p><hr> | |
| 873 <h2>5. Detector Interface</h2> | |
| 874 | |
| 875 <p>Detectors are Python functions that are triggered on certain | |
| 876 kinds of events. The definitions of the | |
| 877 functions live in Python modules placed in a directory set aside | |
| 878 for this purpose. Importing the Roundup database module also | |
| 879 imports all the modules in this directory, and the <tt>init()</tt> | |
| 880 function of each module is called when a database is opened to | |
| 881 provide it a chance to register its detectors. | |
| 882 | |
| 883 <p>There are two kinds of detectors: | |
| 884 | |
| 885 <ul> | |
| 886 <li>an <em>auditor</em> is triggered just before modifying an node | |
| 887 <li>a <em>reactor</em> is triggered just after an node has been modified | |
| 888 </ul> | |
| 889 | |
| 890 <p>When the Roundup database is about to perform a | |
| 891 <tt>create()</tt>, <tt>set()</tt>, or <tt>retire()</tt> | |
| 892 operation, it first calls any auditors that | |
| 893 have been registered for that operation on that class. | |
| 894 Any auditor may raise a <tt>Reject</tt> exception | |
| 895 to abort the operation. | |
| 896 | |
| 897 <p>If none of the auditors raises an exception, the database | |
| 898 proceeds to carry out the operation. After it's done, it | |
| 899 then calls all of the reactors that have been registered | |
| 900 for the operation. | |
| 901 | |
| 902 <h3>5.1. Interface Specification</h3> | |
| 903 | |
| 904 <p>The <tt>audit()</tt> and <tt>react()</tt> methods | |
| 905 register detectors on a given class of nodes. | |
| 906 | |
| 907 <blockquote><pre><small | |
| 908 >class Class: | |
| 909 def <strong>audit</strong>(self, event, detector): | |
| 910 """Register an auditor on this class. | |
| 911 | |
| 912 'event' should be one of "create", "set", or "retire". | |
| 913 'detector' should be a function accepting four arguments. | |
| 914 """ | |
| 915 | |
| 916 def <strong>react</strong>(self, event, detector): | |
| 917 """Register a reactor on this class. | |
| 918 | |
| 919 'event' should be one of "create", "set", or "retire". | |
| 920 'detector' should be a function accepting four arguments. | |
| 921 """ | |
| 922 </small></pre></blockquote> | |
| 923 | |
| 924 <p>Auditors are called with the arguments: | |
| 925 | |
| 926 <blockquote><pre><small | |
| 927 >audit(db, cl, nodeid, newdata)</small></pre></blockquote> | |
| 928 | |
| 929 where <tt>db</tt> is the database, <tt>cl</tt> is an | |
| 930 instance of Class or ItemClass within the database, and <tt>newdata</tt> | |
| 931 is a dictionary mapping property names to values. | |
| 932 | |
| 933 For a <tt>create()</tt> | |
| 934 operation, the <tt>nodeid</tt> argument is <tt>None</tt> and <tt>newdata</tt> | |
| 935 contains all of the initial property values with which the node | |
| 936 is about to be created. | |
| 937 | |
| 938 For a <tt>set()</tt> operation, <tt>newdata</tt> | |
| 939 contains only the names and values of properties that are about | |
| 940 to be changed. | |
| 941 | |
| 942 For a <tt>retire()</tt> operation, <tt>newdata</tt> is <tt>None</tt>. | |
| 943 | |
| 944 <p>Reactors are called with the arguments: | |
| 945 | |
| 946 <blockquote><pre><small | |
| 947 >react(db, cl, nodeid, olddata)</small></pre></blockquote> | |
| 948 | |
| 949 where <tt>db</tt> is the database, <tt>cl</tt> is an | |
| 950 instance of Class or ItemClass within the database, and <tt>olddata</tt> | |
| 951 is a dictionary mapping property names to values. | |
| 952 | |
| 953 For a <tt>create()</tt> | |
| 954 operation, the <tt>nodeid</tt> argument is the id of the | |
| 955 newly-created node and <tt>olddata</tt> is None. | |
| 956 | |
| 957 For a <tt>set()</tt> operation, <tt>olddata</tt> | |
| 958 contains the names and previous values of properties that were changed. | |
| 959 | |
| 960 For a <tt>retire()</tt> operation, <tt>nodeid</tt> is the | |
| 961 id of the retired node and <tt>olddata</tt> is <tt>None</tt>. | |
| 962 | |
| 963 <h3>5.2. Detector Example</h3> | |
| 964 | |
| 965 <p>Here is an example of detectors written for a hypothetical | |
| 966 project-management application, where users can signal approval | |
| 967 of a project by adding themselves to an "approvals" list, and | |
| 968 a project proceeds when it has three approvals. | |
| 969 | |
| 970 <blockquote><pre><small | |
| 971 ># Permit users only to add themselves to the "approvals" list. | |
| 972 | |
| 973 def check_approvals(db, cl, id, newdata): | |
| 974 if newdata.has_key("approvals"): | |
| 975 if cl.get(id, "status") == db.status.lookup("approved"): | |
| 976 raise Reject, "You can't modify the approvals list " \ | |
| 977 "for a project that has already been approved." | |
| 978 old = cl.get(id, "approvals") | |
| 979 new = newdata["approvals"] | |
| 980 for uid in old: | |
| 981 if uid not in new and uid != db.getuid(): | |
| 982 raise Reject, "You can't remove other users from the " | |
| 983 "approvals list; you can only remove yourself." | |
| 984 for uid in new: | |
| 985 if uid not in old and uid != db.getuid(): | |
| 986 raise Reject, "You can't add other users to the approvals " | |
| 987 "list; you can only add yourself." | |
| 988 | |
| 989 # When three people have approved a project, change its | |
| 990 # status from "pending" to "approved". | |
| 991 | |
| 992 def approve_project(db, cl, id, olddata): | |
| 993 if olddata.has_key("approvals") and len(cl.get(id, "approvals")) == 3: | |
| 994 if cl.get(id, "status") == db.status.lookup("pending"): | |
| 995 cl.set(id, status=db.status.lookup("approved")) | |
| 996 | |
| 997 def init(db): | |
| 998 db.project.audit("set", check_approval) | |
| 999 db.project.react("set", approve_project)</small | |
| 1000 ></pre></blockquote> | |
| 1001 | |
| 1002 <p>Here is another example of a detector that can allow or prevent | |
| 1003 the creation of new nodes. In this scenario, patches for a software | |
| 1004 project are submitted by sending in e-mail with an attached file, | |
| 1005 and we want to ensure that there are <tt>text/plain</tt> attachments on | |
| 1006 the message. The maintainer of the package can then apply the | |
| 1007 patch by setting its status to "applied". | |
| 1008 | |
| 1009 <blockquote><pre><small | |
| 1010 ># Only accept attempts to create new patches that come with patch files. | |
| 1011 | |
| 1012 def check_new_patch(db, cl, id, newdata): | |
| 1013 if not newdata["files"]: | |
| 1014 raise Reject, "You can't submit a new patch without " \ | |
| 1015 "attaching a patch file." | |
| 1016 for fileid in newdata["files"]: | |
| 1017 if db.file.get(fileid, "type") != "text/plain": | |
| 1018 raise Reject, "Submitted patch files must be text/plain." | |
| 1019 | |
| 1020 # When the status is changed from "approved" to "applied", apply the patch. | |
| 1021 | |
| 1022 def apply_patch(db, cl, id, olddata): | |
| 1023 if cl.get(id, "status") == db.status.lookup("applied") and \ | |
| 1024 olddata["status"] == db.status.lookup("approved"): | |
| 1025 # ...apply the patch... | |
| 1026 | |
| 1027 def init(db): | |
| 1028 db.patch.audit("create", check_new_patch) | |
| 1029 db.patch.react("set", apply_patch)</small | |
| 1030 ></pre></blockquote> | |
| 1031 | |
| 1032 <p><hr> | |
| 1033 <h2>6. Command Interface</h2> | |
| 1034 | |
| 1035 <p>The command interface is a very simple and minimal interface, | |
| 1036 intended only for quick searches and checks from the shell prompt. | |
| 1037 (Anything more interesting can simply be written in Python using | |
| 1038 the Roundup database module.) | |
| 1039 | |
| 1040 <h3>6.1. Interface Specification</h3> | |
| 1041 | |
| 1042 <p>A single command, <tt>roundup</tt>, provides basic access to | |
| 1043 the hyperdatabase from the command line. | |
| 1044 | |
| 1045 <ul> | |
| 1046 <li><tt>roundup get </tt>[<tt>-list</tt>]<tt> </tt | |
| 1047 ><em>designator</em>[<tt>,</tt | |
| 1048 ><em>designator</em><tt>,</tt>...]<tt> </tt><em>propname</em> | |
| 1049 <li><tt>roundup set </tt><em>designator</em>[<tt>,</tt | |
| 1050 ><em>designator</em><tt>,</tt>...]<tt> </tt><em>propname</em | |
| 1051 ><tt>=</tt><em>value</em> ... | |
| 1052 <li><tt>roundup find </tt>[<tt>-list</tt>]<tt> </tt | |
| 1053 ><em>classname</em><tt> </tt><em>propname</em>=<em>value</em> ... | |
| 1054 </ul> | |
| 1055 | |
| 1056 <p>Property values are represented as strings in command arguments | |
| 1057 and in the printed results: | |
| 1058 | |
| 1059 <ul> | |
| 1060 <li>Strings are, well, strings. | |
| 1061 | |
| 1062 <li>Date values are printed in the full date format in the local | |
| 1063 time zone, and accepted in the full format or any of the partial | |
| 1064 formats explained above. | |
| 1065 | |
| 1066 <li>Link values are printed as node designators. When given as | |
| 1067 an argument, node designators and key strings are both accepted. | |
| 1068 | |
| 1069 <li>Multilink values are printed as lists of node designators | |
| 1070 joined by commas. When given as an argument, node designators | |
| 1071 and key strings are both accepted; an empty string, a single node, | |
| 1072 or a list of nodes joined by commas is accepted. | |
| 1073 </ul> | |
| 1074 | |
| 1075 <p>When multiple nodes are specified to the | |
| 1076 <tt>roundup get</tt> or <tt>roundup set</tt> | |
| 1077 commands, the specified properties are retrieved or set | |
| 1078 on all the listed nodes. | |
| 1079 | |
| 1080 <p>When multiple results are returned by the <tt>roundup get</tt> | |
| 1081 or <tt>roundup find</tt> commands, they are printed one per | |
| 1082 line (default) or joined by commas (with the <tt>-list</tt>) option. | |
| 1083 | |
| 1084 <h3>6.2. Usage Example</h3> | |
| 1085 | |
| 1086 <p>To find all messages regarding in-progress issues that | |
| 1087 contain the word "spam", for example, you could execute the | |
| 1088 following command from the directory where the database | |
| 1089 dumps its files: | |
| 1090 | |
| 1091 <blockquote><pre><small | |
| 1092 >shell% <span class="input">for issue in `roundup find issue status=in-progress`; do</span> | |
| 1093 > <span class="input">grep -l spam `roundup get $issue messages`</span> | |
| 1094 > <span class="input">done</span> | |
| 1095 <span class="output">msg23 | |
| 1096 msg49 | |
| 1097 msg50 | |
| 1098 msg61</span> | |
| 1099 shell%</small></pre></blockquote> | |
| 1100 | |
| 1101 <p>Or, using the <tt>-list</tt> option, this can be written as a single command: | |
| 1102 | |
| 1103 <blockquote><pre><small | |
| 1104 >shell% <span class="input">grep -l spam `roundup get \ | |
| 1105 \`roundup find -list issue status=in-progress\` messages`</span> | |
| 1106 <span class="output">msg23 | |
| 1107 msg49 | |
| 1108 msg50 | |
| 1109 msg61</span> | |
| 1110 shell%</small></pre></blockquote> | |
| 1111 | |
| 1112 <p><hr> | |
| 1113 <h2>7. E-mail User Interface</h2> | |
| 1114 | |
| 1115 <p>The Roundup system must be assigned an e-mail address | |
| 1116 at which to receive mail. Messages should be piped to | |
| 1117 the Roundup mail-handling script by the mail delivery | |
| 1118 system (e.g. using an alias beginning with "|" for sendmail). | |
| 1119 | |
| 1120 <h3>7.1. Message Processing</h3> | |
| 1121 | |
| 1122 <p>Incoming messages are examined for multiple parts. | |
| 1123 In a <tt>multipart/mixed</tt> message or part, each subpart is | |
| 1124 extracted and examined. In a <tt>multipart/alternative</tt> | |
| 1125 message or part, we look for a <tt>text/plain</tt> subpart and | |
| 1126 ignore the other parts. The <tt>text/plain</tt> subparts are | |
| 1127 assembled to form the textual body of the message, to | |
| 1128 be stored in the file associated with a "msg" class node. | |
| 1129 Any parts of other types are each stored in separate | |
| 1130 files and given "file" class nodes that are linked to | |
| 1131 the "msg" node. | |
| 1132 | |
| 1133 <p>The "summary" property on message nodes is taken from | |
| 1134 the first non-quoting section in the message body. | |
| 1135 The message body is divided into sections by blank lines. | |
| 1136 Sections where the second and all subsequent lines begin | |
| 1137 with a ">" or "|" character are considered "quoting | |
| 1138 sections". The first line of the first non-quoting | |
| 1139 section becomes the summary of the message. | |
| 1140 | |
| 1141 <p>All of the addresses in the To: and Cc: headers of the | |
| 1142 incoming message are looked up among the user nodes, and | |
| 1143 the corresponding users are placed in the "recipients" | |
| 1144 property on the new "msg" node. The address in the From: | |
| 1145 header similarly determines the "author" property of the | |
| 1146 new "msg" node. | |
| 1147 The default handling for | |
| 1148 addresses that don't have corresponding users is to create | |
| 1149 new users with no passwords and a username equal to the | |
| 1150 address. (The web interface does not permit logins for | |
| 1151 users with no passwords.) If we prefer to reject mail from | |
| 1152 outside sources, we can simply register an auditor on the | |
| 1153 "user" class that prevents the creation of user nodes with | |
| 1154 no passwords. | |
| 1155 | |
| 1156 <p>The subject line of the incoming message is examined to | |
| 1157 determine whether the message is an attempt to create a new | |
| 1158 item or to discuss an existing item. A designator enclosed | |
| 1159 in square brackets is sought as the first thing on the | |
| 1160 subject line (after skipping any "Fwd:" or "Re:" prefixes). | |
| 1161 | |
| 1162 <p>If an item designator (class name and id number) is found | |
| 1163 there, the newly created "msg" node is added to the "messages" | |
| 1164 property for that item, and any new "file" nodes are added to | |
| 1165 the "files" property for the item. | |
| 1166 | |
| 1167 <p>If just an item class name is found there, we attempt to | |
| 1168 create a new item of that class with its "messages" property | |
| 1169 initialized to contain the new "msg" node and its "files" | |
| 1170 property initialized to contain any new "file" nodes. | |
| 1171 | |
| 1172 <p>Both cases may trigger detectors (in the first case we | |
| 1173 are calling the <tt>set()</tt> method to add the message to the | |
| 1174 item's spool; in the second case we are calling the | |
| 1175 <tt>create()</tt> method to create a new node). If an auditor | |
| 1176 raises an exception, the original message is bounced back to | |
| 1177 the sender with the explanatory message given in the exception. | |
| 1178 | |
| 1179 <h3>7.2. Nosy Lists</h3> | |
| 1180 | |
| 1181 <p>A standard detector is provided that watches for additions | |
| 1182 to the "messages" property. When a new message is added, the | |
| 1183 detector sends it to all the users on the "nosy" list for the | |
| 1184 item that are not already on the "recipients" list of the | |
| 1185 message. Those users are then appended to the "recipients" | |
| 1186 property on the message, so multiple copies of a message | |
| 1187 are never sent to the same user. The journal recorded by | |
| 1188 the hyperdatabase on the "recipients" property then provides | |
| 1189 a log of when the message was sent to whom. | |
| 1190 | |
| 1191 <h3>7.3. Setting Properties</h3> | |
| 1192 | |
| 1193 <p>The e-mail interface also provides a simple way to set | |
| 1194 properties on items. At the end of the subject line, | |
| 1195 <em>propname</em><tt>=</tt><em>value</em> pairs can be | |
| 1196 specified in square brackets, using the same conventions | |
| 1197 as for the <tt>roundup set</tt> shell command. | |
| 1198 | |
| 1199 <p><hr> | |
| 1200 <h2>8. Web User Interface</h2> | |
| 1201 | |
| 1202 <p>The web interface is provided by a CGI script that can be | |
| 1203 run under any web server. A simple web server can easily be | |
| 1204 built on the standard <tt>CGIHTTPServer</tt> module, and | |
| 1205 should also be included in the distribution for quick | |
| 1206 out-of-the-box deployment. | |
| 1207 | |
| 1208 <p>The user interface is constructed from a number of template | |
| 1209 files containing mostly HTML. Among the HTML tags in templates | |
| 1210 are interspersed some nonstandard tags, which we use as | |
| 1211 placeholders to be replaced by properties and their values. | |
| 1212 | |
| 1213 <h3>8.1. Views and View Specifiers</h3> | |
| 1214 | |
| 1215 <p>There are two main kinds of views: index views and item views. | |
| 1216 An index view displays a list of items of a particular class, | |
| 1217 optionally sorted and filtered as requested. An item view | |
| 1218 presents the properties of a particular item for editing | |
| 1219 and displays the message spool for the item. | |
| 1220 | |
| 1221 <p>A <em>view specifier</em> is a string that specifies | |
| 1222 all the options needed to construct a particular view. | |
| 1223 It goes after the URL to the Roundup CGI script or the | |
| 1224 web server to form the complete URL to a view. When the | |
| 1225 result of selecting a link or submitting a form takes | |
| 1226 the user to a new view, the Web browser should be redirected | |
| 1227 to a canonical location containing a complete view specifier | |
| 1228 so that the view can be bookmarked. | |
| 1229 | |
| 1230 <h3>8.2. Displaying Properties</h3> | |
| 1231 | |
| 1232 <p>Properties appear in the user interface in three contexts: | |
| 1233 in indices, in editors, and as filters. For each type of | |
| 1234 property, there are several display possibilities. For example, | |
| 1235 in an index view, a string property may just be printed as | |
| 1236 a plain string, but in an editor view, that property should | |
| 1237 be displayed in an editable field. | |
| 1238 | |
| 1239 <p>The display of a property is handled by functions in | |
| 1240 a <tt>displayers</tt> module. Each function accepts at | |
| 1241 least three standard arguments -- the database, class name, | |
| 1242 and node id -- and returns a chunk of HTML. | |
| 1243 | |
| 1244 <p>Displayer functions are triggered by <tt><display></tt> | |
| 1245 tags in templates. The <tt>call</tt> attribute of the tag | |
| 1246 provides a Python expression for calling the displayer | |
| 1247 function. The three standard arguments are inserted in | |
| 1248 front of the arguments given. For example, the occurrence of | |
| 1249 | |
| 1250 <blockquote><pre><small | |
| 1251 > <display call="plain('status', max=30)"> | |
| 1252 </small></pre></blockquote> | |
| 1253 | |
| 1254 in a template triggers a call to | |
| 1255 | |
| 1256 <blockquote><pre><small | |
| 1257 > plain(db, "issue", 13, "status", max=30) | |
| 1258 </small></pre></blockquote> | |
| 1259 | |
| 1260 when displaying item 13 in the "issue" class. The displayer | |
| 1261 functions can accept extra arguments to further specify | |
| 1262 details about the widgets that should be generated. By defining new | |
| 1263 displayer functions, the user interface can be highly customized. | |
| 1264 | |
| 1265 <p>Some of the standard displayer functions include: | |
| 1266 | |
| 1267 <ul> | |
| 1268 <li><strong>plain</strong>: display a String property directly; | |
| 1269 display a Date property in a specified time zone with an option | |
| 1270 to omit the time from the date stamp; for a Link or Multilink | |
| 1271 property, display the key strings of the linked nodes (or the | |
| 1272 ids if the linked class has no key property) | |
| 1273 | |
| 1274 <li><strong>field</strong>: display a property like the | |
| 1275 <strong>plain</strong> displayer above, but in a text field | |
| 1276 to be edited | |
| 1277 | |
| 1278 <li><strong>menu</strong>: for a Link property, display | |
| 1279 a menu of the available choices | |
| 1280 | |
| 1281 <li><strong>link</strong>: for a Link or Multilink property, | |
| 1282 display the names of the linked nodes, hyperlinked to the | |
| 1283 item views on those nodes | |
| 1284 | |
| 1285 <li><strong>count</strong>: for a Multilink property, display | |
| 1286 a count of the number of links in the list | |
| 1287 | |
| 1288 <li><strong>reldate</strong>: display a Date property in terms | |
| 1289 of an interval relative to the current date (e.g. "+ 3w", "- 2d"). | |
| 1290 | |
| 1291 <li><strong>download</strong>: show a Link("file") or Multilink("file") | |
| 1292 property using links that allow you to download files | |
| 1293 | |
| 1294 <li><strong>checklist</strong>: for a Link or Multilink property, | |
| 1295 display checkboxes for the available choices to permit filtering | |
| 1296 </ul> | |
| 1297 | |
| 1298 <h3>8.3. Index Views</h3> | |
| 1299 | |
| 1300 <p>An index view contains two sections: a filter section | |
| 1301 and an index section. | |
| 1302 The filter section provides some widgets for selecting | |
| 1303 which items appear in the index. The index section is | |
| 1304 a table of items. | |
| 1305 | |
| 1306 <h4>8.3.1. Index View Specifiers</h4> | |
| 1307 | |
| 1308 <p>An index view specifier looks like this (whitespace | |
| 1309 has been added for clarity): | |
| 1310 | |
| 1311 <blockquote><pre><small | |
| 1312 >/issue?status=unread,in-progress,resolved& | |
| 1313 topic=security,ui& | |
| 1314 :group=+priority& | |
| 1315 :sort=-activity& | |
| 1316 :filters=status,topic& | |
| 1317 :columns=title,status,fixer | |
| 1318 </small></pre></blockquote> | |
| 1319 | |
| 1320 <p>The index view is determined by two parts of the | |
| 1321 specifier: the layout part and the filter part. | |
| 1322 The layout part consists of the query parameters that | |
| 1323 begin with colons, and it determines the way that the | |
| 1324 properties of selected nodes are displayed. | |
| 1325 The filter part consists of all the other query parameters, | |
| 1326 and it determines the criteria by which nodes | |
| 1327 are selected for display. | |
| 1328 | |
| 1329 <p>The filter part is interactively manipulated with | |
| 1330 the form widgets displayed in the filter section. The | |
| 1331 layout part is interactively manipulated by clicking | |
| 1332 on the column headings in the table. | |
| 1333 | |
| 1334 <p>The filter part selects the <em>union</em> of the | |
| 1335 sets of items with values matching any specified Link | |
| 1336 properties and the <em>intersection</em> of the sets | |
| 1337 of items with values matching any specified Multilink | |
| 1338 properties. | |
| 1339 | |
| 1340 <p>The example specifies an index of "issue" nodes. | |
| 1341 Only items with a "status" of <em>either</em> | |
| 1342 "unread" or "in-progres" or "resolved" are displayed, | |
| 1343 and only items with "topic" values including <em>both</em> | |
| 1344 "security" <em>and</em> "ui" are displayed. The items | |
| 1345 are grouped by priority, arranged in ascending order; | |
| 1346 and within groups, sorted by activity, arranged in | |
| 1347 descending order. The filter section shows filters | |
| 1348 for the "status" and "topic" properties, and the | |
| 1349 table includes columns for the "title", "status", and | |
| 1350 "fixer" properties. | |
| 1351 | |
| 1352 <p>Associated with each item class is a default | |
| 1353 layout specifier. The layout specifier in the above | |
| 1354 example is the default layout to be provided with | |
| 1355 the default bug-tracker schema described above in | |
| 1356 section 4.4. | |
| 1357 | |
| 1358 <h4>8.3.2. Filter Section</h4> | |
| 1359 | |
| 1360 <p>The template for a filter section provides the | |
| 1361 filtering widgets at the top of the index view. | |
| 1362 Fragments enclosed in <tt><property></tt>...<tt></property></tt> | |
| 1363 tags are included or omitted depending on whether the | |
| 1364 view specifier requests a filter for a particular property. | |
| 1365 | |
| 1366 <p>Here's a simple example of a filter template. | |
| 1367 | |
| 1368 <blockquote><pre><small | |
| 1369 ><property name=status> | |
| 1370 <display call="checklist('status')"> | |
| 1371 </property> | |
| 1372 <br> | |
| 1373 <property name=priority> | |
| 1374 <display call="checklist('priority')"> | |
| 1375 </property> | |
| 1376 <br> | |
| 1377 <property name=fixer> | |
| 1378 <display call="menu('fixer')"> | |
| 1379 </property></small></pre></blockquote> | |
| 1380 | |
| 1381 <h4>8.3.3. Index Section</h4> | |
| 1382 | |
| 1383 <p>The template for an index section describes one row of | |
| 1384 the index table. | |
| 1385 Fragments enclosed in <tt><property></tt>...<tt></property></tt> | |
| 1386 tags are included or omitted depending on whether the | |
| 1387 view specifier requests a column for a particular property. | |
| 1388 The table cells should contain <tt><display></tt> tags | |
| 1389 to display the values of the item's properties. | |
| 1390 | |
| 1391 <p>Here's a simple example of an index template. | |
| 1392 | |
| 1393 <blockquote><pre><small | |
| 1394 ><tr> | |
| 1395 <property name=title> | |
| 1396 <td><display call="plain('title', max=50)"></td> | |
| 1397 </property> | |
| 1398 <property name=status> | |
| 1399 <td><display call="plain('status')"></td> | |
| 1400 </property> | |
| 1401 <property name=fixer> | |
| 1402 <td><display call="plain('fixer')"></td> | |
| 1403 </property> | |
| 1404 </tr></small></pre></blockquote> | |
| 1405 | |
| 1406 <h4>8.3.4. Sorting</h4> | |
| 1407 | |
| 1408 <p>String and Date values are sorted in the natural way. | |
| 1409 Link properties are sorted according to the value of the | |
| 1410 "order" property on the linked nodes if it is present; or | |
| 1411 otherwise on the key string of the linked nodes; or | |
| 1412 finally on the node ids. Multilink properties are | |
| 1413 sorted according to how many links are present. | |
| 1414 | |
| 1415 <h3>8.4. Item Views</h3> | |
| 1416 | |
| 1417 <p>An item view contains an editor section and a spool section. | |
| 1418 At the top of an item view, links to superseding and superseded | |
| 1419 items are always displayed. | |
| 1420 | |
| 1421 <h4>8.4.1. Item View Specifiers</h4> | |
| 1422 | |
| 1423 <p>An item view specifier is simply the item's designator: | |
| 1424 | |
| 1425 <blockquote><pre><small | |
| 1426 >/patch23 | |
| 1427 </small></pre></blockquote> | |
| 1428 | |
| 1429 <h4>8.4.2. Editor Section</h4> | |
| 1430 | |
| 1431 <p>The editor section is generated from a template | |
| 1432 containing <tt><display></tt> tags to insert | |
| 1433 the appropriate widgets for editing properties. | |
| 1434 | |
| 1435 <p>Here's an example of a basic editor template. | |
| 1436 | |
| 1437 <blockquote><pre><small | |
| 1438 ><table> | |
| 1439 <tr> | |
| 1440 <td colspan=2> | |
| 1441 <display call="field('title', size=60)"> | |
| 1442 </td> | |
| 1443 </tr> | |
| 1444 <tr> | |
| 1445 <td> | |
| 1446 <display call="field('fixer', size=30)"> | |
| 1447 </td> | |
| 1448 <td> | |
| 1449 <display call="menu('status')> | |
| 1450 </td> | |
| 1451 </tr> | |
| 1452 <tr> | |
| 1453 <td> | |
| 1454 <display call="field('nosy', size=30)"> | |
| 1455 </td> | |
| 1456 <td> | |
| 1457 <display call="menu('priority')> | |
| 1458 </td> | |
| 1459 </tr> | |
| 1460 <tr> | |
| 1461 <td colspan=2> | |
| 1462 <display call="note()"> | |
| 1463 </td> | |
| 1464 </tr> | |
| 1465 </table> | |
| 1466 </small></pre></blockquote> | |
| 1467 | |
| 1468 <p>As shown in the example, the editor template can also | |
| 1469 request the display of a "note" field, which is a | |
| 1470 text area for entering a note to go along with a change. | |
| 1471 | |
| 1472 <p>When a change is submitted, the system automatically | |
| 1473 generates a message describing the changed properties. | |
| 1474 The message displays all of the property values on the | |
| 1475 item and indicates which ones have changed. | |
| 1476 An example of such a message might be this: | |
| 1477 | |
| 1478 <blockquote><pre><small | |
| 1479 >title: Polly Parrot is dead | |
| 1480 priority: critical | |
| 1481 status: unread -> in-progress | |
| 1482 fixer: (none) | |
| 1483 keywords: parrot,plumage,perch,nailed,dead | |
| 1484 </small></pre></blockquote> | |
| 1485 | |
| 1486 <p>If a note is given in the "note" field, the note is | |
| 1487 appended to the description. The message is then added | |
| 1488 to the item's message spool (thus triggering the standard | |
| 1489 detector to react by sending out this message to the nosy list). | |
| 1490 | |
| 1491 <h4>8.4.3. Spool Section</h4> | |
| 1492 | |
| 1493 <p>The spool section lists messages in the item's "messages" | |
| 1494 property. The index of messages displays the "date", "author", | |
| 1495 and "summary" properties on the message nodes, and selecting a | |
| 1496 message takes you to its content. | |
| 1497 | |
| 1498 <p><hr> | |
| 1499 <h2>9. Deployment Scenarios</h2> | |
| 1500 | |
| 1501 <p>The design described above should be general enough | |
| 1502 to permit the use of Roundup for bug tracking, managing | |
| 1503 projects, managing patches, or holding discussions. By | |
| 1504 using nodes of multiple types, one could deploy a system | |
| 1505 that maintains requirement specifications, catalogs bugs, | |
| 1506 and manages submitted patches, where patches could be | |
| 1507 linked to the bugs and requirements they address. | |
| 1508 | |
| 1509 <p><hr> | |
| 1510 <h2>10. Acknowledgements</h2> | |
| 1511 | |
| 1512 <p>My thanks are due to Christy Heyl for | |
| 1513 reviewing and contributing suggestions to this paper | |
| 1514 and motivating me to get it done, and to | |
| 1515 Jesse Vincent, Mark Miller, Christopher Simons, | |
| 1516 Jeff Dunmall, Wayne Gramlich, and Dean Tribble for | |
| 1517 their assistance with the first-round submission. | |
| 1518 </td> | |
| 1519 </tr> | |
| 1520 </table> | |
| 1521 | |
| 1522 <p> | |
| 1523 | |
| 1524 <center> | |
| 1525 <table> | |
| 1526 <tr> | |
| 1527 <td> <a href="http://www.software-carpentry.com/index.html"><b>[Home]</b></a> </td> | |
| 1528 <td> <a href="http://www.software-carpentry.com/faq.html"><b>[FAQ]</b></a> </td> | |
| 1529 <td> <a href="http://www.software-carpentry.com/license.html"><b>[License]</b></a> </td> | |
| 1530 <td> <a href="http://www.software-carpentry.com/contest-rules.html"><b>[Rules]</b></a> </td> | |
| 1531 <td> <a href="http://www.software-carpentry.com/sc_config/"><b>[Configure]</b></a> </td> | |
| 1532 <td> <a href="http://www.software-carpentry.com/sc_build/"><b>[Build]</b></a> </td> | |
| 1533 <td> <a href="http://www.software-carpentry.com/sc_test/"><b>[Test]</b></a> </td> | |
| 1534 <td> <a href="http://www.software-carpentry.com/sc_track/"><b>[Track]</b></a> </td> | |
| 1535 <td> <a href="http://www.software-carpentry.com/biblio.html"><b>[Resources]</b></a> </td> | |
| 1536 <td> <a href="http://www.software-carpentry.com/lists/"><b>[Archives]</b></a> </td> | |
| 1537 </tr> | |
| 1538 </table> | |
| 1539 </center> | |
| 1540 | |
| 1541 <p><hr> | |
| 1542 <center>Last modified 2001/04/06 11:50:59.9063 US/Mountain</center> | |
| 1543 </BODY> | |
| 1544 </HTML> |
