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