From 01f46c221220f315a25ff77009e909528139dd8d Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 25 Jul 2001 01:23:07 +0000 Subject: [PATCH] Added the Roundup spec to the new documentation directory. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@76 57a73879-2fb5-44c3-a270-3262357dd7e2 --- doc/images/logo-acl-medium.gif | Bin 0 -> 6048 bytes doc/images/logo-codesourcery-medium.gif | Bin 0 -> 3349 bytes .../logo-software-carpentry-standard.gif | Bin 0 -> 16521 bytes doc/spec.html | 1544 +++++++++++++++++ roundup/backends/back_anydbm.py | 13 +- roundup/templates/extended/dbinit.py | 10 +- tests/test_db.py | 9 +- tests/test_schema.py | 2 + 8 files changed, 1567 insertions(+), 11 deletions(-) create mode 100644 doc/images/logo-acl-medium.gif create mode 100644 doc/images/logo-codesourcery-medium.gif create mode 100644 doc/images/logo-software-carpentry-standard.gif create mode 100644 doc/spec.html diff --git a/doc/images/logo-acl-medium.gif b/doc/images/logo-acl-medium.gif new file mode 100644 index 0000000000000000000000000000000000000000..37c519afe8b8153811a22baa62f6eef2130c20c3 GIT binary patch literal 6048 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 literal 0 HcmV?d00001 diff --git a/doc/images/logo-codesourcery-medium.gif b/doc/images/logo-codesourcery-medium.gif new file mode 100644 index 0000000000000000000000000000000000000000..075cc80f107342f4b4ae49d64a3a91ca8f916ede GIT binary patch literal 3349 zcmeIx`9DG&EcthQR&Tglm5 z+1o=CR8%Syh$q!Ez*8$W4J)h67hqScRw{*!1($&?{)!il6-PPUQB@}iEMO`9kH$*7x z9+7m5#N8sOr9y~EEEGwE&@7e+#nKU>a70ctB7!0w5lKeGP^2RgC^8YWAtx4zB_gp@ zB$0?DP|L&+i9{@silJF56-%LB6U)}*&*Y>MDHNGR24zhuCzHyctjVBBZcVlZk=OdS z_<#M+C$RQ?sk-_iolcjZ;2N-gkWk4dh~YQn`Wv^&TmDY~m{mYeaie=%bq@=pZXPz! zR@2J?jXnA9?X`av;B6A72HNZT&k~%@Z1m{3I9O^H*c>+4Q9oQpj2_~9bT&Myv`c?G zHQ3oW`Ujb%r0;pH>2aOA&F#~*b(elKI#uGi=I0u9tqsoQ9cO98TEvRaFb>!km<=JylB^@1}}Q}K4`e8H>*x4W9rX@;R| zM8As&UTF9e{d)|Pi&B297!2qP0Rbi{{?&Hj1gCjwFwMpNNtG}I&pTHfz8D@c&sN^D2 z=qRw_&Pw+~akAd0jpl0HB|jtlG73P~xr`eC3U)l_aLj`n;L!u#sT^(Qwpw(?)FwYx_@9~8b!3P+~Ff%}d}a~XCa z*CGnkSAt#_DqzzpRPB;`6m-Lv^Ix8g&t$+5`<<65;U4CLQBHue6VcrEb*0>W}Tm;4LP>LgS7su3pYTtXoD@X3rnzSnpG+3pZC~`58F~zQXDe z`osogBaBc*_Fr15+`mwvBR*Bc1AO1o{#Y^6OERo3Eu*p_Ss=t?~IaK&$rpW+Hc9*aro{8Sh~ z^kX;C1`J*|Fnn!GnAwiJEpN$eu%d}PmlZlT;G?u$O@nWJtbO`xRaW|jBJe>gBNDyX zL5rLpjxg4MJH`ehCSX?7Z+pOeEk~d4k%WM@4bCoKtUb7W2PRT!y%d?|1aoGvjGDFk zEWer@voGR0P32u-`l_+p^~%pFU+z7vLY)F}edOfcm-%AA%wKx=4U9;81?;$dPU-lw zH-^Wx;qR6gw z23!Dvcc%AqhO{AQ5)V?HuFnigJ2a?*I<>-cx&bDd+yTz9CNIKI`z+z?~h_q1{d zvxz>&Ro8nMQx9R;nNge_WqG8OVYrEVkAmqE+yptJnx4-xlK{C`EfWrU@>N_yhO3Un zKqS~97&}#N%T!^fUH*Cam*kLrz*4&<**L%RldL&JHOc$1axCTb=PFmQIUAwFHy|w3 z{g^_l#isPYwFGqKuYl^)V(*NOC6t}==`#qA-zoSqANriXMMTzrjPpZUgB3e1>sBKV z>*jnL8Cv{YcHbCt^TST?#JP8fBD-0<;)A}Sp5miF2Ns~vC0OLd2hwwg znA2y$dv$XT6Egs1L2{y!RTypX*?IM14;mm#_Np2AUWh%GYO(R4+Q;+UUDof}sa~3n z9=gP19TTLOo%x}@m$a>k zzQQ$LE_O#Ws1l!|E#!B z%9RJm(N4?w;9n>P&HM5eir}2-cV}9{Dhc+9i1b|p#VXWM^f#Yk1Y&r@tK$Op-Hyl7 zzIVjz&VJ<$&hhoxamG89=k7f`V>qY7blblnp+=uW?Ajt-K5JyMo3dnOm1>WH60%U^r zgBA>y?YGGt8NQKAphf};pOb2ysB*x)%0-9S&&fu@z7G@Ml72PZ7454BYmFGaGfX+6 zPTW*drR~-E;-6w~{19DJi`QVOE}$rz5`U`Le3D>(<#6PgN=gA8B;a)01bS!0)_3ee_K<{)te4&%( zOqz#p#x(ADSI?&6`{!~&c$n`^zspzo=xuEU&s7y4 zjH&LZKpmJK*RP@@W62oJ*3$$}yNZv?sx`NYK1k7*da<`OS88wXe#Z{IF!Re)8k-ns zln+pXtGEhgC+;{M>yLJ6u4S3NZJ-7B}eWAIlzGF{MlmVnwGnM%n(U` z;SiziDMQL=W;xwEv$fyy-Y0eHi_=qN3paej55c4LTSGTf#bk?f~ntB`4Gg;1ya(Qf7)d6t*_H7^CvNweb|4dG;Dax%*~CvI|1 UKI5GF%Aw=)nRfZwSPI6G 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 r@A;u4de!||4N$^ + +Software Carpentry Track: Roundup + + + + + + + + + + + +
+[Software Carpentry logo] + + + + + +
+[ACL Logo] +

+[CodeSourcery Logo] +
+
+ +

+ +

Roundup

+

An Issue-Tracking System for Knowledge Workers

+

Ka-Ping Yee
+ping@lfw.org

+

Implementation Guide

+ +

Contents

+ +
    +
  1. Introduction +
  2. The Layer Cake +
  3. Hyperdatabase +
      +
    1. Dates and Date Arithmetic +
    2. Nodes and Classes +
    3. Identifiers and Designators +
    4. Property Names and Types +
    5. Interface Specification +
    6. Application Example +
    +
  4. Roundup Database +
      +
    1. Reserved Classes +
        +
      1. Users +
      2. Messages +
      3. Files +
      +
    2. Item Classes +
    3. Interface Specification +
    4. Default Schema +
    +
  5. Detector Interface +
      +
    1. Interface Specification +
    2. Detector Example +
    +
  6. Command Interface +
      +
    1. Interface Specification +
    2. Usage Example +
    +
  7. E-mail User Interface +
      +
    1. Message Processing +
    2. Nosy Lists +
    3. Setting Properties +
    4. Workflow Example +
    +
  8. Web User Interface +
      +
    1. Views and View Specifiers +
    2. Displaying Properties +
    3. Index Views +
        +
      1. Index View Specifiers +
      2. Filter Section +
      3. Index Section +
      4. Sorting +
      +
    4. Item Views +
        +
      1. Item View Specifiers +
      2. Editor Section +
      3. Spool Section +
      +
    +
  9. Deployment Scenarios +
  10. Acknowledgements +
+ +


+

1. Introduction

+ +

This document presents a description of the components +of the Roundup system and specifies their interfaces and +behaviour in sufficient detail to guide an implementation. +For the philosophy and rationale behind the Roundup design, +see the first-round Software Carpentry submission for Roundup. +This document fleshes out that design as well as specifying +interfaces so that the components can be developed separately. + +


+

2. The Layer Cake

+ +

Lots of software design documents come with a picture of +a cake. Everybody seems to like them. I also like cakes +(i think they are tasty). So i, too, shall include +a picture of a cake here. + +

+ + + + + + + + + + + + + + + + +
+

+E-mail Client + +

+

+Web Browser + +

+

+Detector Scripts + +

+

+Shell + +

+

+E-mail User Interface + +

+

+Web User Interface + +

+

+Detector Interface + +

+

+Command Interface + +

+

+Roundup Database Layer + +

+

+Hyperdatabase Layer + +

+

+Storage Layer + +

+ +

The colourful parts of the cake are part of our system; +the faint grey parts of the cake are external components. + +

I will now proceed to forgo all table manners and +eat from the bottom of the cake to the top. You may want +to stand back a bit so you don't get covered in crumbs. + +


+

3. Hyperdatabase

+ +

The lowest-level component to be implemented is the hyperdatabase. +The hyperdatabase is intended to be +a flexible data store that can hold configurable data in +records which we call nodes. + +

The hyperdatabase is implemented on top of the storage layer, +an external module for storing its data. The storage layer could +be a third-party RDBMS; for a "batteries-included" distribution, +implementing the hyperdatabase on the standard bsddb +module is suggested. + +

3.1. Dates and Date Arithmetic

+ +

Before we get into the hyperdatabase itself, we need a +way of handling dates. The hyperdatabase module provides +Timestamp objects for +representing date-and-time stamps and Interval objects for +representing date-and-time intervals. + +

As strings, date-and-time stamps are specified with +the date in international standard format +(yyyy-mm-dd) +joined to the time (hh:mm:ss) +by a period ("."). Dates in +this form can be easily compared and are fairly readable +when printed. An example of a valid stamp is +"2000-06-24.13:03:59". +We'll call this the "full date format". When Timestamp objects are +printed as strings, they appear in the full date format with +the time always given in GMT. The full date format is always +exactly 19 characters long. + +

For user input, some partial forms are also permitted: +the whole time or just the seconds may be omitted; and the whole date +may be omitted or just the year may be omitted. If the time is given, +the time is interpreted in the user's local time zone. +The Date constructor takes care of these conversions. +In the following examples, suppose that yyyy is the current year, +mm is the current month, and dd is the current +day of the month; and suppose that the user is on Eastern Standard Time. + +

    +
  • "2000-04-17" means <Date 2000-04-17.00:00:00> +
  • "01-25" means <Date yyyy-01-25.00:00:00> +
  • "2000-04-17.03:45" means <Date 2000-04-17.08:45:00> +
  • "08-13.22:13" means <Date yyyy-08-14.03:13:00> +
  • "11-07.09:32:43" means <Date yyyy-11-07.14:32:43> +
  • "14:25" means +<Date yyyy-mm-dd.19:25:00> +
  • "8:47:11" means +<Date yyyy-mm-dd.13:47:11> +
  • the special date "." means "right now" +
+ +

Date intervals are specified using the suffixes +"y", "m", and "d". The suffix "w" (for "week") means 7 days. +Time intervals are specified in hh:mm:ss format (the seconds +may be omitted, but the hours and minutes may not). + +

    +
  • "3y" means three years +
  • "2y 1m" means two years and one month +
  • "1m 25d" means one month and 25 days +
  • "2w 3d" means two weeks and three days +
  • "1d 2:50" means one day, two hours, and 50 minutes +
  • "14:00" means 14 hours +
  • "0:04:33" means four minutes and 33 seconds +
+ +

The Date class should understand simple date expressions of the form +stamp + interval and stamp - interval. +When adding or subtracting intervals involving months or years, the +components are handled separately. For example, when evaluating +"2000-06-25 + 1m 10d", we first add one month to +get 2000-07-25, then add 10 days to get +2000-08-04 (rather than trying to decide whether +1m 10d means 38 or 40 or 41 days). + +

Here is an outline of the Date and Interval classes. + +

+
class Date:
+    def __init__(self, spec, offset):
+        """Construct a date given a specification and a time zone offset.
+
+        'spec' is a full date or a partial form, with an optional
+        added or subtracted interval.  'offset' is the local time
+        zone offset from GMT in hours.
+        """
+
+    def __add__(self, interval):
+        """Add an interval to this date to produce another date."""
+
+    def __sub__(self, interval):
+        """Subtract an interval from this date to produce another date."""
+
+    def __cmp__(self, other):
+        """Compare this date to another date."""
+
+    def __str__(self):
+        """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
+
+    def local(self, offset):
+        """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone."""
+
+class Interval:
+    def __init__(self, spec):
+        """Construct an interval given a specification."""
+
+    def __cmp__(self, other):
+        """Compare this interval to another interval."""
+        
+    def __str__(self):
+        """Return this interval as a string."""
+
+
+ +

Here are some examples of how these classes would behave in practice. +For the following examples, assume that we are on Eastern Standard +Time and the current local time is 19:34:02 on 25 June 2000. + +

>>> Date(".")
+<Date 2000-06-26.00:34:02>
+>>> _.local(-5)
+"2000-06-25.19:34:02"
+>>> Date(". + 2d")
+<Date 2000-06-28.00:34:02>
+>>> Date("1997-04-17", -5)
+<Date 1997-04-17.00:00:00>
+>>> Date("01-25", -5)
+<Date 2000-01-25.00:00:00>
+>>> Date("08-13.22:13", -5)
+<Date 2000-08-14.03:13:00>
+>>> Date("14:25", -5)
+<Date 2000-06-25.19:25:00>
+>>> Interval("  3w  1  d  2:00")
+<Interval 22d 2:00>
+>>> Date(". + 2d") - Interval("3w")
+<Date 2000-06-07.00:34:02>
+ +

3.2. Nodes and Classes

+ +

Nodes contain data in properties. To Python, these +properties are presented as the key-value pairs of a dictionary. +Each node belongs to a class which defines the names +and types of its properties. The database permits the creation +and modification of classes as well as nodes. + +

3.3. Identifiers and Designators

+ +

Each node has a numeric identifier which is unique among +nodes in its class. The nodes are numbered sequentially +within each class in order of creation, starting from 1. +The designator +for a node is a way to identify a node in the database, and +consists of the name of the node's class concatenated with +the node's numeric identifier. + +

For example, if "spam" and "eggs" are classes, the first +node created in class "spam" has id 1 and designator "spam1". +The first node created in class "eggs" also has id 1 but has +the distinct designator "eggs1". Node designators are +conventionally enclosed in square brackets when mentioned +in plain text. This permits a casual mention of, say, +"[patch37]" in an e-mail message to be turned into an active +hyperlink. + +

3.4. Property Names and Types

+ +

Property names must begin with a letter. + +

A property may be one of five basic types: + +

    +
  • String properties are for storing arbitrary-length +strings. + +
  • Date properties store date-and-time stamps. +Their values are Timestamp objects. + +
  • A Link property refers to a single other node +selected from a specified class. The class is part of the property; +the value is an integer, the id of the chosen node. + +
  • A Multilink property refers to possibly many nodes +in a specified class. The value is a list of integers. +
+ +

None is also a permitted value for any of these property +types. An attempt to store None into a String property +stores the empty string; an attempt to store None +into a Multilink property stores an empty list. + +

3.5. Interface Specification

+ +

The hyperdb module provides property objects to designate +the different kinds of properties. These objects are used when +specifying what properties belong in classes. + +

class String:
+    def __init__(self):
+        """An object designating a String property."""
+
+class Date:
+    def __init__(self):
+        """An object designating a Date property."""
+
+class Link:
+    def __init__(self, classname):
+        """An object designating a Link property that links to
+        nodes in a specified class."""
+
+class Multilink:
+    def __init__(self, classname):
+        """An object designating a Multilink property that links
+        to nodes in a specified class."""
+
+ +

Here is the interface provided by the hyperdatabase. + +

class Database:
+    """A database for storing records containing flexible data types."""
+
+    def __init__(self, storagelocator, journaltag):
+        """Open a hyperdatabase given a specifier to some storage.
+
+        The meaning of 'storagelocator' depends on the particular
+        implementation of the hyperdatabase.  It could be a file name,
+        a directory path, a socket descriptor for a connection to a
+        database over the network, etc.
+
+        The 'journaltag' is a token that will be attached to the journal
+        entries for any edits done on the database.  If 'journaltag' is
+        None, the database is opened in read-only mode: the Class.create(),
+        Class.set(), and Class.retire() methods are disabled.
+        """
+
+    def __getattr__(self, classname):
+        """A convenient way of calling self.getclass(classname)."""
+
+    def getclasses(self):
+        """Return a list of the names of all existing classes."""
+
+    def getclass(self, classname):
+        """Get the Class object representing a particular class.
+
+        If 'classname' is not a valid class name, a KeyError is raised.
+        """
+
+class Class:
+    """The handle to a particular class of nodes in a hyperdatabase."""
+
+    def __init__(self, db, classname, **properties):
+        """Create a new class with a given name and property specification.
+
+        'classname' must not collide with the name of an existing class,
+        or a ValueError is raised.  The keyword arguments in 'properties'
+        must map names to property objects, or a TypeError is raised.
+        """
+
+    # Editing nodes:
+
+    def create(self, **propvalues):
+        """Create a new node of this class and return its id.
+
+        The keyword arguments in 'propvalues' map property names to values.
+        The values of arguments must be acceptable for the types of their
+        corresponding properties or a TypeError is raised.  If this class
+        has a key property, it must be present and its value must not
+        collide with other key strings or a ValueError is raised.  Any other
+        properties on this class that are missing from the 'propvalues'
+        dictionary are set to None.  If an id in a link or multilink
+        property does not refer to a valid node, an IndexError is raised.
+        """
+
+    def get(self, nodeid, propname):
+        """Get the value of a property on an existing node of this class.
+
+        'nodeid' must be the id of an existing node of this class or an
+        IndexError is raised.  'propname' must be the name of a property
+        of this class or a KeyError is raised.
+        """
+
+    def set(self, nodeid, **propvalues):
+        """Modify a property on an existing node of this class.
+        
+        'nodeid' must be the id of an existing node of this class or an
+        IndexError is raised.  Each key in 'propvalues' must be the name
+        of a property of this class or a KeyError is raised.  All values
+        in 'propvalues' must be acceptable types for their corresponding
+        properties or a TypeError is raised.  If the value of the key
+        property is set, it must not collide with other key strings or a
+        ValueError is raised.  If the value of a Link or Multilink
+        property contains an invalid node id, a ValueError is raised.
+        """
+
+    def retire(self, nodeid):
+        """Retire a node.
+        
+        The properties on the node remain available from the get() method,
+        and the node's id is never reused.  Retired nodes are not returned
+        by the find(), list(), or lookup() methods, and other nodes may
+        reuse the values of their key properties.
+        """
+
+    def history(self, nodeid):
+        """Retrieve the journal of edits on a particular node.
+
+        'nodeid' must be the id of an existing node of this class or an
+        IndexError is raised.
+
+        The returned list contains tuples of the form
+
+            (date, tag, action, params)
+
+        'date' is a Timestamp object specifying the time of the change and
+        'tag' is the journaltag specified when the database was opened.
+        'action' may be:
+
+            'create' or 'set' -- 'params' is a dictionary of property values
+            'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
+            'retire' -- 'params' is None
+        """
+
+    # Locating nodes:
+
+    def setkey(self, propname):
+        """Select a String property of this class to be the key property.
+
+        'propname' must be the name of a String property of this class or
+        None, or a TypeError is raised.  The values of the key property on
+        all existing nodes must be unique or a ValueError is raised.
+        """
+
+    def getkey(self):
+        """Return the name of the key property for this class or None."""
+
+    def lookup(self, keyvalue):
+        """Locate a particular node by its key property and return its id.
+
+        If this class has no key property, a TypeError is raised.  If the
+        'keyvalue' matches one of the values for the key property among
+        the nodes in this class, the matching node's id is returned;
+        otherwise a KeyError is raised.
+        """
+
+    def find(self, propname, nodeid):
+        """Get the ids of nodes in this class which link to a given node.
+        
+        'propname' must be the name of a property in this class, or a
+        KeyError is raised.  That property must be a Link or Multilink
+        property, or a TypeError is raised.  'nodeid' must be the id of
+        an existing node in the class linked to by the given property,
+        or an IndexError is raised.
+        """
+
+    def list(self):
+        """Return a list of the ids of the active nodes in this class."""
+
+    def count(self):
+        """Get the number of nodes in this class.
+
+        If the returned integer is 'numnodes', the ids of all the nodes
+        in this class run from 1 to numnodes, and numnodes+1 will be the
+        id of the next node to be created in this class.
+        """
+
+    # Manipulating properties:
+
+    def getprops(self):
+        """Return a dictionary mapping property names to property objects."""
+
+    def addprop(self, **properties):
+        """Add properties to this class.
+
+        The keyword arguments in 'properties' must map names to property
+        objects, or a TypeError is raised.  None of the keys in 'properties'
+        may collide with the names of existing properties, or a ValueError
+        is raised before any properties have been added.
+        """
+ +

3.6. Application Example

+ +

Here is an example of how the hyperdatabase module would work in practice. + +

>>> import hyperdb
+>>> db = hyperdb.Database("foo.db", "ping")
+>>> db
+<hyperdb.Database "foo.db" opened by "ping">
+>>> hyperdb.Class(db, "status", name=hyperdb.String())
+<hyperdb.Class "status">
+>>> _.setkey("name")
+>>> db.status.create(name="unread")
+1
+>>> db.status.create(name="in-progress")
+2
+>>> db.status.create(name="testing")
+3
+>>> db.status.create(name="resolved")
+4
+>>> db.status.count()
+4
+>>> db.status.list()
+[1, 2, 3, 4]
+>>> db.status.lookup("in-progress")
+2
+>>> db.status.retire(3)
+>>> db.status.list()
+[1, 2, 4]
+>>> hyperdb.Class(db, "issue", title=hyperdb.String(), status=hyperdb.Link("status"))
+<hyperdb.Class "issue">
+>>> db.issue.create(title="spam", status=1)
+1
+>>> db.issue.create(title="eggs", status=2)
+2
+>>> db.issue.create(title="ham", status=4)
+3
+>>> db.issue.create(title="arguments", status=2)
+4
+>>> db.issue.create(title="abuse", status=1)
+5
+>>> hyperdb.Class(db, "user", username=hyperdb.Key(), password=hyperdb.String())
+<hyperdb.Class "user">
+>>> db.issue.addprop(fixer=hyperdb.Link("user"))
+>>> db.issue.getprops()
+{"title": <hyperdb.String>, "status": <hyperdb.Link to "status">,
+ "user": <hyperdb.Link to "user">}
+>>> db.issue.set(5, status=2)
+>>> db.issue.get(5, "status")
+2
+>>> db.status.get(2, "name")
+"in-progress"
+>>> db.issue.get(5, "title")
+"abuse"
+>>> db.issue.find("status", db.status.lookup("in-progress"))
+[2, 4, 5]
+>>> db.issue.history(5)
+[(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse", "status": 1}),
+ (<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})]
+>>> db.status.history(1)
+[(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")),
+ (<Date 2000-06-28.19:11:04>, "ping", "unlink", ("issue", 5, "status"))]
+>>> db.status.history(2)
+[(<Date 2000-06-28.19:11:04>, "ping", "link", ("issue", 5, "status"))]
+
+ +

For the purposes of journalling, when a Multilink property is +set to a new list of nodes, the hyperdatabase compares the old +list to the new list. +The journal records "unlink" events for all the nodes that appear +in the old list but not the new list, +and "link" events for +all the nodes that appear in the new list but not in the old list. + +


+

4. Roundup Database

+ +

The Roundup database layer is implemented on top of the +hyperdatabase and mediates calls to the database. +Some of the classes in the Roundup database are considered +item classes. +The Roundup database layer adds detectors and user nodes, +and on items it provides mail spools, nosy lists, and superseders. + +

4.1. Reserved Classes

+ +

Internal to this layer we reserve three special classes +of nodes that are not items. + +

4.1.1. Users

+ +

Users are stored in the hyperdatabase as nodes of +class "user". The "user" class has the definition: + +

hyperdb.Class(db, "user", username=hyperdb.String(),
+                          password=hyperdb.String(),
+                          address=hyperdb.String())
+db.user.setkey("username")
+ +

4.1.2. Messages

+ +

E-mail messages are represented by hyperdatabase nodes of class "msg". +The actual text content of the messages is stored in separate files. +(There's no advantage to be gained by stuffing them into the +hyperdatabase, and if messages are stored in ordinary text files, +they can be grepped from the command line.) The text of a message is +saved in a file named after the message node designator (e.g. "msg23") +for the sake of the command interface (see below). Attachments are +stored separately and associated with "file" nodes. +The "msg" class has the definition: + +

hyperdb.Class(db, "msg", author=hyperdb.Link("user"),
+                         recipients=hyperdb.Multilink("user"),
+                         date=hyperdb.Date(),
+                         summary=hyperdb.String(),
+                         files=hyperdb.Multilink("file"))
+ +

The "author" property indicates the author of the message +(a "user" node must exist in the hyperdatabase for any messages +that are stored in the system). +The "summary" property contains a summary of the message for display +in a message index. + +

4.1.3. Files

+ +

Submitted files are represented by hyperdatabase +nodes of class "file". Like e-mail messages, the file content +is stored in files outside the database, +named after the file node designator (e.g. "file17"). +The "file" class has the definition: + +

hyperdb.Class(db, "file", user=hyperdb.Link("user"),
+                          name=hyperdb.String(),
+                          type=hyperdb.String())
+ +

The "user" property indicates the user who submitted the +file, the "name" property holds the original name of the file, +and the "type" property holds the MIME type of the file as received. + +

4.2. Item Classes

+ +

All items have the following standard properties: + +

title=hyperdb.String()
+messages=hyperdb.Multilink("msg")
+files=hyperdb.Multilink("file")
+nosy=hyperdb.Multilink("user")
+superseder=hyperdb.Multilink("item")
+ +

Also, two Date properties named "creation" and "activity" are +fabricated by the Roundup database layer. By "fabricated" we +mean that no such properties are actually stored in the +hyperdatabase, but when properties on items are requested, the +"creation" and "activity" properties are made available. +The value of the "creation" property is the date when an item was +created, and the value of the "activity" property is the +date when any property on the item was last edited (equivalently, +these are the dates on the first and last records in the item's journal). + +

4.3. Interface Specification

+ +

The interface to a Roundup database delegates most method +calls to the hyperdatabase, except for the following +changes and additional methods. + +

class Database:
+    # Overridden methods:
+
+    def __init__(self, storagelocator, journaltag):
+        """When the Roundup database is opened by a particular user,
+        the 'journaltag' is the id of the user's "user" node."""
+
+    def getclass(self, classname):
+        """This method now returns an instance of either Class or
+        ItemClass depending on whether an item class is specified."""
+
+    # New methods:
+
+    def getuid(self):
+        """Return the id of the "user" node associated with the user
+        that owns this connection to the hyperdatabase."""
+
+class Class:
+    # Overridden methods:
+
+    def create(self, **propvalues):
+    def set(self, **propvalues):
+    def retire(self, nodeid):
+        """These operations trigger detectors and can be vetoed.  Attempts
+        to modify the "creation" or "activity" properties cause a KeyError.
+        """
+
+    # New methods:
+
+    def audit(self, event, detector):
+    def react(self, event, detector):
+        """Register a detector (see below for more details)."""
+
+class ItemClass(Class):
+    # Overridden methods:
+
+    def __init__(self, db, classname, **properties):
+        """The newly-created class automatically includes the "messages",
+        "files", "nosy", and "superseder" properties.  If the 'properties'
+        dictionary attempts to specify any of these properties or a
+        "creation" or "activity" property, a ValueError is raised."""
+
+    def get(self, nodeid, propname):
+    def getprops(self):
+        """In addition to the actual properties on the node, these
+        methods provide the "creation" and "activity" properties."""
+
+    # New methods:
+
+    def addmessage(self, nodeid, summary, text):
+        """Add a message to an item's mail spool.
+
+        A new "msg" node is constructed using the current date, the
+        user that owns the database connection as the author, and
+        the specified summary text.  The "files" and "recipients"
+        fields are left empty.  The given text is saved as the body
+        of the message and the node is appended to the "messages"
+        field of the specified item.
+        """
+
+    def sendmessage(self, nodeid, msgid):
+        """Send a message to the members of an item's nosy list.
+
+        The message is sent only to users on the nosy list who are not
+        already on the "recipients" list for the message.  These users
+        are then added to the message's "recipients" list.
+        """
+
+ +

4.4. Default Schema

+ +

The default schema included with Roundup turns it into a +typical software bug tracker. The database is set up like this: + +

pri = Class(db, "priority", name=hyperdb.String(), order=hyperdb.String())
+pri.setkey("name")
+pri.create(name="critical", order="1")
+pri.create(name="urgent", order="2")
+pri.create(name="bug", order="3")
+pri.create(name="feature", order="4")
+pri.create(name="wish", order="5")
+
+stat = Class(db, "status", name=hyperdb.String(), order=hyperdb.String())
+stat.setkey("name")
+stat.create(name="unread", order="1")
+stat.create(name="deferred", order="2")
+stat.create(name="chatting", order="3")
+stat.create(name="need-eg", order="4")
+stat.create(name="in-progress", order="5")
+stat.create(name="testing", order="6")
+stat.create(name="done-cbb", order="7")
+stat.create(name="resolved", order="8")
+
+Class(db, "keyword", name=hyperdb.String())
+
+Class(db, "issue", fixer=hyperdb.Multilink("user"),
+                   topic=hyperdb.Multilink("keyword"),
+                   priority=hyperdb.Link("priority"),
+                   status=hyperdb.Link("status"))
+
+ +

(The "order" property hasn't been explained yet. It +gets used by the Web user interface for sorting.) + +

The above isn't as pretty-looking as the schema specification +in the first-stage submission, but it could be made just as easy +with the addition of a convenience function like Choice +for setting up the "priority" and "status" classes: + +

def Choice(name, *options):
+    cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
+    for i in range(len(options)):
+        cl.create(name=option[i], order=i)
+    return hyperdb.Link(name)
+
+ +


+

5. Detector Interface

+ +

Detectors are Python functions that are triggered on certain +kinds of events. The definitions of the +functions live in Python modules placed in a directory set aside +for this purpose. Importing the Roundup database module also +imports all the modules in this directory, and the init() +function of each module is called when a database is opened to +provide it a chance to register its detectors. + +

There are two kinds of detectors: + +

    +
  • an auditor is triggered just before modifying an node +
  • a reactor is triggered just after an node has been modified +
+ +

When the Roundup database is about to perform a +create(), set(), or retire() +operation, it first calls any auditors that +have been registered for that operation on that class. +Any auditor may raise a Reject exception +to abort the operation. + +

If none of the auditors raises an exception, the database +proceeds to carry out the operation. After it's done, it +then calls all of the reactors that have been registered +for the operation. + +

5.1. Interface Specification

+ +

The audit() and react() methods +register detectors on a given class of nodes. + +

class Class:
+    def audit(self, event, detector):
+        """Register an auditor on this class.
+
+        'event' should be one of "create", "set", or "retire".
+        'detector' should be a function accepting four arguments.
+        """
+
+    def react(self, event, detector):
+        """Register a reactor on this class.
+
+        'event' should be one of "create", "set", or "retire".
+        'detector' should be a function accepting four arguments.
+        """
+
+ +

Auditors are called with the arguments: + +

audit(db, cl, nodeid, newdata)
+ +where db is the database, cl is an +instance of Class or ItemClass within the database, and newdata +is a dictionary mapping property names to values. + +For a create() +operation, the nodeid argument is None and newdata +contains all of the initial property values with which the node +is about to be created. + +For a set() operation, newdata +contains only the names and values of properties that are about +to be changed. + +For a retire() operation, newdata is None. + +

Reactors are called with the arguments: + +

react(db, cl, nodeid, olddata)
+ +where db is the database, cl is an +instance of Class or ItemClass within the database, and olddata +is a dictionary mapping property names to values. + +For a create() +operation, the nodeid argument is the id of the +newly-created node and olddata is None. + +For a set() operation, olddata +contains the names and previous values of properties that were changed. + +For a retire() operation, nodeid is the +id of the retired node and olddata is None. + +

5.2. Detector Example

+ +

Here is an example of detectors written for a hypothetical +project-management application, where users can signal approval +of a project by adding themselves to an "approvals" list, and +a project proceeds when it has three approvals. + +

# Permit users only to add themselves to the "approvals" list.
+
+def check_approvals(db, cl, id, newdata):
+    if newdata.has_key("approvals"):
+        if cl.get(id, "status") == db.status.lookup("approved"):
+            raise Reject, "You can't modify the approvals list " \
+                          "for a project that has already been approved."
+        old = cl.get(id, "approvals")
+        new = newdata["approvals"]
+        for uid in old:
+            if uid not in new and uid != db.getuid():
+                raise Reject, "You can't remove other users from the "
+                              "approvals list; you can only remove yourself."
+        for uid in new:
+            if uid not in old and uid != db.getuid():
+                raise Reject, "You can't add other users to the approvals "
+                              "list; you can only add yourself."
+
+# When three people have approved a project, change its
+# status from "pending" to "approved".
+
+def approve_project(db, cl, id, olddata):
+    if olddata.has_key("approvals") and len(cl.get(id, "approvals")) == 3:
+        if cl.get(id, "status") == db.status.lookup("pending"):
+            cl.set(id, status=db.status.lookup("approved"))
+
+def init(db):
+    db.project.audit("set", check_approval)
+    db.project.react("set", approve_project)
+ +

Here is another example of a detector that can allow or prevent +the creation of new nodes. In this scenario, patches for a software +project are submitted by sending in e-mail with an attached file, +and we want to ensure that there are text/plain attachments on +the message. The maintainer of the package can then apply the +patch by setting its status to "applied". + +

# Only accept attempts to create new patches that come with patch files.
+
+def check_new_patch(db, cl, id, newdata):
+    if not newdata["files"]:
+        raise Reject, "You can't submit a new patch without " \
+                      "attaching a patch file."
+    for fileid in newdata["files"]:
+        if db.file.get(fileid, "type") != "text/plain":
+            raise Reject, "Submitted patch files must be text/plain."
+
+# When the status is changed from "approved" to "applied", apply the patch.
+
+def apply_patch(db, cl, id, olddata):
+    if cl.get(id, "status") == db.status.lookup("applied") and \
+        olddata["status"] == db.status.lookup("approved"):
+        # ...apply the patch...
+
+def init(db):
+    db.patch.audit("create", check_new_patch)
+    db.patch.react("set", apply_patch)
+ +


+

6. Command Interface

+ +

The command interface is a very simple and minimal interface, +intended only for quick searches and checks from the shell prompt. +(Anything more interesting can simply be written in Python using +the Roundup database module.) + +

6.1. Interface Specification

+ +

A single command, roundup, provides basic access to +the hyperdatabase from the command line. + +

    +
  • roundup get [-list] designator[,designator,...] propname +
  • roundup set designator[,designator,...] propname=value ... +
  • roundup find [-list] classname propname=value ... +
+ +

Property values are represented as strings in command arguments +and in the printed results: + +

    +
  • Strings are, well, strings. + +
  • Date values are printed in the full date format in the local +time zone, and accepted in the full format or any of the partial +formats explained above. + +
  • Link values are printed as node designators. When given as +an argument, node designators and key strings are both accepted. + +
  • Multilink values are printed as lists of node designators +joined by commas. When given as an argument, node designators +and key strings are both accepted; an empty string, a single node, +or a list of nodes joined by commas is accepted. +
+ +

When multiple nodes are specified to the +roundup get or roundup set +commands, the specified properties are retrieved or set +on all the listed nodes. + +

When multiple results are returned by the roundup get +or roundup find commands, they are printed one per +line (default) or joined by commas (with the -list) option. + +

6.2. Usage Example

+ +

To find all messages regarding in-progress issues that +contain the word "spam", for example, you could execute the +following command from the directory where the database +dumps its files: + +

shell% for issue in `roundup find issue status=in-progress`; do
+> grep -l spam `roundup get $issue messages`
+> done
+msg23
+msg49
+msg50
+msg61
+shell%
+ +

Or, using the -list option, this can be written as a single command: + +

shell% grep -l spam `roundup get \
+    \`roundup find -list issue status=in-progress\` messages`
+msg23
+msg49
+msg50
+msg61
+shell%
+ +


+

7. E-mail User Interface

+ +

The Roundup system must be assigned an e-mail address +at which to receive mail. Messages should be piped to +the Roundup mail-handling script by the mail delivery +system (e.g. using an alias beginning with "|" for sendmail). + +

7.1. Message Processing

+ +

Incoming messages are examined for multiple parts. +In a multipart/mixed message or part, each subpart is +extracted and examined. In a multipart/alternative +message or part, we look for a text/plain subpart and +ignore the other parts. The text/plain subparts are +assembled to form the textual body of the message, to +be stored in the file associated with a "msg" class node. +Any parts of other types are each stored in separate +files and given "file" class nodes that are linked to +the "msg" node. + +

The "summary" property on message nodes is taken from +the first non-quoting section in the message body. +The message body is divided into sections by blank lines. +Sections where the second and all subsequent lines begin +with a ">" or "|" character are considered "quoting +sections". The first line of the first non-quoting +section becomes the summary of the message. + +

All of the addresses in the To: and Cc: headers of the +incoming message are looked up among the user nodes, and +the corresponding users are placed in the "recipients" +property on the new "msg" node. The address in the From: +header similarly determines the "author" property of the +new "msg" node. +The default handling for +addresses that don't have corresponding users is to create +new users with no passwords and a username equal to the +address. (The web interface does not permit logins for +users with no passwords.) If we prefer to reject mail from +outside sources, we can simply register an auditor on the +"user" class that prevents the creation of user nodes with +no passwords. + +

The subject line of the incoming message is examined to +determine whether the message is an attempt to create a new +item or to discuss an existing item. A designator enclosed +in square brackets is sought as the first thing on the +subject line (after skipping any "Fwd:" or "Re:" prefixes). + +

If an item designator (class name and id number) is found +there, the newly created "msg" node is added to the "messages" +property for that item, and any new "file" nodes are added to +the "files" property for the item. + +

If just an item class name is found there, we attempt to +create a new item of that class with its "messages" property +initialized to contain the new "msg" node and its "files" +property initialized to contain any new "file" nodes. + +

Both cases may trigger detectors (in the first case we +are calling the set() method to add the message to the +item's spool; in the second case we are calling the +create() method to create a new node). If an auditor +raises an exception, the original message is bounced back to +the sender with the explanatory message given in the exception. + +

7.2. Nosy Lists

+ +

A standard detector is provided that watches for additions +to the "messages" property. When a new message is added, the +detector sends it to all the users on the "nosy" list for the +item that are not already on the "recipients" list of the +message. Those users are then appended to the "recipients" +property on the message, so multiple copies of a message +are never sent to the same user. The journal recorded by +the hyperdatabase on the "recipients" property then provides +a log of when the message was sent to whom. + +

7.3. Setting Properties

+ +

The e-mail interface also provides a simple way to set +properties on items. At the end of the subject line, +propname=value pairs can be +specified in square brackets, using the same conventions +as for the roundup set shell command. + +


+

8. Web User Interface

+ +

The web interface is provided by a CGI script that can be +run under any web server. A simple web server can easily be +built on the standard CGIHTTPServer module, and +should also be included in the distribution for quick +out-of-the-box deployment. + +

The user interface is constructed from a number of template +files containing mostly HTML. Among the HTML tags in templates +are interspersed some nonstandard tags, which we use as +placeholders to be replaced by properties and their values. + +

8.1. Views and View Specifiers

+ +

There are two main kinds of views: index views and item views. +An index view displays a list of items of a particular class, +optionally sorted and filtered as requested. An item view +presents the properties of a particular item for editing +and displays the message spool for the item. + +

A view specifier is a string that specifies +all the options needed to construct a particular view. +It goes after the URL to the Roundup CGI script or the +web server to form the complete URL to a view. When the +result of selecting a link or submitting a form takes +the user to a new view, the Web browser should be redirected +to a canonical location containing a complete view specifier +so that the view can be bookmarked. + +

8.2. Displaying Properties

+ +

Properties appear in the user interface in three contexts: +in indices, in editors, and as filters. For each type of +property, there are several display possibilities. For example, +in an index view, a string property may just be printed as +a plain string, but in an editor view, that property should +be displayed in an editable field. + +

The display of a property is handled by functions in +a displayers module. Each function accepts at +least three standard arguments -- the database, class name, +and node id -- and returns a chunk of HTML. + +

Displayer functions are triggered by <display> +tags in templates. The call attribute of the tag +provides a Python expression for calling the displayer +function. The three standard arguments are inserted in +front of the arguments given. For example, the occurrence of + +

    <display call="plain('status', max=30)">
+
+ +in a template triggers a call to + +
    plain(db, "issue", 13, "status", max=30)
+
+ +when displaying item 13 in the "issue" class. The displayer +functions can accept extra arguments to further specify +details about the widgets that should be generated. By defining new +displayer functions, the user interface can be highly customized. + +

Some of the standard displayer functions include: + +

    +
  • plain: display a String property directly; +display a Date property in a specified time zone with an option +to omit the time from the date stamp; for a Link or Multilink +property, display the key strings of the linked nodes (or the +ids if the linked class has no key property) + +
  • field: display a property like the +plain displayer above, but in a text field +to be edited + +
  • menu: for a Link property, display +a menu of the available choices + +
  • link: for a Link or Multilink property, +display the names of the linked nodes, hyperlinked to the +item views on those nodes + +
  • count: for a Multilink property, display +a count of the number of links in the list + +
  • reldate: display a Date property in terms +of an interval relative to the current date (e.g. "+ 3w", "- 2d"). + +
  • download: show a Link("file") or Multilink("file") +property using links that allow you to download files + +
  • checklist: for a Link or Multilink property, +display checkboxes for the available choices to permit filtering +
+ +

8.3. Index Views

+ +

An index view contains two sections: a filter section +and an index section. +The filter section provides some widgets for selecting +which items appear in the index. The index section is +a table of items. + +

8.3.1. Index View Specifiers

+ +

An index view specifier looks like this (whitespace +has been added for clarity): + +

/issue?status=unread,in-progress,resolved&
+        topic=security,ui&
+        :group=+priority&
+        :sort=-activity&
+        :filters=status,topic&
+        :columns=title,status,fixer
+
+ +

The index view is determined by two parts of the +specifier: the layout part and the filter part. +The layout part consists of the query parameters that +begin with colons, and it determines the way that the +properties of selected nodes are displayed. +The filter part consists of all the other query parameters, +and it determines the criteria by which nodes +are selected for display. + +

The filter part is interactively manipulated with +the form widgets displayed in the filter section. The +layout part is interactively manipulated by clicking +on the column headings in the table. + +

The filter part selects the union of the +sets of items with values matching any specified Link +properties and the intersection of the sets +of items with values matching any specified Multilink +properties. + +

The example specifies an index of "issue" nodes. +Only items with a "status" of either +"unread" or "in-progres" or "resolved" are displayed, +and only items with "topic" values including both +"security" and "ui" are displayed. The items +are grouped by priority, arranged in ascending order; +and within groups, sorted by activity, arranged in +descending order. The filter section shows filters +for the "status" and "topic" properties, and the +table includes columns for the "title", "status", and +"fixer" properties. + +

Associated with each item class is a default +layout specifier. The layout specifier in the above +example is the default layout to be provided with +the default bug-tracker schema described above in +section 4.4. + +

8.3.2. Filter Section

+ +

The template for a filter section provides the +filtering widgets at the top of the index view. +Fragments enclosed in <property>...</property> +tags are included or omitted depending on whether the +view specifier requests a filter for a particular property. + +

Here's a simple example of a filter template. + +

<property name=status>
+    <display call="checklist('status')">
+</property>
+<br>
+<property name=priority>
+    <display call="checklist('priority')">
+</property>
+<br>
+<property name=fixer>
+    <display call="menu('fixer')">
+</property>
+ +

8.3.3. Index Section

+ +

The template for an index section describes one row of +the index table. +Fragments enclosed in <property>...</property> +tags are included or omitted depending on whether the +view specifier requests a column for a particular property. +The table cells should contain <display> tags +to display the values of the item's properties. + +

Here's a simple example of an index template. + +

<tr>
+    <property name=title>
+        <td><display call="plain('title', max=50)"></td>
+    </property>
+    <property name=status>
+        <td><display call="plain('status')"></td>
+    </property>
+    <property name=fixer>
+        <td><display call="plain('fixer')"></td>
+    </property>
+</tr>
+ +

8.3.4. Sorting

+ +

String and Date values are sorted in the natural way. +Link properties are sorted according to the value of the +"order" property on the linked nodes if it is present; or +otherwise on the key string of the linked nodes; or +finally on the node ids. Multilink properties are +sorted according to how many links are present. + +

8.4. Item Views

+ +

An item view contains an editor section and a spool section. +At the top of an item view, links to superseding and superseded +items are always displayed. + +

8.4.1. Item View Specifiers

+ +

An item view specifier is simply the item's designator: + +

/patch23
+
+ +

8.4.2. Editor Section

+ +

The editor section is generated from a template +containing <display> tags to insert +the appropriate widgets for editing properties. + +

Here's an example of a basic editor template. + +

<table>
+<tr>
+    <td colspan=2>
+        <display call="field('title', size=60)">
+    </td>
+</tr>
+<tr>
+    <td>
+        <display call="field('fixer', size=30)">
+    </td>
+    <td>
+        <display call="menu('status')>
+    </td>
+</tr>
+<tr>
+    <td>
+        <display call="field('nosy', size=30)">
+    </td>
+    <td>
+        <display call="menu('priority')>
+    </td>
+</tr>
+<tr>
+    <td colspan=2>
+        <display call="note()">
+    </td>
+</tr>
+</table>
+
+ +

As shown in the example, the editor template can also +request the display of a "note" field, which is a +text area for entering a note to go along with a change. + +

When a change is submitted, the system automatically +generates a message describing the changed properties. +The message displays all of the property values on the +item and indicates which ones have changed. +An example of such a message might be this: + +

title: Polly Parrot is dead
+priority: critical
+status: unread -> in-progress
+fixer: (none)
+keywords: parrot,plumage,perch,nailed,dead
+
+ +

If a note is given in the "note" field, the note is +appended to the description. The message is then added +to the item's message spool (thus triggering the standard +detector to react by sending out this message to the nosy list). + +

8.4.3. Spool Section

+ +

The spool section lists messages in the item's "messages" +property. The index of messages displays the "date", "author", +and "summary" properties on the message nodes, and selecting a +message takes you to its content. + +


+

9. Deployment Scenarios

+ +

The design described above should be general enough +to permit the use of Roundup for bug tracking, managing +projects, managing patches, or holding discussions. By +using nodes of multiple types, one could deploy a system +that maintains requirement specifications, catalogs bugs, +and manages submitted patches, where patches could be +linked to the bugs and requirements they address. + +


+

10. Acknowledgements

+ +

My thanks are due to Christy Heyl for +reviewing and contributing suggestions to this paper +and motivating me to get it done, and to +Jesse Vincent, Mark Miller, Christopher Simons, +Jeff Dunmall, Wayne Gramlich, and Dean Tribble for +their assistance with the first-round submission. + + + + +

+ +

+ + + + + + + + + + + + + +
   [Home]      [FAQ]      [License]      [Rules]      [Configure]      [Build]      [Test]      [Track]      [Resources]      [Archives]   
+
+ +


+
Last modified 2001/04/06 11:50:59.9063 US/Mountain
+ + diff --git a/roundup/backends/back_anydbm.py b/roundup/backends/back_anydbm.py index 23beeb0..99febb2 100644 --- a/roundup/backends/back_anydbm.py +++ b/roundup/backends/back_anydbm.py @@ -1,4 +1,4 @@ -#$Id: back_anydbm.py,v 1.2 2001-07-23 08:20:44 richard Exp $ +#$Id: back_anydbm.py,v 1.3 2001-07-25 01:23:07 richard Exp $ import anydbm, os, marshal from roundup import hyperdb, date @@ -66,7 +66,10 @@ class Database(hyperdb.Database): multiple actions ''' path = os.path.join(os.getcwd(), self.dir, 'nodes.%s'%classname) - return anydbm.open(path, mode) + if os.path.exists(path): + return anydbm.open(path, mode) + else: + return anydbm.open(path, 'n') # # Nodes @@ -100,6 +103,7 @@ class Database(hyperdb.Database): # convert the marshalled data to instances properties = self.classes[classname].properties for key in res.keys(): + if key == self.RETIRED_FLAG: continue if properties[key].isDateType: res[key] = date.Date(res[key]) elif properties[key].isIntervalType: @@ -197,4 +201,9 @@ class Database(hyperdb.Database): # #$Log: not supported by cvs2svn $ +#Revision 1.2 2001/07/23 08:20:44 richard +#Moved over to using marshal in the bsddb and anydbm backends. +#roundup-admin now has a "freshen" command that'll load/save all nodes (not +# retired - mod hyperdb.Class.list() so it lists retired nodes) +# # diff --git a/roundup/templates/extended/dbinit.py b/roundup/templates/extended/dbinit.py index ac7c654..44da2b2 100644 --- a/roundup/templates/extended/dbinit.py +++ b/roundup/templates/extended/dbinit.py @@ -1,4 +1,4 @@ -# $Id: dbinit.py,v 1.5 2001-07-23 23:20:35 richard Exp $ +# $Id: dbinit.py,v 1.6 2001-07-25 01:23:07 richard Exp $ import os @@ -25,8 +25,6 @@ class IssueClass(roundupdb.IssueClass): def open(name=None): ''' as from the roundupdb method openDB - storagelocator must be the directory the __init__.py file is in - - os.path.split(__file__)[0] gives us that I think ''' from roundup.hyperdb import String, Date, Link, Multilink @@ -96,9 +94,6 @@ def open(name=None): def init(adminpw): ''' as from the roundupdb method initDB - storagelocator must be the directory the __init__.py file is in - - os.path.split(__file__)[0] gives us that I think - Open the new database, and set up a bunch of attributes. ''' @@ -156,6 +151,9 @@ def init(adminpw): # # $Log: not supported by cvs2svn $ +# Revision 1.5 2001/07/23 23:20:35 richard +# forgot to remove the interfaces from the dbinit module ;) +# # Revision 1.4 2001/07/23 08:45:28 richard # ok, so now "./roundup-admin init" will ask questions in an attempt to get a # workable instance_home set up :) diff --git a/tests/test_db.py b/tests/test_db.py index 67b17c4..5d5511f 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -35,9 +35,10 @@ class DBTestCase(unittest.TestCase): self.db.issue.create(title="arguments", status='2') self.db.issue.create(title="abuse", status='1') self.db.issue.addprop(fixer=Link("user")) - self.db.issue.getprops() -#{"title": , "status": , -#"user": } + props = self.db.issue.getprops() + keys = props.keys() + keys.sort() + self.assertEqual(keys, ['title', 'status', 'user'], 'wrong prop list') self.db.issue.set('5', status=2) self.db.issue.get('5', "status") self.db.status.get('2', "name") @@ -47,6 +48,8 @@ class DBTestCase(unittest.TestCase): self.db.status.history('1') self.db.status.history('2') + def testExceptions(self): + # this tests the exceptions that should be raised def suite(): return unittest.makeSuite(DBTestCase, 'test') diff --git a/tests/test_schema.py b/tests/test_schema.py index 3620840..38318ec 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -42,9 +42,11 @@ class SchemaTestCase(unittest.TestCase): def testB_Issue(self): issue = Class(self.db, "issue", title=String(), status=Link("status")) + self.assert_(issue, 'no class object returned') def testC_User(self): user = Class(self.db, "user", username=String(), password=String()) + self.assert_(user, 'no class object returned') user.setkey("username") -- 2.30.2