16392c68aba7f229095fb0044a34bb525f5a0c03
1 # $Id: rdbms_common.py,v 1.76 2004-03-05 00:08: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.)
23 The schema of the hyperdb being mapped to the database is stored in the
24 database itself as a repr()'ed dictionary of information about each Class
25 that maps to a table. If that information differs from the hyperdb schema,
26 then we update it. We also store in the schema dict a __version__ which
27 allows us to upgrade the database schema when necessary. See upgrade_db().
28 '''
29 __docformat__ = 'restructuredtext'
31 # standard python modules
32 import sys, os, time, re, errno, weakref, copy
34 # roundup modules
35 from roundup import hyperdb, date, password, roundupdb, security
36 from roundup.hyperdb import String, Password, Date, Interval, Link, \
37 Multilink, DatabaseError, Boolean, Number, Node
38 from roundup.backends import locking
40 # support
41 from blobfiles import FileStorage
42 from roundup.indexer import Indexer
43 from sessions import Sessions, OneTimeKeys
44 from roundup.date import Range
46 # number of rows to keep in memory
47 ROW_CACHE_SIZE = 100
49 class Database(FileStorage, hyperdb.Database, roundupdb.Database):
50 ''' Wrapper around an SQL database that presents a hyperdb interface.
52 - some functionality is specific to the actual SQL database, hence
53 the sql_* methods that are NotImplemented
54 - we keep a cache of the latest ROW_CACHE_SIZE row fetches.
55 '''
56 def __init__(self, config, journaltag=None):
57 ''' Open the database and load the schema from it.
58 '''
59 self.config, self.journaltag = config, journaltag
60 self.dir = config.DATABASE
61 self.classes = {}
62 self.indexer = Indexer(self.dir)
63 self.sessions = Sessions(self.config)
64 self.otks = OneTimeKeys(self.config)
65 self.security = security.Security(self)
67 # additional transaction support for external files and the like
68 self.transactions = []
70 # keep a cache of the N most recently retrieved rows of any kind
71 # (classname, nodeid) = row
72 self.cache = {}
73 self.cache_lru = []
75 # database lock
76 self.lockfile = None
78 # open a connection to the database, creating the "conn" attribute
79 self.sql_open_connection()
81 def clearCache(self):
82 self.cache = {}
83 self.cache_lru = []
85 def sql_open_connection(self):
86 ''' Open a connection to the database, creating it if necessary.
88 Must call self.load_dbschema()
89 '''
90 raise NotImplemented
92 def sql(self, sql, args=None):
93 ''' Execute the sql with the optional args.
94 '''
95 if __debug__:
96 print >>hyperdb.DEBUG, (self, sql, args)
97 if args:
98 self.cursor.execute(sql, args)
99 else:
100 self.cursor.execute(sql)
102 def sql_fetchone(self):
103 ''' Fetch a single row. If there's nothing to fetch, return None.
104 '''
105 return self.cursor.fetchone()
107 def sql_fetchall(self):
108 ''' Fetch all rows. If there's nothing to fetch, return [].
109 '''
110 return self.cursor.fetchall()
112 def sql_stringquote(self, value):
113 ''' Quote the string so it's safe to put in the 'sql quotes'
114 '''
115 return re.sub("'", "''", str(value))
117 def load_dbschema(self):
118 ''' Load the schema definition that the database currently implements
119 '''
120 self.cursor.execute('select schema from schema')
121 self.database_schema = eval(self.cursor.fetchone()[0])
123 def save_dbschema(self, schema):
124 ''' Save the schema definition that the database currently implements
125 '''
126 s = repr(self.database_schema)
127 self.sql('insert into schema values (%s)', (s,))
129 def post_init(self):
130 ''' Called once the schema initialisation has finished.
132 We should now confirm that the schema defined by our "classes"
133 attribute actually matches the schema in the database.
134 '''
135 self.upgrade_db()
137 # now detect changes in the schema
138 save = 0
139 for classname, spec in self.classes.items():
140 if self.database_schema.has_key(classname):
141 dbspec = self.database_schema[classname]
142 if self.update_class(spec, dbspec):
143 self.database_schema[classname] = spec.schema()
144 save = 1
145 else:
146 self.create_class(spec)
147 self.database_schema[classname] = spec.schema()
148 save = 1
150 for classname, spec in self.database_schema.items():
151 if not self.classes.has_key(classname):
152 self.drop_class(classname, spec)
153 del self.database_schema[classname]
154 save = 1
156 # update the database version of the schema
157 if save:
158 self.sql('delete from schema')
159 self.save_dbschema(self.database_schema)
161 # reindex the db if necessary
162 if self.indexer.should_reindex():
163 self.reindex()
165 # commit
166 self.conn.commit()
168 # update this number when we need to make changes to the SQL structure
169 # of the backen database
170 current_db_version = 2
171 def upgrade_db(self):
172 ''' Update the SQL database to reflect changes in the backend code.
173 '''
174 version = self.database_schema.get('__version', 1)
175 if version == 1:
176 # version 1 doesn't have the OTK, session and indexing in the
177 # database
178 self.create_version_2_tables()
180 self.database_schema['__version'] = self.current_db_version
183 def refresh_database(self):
184 self.post_init()
186 def reindex(self):
187 for klass in self.classes.values():
188 for nodeid in klass.list():
189 klass.index(nodeid)
190 self.indexer.save_index()
192 def determine_columns(self, properties):
193 ''' Figure the column names and multilink properties from the spec
195 "properties" is a list of (name, prop) where prop may be an
196 instance of a hyperdb "type" _or_ a string repr of that type.
197 '''
198 cols = ['_activity', '_creator', '_creation']
199 mls = []
200 # add the multilinks separately
201 for col, prop in properties:
202 if isinstance(prop, Multilink):
203 mls.append(col)
204 elif isinstance(prop, type('')) and prop.find('Multilink') != -1:
205 mls.append(col)
206 else:
207 cols.append('_'+col)
208 cols.sort()
209 return cols, mls
211 def update_class(self, spec, old_spec, force=0):
212 ''' Determine the differences between the current spec and the
213 database version of the spec, and update where necessary.
215 If 'force' is true, update the database anyway.
216 '''
217 new_has = spec.properties.has_key
218 new_spec = spec.schema()
219 new_spec[1].sort()
220 old_spec[1].sort()
221 if not force and new_spec == old_spec:
222 # no changes
223 return 0
225 if __debug__:
226 print >>hyperdb.DEBUG, 'update_class FIRING'
228 # detect multilinks that have been removed, and drop their table
229 old_has = {}
230 for name,prop in old_spec[1]:
231 old_has[name] = 1
232 if new_has(name) or not isinstance(prop, Multilink):
233 continue
234 # it's a multilink, and it's been removed - drop the old
235 # table. First drop indexes.
236 self.drop_multilink_table_indexes(spec.classname, ml)
237 sql = 'drop table %s_%s'%(spec.classname, prop)
238 if __debug__:
239 print >>hyperdb.DEBUG, 'update_class', (self, sql)
240 self.cursor.execute(sql)
241 old_has = old_has.has_key
243 # now figure how we populate the new table
244 fetch = ['_activity', '_creation', '_creator']
245 properties = spec.getprops()
246 for propname,x in new_spec[1]:
247 prop = properties[propname]
248 if isinstance(prop, Multilink):
249 if force or not old_has(propname):
250 # we need to create the new table
251 self.create_multilink_table(spec, propname)
252 elif old_has(propname):
253 # we copy this col over from the old table
254 fetch.append('_'+propname)
256 # select the data out of the old table
257 fetch.append('id')
258 fetch.append('__retired__')
259 fetchcols = ','.join(fetch)
260 cn = spec.classname
261 sql = 'select %s from _%s'%(fetchcols, cn)
262 if __debug__:
263 print >>hyperdb.DEBUG, 'update_class', (self, sql)
264 self.cursor.execute(sql)
265 olddata = self.cursor.fetchall()
267 # TODO: update all the other index dropping code
268 self.drop_class_table_indexes(cn, old_spec[0])
270 # drop the old table
271 self.cursor.execute('drop table _%s'%cn)
273 # create the new table
274 self.create_class_table(spec)
276 if olddata:
277 # do the insert
278 args = ','.join([self.arg for x in fetch])
279 sql = 'insert into _%s (%s) values (%s)'%(cn, fetchcols, args)
280 if __debug__:
281 print >>hyperdb.DEBUG, 'update_class', (self, sql, olddata[0])
282 for entry in olddata:
283 self.cursor.execute(sql, tuple(entry))
285 return 1
287 def create_class_table(self, spec):
288 ''' create the class table for the given spec
289 '''
290 cols, mls = self.determine_columns(spec.properties.items())
292 # add on our special columns
293 cols.append('id')
294 cols.append('__retired__')
296 # create the base table
297 scols = ','.join(['%s varchar'%x for x in cols])
298 sql = 'create table _%s (%s)'%(spec.classname, scols)
299 if __debug__:
300 print >>hyperdb.DEBUG, 'create_class', (self, sql)
301 self.cursor.execute(sql)
303 self.create_class_table_indexes(spec)
305 return cols, mls
307 def create_class_table_indexes(self, spec):
308 ''' create the class table for the given spec
309 '''
310 # create id index
311 index_sql1 = 'create index _%s_id_idx on _%s(id)'%(
312 spec.classname, spec.classname)
313 if __debug__:
314 print >>hyperdb.DEBUG, 'create_index', (self, index_sql1)
315 self.cursor.execute(index_sql1)
317 # create __retired__ index
318 index_sql2 = 'create index _%s_retired_idx on _%s(__retired__)'%(
319 spec.classname, spec.classname)
320 if __debug__:
321 print >>hyperdb.DEBUG, 'create_index', (self, index_sql2)
322 self.cursor.execute(index_sql2)
324 # create index for key property
325 if spec.key:
326 if __debug__:
327 print >>hyperdb.DEBUG, 'update_class setting keyprop %r'% \
328 spec.key
329 index_sql3 = 'create index _%s_%s_idx on _%s(_%s)'%(
330 spec.classname, spec.key,
331 spec.classname, spec.key)
332 if __debug__:
333 print >>hyperdb.DEBUG, 'create_index', (self, index_sql3)
334 self.cursor.execute(index_sql3)
336 def drop_class_table_indexes(self, cn, key):
337 # drop the old table indexes first
338 l = ['_%s_id_idx'%cn, '_%s_retired_idx'%cn]
339 if key:
340 # key prop too?
341 l.append('_%s_%s_idx'%(cn, key))
343 # TODO: update all the other index dropping code
344 table_name = '_%s'%cn
345 for index_name in l:
346 if not self.sql_index_exists(table_name, index_name):
347 continue
348 index_sql = 'drop index '+index_name
349 if __debug__:
350 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
351 self.cursor.execute(index_sql)
353 def create_journal_table(self, spec):
354 ''' create the journal table for a class given the spec and
355 already-determined cols
356 '''
357 # journal table
358 cols = ','.join(['%s varchar'%x
359 for x in 'nodeid date tag action params'.split()])
360 sql = 'create table %s__journal (%s)'%(spec.classname, cols)
361 if __debug__:
362 print >>hyperdb.DEBUG, 'create_class', (self, sql)
363 self.cursor.execute(sql)
364 self.create_journal_table_indexes(spec)
366 def create_journal_table_indexes(self, spec):
367 # index on nodeid
368 index_sql = 'create index %s_journ_idx on %s__journal(nodeid)'%(
369 spec.classname, spec.classname)
370 if __debug__:
371 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
372 self.cursor.execute(index_sql)
374 def drop_journal_table_indexes(self, classname):
375 index_name = '%s_journ_idx'%classname
376 if not self.sql_index_exists('%s__journal'%classname, index_name):
377 return
378 index_sql = 'drop index '+index_name
379 if __debug__:
380 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
381 self.cursor.execute(index_sql)
383 def create_multilink_table(self, spec, ml):
384 ''' Create a multilink table for the "ml" property of the class
385 given by the spec
386 '''
387 # create the table
388 sql = 'create table %s_%s (linkid varchar, nodeid varchar)'%(
389 spec.classname, ml)
390 if __debug__:
391 print >>hyperdb.DEBUG, 'create_class', (self, sql)
392 self.cursor.execute(sql)
393 self.create_multilink_table_indexes(spec, ml)
395 def create_multilink_table_indexes(self, spec, ml):
396 # create index on linkid
397 index_sql = 'create index %s_%s_l_idx on %s_%s(linkid)'%(
398 spec.classname, ml, spec.classname, ml)
399 if __debug__:
400 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
401 self.cursor.execute(index_sql)
403 # create index on nodeid
404 index_sql = 'create index %s_%s_n_idx on %s_%s(nodeid)'%(
405 spec.classname, ml, spec.classname, ml)
406 if __debug__:
407 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
408 self.cursor.execute(index_sql)
410 def drop_multilink_table_indexes(self, classname, ml):
411 l = [
412 '%s_%s_l_idx'%(classname, ml),
413 '%s_%s_n_idx'%(classname, ml)
414 ]
415 table_name = '%s_%s'%(classname, ml)
416 for index_name in l:
417 if not self.sql_index_exists(table_name, index_name):
418 continue
419 index_sql = 'drop index %s'%index_name
420 if __debug__:
421 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
422 self.cursor.execute(index_sql)
424 def create_class(self, spec):
425 ''' Create a database table according to the given spec.
426 '''
427 cols, mls = self.create_class_table(spec)
428 self.create_journal_table(spec)
430 # now create the multilink tables
431 for ml in mls:
432 self.create_multilink_table(spec, ml)
434 # ID counter
435 sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
436 vals = (spec.classname, 1)
437 if __debug__:
438 print >>hyperdb.DEBUG, 'create_class', (self, sql, vals)
439 self.cursor.execute(sql, vals)
441 def drop_class(self, cn, spec):
442 ''' Drop the given table from the database.
444 Drop the journal and multilink tables too.
445 '''
446 properties = spec[1]
447 # figure the multilinks
448 mls = []
449 for propanme, prop in properties:
450 if isinstance(prop, Multilink):
451 mls.append(propname)
453 # drop class table and indexes
454 self.drop_class_table_indexes(cn, spec[0])
455 sql = 'drop table _%s'%cn
456 if __debug__:
457 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
458 self.cursor.execute(sql)
460 # drop journal table and indexes
461 self.drop_journal_table_indexes(cn)
462 sql = 'drop table %s__journal'%cn
463 if __debug__:
464 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
465 self.cursor.execute(sql)
467 for ml in mls:
468 # drop multilink table and indexes
469 self.drop_multilink_table_indexes(cn, ml)
470 sql = 'drop table %s_%s'%(spec.classname, ml)
471 if __debug__:
472 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
473 self.cursor.execute(sql)
475 #
476 # Classes
477 #
478 def __getattr__(self, classname):
479 ''' A convenient way of calling self.getclass(classname).
480 '''
481 if self.classes.has_key(classname):
482 if __debug__:
483 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
484 return self.classes[classname]
485 raise AttributeError, classname
487 def addclass(self, cl):
488 ''' Add a Class to the hyperdatabase.
489 '''
490 if __debug__:
491 print >>hyperdb.DEBUG, 'addclass', (self, cl)
492 cn = cl.classname
493 if self.classes.has_key(cn):
494 raise ValueError, cn
495 self.classes[cn] = cl
497 def getclasses(self):
498 ''' Return a list of the names of all existing classes.
499 '''
500 if __debug__:
501 print >>hyperdb.DEBUG, 'getclasses', (self,)
502 l = self.classes.keys()
503 l.sort()
504 return l
506 def getclass(self, classname):
507 '''Get the Class object representing a particular class.
509 If 'classname' is not a valid class name, a KeyError is raised.
510 '''
511 if __debug__:
512 print >>hyperdb.DEBUG, 'getclass', (self, classname)
513 try:
514 return self.classes[classname]
515 except KeyError:
516 raise KeyError, 'There is no class called "%s"'%classname
518 def clear(self):
519 '''Delete all database contents.
521 Note: I don't commit here, which is different behaviour to the
522 "nuke from orbit" behaviour in the dbs.
523 '''
524 if __debug__:
525 print >>hyperdb.DEBUG, 'clear', (self,)
526 for cn in self.classes.keys():
527 sql = 'delete from _%s'%cn
528 if __debug__:
529 print >>hyperdb.DEBUG, 'clear', (self, sql)
530 self.cursor.execute(sql)
532 #
533 # Node IDs
534 #
535 def newid(self, classname):
536 ''' Generate a new id for the given class
537 '''
538 # get the next ID
539 sql = 'select num from ids where name=%s'%self.arg
540 if __debug__:
541 print >>hyperdb.DEBUG, 'newid', (self, sql, classname)
542 self.cursor.execute(sql, (classname, ))
543 newid = self.cursor.fetchone()[0]
545 # update the counter
546 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
547 vals = (int(newid)+1, classname)
548 if __debug__:
549 print >>hyperdb.DEBUG, 'newid', (self, sql, vals)
550 self.cursor.execute(sql, vals)
552 # return as string
553 return str(newid)
555 def setid(self, classname, setid):
556 ''' Set the id counter: used during import of database
557 '''
558 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
559 vals = (setid, classname)
560 if __debug__:
561 print >>hyperdb.DEBUG, 'setid', (self, sql, vals)
562 self.cursor.execute(sql, vals)
564 #
565 # Nodes
566 #
567 def addnode(self, classname, nodeid, node):
568 ''' Add the specified node to its class's db.
569 '''
570 if __debug__:
571 print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
573 # determine the column definitions and multilink tables
574 cl = self.classes[classname]
575 cols, mls = self.determine_columns(cl.properties.items())
577 # we'll be supplied these props if we're doing an import
578 if not node.has_key('creator'):
579 # add in the "calculated" properties (dupe so we don't affect
580 # calling code's node assumptions)
581 node = node.copy()
582 node['creation'] = node['activity'] = date.Date()
583 node['creator'] = self.getuid()
585 # default the non-multilink columns
586 for col, prop in cl.properties.items():
587 if not node.has_key(col):
588 if isinstance(prop, Multilink):
589 node[col] = []
590 else:
591 node[col] = None
593 # clear this node out of the cache if it's in there
594 key = (classname, nodeid)
595 if self.cache.has_key(key):
596 del self.cache[key]
597 self.cache_lru.remove(key)
599 # make the node data safe for the DB
600 node = self.serialise(classname, node)
602 # make sure the ordering is correct for column name -> column value
603 vals = tuple([node[col[1:]] for col in cols]) + (nodeid, 0)
604 s = ','.join([self.arg for x in cols]) + ',%s,%s'%(self.arg, self.arg)
605 cols = ','.join(cols) + ',id,__retired__'
607 # perform the inserts
608 sql = 'insert into _%s (%s) values (%s)'%(classname, cols, s)
609 if __debug__:
610 print >>hyperdb.DEBUG, 'addnode', (self, sql, vals)
611 self.cursor.execute(sql, vals)
613 # insert the multilink rows
614 for col in mls:
615 t = '%s_%s'%(classname, col)
616 for entry in node[col]:
617 sql = 'insert into %s (linkid, nodeid) values (%s,%s)'%(t,
618 self.arg, self.arg)
619 self.sql(sql, (entry, nodeid))
621 # make sure we do the commit-time extra stuff for this node
622 self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
624 def setnode(self, classname, nodeid, values, multilink_changes):
625 ''' Change the specified node.
626 '''
627 if __debug__:
628 print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, values)
630 # clear this node out of the cache if it's in there
631 key = (classname, nodeid)
632 if self.cache.has_key(key):
633 del self.cache[key]
634 self.cache_lru.remove(key)
636 # add the special props
637 values = values.copy()
638 values['activity'] = date.Date()
640 # make db-friendly
641 values = self.serialise(classname, values)
643 cl = self.classes[classname]
644 cols = []
645 mls = []
646 # add the multilinks separately
647 props = cl.getprops()
648 for col in values.keys():
649 prop = props[col]
650 if isinstance(prop, Multilink):
651 mls.append(col)
652 else:
653 cols.append('_'+col)
654 cols.sort()
656 # if there's any updates to regular columns, do them
657 if cols:
658 # make sure the ordering is correct for column name -> column value
659 sqlvals = tuple([values[col[1:]] for col in cols]) + (nodeid,)
660 s = ','.join(['%s=%s'%(x, self.arg) for x in cols])
661 cols = ','.join(cols)
663 # perform the update
664 sql = 'update _%s set %s where id=%s'%(classname, s, self.arg)
665 if __debug__:
666 print >>hyperdb.DEBUG, 'setnode', (self, sql, sqlvals)
667 self.cursor.execute(sql, sqlvals)
669 # now the fun bit, updating the multilinks ;)
670 for col, (add, remove) in multilink_changes.items():
671 tn = '%s_%s'%(classname, col)
672 if add:
673 sql = 'insert into %s (nodeid, linkid) values (%s,%s)'%(tn,
674 self.arg, self.arg)
675 for addid in add:
676 self.sql(sql, (nodeid, addid))
677 if remove:
678 sql = 'delete from %s where nodeid=%s and linkid=%s'%(tn,
679 self.arg, self.arg)
680 for removeid in remove:
681 self.sql(sql, (nodeid, removeid))
683 # make sure we do the commit-time extra stuff for this node
684 self.transactions.append((self.doSaveNode, (classname, nodeid, values)))
686 def getnode(self, classname, nodeid):
687 ''' Get a node from the database.
688 '''
689 if __debug__:
690 print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid)
692 # see if we have this node cached
693 key = (classname, nodeid)
694 if self.cache.has_key(key):
695 # push us back to the top of the LRU
696 self.cache_lru.remove(key)
697 self.cache_lru.insert(0, key)
698 # return the cached information
699 return self.cache[key]
701 # figure the columns we're fetching
702 cl = self.classes[classname]
703 cols, mls = self.determine_columns(cl.properties.items())
704 scols = ','.join(cols)
706 # perform the basic property fetch
707 sql = 'select %s from _%s where id=%s'%(scols, classname, self.arg)
708 self.sql(sql, (nodeid,))
710 values = self.sql_fetchone()
711 if values is None:
712 raise IndexError, 'no such %s node %s'%(classname, nodeid)
714 # make up the node
715 node = {}
716 for col in range(len(cols)):
717 node[cols[col][1:]] = values[col]
719 # now the multilinks
720 for col in mls:
721 # get the link ids
722 sql = 'select linkid from %s_%s where nodeid=%s'%(classname, col,
723 self.arg)
724 self.cursor.execute(sql, (nodeid,))
725 # extract the first column from the result
726 node[col] = [x[0] for x in self.cursor.fetchall()]
728 # un-dbificate the node data
729 node = self.unserialise(classname, node)
731 # save off in the cache
732 key = (classname, nodeid)
733 self.cache[key] = node
734 # update the LRU
735 self.cache_lru.insert(0, key)
736 if len(self.cache_lru) > ROW_CACHE_SIZE:
737 del self.cache[self.cache_lru.pop()]
739 return node
741 def destroynode(self, classname, nodeid):
742 '''Remove a node from the database. Called exclusively by the
743 destroy() method on Class.
744 '''
745 if __debug__:
746 print >>hyperdb.DEBUG, 'destroynode', (self, classname, nodeid)
748 # make sure the node exists
749 if not self.hasnode(classname, nodeid):
750 raise IndexError, '%s has no node %s'%(classname, nodeid)
752 # see if we have this node cached
753 if self.cache.has_key((classname, nodeid)):
754 del self.cache[(classname, nodeid)]
756 # see if there's any obvious commit actions that we should get rid of
757 for entry in self.transactions[:]:
758 if entry[1][:2] == (classname, nodeid):
759 self.transactions.remove(entry)
761 # now do the SQL
762 sql = 'delete from _%s where id=%s'%(classname, self.arg)
763 self.sql(sql, (nodeid,))
765 # remove from multilnks
766 cl = self.getclass(classname)
767 x, mls = self.determine_columns(cl.properties.items())
768 for col in mls:
769 # get the link ids
770 sql = 'delete from %s_%s where nodeid=%s'%(classname, col, self.arg)
771 self.sql(sql, (nodeid,))
773 # remove journal entries
774 sql = 'delete from %s__journal where nodeid=%s'%(classname, self.arg)
775 self.sql(sql, (nodeid,))
777 def serialise(self, classname, node):
778 '''Copy the node contents, converting non-marshallable data into
779 marshallable data.
780 '''
781 if __debug__:
782 print >>hyperdb.DEBUG, 'serialise', classname, node
783 properties = self.getclass(classname).getprops()
784 d = {}
785 for k, v in node.items():
786 # if the property doesn't exist, or is the "retired" flag then
787 # it won't be in the properties dict
788 if not properties.has_key(k):
789 d[k] = v
790 continue
792 # get the property spec
793 prop = properties[k]
795 if isinstance(prop, Password) and v is not None:
796 d[k] = str(v)
797 elif isinstance(prop, Date) and v is not None:
798 d[k] = v.serialise()
799 elif isinstance(prop, Interval) and v is not None:
800 d[k] = v.serialise()
801 else:
802 d[k] = v
803 return d
805 def unserialise(self, classname, node):
806 '''Decode the marshalled node data
807 '''
808 if __debug__:
809 print >>hyperdb.DEBUG, 'unserialise', classname, node
810 properties = self.getclass(classname).getprops()
811 d = {}
812 for k, v in node.items():
813 # if the property doesn't exist, or is the "retired" flag then
814 # it won't be in the properties dict
815 if not properties.has_key(k):
816 d[k] = v
817 continue
819 # get the property spec
820 prop = properties[k]
822 if isinstance(prop, Date) and v is not None:
823 d[k] = date.Date(v)
824 elif isinstance(prop, Interval) and v is not None:
825 d[k] = date.Interval(v)
826 elif isinstance(prop, Password) and v is not None:
827 p = password.Password()
828 p.unpack(v)
829 d[k] = p
830 elif isinstance(prop, Boolean) and v is not None:
831 d[k] = int(v)
832 elif isinstance(prop, Number) and v is not None:
833 # try int first, then assume it's a float
834 try:
835 d[k] = int(v)
836 except ValueError:
837 d[k] = float(v)
838 else:
839 d[k] = v
840 return d
842 def hasnode(self, classname, nodeid):
843 ''' Determine if the database has a given node.
844 '''
845 sql = 'select count(*) from _%s where id=%s'%(classname, self.arg)
846 if __debug__:
847 print >>hyperdb.DEBUG, 'hasnode', (self, sql, nodeid)
848 self.cursor.execute(sql, (nodeid,))
849 return int(self.cursor.fetchone()[0])
851 def countnodes(self, classname):
852 ''' Count the number of nodes that exist for a particular Class.
853 '''
854 sql = 'select count(*) from _%s'%classname
855 if __debug__:
856 print >>hyperdb.DEBUG, 'countnodes', (self, sql)
857 self.cursor.execute(sql)
858 return self.cursor.fetchone()[0]
860 def addjournal(self, classname, nodeid, action, params, creator=None,
861 creation=None):
862 ''' Journal the Action
863 'action' may be:
865 'create' or 'set' -- 'params' is a dictionary of property values
866 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
867 'retire' -- 'params' is None
868 '''
869 # serialise the parameters now if necessary
870 if isinstance(params, type({})):
871 if action in ('set', 'create'):
872 params = self.serialise(classname, params)
874 # handle supply of the special journalling parameters (usually
875 # supplied on importing an existing database)
876 if creator:
877 journaltag = creator
878 else:
879 journaltag = self.getuid()
880 if creation:
881 journaldate = creation.serialise()
882 else:
883 journaldate = date.Date().serialise()
885 # create the journal entry
886 cols = ','.join('nodeid date tag action params'.split())
888 if __debug__:
889 print >>hyperdb.DEBUG, 'addjournal', (nodeid, journaldate,
890 journaltag, action, params)
892 self.save_journal(classname, cols, nodeid, journaldate,
893 journaltag, action, params)
895 def getjournal(self, classname, nodeid):
896 ''' get the journal for id
897 '''
898 # make sure the node exists
899 if not self.hasnode(classname, nodeid):
900 raise IndexError, '%s has no node %s'%(classname, nodeid)
902 cols = ','.join('nodeid date tag action params'.split())
903 return self.load_journal(classname, cols, nodeid)
905 def save_journal(self, classname, cols, nodeid, journaldate,
906 journaltag, action, params):
907 ''' Save the journal entry to the database
908 '''
909 # make the params db-friendly
910 params = repr(params)
911 entry = (nodeid, journaldate, journaltag, action, params)
913 # do the insert
914 a = self.arg
915 sql = 'insert into %s__journal (%s) values (%s,%s,%s,%s,%s)'%(classname,
916 cols, a, a, a, a, a)
917 if __debug__:
918 print >>hyperdb.DEBUG, 'addjournal', (self, sql, entry)
919 self.cursor.execute(sql, entry)
921 def load_journal(self, classname, cols, nodeid):
922 ''' Load the journal from the database
923 '''
924 # now get the journal entries
925 sql = 'select %s from %s__journal where nodeid=%s'%(cols, classname,
926 self.arg)
927 if __debug__:
928 print >>hyperdb.DEBUG, 'load_journal', (self, sql, nodeid)
929 self.cursor.execute(sql, (nodeid,))
930 res = []
931 for nodeid, date_stamp, user, action, params in self.cursor.fetchall():
932 params = eval(params)
933 res.append((nodeid, date.Date(date_stamp), user, action, params))
934 return res
936 def pack(self, pack_before):
937 ''' Delete all journal entries except "create" before 'pack_before'.
938 '''
939 # get a 'yyyymmddhhmmss' version of the date
940 date_stamp = pack_before.serialise()
942 # do the delete
943 for classname in self.classes.keys():
944 sql = "delete from %s__journal where date<%s and "\
945 "action<>'create'"%(classname, self.arg)
946 if __debug__:
947 print >>hyperdb.DEBUG, 'pack', (self, sql, date_stamp)
948 self.cursor.execute(sql, (date_stamp,))
950 def sql_commit(self):
951 ''' Actually commit to the database.
952 '''
953 self.conn.commit()
955 def commit(self):
956 ''' Commit the current transactions.
958 Save all data changed since the database was opened or since the
959 last commit() or rollback().
960 '''
961 if __debug__:
962 print >>hyperdb.DEBUG, 'commit', (self,)
964 # commit the database
965 self.sql_commit()
967 # now, do all the other transaction stuff
968 reindex = {}
969 for method, args in self.transactions:
970 reindex[method(*args)] = 1
972 # reindex the nodes that request it
973 for classname, nodeid in filter(None, reindex.keys()):
974 print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
975 self.getclass(classname).index(nodeid)
977 # save the indexer state
978 self.indexer.save_index()
980 # clear out the transactions
981 self.transactions = []
983 def sql_rollback(self):
984 self.conn.rollback()
986 def rollback(self):
987 ''' Reverse all actions from the current transaction.
989 Undo all the changes made since the database was opened or the last
990 commit() or rollback() was performed.
991 '''
992 if __debug__:
993 print >>hyperdb.DEBUG, 'rollback', (self,)
995 self.sql_rollback()
997 # roll back "other" transaction stuff
998 for method, args in self.transactions:
999 # delete temporary files
1000 if method == self.doStoreFile:
1001 self.rollbackStoreFile(*args)
1002 self.transactions = []
1004 # clear the cache
1005 self.clearCache()
1007 def doSaveNode(self, classname, nodeid, node):
1008 ''' dummy that just generates a reindex event
1009 '''
1010 # return the classname, nodeid so we reindex this content
1011 return (classname, nodeid)
1013 def sql_close(self):
1014 self.conn.close()
1016 def close(self):
1017 ''' Close off the connection.
1018 '''
1019 self.sql_close()
1020 if self.lockfile is not None:
1021 locking.release_lock(self.lockfile)
1022 if self.lockfile is not None:
1023 self.lockfile.close()
1024 self.lockfile = None
1026 #
1027 # The base Class class
1028 #
1029 class Class(hyperdb.Class):
1030 ''' The handle to a particular class of nodes in a hyperdatabase.
1032 All methods except __repr__ and getnode must be implemented by a
1033 concrete backend Class.
1034 '''
1036 def __init__(self, db, classname, **properties):
1037 '''Create a new class with a given name and property specification.
1039 'classname' must not collide with the name of an existing class,
1040 or a ValueError is raised. The keyword arguments in 'properties'
1041 must map names to property objects, or a TypeError is raised.
1042 '''
1043 if (properties.has_key('creation') or properties.has_key('activity')
1044 or properties.has_key('creator')):
1045 raise ValueError, '"creation", "activity" and "creator" are '\
1046 'reserved'
1048 self.classname = classname
1049 self.properties = properties
1050 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
1051 self.key = ''
1053 # should we journal changes (default yes)
1054 self.do_journal = 1
1056 # do the db-related init stuff
1057 db.addclass(self)
1059 self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
1060 self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
1062 def schema(self):
1063 ''' A dumpable version of the schema that we can store in the
1064 database
1065 '''
1066 return (self.key, [(x, repr(y)) for x,y in self.properties.items()])
1068 def enableJournalling(self):
1069 '''Turn journalling on for this class
1070 '''
1071 self.do_journal = 1
1073 def disableJournalling(self):
1074 '''Turn journalling off for this class
1075 '''
1076 self.do_journal = 0
1078 # Editing nodes:
1079 def create(self, **propvalues):
1080 ''' Create a new node of this class and return its id.
1082 The keyword arguments in 'propvalues' map property names to values.
1084 The values of arguments must be acceptable for the types of their
1085 corresponding properties or a TypeError is raised.
1087 If this class has a key property, it must be present and its value
1088 must not collide with other key strings or a ValueError is raised.
1090 Any other properties on this class that are missing from the
1091 'propvalues' dictionary are set to None.
1093 If an id in a link or multilink property does not refer to a valid
1094 node, an IndexError is raised.
1095 '''
1096 self.fireAuditors('create', None, propvalues)
1097 newid = self.create_inner(**propvalues)
1098 self.fireReactors('create', newid, None)
1099 return newid
1101 def create_inner(self, **propvalues):
1102 ''' Called by create, in-between the audit and react calls.
1103 '''
1104 if propvalues.has_key('id'):
1105 raise KeyError, '"id" is reserved'
1107 if self.db.journaltag is None:
1108 raise DatabaseError, 'Database open read-only'
1110 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1111 raise KeyError, '"creation" and "activity" are reserved'
1113 # new node's id
1114 newid = self.db.newid(self.classname)
1116 # validate propvalues
1117 num_re = re.compile('^\d+$')
1118 for key, value in propvalues.items():
1119 if key == self.key:
1120 try:
1121 self.lookup(value)
1122 except KeyError:
1123 pass
1124 else:
1125 raise ValueError, 'node with key "%s" exists'%value
1127 # try to handle this property
1128 try:
1129 prop = self.properties[key]
1130 except KeyError:
1131 raise KeyError, '"%s" has no property "%s"'%(self.classname,
1132 key)
1134 if value is not None and isinstance(prop, Link):
1135 if type(value) != type(''):
1136 raise ValueError, 'link value must be String'
1137 link_class = self.properties[key].classname
1138 # if it isn't a number, it's a key
1139 if not num_re.match(value):
1140 try:
1141 value = self.db.classes[link_class].lookup(value)
1142 except (TypeError, KeyError):
1143 raise IndexError, 'new property "%s": %s not a %s'%(
1144 key, value, link_class)
1145 elif not self.db.getclass(link_class).hasnode(value):
1146 raise IndexError, '%s has no node %s'%(link_class, value)
1148 # save off the value
1149 propvalues[key] = value
1151 # register the link with the newly linked node
1152 if self.do_journal and self.properties[key].do_journal:
1153 self.db.addjournal(link_class, value, 'link',
1154 (self.classname, newid, key))
1156 elif isinstance(prop, Multilink):
1157 if type(value) != type([]):
1158 raise TypeError, 'new property "%s" not a list of ids'%key
1160 # clean up and validate the list of links
1161 link_class = self.properties[key].classname
1162 l = []
1163 for entry in value:
1164 if type(entry) != type(''):
1165 raise ValueError, '"%s" multilink value (%r) '\
1166 'must contain Strings'%(key, value)
1167 # if it isn't a number, it's a key
1168 if not num_re.match(entry):
1169 try:
1170 entry = self.db.classes[link_class].lookup(entry)
1171 except (TypeError, KeyError):
1172 raise IndexError, 'new property "%s": %s not a %s'%(
1173 key, entry, self.properties[key].classname)
1174 l.append(entry)
1175 value = l
1176 propvalues[key] = value
1178 # handle additions
1179 for nodeid in value:
1180 if not self.db.getclass(link_class).hasnode(nodeid):
1181 raise IndexError, '%s has no node %s'%(link_class,
1182 nodeid)
1183 # register the link with the newly linked node
1184 if self.do_journal and self.properties[key].do_journal:
1185 self.db.addjournal(link_class, nodeid, 'link',
1186 (self.classname, newid, key))
1188 elif isinstance(prop, String):
1189 if type(value) != type('') and type(value) != type(u''):
1190 raise TypeError, 'new property "%s" not a string'%key
1192 elif isinstance(prop, Password):
1193 if not isinstance(value, password.Password):
1194 raise TypeError, 'new property "%s" not a Password'%key
1196 elif isinstance(prop, Date):
1197 if value is not None and not isinstance(value, date.Date):
1198 raise TypeError, 'new property "%s" not a Date'%key
1200 elif isinstance(prop, Interval):
1201 if value is not None and not isinstance(value, date.Interval):
1202 raise TypeError, 'new property "%s" not an Interval'%key
1204 elif value is not None and isinstance(prop, Number):
1205 try:
1206 float(value)
1207 except ValueError:
1208 raise TypeError, 'new property "%s" not numeric'%key
1210 elif value is not None and isinstance(prop, Boolean):
1211 try:
1212 int(value)
1213 except ValueError:
1214 raise TypeError, 'new property "%s" not boolean'%key
1216 # make sure there's data where there needs to be
1217 for key, prop in self.properties.items():
1218 if propvalues.has_key(key):
1219 continue
1220 if key == self.key:
1221 raise ValueError, 'key property "%s" is required'%key
1222 if isinstance(prop, Multilink):
1223 propvalues[key] = []
1224 else:
1225 propvalues[key] = None
1227 # done
1228 self.db.addnode(self.classname, newid, propvalues)
1229 if self.do_journal:
1230 self.db.addjournal(self.classname, newid, 'create', {})
1232 return newid
1234 def export_list(self, propnames, nodeid):
1235 ''' Export a node - generate a list of CSV-able data in the order
1236 specified by propnames for the given node.
1237 '''
1238 properties = self.getprops()
1239 l = []
1240 for prop in propnames:
1241 proptype = properties[prop]
1242 value = self.get(nodeid, prop)
1243 # "marshal" data where needed
1244 if value is None:
1245 pass
1246 elif isinstance(proptype, hyperdb.Date):
1247 value = value.get_tuple()
1248 elif isinstance(proptype, hyperdb.Interval):
1249 value = value.get_tuple()
1250 elif isinstance(proptype, hyperdb.Password):
1251 value = str(value)
1252 l.append(repr(value))
1253 l.append(repr(self.is_retired(nodeid)))
1254 return l
1256 def import_list(self, propnames, proplist):
1257 ''' Import a node - all information including "id" is present and
1258 should not be sanity checked. Triggers are not triggered. The
1259 journal should be initialised using the "creator" and "created"
1260 information.
1262 Return the nodeid of the node imported.
1263 '''
1264 if self.db.journaltag is None:
1265 raise DatabaseError, 'Database open read-only'
1266 properties = self.getprops()
1268 # make the new node's property map
1269 d = {}
1270 retire = 0
1271 newid = None
1272 for i in range(len(propnames)):
1273 # Use eval to reverse the repr() used to output the CSV
1274 value = eval(proplist[i])
1276 # Figure the property for this column
1277 propname = propnames[i]
1279 # "unmarshal" where necessary
1280 if propname == 'id':
1281 newid = value
1282 continue
1283 elif propname == 'is retired':
1284 # is the item retired?
1285 if int(value):
1286 retire = 1
1287 continue
1288 elif value is None:
1289 d[propname] = None
1290 continue
1292 prop = properties[propname]
1293 if value is None:
1294 # don't set Nones
1295 continue
1296 elif isinstance(prop, hyperdb.Date):
1297 value = date.Date(value)
1298 elif isinstance(prop, hyperdb.Interval):
1299 value = date.Interval(value)
1300 elif isinstance(prop, hyperdb.Password):
1301 pwd = password.Password()
1302 pwd.unpack(value)
1303 value = pwd
1304 d[propname] = value
1306 # get a new id if necessary
1307 if newid is None:
1308 newid = self.db.newid(self.classname)
1310 # add the node and journal
1311 self.db.addnode(self.classname, newid, d)
1313 # retire?
1314 if retire:
1315 # use the arg for __retired__ to cope with any odd database type
1316 # conversion (hello, sqlite)
1317 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1318 self.db.arg, self.db.arg)
1319 if __debug__:
1320 print >>hyperdb.DEBUG, 'retire', (self, sql, newid)
1321 self.db.cursor.execute(sql, (1, newid))
1323 # extract the extraneous journalling gumpf and nuke it
1324 if d.has_key('creator'):
1325 creator = d['creator']
1326 del d['creator']
1327 else:
1328 creator = None
1329 if d.has_key('creation'):
1330 creation = d['creation']
1331 del d['creation']
1332 else:
1333 creation = None
1334 if d.has_key('activity'):
1335 del d['activity']
1336 self.db.addjournal(self.classname, newid, 'create', {}, creator,
1337 creation)
1338 return newid
1340 _marker = []
1341 def get(self, nodeid, propname, default=_marker, cache=1):
1342 '''Get the value of a property on an existing node of this class.
1344 'nodeid' must be the id of an existing node of this class or an
1345 IndexError is raised. 'propname' must be the name of a property
1346 of this class or a KeyError is raised.
1348 'cache' exists for backwards compatibility, and is not used.
1349 '''
1350 if propname == 'id':
1351 return nodeid
1353 # get the node's dict
1354 d = self.db.getnode(self.classname, nodeid)
1356 if propname == 'creation':
1357 if d.has_key('creation'):
1358 return d['creation']
1359 else:
1360 return date.Date()
1361 if propname == 'activity':
1362 if d.has_key('activity'):
1363 return d['activity']
1364 else:
1365 return date.Date()
1366 if propname == 'creator':
1367 if d.has_key('creator'):
1368 return d['creator']
1369 else:
1370 return self.db.getuid()
1372 # get the property (raises KeyErorr if invalid)
1373 prop = self.properties[propname]
1375 if not d.has_key(propname):
1376 if default is self._marker:
1377 if isinstance(prop, Multilink):
1378 return []
1379 else:
1380 return None
1381 else:
1382 return default
1384 # don't pass our list to other code
1385 if isinstance(prop, Multilink):
1386 return d[propname][:]
1388 return d[propname]
1390 def set(self, nodeid, **propvalues):
1391 '''Modify a property on an existing node of this class.
1393 'nodeid' must be the id of an existing node of this class or an
1394 IndexError is raised.
1396 Each key in 'propvalues' must be the name of a property of this
1397 class or a KeyError is raised.
1399 All values in 'propvalues' must be acceptable types for their
1400 corresponding properties or a TypeError is raised.
1402 If the value of the key property is set, it must not collide with
1403 other key strings or a ValueError is raised.
1405 If the value of a Link or Multilink property contains an invalid
1406 node id, a ValueError is raised.
1407 '''
1408 if not propvalues:
1409 return propvalues
1411 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1412 raise KeyError, '"creation" and "activity" are reserved'
1414 if propvalues.has_key('id'):
1415 raise KeyError, '"id" is reserved'
1417 if self.db.journaltag is None:
1418 raise DatabaseError, 'Database open read-only'
1420 self.fireAuditors('set', nodeid, propvalues)
1421 # Take a copy of the node dict so that the subsequent set
1422 # operation doesn't modify the oldvalues structure.
1423 # XXX used to try the cache here first
1424 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
1426 node = self.db.getnode(self.classname, nodeid)
1427 if self.is_retired(nodeid):
1428 raise IndexError, 'Requested item is retired'
1429 num_re = re.compile('^\d+$')
1431 # if the journal value is to be different, store it in here
1432 journalvalues = {}
1434 # remember the add/remove stuff for multilinks, making it easier
1435 # for the Database layer to do its stuff
1436 multilink_changes = {}
1438 for propname, value in propvalues.items():
1439 # check to make sure we're not duplicating an existing key
1440 if propname == self.key and node[propname] != value:
1441 try:
1442 self.lookup(value)
1443 except KeyError:
1444 pass
1445 else:
1446 raise ValueError, 'node with key "%s" exists'%value
1448 # this will raise the KeyError if the property isn't valid
1449 # ... we don't use getprops() here because we only care about
1450 # the writeable properties.
1451 try:
1452 prop = self.properties[propname]
1453 except KeyError:
1454 raise KeyError, '"%s" has no property named "%s"'%(
1455 self.classname, propname)
1457 # if the value's the same as the existing value, no sense in
1458 # doing anything
1459 current = node.get(propname, None)
1460 if value == current:
1461 del propvalues[propname]
1462 continue
1463 journalvalues[propname] = current
1465 # do stuff based on the prop type
1466 if isinstance(prop, Link):
1467 link_class = prop.classname
1468 # if it isn't a number, it's a key
1469 if value is not None and not isinstance(value, type('')):
1470 raise ValueError, 'property "%s" link value be a string'%(
1471 propname)
1472 if isinstance(value, type('')) and not num_re.match(value):
1473 try:
1474 value = self.db.classes[link_class].lookup(value)
1475 except (TypeError, KeyError):
1476 raise IndexError, 'new property "%s": %s not a %s'%(
1477 propname, value, prop.classname)
1479 if (value is not None and
1480 not self.db.getclass(link_class).hasnode(value)):
1481 raise IndexError, '%s has no node %s'%(link_class, value)
1483 if self.do_journal and prop.do_journal:
1484 # register the unlink with the old linked node
1485 if node[propname] is not None:
1486 self.db.addjournal(link_class, node[propname], 'unlink',
1487 (self.classname, nodeid, propname))
1489 # register the link with the newly linked node
1490 if value is not None:
1491 self.db.addjournal(link_class, value, 'link',
1492 (self.classname, nodeid, propname))
1494 elif isinstance(prop, Multilink):
1495 if type(value) != type([]):
1496 raise TypeError, 'new property "%s" not a list of'\
1497 ' ids'%propname
1498 link_class = self.properties[propname].classname
1499 l = []
1500 for entry in value:
1501 # if it isn't a number, it's a key
1502 if type(entry) != type(''):
1503 raise ValueError, 'new property "%s" link value ' \
1504 'must be a string'%propname
1505 if not num_re.match(entry):
1506 try:
1507 entry = self.db.classes[link_class].lookup(entry)
1508 except (TypeError, KeyError):
1509 raise IndexError, 'new property "%s": %s not a %s'%(
1510 propname, entry,
1511 self.properties[propname].classname)
1512 l.append(entry)
1513 value = l
1514 propvalues[propname] = value
1516 # figure the journal entry for this property
1517 add = []
1518 remove = []
1520 # handle removals
1521 if node.has_key(propname):
1522 l = node[propname]
1523 else:
1524 l = []
1525 for id in l[:]:
1526 if id in value:
1527 continue
1528 # register the unlink with the old linked node
1529 if self.do_journal and self.properties[propname].do_journal:
1530 self.db.addjournal(link_class, id, 'unlink',
1531 (self.classname, nodeid, propname))
1532 l.remove(id)
1533 remove.append(id)
1535 # handle additions
1536 for id in value:
1537 if not self.db.getclass(link_class).hasnode(id):
1538 raise IndexError, '%s has no node %s'%(link_class, id)
1539 if id in l:
1540 continue
1541 # register the link with the newly linked node
1542 if self.do_journal and self.properties[propname].do_journal:
1543 self.db.addjournal(link_class, id, 'link',
1544 (self.classname, nodeid, propname))
1545 l.append(id)
1546 add.append(id)
1548 # figure the journal entry
1549 l = []
1550 if add:
1551 l.append(('+', add))
1552 if remove:
1553 l.append(('-', remove))
1554 multilink_changes[propname] = (add, remove)
1555 if l:
1556 journalvalues[propname] = tuple(l)
1558 elif isinstance(prop, String):
1559 if value is not None and type(value) != type('') and type(value) != type(u''):
1560 raise TypeError, 'new property "%s" not a string'%propname
1562 elif isinstance(prop, Password):
1563 if not isinstance(value, password.Password):
1564 raise TypeError, 'new property "%s" not a Password'%propname
1565 propvalues[propname] = value
1567 elif value is not None and isinstance(prop, Date):
1568 if not isinstance(value, date.Date):
1569 raise TypeError, 'new property "%s" not a Date'% propname
1570 propvalues[propname] = value
1572 elif value is not None and isinstance(prop, Interval):
1573 if not isinstance(value, date.Interval):
1574 raise TypeError, 'new property "%s" not an '\
1575 'Interval'%propname
1576 propvalues[propname] = value
1578 elif value is not None and isinstance(prop, Number):
1579 try:
1580 float(value)
1581 except ValueError:
1582 raise TypeError, 'new property "%s" not numeric'%propname
1584 elif value is not None and isinstance(prop, Boolean):
1585 try:
1586 int(value)
1587 except ValueError:
1588 raise TypeError, 'new property "%s" not boolean'%propname
1590 # nothing to do?
1591 if not propvalues:
1592 return propvalues
1594 # do the set, and journal it
1595 self.db.setnode(self.classname, nodeid, propvalues, multilink_changes)
1597 if self.do_journal:
1598 self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
1600 self.fireReactors('set', nodeid, oldvalues)
1602 return propvalues
1604 def retire(self, nodeid):
1605 '''Retire a node.
1607 The properties on the node remain available from the get() method,
1608 and the node's id is never reused.
1610 Retired nodes are not returned by the find(), list(), or lookup()
1611 methods, and other nodes may reuse the values of their key properties.
1612 '''
1613 if self.db.journaltag is None:
1614 raise DatabaseError, 'Database open read-only'
1616 self.fireAuditors('retire', nodeid, None)
1618 # use the arg for __retired__ to cope with any odd database type
1619 # conversion (hello, sqlite)
1620 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1621 self.db.arg, self.db.arg)
1622 if __debug__:
1623 print >>hyperdb.DEBUG, 'retire', (self, sql, nodeid)
1624 self.db.cursor.execute(sql, (1, nodeid))
1625 if self.do_journal:
1626 self.db.addjournal(self.classname, nodeid, 'retired', None)
1628 self.fireReactors('retire', nodeid, None)
1630 def restore(self, nodeid):
1631 '''Restore a retired node.
1633 Make node available for all operations like it was before retirement.
1634 '''
1635 if self.db.journaltag is None:
1636 raise DatabaseError, 'Database open read-only'
1638 node = self.db.getnode(self.classname, nodeid)
1639 # check if key property was overrided
1640 key = self.getkey()
1641 try:
1642 id = self.lookup(node[key])
1643 except KeyError:
1644 pass
1645 else:
1646 raise KeyError, "Key property (%s) of retired node clashes with \
1647 existing one (%s)" % (key, node[key])
1649 self.fireAuditors('restore', nodeid, None)
1650 # use the arg for __retired__ to cope with any odd database type
1651 # conversion (hello, sqlite)
1652 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1653 self.db.arg, self.db.arg)
1654 if __debug__:
1655 print >>hyperdb.DEBUG, 'restore', (self, sql, nodeid)
1656 self.db.cursor.execute(sql, (0, nodeid))
1657 if self.do_journal:
1658 self.db.addjournal(self.classname, nodeid, 'restored', None)
1660 self.fireReactors('restore', nodeid, None)
1662 def is_retired(self, nodeid):
1663 '''Return true if the node is rerired
1664 '''
1665 sql = 'select __retired__ from _%s where id=%s'%(self.classname,
1666 self.db.arg)
1667 if __debug__:
1668 print >>hyperdb.DEBUG, 'is_retired', (self, sql, nodeid)
1669 self.db.cursor.execute(sql, (nodeid,))
1670 return int(self.db.sql_fetchone()[0])
1672 def destroy(self, nodeid):
1673 '''Destroy a node.
1675 WARNING: this method should never be used except in extremely rare
1676 situations where there could never be links to the node being
1677 deleted
1679 WARNING: use retire() instead
1681 WARNING: the properties of this node will not be available ever again
1683 WARNING: really, use retire() instead
1685 Well, I think that's enough warnings. This method exists mostly to
1686 support the session storage of the cgi interface.
1688 The node is completely removed from the hyperdb, including all journal
1689 entries. It will no longer be available, and will generally break code
1690 if there are any references to the node.
1691 '''
1692 if self.db.journaltag is None:
1693 raise DatabaseError, 'Database open read-only'
1694 self.db.destroynode(self.classname, nodeid)
1696 def history(self, nodeid):
1697 '''Retrieve the journal of edits on a particular node.
1699 'nodeid' must be the id of an existing node of this class or an
1700 IndexError is raised.
1702 The returned list contains tuples of the form
1704 (nodeid, date, tag, action, params)
1706 'date' is a Timestamp object specifying the time of the change and
1707 'tag' is the journaltag specified when the database was opened.
1708 '''
1709 if not self.do_journal:
1710 raise ValueError, 'Journalling is disabled for this class'
1711 return self.db.getjournal(self.classname, nodeid)
1713 # Locating nodes:
1714 def hasnode(self, nodeid):
1715 '''Determine if the given nodeid actually exists
1716 '''
1717 return self.db.hasnode(self.classname, nodeid)
1719 def setkey(self, propname):
1720 '''Select a String property of this class to be the key property.
1722 'propname' must be the name of a String property of this class or
1723 None, or a TypeError is raised. The values of the key property on
1724 all existing nodes must be unique or a ValueError is raised.
1725 '''
1726 # XXX create an index on the key prop column. We should also
1727 # record that we've created this index in the schema somewhere.
1728 prop = self.getprops()[propname]
1729 if not isinstance(prop, String):
1730 raise TypeError, 'key properties must be String'
1731 self.key = propname
1733 def getkey(self):
1734 '''Return the name of the key property for this class or None.'''
1735 return self.key
1737 def labelprop(self, default_to_id=0):
1738 '''Return the property name for a label for the given node.
1740 This method attempts to generate a consistent label for the node.
1741 It tries the following in order:
1743 1. key property
1744 2. "name" property
1745 3. "title" property
1746 4. first property from the sorted property name list
1747 '''
1748 k = self.getkey()
1749 if k:
1750 return k
1751 props = self.getprops()
1752 if props.has_key('name'):
1753 return 'name'
1754 elif props.has_key('title'):
1755 return 'title'
1756 if default_to_id:
1757 return 'id'
1758 props = props.keys()
1759 props.sort()
1760 return props[0]
1762 def lookup(self, keyvalue):
1763 '''Locate a particular node by its key property and return its id.
1765 If this class has no key property, a TypeError is raised. If the
1766 'keyvalue' matches one of the values for the key property among
1767 the nodes in this class, the matching node's id is returned;
1768 otherwise a KeyError is raised.
1769 '''
1770 if not self.key:
1771 raise TypeError, 'No key property set for class %s'%self.classname
1773 # use the arg to handle any odd database type conversion (hello,
1774 # sqlite)
1775 sql = "select id from _%s where _%s=%s and __retired__ <> %s"%(
1776 self.classname, self.key, self.db.arg, self.db.arg)
1777 self.db.sql(sql, (keyvalue, 1))
1779 # see if there was a result that's not retired
1780 row = self.db.sql_fetchone()
1781 if not row:
1782 raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
1783 keyvalue, self.classname)
1785 # return the id
1786 return row[0]
1788 def find(self, **propspec):
1789 '''Get the ids of nodes in this class which link to the given nodes.
1791 'propspec' consists of keyword args propname=nodeid or
1792 propname={nodeid:1, }
1793 'propname' must be the name of a property in this class, or a
1794 KeyError is raised. That property must be a Link or
1795 Multilink property, or a TypeError is raised.
1797 Any node in this class whose 'propname' property links to any of the
1798 nodeids will be returned. Used by the full text indexing, which knows
1799 that "foo" occurs in msg1, msg3 and file7, so we have hits on these
1800 issues:
1802 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1803 '''
1804 if __debug__:
1805 print >>hyperdb.DEBUG, 'find', (self, propspec)
1807 # shortcut
1808 if not propspec:
1809 return []
1811 # validate the args
1812 props = self.getprops()
1813 propspec = propspec.items()
1814 for propname, nodeids in propspec:
1815 # check the prop is OK
1816 prop = props[propname]
1817 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
1818 raise TypeError, "'%s' not a Link/Multilink property"%propname
1820 # first, links
1821 a = self.db.arg
1822 allvalues = (1,)
1823 o = []
1824 where = []
1825 for prop, values in propspec:
1826 if not isinstance(props[prop], hyperdb.Link):
1827 continue
1828 if type(values) is type({}) and len(values) == 1:
1829 values = values.keys()[0]
1830 if type(values) is type(''):
1831 allvalues += (values,)
1832 where.append('_%s = %s'%(prop, a))
1833 elif values is None:
1834 where.append('_%s is NULL'%prop)
1835 else:
1836 allvalues += tuple(values.keys())
1837 where.append('_%s in (%s)'%(prop, ','.join([a]*len(values))))
1838 tables = ['_%s'%self.classname]
1839 if where:
1840 o.append('(' + ' and '.join(where) + ')')
1842 # now multilinks
1843 for prop, values in propspec:
1844 if not isinstance(props[prop], hyperdb.Multilink):
1845 continue
1846 if not values:
1847 continue
1848 if type(values) is type(''):
1849 allvalues += (values,)
1850 s = a
1851 else:
1852 allvalues += tuple(values.keys())
1853 s = ','.join([a]*len(values))
1854 tn = '%s_%s'%(self.classname, prop)
1855 tables.append(tn)
1856 o.append('(id=%s.nodeid and %s.linkid in (%s))'%(tn, tn, s))
1858 if not o:
1859 return []
1860 elif len(o) > 1:
1861 o = '(' + ' or '.join(['(%s)'%i for i in o]) + ')'
1862 else:
1863 o = o[0]
1864 t = ', '.join(tables)
1865 sql = 'select distinct(id) from %s where __retired__ <> %s and %s'%(t, a, o)
1866 self.db.sql(sql, allvalues)
1867 l = [x[0] for x in self.db.sql_fetchall()]
1868 if __debug__:
1869 print >>hyperdb.DEBUG, 'find ... ', l
1870 return l
1872 def stringFind(self, **requirements):
1873 '''Locate a particular node by matching a set of its String
1874 properties in a caseless search.
1876 If the property is not a String property, a TypeError is raised.
1878 The return is a list of the id of all nodes that match.
1879 '''
1880 where = []
1881 args = []
1882 for propname in requirements.keys():
1883 prop = self.properties[propname]
1884 if not isinstance(prop, String):
1885 raise TypeError, "'%s' not a String property"%propname
1886 where.append(propname)
1887 args.append(requirements[propname].lower())
1889 # generate the where clause
1890 s = ' and '.join(['lower(_%s)=%s'%(col, self.db.arg) for col in where])
1891 sql = 'select id from _%s where %s and __retired__=%s'%(self.classname,
1892 s, self.db.arg)
1893 args.append(0)
1894 self.db.sql(sql, tuple(args))
1895 l = [x[0] for x in self.db.sql_fetchall()]
1896 if __debug__:
1897 print >>hyperdb.DEBUG, 'find ... ', l
1898 return l
1900 def list(self):
1901 ''' Return a list of the ids of the active nodes in this class.
1902 '''
1903 return self.getnodeids(retired=0)
1905 def getnodeids(self, retired=None):
1906 ''' Retrieve all the ids of the nodes for a particular Class.
1908 Set retired=None to get all nodes. Otherwise it'll get all the
1909 retired or non-retired nodes, depending on the flag.
1910 '''
1911 # flip the sense of the 'retired' flag if we don't want all of them
1912 if retired is not None:
1913 if retired:
1914 args = (0, )
1915 else:
1916 args = (1, )
1917 sql = 'select id from _%s where __retired__ <> %s'%(self.classname,
1918 self.db.arg)
1919 else:
1920 args = ()
1921 sql = 'select id from _%s'%self.classname
1922 if __debug__:
1923 print >>hyperdb.DEBUG, 'getnodeids', (self, sql, retired)
1924 self.db.cursor.execute(sql, args)
1925 ids = [x[0] for x in self.db.cursor.fetchall()]
1926 return ids
1928 def filter(self, search_matches, filterspec, sort=(None,None),
1929 group=(None,None)):
1930 '''Return a list of the ids of the active nodes in this class that
1931 match the 'filter' spec, sorted by the group spec and then the
1932 sort spec
1934 "filterspec" is {propname: value(s)}
1936 "sort" and "group" are (dir, prop) where dir is '+', '-' or None
1937 and prop is a prop name or None
1939 "search_matches" is {nodeid: marker}
1941 The filter must match all properties specificed - but if the
1942 property value to match is a list, any one of the values in the
1943 list may match for that property to match.
1944 '''
1945 # just don't bother if the full-text search matched diddly
1946 if search_matches == {}:
1947 return []
1949 cn = self.classname
1951 timezone = self.db.getUserTimezone()
1953 # figure the WHERE clause from the filterspec
1954 props = self.getprops()
1955 frum = ['_'+cn]
1956 where = []
1957 args = []
1958 a = self.db.arg
1959 for k, v in filterspec.items():
1960 propclass = props[k]
1961 # now do other where clause stuff
1962 if isinstance(propclass, Multilink):
1963 tn = '%s_%s'%(cn, k)
1964 if v in ('-1', ['-1']):
1965 # only match rows that have count(linkid)=0 in the
1966 # corresponding multilink table)
1967 where.append('id not in (select nodeid from %s)'%tn)
1968 elif isinstance(v, type([])):
1969 frum.append(tn)
1970 s = ','.join([a for x in v])
1971 where.append('id=%s.nodeid and %s.linkid in (%s)'%(tn,tn,s))
1972 args = args + v
1973 else:
1974 frum.append(tn)
1975 where.append('id=%s.nodeid and %s.linkid=%s'%(tn, tn, a))
1976 args.append(v)
1977 elif k == 'id':
1978 if isinstance(v, type([])):
1979 s = ','.join([a for x in v])
1980 where.append('%s in (%s)'%(k, s))
1981 args = args + v
1982 else:
1983 where.append('%s=%s'%(k, a))
1984 args.append(v)
1985 elif isinstance(propclass, String):
1986 if not isinstance(v, type([])):
1987 v = [v]
1989 # Quote the bits in the string that need it and then embed
1990 # in a "substring" search. Note - need to quote the '%' so
1991 # they make it through the python layer happily
1992 v = ['%%'+self.db.sql_stringquote(s)+'%%' for s in v]
1994 # now add to the where clause
1995 where.append(' or '.join(["_%s LIKE '%s'"%(k, s) for s in v]))
1996 # note: args are embedded in the query string now
1997 elif isinstance(propclass, Link):
1998 if isinstance(v, type([])):
1999 if '-1' in v:
2000 v = v[:]
2001 v.remove('-1')
2002 xtra = ' or _%s is NULL'%k
2003 else:
2004 xtra = ''
2005 if v:
2006 s = ','.join([a for x in v])
2007 where.append('(_%s in (%s)%s)'%(k, s, xtra))
2008 args = args + v
2009 else:
2010 where.append('_%s is NULL'%k)
2011 else:
2012 if v == '-1':
2013 v = None
2014 where.append('_%s is NULL'%k)
2015 else:
2016 where.append('_%s=%s'%(k, a))
2017 args.append(v)
2018 elif isinstance(propclass, Date):
2019 if isinstance(v, type([])):
2020 s = ','.join([a for x in v])
2021 where.append('_%s in (%s)'%(k, s))
2022 args = args + [date.Date(x).serialise() for x in v]
2023 else:
2024 try:
2025 # Try to filter on range of dates
2026 date_rng = Range(v, date.Date, offset=timezone)
2027 if (date_rng.from_value):
2028 where.append('_%s >= %s'%(k, a))
2029 args.append(date_rng.from_value.serialise())
2030 if (date_rng.to_value):
2031 where.append('_%s <= %s'%(k, a))
2032 args.append(date_rng.to_value.serialise())
2033 except ValueError:
2034 # If range creation fails - ignore that search parameter
2035 pass
2036 elif isinstance(propclass, Interval):
2037 if isinstance(v, type([])):
2038 s = ','.join([a for x in v])
2039 where.append('_%s in (%s)'%(k, s))
2040 args = args + [date.Interval(x).serialise() for x in v]
2041 else:
2042 try:
2043 # Try to filter on range of intervals
2044 date_rng = Range(v, date.Interval)
2045 if (date_rng.from_value):
2046 where.append('_%s >= %s'%(k, a))
2047 args.append(date_rng.from_value.serialise())
2048 if (date_rng.to_value):
2049 where.append('_%s <= %s'%(k, a))
2050 args.append(date_rng.to_value.serialise())
2051 except ValueError:
2052 # If range creation fails - ignore that search parameter
2053 pass
2054 #where.append('_%s=%s'%(k, a))
2055 #args.append(date.Interval(v).serialise())
2056 else:
2057 if isinstance(v, type([])):
2058 s = ','.join([a for x in v])
2059 where.append('_%s in (%s)'%(k, s))
2060 args = args + v
2061 else:
2062 where.append('_%s=%s'%(k, a))
2063 args.append(v)
2065 # don't match retired nodes
2066 where.append('__retired__ <> 1')
2068 # add results of full text search
2069 if search_matches is not None:
2070 v = search_matches.keys()
2071 s = ','.join([a for x in v])
2072 where.append('id in (%s)'%s)
2073 args = args + v
2075 # "grouping" is just the first-order sorting in the SQL fetch
2076 # can modify it...)
2077 orderby = []
2078 ordercols = []
2079 if group[0] is not None and group[1] is not None:
2080 if group[0] != '-':
2081 orderby.append('_'+group[1])
2082 ordercols.append('_'+group[1])
2083 else:
2084 orderby.append('_'+group[1]+' desc')
2085 ordercols.append('_'+group[1])
2087 # now add in the sorting
2088 group = ''
2089 if sort[0] is not None and sort[1] is not None:
2090 direction, colname = sort
2091 if direction != '-':
2092 if colname == 'id':
2093 orderby.append(colname)
2094 else:
2095 orderby.append('_'+colname)
2096 ordercols.append('_'+colname)
2097 else:
2098 if colname == 'id':
2099 orderby.append(colname+' desc')
2100 ordercols.append(colname)
2101 else:
2102 orderby.append('_'+colname+' desc')
2103 ordercols.append('_'+colname)
2105 # construct the SQL
2106 frum = ','.join(frum)
2107 if where:
2108 where = ' where ' + (' and '.join(where))
2109 else:
2110 where = ''
2111 cols = ['id']
2112 if orderby:
2113 cols = cols + ordercols
2114 order = ' order by %s'%(','.join(orderby))
2115 else:
2116 order = ''
2117 cols = ','.join(cols)
2118 sql = 'select %s from %s %s%s%s'%(cols, frum, where, group, order)
2119 args = tuple(args)
2120 if __debug__:
2121 print >>hyperdb.DEBUG, 'filter', (self, sql, args)
2122 if args:
2123 self.db.cursor.execute(sql, args)
2124 else:
2125 # psycopg doesn't like empty args
2126 self.db.cursor.execute(sql)
2127 l = self.db.sql_fetchall()
2129 # return the IDs (the first column)
2130 return [row[0] for row in l]
2132 def count(self):
2133 '''Get the number of nodes in this class.
2135 If the returned integer is 'numnodes', the ids of all the nodes
2136 in this class run from 1 to numnodes, and numnodes+1 will be the
2137 id of the next node to be created in this class.
2138 '''
2139 return self.db.countnodes(self.classname)
2141 # Manipulating properties:
2142 def getprops(self, protected=1):
2143 '''Return a dictionary mapping property names to property objects.
2144 If the "protected" flag is true, we include protected properties -
2145 those which may not be modified.
2146 '''
2147 d = self.properties.copy()
2148 if protected:
2149 d['id'] = String()
2150 d['creation'] = hyperdb.Date()
2151 d['activity'] = hyperdb.Date()
2152 d['creator'] = hyperdb.Link('user')
2153 return d
2155 def addprop(self, **properties):
2156 '''Add properties to this class.
2158 The keyword arguments in 'properties' must map names to property
2159 objects, or a TypeError is raised. None of the keys in 'properties'
2160 may collide with the names of existing properties, or a ValueError
2161 is raised before any properties have been added.
2162 '''
2163 for key in properties.keys():
2164 if self.properties.has_key(key):
2165 raise ValueError, key
2166 self.properties.update(properties)
2168 def index(self, nodeid):
2169 '''Add (or refresh) the node to search indexes
2170 '''
2171 # find all the String properties that have indexme
2172 for prop, propclass in self.getprops().items():
2173 if isinstance(propclass, String) and propclass.indexme:
2174 try:
2175 value = str(self.get(nodeid, prop))
2176 except IndexError:
2177 # node no longer exists - entry should be removed
2178 self.db.indexer.purge_entry((self.classname, nodeid, prop))
2179 else:
2180 # and index them under (classname, nodeid, property)
2181 self.db.indexer.add_text((self.classname, nodeid, prop),
2182 value)
2185 #
2186 # Detector interface
2187 #
2188 def audit(self, event, detector):
2189 '''Register a detector
2190 '''
2191 l = self.auditors[event]
2192 if detector not in l:
2193 self.auditors[event].append(detector)
2195 def fireAuditors(self, action, nodeid, newvalues):
2196 '''Fire all registered auditors.
2197 '''
2198 for audit in self.auditors[action]:
2199 audit(self.db, self, nodeid, newvalues)
2201 def react(self, event, detector):
2202 '''Register a detector
2203 '''
2204 l = self.reactors[event]
2205 if detector not in l:
2206 self.reactors[event].append(detector)
2208 def fireReactors(self, action, nodeid, oldvalues):
2209 '''Fire all registered reactors.
2210 '''
2211 for react in self.reactors[action]:
2212 react(self.db, self, nodeid, oldvalues)
2214 class FileClass(Class, hyperdb.FileClass):
2215 '''This class defines a large chunk of data. To support this, it has a
2216 mandatory String property "content" which is typically saved off
2217 externally to the hyperdb.
2219 The default MIME type of this data is defined by the
2220 "default_mime_type" class attribute, which may be overridden by each
2221 node if the class defines a "type" String property.
2222 '''
2223 default_mime_type = 'text/plain'
2225 def create(self, **propvalues):
2226 ''' snaffle the file propvalue and store in a file
2227 '''
2228 # we need to fire the auditors now, or the content property won't
2229 # be in propvalues for the auditors to play with
2230 self.fireAuditors('create', None, propvalues)
2232 # now remove the content property so it's not stored in the db
2233 content = propvalues['content']
2234 del propvalues['content']
2236 # do the database create
2237 newid = Class.create_inner(self, **propvalues)
2239 # fire reactors
2240 self.fireReactors('create', newid, None)
2242 # store off the content as a file
2243 self.db.storefile(self.classname, newid, None, content)
2244 return newid
2246 def import_list(self, propnames, proplist):
2247 ''' Trap the "content" property...
2248 '''
2249 # dupe this list so we don't affect others
2250 propnames = propnames[:]
2252 # extract the "content" property from the proplist
2253 i = propnames.index('content')
2254 content = eval(proplist[i])
2255 del propnames[i]
2256 del proplist[i]
2258 # do the normal import
2259 newid = Class.import_list(self, propnames, proplist)
2261 # save off the "content" file
2262 self.db.storefile(self.classname, newid, None, content)
2263 return newid
2265 _marker = []
2266 def get(self, nodeid, propname, default=_marker, cache=1):
2267 ''' Trap the content propname and get it from the file
2269 'cache' exists for backwards compatibility, and is not used.
2270 '''
2271 poss_msg = 'Possibly a access right configuration problem.'
2272 if propname == 'content':
2273 try:
2274 return self.db.getfile(self.classname, nodeid, None)
2275 except IOError, (strerror):
2276 # BUG: by catching this we donot see an error in the log.
2277 return 'ERROR reading file: %s%s\n%s\n%s'%(
2278 self.classname, nodeid, poss_msg, strerror)
2279 if default is not self._marker:
2280 return Class.get(self, nodeid, propname, default)
2281 else:
2282 return Class.get(self, nodeid, propname)
2284 def getprops(self, protected=1):
2285 ''' In addition to the actual properties on the node, these methods
2286 provide the "content" property. If the "protected" flag is true,
2287 we include protected properties - those which may not be
2288 modified.
2289 '''
2290 d = Class.getprops(self, protected=protected).copy()
2291 d['content'] = hyperdb.String()
2292 return d
2294 def index(self, nodeid):
2295 ''' Index the node in the search index.
2297 We want to index the content in addition to the normal String
2298 property indexing.
2299 '''
2300 # perform normal indexing
2301 Class.index(self, nodeid)
2303 # get the content to index
2304 content = self.get(nodeid, 'content')
2306 # figure the mime type
2307 if self.properties.has_key('type'):
2308 mime_type = self.get(nodeid, 'type')
2309 else:
2310 mime_type = self.default_mime_type
2312 # and index!
2313 self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
2314 mime_type)
2316 # XXX deviation from spec - was called ItemClass
2317 class IssueClass(Class, roundupdb.IssueClass):
2318 # Overridden methods:
2319 def __init__(self, db, classname, **properties):
2320 '''The newly-created class automatically includes the "messages",
2321 "files", "nosy", and "superseder" properties. If the 'properties'
2322 dictionary attempts to specify any of these properties or a
2323 "creation" or "activity" property, a ValueError is raised.
2324 '''
2325 if not properties.has_key('title'):
2326 properties['title'] = hyperdb.String(indexme='yes')
2327 if not properties.has_key('messages'):
2328 properties['messages'] = hyperdb.Multilink("msg")
2329 if not properties.has_key('files'):
2330 properties['files'] = hyperdb.Multilink("file")
2331 if not properties.has_key('nosy'):
2332 # note: journalling is turned off as it really just wastes
2333 # space. this behaviour may be overridden in an instance
2334 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
2335 if not properties.has_key('superseder'):
2336 properties['superseder'] = hyperdb.Multilink(classname)
2337 Class.__init__(self, db, classname, **properties)