e7d141c64c516dbfb1b49cb5c05b374140bdbf57
1 # $Id: rdbms_common.py,v 1.75 2004-02-11 23:55:09 richard Exp $
2 ''' Relational database (SQL) backend common code.
4 Basics:
6 - map roundup classes to relational tables
7 - automatically detect schema changes and modify the table schemas
8 appropriately (we store the "database version" of the schema in the
9 database itself as the only row of the "schema" table)
10 - multilinks (which represent a many-to-many relationship) are handled through
11 intermediate tables
12 - journals are stored adjunct to the per-class tables
13 - table names and columns have "_" prepended so the names can't clash with
14 restricted names (like "order")
15 - retirement is determined by the __retired__ column being true
17 Database-specific changes may generally be pushed out to the overridable
18 sql_* methods, since everything else should be fairly generic. There's
19 probably a bit of work to be done if a database is used that actually
20 honors column typing, since the initial databases don't (sqlite stores
21 everything as a string.)
22 '''
23 __docformat__ = 'restructuredtext'
25 # standard python modules
26 import sys, os, time, re, errno, weakref, copy
28 # roundup modules
29 from roundup import hyperdb, date, password, roundupdb, security
30 from roundup.hyperdb import String, Password, Date, Interval, Link, \
31 Multilink, DatabaseError, Boolean, Number, Node
32 from roundup.backends import locking
34 # support
35 from blobfiles import FileStorage
36 from roundup.indexer import Indexer
37 from sessions import Sessions, OneTimeKeys
38 from roundup.date import Range
40 # number of rows to keep in memory
41 ROW_CACHE_SIZE = 100
43 class Database(FileStorage, hyperdb.Database, roundupdb.Database):
44 ''' Wrapper around an SQL database that presents a hyperdb interface.
46 - some functionality is specific to the actual SQL database, hence
47 the sql_* methods that are NotImplemented
48 - we keep a cache of the latest ROW_CACHE_SIZE row fetches.
49 '''
50 def __init__(self, config, journaltag=None):
51 ''' Open the database and load the schema from it.
52 '''
53 self.config, self.journaltag = config, journaltag
54 self.dir = config.DATABASE
55 self.classes = {}
56 self.indexer = Indexer(self.dir)
57 self.sessions = Sessions(self.config)
58 self.otks = OneTimeKeys(self.config)
59 self.security = security.Security(self)
61 # additional transaction support for external files and the like
62 self.transactions = []
64 # keep a cache of the N most recently retrieved rows of any kind
65 # (classname, nodeid) = row
66 self.cache = {}
67 self.cache_lru = []
69 # database lock
70 self.lockfile = None
72 # open a connection to the database, creating the "conn" attribute
73 self.sql_open_connection()
75 def clearCache(self):
76 self.cache = {}
77 self.cache_lru = []
79 def sql_open_connection(self):
80 ''' Open a connection to the database, creating it if necessary
81 '''
82 raise NotImplemented
84 def sql(self, sql, args=None):
85 ''' Execute the sql with the optional args.
86 '''
87 if __debug__:
88 print >>hyperdb.DEBUG, (self, sql, args)
89 if args:
90 self.cursor.execute(sql, args)
91 else:
92 self.cursor.execute(sql)
94 def sql_fetchone(self):
95 ''' Fetch a single row. If there's nothing to fetch, return None.
96 '''
97 return self.cursor.fetchone()
99 def sql_fetchall(self):
100 ''' Fetch all rows. If there's nothing to fetch, return [].
101 '''
102 return self.cursor.fetchall()
104 def sql_stringquote(self, value):
105 ''' Quote the string so it's safe to put in the 'sql quotes'
106 '''
107 return re.sub("'", "''", str(value))
109 def save_dbschema(self, schema):
110 ''' Save the schema definition that the database currently implements
111 '''
112 s = repr(self.database_schema)
113 self.sql('insert into schema values (%s)', (s,))
115 def load_dbschema(self):
116 ''' Load the schema definition that the database currently implements
117 '''
118 self.cursor.execute('select schema from schema')
119 return eval(self.cursor.fetchone()[0])
121 def post_init(self):
122 ''' Called once the schema initialisation has finished.
124 We should now confirm that the schema defined by our "classes"
125 attribute actually matches the schema in the database.
126 '''
127 # now detect changes in the schema
128 save = 0
129 for classname, spec in self.classes.items():
130 if self.database_schema.has_key(classname):
131 dbspec = self.database_schema[classname]
132 if self.update_class(spec, dbspec):
133 self.database_schema[classname] = spec.schema()
134 save = 1
135 else:
136 self.create_class(spec)
137 self.database_schema[classname] = spec.schema()
138 save = 1
140 for classname, spec in self.database_schema.items():
141 if not self.classes.has_key(classname):
142 self.drop_class(classname, spec)
143 del self.database_schema[classname]
144 save = 1
146 # update the database version of the schema
147 if save:
148 self.sql('delete from schema')
149 self.save_dbschema(self.database_schema)
151 # reindex the db if necessary
152 if self.indexer.should_reindex():
153 self.reindex()
155 # commit
156 self.conn.commit()
158 def refresh_database(self):
159 self.post_init()
161 def reindex(self):
162 for klass in self.classes.values():
163 for nodeid in klass.list():
164 klass.index(nodeid)
165 self.indexer.save_index()
167 def determine_columns(self, properties):
168 ''' Figure the column names and multilink properties from the spec
170 "properties" is a list of (name, prop) where prop may be an
171 instance of a hyperdb "type" _or_ a string repr of that type.
172 '''
173 cols = ['_activity', '_creator', '_creation']
174 mls = []
175 # add the multilinks separately
176 for col, prop in properties:
177 if isinstance(prop, Multilink):
178 mls.append(col)
179 elif isinstance(prop, type('')) and prop.find('Multilink') != -1:
180 mls.append(col)
181 else:
182 cols.append('_'+col)
183 cols.sort()
184 return cols, mls
186 def update_class(self, spec, old_spec, force=0):
187 ''' Determine the differences between the current spec and the
188 database version of the spec, and update where necessary.
190 If 'force' is true, update the database anyway.
191 '''
192 new_has = spec.properties.has_key
193 new_spec = spec.schema()
194 new_spec[1].sort()
195 old_spec[1].sort()
196 if not force and new_spec == old_spec:
197 # no changes
198 return 0
200 if __debug__:
201 print >>hyperdb.DEBUG, 'update_class FIRING'
203 # detect multilinks that have been removed, and drop their table
204 old_has = {}
205 for name,prop in old_spec[1]:
206 old_has[name] = 1
207 if new_has(name) or not isinstance(prop, Multilink):
208 continue
209 # it's a multilink, and it's been removed - drop the old
210 # table. First drop indexes.
211 self.drop_multilink_table_indexes(spec.classname, ml)
212 sql = 'drop table %s_%s'%(spec.classname, prop)
213 if __debug__:
214 print >>hyperdb.DEBUG, 'update_class', (self, sql)
215 self.cursor.execute(sql)
216 old_has = old_has.has_key
218 # now figure how we populate the new table
219 fetch = ['_activity', '_creation', '_creator']
220 properties = spec.getprops()
221 for propname,x in new_spec[1]:
222 prop = properties[propname]
223 if isinstance(prop, Multilink):
224 if force or not old_has(propname):
225 # we need to create the new table
226 self.create_multilink_table(spec, propname)
227 elif old_has(propname):
228 # we copy this col over from the old table
229 fetch.append('_'+propname)
231 # select the data out of the old table
232 fetch.append('id')
233 fetch.append('__retired__')
234 fetchcols = ','.join(fetch)
235 cn = spec.classname
236 sql = 'select %s from _%s'%(fetchcols, cn)
237 if __debug__:
238 print >>hyperdb.DEBUG, 'update_class', (self, sql)
239 self.cursor.execute(sql)
240 olddata = self.cursor.fetchall()
242 # TODO: update all the other index dropping code
243 self.drop_class_table_indexes(cn, old_spec[0])
245 # drop the old table
246 self.cursor.execute('drop table _%s'%cn)
248 # create the new table
249 self.create_class_table(spec)
251 if olddata:
252 # do the insert
253 args = ','.join([self.arg for x in fetch])
254 sql = 'insert into _%s (%s) values (%s)'%(cn, fetchcols, args)
255 if __debug__:
256 print >>hyperdb.DEBUG, 'update_class', (self, sql, olddata[0])
257 for entry in olddata:
258 self.cursor.execute(sql, tuple(entry))
260 return 1
262 def create_class_table(self, spec):
263 ''' create the class table for the given spec
264 '''
265 cols, mls = self.determine_columns(spec.properties.items())
267 # add on our special columns
268 cols.append('id')
269 cols.append('__retired__')
271 # create the base table
272 scols = ','.join(['%s varchar'%x for x in cols])
273 sql = 'create table _%s (%s)'%(spec.classname, scols)
274 if __debug__:
275 print >>hyperdb.DEBUG, 'create_class', (self, sql)
276 self.cursor.execute(sql)
278 self.create_class_table_indexes(spec)
280 return cols, mls
282 def create_class_table_indexes(self, spec):
283 ''' create the class table for the given spec
284 '''
285 # create id index
286 index_sql1 = 'create index _%s_id_idx on _%s(id)'%(
287 spec.classname, spec.classname)
288 if __debug__:
289 print >>hyperdb.DEBUG, 'create_index', (self, index_sql1)
290 self.cursor.execute(index_sql1)
292 # create __retired__ index
293 index_sql2 = 'create index _%s_retired_idx on _%s(__retired__)'%(
294 spec.classname, spec.classname)
295 if __debug__:
296 print >>hyperdb.DEBUG, 'create_index', (self, index_sql2)
297 self.cursor.execute(index_sql2)
299 # create index for key property
300 if spec.key:
301 if __debug__:
302 print >>hyperdb.DEBUG, 'update_class setting keyprop %r'% \
303 spec.key
304 index_sql3 = 'create index _%s_%s_idx on _%s(_%s)'%(
305 spec.classname, spec.key,
306 spec.classname, spec.key)
307 if __debug__:
308 print >>hyperdb.DEBUG, 'create_index', (self, index_sql3)
309 self.cursor.execute(index_sql3)
311 def drop_class_table_indexes(self, cn, key):
312 # drop the old table indexes first
313 l = ['_%s_id_idx'%cn, '_%s_retired_idx'%cn]
314 if key:
315 # key prop too?
316 l.append('_%s_%s_idx'%(cn, key))
318 # TODO: update all the other index dropping code
319 table_name = '_%s'%cn
320 for index_name in l:
321 if not self.sql_index_exists(table_name, index_name):
322 continue
323 index_sql = 'drop index '+index_name
324 if __debug__:
325 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
326 self.cursor.execute(index_sql)
328 def create_journal_table(self, spec):
329 ''' create the journal table for a class given the spec and
330 already-determined cols
331 '''
332 # journal table
333 cols = ','.join(['%s varchar'%x
334 for x in 'nodeid date tag action params'.split()])
335 sql = 'create table %s__journal (%s)'%(spec.classname, cols)
336 if __debug__:
337 print >>hyperdb.DEBUG, 'create_class', (self, sql)
338 self.cursor.execute(sql)
339 self.create_journal_table_indexes(spec)
341 def create_journal_table_indexes(self, spec):
342 # index on nodeid
343 index_sql = 'create index %s_journ_idx on %s__journal(nodeid)'%(
344 spec.classname, spec.classname)
345 if __debug__:
346 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
347 self.cursor.execute(index_sql)
349 def drop_journal_table_indexes(self, classname):
350 index_name = '%s_journ_idx'%classname
351 if not self.sql_index_exists('%s__journal'%classname, index_name):
352 return
353 index_sql = 'drop index '+index_name
354 if __debug__:
355 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
356 self.cursor.execute(index_sql)
358 def create_multilink_table(self, spec, ml):
359 ''' Create a multilink table for the "ml" property of the class
360 given by the spec
361 '''
362 # create the table
363 sql = 'create table %s_%s (linkid varchar, nodeid varchar)'%(
364 spec.classname, ml)
365 if __debug__:
366 print >>hyperdb.DEBUG, 'create_class', (self, sql)
367 self.cursor.execute(sql)
368 self.create_multilink_table_indexes(spec, ml)
370 def create_multilink_table_indexes(self, spec, ml):
371 # create index on linkid
372 index_sql = 'create index %s_%s_l_idx on %s_%s(linkid)'%(
373 spec.classname, ml, spec.classname, ml)
374 if __debug__:
375 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
376 self.cursor.execute(index_sql)
378 # create index on nodeid
379 index_sql = 'create index %s_%s_n_idx on %s_%s(nodeid)'%(
380 spec.classname, ml, spec.classname, ml)
381 if __debug__:
382 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
383 self.cursor.execute(index_sql)
385 def drop_multilink_table_indexes(self, classname, ml):
386 l = [
387 '%s_%s_l_idx'%(classname, ml),
388 '%s_%s_n_idx'%(classname, ml)
389 ]
390 table_name = '%s_%s'%(classname, ml)
391 for index_name in l:
392 if not self.sql_index_exists(table_name, index_name):
393 continue
394 index_sql = 'drop index %s'%index_name
395 if __debug__:
396 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
397 self.cursor.execute(index_sql)
399 def create_class(self, spec):
400 ''' Create a database table according to the given spec.
401 '''
402 cols, mls = self.create_class_table(spec)
403 self.create_journal_table(spec)
405 # now create the multilink tables
406 for ml in mls:
407 self.create_multilink_table(spec, ml)
409 # ID counter
410 sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
411 vals = (spec.classname, 1)
412 if __debug__:
413 print >>hyperdb.DEBUG, 'create_class', (self, sql, vals)
414 self.cursor.execute(sql, vals)
416 def drop_class(self, cn, spec):
417 ''' Drop the given table from the database.
419 Drop the journal and multilink tables too.
420 '''
421 properties = spec[1]
422 # figure the multilinks
423 mls = []
424 for propanme, prop in properties:
425 if isinstance(prop, Multilink):
426 mls.append(propname)
428 # drop class table and indexes
429 self.drop_class_table_indexes(cn, spec[0])
430 sql = 'drop table _%s'%cn
431 if __debug__:
432 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
433 self.cursor.execute(sql)
435 # drop journal table and indexes
436 self.drop_journal_table_indexes(cn)
437 sql = 'drop table %s__journal'%cn
438 if __debug__:
439 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
440 self.cursor.execute(sql)
442 for ml in mls:
443 # drop multilink table and indexes
444 self.drop_multilink_table_indexes(cn, ml)
445 sql = 'drop table %s_%s'%(spec.classname, ml)
446 if __debug__:
447 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
448 self.cursor.execute(sql)
450 #
451 # Classes
452 #
453 def __getattr__(self, classname):
454 ''' A convenient way of calling self.getclass(classname).
455 '''
456 if self.classes.has_key(classname):
457 if __debug__:
458 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
459 return self.classes[classname]
460 raise AttributeError, classname
462 def addclass(self, cl):
463 ''' Add a Class to the hyperdatabase.
464 '''
465 if __debug__:
466 print >>hyperdb.DEBUG, 'addclass', (self, cl)
467 cn = cl.classname
468 if self.classes.has_key(cn):
469 raise ValueError, cn
470 self.classes[cn] = cl
472 def getclasses(self):
473 ''' Return a list of the names of all existing classes.
474 '''
475 if __debug__:
476 print >>hyperdb.DEBUG, 'getclasses', (self,)
477 l = self.classes.keys()
478 l.sort()
479 return l
481 def getclass(self, classname):
482 '''Get the Class object representing a particular class.
484 If 'classname' is not a valid class name, a KeyError is raised.
485 '''
486 if __debug__:
487 print >>hyperdb.DEBUG, 'getclass', (self, classname)
488 try:
489 return self.classes[classname]
490 except KeyError:
491 raise KeyError, 'There is no class called "%s"'%classname
493 def clear(self):
494 '''Delete all database contents.
496 Note: I don't commit here, which is different behaviour to the
497 "nuke from orbit" behaviour in the dbs.
498 '''
499 if __debug__:
500 print >>hyperdb.DEBUG, 'clear', (self,)
501 for cn in self.classes.keys():
502 sql = 'delete from _%s'%cn
503 if __debug__:
504 print >>hyperdb.DEBUG, 'clear', (self, sql)
505 self.cursor.execute(sql)
507 #
508 # Node IDs
509 #
510 def newid(self, classname):
511 ''' Generate a new id for the given class
512 '''
513 # get the next ID
514 sql = 'select num from ids where name=%s'%self.arg
515 if __debug__:
516 print >>hyperdb.DEBUG, 'newid', (self, sql, classname)
517 self.cursor.execute(sql, (classname, ))
518 newid = self.cursor.fetchone()[0]
520 # update the counter
521 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
522 vals = (int(newid)+1, classname)
523 if __debug__:
524 print >>hyperdb.DEBUG, 'newid', (self, sql, vals)
525 self.cursor.execute(sql, vals)
527 # return as string
528 return str(newid)
530 def setid(self, classname, setid):
531 ''' Set the id counter: used during import of database
532 '''
533 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
534 vals = (setid, classname)
535 if __debug__:
536 print >>hyperdb.DEBUG, 'setid', (self, sql, vals)
537 self.cursor.execute(sql, vals)
539 #
540 # Nodes
541 #
542 def addnode(self, classname, nodeid, node):
543 ''' Add the specified node to its class's db.
544 '''
545 if __debug__:
546 print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
548 # determine the column definitions and multilink tables
549 cl = self.classes[classname]
550 cols, mls = self.determine_columns(cl.properties.items())
552 # we'll be supplied these props if we're doing an import
553 if not node.has_key('creator'):
554 # add in the "calculated" properties (dupe so we don't affect
555 # calling code's node assumptions)
556 node = node.copy()
557 node['creation'] = node['activity'] = date.Date()
558 node['creator'] = self.getuid()
560 # default the non-multilink columns
561 for col, prop in cl.properties.items():
562 if not node.has_key(col):
563 if isinstance(prop, Multilink):
564 node[col] = []
565 else:
566 node[col] = None
568 # clear this node out of the cache if it's in there
569 key = (classname, nodeid)
570 if self.cache.has_key(key):
571 del self.cache[key]
572 self.cache_lru.remove(key)
574 # make the node data safe for the DB
575 node = self.serialise(classname, node)
577 # make sure the ordering is correct for column name -> column value
578 vals = tuple([node[col[1:]] for col in cols]) + (nodeid, 0)
579 s = ','.join([self.arg for x in cols]) + ',%s,%s'%(self.arg, self.arg)
580 cols = ','.join(cols) + ',id,__retired__'
582 # perform the inserts
583 sql = 'insert into _%s (%s) values (%s)'%(classname, cols, s)
584 if __debug__:
585 print >>hyperdb.DEBUG, 'addnode', (self, sql, vals)
586 self.cursor.execute(sql, vals)
588 # insert the multilink rows
589 for col in mls:
590 t = '%s_%s'%(classname, col)
591 for entry in node[col]:
592 sql = 'insert into %s (linkid, nodeid) values (%s,%s)'%(t,
593 self.arg, self.arg)
594 self.sql(sql, (entry, nodeid))
596 # make sure we do the commit-time extra stuff for this node
597 self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
599 def setnode(self, classname, nodeid, values, multilink_changes):
600 ''' Change the specified node.
601 '''
602 if __debug__:
603 print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, values)
605 # clear this node out of the cache if it's in there
606 key = (classname, nodeid)
607 if self.cache.has_key(key):
608 del self.cache[key]
609 self.cache_lru.remove(key)
611 # add the special props
612 values = values.copy()
613 values['activity'] = date.Date()
615 # make db-friendly
616 values = self.serialise(classname, values)
618 cl = self.classes[classname]
619 cols = []
620 mls = []
621 # add the multilinks separately
622 props = cl.getprops()
623 for col in values.keys():
624 prop = props[col]
625 if isinstance(prop, Multilink):
626 mls.append(col)
627 else:
628 cols.append('_'+col)
629 cols.sort()
631 # if there's any updates to regular columns, do them
632 if cols:
633 # make sure the ordering is correct for column name -> column value
634 sqlvals = tuple([values[col[1:]] for col in cols]) + (nodeid,)
635 s = ','.join(['%s=%s'%(x, self.arg) for x in cols])
636 cols = ','.join(cols)
638 # perform the update
639 sql = 'update _%s set %s where id=%s'%(classname, s, self.arg)
640 if __debug__:
641 print >>hyperdb.DEBUG, 'setnode', (self, sql, sqlvals)
642 self.cursor.execute(sql, sqlvals)
644 # now the fun bit, updating the multilinks ;)
645 for col, (add, remove) in multilink_changes.items():
646 tn = '%s_%s'%(classname, col)
647 if add:
648 sql = 'insert into %s (nodeid, linkid) values (%s,%s)'%(tn,
649 self.arg, self.arg)
650 for addid in add:
651 self.sql(sql, (nodeid, addid))
652 if remove:
653 sql = 'delete from %s where nodeid=%s and linkid=%s'%(tn,
654 self.arg, self.arg)
655 for removeid in remove:
656 self.sql(sql, (nodeid, removeid))
658 # make sure we do the commit-time extra stuff for this node
659 self.transactions.append((self.doSaveNode, (classname, nodeid, values)))
661 def getnode(self, classname, nodeid):
662 ''' Get a node from the database.
663 '''
664 if __debug__:
665 print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid)
667 # see if we have this node cached
668 key = (classname, nodeid)
669 if self.cache.has_key(key):
670 # push us back to the top of the LRU
671 self.cache_lru.remove(key)
672 self.cache_lru.insert(0, key)
673 # return the cached information
674 return self.cache[key]
676 # figure the columns we're fetching
677 cl = self.classes[classname]
678 cols, mls = self.determine_columns(cl.properties.items())
679 scols = ','.join(cols)
681 # perform the basic property fetch
682 sql = 'select %s from _%s where id=%s'%(scols, classname, self.arg)
683 self.sql(sql, (nodeid,))
685 values = self.sql_fetchone()
686 if values is None:
687 raise IndexError, 'no such %s node %s'%(classname, nodeid)
689 # make up the node
690 node = {}
691 for col in range(len(cols)):
692 node[cols[col][1:]] = values[col]
694 # now the multilinks
695 for col in mls:
696 # get the link ids
697 sql = 'select linkid from %s_%s where nodeid=%s'%(classname, col,
698 self.arg)
699 self.cursor.execute(sql, (nodeid,))
700 # extract the first column from the result
701 node[col] = [x[0] for x in self.cursor.fetchall()]
703 # un-dbificate the node data
704 node = self.unserialise(classname, node)
706 # save off in the cache
707 key = (classname, nodeid)
708 self.cache[key] = node
709 # update the LRU
710 self.cache_lru.insert(0, key)
711 if len(self.cache_lru) > ROW_CACHE_SIZE:
712 del self.cache[self.cache_lru.pop()]
714 return node
716 def destroynode(self, classname, nodeid):
717 '''Remove a node from the database. Called exclusively by the
718 destroy() method on Class.
719 '''
720 if __debug__:
721 print >>hyperdb.DEBUG, 'destroynode', (self, classname, nodeid)
723 # make sure the node exists
724 if not self.hasnode(classname, nodeid):
725 raise IndexError, '%s has no node %s'%(classname, nodeid)
727 # see if we have this node cached
728 if self.cache.has_key((classname, nodeid)):
729 del self.cache[(classname, nodeid)]
731 # see if there's any obvious commit actions that we should get rid of
732 for entry in self.transactions[:]:
733 if entry[1][:2] == (classname, nodeid):
734 self.transactions.remove(entry)
736 # now do the SQL
737 sql = 'delete from _%s where id=%s'%(classname, self.arg)
738 self.sql(sql, (nodeid,))
740 # remove from multilnks
741 cl = self.getclass(classname)
742 x, mls = self.determine_columns(cl.properties.items())
743 for col in mls:
744 # get the link ids
745 sql = 'delete from %s_%s where nodeid=%s'%(classname, col, self.arg)
746 self.sql(sql, (nodeid,))
748 # remove journal entries
749 sql = 'delete from %s__journal where nodeid=%s'%(classname, self.arg)
750 self.sql(sql, (nodeid,))
752 def serialise(self, classname, node):
753 '''Copy the node contents, converting non-marshallable data into
754 marshallable data.
755 '''
756 if __debug__:
757 print >>hyperdb.DEBUG, 'serialise', classname, node
758 properties = self.getclass(classname).getprops()
759 d = {}
760 for k, v in node.items():
761 # if the property doesn't exist, or is the "retired" flag then
762 # it won't be in the properties dict
763 if not properties.has_key(k):
764 d[k] = v
765 continue
767 # get the property spec
768 prop = properties[k]
770 if isinstance(prop, Password) and v is not None:
771 d[k] = str(v)
772 elif isinstance(prop, Date) and v is not None:
773 d[k] = v.serialise()
774 elif isinstance(prop, Interval) and v is not None:
775 d[k] = v.serialise()
776 else:
777 d[k] = v
778 return d
780 def unserialise(self, classname, node):
781 '''Decode the marshalled node data
782 '''
783 if __debug__:
784 print >>hyperdb.DEBUG, 'unserialise', classname, node
785 properties = self.getclass(classname).getprops()
786 d = {}
787 for k, v in node.items():
788 # if the property doesn't exist, or is the "retired" flag then
789 # it won't be in the properties dict
790 if not properties.has_key(k):
791 d[k] = v
792 continue
794 # get the property spec
795 prop = properties[k]
797 if isinstance(prop, Date) and v is not None:
798 d[k] = date.Date(v)
799 elif isinstance(prop, Interval) and v is not None:
800 d[k] = date.Interval(v)
801 elif isinstance(prop, Password) and v is not None:
802 p = password.Password()
803 p.unpack(v)
804 d[k] = p
805 elif isinstance(prop, Boolean) and v is not None:
806 d[k] = int(v)
807 elif isinstance(prop, Number) and v is not None:
808 # try int first, then assume it's a float
809 try:
810 d[k] = int(v)
811 except ValueError:
812 d[k] = float(v)
813 else:
814 d[k] = v
815 return d
817 def hasnode(self, classname, nodeid):
818 ''' Determine if the database has a given node.
819 '''
820 sql = 'select count(*) from _%s where id=%s'%(classname, self.arg)
821 if __debug__:
822 print >>hyperdb.DEBUG, 'hasnode', (self, sql, nodeid)
823 self.cursor.execute(sql, (nodeid,))
824 return int(self.cursor.fetchone()[0])
826 def countnodes(self, classname):
827 ''' Count the number of nodes that exist for a particular Class.
828 '''
829 sql = 'select count(*) from _%s'%classname
830 if __debug__:
831 print >>hyperdb.DEBUG, 'countnodes', (self, sql)
832 self.cursor.execute(sql)
833 return self.cursor.fetchone()[0]
835 def addjournal(self, classname, nodeid, action, params, creator=None,
836 creation=None):
837 ''' Journal the Action
838 'action' may be:
840 'create' or 'set' -- 'params' is a dictionary of property values
841 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
842 'retire' -- 'params' is None
843 '''
844 # serialise the parameters now if necessary
845 if isinstance(params, type({})):
846 if action in ('set', 'create'):
847 params = self.serialise(classname, params)
849 # handle supply of the special journalling parameters (usually
850 # supplied on importing an existing database)
851 if creator:
852 journaltag = creator
853 else:
854 journaltag = self.getuid()
855 if creation:
856 journaldate = creation.serialise()
857 else:
858 journaldate = date.Date().serialise()
860 # create the journal entry
861 cols = ','.join('nodeid date tag action params'.split())
863 if __debug__:
864 print >>hyperdb.DEBUG, 'addjournal', (nodeid, journaldate,
865 journaltag, action, params)
867 self.save_journal(classname, cols, nodeid, journaldate,
868 journaltag, action, params)
870 def getjournal(self, classname, nodeid):
871 ''' get the journal for id
872 '''
873 # make sure the node exists
874 if not self.hasnode(classname, nodeid):
875 raise IndexError, '%s has no node %s'%(classname, nodeid)
877 cols = ','.join('nodeid date tag action params'.split())
878 return self.load_journal(classname, cols, nodeid)
880 def save_journal(self, classname, cols, nodeid, journaldate,
881 journaltag, action, params):
882 ''' Save the journal entry to the database
883 '''
884 # make the params db-friendly
885 params = repr(params)
886 entry = (nodeid, journaldate, journaltag, action, params)
888 # do the insert
889 a = self.arg
890 sql = 'insert into %s__journal (%s) values (%s,%s,%s,%s,%s)'%(classname,
891 cols, a, a, a, a, a)
892 if __debug__:
893 print >>hyperdb.DEBUG, 'addjournal', (self, sql, entry)
894 self.cursor.execute(sql, entry)
896 def load_journal(self, classname, cols, nodeid):
897 ''' Load the journal from the database
898 '''
899 # now get the journal entries
900 sql = 'select %s from %s__journal where nodeid=%s'%(cols, classname,
901 self.arg)
902 if __debug__:
903 print >>hyperdb.DEBUG, 'load_journal', (self, sql, nodeid)
904 self.cursor.execute(sql, (nodeid,))
905 res = []
906 for nodeid, date_stamp, user, action, params in self.cursor.fetchall():
907 params = eval(params)
908 res.append((nodeid, date.Date(date_stamp), user, action, params))
909 return res
911 def pack(self, pack_before):
912 ''' Delete all journal entries except "create" before 'pack_before'.
913 '''
914 # get a 'yyyymmddhhmmss' version of the date
915 date_stamp = pack_before.serialise()
917 # do the delete
918 for classname in self.classes.keys():
919 sql = "delete from %s__journal where date<%s and "\
920 "action<>'create'"%(classname, self.arg)
921 if __debug__:
922 print >>hyperdb.DEBUG, 'pack', (self, sql, date_stamp)
923 self.cursor.execute(sql, (date_stamp,))
925 def sql_commit(self):
926 ''' Actually commit to the database.
927 '''
928 self.conn.commit()
930 def commit(self):
931 ''' Commit the current transactions.
933 Save all data changed since the database was opened or since the
934 last commit() or rollback().
935 '''
936 if __debug__:
937 print >>hyperdb.DEBUG, 'commit', (self,)
939 # commit the database
940 self.sql_commit()
942 # now, do all the other transaction stuff
943 reindex = {}
944 for method, args in self.transactions:
945 reindex[method(*args)] = 1
947 # reindex the nodes that request it
948 for classname, nodeid in filter(None, reindex.keys()):
949 print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
950 self.getclass(classname).index(nodeid)
952 # save the indexer state
953 self.indexer.save_index()
955 # clear out the transactions
956 self.transactions = []
958 def sql_rollback(self):
959 self.conn.rollback()
961 def rollback(self):
962 ''' Reverse all actions from the current transaction.
964 Undo all the changes made since the database was opened or the last
965 commit() or rollback() was performed.
966 '''
967 if __debug__:
968 print >>hyperdb.DEBUG, 'rollback', (self,)
970 self.sql_rollback()
972 # roll back "other" transaction stuff
973 for method, args in self.transactions:
974 # delete temporary files
975 if method == self.doStoreFile:
976 self.rollbackStoreFile(*args)
977 self.transactions = []
979 # clear the cache
980 self.clearCache()
982 def doSaveNode(self, classname, nodeid, node):
983 ''' dummy that just generates a reindex event
984 '''
985 # return the classname, nodeid so we reindex this content
986 return (classname, nodeid)
988 def sql_close(self):
989 self.conn.close()
991 def close(self):
992 ''' Close off the connection.
993 '''
994 self.sql_close()
995 if self.lockfile is not None:
996 locking.release_lock(self.lockfile)
997 if self.lockfile is not None:
998 self.lockfile.close()
999 self.lockfile = None
1001 #
1002 # The base Class class
1003 #
1004 class Class(hyperdb.Class):
1005 ''' The handle to a particular class of nodes in a hyperdatabase.
1007 All methods except __repr__ and getnode must be implemented by a
1008 concrete backend Class.
1009 '''
1011 def __init__(self, db, classname, **properties):
1012 '''Create a new class with a given name and property specification.
1014 'classname' must not collide with the name of an existing class,
1015 or a ValueError is raised. The keyword arguments in 'properties'
1016 must map names to property objects, or a TypeError is raised.
1017 '''
1018 if (properties.has_key('creation') or properties.has_key('activity')
1019 or properties.has_key('creator')):
1020 raise ValueError, '"creation", "activity" and "creator" are '\
1021 'reserved'
1023 self.classname = classname
1024 self.properties = properties
1025 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
1026 self.key = ''
1028 # should we journal changes (default yes)
1029 self.do_journal = 1
1031 # do the db-related init stuff
1032 db.addclass(self)
1034 self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
1035 self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
1037 def schema(self):
1038 ''' A dumpable version of the schema that we can store in the
1039 database
1040 '''
1041 return (self.key, [(x, repr(y)) for x,y in self.properties.items()])
1043 def enableJournalling(self):
1044 '''Turn journalling on for this class
1045 '''
1046 self.do_journal = 1
1048 def disableJournalling(self):
1049 '''Turn journalling off for this class
1050 '''
1051 self.do_journal = 0
1053 # Editing nodes:
1054 def create(self, **propvalues):
1055 ''' Create a new node of this class and return its id.
1057 The keyword arguments in 'propvalues' map property names to values.
1059 The values of arguments must be acceptable for the types of their
1060 corresponding properties or a TypeError is raised.
1062 If this class has a key property, it must be present and its value
1063 must not collide with other key strings or a ValueError is raised.
1065 Any other properties on this class that are missing from the
1066 'propvalues' dictionary are set to None.
1068 If an id in a link or multilink property does not refer to a valid
1069 node, an IndexError is raised.
1070 '''
1071 self.fireAuditors('create', None, propvalues)
1072 newid = self.create_inner(**propvalues)
1073 self.fireReactors('create', newid, None)
1074 return newid
1076 def create_inner(self, **propvalues):
1077 ''' Called by create, in-between the audit and react calls.
1078 '''
1079 if propvalues.has_key('id'):
1080 raise KeyError, '"id" is reserved'
1082 if self.db.journaltag is None:
1083 raise DatabaseError, 'Database open read-only'
1085 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1086 raise KeyError, '"creation" and "activity" are reserved'
1088 # new node's id
1089 newid = self.db.newid(self.classname)
1091 # validate propvalues
1092 num_re = re.compile('^\d+$')
1093 for key, value in propvalues.items():
1094 if key == self.key:
1095 try:
1096 self.lookup(value)
1097 except KeyError:
1098 pass
1099 else:
1100 raise ValueError, 'node with key "%s" exists'%value
1102 # try to handle this property
1103 try:
1104 prop = self.properties[key]
1105 except KeyError:
1106 raise KeyError, '"%s" has no property "%s"'%(self.classname,
1107 key)
1109 if value is not None and isinstance(prop, Link):
1110 if type(value) != type(''):
1111 raise ValueError, 'link value must be String'
1112 link_class = self.properties[key].classname
1113 # if it isn't a number, it's a key
1114 if not num_re.match(value):
1115 try:
1116 value = self.db.classes[link_class].lookup(value)
1117 except (TypeError, KeyError):
1118 raise IndexError, 'new property "%s": %s not a %s'%(
1119 key, value, link_class)
1120 elif not self.db.getclass(link_class).hasnode(value):
1121 raise IndexError, '%s has no node %s'%(link_class, value)
1123 # save off the value
1124 propvalues[key] = value
1126 # register the link with the newly linked node
1127 if self.do_journal and self.properties[key].do_journal:
1128 self.db.addjournal(link_class, value, 'link',
1129 (self.classname, newid, key))
1131 elif isinstance(prop, Multilink):
1132 if type(value) != type([]):
1133 raise TypeError, 'new property "%s" not a list of ids'%key
1135 # clean up and validate the list of links
1136 link_class = self.properties[key].classname
1137 l = []
1138 for entry in value:
1139 if type(entry) != type(''):
1140 raise ValueError, '"%s" multilink value (%r) '\
1141 'must contain Strings'%(key, value)
1142 # if it isn't a number, it's a key
1143 if not num_re.match(entry):
1144 try:
1145 entry = self.db.classes[link_class].lookup(entry)
1146 except (TypeError, KeyError):
1147 raise IndexError, 'new property "%s": %s not a %s'%(
1148 key, entry, self.properties[key].classname)
1149 l.append(entry)
1150 value = l
1151 propvalues[key] = value
1153 # handle additions
1154 for nodeid in value:
1155 if not self.db.getclass(link_class).hasnode(nodeid):
1156 raise IndexError, '%s has no node %s'%(link_class,
1157 nodeid)
1158 # register the link with the newly linked node
1159 if self.do_journal and self.properties[key].do_journal:
1160 self.db.addjournal(link_class, nodeid, 'link',
1161 (self.classname, newid, key))
1163 elif isinstance(prop, String):
1164 if type(value) != type('') and type(value) != type(u''):
1165 raise TypeError, 'new property "%s" not a string'%key
1167 elif isinstance(prop, Password):
1168 if not isinstance(value, password.Password):
1169 raise TypeError, 'new property "%s" not a Password'%key
1171 elif isinstance(prop, Date):
1172 if value is not None and not isinstance(value, date.Date):
1173 raise TypeError, 'new property "%s" not a Date'%key
1175 elif isinstance(prop, Interval):
1176 if value is not None and not isinstance(value, date.Interval):
1177 raise TypeError, 'new property "%s" not an Interval'%key
1179 elif value is not None and isinstance(prop, Number):
1180 try:
1181 float(value)
1182 except ValueError:
1183 raise TypeError, 'new property "%s" not numeric'%key
1185 elif value is not None and isinstance(prop, Boolean):
1186 try:
1187 int(value)
1188 except ValueError:
1189 raise TypeError, 'new property "%s" not boolean'%key
1191 # make sure there's data where there needs to be
1192 for key, prop in self.properties.items():
1193 if propvalues.has_key(key):
1194 continue
1195 if key == self.key:
1196 raise ValueError, 'key property "%s" is required'%key
1197 if isinstance(prop, Multilink):
1198 propvalues[key] = []
1199 else:
1200 propvalues[key] = None
1202 # done
1203 self.db.addnode(self.classname, newid, propvalues)
1204 if self.do_journal:
1205 self.db.addjournal(self.classname, newid, 'create', {})
1207 return newid
1209 def export_list(self, propnames, nodeid):
1210 ''' Export a node - generate a list of CSV-able data in the order
1211 specified by propnames for the given node.
1212 '''
1213 properties = self.getprops()
1214 l = []
1215 for prop in propnames:
1216 proptype = properties[prop]
1217 value = self.get(nodeid, prop)
1218 # "marshal" data where needed
1219 if value is None:
1220 pass
1221 elif isinstance(proptype, hyperdb.Date):
1222 value = value.get_tuple()
1223 elif isinstance(proptype, hyperdb.Interval):
1224 value = value.get_tuple()
1225 elif isinstance(proptype, hyperdb.Password):
1226 value = str(value)
1227 l.append(repr(value))
1228 l.append(repr(self.is_retired(nodeid)))
1229 return l
1231 def import_list(self, propnames, proplist):
1232 ''' Import a node - all information including "id" is present and
1233 should not be sanity checked. Triggers are not triggered. The
1234 journal should be initialised using the "creator" and "created"
1235 information.
1237 Return the nodeid of the node imported.
1238 '''
1239 if self.db.journaltag is None:
1240 raise DatabaseError, 'Database open read-only'
1241 properties = self.getprops()
1243 # make the new node's property map
1244 d = {}
1245 retire = 0
1246 newid = None
1247 for i in range(len(propnames)):
1248 # Use eval to reverse the repr() used to output the CSV
1249 value = eval(proplist[i])
1251 # Figure the property for this column
1252 propname = propnames[i]
1254 # "unmarshal" where necessary
1255 if propname == 'id':
1256 newid = value
1257 continue
1258 elif propname == 'is retired':
1259 # is the item retired?
1260 if int(value):
1261 retire = 1
1262 continue
1263 elif value is None:
1264 d[propname] = None
1265 continue
1267 prop = properties[propname]
1268 if value is None:
1269 # don't set Nones
1270 continue
1271 elif isinstance(prop, hyperdb.Date):
1272 value = date.Date(value)
1273 elif isinstance(prop, hyperdb.Interval):
1274 value = date.Interval(value)
1275 elif isinstance(prop, hyperdb.Password):
1276 pwd = password.Password()
1277 pwd.unpack(value)
1278 value = pwd
1279 d[propname] = value
1281 # get a new id if necessary
1282 if newid is None:
1283 newid = self.db.newid(self.classname)
1285 # add the node and journal
1286 self.db.addnode(self.classname, newid, d)
1288 # retire?
1289 if retire:
1290 # use the arg for __retired__ to cope with any odd database type
1291 # conversion (hello, sqlite)
1292 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1293 self.db.arg, self.db.arg)
1294 if __debug__:
1295 print >>hyperdb.DEBUG, 'retire', (self, sql, newid)
1296 self.db.cursor.execute(sql, (1, newid))
1298 # extract the extraneous journalling gumpf and nuke it
1299 if d.has_key('creator'):
1300 creator = d['creator']
1301 del d['creator']
1302 else:
1303 creator = None
1304 if d.has_key('creation'):
1305 creation = d['creation']
1306 del d['creation']
1307 else:
1308 creation = None
1309 if d.has_key('activity'):
1310 del d['activity']
1311 self.db.addjournal(self.classname, newid, 'create', {}, creator,
1312 creation)
1313 return newid
1315 _marker = []
1316 def get(self, nodeid, propname, default=_marker, cache=1):
1317 '''Get the value of a property on an existing node of this class.
1319 'nodeid' must be the id of an existing node of this class or an
1320 IndexError is raised. 'propname' must be the name of a property
1321 of this class or a KeyError is raised.
1323 'cache' exists for backwards compatibility, and is not used.
1324 '''
1325 if propname == 'id':
1326 return nodeid
1328 # get the node's dict
1329 d = self.db.getnode(self.classname, nodeid)
1331 if propname == 'creation':
1332 if d.has_key('creation'):
1333 return d['creation']
1334 else:
1335 return date.Date()
1336 if propname == 'activity':
1337 if d.has_key('activity'):
1338 return d['activity']
1339 else:
1340 return date.Date()
1341 if propname == 'creator':
1342 if d.has_key('creator'):
1343 return d['creator']
1344 else:
1345 return self.db.getuid()
1347 # get the property (raises KeyErorr if invalid)
1348 prop = self.properties[propname]
1350 if not d.has_key(propname):
1351 if default is self._marker:
1352 if isinstance(prop, Multilink):
1353 return []
1354 else:
1355 return None
1356 else:
1357 return default
1359 # don't pass our list to other code
1360 if isinstance(prop, Multilink):
1361 return d[propname][:]
1363 return d[propname]
1365 def set(self, nodeid, **propvalues):
1366 '''Modify a property on an existing node of this class.
1368 'nodeid' must be the id of an existing node of this class or an
1369 IndexError is raised.
1371 Each key in 'propvalues' must be the name of a property of this
1372 class or a KeyError is raised.
1374 All values in 'propvalues' must be acceptable types for their
1375 corresponding properties or a TypeError is raised.
1377 If the value of the key property is set, it must not collide with
1378 other key strings or a ValueError is raised.
1380 If the value of a Link or Multilink property contains an invalid
1381 node id, a ValueError is raised.
1382 '''
1383 if not propvalues:
1384 return propvalues
1386 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1387 raise KeyError, '"creation" and "activity" are reserved'
1389 if propvalues.has_key('id'):
1390 raise KeyError, '"id" is reserved'
1392 if self.db.journaltag is None:
1393 raise DatabaseError, 'Database open read-only'
1395 self.fireAuditors('set', nodeid, propvalues)
1396 # Take a copy of the node dict so that the subsequent set
1397 # operation doesn't modify the oldvalues structure.
1398 # XXX used to try the cache here first
1399 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
1401 node = self.db.getnode(self.classname, nodeid)
1402 if self.is_retired(nodeid):
1403 raise IndexError, 'Requested item is retired'
1404 num_re = re.compile('^\d+$')
1406 # if the journal value is to be different, store it in here
1407 journalvalues = {}
1409 # remember the add/remove stuff for multilinks, making it easier
1410 # for the Database layer to do its stuff
1411 multilink_changes = {}
1413 for propname, value in propvalues.items():
1414 # check to make sure we're not duplicating an existing key
1415 if propname == self.key and node[propname] != value:
1416 try:
1417 self.lookup(value)
1418 except KeyError:
1419 pass
1420 else:
1421 raise ValueError, 'node with key "%s" exists'%value
1423 # this will raise the KeyError if the property isn't valid
1424 # ... we don't use getprops() here because we only care about
1425 # the writeable properties.
1426 try:
1427 prop = self.properties[propname]
1428 except KeyError:
1429 raise KeyError, '"%s" has no property named "%s"'%(
1430 self.classname, propname)
1432 # if the value's the same as the existing value, no sense in
1433 # doing anything
1434 current = node.get(propname, None)
1435 if value == current:
1436 del propvalues[propname]
1437 continue
1438 journalvalues[propname] = current
1440 # do stuff based on the prop type
1441 if isinstance(prop, Link):
1442 link_class = prop.classname
1443 # if it isn't a number, it's a key
1444 if value is not None and not isinstance(value, type('')):
1445 raise ValueError, 'property "%s" link value be a string'%(
1446 propname)
1447 if isinstance(value, type('')) and not num_re.match(value):
1448 try:
1449 value = self.db.classes[link_class].lookup(value)
1450 except (TypeError, KeyError):
1451 raise IndexError, 'new property "%s": %s not a %s'%(
1452 propname, value, prop.classname)
1454 if (value is not None and
1455 not self.db.getclass(link_class).hasnode(value)):
1456 raise IndexError, '%s has no node %s'%(link_class, value)
1458 if self.do_journal and prop.do_journal:
1459 # register the unlink with the old linked node
1460 if node[propname] is not None:
1461 self.db.addjournal(link_class, node[propname], 'unlink',
1462 (self.classname, nodeid, propname))
1464 # register the link with the newly linked node
1465 if value is not None:
1466 self.db.addjournal(link_class, value, 'link',
1467 (self.classname, nodeid, propname))
1469 elif isinstance(prop, Multilink):
1470 if type(value) != type([]):
1471 raise TypeError, 'new property "%s" not a list of'\
1472 ' ids'%propname
1473 link_class = self.properties[propname].classname
1474 l = []
1475 for entry in value:
1476 # if it isn't a number, it's a key
1477 if type(entry) != type(''):
1478 raise ValueError, 'new property "%s" link value ' \
1479 'must be a string'%propname
1480 if not num_re.match(entry):
1481 try:
1482 entry = self.db.classes[link_class].lookup(entry)
1483 except (TypeError, KeyError):
1484 raise IndexError, 'new property "%s": %s not a %s'%(
1485 propname, entry,
1486 self.properties[propname].classname)
1487 l.append(entry)
1488 value = l
1489 propvalues[propname] = value
1491 # figure the journal entry for this property
1492 add = []
1493 remove = []
1495 # handle removals
1496 if node.has_key(propname):
1497 l = node[propname]
1498 else:
1499 l = []
1500 for id in l[:]:
1501 if id in value:
1502 continue
1503 # register the unlink with the old linked node
1504 if self.do_journal and self.properties[propname].do_journal:
1505 self.db.addjournal(link_class, id, 'unlink',
1506 (self.classname, nodeid, propname))
1507 l.remove(id)
1508 remove.append(id)
1510 # handle additions
1511 for id in value:
1512 if not self.db.getclass(link_class).hasnode(id):
1513 raise IndexError, '%s has no node %s'%(link_class, id)
1514 if id in l:
1515 continue
1516 # register the link with the newly linked node
1517 if self.do_journal and self.properties[propname].do_journal:
1518 self.db.addjournal(link_class, id, 'link',
1519 (self.classname, nodeid, propname))
1520 l.append(id)
1521 add.append(id)
1523 # figure the journal entry
1524 l = []
1525 if add:
1526 l.append(('+', add))
1527 if remove:
1528 l.append(('-', remove))
1529 multilink_changes[propname] = (add, remove)
1530 if l:
1531 journalvalues[propname] = tuple(l)
1533 elif isinstance(prop, String):
1534 if value is not None and type(value) != type('') and type(value) != type(u''):
1535 raise TypeError, 'new property "%s" not a string'%propname
1537 elif isinstance(prop, Password):
1538 if not isinstance(value, password.Password):
1539 raise TypeError, 'new property "%s" not a Password'%propname
1540 propvalues[propname] = value
1542 elif value is not None and isinstance(prop, Date):
1543 if not isinstance(value, date.Date):
1544 raise TypeError, 'new property "%s" not a Date'% propname
1545 propvalues[propname] = value
1547 elif value is not None and isinstance(prop, Interval):
1548 if not isinstance(value, date.Interval):
1549 raise TypeError, 'new property "%s" not an '\
1550 'Interval'%propname
1551 propvalues[propname] = value
1553 elif value is not None and isinstance(prop, Number):
1554 try:
1555 float(value)
1556 except ValueError:
1557 raise TypeError, 'new property "%s" not numeric'%propname
1559 elif value is not None and isinstance(prop, Boolean):
1560 try:
1561 int(value)
1562 except ValueError:
1563 raise TypeError, 'new property "%s" not boolean'%propname
1565 # nothing to do?
1566 if not propvalues:
1567 return propvalues
1569 # do the set, and journal it
1570 self.db.setnode(self.classname, nodeid, propvalues, multilink_changes)
1572 if self.do_journal:
1573 self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
1575 self.fireReactors('set', nodeid, oldvalues)
1577 return propvalues
1579 def retire(self, nodeid):
1580 '''Retire a node.
1582 The properties on the node remain available from the get() method,
1583 and the node's id is never reused.
1585 Retired nodes are not returned by the find(), list(), or lookup()
1586 methods, and other nodes may reuse the values of their key properties.
1587 '''
1588 if self.db.journaltag is None:
1589 raise DatabaseError, 'Database open read-only'
1591 self.fireAuditors('retire', nodeid, None)
1593 # use the arg for __retired__ to cope with any odd database type
1594 # conversion (hello, sqlite)
1595 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1596 self.db.arg, self.db.arg)
1597 if __debug__:
1598 print >>hyperdb.DEBUG, 'retire', (self, sql, nodeid)
1599 self.db.cursor.execute(sql, (1, nodeid))
1600 if self.do_journal:
1601 self.db.addjournal(self.classname, nodeid, 'retired', None)
1603 self.fireReactors('retire', nodeid, None)
1605 def restore(self, nodeid):
1606 '''Restore a retired node.
1608 Make node available for all operations like it was before retirement.
1609 '''
1610 if self.db.journaltag is None:
1611 raise DatabaseError, 'Database open read-only'
1613 node = self.db.getnode(self.classname, nodeid)
1614 # check if key property was overrided
1615 key = self.getkey()
1616 try:
1617 id = self.lookup(node[key])
1618 except KeyError:
1619 pass
1620 else:
1621 raise KeyError, "Key property (%s) of retired node clashes with \
1622 existing one (%s)" % (key, node[key])
1624 self.fireAuditors('restore', nodeid, None)
1625 # use the arg for __retired__ to cope with any odd database type
1626 # conversion (hello, sqlite)
1627 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1628 self.db.arg, self.db.arg)
1629 if __debug__:
1630 print >>hyperdb.DEBUG, 'restore', (self, sql, nodeid)
1631 self.db.cursor.execute(sql, (0, nodeid))
1632 if self.do_journal:
1633 self.db.addjournal(self.classname, nodeid, 'restored', None)
1635 self.fireReactors('restore', nodeid, None)
1637 def is_retired(self, nodeid):
1638 '''Return true if the node is rerired
1639 '''
1640 sql = 'select __retired__ from _%s where id=%s'%(self.classname,
1641 self.db.arg)
1642 if __debug__:
1643 print >>hyperdb.DEBUG, 'is_retired', (self, sql, nodeid)
1644 self.db.cursor.execute(sql, (nodeid,))
1645 return int(self.db.sql_fetchone()[0])
1647 def destroy(self, nodeid):
1648 '''Destroy a node.
1650 WARNING: this method should never be used except in extremely rare
1651 situations where there could never be links to the node being
1652 deleted
1654 WARNING: use retire() instead
1656 WARNING: the properties of this node will not be available ever again
1658 WARNING: really, use retire() instead
1660 Well, I think that's enough warnings. This method exists mostly to
1661 support the session storage of the cgi interface.
1663 The node is completely removed from the hyperdb, including all journal
1664 entries. It will no longer be available, and will generally break code
1665 if there are any references to the node.
1666 '''
1667 if self.db.journaltag is None:
1668 raise DatabaseError, 'Database open read-only'
1669 self.db.destroynode(self.classname, nodeid)
1671 def history(self, nodeid):
1672 '''Retrieve the journal of edits on a particular node.
1674 'nodeid' must be the id of an existing node of this class or an
1675 IndexError is raised.
1677 The returned list contains tuples of the form
1679 (nodeid, date, tag, action, params)
1681 'date' is a Timestamp object specifying the time of the change and
1682 'tag' is the journaltag specified when the database was opened.
1683 '''
1684 if not self.do_journal:
1685 raise ValueError, 'Journalling is disabled for this class'
1686 return self.db.getjournal(self.classname, nodeid)
1688 # Locating nodes:
1689 def hasnode(self, nodeid):
1690 '''Determine if the given nodeid actually exists
1691 '''
1692 return self.db.hasnode(self.classname, nodeid)
1694 def setkey(self, propname):
1695 '''Select a String property of this class to be the key property.
1697 'propname' must be the name of a String property of this class or
1698 None, or a TypeError is raised. The values of the key property on
1699 all existing nodes must be unique or a ValueError is raised.
1700 '''
1701 # XXX create an index on the key prop column. We should also
1702 # record that we've created this index in the schema somewhere.
1703 prop = self.getprops()[propname]
1704 if not isinstance(prop, String):
1705 raise TypeError, 'key properties must be String'
1706 self.key = propname
1708 def getkey(self):
1709 '''Return the name of the key property for this class or None.'''
1710 return self.key
1712 def labelprop(self, default_to_id=0):
1713 '''Return the property name for a label for the given node.
1715 This method attempts to generate a consistent label for the node.
1716 It tries the following in order:
1718 1. key property
1719 2. "name" property
1720 3. "title" property
1721 4. first property from the sorted property name list
1722 '''
1723 k = self.getkey()
1724 if k:
1725 return k
1726 props = self.getprops()
1727 if props.has_key('name'):
1728 return 'name'
1729 elif props.has_key('title'):
1730 return 'title'
1731 if default_to_id:
1732 return 'id'
1733 props = props.keys()
1734 props.sort()
1735 return props[0]
1737 def lookup(self, keyvalue):
1738 '''Locate a particular node by its key property and return its id.
1740 If this class has no key property, a TypeError is raised. If the
1741 'keyvalue' matches one of the values for the key property among
1742 the nodes in this class, the matching node's id is returned;
1743 otherwise a KeyError is raised.
1744 '''
1745 if not self.key:
1746 raise TypeError, 'No key property set for class %s'%self.classname
1748 # use the arg to handle any odd database type conversion (hello,
1749 # sqlite)
1750 sql = "select id from _%s where _%s=%s and __retired__ <> %s"%(
1751 self.classname, self.key, self.db.arg, self.db.arg)
1752 self.db.sql(sql, (keyvalue, 1))
1754 # see if there was a result that's not retired
1755 row = self.db.sql_fetchone()
1756 if not row:
1757 raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
1758 keyvalue, self.classname)
1760 # return the id
1761 return row[0]
1763 def find(self, **propspec):
1764 '''Get the ids of nodes in this class which link to the given nodes.
1766 'propspec' consists of keyword args propname=nodeid or
1767 propname={nodeid:1, }
1768 'propname' must be the name of a property in this class, or a
1769 KeyError is raised. That property must be a Link or
1770 Multilink property, or a TypeError is raised.
1772 Any node in this class whose 'propname' property links to any of the
1773 nodeids will be returned. Used by the full text indexing, which knows
1774 that "foo" occurs in msg1, msg3 and file7, so we have hits on these
1775 issues:
1777 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1778 '''
1779 if __debug__:
1780 print >>hyperdb.DEBUG, 'find', (self, propspec)
1782 # shortcut
1783 if not propspec:
1784 return []
1786 # validate the args
1787 props = self.getprops()
1788 propspec = propspec.items()
1789 for propname, nodeids in propspec:
1790 # check the prop is OK
1791 prop = props[propname]
1792 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
1793 raise TypeError, "'%s' not a Link/Multilink property"%propname
1795 # first, links
1796 a = self.db.arg
1797 allvalues = (1,)
1798 o = []
1799 where = []
1800 for prop, values in propspec:
1801 if not isinstance(props[prop], hyperdb.Link):
1802 continue
1803 if type(values) is type({}) and len(values) == 1:
1804 values = values.keys()[0]
1805 if type(values) is type(''):
1806 allvalues += (values,)
1807 where.append('_%s = %s'%(prop, a))
1808 elif values is None:
1809 where.append('_%s is NULL'%prop)
1810 else:
1811 allvalues += tuple(values.keys())
1812 where.append('_%s in (%s)'%(prop, ','.join([a]*len(values))))
1813 tables = ['_%s'%self.classname]
1814 if where:
1815 o.append('(' + ' and '.join(where) + ')')
1817 # now multilinks
1818 for prop, values in propspec:
1819 if not isinstance(props[prop], hyperdb.Multilink):
1820 continue
1821 if not values:
1822 continue
1823 if type(values) is type(''):
1824 allvalues += (values,)
1825 s = a
1826 else:
1827 allvalues += tuple(values.keys())
1828 s = ','.join([a]*len(values))
1829 tn = '%s_%s'%(self.classname, prop)
1830 tables.append(tn)
1831 o.append('(id=%s.nodeid and %s.linkid in (%s))'%(tn, tn, s))
1833 if not o:
1834 return []
1835 elif len(o) > 1:
1836 o = '(' + ' or '.join(['(%s)'%i for i in o]) + ')'
1837 else:
1838 o = o[0]
1839 t = ', '.join(tables)
1840 sql = 'select distinct(id) from %s where __retired__ <> %s and %s'%(t, a, o)
1841 self.db.sql(sql, allvalues)
1842 l = [x[0] for x in self.db.sql_fetchall()]
1843 if __debug__:
1844 print >>hyperdb.DEBUG, 'find ... ', l
1845 return l
1847 def stringFind(self, **requirements):
1848 '''Locate a particular node by matching a set of its String
1849 properties in a caseless search.
1851 If the property is not a String property, a TypeError is raised.
1853 The return is a list of the id of all nodes that match.
1854 '''
1855 where = []
1856 args = []
1857 for propname in requirements.keys():
1858 prop = self.properties[propname]
1859 if not isinstance(prop, String):
1860 raise TypeError, "'%s' not a String property"%propname
1861 where.append(propname)
1862 args.append(requirements[propname].lower())
1864 # generate the where clause
1865 s = ' and '.join(['lower(_%s)=%s'%(col, self.db.arg) for col in where])
1866 sql = 'select id from _%s where %s and __retired__=%s'%(self.classname,
1867 s, self.db.arg)
1868 args.append(0)
1869 self.db.sql(sql, tuple(args))
1870 l = [x[0] for x in self.db.sql_fetchall()]
1871 if __debug__:
1872 print >>hyperdb.DEBUG, 'find ... ', l
1873 return l
1875 def list(self):
1876 ''' Return a list of the ids of the active nodes in this class.
1877 '''
1878 return self.getnodeids(retired=0)
1880 def getnodeids(self, retired=None):
1881 ''' Retrieve all the ids of the nodes for a particular Class.
1883 Set retired=None to get all nodes. Otherwise it'll get all the
1884 retired or non-retired nodes, depending on the flag.
1885 '''
1886 # flip the sense of the 'retired' flag if we don't want all of them
1887 if retired is not None:
1888 if retired:
1889 args = (0, )
1890 else:
1891 args = (1, )
1892 sql = 'select id from _%s where __retired__ <> %s'%(self.classname,
1893 self.db.arg)
1894 else:
1895 args = ()
1896 sql = 'select id from _%s'%self.classname
1897 if __debug__:
1898 print >>hyperdb.DEBUG, 'getnodeids', (self, sql, retired)
1899 self.db.cursor.execute(sql, args)
1900 ids = [x[0] for x in self.db.cursor.fetchall()]
1901 return ids
1903 def filter(self, search_matches, filterspec, sort=(None,None),
1904 group=(None,None)):
1905 '''Return a list of the ids of the active nodes in this class that
1906 match the 'filter' spec, sorted by the group spec and then the
1907 sort spec
1909 "filterspec" is {propname: value(s)}
1911 "sort" and "group" are (dir, prop) where dir is '+', '-' or None
1912 and prop is a prop name or None
1914 "search_matches" is {nodeid: marker}
1916 The filter must match all properties specificed - but if the
1917 property value to match is a list, any one of the values in the
1918 list may match for that property to match.
1919 '''
1920 # just don't bother if the full-text search matched diddly
1921 if search_matches == {}:
1922 return []
1924 cn = self.classname
1926 timezone = self.db.getUserTimezone()
1928 # figure the WHERE clause from the filterspec
1929 props = self.getprops()
1930 frum = ['_'+cn]
1931 where = []
1932 args = []
1933 a = self.db.arg
1934 for k, v in filterspec.items():
1935 propclass = props[k]
1936 # now do other where clause stuff
1937 if isinstance(propclass, Multilink):
1938 tn = '%s_%s'%(cn, k)
1939 if v in ('-1', ['-1']):
1940 # only match rows that have count(linkid)=0 in the
1941 # corresponding multilink table)
1942 where.append('id not in (select nodeid from %s)'%tn)
1943 elif isinstance(v, type([])):
1944 frum.append(tn)
1945 s = ','.join([a for x in v])
1946 where.append('id=%s.nodeid and %s.linkid in (%s)'%(tn,tn,s))
1947 args = args + v
1948 else:
1949 frum.append(tn)
1950 where.append('id=%s.nodeid and %s.linkid=%s'%(tn, tn, a))
1951 args.append(v)
1952 elif k == 'id':
1953 if isinstance(v, type([])):
1954 s = ','.join([a for x in v])
1955 where.append('%s in (%s)'%(k, s))
1956 args = args + v
1957 else:
1958 where.append('%s=%s'%(k, a))
1959 args.append(v)
1960 elif isinstance(propclass, String):
1961 if not isinstance(v, type([])):
1962 v = [v]
1964 # Quote the bits in the string that need it and then embed
1965 # in a "substring" search. Note - need to quote the '%' so
1966 # they make it through the python layer happily
1967 v = ['%%'+self.db.sql_stringquote(s)+'%%' for s in v]
1969 # now add to the where clause
1970 where.append(' or '.join(["_%s LIKE '%s'"%(k, s) for s in v]))
1971 # note: args are embedded in the query string now
1972 elif isinstance(propclass, Link):
1973 if isinstance(v, type([])):
1974 if '-1' in v:
1975 v = v[:]
1976 v.remove('-1')
1977 xtra = ' or _%s is NULL'%k
1978 else:
1979 xtra = ''
1980 if v:
1981 s = ','.join([a for x in v])
1982 where.append('(_%s in (%s)%s)'%(k, s, xtra))
1983 args = args + v
1984 else:
1985 where.append('_%s is NULL'%k)
1986 else:
1987 if v == '-1':
1988 v = None
1989 where.append('_%s is NULL'%k)
1990 else:
1991 where.append('_%s=%s'%(k, a))
1992 args.append(v)
1993 elif isinstance(propclass, Date):
1994 if isinstance(v, type([])):
1995 s = ','.join([a for x in v])
1996 where.append('_%s in (%s)'%(k, s))
1997 args = args + [date.Date(x).serialise() for x in v]
1998 else:
1999 try:
2000 # Try to filter on range of dates
2001 date_rng = Range(v, date.Date, offset=timezone)
2002 if (date_rng.from_value):
2003 where.append('_%s >= %s'%(k, a))
2004 args.append(date_rng.from_value.serialise())
2005 if (date_rng.to_value):
2006 where.append('_%s <= %s'%(k, a))
2007 args.append(date_rng.to_value.serialise())
2008 except ValueError:
2009 # If range creation fails - ignore that search parameter
2010 pass
2011 elif isinstance(propclass, Interval):
2012 if isinstance(v, type([])):
2013 s = ','.join([a for x in v])
2014 where.append('_%s in (%s)'%(k, s))
2015 args = args + [date.Interval(x).serialise() for x in v]
2016 else:
2017 try:
2018 # Try to filter on range of intervals
2019 date_rng = Range(v, date.Interval)
2020 if (date_rng.from_value):
2021 where.append('_%s >= %s'%(k, a))
2022 args.append(date_rng.from_value.serialise())
2023 if (date_rng.to_value):
2024 where.append('_%s <= %s'%(k, a))
2025 args.append(date_rng.to_value.serialise())
2026 except ValueError:
2027 # If range creation fails - ignore that search parameter
2028 pass
2029 #where.append('_%s=%s'%(k, a))
2030 #args.append(date.Interval(v).serialise())
2031 else:
2032 if isinstance(v, type([])):
2033 s = ','.join([a for x in v])
2034 where.append('_%s in (%s)'%(k, s))
2035 args = args + v
2036 else:
2037 where.append('_%s=%s'%(k, a))
2038 args.append(v)
2040 # don't match retired nodes
2041 where.append('__retired__ <> 1')
2043 # add results of full text search
2044 if search_matches is not None:
2045 v = search_matches.keys()
2046 s = ','.join([a for x in v])
2047 where.append('id in (%s)'%s)
2048 args = args + v
2050 # "grouping" is just the first-order sorting in the SQL fetch
2051 # can modify it...)
2052 orderby = []
2053 ordercols = []
2054 if group[0] is not None and group[1] is not None:
2055 if group[0] != '-':
2056 orderby.append('_'+group[1])
2057 ordercols.append('_'+group[1])
2058 else:
2059 orderby.append('_'+group[1]+' desc')
2060 ordercols.append('_'+group[1])
2062 # now add in the sorting
2063 group = ''
2064 if sort[0] is not None and sort[1] is not None:
2065 direction, colname = sort
2066 if direction != '-':
2067 if colname == 'id':
2068 orderby.append(colname)
2069 else:
2070 orderby.append('_'+colname)
2071 ordercols.append('_'+colname)
2072 else:
2073 if colname == 'id':
2074 orderby.append(colname+' desc')
2075 ordercols.append(colname)
2076 else:
2077 orderby.append('_'+colname+' desc')
2078 ordercols.append('_'+colname)
2080 # construct the SQL
2081 frum = ','.join(frum)
2082 if where:
2083 where = ' where ' + (' and '.join(where))
2084 else:
2085 where = ''
2086 cols = ['id']
2087 if orderby:
2088 cols = cols + ordercols
2089 order = ' order by %s'%(','.join(orderby))
2090 else:
2091 order = ''
2092 cols = ','.join(cols)
2093 sql = 'select %s from %s %s%s%s'%(cols, frum, where, group, order)
2094 args = tuple(args)
2095 if __debug__:
2096 print >>hyperdb.DEBUG, 'filter', (self, sql, args)
2097 if args:
2098 self.db.cursor.execute(sql, args)
2099 else:
2100 # psycopg doesn't like empty args
2101 self.db.cursor.execute(sql)
2102 l = self.db.sql_fetchall()
2104 # return the IDs (the first column)
2105 return [row[0] for row in l]
2107 def count(self):
2108 '''Get the number of nodes in this class.
2110 If the returned integer is 'numnodes', the ids of all the nodes
2111 in this class run from 1 to numnodes, and numnodes+1 will be the
2112 id of the next node to be created in this class.
2113 '''
2114 return self.db.countnodes(self.classname)
2116 # Manipulating properties:
2117 def getprops(self, protected=1):
2118 '''Return a dictionary mapping property names to property objects.
2119 If the "protected" flag is true, we include protected properties -
2120 those which may not be modified.
2121 '''
2122 d = self.properties.copy()
2123 if protected:
2124 d['id'] = String()
2125 d['creation'] = hyperdb.Date()
2126 d['activity'] = hyperdb.Date()
2127 d['creator'] = hyperdb.Link('user')
2128 return d
2130 def addprop(self, **properties):
2131 '''Add properties to this class.
2133 The keyword arguments in 'properties' must map names to property
2134 objects, or a TypeError is raised. None of the keys in 'properties'
2135 may collide with the names of existing properties, or a ValueError
2136 is raised before any properties have been added.
2137 '''
2138 for key in properties.keys():
2139 if self.properties.has_key(key):
2140 raise ValueError, key
2141 self.properties.update(properties)
2143 def index(self, nodeid):
2144 '''Add (or refresh) the node to search indexes
2145 '''
2146 # find all the String properties that have indexme
2147 for prop, propclass in self.getprops().items():
2148 if isinstance(propclass, String) and propclass.indexme:
2149 try:
2150 value = str(self.get(nodeid, prop))
2151 except IndexError:
2152 # node no longer exists - entry should be removed
2153 self.db.indexer.purge_entry((self.classname, nodeid, prop))
2154 else:
2155 # and index them under (classname, nodeid, property)
2156 self.db.indexer.add_text((self.classname, nodeid, prop),
2157 value)
2160 #
2161 # Detector interface
2162 #
2163 def audit(self, event, detector):
2164 '''Register a detector
2165 '''
2166 l = self.auditors[event]
2167 if detector not in l:
2168 self.auditors[event].append(detector)
2170 def fireAuditors(self, action, nodeid, newvalues):
2171 '''Fire all registered auditors.
2172 '''
2173 for audit in self.auditors[action]:
2174 audit(self.db, self, nodeid, newvalues)
2176 def react(self, event, detector):
2177 '''Register a detector
2178 '''
2179 l = self.reactors[event]
2180 if detector not in l:
2181 self.reactors[event].append(detector)
2183 def fireReactors(self, action, nodeid, oldvalues):
2184 '''Fire all registered reactors.
2185 '''
2186 for react in self.reactors[action]:
2187 react(self.db, self, nodeid, oldvalues)
2189 class FileClass(Class, hyperdb.FileClass):
2190 '''This class defines a large chunk of data. To support this, it has a
2191 mandatory String property "content" which is typically saved off
2192 externally to the hyperdb.
2194 The default MIME type of this data is defined by the
2195 "default_mime_type" class attribute, which may be overridden by each
2196 node if the class defines a "type" String property.
2197 '''
2198 default_mime_type = 'text/plain'
2200 def create(self, **propvalues):
2201 ''' snaffle the file propvalue and store in a file
2202 '''
2203 # we need to fire the auditors now, or the content property won't
2204 # be in propvalues for the auditors to play with
2205 self.fireAuditors('create', None, propvalues)
2207 # now remove the content property so it's not stored in the db
2208 content = propvalues['content']
2209 del propvalues['content']
2211 # do the database create
2212 newid = Class.create_inner(self, **propvalues)
2214 # fire reactors
2215 self.fireReactors('create', newid, None)
2217 # store off the content as a file
2218 self.db.storefile(self.classname, newid, None, content)
2219 return newid
2221 def import_list(self, propnames, proplist):
2222 ''' Trap the "content" property...
2223 '''
2224 # dupe this list so we don't affect others
2225 propnames = propnames[:]
2227 # extract the "content" property from the proplist
2228 i = propnames.index('content')
2229 content = eval(proplist[i])
2230 del propnames[i]
2231 del proplist[i]
2233 # do the normal import
2234 newid = Class.import_list(self, propnames, proplist)
2236 # save off the "content" file
2237 self.db.storefile(self.classname, newid, None, content)
2238 return newid
2240 _marker = []
2241 def get(self, nodeid, propname, default=_marker, cache=1):
2242 ''' Trap the content propname and get it from the file
2244 'cache' exists for backwards compatibility, and is not used.
2245 '''
2246 poss_msg = 'Possibly a access right configuration problem.'
2247 if propname == 'content':
2248 try:
2249 return self.db.getfile(self.classname, nodeid, None)
2250 except IOError, (strerror):
2251 # BUG: by catching this we donot see an error in the log.
2252 return 'ERROR reading file: %s%s\n%s\n%s'%(
2253 self.classname, nodeid, poss_msg, strerror)
2254 if default is not self._marker:
2255 return Class.get(self, nodeid, propname, default)
2256 else:
2257 return Class.get(self, nodeid, propname)
2259 def getprops(self, protected=1):
2260 ''' In addition to the actual properties on the node, these methods
2261 provide the "content" property. If the "protected" flag is true,
2262 we include protected properties - those which may not be
2263 modified.
2264 '''
2265 d = Class.getprops(self, protected=protected).copy()
2266 d['content'] = hyperdb.String()
2267 return d
2269 def index(self, nodeid):
2270 ''' Index the node in the search index.
2272 We want to index the content in addition to the normal String
2273 property indexing.
2274 '''
2275 # perform normal indexing
2276 Class.index(self, nodeid)
2278 # get the content to index
2279 content = self.get(nodeid, 'content')
2281 # figure the mime type
2282 if self.properties.has_key('type'):
2283 mime_type = self.get(nodeid, 'type')
2284 else:
2285 mime_type = self.default_mime_type
2287 # and index!
2288 self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
2289 mime_type)
2291 # XXX deviation from spec - was called ItemClass
2292 class IssueClass(Class, roundupdb.IssueClass):
2293 # Overridden methods:
2294 def __init__(self, db, classname, **properties):
2295 '''The newly-created class automatically includes the "messages",
2296 "files", "nosy", and "superseder" properties. If the 'properties'
2297 dictionary attempts to specify any of these properties or a
2298 "creation" or "activity" property, a ValueError is raised.
2299 '''
2300 if not properties.has_key('title'):
2301 properties['title'] = hyperdb.String(indexme='yes')
2302 if not properties.has_key('messages'):
2303 properties['messages'] = hyperdb.Multilink("msg")
2304 if not properties.has_key('files'):
2305 properties['files'] = hyperdb.Multilink("file")
2306 if not properties.has_key('nosy'):
2307 # note: journalling is turned off as it really just wastes
2308 # space. this behaviour may be overridden in an instance
2309 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
2310 if not properties.has_key('superseder'):
2311 properties['superseder'] = hyperdb.Multilink(classname)
2312 Class.__init__(self, db, classname, **properties)