1 from roundup import hyperdb, date, password, roundupdb, security
2 import metakit
3 from sessions import Sessions
4 import re, marshal, os, sys, weakref, time, calendar
5 from roundup import indexer
6 import locking
8 _dbs = {}
10 def Database(config, journaltag=None):
11 db = _dbs.get(config.DATABASE, None)
12 if db is None or db._db is None:
13 db = _Database(config, journaltag)
14 _dbs[config.DATABASE] = db
15 return db
17 class _Database(hyperdb.Database):
18 def __init__(self, config, journaltag=None):
19 self.config = config
20 self.journaltag = journaltag
21 self.classes = {}
22 self.dirty = 0
23 self.lockfile = None
24 self._db = self.__open()
25 self.indexer = Indexer(self.config.DATABASE, self._db)
26 self.sessions = Sessions(self.config)
27 self.security = security.Security(self)
29 os.umask(0002)
30 def post_init(self):
31 if self.indexer.should_reindex():
32 self.reindex()
34 def reindex(self):
35 for klass in self.classes.values():
36 for nodeid in klass.list():
37 klass.index(nodeid)
38 self.indexer.save_index()
41 # --- defined in ping's spec
42 def __getattr__(self, classname):
43 if classname == 'curuserid':
44 try:
45 self.curuserid = x = int(self.classes['user'].lookup(self.journaltag))
46 except KeyError:
47 x = 0
48 return x
49 elif classname == 'transactions':
50 return self.dirty
51 return self.getclass(classname)
52 def getclass(self, classname):
53 return self.classes[classname]
54 def getclasses(self):
55 return self.classes.keys()
56 # --- end of ping's spec
57 # --- exposed methods
58 def commit(self):
59 if self.dirty:
60 self._db.commit()
61 for cl in self.classes.values():
62 cl._commit()
63 self.indexer.save_index()
64 self.dirty = 0
65 def rollback(self):
66 if self.dirty:
67 for cl in self.classes.values():
68 cl._rollback()
69 self._db.rollback()
70 self.dirty = 0
71 def clear(self):
72 for cl in self.classes.values():
73 cl._clear()
74 def hasnode(self, classname, nodeid):
75 return self.getclass(classname).hasnode(nodeid)
76 def pack(self, pack_before):
77 pass
78 def addclass(self, cl):
79 self.classes[cl.classname] = cl
80 if self.tables.find(name=cl.classname) < 0:
81 self.tables.append(name=cl.classname)
82 def addjournal(self, tablenm, nodeid, action, params):
83 tblid = self.tables.find(name=tablenm)
84 if tblid == -1:
85 tblid = self.tables.append(name=tablenm)
86 # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
87 self.hist.append(tableid=tblid,
88 nodeid=int(nodeid),
89 date=int(time.time()),
90 action=action,
91 user = self.curuserid,
92 params = marshal.dumps(params))
93 def gethistory(self, tablenm, nodeid):
94 rslt = []
95 tblid = self.tables.find(name=tablenm)
96 if tblid == -1:
97 return rslt
98 q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
99 i = 0
100 userclass = self.getclass('user')
101 for row in q:
102 try:
103 params = marshal.loads(row.params)
104 except ValueError:
105 print "history couldn't unmarshal %r" % row.params
106 params = {}
107 usernm = userclass.get(str(row.user), 'username')
108 dt = date.Date(time.gmtime(row.date))
109 rslt.append((i, dt, usernm, _actionnames[row.action], params))
110 i += 1
111 return rslt
113 def close(self):
114 for cl in self.classes.values():
115 cl.db = None
116 self._db = None
117 locking.release_lock(self.lockfile)
118 del _dbs[self.config.DATABASE]
119 self.lockfile.close()
120 self.classes = {}
121 self.indexer = None
123 # --- internal
124 def __open(self):
125 self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
126 lockfilenm = db[:-3]+'lck'
127 self.lockfile = locking.acquire_lock(lockfilenm)
128 self.lockfile.write(str(os.getpid()))
129 self.lockfile.flush()
130 self.fastopen = 0
131 if os.path.exists(db):
132 dbtm = os.path.getmtime(db)
133 pkgnm = self.config.__name__.split('.')[0]
134 schemamod = sys.modules.get(pkgnm+'.dbinit', None)
135 if schemamod:
136 if os.path.exists(schemamod.__file__):
137 schematm = os.path.getmtime(schemamod.__file__)
138 if schematm < dbtm:
139 # found schema mod - it's older than the db
140 self.fastopen = 1
141 else:
142 # can't find schemamod - must be frozen
143 self.fastopen = 1
144 db = metakit.storage(db, 1)
145 hist = db.view('history')
146 tables = db.view('tables')
147 if not self.fastopen:
148 if not hist.structure():
149 hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
150 if not tables.structure():
151 tables = db.getas('tables[name:S]')
152 self.tables = tables
153 self.hist = hist
154 return db
156 _STRINGTYPE = type('')
157 _LISTTYPE = type([])
158 _CREATE, _SET, _RETIRE, _LINK, _UNLINK = range(5)
160 _actionnames = {
161 _CREATE : 'create',
162 _SET : 'set',
163 _RETIRE : 'retire',
164 _LINK : 'link',
165 _UNLINK : 'unlink',
166 }
168 _marker = []
170 _ALLOWSETTINGPRIVATEPROPS = 0
172 class Class:
173 privateprops = None
174 def __init__(self, db, classname, **properties):
175 #self.db = weakref.proxy(db)
176 self.db = db
177 self.classname = classname
178 self.keyname = None
179 self.ruprops = properties
180 self.privateprops = { 'id' : hyperdb.String(),
181 'activity' : hyperdb.Date(),
182 'creation' : hyperdb.Date(),
183 'creator' : hyperdb.Link('user') }
184 self.auditors = {'create': [], 'set': [], 'retire': []} # event -> list of callables
185 self.reactors = {'create': [], 'set': [], 'retire': []} # ditto
186 view = self.__getview()
187 self.maxid = 1
188 if view:
189 self.maxid = view[-1].id + 1
190 self.uncommitted = {}
191 self.rbactions = []
192 # people reach inside!!
193 self.properties = self.ruprops
194 self.db.addclass(self)
195 self.idcache = {}
197 # default is to journal changes
198 self.do_journal = 1
200 def enableJournalling(self):
201 '''Turn journalling on for this class
202 '''
203 self.do_journal = 1
205 def disableJournalling(self):
206 '''Turn journalling off for this class
207 '''
208 self.do_journal = 0
210 # --- the roundup.Class methods
211 def audit(self, event, detector):
212 l = self.auditors[event]
213 if detector not in l:
214 self.auditors[event].append(detector)
215 def fireAuditors(self, action, nodeid, newvalues):
216 for audit in self.auditors[action]:
217 audit(self.db, self, nodeid, newvalues)
218 def fireReactors(self, action, nodeid, oldvalues):
219 for react in self.reactors[action]:
220 react(self.db, self, nodeid, oldvalues)
221 def react(self, event, detector):
222 l = self.reactors[event]
223 if detector not in l:
224 self.reactors[event].append(detector)
225 # --- the hyperdb.Class methods
226 def create(self, **propvalues):
227 self.fireAuditors('create', None, propvalues)
228 rowdict = {}
229 rowdict['id'] = newid = self.maxid
230 self.maxid += 1
231 ndx = self.getview(1).append(rowdict)
232 propvalues['#ISNEW'] = 1
233 try:
234 self.set(str(newid), **propvalues)
235 except Exception:
236 self.maxid -= 1
237 raise
238 return str(newid)
240 def get(self, nodeid, propname, default=_marker, cache=1):
241 # default and cache aren't in the spec
242 # cache=0 means "original value"
244 view = self.getview()
245 id = int(nodeid)
246 if cache == 0:
247 oldnode = self.uncommitted.get(id, None)
248 if oldnode and oldnode.has_key(propname):
249 return oldnode[propname]
250 ndx = self.idcache.get(id, None)
251 if ndx is None:
252 ndx = view.find(id=id)
253 if ndx < 0:
254 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
255 self.idcache[id] = ndx
256 try:
257 raw = getattr(view[ndx], propname)
258 except AttributeError:
259 raise KeyError, propname
260 rutyp = self.ruprops.get(propname, None)
261 if rutyp is None:
262 rutyp = self.privateprops[propname]
263 converter = _converters.get(rutyp.__class__, None)
264 if converter:
265 raw = converter(raw)
266 return raw
268 def set(self, nodeid, **propvalues):
269 isnew = 0
270 if propvalues.has_key('#ISNEW'):
271 isnew = 1
272 del propvalues['#ISNEW']
273 if not isnew:
274 self.fireAuditors('set', nodeid, propvalues)
275 if not propvalues:
276 return
277 if propvalues.has_key('id'):
278 raise KeyError, '"id" is reserved'
279 if self.db.journaltag is None:
280 raise DatabaseError, 'Database open read-only'
281 view = self.getview(1)
282 # node must exist & not be retired
283 id = int(nodeid)
284 ndx = view.find(id=id)
285 if ndx < 0:
286 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
287 row = view[ndx]
288 if row._isdel:
289 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
290 oldnode = self.uncommitted.setdefault(id, {})
291 changes = {}
293 for key, value in propvalues.items():
294 # this will raise the KeyError if the property isn't valid
295 # ... we don't use getprops() here because we only care about
296 # the writeable properties.
297 if _ALLOWSETTINGPRIVATEPROPS:
298 prop = self.ruprops.get(key, None)
299 if not prop:
300 prop = self.privateprops[key]
301 else:
302 prop = self.ruprops[key]
303 converter = _converters.get(prop.__class__, lambda v: v)
304 # if the value's the same as the existing value, no sense in
305 # doing anything
306 oldvalue = converter(getattr(row, key))
307 if value == oldvalue:
308 del propvalues[key]
309 continue
311 # check to make sure we're not duplicating an existing key
312 if key == self.keyname:
313 iv = self.getindexview(1)
314 ndx = iv.find(k=value)
315 if ndx == -1:
316 iv.append(k=value, i=row.id)
317 if not isnew:
318 ndx = iv.find(k=oldvalue)
319 if ndx > -1:
320 iv.delete(ndx)
321 else:
322 raise ValueError, 'node with key "%s" exists'%value
324 # do stuff based on the prop type
325 if isinstance(prop, hyperdb.Link):
326 link_class = prop.classname
327 # must be a string or None
328 if value is not None and not isinstance(value, type('')):
329 raise ValueError, 'property "%s" link value be a string'%(
330 propname)
331 # if it isn't a number, it's a key
332 try:
333 int(value)
334 except ValueError:
335 try:
336 value = self.db.getclass(link_class).lookup(value)
337 except (TypeError, KeyError):
338 raise IndexError, 'new property "%s": %s not a %s'%(
339 key, value, prop.classname)
341 if (value is not None and
342 not self.db.getclass(link_class).hasnode(value)):
343 raise IndexError, '%s has no node %s'%(link_class, value)
345 setattr(row, key, int(value))
346 changes[key] = oldvalue
348 if self.do_journal and prop.do_journal:
349 # register the unlink with the old linked node
350 if oldvalue:
351 self.db.addjournal(link_class, value, _UNLINK,
352 (self.classname, str(row.id), key))
354 # register the link with the newly linked node
355 if value:
356 self.db.addjournal(link_class, value, _LINK,
357 (self.classname, str(row.id), key))
359 elif isinstance(prop, hyperdb.Multilink):
360 if type(value) != _LISTTYPE:
361 raise TypeError, 'new property "%s" not a list of ids'%key
362 link_class = prop.classname
363 l = []
364 for entry in value:
365 if type(entry) != _STRINGTYPE:
366 raise ValueError, 'new property "%s" link value ' \
367 'must be a string'%key
368 # if it isn't a number, it's a key
369 try:
370 int(entry)
371 except ValueError:
372 try:
373 entry = self.db.getclass(link_class).lookup(entry)
374 except (TypeError, KeyError):
375 raise IndexError, 'new property "%s": %s not a %s'%(
376 key, entry, prop.classname)
377 l.append(entry)
378 propvalues[key] = value = l
380 # handle removals
381 rmvd = []
382 for id in oldvalue:
383 if id not in value:
384 rmvd.append(id)
385 # register the unlink with the old linked node
386 if self.do_journal and prop.do_journal:
387 self.db.addjournal(link_class, id, _UNLINK, (self.classname, str(row.id), key))
389 # handle additions
390 adds = []
391 for id in value:
392 if id not in oldvalue:
393 if not self.db.getclass(link_class).hasnode(id):
394 raise IndexError, '%s has no node %s'%(
395 link_class, id)
396 adds.append(id)
397 # register the link with the newly linked node
398 if self.do_journal and prop.do_journal:
399 self.db.addjournal(link_class, id, _LINK, (self.classname, str(row.id), key))
401 sv = getattr(row, key)
402 i = 0
403 while i < len(sv):
404 if str(sv[i].fid) in rmvd:
405 sv.delete(i)
406 else:
407 i += 1
408 for id in adds:
409 sv.append(fid=int(id))
410 changes[key] = oldvalue
413 elif isinstance(prop, hyperdb.String):
414 if value is not None and type(value) != _STRINGTYPE:
415 raise TypeError, 'new property "%s" not a string'%key
416 setattr(row, key, value)
417 changes[key] = oldvalue
418 if hasattr(prop, 'isfilename') and prop.isfilename:
419 propvalues[key] = os.path.basename(value)
420 if prop.indexme:
421 self.db.indexer.add_text((self.classname, nodeid, key), value, 'text/plain')
423 elif isinstance(prop, hyperdb.Password):
424 if not isinstance(value, password.Password):
425 raise TypeError, 'new property "%s" not a Password'% key
426 setattr(row, key, str(value))
427 changes[key] = str(oldvalue)
428 propvalues[key] = str(value)
430 elif value is not None and isinstance(prop, hyperdb.Date):
431 if not isinstance(value, date.Date):
432 raise TypeError, 'new property "%s" not a Date'% key
433 setattr(row, key, int(calendar.timegm(value.get_tuple())))
434 changes[key] = str(oldvalue)
435 propvalues[key] = str(value)
437 elif value is not None and isinstance(prop, hyperdb.Interval):
438 if not isinstance(value, date.Interval):
439 raise TypeError, 'new property "%s" not an Interval'% key
440 setattr(row, key, str(value))
441 changes[key] = str(oldvalue)
442 propvalues[key] = str(value)
444 elif value is not None and isinstance(prop, hyperdb.Number):
445 setattr(row, key, int(value))
446 changes[key] = oldvalue
447 propvalues[key] = value
449 elif value is not None and isinstance(prop, hyperdb.Boolean):
450 bv = value != 0
451 setattr(row, key, bv)
452 changes[key] = oldvalue
453 propvalues[key] = value
455 oldnode[key] = oldvalue
457 # nothing to do?
458 if not propvalues:
459 return
460 if not propvalues.has_key('activity'):
461 row.activity = int(time.time())
462 if isnew:
463 if not row.creation:
464 row.creation = int(time.time())
465 if not row.creator:
466 row.creator = self.db.curuserid
468 self.db.dirty = 1
469 if self.do_journal:
470 if isnew:
471 self.db.addjournal(self.classname, nodeid, _CREATE, {})
472 self.fireReactors('create', nodeid, None)
473 else:
474 self.db.addjournal(self.classname, nodeid, _SET, changes)
475 self.fireReactors('set', nodeid, oldnode)
477 def retire(self, nodeid):
478 self.fireAuditors('retire', nodeid, None)
479 view = self.getview(1)
480 ndx = view.find(id=int(nodeid))
481 if ndx < 0:
482 raise KeyError, "nodeid %s not found" % nodeid
483 row = view[ndx]
484 oldvalues = self.uncommitted.setdefault(row.id, {})
485 oldval = oldvalues['_isdel'] = row._isdel
486 row._isdel = 1
487 if self.do_journal:
488 self.db.addjournal(self.classname, nodeid, _RETIRE, {})
489 if self.keyname:
490 iv = self.getindexview(1)
491 ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
492 if ndx > -1:
493 iv.delete(ndx)
494 self.db.dirty = 1
495 self.fireReactors('retire', nodeid, None)
496 def history(self, nodeid):
497 if not self.do_journal:
498 raise ValueError, 'Journalling is disabled for this class'
499 return self.db.gethistory(self.classname, nodeid)
500 def setkey(self, propname):
501 if self.keyname:
502 if propname == self.keyname:
503 return
504 raise ValueError, "%s already indexed on %s" % (self.classname, self.keyname)
505 # first setkey for this run
506 self.keyname = propname
507 iv = self.db._db.view('_%s' % self.classname)
508 if self.db.fastopen and iv.structure():
509 return
510 # very first setkey ever
511 self.db.dirty = 1
512 iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
513 iv = iv.ordered(1)
514 # print "setkey building index"
515 for row in self.getview():
516 iv.append(k=getattr(row, propname), i=row.id)
517 self.db.commit()
518 def getkey(self):
519 return self.keyname
520 def lookup(self, keyvalue):
521 if type(keyvalue) is not _STRINGTYPE:
522 raise TypeError, "%r is not a string" % keyvalue
523 iv = self.getindexview()
524 if iv:
525 ndx = iv.find(k=keyvalue)
526 if ndx > -1:
527 return str(iv[ndx].i)
528 else:
529 view = self.getview()
530 ndx = view.find({self.keyname:keyvalue, '_isdel':0})
531 if ndx > -1:
532 return str(view[ndx].id)
533 raise KeyError, keyvalue
535 def destroy(self, keyvalue):
536 #TODO clean this up once Richard's said how it should work
537 iv = self.getindexview()
538 if iv:
539 ndx = iv.find(k=keyvalue)
540 if ndx > -1:
541 id = iv[ndx].i
542 iv.delete(ndx)
543 view = self.getview()
544 ndx = view.find(id=id)
545 if ndx > -1:
546 view.delete(ndx)
548 def find(self, **propspec):
549 """Get the ids of nodes in this class which link to the given nodes.
551 'propspec' consists of keyword args propname={nodeid:1,}
552 'propname' must be the name of a property in this class, or a
553 KeyError is raised. That property must be a Link or
554 Multilink property, or a TypeError is raised.
556 Any node in this class whose propname property links to any of the
557 nodeids will be returned. Used by the full text indexing, which knows
558 that "foo" occurs in msg1, msg3 and file7; so we have hits on these
559 issues:
561 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
563 """
564 propspec = propspec.items()
565 for propname, nodeid in propspec:
566 # check the prop is OK
567 prop = self.ruprops[propname]
568 if (not isinstance(prop, hyperdb.Link) and
569 not isinstance(prop, hyperdb.Multilink)):
570 raise TypeError, "'%s' not a Link/Multilink property"%propname
572 vws = []
573 for propname, ids in propspec:
574 if type(ids) is _STRINGTYPE:
575 ids = {ids:1}
576 prop = self.ruprops[propname]
577 view = self.getview()
578 if isinstance(prop, hyperdb.Multilink):
579 view = view.flatten(getattr(view, propname))
580 def ff(row, nm=propname, ids=ids):
581 return ids.has_key(str(row.fid))
582 else:
583 def ff(row, nm=propname, ids=ids):
584 return ids.has_key(str(getattr(row, nm)))
585 ndxview = view.filter(ff)
586 vws.append(ndxview.unique())
588 # handle the empty match case
589 if not vws:
590 return []
592 ndxview = vws[0]
593 for v in vws[1:]:
594 ndxview = ndxview.union(v)
595 view = view.remapwith(ndxview)
596 rslt = []
597 for row in view:
598 rslt.append(str(row.id))
599 return rslt
602 def list(self):
603 l = []
604 for row in self.getview().select(_isdel=0):
605 l.append(str(row.id))
606 return l
607 def count(self):
608 return len(self.getview())
609 def getprops(self, protected=1):
610 # protected is not in ping's spec
611 allprops = self.ruprops.copy()
612 if protected and self.privateprops is not None:
613 allprops.update(self.privateprops)
614 return allprops
615 def addprop(self, **properties):
616 for key in properties.keys():
617 if self.ruprops.has_key(key):
618 raise ValueError, "%s is already a property of %s" % (key, self.classname)
619 self.ruprops.update(properties)
620 self.db.fastopen = 0
621 view = self.__getview()
622 self.db.commit()
623 # ---- end of ping's spec
624 def filter(self, search_matches, filterspec, sort, group):
625 # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
626 # filterspec is a dict {propname:value}
627 # sort and group are lists of propnames
629 where = {'_isdel':0}
630 mlcriteria = {}
631 regexes = {}
632 orcriteria = {}
633 for propname, value in filterspec.items():
634 prop = self.ruprops.get(propname, None)
635 if prop is None:
636 prop = self.privateprops[propname]
637 if isinstance(prop, hyperdb.Multilink):
638 if type(value) is not _LISTTYPE:
639 value = [value]
640 # transform keys to ids
641 u = []
642 for item in value:
643 try:
644 item = int(item)
645 except (TypeError, ValueError):
646 item = int(self.db.getclass(prop.classname).lookup(item))
647 if item == -1:
648 item = 0
649 u.append(item)
650 mlcriteria[propname] = u
651 elif isinstance(prop, hyperdb.Link):
652 if type(value) is not _LISTTYPE:
653 value = [value]
654 # transform keys to ids
655 u = []
656 for item in value:
657 try:
658 item = int(item)
659 except (TypeError, ValueError):
660 item = int(self.db.getclass(prop.classname).lookup(item))
661 if item == -1:
662 item = 0
663 u.append(item)
664 if len(u) == 1:
665 where[propname] = u[0]
666 else:
667 orcriteria[propname] = u
668 elif isinstance(prop, hyperdb.String):
669 # simple glob searching
670 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
671 v = v.replace('?', '.')
672 v = v.replace('*', '.*?')
673 regexes[propname] = re.compile(v, re.I)
674 elif propname == 'id':
675 where[propname] = int(value)
676 elif isinstance(prop, hyperdb.Boolean):
677 if type(value) is _STRINGTYPE:
678 bv = value.lower() in ('yes', 'true', 'on', '1')
679 else:
680 bv = value
681 where[propname] = bv
682 elif isinstance(prop, hyperdb.Number):
683 where[propname] = int(value)
684 else:
685 where[propname] = str(value)
686 v = self.getview()
687 #print "filter start at %s" % time.time()
688 if where:
689 v = v.select(where)
690 #print "filter where at %s" % time.time()
692 if mlcriteria:
693 # multilink - if any of the nodeids required by the
694 # filterspec aren't in this node's property, then skip
695 # it
696 def ff(row, ml=mlcriteria):
697 for propname, values in ml.items():
698 sv = getattr(row, propname)
699 for id in values:
700 if sv.find(fid=id) == -1:
701 return 0
702 return 1
703 iv = v.filter(ff)
704 v = v.remapwith(iv)
706 #print "filter mlcrit at %s" % time.time()
708 if orcriteria:
709 def ff(row, crit=orcriteria):
710 for propname, allowed in crit.items():
711 val = getattr(row, propname)
712 if val not in allowed:
713 return 0
714 return 1
716 iv = v.filter(ff)
717 v = v.remapwith(iv)
719 #print "filter orcrit at %s" % time.time()
720 if regexes:
721 def ff(row, r=regexes):
722 for propname, regex in r.items():
723 val = getattr(row, propname)
724 if not regex.search(val):
725 return 0
726 return 1
728 iv = v.filter(ff)
729 v = v.remapwith(iv)
730 #print "filter regexs at %s" % time.time()
732 if sort or group:
733 sortspec = []
734 rev = []
735 for propname in group + sort:
736 isreversed = 0
737 if propname[0] == '-':
738 propname = propname[1:]
739 isreversed = 1
740 try:
741 prop = getattr(v, propname)
742 except AttributeError:
743 print "MK has no property %s" % propname
744 continue
745 propclass = self.ruprops.get(propname, None)
746 if propclass is None:
747 propclass = self.privateprops.get(propname, None)
748 if propclass is None:
749 print "Schema has no property %s" % propname
750 continue
751 if isinstance(propclass, hyperdb.Link):
752 linkclass = self.db.getclass(propclass.classname)
753 lv = linkclass.getview()
754 lv = lv.rename('id', propname)
755 v = v.join(lv, prop, 1)
756 if linkclass.getprops().has_key('order'):
757 propname = 'order'
758 else:
759 propname = linkclass.labelprop()
760 prop = getattr(v, propname)
761 if isreversed:
762 rev.append(prop)
763 sortspec.append(prop)
764 v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
765 #print "filter sort at %s" % time.time()
767 rslt = []
768 for row in v:
769 id = str(row.id)
770 if search_matches is not None:
771 if search_matches.has_key(id):
772 rslt.append(id)
773 else:
774 rslt.append(id)
775 return rslt
777 def hasnode(self, nodeid):
778 return int(nodeid) < self.maxid
780 def labelprop(self, default_to_id=0):
781 ''' Return the property name for a label for the given node.
783 This method attempts to generate a consistent label for the node.
784 It tries the following in order:
785 1. key property
786 2. "name" property
787 3. "title" property
788 4. first property from the sorted property name list
789 '''
790 k = self.getkey()
791 if k:
792 return k
793 props = self.getprops()
794 if props.has_key('name'):
795 return 'name'
796 elif props.has_key('title'):
797 return 'title'
798 if default_to_id:
799 return 'id'
800 props = props.keys()
801 props.sort()
802 return props[0]
803 def stringFind(self, **requirements):
804 """Locate a particular node by matching a set of its String
805 properties in a caseless search.
807 If the property is not a String property, a TypeError is raised.
809 The return is a list of the id of all nodes that match.
810 """
811 for propname in requirements.keys():
812 prop = self.properties[propname]
813 if isinstance(not prop, hyperdb.String):
814 raise TypeError, "'%s' not a String property"%propname
815 requirements[propname] = requirements[propname].lower()
816 requirements['_isdel'] = 0
818 l = []
819 for row in self.getview().select(requirements):
820 l.append(str(row.id))
821 return l
823 def addjournal(self, nodeid, action, params):
824 self.db.addjournal(self.classname, nodeid, action, params)
826 def index(self, nodeid):
827 ''' Add (or refresh) the node to search indexes '''
828 # find all the String properties that have indexme
829 for prop, propclass in self.getprops().items():
830 if isinstance(propclass, hyperdb.String) and propclass.indexme:
831 # index them under (classname, nodeid, property)
832 self.db.indexer.add_text((self.classname, nodeid, prop),
833 str(self.get(nodeid, prop)))
835 # --- used by Database
836 def _commit(self):
837 """ called post commit of the DB.
838 interested subclasses may override """
839 self.uncommitted = {}
840 self.rbactions = []
841 self.idcache = {}
842 def _rollback(self):
843 """ called pre rollback of the DB.
844 interested subclasses may override """
845 for action in self.rbactions:
846 action()
847 self.rbactions = []
848 self.uncommitted = {}
849 self.idcache = {}
850 def _clear(self):
851 view = self.getview(1)
852 if len(view):
853 view[:] = []
854 self.db.dirty = 1
855 iv = self.getindexview(1)
856 if iv:
857 iv[:] = []
858 def rollbackaction(self, action):
859 """ call this to register a callback called on rollback
860 callback is removed on end of transaction """
861 self.rbactions.append(action)
862 # --- internal
863 def __getview(self):
864 db = self.db._db
865 view = db.view(self.classname)
866 mkprops = view.structure()
867 if mkprops and self.db.fastopen:
868 return view.ordered(1)
869 # is the definition the same?
870 for nm, rutyp in self.ruprops.items():
871 for mkprop in mkprops:
872 if mkprop.name == nm:
873 break
874 else:
875 mkprop = None
876 if mkprop is None:
877 break
878 if _typmap[rutyp.__class__] != mkprop.type:
879 break
880 else:
881 return view.ordered(1)
882 # need to create or restructure the mk view
883 # id comes first, so MK will order it for us
884 self.db.dirty = 1
885 s = ["%s[id:I" % self.classname]
886 for nm, rutyp in self.ruprops.items():
887 mktyp = _typmap[rutyp.__class__]
888 s.append('%s:%s' % (nm, mktyp))
889 if mktyp == 'V':
890 s[-1] += ('[fid:I]')
891 s.append('_isdel:I,activity:I,creation:I,creator:I]')
892 v = self.db._db.getas(','.join(s))
893 self.db.commit()
894 return v.ordered(1)
895 def getview(self, RW=0):
896 return self.db._db.view(self.classname).ordered(1)
897 def getindexview(self, RW=0):
898 return self.db._db.view("_%s" % self.classname).ordered(1)
900 def _fetchML(sv):
901 l = []
902 for row in sv:
903 if row.fid:
904 l.append(str(row.fid))
905 return l
907 def _fetchPW(s):
908 p = password.Password()
909 p.unpack(s)
910 return p
912 def _fetchLink(n):
913 return n and str(n) or None
915 def _fetchDate(n):
916 return date.Date(time.gmtime(n))
918 _converters = {
919 hyperdb.Date : _fetchDate,
920 hyperdb.Link : _fetchLink,
921 hyperdb.Multilink : _fetchML,
922 hyperdb.Interval : date.Interval,
923 hyperdb.Password : _fetchPW,
924 hyperdb.Boolean : lambda n: n,
925 hyperdb.Number : lambda n: n,
926 hyperdb.String : str,
927 }
929 class FileName(hyperdb.String):
930 isfilename = 1
932 _typmap = {
933 FileName : 'S',
934 hyperdb.String : 'S',
935 hyperdb.Date : 'I',
936 hyperdb.Link : 'I',
937 hyperdb.Multilink : 'V',
938 hyperdb.Interval : 'S',
939 hyperdb.Password : 'S',
940 hyperdb.Boolean : 'I',
941 hyperdb.Number : 'I',
942 }
943 class FileClass(Class):
944 ' like Class but with a content property '
945 default_mime_type = 'text/plain'
946 def __init__(self, db, classname, **properties):
947 properties['content'] = FileName()
948 if not properties.has_key('type'):
949 properties['type'] = hyperdb.String()
950 Class.__init__(self, db, classname, **properties)
951 def get(self, nodeid, propname, default=_marker, cache=1):
952 x = Class.get(self, nodeid, propname, default, cache)
953 if propname == 'content':
954 if x.startswith('file:'):
955 fnm = x[5:]
956 try:
957 x = open(fnm, 'rb').read()
958 except Exception, e:
959 x = repr(e)
960 return x
961 def create(self, **propvalues):
962 content = propvalues['content']
963 del propvalues['content']
964 newid = Class.create(self, **propvalues)
965 if not content:
966 return newid
967 nm = bnm = '%s%s' % (self.classname, newid)
968 sd = str(int(int(newid) / 1000))
969 d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
970 if not os.path.exists(d):
971 os.makedirs(d)
972 nm = os.path.join(d, nm)
973 open(nm, 'wb').write(content)
974 self.set(newid, content = 'file:'+nm)
975 mimetype = propvalues.get('type', self.default_mime_type)
976 self.db.indexer.add_text((self.classname, newid, 'content'), content, mimetype)
977 def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
978 action1(fnm)
979 self.rollbackaction(undo)
980 return newid
981 def index(self, nodeid):
982 Class.index(self, nodeid)
983 mimetype = self.get(nodeid, 'type')
984 if not mimetype:
985 mimetype = self.default_mime_type
986 self.db.indexer.add_text((self.classname, nodeid, 'content'),
987 self.get(nodeid, 'content'), mimetype)
989 class IssueClass(Class, roundupdb.IssueClass):
990 # Overridden methods:
991 def __init__(self, db, classname, **properties):
992 """The newly-created class automatically includes the "messages",
993 "files", "nosy", and "superseder" properties. If the 'properties'
994 dictionary attempts to specify any of these properties or a
995 "creation" or "activity" property, a ValueError is raised."""
996 if not properties.has_key('title'):
997 properties['title'] = hyperdb.String(indexme='yes')
998 if not properties.has_key('messages'):
999 properties['messages'] = hyperdb.Multilink("msg")
1000 if not properties.has_key('files'):
1001 properties['files'] = hyperdb.Multilink("file")
1002 if not properties.has_key('nosy'):
1003 properties['nosy'] = hyperdb.Multilink("user")
1004 if not properties.has_key('superseder'):
1005 properties['superseder'] = hyperdb.Multilink(classname)
1006 Class.__init__(self, db, classname, **properties)
1008 CURVERSION = 1
1010 class Indexer(indexer.Indexer):
1011 disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1012 def __init__(self, path, datadb):
1013 self.db = metakit.storage(os.path.join(path, 'index.mk4'), 1)
1014 self.datadb = datadb
1015 self.reindex = 0
1016 v = self.db.view('version')
1017 if not v.structure():
1018 v = self.db.getas('version[vers:I]')
1019 self.db.commit()
1020 v.append(vers=CURVERSION)
1021 self.reindex = 1
1022 elif v[0].vers != CURVERSION:
1023 v[0].vers = CURVERSION
1024 self.reindex = 1
1025 if self.reindex:
1026 self.db.getas('ids[tblid:I,nodeid:I,propid:I]')
1027 self.db.getas('index[word:S,hits[pos:I]]')
1028 self.db.commit()
1029 self.reindex = 1
1030 self.changed = 0
1031 self.propcache = {}
1032 def force_reindex(self):
1033 v = self.db.view('ids')
1034 v[:] = []
1035 v = self.db.view('index')
1036 v[:] = []
1037 self.db.commit()
1038 self.reindex = 1
1039 def should_reindex(self):
1040 return self.reindex
1041 def _getprops(self, classname):
1042 props = self.propcache.get(classname, None)
1043 if props is None:
1044 props = self.datadb.view(classname).structure()
1045 props = [prop.name for prop in props]
1046 self.propcache[classname] = props
1047 return props
1048 def _getpropid(self, classname, propname):
1049 return self._getprops(classname).index(propname)
1050 def _getpropname(self, classname, propid):
1051 return self._getprops(classname)[propid]
1052 def add_text(self, identifier, text, mime_type='text/plain'):
1053 if mime_type != 'text/plain':
1054 return
1055 classname, nodeid, property = identifier
1056 tbls = self.datadb.view('tables')
1057 tblid = tbls.find(name=classname)
1058 if tblid < 0:
1059 raise KeyError, "unknown class %r"%classname
1060 nodeid = int(nodeid)
1061 propid = self._getpropid(classname, property)
1062 pos = self.db.view('ids').append(tblid=tblid,nodeid=nodeid,propid=propid)
1064 wordlist = re.findall(r'\b\w{3,25}\b', text)
1065 words = {}
1066 for word in wordlist:
1067 word = word.upper()
1068 if not self.disallows.has_key(word):
1069 words[word] = 1
1070 words = words.keys()
1072 index = self.db.view('index').ordered(1)
1073 for word in words:
1074 ndx = index.find(word=word)
1075 if ndx < 0:
1076 ndx = index.append(word=word)
1077 hits = index[ndx].hits
1078 if len(hits)==0 or hits.find(pos=pos) < 0:
1079 hits.append(pos=pos)
1080 self.changed = 1
1081 def find(self, wordlist):
1082 hits = None
1083 index = self.db.view('index').ordered(1)
1084 for word in wordlist:
1085 if not 2 < len(word) < 26:
1086 continue
1087 ndx = index.find(word=word)
1088 if ndx < 0:
1089 return {}
1090 if hits is None:
1091 hits = index[ndx].hits
1092 else:
1093 hits = hits.intersect(index[ndx].hits)
1094 if len(hits) == 0:
1095 return {}
1096 if hits is None:
1097 return {}
1098 rslt = {}
1099 ids = self.db.view('ids').remapwith(hits)
1100 tbls = self.datadb.view('tables')
1101 for i in range(len(ids)):
1102 hit = ids[i]
1103 classname = tbls[hit.tblid].name
1104 nodeid = str(hit.nodeid)
1105 property = self._getpropname(classname, hit.propid)
1106 rslt[i] = (classname, nodeid, property)
1107 return rslt
1108 def save_index(self):
1109 if self.changed:
1110 self.db.commit()
1111 self.changed = 0