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