34828679aaa7a389b68305a2dc2a36f06e039a3e
1 # $Id: back_metakit.py,v 1.47 2003-05-09 01:47:50 richard Exp $
2 '''
3 Metakit backend for Roundup, originally by Gordon McMillan.
5 Notes by Richard:
7 This backend has some behaviour specific to metakit:
9 - there's no concept of an explicit "unset" in metakit, so all types
10 have some "unset" value:
12 ========= ===== ====================================================
13 Type Value Action when fetching from mk
14 ========= ===== ====================================================
15 Strings '' convert to None
16 Date 0 (seconds since 1970-01-01.00:00:00) convert to None
17 Interval '' convert to None
18 Number 0 ambiguious :( - do nothing
19 Boolean 0 ambiguious :( - do nothing
20 Link 0 convert to None
21 Multilink [] actually, mk can handle this one ;)
22 Passowrd '' convert to None
23 ========= ===== ====================================================
25 The get/set routines handle these values accordingly by converting
26 to/from None where they can. The Number/Boolean types are not able
27 to handle an "unset" at all, so they default the "unset" to 0.
29 - probably a bunch of stuff that I'm not aware of yet because I haven't
30 fully read through the source. One of these days....
31 '''
32 from roundup import hyperdb, date, password, roundupdb, security
33 import metakit
34 from sessions import Sessions, OneTimeKeys
35 import re, marshal, os, sys, weakref, time, calendar
36 from roundup import indexer
37 import locking
38 from roundup.date import Range
40 _dbs = {}
42 def Database(config, journaltag=None):
43 ''' Only have a single instance of the Database class for each instance
44 '''
45 db = _dbs.get(config.DATABASE, None)
46 if db is None or db._db is None:
47 db = _Database(config, journaltag)
48 _dbs[config.DATABASE] = db
49 else:
50 db.journaltag = journaltag
51 try:
52 delattr(db, 'curuserid')
53 except AttributeError:
54 pass
55 return db
57 class _Database(hyperdb.Database, roundupdb.Database):
58 def __init__(self, config, journaltag=None):
59 self.config = config
60 self.journaltag = journaltag
61 self.classes = {}
62 self.dirty = 0
63 self.lockfile = None
64 self._db = self.__open()
65 self.indexer = Indexer(self.config.DATABASE, self._db)
66 self.sessions = Sessions(self.config)
67 self.otks = OneTimeKeys(self.config)
68 self.security = security.Security(self)
70 os.umask(0002)
72 def post_init(self):
73 if self.indexer.should_reindex():
74 self.reindex()
76 def reindex(self):
77 for klass in self.classes.values():
78 for nodeid in klass.list():
79 klass.index(nodeid)
80 self.indexer.save_index()
82 # --- defined in ping's spec
83 def __getattr__(self, classname):
84 if classname == 'curuserid':
85 if self.journaltag is None:
86 return None
88 # try to set the curuserid from the journaltag
89 try:
90 x = int(self.classes['user'].lookup(self.journaltag))
91 self.curuserid = x
92 except KeyError:
93 if self.journaltag == 'admin':
94 self.curuserid = x = 1
95 else:
96 x = 0
97 return x
98 elif classname == 'transactions':
99 return self.dirty
100 # fall back on the classes
101 return self.getclass(classname)
102 def getclass(self, classname):
103 try:
104 return self.classes[classname]
105 except KeyError:
106 raise KeyError, 'There is no class called "%s"'%classname
107 def getclasses(self):
108 return self.classes.keys()
109 # --- end of ping's spec
111 # --- exposed methods
112 def commit(self):
113 if self.dirty:
114 self._db.commit()
115 for cl in self.classes.values():
116 cl._commit()
117 self.indexer.save_index()
118 self.dirty = 0
119 def rollback(self):
120 if self.dirty:
121 for cl in self.classes.values():
122 cl._rollback()
123 self._db.rollback()
124 self._db = None
125 self._db = metakit.storage(self.dbnm, 1)
126 self.hist = self._db.view('history')
127 self.tables = self._db.view('tables')
128 self.indexer.rollback()
129 self.indexer.datadb = self._db
130 self.dirty = 0
131 def clearCache(self):
132 for cl in self.classes.values():
133 cl._commit()
134 def clear(self):
135 for cl in self.classes.values():
136 cl._clear()
137 def hasnode(self, classname, nodeid):
138 return self.getclass(classname).hasnode(nodeid)
139 def pack(self, pack_before):
140 mindate = int(calendar.timegm(pack_before.get_tuple()))
141 i = 0
142 while i < len(self.hist):
143 if self.hist[i].date < mindate and self.hist[i].action != _CREATE:
144 self.hist.delete(i)
145 else:
146 i = i + 1
147 def addclass(self, cl):
148 self.classes[cl.classname] = cl
149 if self.tables.find(name=cl.classname) < 0:
150 self.tables.append(name=cl.classname)
151 def addjournal(self, tablenm, nodeid, action, params, creator=None,
152 creation=None):
153 tblid = self.tables.find(name=tablenm)
154 if tblid == -1:
155 tblid = self.tables.append(name=tablenm)
156 if creator is None:
157 creator = self.curuserid
158 else:
159 try:
160 creator = int(creator)
161 except TypeError:
162 creator = int(self.getclass('user').lookup(creator))
163 if creation is None:
164 creation = int(time.time())
165 elif isinstance(creation, date.Date):
166 creation = int(calendar.timegm(creation.get_tuple()))
167 # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
168 self.hist.append(tableid=tblid,
169 nodeid=int(nodeid),
170 date=creation,
171 action=action,
172 user = creator,
173 params = marshal.dumps(params))
174 def getjournal(self, tablenm, nodeid):
175 rslt = []
176 tblid = self.tables.find(name=tablenm)
177 if tblid == -1:
178 return rslt
179 q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
180 if len(q) == 0:
181 raise IndexError, "no history for id %s in %s" % (nodeid, tablenm)
182 i = 0
183 #userclass = self.getclass('user')
184 for row in q:
185 try:
186 params = marshal.loads(row.params)
187 except ValueError:
188 print "history couldn't unmarshal %r" % row.params
189 params = {}
190 #usernm = userclass.get(str(row.user), 'username')
191 dt = date.Date(time.gmtime(row.date))
192 #rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
193 rslt.append((nodeid, dt, str(row.user), _actionnames[row.action],
194 params))
195 return rslt
197 def destroyjournal(self, tablenm, nodeid):
198 nodeid = int(nodeid)
199 tblid = self.tables.find(name=tablenm)
200 if tblid == -1:
201 return
202 i = 0
203 hist = self.hist
204 while i < len(hist):
205 if hist[i].tableid == tblid and hist[i].nodeid == nodeid:
206 hist.delete(i)
207 else:
208 i = i + 1
209 self.dirty = 1
211 def close(self):
212 for cl in self.classes.values():
213 cl.db = None
214 self._db = None
215 if self.lockfile is not None:
216 locking.release_lock(self.lockfile)
217 if _dbs.has_key(self.config.DATABASE):
218 del _dbs[self.config.DATABASE]
219 if self.lockfile is not None:
220 self.lockfile.close()
221 self.lockfile = None
222 self.classes = {}
223 self.indexer = None
225 # --- internal
226 def __open(self):
227 ''' Open the metakit database
228 '''
229 # make the database dir if it doesn't exist
230 if not os.path.exists(self.config.DATABASE):
231 os.makedirs(self.config.DATABASE)
233 # figure the file names
234 self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
235 lockfilenm = db[:-3]+'lck'
237 # get the database lock
238 self.lockfile = locking.acquire_lock(lockfilenm)
239 self.lockfile.write(str(os.getpid()))
240 self.lockfile.flush()
242 # see if the schema has changed since last db access
243 self.fastopen = 0
244 if os.path.exists(db):
245 dbtm = os.path.getmtime(db)
246 pkgnm = self.config.__name__.split('.')[0]
247 schemamod = sys.modules.get(pkgnm+'.dbinit', None)
248 if schemamod:
249 if os.path.exists(schemamod.__file__):
250 schematm = os.path.getmtime(schemamod.__file__)
251 if schematm < dbtm:
252 # found schema mod - it's older than the db
253 self.fastopen = 1
254 else:
255 # can't find schemamod - must be frozen
256 self.fastopen = 1
258 # open the db
259 db = metakit.storage(db, 1)
260 hist = db.view('history')
261 tables = db.view('tables')
262 if not self.fastopen:
263 # create the database if it's brand new
264 if not hist.structure():
265 hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
266 if not tables.structure():
267 tables = db.getas('tables[name:S]')
268 db.commit()
270 # we now have an open, initialised database
271 self.tables = tables
272 self.hist = hist
273 return db
275 def setid(self, classname, maxid):
276 ''' No-op in metakit
277 '''
278 pass
280 _STRINGTYPE = type('')
281 _LISTTYPE = type([])
282 _CREATE, _SET, _RETIRE, _LINK, _UNLINK, _RESTORE = range(6)
284 _actionnames = {
285 _CREATE : 'create',
286 _SET : 'set',
287 _RETIRE : 'retire',
288 _RESTORE : 'restore',
289 _LINK : 'link',
290 _UNLINK : 'unlink',
291 }
293 _marker = []
295 _ALLOWSETTINGPRIVATEPROPS = 0
297 class Class:
298 privateprops = None
299 def __init__(self, db, classname, **properties):
300 #self.db = weakref.proxy(db)
301 self.db = db
302 self.classname = classname
303 self.keyname = None
304 self.ruprops = properties
305 self.privateprops = { 'id' : hyperdb.String(),
306 'activity' : hyperdb.Date(),
307 'creation' : hyperdb.Date(),
308 'creator' : hyperdb.Link('user') }
310 # event -> list of callables
311 self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
312 self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
314 view = self.__getview()
315 self.maxid = 1
316 if view:
317 self.maxid = view[-1].id + 1
318 self.uncommitted = {}
319 self.rbactions = []
321 # people reach inside!!
322 self.properties = self.ruprops
323 self.db.addclass(self)
324 self.idcache = {}
326 # default is to journal changes
327 self.do_journal = 1
329 def enableJournalling(self):
330 '''Turn journalling on for this class
331 '''
332 self.do_journal = 1
334 def disableJournalling(self):
335 '''Turn journalling off for this class
336 '''
337 self.do_journal = 0
339 # --- the roundup.Class methods
340 def audit(self, event, detector):
341 l = self.auditors[event]
342 if detector not in l:
343 self.auditors[event].append(detector)
344 def fireAuditors(self, action, nodeid, newvalues):
345 for audit in self.auditors[action]:
346 audit(self.db, self, nodeid, newvalues)
347 def fireReactors(self, action, nodeid, oldvalues):
348 for react in self.reactors[action]:
349 react(self.db, self, nodeid, oldvalues)
350 def react(self, event, detector):
351 l = self.reactors[event]
352 if detector not in l:
353 self.reactors[event].append(detector)
355 # --- the hyperdb.Class methods
356 def create(self, **propvalues):
357 self.fireAuditors('create', None, propvalues)
358 newid = self.create_inner(**propvalues)
359 # self.set() (called in self.create_inner()) does reactors)
360 return newid
362 def create_inner(self, **propvalues):
363 rowdict = {}
364 rowdict['id'] = newid = self.maxid
365 self.maxid += 1
366 ndx = self.getview(1).append(rowdict)
367 propvalues['#ISNEW'] = 1
368 try:
369 self.set(str(newid), **propvalues)
370 except Exception:
371 self.maxid -= 1
372 raise
373 return str(newid)
375 def get(self, nodeid, propname, default=_marker, cache=1):
376 # default and cache aren't in the spec
377 # cache=0 means "original value"
379 view = self.getview()
380 id = int(nodeid)
381 if cache == 0:
382 oldnode = self.uncommitted.get(id, None)
383 if oldnode and oldnode.has_key(propname):
384 return oldnode[propname]
385 ndx = self.idcache.get(id, None)
386 if ndx is None:
387 ndx = view.find(id=id)
388 if ndx < 0:
389 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
390 self.idcache[id] = ndx
391 try:
392 raw = getattr(view[ndx], propname)
393 except AttributeError:
394 raise KeyError, propname
395 rutyp = self.ruprops.get(propname, None)
396 if rutyp is None:
397 rutyp = self.privateprops[propname]
398 converter = _converters.get(rutyp.__class__, None)
399 if converter:
400 raw = converter(raw)
401 return raw
403 def set(self, nodeid, **propvalues):
404 isnew = 0
405 if propvalues.has_key('#ISNEW'):
406 isnew = 1
407 del propvalues['#ISNEW']
408 if not isnew:
409 self.fireAuditors('set', nodeid, propvalues)
410 if not propvalues:
411 return propvalues
412 if propvalues.has_key('id'):
413 raise KeyError, '"id" is reserved'
414 if self.db.journaltag is None:
415 raise hyperdb.DatabaseError, 'Database open read-only'
416 view = self.getview(1)
418 # node must exist & not be retired
419 id = int(nodeid)
420 ndx = view.find(id=id)
421 if ndx < 0:
422 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
423 row = view[ndx]
424 if row._isdel:
425 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
426 oldnode = self.uncommitted.setdefault(id, {})
427 changes = {}
429 for key, value in propvalues.items():
430 # this will raise the KeyError if the property isn't valid
431 # ... we don't use getprops() here because we only care about
432 # the writeable properties.
433 if _ALLOWSETTINGPRIVATEPROPS:
434 prop = self.ruprops.get(key, None)
435 if not prop:
436 prop = self.privateprops[key]
437 else:
438 prop = self.ruprops[key]
439 converter = _converters.get(prop.__class__, lambda v: v)
440 # if the value's the same as the existing value, no sense in
441 # doing anything
442 oldvalue = converter(getattr(row, key))
443 if value == oldvalue:
444 del propvalues[key]
445 continue
447 # check to make sure we're not duplicating an existing key
448 if key == self.keyname:
449 iv = self.getindexview(1)
450 ndx = iv.find(k=value)
451 if ndx == -1:
452 iv.append(k=value, i=row.id)
453 if not isnew:
454 ndx = iv.find(k=oldvalue)
455 if ndx > -1:
456 iv.delete(ndx)
457 else:
458 raise ValueError, 'node with key "%s" exists'%value
460 # do stuff based on the prop type
461 if isinstance(prop, hyperdb.Link):
462 link_class = prop.classname
463 # must be a string or None
464 if value is not None and not isinstance(value, type('')):
465 raise ValueError, 'property "%s" link value be a string'%(
466 key)
467 # Roundup sets to "unselected" by passing None
468 if value is None:
469 value = 0
470 # if it isn't a number, it's a key
471 try:
472 int(value)
473 except ValueError:
474 try:
475 value = self.db.getclass(link_class).lookup(value)
476 except (TypeError, KeyError):
477 raise IndexError, 'new property "%s": %s not a %s'%(
478 key, value, prop.classname)
480 if (value is not None and
481 not self.db.getclass(link_class).hasnode(value)):
482 raise IndexError, '%s has no node %s'%(link_class, value)
484 setattr(row, key, int(value))
485 changes[key] = oldvalue
487 if self.do_journal and prop.do_journal:
488 # register the unlink with the old linked node
489 if oldvalue:
490 self.db.addjournal(link_class, oldvalue, _UNLINK,
491 (self.classname, str(row.id), key))
493 # register the link with the newly linked node
494 if value:
495 self.db.addjournal(link_class, value, _LINK,
496 (self.classname, str(row.id), key))
498 elif isinstance(prop, hyperdb.Multilink):
499 if value is not None and type(value) != _LISTTYPE:
500 raise TypeError, 'new property "%s" not a list of ids'%key
501 link_class = prop.classname
502 l = []
503 if value is None:
504 value = []
505 for entry in value:
506 if type(entry) != _STRINGTYPE:
507 raise ValueError, 'new property "%s" link value ' \
508 'must be a string'%key
509 # if it isn't a number, it's a key
510 try:
511 int(entry)
512 except ValueError:
513 try:
514 entry = self.db.getclass(link_class).lookup(entry)
515 except (TypeError, KeyError):
516 raise IndexError, 'new property "%s": %s not a %s'%(
517 key, entry, prop.classname)
518 l.append(entry)
519 propvalues[key] = value = l
521 # handle removals
522 rmvd = []
523 for id in oldvalue:
524 if id not in value:
525 rmvd.append(id)
526 # register the unlink with the old linked node
527 if self.do_journal and prop.do_journal:
528 self.db.addjournal(link_class, id, _UNLINK,
529 (self.classname, str(row.id), key))
531 # handle additions
532 adds = []
533 for id in value:
534 if id not in oldvalue:
535 if not self.db.getclass(link_class).hasnode(id):
536 raise IndexError, '%s has no node %s'%(
537 link_class, id)
538 adds.append(id)
539 # register the link with the newly linked node
540 if self.do_journal and prop.do_journal:
541 self.db.addjournal(link_class, id, _LINK,
542 (self.classname, str(row.id), key))
544 # perform the modifications on the actual property value
545 sv = getattr(row, key)
546 i = 0
547 while i < len(sv):
548 if str(sv[i].fid) in rmvd:
549 sv.delete(i)
550 else:
551 i += 1
552 for id in adds:
553 sv.append(fid=int(id))
555 # figure the journal entry
556 l = []
557 if adds:
558 l.append(('+', adds))
559 if rmvd:
560 l.append(('-', rmvd))
561 if l:
562 changes[key] = tuple(l)
563 #changes[key] = oldvalue
565 if not rmvd and not adds:
566 del propvalues[key]
568 elif isinstance(prop, hyperdb.String):
569 if value is not None and type(value) != _STRINGTYPE:
570 raise TypeError, 'new property "%s" not a string'%key
571 if value is None:
572 value = ''
573 setattr(row, key, value)
574 changes[key] = oldvalue
575 if hasattr(prop, 'isfilename') and prop.isfilename:
576 propvalues[key] = os.path.basename(value)
577 if prop.indexme:
578 self.db.indexer.add_text((self.classname, nodeid, key),
579 value, 'text/plain')
581 elif isinstance(prop, hyperdb.Password):
582 if value is not None and not isinstance(value, password.Password):
583 raise TypeError, 'new property "%s" not a Password'% key
584 if value is None:
585 value = ''
586 setattr(row, key, str(value))
587 changes[key] = str(oldvalue)
588 propvalues[key] = str(value)
590 elif isinstance(prop, hyperdb.Date):
591 if value is not None and not isinstance(value, date.Date):
592 raise TypeError, 'new property "%s" not a Date'% key
593 if value is None:
594 setattr(row, key, 0)
595 else:
596 setattr(row, key, int(calendar.timegm(value.get_tuple())))
597 changes[key] = str(oldvalue)
598 propvalues[key] = str(value)
600 elif isinstance(prop, hyperdb.Interval):
601 if value is not None and not isinstance(value, date.Interval):
602 raise TypeError, 'new property "%s" not an Interval'% key
603 if value is None:
604 setattr(row, key, '')
605 else:
606 # kedder: we should store interval values serialized
607 setattr(row, key, value.serialise())
608 changes[key] = str(oldvalue)
609 propvalues[key] = str(value)
611 elif isinstance(prop, hyperdb.Number):
612 if value is None:
613 value = 0
614 try:
615 v = int(value)
616 except ValueError:
617 raise TypeError, "%s (%s) is not numeric"%(key, repr(value))
618 setattr(row, key, v)
619 changes[key] = oldvalue
620 propvalues[key] = value
622 elif isinstance(prop, hyperdb.Boolean):
623 if value is None:
624 bv = 0
625 elif value not in (0,1):
626 raise TypeError, "%s (%s) is not boolean"%(key, repr(value))
627 else:
628 bv = value
629 setattr(row, key, bv)
630 changes[key] = oldvalue
631 propvalues[key] = value
633 oldnode[key] = oldvalue
635 # nothing to do?
636 if not propvalues:
637 return propvalues
638 if not propvalues.has_key('activity'):
639 row.activity = int(time.time())
640 if isnew:
641 if not row.creation:
642 row.creation = int(time.time())
643 if not row.creator:
644 row.creator = self.db.curuserid
646 self.db.dirty = 1
647 if self.do_journal:
648 if isnew:
649 self.db.addjournal(self.classname, nodeid, _CREATE, {})
650 self.fireReactors('create', nodeid, None)
651 else:
652 self.db.addjournal(self.classname, nodeid, _SET, changes)
653 self.fireReactors('set', nodeid, oldnode)
655 return propvalues
657 def retire(self, nodeid):
658 if self.db.journaltag is None:
659 raise hyperdb.DatabaseError, 'Database open read-only'
660 self.fireAuditors('retire', nodeid, None)
661 view = self.getview(1)
662 ndx = view.find(id=int(nodeid))
663 if ndx < 0:
664 raise KeyError, "nodeid %s not found" % nodeid
666 row = view[ndx]
667 oldvalues = self.uncommitted.setdefault(row.id, {})
668 oldval = oldvalues['_isdel'] = row._isdel
669 row._isdel = 1
671 if self.do_journal:
672 self.db.addjournal(self.classname, nodeid, _RETIRE, {})
673 if self.keyname:
674 iv = self.getindexview(1)
675 ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
676 if ndx > -1:
677 iv.delete(ndx)
678 self.db.dirty = 1
679 self.fireReactors('retire', nodeid, None)
681 def restore(self, nodeid):
682 '''Restpre a retired node.
684 Make node available for all operations like it was before retirement.
685 '''
686 if self.db.journaltag is None:
687 raise hyperdb.DatabaseError, 'Database open read-only'
689 # check if key property was overrided
690 key = self.getkey()
691 keyvalue = self.get(nodeid, key)
692 try:
693 id = self.lookup(keyvalue)
694 except KeyError:
695 pass
696 else:
697 raise KeyError, "Key property (%s) of retired node clashes with \
698 existing one (%s)" % (key, keyvalue)
699 # Now we can safely restore node
700 self.fireAuditors('restore', nodeid, None)
701 view = self.getview(1)
702 ndx = view.find(id=int(nodeid))
703 if ndx < 0:
704 raise KeyError, "nodeid %s not found" % nodeid
706 row = view[ndx]
707 oldvalues = self.uncommitted.setdefault(row.id, {})
708 oldval = oldvalues['_isdel'] = row._isdel
709 row._isdel = 0
711 if self.do_journal:
712 self.db.addjournal(self.classname, nodeid, _RESTORE, {})
713 if self.keyname:
714 iv = self.getindexview(1)
715 ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
716 if ndx > -1:
717 iv.delete(ndx)
718 self.db.dirty = 1
719 self.fireReactors('restore', nodeid, None)
721 def is_retired(self, nodeid):
722 view = self.getview(1)
723 # node must exist & not be retired
724 id = int(nodeid)
725 ndx = view.find(id=id)
726 if ndx < 0:
727 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
728 row = view[ndx]
729 return row._isdel
731 def history(self, nodeid):
732 if not self.do_journal:
733 raise ValueError, 'Journalling is disabled for this class'
734 return self.db.getjournal(self.classname, nodeid)
736 def setkey(self, propname):
737 if self.keyname:
738 if propname == self.keyname:
739 return
740 raise ValueError, "%s already indexed on %s"%(self.classname,
741 self.keyname)
742 prop = self.properties.get(propname, None)
743 if prop is None:
744 prop = self.privateprops.get(propname, None)
745 if prop is None:
746 raise KeyError, "no property %s" % propname
747 if not isinstance(prop, hyperdb.String):
748 raise TypeError, "%s is not a String" % propname
750 # first setkey for this run
751 self.keyname = propname
752 iv = self.db._db.view('_%s' % self.classname)
753 if self.db.fastopen and iv.structure():
754 return
756 # very first setkey ever
757 self.db.dirty = 1
758 iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
759 iv = iv.ordered(1)
760 for row in self.getview():
761 iv.append(k=getattr(row, propname), i=row.id)
762 self.db.commit()
764 def getkey(self):
765 return self.keyname
767 def lookup(self, keyvalue):
768 if type(keyvalue) is not _STRINGTYPE:
769 raise TypeError, "%r is not a string" % keyvalue
770 iv = self.getindexview()
771 if iv:
772 ndx = iv.find(k=keyvalue)
773 if ndx > -1:
774 return str(iv[ndx].i)
775 else:
776 view = self.getview()
777 ndx = view.find({self.keyname:keyvalue, '_isdel':0})
778 if ndx > -1:
779 return str(view[ndx].id)
780 raise KeyError, keyvalue
782 def destroy(self, id):
783 view = self.getview(1)
784 ndx = view.find(id=int(id))
785 if ndx > -1:
786 if self.keyname:
787 keyvalue = getattr(view[ndx], self.keyname)
788 iv = self.getindexview(1)
789 if iv:
790 ivndx = iv.find(k=keyvalue)
791 if ivndx > -1:
792 iv.delete(ivndx)
793 view.delete(ndx)
794 self.db.destroyjournal(self.classname, id)
795 self.db.dirty = 1
797 def find(self, **propspec):
798 """Get the ids of nodes in this class which link to the given nodes.
800 'propspec' consists of keyword args propname={nodeid:1,}
801 'propname' must be the name of a property in this class, or a
802 KeyError is raised. That property must be a Link or
803 Multilink property, or a TypeError is raised.
805 Any node in this class whose propname property links to any of the
806 nodeids will be returned. Used by the full text indexing, which knows
807 that "foo" occurs in msg1, msg3 and file7; so we have hits on these
808 issues:
810 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
812 """
813 propspec = propspec.items()
814 for propname, nodeid in propspec:
815 # check the prop is OK
816 prop = self.ruprops[propname]
817 if (not isinstance(prop, hyperdb.Link) and
818 not isinstance(prop, hyperdb.Multilink)):
819 raise TypeError, "'%s' not a Link/Multilink property"%propname
821 vws = []
822 for propname, ids in propspec:
823 if type(ids) is _STRINGTYPE:
824 ids = {int(ids):1}
825 elif ids is None:
826 ids = {0:1}
827 else:
828 d = {}
829 for id in ids.keys():
830 d[int(id)] = 1
831 ids = d
832 prop = self.ruprops[propname]
833 view = self.getview()
834 if isinstance(prop, hyperdb.Multilink):
835 def ff(row, nm=propname, ids=ids):
836 sv = getattr(row, nm)
837 for sr in sv:
838 if ids.has_key(sr.fid):
839 return 1
840 return 0
841 else:
842 def ff(row, nm=propname, ids=ids):
843 return ids.has_key(getattr(row, nm))
844 ndxview = view.filter(ff)
845 vws.append(ndxview.unique())
847 # handle the empty match case
848 if not vws:
849 return []
851 ndxview = vws[0]
852 for v in vws[1:]:
853 ndxview = ndxview.union(v)
854 view = self.getview().remapwith(ndxview)
855 rslt = []
856 for row in view:
857 rslt.append(str(row.id))
858 return rslt
861 def list(self):
862 l = []
863 for row in self.getview().select(_isdel=0):
864 l.append(str(row.id))
865 return l
867 def getnodeids(self):
868 l = []
869 for row in self.getview():
870 l.append(str(row.id))
871 return l
873 def count(self):
874 return len(self.getview())
876 def getprops(self, protected=1):
877 # protected is not in ping's spec
878 allprops = self.ruprops.copy()
879 if protected and self.privateprops is not None:
880 allprops.update(self.privateprops)
881 return allprops
883 def addprop(self, **properties):
884 for key in properties.keys():
885 if self.ruprops.has_key(key):
886 raise ValueError, "%s is already a property of %s"%(key,
887 self.classname)
888 self.ruprops.update(properties)
889 # Class structure has changed
890 self.db.fastopen = 0
891 view = self.__getview()
892 self.db.commit()
893 # ---- end of ping's spec
895 def filter(self, search_matches, filterspec, sort=(None,None),
896 group=(None,None)):
897 # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
898 # filterspec is a dict {propname:value}
899 # sort and group are (dir, prop) where dir is '+', '-' or None
900 # and prop is a prop name or None
902 timezone = self.db.getUserTimezone()
904 where = {'_isdel':0}
905 wherehigh = {}
906 mlcriteria = {}
907 regexes = {}
908 orcriteria = {}
909 for propname, value in filterspec.items():
910 prop = self.ruprops.get(propname, None)
911 if prop is None:
912 prop = self.privateprops[propname]
913 if isinstance(prop, hyperdb.Multilink):
914 if value in ('-1', ['-1']):
915 value = []
916 elif type(value) is not _LISTTYPE:
917 value = [value]
918 # transform keys to ids
919 u = []
920 for item in value:
921 try:
922 item = int(item)
923 except (TypeError, ValueError):
924 item = int(self.db.getclass(prop.classname).lookup(item))
925 if item == -1:
926 item = 0
927 u.append(item)
928 mlcriteria[propname] = u
929 elif isinstance(prop, hyperdb.Link):
930 if type(value) is not _LISTTYPE:
931 value = [value]
932 # transform keys to ids
933 u = []
934 for item in value:
935 try:
936 item = int(item)
937 except (TypeError, ValueError):
938 item = int(self.db.getclass(prop.classname).lookup(item))
939 if item == -1:
940 item = 0
941 u.append(item)
942 if len(u) == 1:
943 where[propname] = u[0]
944 else:
945 orcriteria[propname] = u
946 elif isinstance(prop, hyperdb.String):
947 if type(value) is not type([]):
948 value = [value]
949 m = []
950 for v in value:
951 # simple glob searching
952 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
953 v = v.replace('?', '.')
954 v = v.replace('*', '.*?')
955 m.append(v)
956 regexes[propname] = re.compile('(%s)'%('|'.join(m)), re.I)
957 elif propname == 'id':
958 where[propname] = int(value)
959 elif isinstance(prop, hyperdb.Boolean):
960 if type(value) is _STRINGTYPE:
961 bv = value.lower() in ('yes', 'true', 'on', '1')
962 else:
963 bv = value
964 where[propname] = bv
965 elif isinstance(prop, hyperdb.Date):
966 try:
967 # Try to filter on range of dates
968 date_rng = Range(value, date.Date, offset=timezone)
969 if date_rng.from_value:
970 t = date_rng.from_value.get_tuple()
971 where[propname] = int(calendar.timegm(t))
972 else:
973 # use minimum possible value to exclude items without
974 # 'prop' property
975 where[propname] = 0
976 if date_rng.to_value:
977 t = date_rng.to_value.get_tuple()
978 wherehigh[propname] = int(calendar.timegm(t))
979 else:
980 wherehigh[propname] = None
981 except ValueError:
982 # If range creation fails - ignore that search parameter
983 pass
984 elif isinstance(prop, hyperdb.Interval):
985 try:
986 # Try to filter on range of intervals
987 date_rng = Range(value, date.Interval)
988 if date_rng.from_value:
989 #t = date_rng.from_value.get_tuple()
990 where[propname] = date_rng.from_value.serialise()
991 else:
992 # use minimum possible value to exclude items without
993 # 'prop' property
994 where[propname] = '-99999999999999'
995 if date_rng.to_value:
996 #t = date_rng.to_value.get_tuple()
997 wherehigh[propname] = date_rng.to_value.serialise()
998 else:
999 wherehigh[propname] = None
1000 except ValueError:
1001 # If range creation fails - ignore that search parameter
1002 pass
1003 elif isinstance(prop, hyperdb.Number):
1004 where[propname] = int(value)
1005 else:
1006 where[propname] = str(value)
1007 v = self.getview()
1008 #print "filter start at %s" % time.time()
1009 if where:
1010 where_higherbound = where.copy()
1011 where_higherbound.update(wherehigh)
1012 v = v.select(where, where_higherbound)
1013 #print "filter where at %s" % time.time()
1015 if mlcriteria:
1016 # multilink - if any of the nodeids required by the
1017 # filterspec aren't in this node's property, then skip it
1018 def ff(row, ml=mlcriteria):
1019 for propname, values in ml.items():
1020 sv = getattr(row, propname)
1021 if not values and sv:
1022 return 0
1023 for id in values:
1024 if sv.find(fid=id) == -1:
1025 return 0
1026 return 1
1027 iv = v.filter(ff)
1028 v = v.remapwith(iv)
1030 #print "filter mlcrit at %s" % time.time()
1032 if orcriteria:
1033 def ff(row, crit=orcriteria):
1034 for propname, allowed in crit.items():
1035 val = getattr(row, propname)
1036 if val not in allowed:
1037 return 0
1038 return 1
1040 iv = v.filter(ff)
1041 v = v.remapwith(iv)
1043 #print "filter orcrit at %s" % time.time()
1044 if regexes:
1045 def ff(row, r=regexes):
1046 for propname, regex in r.items():
1047 val = str(getattr(row, propname))
1048 if not regex.search(val):
1049 return 0
1050 return 1
1052 iv = v.filter(ff)
1053 v = v.remapwith(iv)
1054 #print "filter regexs at %s" % time.time()
1056 if sort or group:
1057 sortspec = []
1058 rev = []
1059 for dir, propname in group, sort:
1060 if propname is None: continue
1061 isreversed = 0
1062 if dir == '-':
1063 isreversed = 1
1064 try:
1065 prop = getattr(v, propname)
1066 except AttributeError:
1067 print "MK has no property %s" % propname
1068 continue
1069 propclass = self.ruprops.get(propname, None)
1070 if propclass is None:
1071 propclass = self.privateprops.get(propname, None)
1072 if propclass is None:
1073 print "Schema has no property %s" % propname
1074 continue
1075 if isinstance(propclass, hyperdb.Link):
1076 linkclass = self.db.getclass(propclass.classname)
1077 lv = linkclass.getview()
1078 lv = lv.rename('id', propname)
1079 v = v.join(lv, prop, 1)
1080 if linkclass.getprops().has_key('order'):
1081 propname = 'order'
1082 else:
1083 propname = linkclass.labelprop()
1084 prop = getattr(v, propname)
1085 if isreversed:
1086 rev.append(prop)
1087 sortspec.append(prop)
1088 v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
1089 #print "filter sort at %s" % time.time()
1091 rslt = []
1092 for row in v:
1093 id = str(row.id)
1094 if search_matches is not None:
1095 if search_matches.has_key(id):
1096 rslt.append(id)
1097 else:
1098 rslt.append(id)
1099 return rslt
1101 def hasnode(self, nodeid):
1102 return int(nodeid) < self.maxid
1104 def labelprop(self, default_to_id=0):
1105 ''' Return the property name for a label for the given node.
1107 This method attempts to generate a consistent label for the node.
1108 It tries the following in order:
1109 1. key property
1110 2. "name" property
1111 3. "title" property
1112 4. first property from the sorted property name list
1113 '''
1114 k = self.getkey()
1115 if k:
1116 return k
1117 props = self.getprops()
1118 if props.has_key('name'):
1119 return 'name'
1120 elif props.has_key('title'):
1121 return 'title'
1122 if default_to_id:
1123 return 'id'
1124 props = props.keys()
1125 props.sort()
1126 return props[0]
1128 def stringFind(self, **requirements):
1129 """Locate a particular node by matching a set of its String
1130 properties in a caseless search.
1132 If the property is not a String property, a TypeError is raised.
1134 The return is a list of the id of all nodes that match.
1135 """
1136 for propname in requirements.keys():
1137 prop = self.properties[propname]
1138 if isinstance(not prop, hyperdb.String):
1139 raise TypeError, "'%s' not a String property"%propname
1140 requirements[propname] = requirements[propname].lower()
1141 requirements['_isdel'] = 0
1143 l = []
1144 for row in self.getview().select(requirements):
1145 l.append(str(row.id))
1146 return l
1148 def addjournal(self, nodeid, action, params):
1149 self.db.addjournal(self.classname, nodeid, action, params)
1151 def index(self, nodeid):
1152 ''' Add (or refresh) the node to search indexes '''
1153 # find all the String properties that have indexme
1154 for prop, propclass in self.getprops().items():
1155 if isinstance(propclass, hyperdb.String) and propclass.indexme:
1156 # index them under (classname, nodeid, property)
1157 self.db.indexer.add_text((self.classname, nodeid, prop),
1158 str(self.get(nodeid, prop)))
1160 def export_list(self, propnames, nodeid):
1161 ''' Export a node - generate a list of CSV-able data in the order
1162 specified by propnames for the given node.
1163 '''
1164 properties = self.getprops()
1165 l = []
1166 for prop in propnames:
1167 proptype = properties[prop]
1168 value = self.get(nodeid, prop)
1169 # "marshal" data where needed
1170 if value is None:
1171 pass
1172 elif isinstance(proptype, hyperdb.Date):
1173 value = value.get_tuple()
1174 elif isinstance(proptype, hyperdb.Interval):
1175 value = value.get_tuple()
1176 elif isinstance(proptype, hyperdb.Password):
1177 value = str(value)
1178 l.append(repr(value))
1180 # append retired flag
1181 l.append(self.is_retired(nodeid))
1183 return l
1185 def import_list(self, propnames, proplist):
1186 ''' Import a node - all information including "id" is present and
1187 should not be sanity checked. Triggers are not triggered. The
1188 journal should be initialised using the "creator" and "creation"
1189 information.
1191 Return the nodeid of the node imported.
1192 '''
1193 if self.db.journaltag is None:
1194 raise hyperdb.DatabaseError, 'Database open read-only'
1195 properties = self.getprops()
1197 d = {}
1198 view = self.getview(1)
1199 for i in range(len(propnames)):
1200 value = eval(proplist[i])
1201 if not value:
1202 continue
1204 propname = propnames[i]
1205 if propname == 'id':
1206 newid = value = int(value)
1207 elif propname == 'is retired':
1208 # is the item retired?
1209 if int(value):
1210 d['_isdel'] = 1
1211 continue
1213 prop = properties[propname]
1214 if isinstance(prop, hyperdb.Date):
1215 value = int(calendar.timegm(value))
1216 elif isinstance(prop, hyperdb.Interval):
1217 value = date.Interval(value).serialise()
1218 elif isinstance(prop, hyperdb.Number):
1219 value = int(value)
1220 elif isinstance(prop, hyperdb.Boolean):
1221 value = int(value)
1222 elif isinstance(prop, hyperdb.Link) and value:
1223 value = int(value)
1224 elif isinstance(prop, hyperdb.Multilink):
1225 # we handle multilinks separately
1226 continue
1227 d[propname] = value
1229 # possibly make a new node
1230 if not d.has_key('id'):
1231 d['id'] = newid = self.maxid
1232 self.maxid += 1
1234 # save off the node
1235 view.append(d)
1237 # fix up multilinks
1238 ndx = view.find(id=newid)
1239 row = view[ndx]
1240 for i in range(len(propnames)):
1241 value = eval(proplist[i])
1242 propname = propnames[i]
1243 if propname == 'is retired':
1244 continue
1245 prop = properties[propname]
1246 if not isinstance(prop, hyperdb.Multilink):
1247 continue
1248 sv = getattr(row, propname)
1249 for entry in value:
1250 sv.append(int(entry))
1252 self.db.dirty = 1
1253 creator = d.get('creator', 0)
1254 creation = d.get('creation', 0)
1255 self.db.addjournal(self.classname, str(newid), _CREATE, {}, creator,
1256 creation)
1257 return newid
1259 # --- used by Database
1260 def _commit(self):
1261 """ called post commit of the DB.
1262 interested subclasses may override """
1263 self.uncommitted = {}
1264 self.rbactions = []
1265 self.idcache = {}
1266 def _rollback(self):
1267 """ called pre rollback of the DB.
1268 interested subclasses may override """
1269 for action in self.rbactions:
1270 action()
1271 self.rbactions = []
1272 self.uncommitted = {}
1273 self.idcache = {}
1274 def _clear(self):
1275 view = self.getview(1)
1276 if len(view):
1277 view[:] = []
1278 self.db.dirty = 1
1279 iv = self.getindexview(1)
1280 if iv:
1281 iv[:] = []
1282 def rollbackaction(self, action):
1283 """ call this to register a callback called on rollback
1284 callback is removed on end of transaction """
1285 self.rbactions.append(action)
1286 # --- internal
1287 def __getview(self):
1288 ''' Find the interface for a specific Class in the hyperdb.
1290 This method checks to see whether the schema has changed and
1291 re-works the underlying metakit structure if it has.
1292 '''
1293 db = self.db._db
1294 view = db.view(self.classname)
1295 mkprops = view.structure()
1297 # if we have structure in the database, and the structure hasn't
1298 # changed
1299 if mkprops and self.db.fastopen:
1300 return view.ordered(1)
1302 # is the definition the same?
1303 for nm, rutyp in self.ruprops.items():
1304 for mkprop in mkprops:
1305 if mkprop.name == nm:
1306 break
1307 else:
1308 mkprop = None
1309 if mkprop is None:
1310 break
1311 if _typmap[rutyp.__class__] != mkprop.type:
1312 break
1313 else:
1314 return view.ordered(1)
1315 # need to create or restructure the mk view
1316 # id comes first, so MK will order it for us
1317 self.db.dirty = 1
1318 s = ["%s[id:I" % self.classname]
1319 for nm, rutyp in self.ruprops.items():
1320 mktyp = _typmap[rutyp.__class__]
1321 s.append('%s:%s' % (nm, mktyp))
1322 if mktyp == 'V':
1323 s[-1] += ('[fid:I]')
1324 s.append('_isdel:I,activity:I,creation:I,creator:I]')
1325 v = self.db._db.getas(','.join(s))
1326 self.db.commit()
1327 return v.ordered(1)
1328 def getview(self, RW=0):
1329 return self.db._db.view(self.classname).ordered(1)
1330 def getindexview(self, RW=0):
1331 return self.db._db.view("_%s" % self.classname).ordered(1)
1333 def _fetchML(sv):
1334 l = []
1335 for row in sv:
1336 if row.fid:
1337 l.append(str(row.fid))
1338 return l
1340 def _fetchPW(s):
1341 ''' Convert to a password.Password unless the password is '' which is
1342 our sentinel for "unset".
1343 '''
1344 if s == '':
1345 return None
1346 p = password.Password()
1347 p.unpack(s)
1348 return p
1350 def _fetchLink(n):
1351 ''' Return None if the link is 0 - otherwise strify it.
1352 '''
1353 return n and str(n) or None
1355 def _fetchDate(n):
1356 ''' Convert the timestamp to a date.Date instance - unless it's 0 which
1357 is our sentinel for "unset".
1358 '''
1359 if n == 0:
1360 return None
1361 return date.Date(time.gmtime(n))
1363 def _fetchInterval(n):
1364 ''' Convert to a date.Interval unless the interval is '' which is our
1365 sentinel for "unset".
1366 '''
1367 if n == '':
1368 return None
1369 return date.Interval(n)
1371 _converters = {
1372 hyperdb.Date : _fetchDate,
1373 hyperdb.Link : _fetchLink,
1374 hyperdb.Multilink : _fetchML,
1375 hyperdb.Interval : _fetchInterval,
1376 hyperdb.Password : _fetchPW,
1377 hyperdb.Boolean : lambda n: n,
1378 hyperdb.Number : lambda n: n,
1379 hyperdb.String : lambda s: s and str(s) or None,
1380 }
1382 class FileName(hyperdb.String):
1383 isfilename = 1
1385 _typmap = {
1386 FileName : 'S',
1387 hyperdb.String : 'S',
1388 hyperdb.Date : 'I',
1389 hyperdb.Link : 'I',
1390 hyperdb.Multilink : 'V',
1391 hyperdb.Interval : 'S',
1392 hyperdb.Password : 'S',
1393 hyperdb.Boolean : 'I',
1394 hyperdb.Number : 'I',
1395 }
1396 class FileClass(Class, hyperdb.FileClass):
1397 ''' like Class but with a content property
1398 '''
1399 default_mime_type = 'text/plain'
1400 def __init__(self, db, classname, **properties):
1401 properties['content'] = FileName()
1402 if not properties.has_key('type'):
1403 properties['type'] = hyperdb.String()
1404 Class.__init__(self, db, classname, **properties)
1406 def get(self, nodeid, propname, default=_marker, cache=1):
1407 x = Class.get(self, nodeid, propname, default, cache)
1408 poss_msg = 'Possibly an access right configuration problem.'
1409 if propname == 'content':
1410 if x.startswith('file:'):
1411 fnm = x[5:]
1412 try:
1413 x = open(fnm, 'rb').read()
1414 except IOError, (strerror):
1415 # XXX by catching this we donot see an error in the log.
1416 return 'ERROR reading file: %s%s\n%s\n%s'%(
1417 self.classname, nodeid, poss_msg, strerror)
1418 return x
1420 def create(self, **propvalues):
1421 self.fireAuditors('create', None, propvalues)
1422 content = propvalues['content']
1423 del propvalues['content']
1424 newid = Class.create_inner(self, **propvalues)
1425 if not content:
1426 return newid
1427 nm = bnm = '%s%s' % (self.classname, newid)
1428 sd = str(int(int(newid) / 1000))
1429 d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1430 if not os.path.exists(d):
1431 os.makedirs(d)
1432 nm = os.path.join(d, nm)
1433 open(nm, 'wb').write(content)
1434 self.set(newid, content = 'file:'+nm)
1435 mimetype = propvalues.get('type', self.default_mime_type)
1436 self.db.indexer.add_text((self.classname, newid, 'content'), content,
1437 mimetype)
1438 def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1439 action1(fnm)
1440 self.rollbackaction(undo)
1441 return newid
1443 def index(self, nodeid):
1444 Class.index(self, nodeid)
1445 mimetype = self.get(nodeid, 'type')
1446 if not mimetype:
1447 mimetype = self.default_mime_type
1448 self.db.indexer.add_text((self.classname, nodeid, 'content'),
1449 self.get(nodeid, 'content'), mimetype)
1451 class IssueClass(Class, roundupdb.IssueClass):
1452 ''' The newly-created class automatically includes the "messages",
1453 "files", "nosy", and "superseder" properties. If the 'properties'
1454 dictionary attempts to specify any of these properties or a
1455 "creation" or "activity" property, a ValueError is raised.
1456 '''
1457 def __init__(self, db, classname, **properties):
1458 if not properties.has_key('title'):
1459 properties['title'] = hyperdb.String(indexme='yes')
1460 if not properties.has_key('messages'):
1461 properties['messages'] = hyperdb.Multilink("msg")
1462 if not properties.has_key('files'):
1463 properties['files'] = hyperdb.Multilink("file")
1464 if not properties.has_key('nosy'):
1465 # note: journalling is turned off as it really just wastes
1466 # space. this behaviour may be overridden in an instance
1467 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1468 if not properties.has_key('superseder'):
1469 properties['superseder'] = hyperdb.Multilink(classname)
1470 Class.__init__(self, db, classname, **properties)
1472 CURVERSION = 2
1474 class Indexer(indexer.Indexer):
1475 disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1476 def __init__(self, path, datadb):
1477 self.path = os.path.join(path, 'index.mk4')
1478 self.db = metakit.storage(self.path, 1)
1479 self.datadb = datadb
1480 self.reindex = 0
1481 v = self.db.view('version')
1482 if not v.structure():
1483 v = self.db.getas('version[vers:I]')
1484 self.db.commit()
1485 v.append(vers=CURVERSION)
1486 self.reindex = 1
1487 elif v[0].vers != CURVERSION:
1488 v[0].vers = CURVERSION
1489 self.reindex = 1
1490 if self.reindex:
1491 self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
1492 self.db.getas('index[word:S,hits[pos:I]]')
1493 self.db.commit()
1494 self.reindex = 1
1495 self.changed = 0
1496 self.propcache = {}
1498 def force_reindex(self):
1499 v = self.db.view('ids')
1500 v[:] = []
1501 v = self.db.view('index')
1502 v[:] = []
1503 self.db.commit()
1504 self.reindex = 1
1506 def should_reindex(self):
1507 return self.reindex
1509 def _getprops(self, classname):
1510 props = self.propcache.get(classname, None)
1511 if props is None:
1512 props = self.datadb.view(classname).structure()
1513 props = [prop.name for prop in props]
1514 self.propcache[classname] = props
1515 return props
1517 def _getpropid(self, classname, propname):
1518 return self._getprops(classname).index(propname)
1520 def _getpropname(self, classname, propid):
1521 return self._getprops(classname)[propid]
1523 def add_text(self, identifier, text, mime_type='text/plain'):
1524 if mime_type != 'text/plain':
1525 return
1526 classname, nodeid, property = identifier
1527 tbls = self.datadb.view('tables')
1528 tblid = tbls.find(name=classname)
1529 if tblid < 0:
1530 raise KeyError, "unknown class %r"%classname
1531 nodeid = int(nodeid)
1532 propid = self._getpropid(classname, property)
1533 ids = self.db.view('ids')
1534 oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
1535 if oldpos > -1:
1536 ids[oldpos].ignore = 1
1537 self.changed = 1
1538 pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
1540 wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
1541 words = {}
1542 for word in wordlist:
1543 if not self.disallows.has_key(word):
1544 words[word] = 1
1545 words = words.keys()
1547 index = self.db.view('index').ordered(1)
1548 for word in words:
1549 ndx = index.find(word=word)
1550 if ndx < 0:
1551 index.append(word=word)
1552 ndx = index.find(word=word)
1553 index[ndx].hits.append(pos=pos)
1554 self.changed = 1
1556 def find(self, wordlist):
1557 hits = None
1558 index = self.db.view('index').ordered(1)
1559 for word in wordlist:
1560 word = word.upper()
1561 if not 2 < len(word) < 26:
1562 continue
1563 ndx = index.find(word=word)
1564 if ndx < 0:
1565 return {}
1566 if hits is None:
1567 hits = index[ndx].hits
1568 else:
1569 hits = hits.intersect(index[ndx].hits)
1570 if len(hits) == 0:
1571 return {}
1572 if hits is None:
1573 return {}
1574 rslt = {}
1575 ids = self.db.view('ids').remapwith(hits)
1576 tbls = self.datadb.view('tables')
1577 for i in range(len(ids)):
1578 hit = ids[i]
1579 if not hit.ignore:
1580 classname = tbls[hit.tblid].name
1581 nodeid = str(hit.nodeid)
1582 property = self._getpropname(classname, hit.propid)
1583 rslt[i] = (classname, nodeid, property)
1584 return rslt
1586 def save_index(self):
1587 if self.changed:
1588 self.db.commit()
1589 self.changed = 0
1591 def rollback(self):
1592 if self.changed:
1593 self.db.rollback()
1594 self.db = metakit.storage(self.path, 1)
1595 self.changed = 0