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