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