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