diff --git a/roundup/hyperdb.py b/roundup/hyperdb.py
index d69e4edc4a95124c8af3a079d32948c8a9051c7b..2905166251310174fca98b8f340bb247d90ec462 100644 (file)
--- a/roundup/hyperdb.py
+++ b/roundup/hyperdb.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: hyperdb.py,v 1.46 2002-01-07 10:42:23 richard Exp $
+# $Id: hyperdb.py,v 1.76 2002-07-18 11:17:30 gmcm Exp $
__doc__ = """
Hyperdatabase implementation, especially field types.
"""
# standard python modules
-import cPickle, re, string, weakref
+import sys, os, time, re
# roundup modules
import date, password
+# configure up the DEBUG and TRACE captures
+class Sink:
+ def write(self, content):
+ pass
+DEBUG = os.environ.get('HYPERDBDEBUG', '')
+if DEBUG and __debug__:
+ if DEBUG == 'stdout':
+ DEBUG = sys.stdout
+ else:
+ DEBUG = open(DEBUG, 'a')
+else:
+ DEBUG = Sink()
+TRACE = os.environ.get('HYPERDBTRACE', '')
+if TRACE and __debug__:
+ if TRACE == 'stdout':
+ TRACE = sys.stdout
+ else:
+ TRACE = open(TRACE, 'w')
+else:
+ TRACE = Sink()
+def traceMark():
+ print >>TRACE, '**MARK', time.ctime()
+del Sink
#
# Types
#
class String:
"""An object designating a String property."""
+ def __init__(self, indexme='no'):
+ self.indexme = indexme == 'yes'
def __repr__(self):
+ ' more useful for dumps '
return '<%s>'%self.__class__
class Password:
"""An object designating a Password property."""
def __repr__(self):
+ ' more useful for dumps '
return '<%s>'%self.__class__
class Date:
"""An object designating a Date property."""
def __repr__(self):
+ ' more useful for dumps '
return '<%s>'%self.__class__
class Interval:
"""An object designating an Interval property."""
def __repr__(self):
+ ' more useful for dumps '
return '<%s>'%self.__class__
class Link:
"""An object designating a Link property that links to a
node in a specified class."""
- def __init__(self, classname):
+ def __init__(self, classname, do_journal='no'):
+ ''' Default is to not journal link and unlink events
+ '''
self.classname = classname
+ self.do_journal = do_journal == 'yes'
def __repr__(self):
+ ' more useful for dumps '
return '<%s to "%s">'%(self.__class__, self.classname)
class Multilink:
"""An object designating a Multilink property that links
to nodes in a specified class.
+
+ "classname" indicates the class to link to
+
+ "do_journal" indicates whether the linked-to nodes should have
+ 'link' and 'unlink' events placed in their journal
"""
- def __init__(self, classname):
+ def __init__(self, classname, do_journal='no'):
+ ''' Default is to not journal link and unlink events
+ '''
self.classname = classname
+ self.do_journal = do_journal == 'yes'
def __repr__(self):
+ ' more useful for dumps '
return '<%s to "%s">'%(self.__class__, self.classname)
-class DatabaseError(ValueError):
+class Boolean:
+ """An object designating a boolean property"""
+ def __repr__(self):
+ 'more useful for dumps'
+ return '<%s>' % self.__class__
+
+class Number:
+ """An object designating a numeric property"""
+ def __repr__(self):
+ 'more useful for dumps'
+ return '<%s>' % self.__class__
+#
+# Support for splitting designators
+#
+class DesignatorError(ValueError):
pass
-
+def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
+ ''' Take a foo123 and return ('foo', 123)
+ '''
+ m = dre.match(designator)
+ if m is None:
+ raise DesignatorError, '"%s" not a node designator'%designator
+ return m.group(1), m.group(2)
#
# the base Database class
#
+class DatabaseError(ValueError):
+ '''Error to be raised when there is some problem in the database code
+ '''
+ pass
class Database:
'''A database for storing records containing flexible data types.
This is necessary to determine if any values have changed during a
transaction.
+
+Implementation
+--------------
+
+All methods except __repr__ and getnode must be implemented by a
+concrete backend Class.
+
'''
# flag to set on retired entries
RETIRED_FLAG = '__hyperdb_retired'
- def __init__(self, storagelocator, journaltag=None):
+ # XXX deviates from spec: storagelocator is obtained from the config
+ def __init__(self, config, journaltag=None):
"""Open a hyperdatabase given a specifier to some storage.
+ The 'storagelocator' is obtained from config.DATABASE.
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
"""
raise NotImplementedError
+ def post_init(self):
+ """Called once the schema initialisation has finished."""
+ raise NotImplementedError
+
def __getattr__(self, classname):
"""A convenient way of calling self.getclass(classname)."""
raise NotImplementedError
'''
raise NotImplementedError
+ def serialise(self, classname, node):
+ '''Copy the node contents, converting non-marshallable data into
+ marshallable data.
+ '''
+ return node
+
def setnode(self, classname, nodeid, node):
'''Change the specified node.
'''
raise NotImplementedError
+ def unserialise(self, classname, node):
+ '''Decode the marshalled node data
+ '''
+ return node
+
def getnode(self, classname, nodeid, db=None, cache=1):
'''Get a node from the database.
'''
'''
raise NotImplementedError
+ def pack(self, pack_before):
+ ''' pack the database
+ '''
+ raise NotImplementedError
+
def commit(self):
''' Commit the current transactions.
'''
raise NotImplementedError
-_marker = []
#
# The base Class class
#
class Class:
- """The handle to a particular class of nodes in a hyperdatabase."""
+ """ The handle to a particular class of nodes in a hyperdatabase.
+
+ All methods except __repr__ and getnode must be implemented by a
+ concrete backend Class.
+ """
def __init__(self, db, classname, **properties):
"""Create a new class with a given name and property specification.
or a ValueError is raised. The keyword arguments in 'properties'
must map names to property objects, or a TypeError is raised.
"""
- self.classname = classname
- self.properties = properties
- self.db = weakref.proxy(db) # use a weak ref to avoid circularity
- self.key = ''
-
- # do the db-related init stuff
- db.addclass(self)
+ raise NotImplementedError
def __repr__(self):
+ '''Slightly more useful representation
+ '''
return '<hypderdb.Class "%s">'%self.classname
# Editing nodes:
If an id in a link or multilink property does not refer to a valid
node, an IndexError is raised.
"""
- if propvalues.has_key('id'):
- raise KeyError, '"id" is reserved'
-
- if self.db.journaltag is None:
- raise DatabaseError, 'Database open read-only'
-
- # new node's id
- newid = str(self.count() + 1)
-
- # validate propvalues
- num_re = re.compile('^\d+$')
- for key, value in propvalues.items():
- if key == self.key:
- try:
- self.lookup(value)
- except KeyError:
- pass
- else:
- raise ValueError, 'node with key "%s" exists'%value
-
- # try to handle this property
- try:
- prop = self.properties[key]
- except KeyError:
- raise KeyError, '"%s" has no property "%s"'%(self.classname,
- key)
-
- if isinstance(prop, Link):
- if type(value) != type(''):
- raise ValueError, 'link value must be String'
- link_class = self.properties[key].classname
- # if it isn't a number, it's a key
- if not num_re.match(value):
- try:
- value = self.db.classes[link_class].lookup(value)
- except (TypeError, KeyError):
- raise IndexError, 'new property "%s": %s not a %s'%(
- key, value, link_class)
- elif not self.db.hasnode(link_class, value):
- raise IndexError, '%s has no node %s'%(link_class, value)
-
- # save off the value
- propvalues[key] = value
-
- # register the link with the newly linked node
- self.db.addjournal(link_class, value, 'link',
- (self.classname, newid, key))
-
- elif isinstance(prop, Multilink):
- if type(value) != type([]):
- raise TypeError, 'new property "%s" not a list of ids'%key
- link_class = self.properties[key].classname
- l = []
- for entry in value:
- if type(entry) != type(''):
- raise ValueError, 'link value must be String'
- # if it isn't a number, it's a key
- if not num_re.match(entry):
- try:
- entry = self.db.classes[link_class].lookup(entry)
- except (TypeError, KeyError):
- raise IndexError, 'new property "%s": %s not a %s'%(
- key, entry, self.properties[key].classname)
- l.append(entry)
- value = l
- propvalues[key] = value
-
- # handle additions
- for id in value:
- if not self.db.hasnode(link_class, id):
- raise IndexError, '%s has no node %s'%(link_class, id)
- # register the link with the newly linked node
- self.db.addjournal(link_class, id, 'link',
- (self.classname, newid, key))
-
- elif isinstance(prop, String):
- if type(value) != type(''):
- raise TypeError, 'new property "%s" not a string'%key
-
- elif isinstance(prop, Password):
- if not isinstance(value, password.Password):
- raise TypeError, 'new property "%s" not a Password'%key
-
- elif isinstance(prop, Date):
- if not isinstance(value, date.Date):
- raise TypeError, 'new property "%s" not a Date'%key
-
- elif isinstance(prop, Interval):
- if not isinstance(value, date.Interval):
- raise TypeError, 'new property "%s" not an Interval'%key
-
- # make sure there's data where there needs to be
- for key, prop in self.properties.items():
- if propvalues.has_key(key):
- continue
- if key == self.key:
- raise ValueError, 'key property "%s" is required'%key
- if isinstance(prop, Multilink):
- propvalues[key] = []
- else:
- propvalues[key] = None
-
- # convert all data to strings
- for key, prop in self.properties.items():
- if isinstance(prop, Date):
- propvalues[key] = propvalues[key].get_tuple()
- elif isinstance(prop, Interval):
- propvalues[key] = propvalues[key].get_tuple()
- elif isinstance(prop, Password):
- propvalues[key] = str(propvalues[key])
-
- # done
- self.db.addnode(self.classname, newid, propvalues)
- self.db.addjournal(self.classname, newid, 'create', propvalues)
- return newid
+ raise NotImplementedError
+ _marker = []
def get(self, nodeid, propname, default=_marker, cache=1):
"""Get the value of a property on an existing node of this class.
determine what its values prior to modification are, you need to
set cache=0.
"""
- if propname == 'id':
- return nodeid
-
- # get the node's dict
- d = self.db.getnode(self.classname, nodeid, cache=cache)
- if not d.has_key(propname) and default is not _marker:
- return default
-
- # get the value
- prop = self.properties[propname]
-
- # possibly convert the marshalled data to instances
- if isinstance(prop, Date):
- return date.Date(d[propname])
- elif isinstance(prop, Interval):
- return date.Interval(d[propname])
- elif isinstance(prop, Password):
- p = password.Password()
- p.unpack(d[propname])
- return p
-
- return d[propname]
+ raise NotImplementedError
# XXX not in spec
def getnode(self, nodeid, cache=1):
If the value of a Link or Multilink property contains an invalid
node id, a ValueError is raised.
"""
- if not propvalues:
- return
-
- if propvalues.has_key('id'):
- raise KeyError, '"id" is reserved'
-
- if self.db.journaltag is None:
- raise DatabaseError, 'Database open read-only'
-
- node = self.db.getnode(self.classname, nodeid)
- if node.has_key(self.db.RETIRED_FLAG):
- raise IndexError
- num_re = re.compile('^\d+$')
- for key, value in propvalues.items():
- # check to make sure we're not duplicating an existing key
- if key == self.key and node[key] != value:
- try:
- self.lookup(value)
- except KeyError:
- pass
- else:
- raise ValueError, 'node with key "%s" exists'%value
-
- # this will raise the KeyError if the property isn't valid
- # ... we don't use getprops() here because we only care about
- # the writeable properties.
- prop = self.properties[key]
-
- if isinstance(prop, Link):
- link_class = self.properties[key].classname
- # if it isn't a number, it's a key
- if type(value) != type(''):
- raise ValueError, 'link value must be String'
- if not num_re.match(value):
- try:
- value = self.db.classes[link_class].lookup(value)
- except (TypeError, KeyError):
- raise IndexError, 'new property "%s": %s not a %s'%(
- key, value, self.properties[key].classname)
-
- if not self.db.hasnode(link_class, value):
- raise IndexError, '%s has no node %s'%(link_class, value)
-
- # register the unlink with the old linked node
- if node[key] is not None:
- self.db.addjournal(link_class, node[key], 'unlink',
- (self.classname, nodeid, key))
-
- # register the link with the newly linked node
- if value is not None:
- self.db.addjournal(link_class, value, 'link',
- (self.classname, nodeid, key))
-
- elif isinstance(prop, Multilink):
- if type(value) != type([]):
- raise TypeError, 'new property "%s" not a list of ids'%key
- link_class = self.properties[key].classname
- l = []
- for entry in value:
- # if it isn't a number, it's a key
- if type(entry) != type(''):
- raise ValueError, 'link value must be String'
- if not num_re.match(entry):
- try:
- entry = self.db.classes[link_class].lookup(entry)
- except (TypeError, KeyError):
- raise IndexError, 'new property "%s": %s not a %s'%(
- key, entry, self.properties[key].classname)
- l.append(entry)
- value = l
- propvalues[key] = value
-
- #handle removals
- l = node[key]
- for id in l[:]:
- if id in value:
- continue
- # register the unlink with the old linked node
- self.db.addjournal(link_class, id, 'unlink',
- (self.classname, nodeid, key))
- l.remove(id)
-
- # handle additions
- for id in value:
- if not self.db.hasnode(link_class, id):
- raise IndexError, '%s has no node %s'%(link_class, id)
- if id in l:
- continue
- # register the link with the newly linked node
- self.db.addjournal(link_class, id, 'link',
- (self.classname, nodeid, key))
- l.append(id)
-
- elif isinstance(prop, String):
- if value is not None and type(value) != type(''):
- raise TypeError, 'new property "%s" not a string'%key
-
- elif isinstance(prop, Password):
- if not isinstance(value, password.Password):
- raise TypeError, 'new property "%s" not a Password'% key
- propvalues[key] = value = str(value)
-
- elif isinstance(prop, Date):
- if not isinstance(value, date.Date):
- raise TypeError, 'new property "%s" not a Date'% key
- propvalues[key] = value = value.get_tuple()
-
- elif isinstance(prop, Interval):
- if not isinstance(value, date.Interval):
- raise TypeError, 'new property "%s" not an Interval'% key
- propvalues[key] = value = value.get_tuple()
-
- node[key] = value
-
- self.db.setnode(self.classname, nodeid, node)
- self.db.addjournal(self.classname, nodeid, 'set', propvalues)
+ raise NotImplementedError
def retire(self, nodeid):
"""Retire a node.
Retired nodes are not returned by the find(), list(), or lookup()
methods, and other nodes may reuse the values of their key properties.
"""
- if self.db.journaltag is None:
- raise DatabaseError, 'Database open read-only'
- node = self.db.getnode(self.classname, nodeid)
- node[self.db.RETIRED_FLAG] = 1
- self.db.setnode(self.classname, nodeid, node)
- self.db.addjournal(self.classname, nodeid, 'retired', None)
+ raise NotImplementedError
def history(self, nodeid):
"""Retrieve the journal of edits on a particular node.
'date' is a Timestamp object specifying the time of the change and
'tag' is the journaltag specified when the database was opened.
"""
- return self.db.getjournal(self.classname, nodeid)
+ raise NotImplementedError
# Locating nodes:
+ def hasnode(self, nodeid):
+ '''Determine if the given nodeid actually exists
+ '''
+ raise NotImplementedError
def setkey(self, propname):
"""Select a String property of this class to be the key property.
None, or a TypeError is raised. The values of the key property on
all existing nodes must be unique or a ValueError is raised.
"""
- # TODO: validate that the property is a String!
- self.key = propname
+ raise NotImplementedError
def getkey(self):
"""Return the name of the key property for this class or None."""
- return self.key
+ raise NotImplementedError
def labelprop(self, default_to_id=0):
''' Return the property name for a label for the given node.
3. "title" property
4. first property from the sorted property name list
'''
- k = self.getkey()
- if k:
- return k
- props = self.getprops()
- if props.has_key('name'):
- return 'name'
- elif props.has_key('title'):
- return 'title'
- if default_to_id:
- return 'id'
- props = props.keys()
- props.sort()
- return props[0]
-
- # TODO: set up a separate index db file for this? profile?
+ raise NotImplementedError
+
def lookup(self, keyvalue):
"""Locate a particular node by its key property and return its id.
the nodes in this class, the matching node's id is returned;
otherwise a KeyError is raised.
"""
- cldb = self.db.getclassdb(self.classname)
- for nodeid in self.db.getnodeids(self.classname, cldb):
- node = self.db.getnode(self.classname, nodeid, cldb)
- if node.has_key(self.db.RETIRED_FLAG):
- continue
- if node[self.key] == keyvalue:
- return nodeid
- raise KeyError, keyvalue
+ raise NotImplementedError
# XXX: change from spec - allows multiple props to match
def find(self, **propspec):
- """Get the ids of nodes in this class which link to a given node.
+ """Get the ids of nodes in this class which link to the given nodes.
- 'propspec' consists of keyword args propname=nodeid
+ 'propspec' consists of keyword args propname={nodeid:1,}
'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.
- """
- propspec = propspec.items()
- for propname, nodeid in propspec:
- # check the prop is OK
- prop = self.properties[propname]
- if not isinstance(prop, Link) and not isinstance(prop, Multilink):
- raise TypeError, "'%s' not a Link/Multilink property"%propname
- if not self.db.hasnode(prop.classname, nodeid):
- raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
-
- # ok, now do the find
- cldb = self.db.getclassdb(self.classname)
- l = []
- for id in self.db.getnodeids(self.classname, cldb):
- node = self.db.getnode(self.classname, id, cldb)
- if node.has_key(self.db.RETIRED_FLAG):
- continue
- for propname, nodeid in propspec:
- property = node[propname]
- if isinstance(prop, Link) and nodeid == property:
- l.append(id)
- elif isinstance(prop, Multilink) and nodeid in property:
- l.append(id)
- return l
-
- def stringFind(self, **requirements):
- """Locate a particular node by matching a set of its String
- properties in a caseless search.
+ Any node in this class whose 'propname' property links to any of the
+ nodeids will be returned. Used by the full text indexing, which knows
+ that "foo" occurs in msg1, msg3 and file7, so we have hits on these
+ issues:
- If the property is not a String property, a TypeError is raised.
-
- The return is a list of the id of all nodes that match.
+ db.issue.find(messages={'1':1,'3':1}, files={'7':1})
"""
- for propname in requirements.keys():
- prop = self.properties[propname]
- if isinstance(not prop, String):
- raise TypeError, "'%s' not a String property"%propname
- requirements[propname] = requirements[propname].lower()
- l = []
- cldb = self.db.getclassdb(self.classname)
- for nodeid in self.db.getnodeids(self.classname, cldb):
- node = self.db.getnode(self.classname, nodeid, cldb)
- if node.has_key(self.db.RETIRED_FLAG):
- continue
- for key, value in requirements.items():
- if node[key] and node[key].lower() != value:
- break
- else:
- l.append(nodeid)
- return l
-
- def list(self):
- """Return a list of the ids of the active nodes in this class."""
- l = []
- cn = self.classname
- cldb = self.db.getclassdb(cn)
- for nodeid in self.db.getnodeids(cn, cldb):
- node = self.db.getnode(cn, nodeid, cldb)
- if node.has_key(self.db.RETIRED_FLAG):
- continue
- l.append(nodeid)
- l.sort()
- return l
+ raise NotImplementedError
# XXX not in spec
- def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
+ def filter(self, search_matches, filterspec, sort, group,
+ num_re = re.compile('^\d+$')):
''' Return a list of the ids of the active nodes in this class that
match the 'filter' spec, sorted by the group spec and then the
sort spec
'''
- cn = self.classname
-
- # optimise filterspec
- l = []
- props = self.getprops()
- for k, v in filterspec.items():
- propclass = props[k]
- if isinstance(propclass, Link):
- if type(v) is not type([]):
- v = [v]
- # replace key values with node ids
- u = []
- link_class = self.db.classes[propclass.classname]
- for entry in v:
- if entry == '-1': entry = None
- elif not num_re.match(entry):
- try:
- entry = link_class.lookup(entry)
- except (TypeError,KeyError):
- raise ValueError, 'property "%s": %s not a %s'%(
- k, entry, self.properties[k].classname)
- u.append(entry)
-
- l.append((0, k, u))
- elif isinstance(propclass, Multilink):
- if type(v) is not type([]):
- v = [v]
- # replace key values with node ids
- u = []
- link_class = self.db.classes[propclass.classname]
- for entry in v:
- if not num_re.match(entry):
- try:
- entry = link_class.lookup(entry)
- except (TypeError,KeyError):
- raise ValueError, 'new property "%s": %s not a %s'%(
- k, entry, self.properties[k].classname)
- u.append(entry)
- l.append((1, k, u))
- elif isinstance(propclass, String):
- # simple glob searching
- v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
- v = v.replace('?', '.')
- v = v.replace('*', '.*?')
- l.append((2, k, re.compile(v, re.I)))
- else:
- l.append((6, k, v))
- filterspec = l
-
- # now, find all the nodes that are active and pass filtering
- l = []
- cldb = self.db.getclassdb(cn)
- for nodeid in self.db.getnodeids(cn, cldb):
- node = self.db.getnode(cn, nodeid, cldb)
- if node.has_key(self.db.RETIRED_FLAG):
- continue
- # apply filter
- for t, k, v in filterspec:
- # this node doesn't have this property, so reject it
- if not node.has_key(k): break
-
- if t == 0 and node[k] not in v:
- # link - if this node'd property doesn't appear in the
- # filterspec's nodeid list, skip it
- break
- elif t == 1:
- # multilink - if any of the nodeids required by the
- # filterspec aren't in this node's property, then skip
- # it
- for value in v:
- if value not in node[k]:
- break
- else:
- continue
- break
- elif t == 2 and not v.search(node[k]):
- # RE search
- break
- elif t == 6 and node[k] != v:
- # straight value comparison for the other types
- break
- else:
- l.append((nodeid, node))
- l.sort()
-
- # optimise sort
- m = []
- for entry in sort:
- if entry[0] != '-':
- m.append(('+', entry))
- else:
- m.append((entry[0], entry[1:]))
- sort = m
-
- # optimise group
- m = []
- for entry in group:
- if entry[0] != '-':
- m.append(('+', entry))
- else:
- m.append((entry[0], entry[1:]))
- group = m
- # now, sort the result
- def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
- db = self.db, cl=self):
- a_id, an = a
- b_id, bn = b
- # sort by group and then sort
- for list in group, sort:
- for dir, prop in list:
- # sorting is class-specific
- propclass = properties[prop]
-
- # handle the properties that might be "faked"
- # also, handle possible missing properties
- try:
- if not an.has_key(prop):
- an[prop] = cl.get(a_id, prop)
- av = an[prop]
- except KeyError:
- # the node doesn't have a value for this property
- if isinstance(propclass, Multilink): av = []
- else: av = ''
- try:
- if not bn.has_key(prop):
- bn[prop] = cl.get(b_id, prop)
- bv = bn[prop]
- except KeyError:
- # the node doesn't have a value for this property
- if isinstance(propclass, Multilink): bv = []
- else: bv = ''
-
- # String and Date values are sorted in the natural way
- if isinstance(propclass, String):
- # clean up the strings
- if av and av[0] in string.uppercase:
- av = an[prop] = av.lower()
- if bv and bv[0] in string.uppercase:
- bv = bn[prop] = bv.lower()
- if (isinstance(propclass, String) or
- isinstance(propclass, Date)):
- # it might be a string that's really an integer
- try:
- av = int(av)
- bv = int(bv)
- except:
- pass
- if dir == '+':
- r = cmp(av, bv)
- if r != 0: return r
- elif dir == '-':
- r = cmp(bv, av)
- if r != 0: return r
-
- # 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.
- elif isinstance(propclass, Link):
- link = db.classes[propclass.classname]
- if av is None and bv is not None: return -1
- if av is not None and bv is None: return 1
- if av is None and bv is None: continue
- if link.getprops().has_key('order'):
- if dir == '+':
- r = cmp(link.get(av, 'order'),
- link.get(bv, 'order'))
- if r != 0: return r
- elif dir == '-':
- r = cmp(link.get(bv, 'order'),
- link.get(av, 'order'))
- if r != 0: return r
- elif link.getkey():
- key = link.getkey()
- if dir == '+':
- r = cmp(link.get(av, key), link.get(bv, key))
- if r != 0: return r
- elif dir == '-':
- r = cmp(link.get(bv, key), link.get(av, key))
- if r != 0: return r
- else:
- if dir == '+':
- r = cmp(av, bv)
- if r != 0: return r
- elif dir == '-':
- r = cmp(bv, av)
- if r != 0: return r
-
- # Multilink properties are sorted according to how many
- # links are present.
- elif isinstance(propclass, Multilink):
- if dir == '+':
- r = cmp(len(av), len(bv))
- if r != 0: return r
- elif dir == '-':
- r = cmp(len(bv), len(av))
- if r != 0: return r
- # end for dir, prop in list:
- # end for list in sort, group:
- # if all else fails, compare the ids
- return cmp(a[0], b[0])
-
- l.sort(sortfun)
- return [i[0] for i in l]
+ raise NotImplementedError
def count(self):
"""Get the number of nodes in this class.
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.
"""
- return self.db.countnodes(self.classname)
+ raise NotImplementedError
# Manipulating properties:
-
def getprops(self, protected=1):
"""Return a dictionary mapping property names to property objects.
If the "protected" flag is true, we include protected properties -
- those which may not be modified."""
- d = self.properties.copy()
- if protected:
- d['id'] = String()
- return d
+ those which may not be modified.
+ """
+ raise NotImplementedError
def addprop(self, **properties):
"""Add properties to this class.
may collide with the names of existing properties, or a ValueError
is raised before any properties have been added.
"""
- for key in properties.keys():
- if self.properties.has_key(key):
- raise ValueError, key
- self.properties.update(properties)
+ raise NotImplementedError
+ def index(self, nodeid):
+ '''Add (or refresh) the node to search indexes
+ '''
+ raise NotImplementedError
# XXX not in spec
class Node:
return self.cl.retire(self.nodeid)
-def Choice(name, *options):
- cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
+def Choice(name, db, *options):
+ '''Quick helper to create a simple class with choices
+ '''
+ cl = Class(db, name, name=String(), order=String())
for i in range(len(options)):
- cl.create(name=option[i], order=i)
+ cl.create(name=options[i], order=i)
return hyperdb.Link(name)
#
# $Log: not supported by cvs2svn $
+# Revision 1.75 2002/07/14 02:05:53 richard
+# . all storage-specific code (ie. backend) is now implemented by the backends
+#
+# Revision 1.74 2002/07/10 00:24:10 richard
+# braino
+#
+# Revision 1.73 2002/07/10 00:19:48 richard
+# Added explicit closing of backend database handles.
+#
+# Revision 1.72 2002/07/09 21:53:38 gmcm
+# Optimize Class.find so that the propspec can contain a set of ids to match.
+# This is used by indexer.search so it can do just one find for all the index matches.
+# This was already confusing code, but for common terms (lots of index matches),
+# it is enormously faster.
+#
+# Revision 1.71 2002/07/09 03:02:52 richard
+# More indexer work:
+# - all String properties may now be indexed too. Currently there's a bit of
+# "issue" specific code in the actual searching which needs to be
+# addressed. In a nutshell:
+# + pass 'indexme="yes"' as a String() property initialisation arg, eg:
+# file = FileClass(db, "file", name=String(), type=String(),
+# comment=String(indexme="yes"))
+# + the comment will then be indexed and be searchable, with the results
+# related back to the issue that the file is linked to
+# - as a result of this work, the FileClass has a default MIME type that may
+# be overridden in a subclass, or by the use of a "type" property as is
+# done in the default templates.
+# - the regeneration of the indexes (if necessary) is done once the schema is
+# set up in the dbinit.
+#
+# Revision 1.70 2002/06/27 12:06:20 gmcm
+# Improve an error message.
+#
+# Revision 1.69 2002/06/17 23:15:29 richard
+# Can debug to stdout now
+#
+# Revision 1.68 2002/06/11 06:52:03 richard
+# . #564271 ] find() and new properties
+#
+# Revision 1.67 2002/06/11 05:02:37 richard
+# . #565979 ] code error in hyperdb.Class.find
+#
+# Revision 1.66 2002/05/25 07:16:24 rochecompaan
+# Merged search_indexing-branch with HEAD
+#
+# Revision 1.65 2002/05/22 04:12:05 richard
+# . applied patch #558876 ] cgi client customization
+# ... with significant additions and modifications ;)
+# - extended handling of ML assignedto to all places it's handled
+# - added more NotFound info
+#
+# Revision 1.64 2002/05/15 06:21:21 richard
+# . node caching now works, and gives a small boost in performance
+#
+# As a part of this, I cleaned up the DEBUG output and implemented TRACE
+# output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
+# CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
+# (using if __debug__ which is compiled out with -O)
+#
+# Revision 1.63 2002/04/15 23:25:15 richard
+# . node ids are now generated from a lockable store - no more race conditions
+#
+# We're using the portalocker code by Jonathan Feinberg that was contributed
+# to the ASPN Python cookbook. This gives us locking across Unix and Windows.
+#
+# Revision 1.62 2002/04/03 07:05:50 richard
+# d'oh! killed retirement of nodes :(
+# all better now...
+#
+# Revision 1.61 2002/04/03 06:11:51 richard
+# Fix for old databases that contain properties that don't exist any more.
+#
+# Revision 1.60 2002/04/03 05:54:31 richard
+# Fixed serialisation problem by moving the serialisation step out of the
+# hyperdb.Class (get, set) into the hyperdb.Database.
+#
+# Also fixed htmltemplate after the showid changes I made yesterday.
+#
+# Unit tests for all of the above written.
+#
+# Revision 1.59.2.2 2002/04/20 13:23:33 rochecompaan
+# We now have a separate search page for nodes. Search links for
+# different classes can be customized in instance_config similar to
+# index links.
+#
+# Revision 1.59.2.1 2002/04/19 19:54:42 rochecompaan
+# cgi_client.py
+# removed search link for the time being
+# moved rendering of matches to htmltemplate
+# hyperdb.py
+# filtering of nodes on full text search incorporated in filter method
+# roundupdb.py
+# added paramater to call of filter method
+# roundup_indexer.py
+# added search method to RoundupIndexer class
+#
+# Revision 1.59 2002/03/12 22:52:26 richard
+# more pychecker warnings removed
+#
+# Revision 1.58 2002/02/27 03:23:16 richard
+# Ran it through pychecker, made fixes
+#
+# Revision 1.57 2002/02/20 05:23:24 richard
+# Didn't accomodate new values for new properties
+#
+# Revision 1.56 2002/02/20 05:05:28 richard
+# . Added simple editing for classes that don't define a templated interface.
+# - access using the admin "class list" interface
+# - limited to admin-only
+# - requires the csv module from object-craft (url given if it's missing)
+#
+# Revision 1.55 2002/02/15 07:27:12 richard
+# Oops, precedences around the way w0rng.
+#
+# Revision 1.54 2002/02/15 07:08:44 richard
+# . Alternate email addresses are now available for users. See the MIGRATION
+# file for info on how to activate the feature.
+#
+# Revision 1.53 2002/01/22 07:21:13 richard
+# . fixed back_bsddb so it passed the journal tests
+#
+# ... it didn't seem happy using the back_anydbm _open method, which is odd.
+# Yet another occurrance of whichdb not being able to recognise older bsddb
+# databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
+# process.
+#
+# Revision 1.52 2002/01/21 16:33:19 rochecompaan
+# You can now use the roundup-admin tool to pack the database
+#
+# Revision 1.51 2002/01/21 03:01:29 richard
+# brief docco on the do_journal argument
+#
+# Revision 1.50 2002/01/19 13:16:04 rochecompaan
+# Journal entries for link and multilink properties can now be switched on
+# or off.
+#
+# Revision 1.49 2002/01/16 07:02:57 richard
+# . lots of date/interval related changes:
+# - more relaxed date format for input
+#
+# Revision 1.48 2002/01/14 06:32:34 richard
+# . #502951 ] adding new properties to old database
+#
+# Revision 1.47 2002/01/14 02:20:15 richard
+# . changed all config accesses so they access either the instance or the
+# config attriubute on the db. This means that all config is obtained from
+# instance_config instead of the mish-mash of classes. This will make
+# switching to a ConfigParser setup easier too, I hope.
+#
+# At a minimum, this makes migration a _little_ easier (a lot easier in the
+# 0.5.0 switch, I hope!)
+#
+# Revision 1.46 2002/01/07 10:42:23 richard
+# oops
+#
# Revision 1.45 2002/01/02 04:18:17 richard
# hyperdb docstrings
#