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 else:
16 db.journaltag = journaltag
17 try:
18 delattr(db, 'curuserid')
19 except AttributeError:
20 pass
21 return db
23 class _Database(hyperdb.Database):
24 def __init__(self, config, journaltag=None):
25 self.config = config
26 self.journaltag = journaltag
27 self.classes = {}
28 self.dirty = 0
29 self.lockfile = None
30 self._db = self.__open()
31 self.indexer = Indexer(self.config.DATABASE, self._db)
32 self.sessions = Sessions(self.config)
33 self.security = security.Security(self)
35 os.umask(0002)
37 def post_init(self):
38 if self.indexer.should_reindex():
39 self.reindex()
41 def reindex(self):
42 for klass in self.classes.values():
43 for nodeid in klass.list():
44 klass.index(nodeid)
45 self.indexer.save_index()
47 # --- defined in ping's spec
48 def __getattr__(self, classname):
49 if classname == 'curuserid':
50 if self.journaltag is None:
51 return None
53 try:
54 self.curuserid = x = int(self.classes['user'].lookup(self.journaltag))
55 except KeyError:
56 if self.journaltag == 'admin':
57 self.curuserid = x = 1
58 else:
59 x = 0
60 return x
61 elif classname == 'transactions':
62 return self.dirty
63 return self.getclass(classname)
64 def getclass(self, classname):
65 try:
66 return self.classes[classname]
67 except KeyError:
68 raise KeyError, 'There is no class called "%s"'%classname
69 def getclasses(self):
70 return self.classes.keys()
71 # --- end of ping's spec
72 # --- exposed methods
73 def commit(self):
74 if self.dirty:
75 self._db.commit()
76 for cl in self.classes.values():
77 cl._commit()
78 self.indexer.save_index()
79 self.dirty = 0
80 def rollback(self):
81 if self.dirty:
82 for cl in self.classes.values():
83 cl._rollback()
84 self._db.rollback()
85 self._db = None
86 self._db = metakit.storage(self.dbnm, 1)
87 self.hist = self._db.view('history')
88 self.tables = self._db.view('tables')
89 self.indexer.rollback()
90 self.indexer.datadb = self._db
91 self.dirty = 0
92 def clearCache(self):
93 for cl in self.classes.values():
94 cl._commit()
95 def clear(self):
96 for cl in self.classes.values():
97 cl._clear()
98 def hasnode(self, classname, nodeid):
99 return self.getclass(classname).hasnode(nodeid)
100 def pack(self, pack_before):
101 mindate = int(calendar.timegm(pack_before.get_tuple()))
102 i = 0
103 while i < len(self.hist):
104 if self.hist[i].date < mindate and self.hist[i].action != _CREATE:
105 self.hist.delete(i)
106 else:
107 i = i + 1
108 def addclass(self, cl):
109 self.classes[cl.classname] = cl
110 if self.tables.find(name=cl.classname) < 0:
111 self.tables.append(name=cl.classname)
112 def addjournal(self, tablenm, nodeid, action, params, creator=None,
113 creation=None):
114 tblid = self.tables.find(name=tablenm)
115 if tblid == -1:
116 tblid = self.tables.append(name=tablenm)
117 if creator is None:
118 creator = self.curuserid
119 else:
120 try:
121 creator = int(creator)
122 except TypeError:
123 creator = int(self.getclass('user').lookup(creator))
124 if creation is None:
125 creation = int(time.time())
126 elif isinstance(creation, date.Date):
127 creation = int(calendar.timegm(creation.get_tuple()))
128 # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
129 self.hist.append(tableid=tblid,
130 nodeid=int(nodeid),
131 date=creation,
132 action=action,
133 user = creator,
134 params = marshal.dumps(params))
135 def getjournal(self, tablenm, nodeid):
136 rslt = []
137 tblid = self.tables.find(name=tablenm)
138 if tblid == -1:
139 return rslt
140 q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
141 if len(q) == 0:
142 raise IndexError, "no history for id %s in %s" % (nodeid, tablenm)
143 i = 0
144 #userclass = self.getclass('user')
145 for row in q:
146 try:
147 params = marshal.loads(row.params)
148 except ValueError:
149 print "history couldn't unmarshal %r" % row.params
150 params = {}
151 #usernm = userclass.get(str(row.user), 'username')
152 dt = date.Date(time.gmtime(row.date))
153 #rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
154 rslt.append((nodeid, dt, str(row.user), _actionnames[row.action], params))
155 return rslt
157 def destroyjournal(self, tablenm, nodeid):
158 nodeid = int(nodeid)
159 tblid = self.tables.find(name=tablenm)
160 if tblid == -1:
161 return
162 i = 0
163 hist = self.hist
164 while i < len(hist):
165 if hist[i].tableid == tblid and hist[i].nodeid == nodeid:
166 hist.delete(i)
167 else:
168 i = i + 1
169 self.dirty = 1
171 def close(self):
172 for cl in self.classes.values():
173 cl.db = None
174 self._db = None
175 if self.lockfile is not None:
176 locking.release_lock(self.lockfile)
177 if _dbs.has_key(self.config.DATABASE):
178 del _dbs[self.config.DATABASE]
179 if self.lockfile is not None:
180 self.lockfile.close()
181 self.lockfile = None
182 self.classes = {}
183 self.indexer = None
185 # --- internal
186 def __open(self):
187 if not os.path.exists(self.config.DATABASE):
188 os.makedirs(self.config.DATABASE)
189 self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
190 lockfilenm = db[:-3]+'lck'
191 self.lockfile = locking.acquire_lock(lockfilenm)
192 self.lockfile.write(str(os.getpid()))
193 self.lockfile.flush()
194 self.fastopen = 0
195 if os.path.exists(db):
196 dbtm = os.path.getmtime(db)
197 pkgnm = self.config.__name__.split('.')[0]
198 schemamod = sys.modules.get(pkgnm+'.dbinit', None)
199 if schemamod:
200 if os.path.exists(schemamod.__file__):
201 schematm = os.path.getmtime(schemamod.__file__)
202 if schematm < dbtm:
203 # found schema mod - it's older than the db
204 self.fastopen = 1
205 else:
206 # can't find schemamod - must be frozen
207 self.fastopen = 1
208 db = metakit.storage(db, 1)
209 hist = db.view('history')
210 tables = db.view('tables')
211 if not self.fastopen:
212 if not hist.structure():
213 hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
214 if not tables.structure():
215 tables = db.getas('tables[name:S]')
216 db.commit()
217 self.tables = tables
218 self.hist = hist
219 return db
221 _STRINGTYPE = type('')
222 _LISTTYPE = type([])
223 _CREATE, _SET, _RETIRE, _LINK, _UNLINK = range(5)
225 _actionnames = {
226 _CREATE : 'create',
227 _SET : 'set',
228 _RETIRE : 'retire',
229 _LINK : 'link',
230 _UNLINK : 'unlink',
231 }
233 _marker = []
235 _ALLOWSETTINGPRIVATEPROPS = 0
237 class Class:
238 privateprops = None
239 def __init__(self, db, classname, **properties):
240 #self.db = weakref.proxy(db)
241 self.db = db
242 self.classname = classname
243 self.keyname = None
244 self.ruprops = properties
245 self.privateprops = { 'id' : hyperdb.String(),
246 'activity' : hyperdb.Date(),
247 'creation' : hyperdb.Date(),
248 'creator' : hyperdb.Link('user') }
250 # event -> list of callables
251 self.auditors = {'create': [], 'set': [], 'retire': []}
252 self.reactors = {'create': [], 'set': [], 'retire': []}
254 view = self.__getview()
255 self.maxid = 1
256 if view:
257 self.maxid = view[-1].id + 1
258 self.uncommitted = {}
259 self.rbactions = []
261 # people reach inside!!
262 self.properties = self.ruprops
263 self.db.addclass(self)
264 self.idcache = {}
266 # default is to journal changes
267 self.do_journal = 1
269 def enableJournalling(self):
270 '''Turn journalling on for this class
271 '''
272 self.do_journal = 1
274 def disableJournalling(self):
275 '''Turn journalling off for this class
276 '''
277 self.do_journal = 0
279 # --- the roundup.Class methods
280 def audit(self, event, detector):
281 l = self.auditors[event]
282 if detector not in l:
283 self.auditors[event].append(detector)
284 def fireAuditors(self, action, nodeid, newvalues):
285 for audit in self.auditors[action]:
286 audit(self.db, self, nodeid, newvalues)
287 def fireReactors(self, action, nodeid, oldvalues):
288 for react in self.reactors[action]:
289 react(self.db, self, nodeid, oldvalues)
290 def react(self, event, detector):
291 l = self.reactors[event]
292 if detector not in l:
293 self.reactors[event].append(detector)
295 # --- the hyperdb.Class methods
296 def create(self, **propvalues):
297 self.fireAuditors('create', None, propvalues)
298 rowdict = {}
299 rowdict['id'] = newid = self.maxid
300 self.maxid += 1
301 ndx = self.getview(1).append(rowdict)
302 propvalues['#ISNEW'] = 1
303 try:
304 self.set(str(newid), **propvalues)
305 except Exception:
306 self.maxid -= 1
307 raise
308 return str(newid)
310 def get(self, nodeid, propname, default=_marker, cache=1):
311 # default and cache aren't in the spec
312 # cache=0 means "original value"
314 view = self.getview()
315 id = int(nodeid)
316 if cache == 0:
317 oldnode = self.uncommitted.get(id, None)
318 if oldnode and oldnode.has_key(propname):
319 return oldnode[propname]
320 ndx = self.idcache.get(id, None)
321 if ndx is None:
322 ndx = view.find(id=id)
323 if ndx < 0:
324 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
325 self.idcache[id] = ndx
326 try:
327 raw = getattr(view[ndx], propname)
328 except AttributeError:
329 raise KeyError, propname
330 rutyp = self.ruprops.get(propname, None)
331 if rutyp is None:
332 rutyp = self.privateprops[propname]
333 converter = _converters.get(rutyp.__class__, None)
334 if converter:
335 raw = converter(raw)
336 return raw
338 def set(self, nodeid, **propvalues):
339 isnew = 0
340 if propvalues.has_key('#ISNEW'):
341 isnew = 1
342 del propvalues['#ISNEW']
343 if not isnew:
344 self.fireAuditors('set', nodeid, propvalues)
345 if not propvalues:
346 return propvalues
347 if propvalues.has_key('id'):
348 raise KeyError, '"id" is reserved'
349 if self.db.journaltag is None:
350 raise hyperdb.DatabaseError, 'Database open read-only'
351 view = self.getview(1)
353 # node must exist & not be retired
354 id = int(nodeid)
355 ndx = view.find(id=id)
356 if ndx < 0:
357 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
358 row = view[ndx]
359 if row._isdel:
360 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
361 oldnode = self.uncommitted.setdefault(id, {})
362 changes = {}
364 for key, value in propvalues.items():
365 # this will raise the KeyError if the property isn't valid
366 # ... we don't use getprops() here because we only care about
367 # the writeable properties.
368 if _ALLOWSETTINGPRIVATEPROPS:
369 prop = self.ruprops.get(key, None)
370 if not prop:
371 prop = self.privateprops[key]
372 else:
373 prop = self.ruprops[key]
374 converter = _converters.get(prop.__class__, lambda v: v)
375 # if the value's the same as the existing value, no sense in
376 # doing anything
377 oldvalue = converter(getattr(row, key))
378 if value == oldvalue:
379 del propvalues[key]
380 continue
382 # check to make sure we're not duplicating an existing key
383 if key == self.keyname:
384 iv = self.getindexview(1)
385 ndx = iv.find(k=value)
386 if ndx == -1:
387 iv.append(k=value, i=row.id)
388 if not isnew:
389 ndx = iv.find(k=oldvalue)
390 if ndx > -1:
391 iv.delete(ndx)
392 else:
393 raise ValueError, 'node with key "%s" exists'%value
395 # do stuff based on the prop type
396 if isinstance(prop, hyperdb.Link):
397 link_class = prop.classname
398 # must be a string or None
399 if value is not None and not isinstance(value, type('')):
400 raise ValueError, 'property "%s" link value be a string'%(
401 key)
402 # Roundup sets to "unselected" by passing None
403 if value is None:
404 value = 0
405 # if it isn't a number, it's a key
406 try:
407 int(value)
408 except ValueError:
409 try:
410 value = self.db.getclass(link_class).lookup(value)
411 except (TypeError, KeyError):
412 raise IndexError, 'new property "%s": %s not a %s'%(
413 key, value, prop.classname)
415 if (value is not None and
416 not self.db.getclass(link_class).hasnode(value)):
417 raise IndexError, '%s has no node %s'%(link_class, value)
419 setattr(row, key, int(value))
420 changes[key] = oldvalue
422 if self.do_journal and prop.do_journal:
423 # register the unlink with the old linked node
424 if oldvalue:
425 self.db.addjournal(link_class, value, _UNLINK,
426 (self.classname, str(row.id), key))
428 # register the link with the newly linked node
429 if value:
430 self.db.addjournal(link_class, value, _LINK,
431 (self.classname, str(row.id), key))
433 elif isinstance(prop, hyperdb.Multilink):
434 if value is not None and type(value) != _LISTTYPE:
435 raise TypeError, 'new property "%s" not a list of ids'%key
436 link_class = prop.classname
437 l = []
438 if value is None:
439 value = []
440 for entry in value:
441 if type(entry) != _STRINGTYPE:
442 raise ValueError, 'new property "%s" link value ' \
443 'must be a string'%key
444 # if it isn't a number, it's a key
445 try:
446 int(entry)
447 except ValueError:
448 try:
449 entry = self.db.getclass(link_class).lookup(entry)
450 except (TypeError, KeyError):
451 raise IndexError, 'new property "%s": %s not a %s'%(
452 key, entry, prop.classname)
453 l.append(entry)
454 propvalues[key] = value = l
456 # handle removals
457 rmvd = []
458 for id in oldvalue:
459 if id not in value:
460 rmvd.append(id)
461 # register the unlink with the old linked node
462 if self.do_journal and prop.do_journal:
463 self.db.addjournal(link_class, id, _UNLINK,
464 (self.classname, str(row.id), key))
466 # handle additions
467 adds = []
468 for id in value:
469 if id not in oldvalue:
470 if not self.db.getclass(link_class).hasnode(id):
471 raise IndexError, '%s has no node %s'%(
472 link_class, id)
473 adds.append(id)
474 # register the link with the newly linked node
475 if self.do_journal and prop.do_journal:
476 self.db.addjournal(link_class, id, _LINK,
477 (self.classname, str(row.id), key))
479 sv = getattr(row, key)
480 i = 0
481 while i < len(sv):
482 if str(sv[i].fid) in rmvd:
483 sv.delete(i)
484 else:
485 i += 1
486 for id in adds:
487 sv.append(fid=int(id))
488 changes[key] = oldvalue
489 if not rmvd and not adds:
490 del propvalues[key]
492 elif isinstance(prop, hyperdb.String):
493 if value is not None and type(value) != _STRINGTYPE:
494 raise TypeError, 'new property "%s" not a string'%key
495 if value is None:
496 value = ''
497 setattr(row, key, value)
498 changes[key] = oldvalue
499 if hasattr(prop, 'isfilename') and prop.isfilename:
500 propvalues[key] = os.path.basename(value)
501 if prop.indexme:
502 self.db.indexer.add_text((self.classname, nodeid, key),
503 value, 'text/plain')
505 elif isinstance(prop, hyperdb.Password):
506 if value is not None and not isinstance(value, password.Password):
507 raise TypeError, 'new property "%s" not a Password'% key
508 if value is None:
509 value = ''
510 setattr(row, key, str(value))
511 changes[key] = str(oldvalue)
512 propvalues[key] = str(value)
514 elif isinstance(prop, hyperdb.Date):
515 if value is not None and not isinstance(value, date.Date):
516 raise TypeError, 'new property "%s" not a Date'% key
517 if value is None:
518 setattr(row, key, 0)
519 else:
520 setattr(row, key, int(calendar.timegm(value.get_tuple())))
521 changes[key] = str(oldvalue)
522 propvalues[key] = str(value)
524 elif isinstance(prop, hyperdb.Interval):
525 if value is not None and not isinstance(value, date.Interval):
526 raise TypeError, 'new property "%s" not an Interval'% key
527 if value is None:
528 setattr(row, key, '')
529 else:
530 setattr(row, key, str(value))
531 changes[key] = str(oldvalue)
532 propvalues[key] = str(value)
534 elif isinstance(prop, hyperdb.Number):
535 if value is None:
536 value = 0
537 try:
538 v = int(value)
539 except ValueError:
540 raise TypeError, "%s (%s) is not numeric" % (key, repr(value))
541 setattr(row, key, v)
542 changes[key] = oldvalue
543 propvalues[key] = value
545 elif isinstance(prop, hyperdb.Boolean):
546 if value is None:
547 bv = 0
548 elif value not in (0,1):
549 raise TypeError, "%s (%s) is not boolean" % (key, repr(value))
550 else:
551 bv = value
552 setattr(row, key, bv)
553 changes[key] = oldvalue
554 propvalues[key] = value
556 oldnode[key] = oldvalue
558 # nothing to do?
559 if not propvalues:
560 return propvalues
561 if not propvalues.has_key('activity'):
562 row.activity = int(time.time())
563 if isnew:
564 if not row.creation:
565 row.creation = int(time.time())
566 if not row.creator:
567 row.creator = self.db.curuserid
569 self.db.dirty = 1
570 if self.do_journal:
571 if isnew:
572 self.db.addjournal(self.classname, nodeid, _CREATE, {})
573 self.fireReactors('create', nodeid, None)
574 else:
575 self.db.addjournal(self.classname, nodeid, _SET, changes)
576 self.fireReactors('set', nodeid, oldnode)
578 return propvalues
580 def retire(self, nodeid):
581 if self.db.journaltag is None:
582 raise hyperdb.DatabaseError, 'Database open read-only'
583 self.fireAuditors('retire', nodeid, None)
584 view = self.getview(1)
585 ndx = view.find(id=int(nodeid))
586 if ndx < 0:
587 raise KeyError, "nodeid %s not found" % nodeid
588 row = view[ndx]
589 oldvalues = self.uncommitted.setdefault(row.id, {})
590 oldval = oldvalues['_isdel'] = row._isdel
591 row._isdel = 1
592 if self.do_journal:
593 self.db.addjournal(self.classname, nodeid, _RETIRE, {})
594 if self.keyname:
595 iv = self.getindexview(1)
596 ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
597 if ndx > -1:
598 iv.delete(ndx)
599 self.db.dirty = 1
600 self.fireReactors('retire', nodeid, None)
602 def history(self, nodeid):
603 if not self.do_journal:
604 raise ValueError, 'Journalling is disabled for this class'
605 return self.db.getjournal(self.classname, nodeid)
607 def setkey(self, propname):
608 if self.keyname:
609 if propname == self.keyname:
610 return
611 raise ValueError, "%s already indexed on %s"%(self.classname,
612 self.keyname)
613 prop = self.properties.get(propname, None)
614 if prop is None:
615 prop = self.privateprops.get(propname, None)
616 if prop is None:
617 raise KeyError, "no property %s" % propname
618 if not isinstance(prop, hyperdb.String):
619 raise TypeError, "%s is not a String" % propname
621 # first setkey for this run
622 self.keyname = propname
623 iv = self.db._db.view('_%s' % self.classname)
624 if self.db.fastopen and iv.structure():
625 return
627 # very first setkey ever
628 self.db.dirty = 1
629 iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
630 iv = iv.ordered(1)
631 for row in self.getview():
632 iv.append(k=getattr(row, propname), i=row.id)
633 self.db.commit()
635 def getkey(self):
636 return self.keyname
638 def lookup(self, keyvalue):
639 if type(keyvalue) is not _STRINGTYPE:
640 raise TypeError, "%r is not a string" % keyvalue
641 iv = self.getindexview()
642 if iv:
643 ndx = iv.find(k=keyvalue)
644 if ndx > -1:
645 return str(iv[ndx].i)
646 else:
647 view = self.getview()
648 ndx = view.find({self.keyname:keyvalue, '_isdel':0})
649 if ndx > -1:
650 return str(view[ndx].id)
651 raise KeyError, keyvalue
653 def destroy(self, id):
654 view = self.getview(1)
655 ndx = view.find(id=int(id))
656 if ndx > -1:
657 if self.keyname:
658 keyvalue = getattr(view[ndx], self.keyname)
659 iv = self.getindexview(1)
660 if iv:
661 ivndx = iv.find(k=keyvalue)
662 if ivndx > -1:
663 iv.delete(ivndx)
664 view.delete(ndx)
665 self.db.destroyjournal(self.classname, id)
666 self.db.dirty = 1
668 def find(self, **propspec):
669 """Get the ids of nodes in this class which link to the given nodes.
671 'propspec' consists of keyword args propname={nodeid:1,}
672 'propname' must be the name of a property in this class, or a
673 KeyError is raised. That property must be a Link or
674 Multilink property, or a TypeError is raised.
676 Any node in this class whose propname property links to any of the
677 nodeids will be returned. Used by the full text indexing, which knows
678 that "foo" occurs in msg1, msg3 and file7; so we have hits on these
679 issues:
681 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
683 """
684 propspec = propspec.items()
685 for propname, nodeid in propspec:
686 # check the prop is OK
687 prop = self.ruprops[propname]
688 if (not isinstance(prop, hyperdb.Link) and
689 not isinstance(prop, hyperdb.Multilink)):
690 raise TypeError, "'%s' not a Link/Multilink property"%propname
692 vws = []
693 for propname, ids in propspec:
694 if type(ids) is _STRINGTYPE:
695 ids = {int(ids):1}
696 else:
697 d = {}
698 for id in ids.keys():
699 d[int(id)] = 1
700 ids = d
701 prop = self.ruprops[propname]
702 view = self.getview()
703 if isinstance(prop, hyperdb.Multilink):
704 def ff(row, nm=propname, ids=ids):
705 sv = getattr(row, nm)
706 for sr in sv:
707 if ids.has_key(sr.fid):
708 return 1
709 return 0
710 else:
711 def ff(row, nm=propname, ids=ids):
712 return ids.has_key(getattr(row, nm))
713 ndxview = view.filter(ff)
714 vws.append(ndxview.unique())
716 # handle the empty match case
717 if not vws:
718 return []
720 ndxview = vws[0]
721 for v in vws[1:]:
722 ndxview = ndxview.union(v)
723 view = self.getview().remapwith(ndxview)
724 rslt = []
725 for row in view:
726 rslt.append(str(row.id))
727 return rslt
730 def list(self):
731 l = []
732 for row in self.getview().select(_isdel=0):
733 l.append(str(row.id))
734 return l
736 def count(self):
737 return len(self.getview())
739 def getprops(self, protected=1):
740 # protected is not in ping's spec
741 allprops = self.ruprops.copy()
742 if protected and self.privateprops is not None:
743 allprops.update(self.privateprops)
744 return allprops
746 def addprop(self, **properties):
747 for key in properties.keys():
748 if self.ruprops.has_key(key):
749 raise ValueError, "%s is already a property of %s"%(key,
750 self.classname)
751 self.ruprops.update(properties)
752 self.db.fastopen = 0
753 view = self.__getview()
754 self.db.commit()
755 # ---- end of ping's spec
757 def filter(self, search_matches, filterspec, sort=(None,None),
758 group=(None,None)):
759 # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
760 # filterspec is a dict {propname:value}
761 # sort and group are (dir, prop) where dir is '+', '-' or None
762 # and prop is a prop name or None
763 where = {'_isdel':0}
764 mlcriteria = {}
765 regexes = {}
766 orcriteria = {}
767 for propname, value in filterspec.items():
768 prop = self.ruprops.get(propname, None)
769 if prop is None:
770 prop = self.privateprops[propname]
771 if isinstance(prop, hyperdb.Multilink):
772 if type(value) is not _LISTTYPE:
773 value = [value]
774 # transform keys to ids
775 u = []
776 for item in value:
777 try:
778 item = int(item)
779 except (TypeError, ValueError):
780 item = int(self.db.getclass(prop.classname).lookup(item))
781 if item == -1:
782 item = 0
783 u.append(item)
784 mlcriteria[propname] = u
785 elif isinstance(prop, hyperdb.Link):
786 if type(value) is not _LISTTYPE:
787 value = [value]
788 # transform keys to ids
789 u = []
790 for item in value:
791 try:
792 item = int(item)
793 except (TypeError, ValueError):
794 item = int(self.db.getclass(prop.classname).lookup(item))
795 if item == -1:
796 item = 0
797 u.append(item)
798 if len(u) == 1:
799 where[propname] = u[0]
800 else:
801 orcriteria[propname] = u
802 elif isinstance(prop, hyperdb.String):
803 # simple glob searching
804 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
805 v = v.replace('?', '.')
806 v = v.replace('*', '.*?')
807 regexes[propname] = re.compile(v, re.I)
808 elif propname == 'id':
809 where[propname] = int(value)
810 elif isinstance(prop, hyperdb.Boolean):
811 if type(value) is _STRINGTYPE:
812 bv = value.lower() in ('yes', 'true', 'on', '1')
813 else:
814 bv = value
815 where[propname] = bv
816 elif isinstance(prop, hyperdb.Number):
817 where[propname] = int(value)
818 else:
819 where[propname] = str(value)
820 v = self.getview()
821 #print "filter start at %s" % time.time()
822 if where:
823 v = v.select(where)
824 #print "filter where at %s" % time.time()
826 if mlcriteria:
827 # multilink - if any of the nodeids required by the
828 # filterspec aren't in this node's property, then skip it
829 def ff(row, ml=mlcriteria):
830 for propname, values in ml.items():
831 sv = getattr(row, propname)
832 for id in values:
833 if sv.find(fid=id) == -1:
834 return 0
835 return 1
836 iv = v.filter(ff)
837 v = v.remapwith(iv)
839 #print "filter mlcrit at %s" % time.time()
841 if orcriteria:
842 def ff(row, crit=orcriteria):
843 for propname, allowed in crit.items():
844 val = getattr(row, propname)
845 if val not in allowed:
846 return 0
847 return 1
849 iv = v.filter(ff)
850 v = v.remapwith(iv)
852 #print "filter orcrit at %s" % time.time()
853 if regexes:
854 def ff(row, r=regexes):
855 for propname, regex in r.items():
856 val = getattr(row, propname)
857 if not regex.search(val):
858 return 0
859 return 1
861 iv = v.filter(ff)
862 v = v.remapwith(iv)
863 #print "filter regexs at %s" % time.time()
865 if sort or group:
866 sortspec = []
867 rev = []
868 for dir, propname in group, sort:
869 if propname is None: continue
870 isreversed = 0
871 if dir == '-':
872 isreversed = 1
873 try:
874 prop = getattr(v, propname)
875 except AttributeError:
876 print "MK has no property %s" % propname
877 continue
878 propclass = self.ruprops.get(propname, None)
879 if propclass is None:
880 propclass = self.privateprops.get(propname, None)
881 if propclass is None:
882 print "Schema has no property %s" % propname
883 continue
884 if isinstance(propclass, hyperdb.Link):
885 linkclass = self.db.getclass(propclass.classname)
886 lv = linkclass.getview()
887 lv = lv.rename('id', propname)
888 v = v.join(lv, prop, 1)
889 if linkclass.getprops().has_key('order'):
890 propname = 'order'
891 else:
892 propname = linkclass.labelprop()
893 prop = getattr(v, propname)
894 if isreversed:
895 rev.append(prop)
896 sortspec.append(prop)
897 v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
898 #print "filter sort at %s" % time.time()
900 rslt = []
901 for row in v:
902 id = str(row.id)
903 if search_matches is not None:
904 if search_matches.has_key(id):
905 rslt.append(id)
906 else:
907 rslt.append(id)
908 return rslt
910 def hasnode(self, nodeid):
911 return int(nodeid) < self.maxid
913 def labelprop(self, default_to_id=0):
914 ''' Return the property name for a label for the given node.
916 This method attempts to generate a consistent label for the node.
917 It tries the following in order:
918 1. key property
919 2. "name" property
920 3. "title" property
921 4. first property from the sorted property name list
922 '''
923 k = self.getkey()
924 if k:
925 return k
926 props = self.getprops()
927 if props.has_key('name'):
928 return 'name'
929 elif props.has_key('title'):
930 return 'title'
931 if default_to_id:
932 return 'id'
933 props = props.keys()
934 props.sort()
935 return props[0]
937 def stringFind(self, **requirements):
938 """Locate a particular node by matching a set of its String
939 properties in a caseless search.
941 If the property is not a String property, a TypeError is raised.
943 The return is a list of the id of all nodes that match.
944 """
945 for propname in requirements.keys():
946 prop = self.properties[propname]
947 if isinstance(not prop, hyperdb.String):
948 raise TypeError, "'%s' not a String property"%propname
949 requirements[propname] = requirements[propname].lower()
950 requirements['_isdel'] = 0
952 l = []
953 for row in self.getview().select(requirements):
954 l.append(str(row.id))
955 return l
957 def addjournal(self, nodeid, action, params):
958 self.db.addjournal(self.classname, nodeid, action, params)
960 def index(self, nodeid):
961 ''' Add (or refresh) the node to search indexes '''
962 # find all the String properties that have indexme
963 for prop, propclass in self.getprops().items():
964 if isinstance(propclass, hyperdb.String) and propclass.indexme:
965 # index them under (classname, nodeid, property)
966 self.db.indexer.add_text((self.classname, nodeid, prop),
967 str(self.get(nodeid, prop)))
969 def export_list(self, propnames, nodeid):
970 ''' Export a node - generate a list of CSV-able data in the order
971 specified by propnames for the given node.
972 '''
973 properties = self.getprops()
974 l = []
975 for prop in propnames:
976 proptype = properties[prop]
977 value = self.get(nodeid, prop)
978 # "marshal" data where needed
979 if value is None:
980 pass
981 elif isinstance(proptype, hyperdb.Date):
982 value = value.get_tuple()
983 elif isinstance(proptype, hyperdb.Interval):
984 value = value.get_tuple()
985 elif isinstance(proptype, hyperdb.Password):
986 value = str(value)
987 l.append(repr(value))
988 return l
990 def import_list(self, propnames, proplist):
991 ''' Import a node - all information including "id" is present and
992 should not be sanity checked. Triggers are not triggered. The
993 journal should be initialised using the "creator" and "creation"
994 information.
996 Return the nodeid of the node imported.
997 '''
998 if self.db.journaltag is None:
999 raise hyperdb.DatabaseError, 'Database open read-only'
1000 properties = self.getprops()
1002 d = {}
1003 view = self.getview(1)
1004 for i in range(len(propnames)):
1005 value = eval(proplist[i])
1006 propname = propnames[i]
1007 prop = properties[propname]
1008 if propname == 'id':
1009 newid = value
1010 value = int(value)
1011 elif isinstance(prop, hyperdb.Date):
1012 value = int(calendar.timegm(value))
1013 elif isinstance(prop, hyperdb.Interval):
1014 value = str(date.Interval(value))
1015 d[propname] = value
1016 view.append(d)
1017 creator = d.get('creator', None)
1018 creation = d.get('creation', None)
1019 self.db.addjournal(self.classname, newid, 'create', {}, creator,
1020 creation)
1021 return newid
1023 # --- used by Database
1024 def _commit(self):
1025 """ called post commit of the DB.
1026 interested subclasses may override """
1027 self.uncommitted = {}
1028 self.rbactions = []
1029 self.idcache = {}
1030 def _rollback(self):
1031 """ called pre rollback of the DB.
1032 interested subclasses may override """
1033 for action in self.rbactions:
1034 action()
1035 self.rbactions = []
1036 self.uncommitted = {}
1037 self.idcache = {}
1038 def _clear(self):
1039 view = self.getview(1)
1040 if len(view):
1041 view[:] = []
1042 self.db.dirty = 1
1043 iv = self.getindexview(1)
1044 if iv:
1045 iv[:] = []
1046 def rollbackaction(self, action):
1047 """ call this to register a callback called on rollback
1048 callback is removed on end of transaction """
1049 self.rbactions.append(action)
1050 # --- internal
1051 def __getview(self):
1052 db = self.db._db
1053 view = db.view(self.classname)
1054 mkprops = view.structure()
1055 if mkprops and self.db.fastopen:
1056 return view.ordered(1)
1057 # is the definition the same?
1058 for nm, rutyp in self.ruprops.items():
1059 for mkprop in mkprops:
1060 if mkprop.name == nm:
1061 break
1062 else:
1063 mkprop = None
1064 if mkprop is None:
1065 break
1066 if _typmap[rutyp.__class__] != mkprop.type:
1067 break
1068 else:
1069 return view.ordered(1)
1070 # need to create or restructure the mk view
1071 # id comes first, so MK will order it for us
1072 self.db.dirty = 1
1073 s = ["%s[id:I" % self.classname]
1074 for nm, rutyp in self.ruprops.items():
1075 mktyp = _typmap[rutyp.__class__]
1076 s.append('%s:%s' % (nm, mktyp))
1077 if mktyp == 'V':
1078 s[-1] += ('[fid:I]')
1079 s.append('_isdel:I,activity:I,creation:I,creator:I]')
1080 v = self.db._db.getas(','.join(s))
1081 self.db.commit()
1082 return v.ordered(1)
1083 def getview(self, RW=0):
1084 return self.db._db.view(self.classname).ordered(1)
1085 def getindexview(self, RW=0):
1086 return self.db._db.view("_%s" % self.classname).ordered(1)
1088 def _fetchML(sv):
1089 l = []
1090 for row in sv:
1091 if row.fid:
1092 l.append(str(row.fid))
1093 return l
1095 def _fetchPW(s):
1096 p = password.Password()
1097 p.unpack(s)
1098 return p
1100 def _fetchLink(n):
1101 return n and str(n) or None
1103 def _fetchDate(n):
1104 return date.Date(time.gmtime(n))
1106 _converters = {
1107 hyperdb.Date : _fetchDate,
1108 hyperdb.Link : _fetchLink,
1109 hyperdb.Multilink : _fetchML,
1110 hyperdb.Interval : date.Interval,
1111 hyperdb.Password : _fetchPW,
1112 hyperdb.Boolean : lambda n: n,
1113 hyperdb.Number : lambda n: n,
1114 hyperdb.String : str,
1115 }
1117 class FileName(hyperdb.String):
1118 isfilename = 1
1120 _typmap = {
1121 FileName : 'S',
1122 hyperdb.String : 'S',
1123 hyperdb.Date : 'I',
1124 hyperdb.Link : 'I',
1125 hyperdb.Multilink : 'V',
1126 hyperdb.Interval : 'S',
1127 hyperdb.Password : 'S',
1128 hyperdb.Boolean : 'I',
1129 hyperdb.Number : 'I',
1130 }
1131 class FileClass(Class):
1132 ''' like Class but with a content property
1133 '''
1134 default_mime_type = 'text/plain'
1135 def __init__(self, db, classname, **properties):
1136 properties['content'] = FileName()
1137 if not properties.has_key('type'):
1138 properties['type'] = hyperdb.String()
1139 Class.__init__(self, db, classname, **properties)
1141 def get(self, nodeid, propname, default=_marker, cache=1):
1142 x = Class.get(self, nodeid, propname, default, cache)
1143 if propname == 'content':
1144 if x.startswith('file:'):
1145 fnm = x[5:]
1146 try:
1147 x = open(fnm, 'rb').read()
1148 except Exception, e:
1149 x = repr(e)
1150 return x
1152 def create(self, **propvalues):
1153 content = propvalues['content']
1154 del propvalues['content']
1155 newid = Class.create(self, **propvalues)
1156 if not content:
1157 return newid
1158 nm = bnm = '%s%s' % (self.classname, newid)
1159 sd = str(int(int(newid) / 1000))
1160 d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1161 if not os.path.exists(d):
1162 os.makedirs(d)
1163 nm = os.path.join(d, nm)
1164 open(nm, 'wb').write(content)
1165 self.set(newid, content = 'file:'+nm)
1166 mimetype = propvalues.get('type', self.default_mime_type)
1167 self.db.indexer.add_text((self.classname, newid, 'content'), content,
1168 mimetype)
1169 def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1170 action1(fnm)
1171 self.rollbackaction(undo)
1172 return newid
1174 def index(self, nodeid):
1175 Class.index(self, nodeid)
1176 mimetype = self.get(nodeid, 'type')
1177 if not mimetype:
1178 mimetype = self.default_mime_type
1179 self.db.indexer.add_text((self.classname, nodeid, 'content'),
1180 self.get(nodeid, 'content'), mimetype)
1182 class IssueClass(Class, roundupdb.IssueClass):
1183 ''' The newly-created class automatically includes the "messages",
1184 "files", "nosy", and "superseder" properties. If the 'properties'
1185 dictionary attempts to specify any of these properties or a
1186 "creation" or "activity" property, a ValueError is raised.
1187 '''
1188 def __init__(self, db, classname, **properties):
1189 if not properties.has_key('title'):
1190 properties['title'] = hyperdb.String(indexme='yes')
1191 if not properties.has_key('messages'):
1192 properties['messages'] = hyperdb.Multilink("msg")
1193 if not properties.has_key('files'):
1194 properties['files'] = hyperdb.Multilink("file")
1195 if not properties.has_key('nosy'):
1196 # note: journalling is turned off as it really just wastes
1197 # space. this behaviour may be overridden in an instance
1198 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1199 if not properties.has_key('superseder'):
1200 properties['superseder'] = hyperdb.Multilink(classname)
1201 Class.__init__(self, db, classname, **properties)
1203 CURVERSION = 2
1205 class Indexer(indexer.Indexer):
1206 disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1207 def __init__(self, path, datadb):
1208 self.path = os.path.join(path, 'index.mk4')
1209 self.db = metakit.storage(self.path, 1)
1210 self.datadb = datadb
1211 self.reindex = 0
1212 v = self.db.view('version')
1213 if not v.structure():
1214 v = self.db.getas('version[vers:I]')
1215 self.db.commit()
1216 v.append(vers=CURVERSION)
1217 self.reindex = 1
1218 elif v[0].vers != CURVERSION:
1219 v[0].vers = CURVERSION
1220 self.reindex = 1
1221 if self.reindex:
1222 self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
1223 self.db.getas('index[word:S,hits[pos:I]]')
1224 self.db.commit()
1225 self.reindex = 1
1226 self.changed = 0
1227 self.propcache = {}
1229 def force_reindex(self):
1230 v = self.db.view('ids')
1231 v[:] = []
1232 v = self.db.view('index')
1233 v[:] = []
1234 self.db.commit()
1235 self.reindex = 1
1237 def should_reindex(self):
1238 return self.reindex
1240 def _getprops(self, classname):
1241 props = self.propcache.get(classname, None)
1242 if props is None:
1243 props = self.datadb.view(classname).structure()
1244 props = [prop.name for prop in props]
1245 self.propcache[classname] = props
1246 return props
1248 def _getpropid(self, classname, propname):
1249 return self._getprops(classname).index(propname)
1251 def _getpropname(self, classname, propid):
1252 return self._getprops(classname)[propid]
1254 def add_text(self, identifier, text, mime_type='text/plain'):
1255 if mime_type != 'text/plain':
1256 return
1257 classname, nodeid, property = identifier
1258 tbls = self.datadb.view('tables')
1259 tblid = tbls.find(name=classname)
1260 if tblid < 0:
1261 raise KeyError, "unknown class %r"%classname
1262 nodeid = int(nodeid)
1263 propid = self._getpropid(classname, property)
1264 ids = self.db.view('ids')
1265 oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
1266 if oldpos > -1:
1267 ids[oldpos].ignore = 1
1268 self.changed = 1
1269 pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
1271 wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
1272 words = {}
1273 for word in wordlist:
1274 if not self.disallows.has_key(word):
1275 words[word] = 1
1276 words = words.keys()
1278 index = self.db.view('index').ordered(1)
1279 for word in words:
1280 ndx = index.find(word=word)
1281 if ndx < 0:
1282 index.append(word=word)
1283 ndx = index.find(word=word)
1284 index[ndx].hits.append(pos=pos)
1285 self.changed = 1
1287 def find(self, wordlist):
1288 hits = None
1289 index = self.db.view('index').ordered(1)
1290 for word in wordlist:
1291 word = word.upper()
1292 if not 2 < len(word) < 26:
1293 continue
1294 ndx = index.find(word=word)
1295 if ndx < 0:
1296 return {}
1297 if hits is None:
1298 hits = index[ndx].hits
1299 else:
1300 hits = hits.intersect(index[ndx].hits)
1301 if len(hits) == 0:
1302 return {}
1303 if hits is None:
1304 return {}
1305 rslt = {}
1306 ids = self.db.view('ids').remapwith(hits)
1307 tbls = self.datadb.view('tables')
1308 for i in range(len(ids)):
1309 hit = ids[i]
1310 if not hit.ignore:
1311 classname = tbls[hit.tblid].name
1312 nodeid = str(hit.nodeid)
1313 property = self._getpropname(classname, hit.propid)
1314 rslt[i] = (classname, nodeid, property)
1315 return rslt
1317 def save_index(self):
1318 if self.changed:
1319 self.db.commit()
1320 self.changed = 0
1322 def rollback(self):
1323 if self.changed:
1324 self.db.rollback()
1325 self.db = metakit.storage(self.path, 1)
1326 self.changed = 0