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 # perform the modifications on the actual property value
480 sv = getattr(row, key)
481 i = 0
482 while i < len(sv):
483 if str(sv[i].fid) in rmvd:
484 sv.delete(i)
485 else:
486 i += 1
487 for id in adds:
488 sv.append(fid=int(id))
490 # figure the journal entry
491 l = []
492 if adds:
493 l.append(('+', adds))
494 if rmvd:
495 l.append(('-', rmvd))
496 if l:
497 changes[key] = tuple(l)
498 #changes[key] = oldvalue
500 if not rmvd and not adds:
501 del propvalues[key]
503 elif isinstance(prop, hyperdb.String):
504 if value is not None and type(value) != _STRINGTYPE:
505 raise TypeError, 'new property "%s" not a string'%key
506 if value is None:
507 value = ''
508 setattr(row, key, value)
509 changes[key] = oldvalue
510 if hasattr(prop, 'isfilename') and prop.isfilename:
511 propvalues[key] = os.path.basename(value)
512 if prop.indexme:
513 self.db.indexer.add_text((self.classname, nodeid, key),
514 value, 'text/plain')
516 elif isinstance(prop, hyperdb.Password):
517 if value is not None and not isinstance(value, password.Password):
518 raise TypeError, 'new property "%s" not a Password'% key
519 if value is None:
520 value = ''
521 setattr(row, key, str(value))
522 changes[key] = str(oldvalue)
523 propvalues[key] = str(value)
525 elif isinstance(prop, hyperdb.Date):
526 if value is not None and not isinstance(value, date.Date):
527 raise TypeError, 'new property "%s" not a Date'% key
528 if value is None:
529 setattr(row, key, 0)
530 else:
531 setattr(row, key, int(calendar.timegm(value.get_tuple())))
532 changes[key] = str(oldvalue)
533 propvalues[key] = str(value)
535 elif isinstance(prop, hyperdb.Interval):
536 if value is not None and not isinstance(value, date.Interval):
537 raise TypeError, 'new property "%s" not an Interval'% key
538 if value is None:
539 setattr(row, key, '')
540 else:
541 setattr(row, key, str(value))
542 changes[key] = str(oldvalue)
543 propvalues[key] = str(value)
545 elif isinstance(prop, hyperdb.Number):
546 if value is None:
547 value = 0
548 try:
549 v = int(value)
550 except ValueError:
551 raise TypeError, "%s (%s) is not numeric" % (key, repr(value))
552 setattr(row, key, v)
553 changes[key] = oldvalue
554 propvalues[key] = value
556 elif isinstance(prop, hyperdb.Boolean):
557 if value is None:
558 bv = 0
559 elif value not in (0,1):
560 raise TypeError, "%s (%s) is not boolean" % (key, repr(value))
561 else:
562 bv = value
563 setattr(row, key, bv)
564 changes[key] = oldvalue
565 propvalues[key] = value
567 oldnode[key] = oldvalue
569 # nothing to do?
570 if not propvalues:
571 return propvalues
572 if not propvalues.has_key('activity'):
573 row.activity = int(time.time())
574 if isnew:
575 if not row.creation:
576 row.creation = int(time.time())
577 if not row.creator:
578 row.creator = self.db.curuserid
580 self.db.dirty = 1
581 if self.do_journal:
582 if isnew:
583 self.db.addjournal(self.classname, nodeid, _CREATE, {})
584 self.fireReactors('create', nodeid, None)
585 else:
586 self.db.addjournal(self.classname, nodeid, _SET, changes)
587 self.fireReactors('set', nodeid, oldnode)
589 return propvalues
591 def retire(self, nodeid):
592 if self.db.journaltag is None:
593 raise hyperdb.DatabaseError, 'Database open read-only'
594 self.fireAuditors('retire', nodeid, None)
595 view = self.getview(1)
596 ndx = view.find(id=int(nodeid))
597 if ndx < 0:
598 raise KeyError, "nodeid %s not found" % nodeid
599 row = view[ndx]
600 oldvalues = self.uncommitted.setdefault(row.id, {})
601 oldval = oldvalues['_isdel'] = row._isdel
602 row._isdel = 1
603 if self.do_journal:
604 self.db.addjournal(self.classname, nodeid, _RETIRE, {})
605 if self.keyname:
606 iv = self.getindexview(1)
607 ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
608 if ndx > -1:
609 iv.delete(ndx)
610 self.db.dirty = 1
611 self.fireReactors('retire', nodeid, None)
613 def history(self, nodeid):
614 if not self.do_journal:
615 raise ValueError, 'Journalling is disabled for this class'
616 return self.db.getjournal(self.classname, nodeid)
618 def setkey(self, propname):
619 if self.keyname:
620 if propname == self.keyname:
621 return
622 raise ValueError, "%s already indexed on %s"%(self.classname,
623 self.keyname)
624 prop = self.properties.get(propname, None)
625 if prop is None:
626 prop = self.privateprops.get(propname, None)
627 if prop is None:
628 raise KeyError, "no property %s" % propname
629 if not isinstance(prop, hyperdb.String):
630 raise TypeError, "%s is not a String" % propname
632 # first setkey for this run
633 self.keyname = propname
634 iv = self.db._db.view('_%s' % self.classname)
635 if self.db.fastopen and iv.structure():
636 return
638 # very first setkey ever
639 self.db.dirty = 1
640 iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
641 iv = iv.ordered(1)
642 for row in self.getview():
643 iv.append(k=getattr(row, propname), i=row.id)
644 self.db.commit()
646 def getkey(self):
647 return self.keyname
649 def lookup(self, keyvalue):
650 if type(keyvalue) is not _STRINGTYPE:
651 raise TypeError, "%r is not a string" % keyvalue
652 iv = self.getindexview()
653 if iv:
654 ndx = iv.find(k=keyvalue)
655 if ndx > -1:
656 return str(iv[ndx].i)
657 else:
658 view = self.getview()
659 ndx = view.find({self.keyname:keyvalue, '_isdel':0})
660 if ndx > -1:
661 return str(view[ndx].id)
662 raise KeyError, keyvalue
664 def destroy(self, id):
665 view = self.getview(1)
666 ndx = view.find(id=int(id))
667 if ndx > -1:
668 if self.keyname:
669 keyvalue = getattr(view[ndx], self.keyname)
670 iv = self.getindexview(1)
671 if iv:
672 ivndx = iv.find(k=keyvalue)
673 if ivndx > -1:
674 iv.delete(ivndx)
675 view.delete(ndx)
676 self.db.destroyjournal(self.classname, id)
677 self.db.dirty = 1
679 def find(self, **propspec):
680 """Get the ids of nodes in this class which link to the given nodes.
682 'propspec' consists of keyword args propname={nodeid:1,}
683 'propname' must be the name of a property in this class, or a
684 KeyError is raised. That property must be a Link or
685 Multilink property, or a TypeError is raised.
687 Any node in this class whose propname property links to any of the
688 nodeids will be returned. Used by the full text indexing, which knows
689 that "foo" occurs in msg1, msg3 and file7; so we have hits on these
690 issues:
692 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
694 """
695 propspec = propspec.items()
696 for propname, nodeid in propspec:
697 # check the prop is OK
698 prop = self.ruprops[propname]
699 if (not isinstance(prop, hyperdb.Link) and
700 not isinstance(prop, hyperdb.Multilink)):
701 raise TypeError, "'%s' not a Link/Multilink property"%propname
703 vws = []
704 for propname, ids in propspec:
705 if type(ids) is _STRINGTYPE:
706 ids = {int(ids):1}
707 else:
708 d = {}
709 for id in ids.keys():
710 d[int(id)] = 1
711 ids = d
712 prop = self.ruprops[propname]
713 view = self.getview()
714 if isinstance(prop, hyperdb.Multilink):
715 def ff(row, nm=propname, ids=ids):
716 sv = getattr(row, nm)
717 for sr in sv:
718 if ids.has_key(sr.fid):
719 return 1
720 return 0
721 else:
722 def ff(row, nm=propname, ids=ids):
723 return ids.has_key(getattr(row, nm))
724 ndxview = view.filter(ff)
725 vws.append(ndxview.unique())
727 # handle the empty match case
728 if not vws:
729 return []
731 ndxview = vws[0]
732 for v in vws[1:]:
733 ndxview = ndxview.union(v)
734 view = self.getview().remapwith(ndxview)
735 rslt = []
736 for row in view:
737 rslt.append(str(row.id))
738 return rslt
741 def list(self):
742 l = []
743 for row in self.getview().select(_isdel=0):
744 l.append(str(row.id))
745 return l
747 def count(self):
748 return len(self.getview())
750 def getprops(self, protected=1):
751 # protected is not in ping's spec
752 allprops = self.ruprops.copy()
753 if protected and self.privateprops is not None:
754 allprops.update(self.privateprops)
755 return allprops
757 def addprop(self, **properties):
758 for key in properties.keys():
759 if self.ruprops.has_key(key):
760 raise ValueError, "%s is already a property of %s"%(key,
761 self.classname)
762 self.ruprops.update(properties)
763 self.db.fastopen = 0
764 view = self.__getview()
765 self.db.commit()
766 # ---- end of ping's spec
768 def filter(self, search_matches, filterspec, sort=(None,None),
769 group=(None,None)):
770 # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
771 # filterspec is a dict {propname:value}
772 # sort and group are (dir, prop) where dir is '+', '-' or None
773 # and prop is a prop name or None
774 where = {'_isdel':0}
775 mlcriteria = {}
776 regexes = {}
777 orcriteria = {}
778 for propname, value in filterspec.items():
779 prop = self.ruprops.get(propname, None)
780 if prop is None:
781 prop = self.privateprops[propname]
782 if isinstance(prop, hyperdb.Multilink):
783 if type(value) is not _LISTTYPE:
784 value = [value]
785 # transform keys to ids
786 u = []
787 for item in value:
788 try:
789 item = int(item)
790 except (TypeError, ValueError):
791 item = int(self.db.getclass(prop.classname).lookup(item))
792 if item == -1:
793 item = 0
794 u.append(item)
795 mlcriteria[propname] = u
796 elif isinstance(prop, hyperdb.Link):
797 if type(value) is not _LISTTYPE:
798 value = [value]
799 # transform keys to ids
800 u = []
801 for item in value:
802 try:
803 item = int(item)
804 except (TypeError, ValueError):
805 item = int(self.db.getclass(prop.classname).lookup(item))
806 if item == -1:
807 item = 0
808 u.append(item)
809 if len(u) == 1:
810 where[propname] = u[0]
811 else:
812 orcriteria[propname] = u
813 elif isinstance(prop, hyperdb.String):
814 # simple glob searching
815 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
816 v = v.replace('?', '.')
817 v = v.replace('*', '.*?')
818 regexes[propname] = re.compile(v, re.I)
819 elif propname == 'id':
820 where[propname] = int(value)
821 elif isinstance(prop, hyperdb.Boolean):
822 if type(value) is _STRINGTYPE:
823 bv = value.lower() in ('yes', 'true', 'on', '1')
824 else:
825 bv = value
826 where[propname] = bv
827 elif isinstance(prop, hyperdb.Date):
828 t = date.Date(value).get_tuple()
829 where[propname] = int(calendar.timegm(t))
830 elif isinstance(prop, hyperdb.Interval):
831 where[propname] = str(date.Interval(value))
832 elif isinstance(prop, hyperdb.Number):
833 where[propname] = int(value)
834 else:
835 where[propname] = str(value)
836 v = self.getview()
837 #print "filter start at %s" % time.time()
838 if where:
839 v = v.select(where)
840 #print "filter where at %s" % time.time()
842 if mlcriteria:
843 # multilink - if any of the nodeids required by the
844 # filterspec aren't in this node's property, then skip it
845 def ff(row, ml=mlcriteria):
846 for propname, values in ml.items():
847 sv = getattr(row, propname)
848 for id in values:
849 if sv.find(fid=id) == -1:
850 return 0
851 return 1
852 iv = v.filter(ff)
853 v = v.remapwith(iv)
855 #print "filter mlcrit at %s" % time.time()
857 if orcriteria:
858 def ff(row, crit=orcriteria):
859 for propname, allowed in crit.items():
860 val = getattr(row, propname)
861 if val not in allowed:
862 return 0
863 return 1
865 iv = v.filter(ff)
866 v = v.remapwith(iv)
868 #print "filter orcrit at %s" % time.time()
869 if regexes:
870 def ff(row, r=regexes):
871 for propname, regex in r.items():
872 val = getattr(row, propname)
873 if not regex.search(val):
874 return 0
875 return 1
877 iv = v.filter(ff)
878 v = v.remapwith(iv)
879 #print "filter regexs at %s" % time.time()
881 if sort or group:
882 sortspec = []
883 rev = []
884 for dir, propname in group, sort:
885 if propname is None: continue
886 isreversed = 0
887 if dir == '-':
888 isreversed = 1
889 try:
890 prop = getattr(v, propname)
891 except AttributeError:
892 print "MK has no property %s" % propname
893 continue
894 propclass = self.ruprops.get(propname, None)
895 if propclass is None:
896 propclass = self.privateprops.get(propname, None)
897 if propclass is None:
898 print "Schema has no property %s" % propname
899 continue
900 if isinstance(propclass, hyperdb.Link):
901 linkclass = self.db.getclass(propclass.classname)
902 lv = linkclass.getview()
903 lv = lv.rename('id', propname)
904 v = v.join(lv, prop, 1)
905 if linkclass.getprops().has_key('order'):
906 propname = 'order'
907 else:
908 propname = linkclass.labelprop()
909 prop = getattr(v, propname)
910 if isreversed:
911 rev.append(prop)
912 sortspec.append(prop)
913 v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
914 #print "filter sort at %s" % time.time()
916 rslt = []
917 for row in v:
918 id = str(row.id)
919 if search_matches is not None:
920 if search_matches.has_key(id):
921 rslt.append(id)
922 else:
923 rslt.append(id)
924 return rslt
926 def hasnode(self, nodeid):
927 return int(nodeid) < self.maxid
929 def labelprop(self, default_to_id=0):
930 ''' Return the property name for a label for the given node.
932 This method attempts to generate a consistent label for the node.
933 It tries the following in order:
934 1. key property
935 2. "name" property
936 3. "title" property
937 4. first property from the sorted property name list
938 '''
939 k = self.getkey()
940 if k:
941 return k
942 props = self.getprops()
943 if props.has_key('name'):
944 return 'name'
945 elif props.has_key('title'):
946 return 'title'
947 if default_to_id:
948 return 'id'
949 props = props.keys()
950 props.sort()
951 return props[0]
953 def stringFind(self, **requirements):
954 """Locate a particular node by matching a set of its String
955 properties in a caseless search.
957 If the property is not a String property, a TypeError is raised.
959 The return is a list of the id of all nodes that match.
960 """
961 for propname in requirements.keys():
962 prop = self.properties[propname]
963 if isinstance(not prop, hyperdb.String):
964 raise TypeError, "'%s' not a String property"%propname
965 requirements[propname] = requirements[propname].lower()
966 requirements['_isdel'] = 0
968 l = []
969 for row in self.getview().select(requirements):
970 l.append(str(row.id))
971 return l
973 def addjournal(self, nodeid, action, params):
974 self.db.addjournal(self.classname, nodeid, action, params)
976 def index(self, nodeid):
977 ''' Add (or refresh) the node to search indexes '''
978 # find all the String properties that have indexme
979 for prop, propclass in self.getprops().items():
980 if isinstance(propclass, hyperdb.String) and propclass.indexme:
981 # index them under (classname, nodeid, property)
982 self.db.indexer.add_text((self.classname, nodeid, prop),
983 str(self.get(nodeid, prop)))
985 def export_list(self, propnames, nodeid):
986 ''' Export a node - generate a list of CSV-able data in the order
987 specified by propnames for the given node.
988 '''
989 properties = self.getprops()
990 l = []
991 for prop in propnames:
992 proptype = properties[prop]
993 value = self.get(nodeid, prop)
994 # "marshal" data where needed
995 if value is None:
996 pass
997 elif isinstance(proptype, hyperdb.Date):
998 value = value.get_tuple()
999 elif isinstance(proptype, hyperdb.Interval):
1000 value = value.get_tuple()
1001 elif isinstance(proptype, hyperdb.Password):
1002 value = str(value)
1003 l.append(repr(value))
1004 return l
1006 def import_list(self, propnames, proplist):
1007 ''' Import a node - all information including "id" is present and
1008 should not be sanity checked. Triggers are not triggered. The
1009 journal should be initialised using the "creator" and "creation"
1010 information.
1012 Return the nodeid of the node imported.
1013 '''
1014 if self.db.journaltag is None:
1015 raise hyperdb.DatabaseError, 'Database open read-only'
1016 properties = self.getprops()
1018 d = {}
1019 view = self.getview(1)
1020 for i in range(len(propnames)):
1021 value = eval(proplist[i])
1022 propname = propnames[i]
1023 prop = properties[propname]
1024 if propname == 'id':
1025 newid = value
1026 value = int(value)
1027 elif isinstance(prop, hyperdb.Date):
1028 value = int(calendar.timegm(value))
1029 elif isinstance(prop, hyperdb.Interval):
1030 value = str(date.Interval(value))
1031 d[propname] = value
1032 view.append(d)
1033 creator = d.get('creator', None)
1034 creation = d.get('creation', None)
1035 self.db.addjournal(self.classname, newid, 'create', {}, creator,
1036 creation)
1037 return newid
1039 # --- used by Database
1040 def _commit(self):
1041 """ called post commit of the DB.
1042 interested subclasses may override """
1043 self.uncommitted = {}
1044 self.rbactions = []
1045 self.idcache = {}
1046 def _rollback(self):
1047 """ called pre rollback of the DB.
1048 interested subclasses may override """
1049 for action in self.rbactions:
1050 action()
1051 self.rbactions = []
1052 self.uncommitted = {}
1053 self.idcache = {}
1054 def _clear(self):
1055 view = self.getview(1)
1056 if len(view):
1057 view[:] = []
1058 self.db.dirty = 1
1059 iv = self.getindexview(1)
1060 if iv:
1061 iv[:] = []
1062 def rollbackaction(self, action):
1063 """ call this to register a callback called on rollback
1064 callback is removed on end of transaction """
1065 self.rbactions.append(action)
1066 # --- internal
1067 def __getview(self):
1068 db = self.db._db
1069 view = db.view(self.classname)
1070 mkprops = view.structure()
1071 if mkprops and self.db.fastopen:
1072 return view.ordered(1)
1073 # is the definition the same?
1074 for nm, rutyp in self.ruprops.items():
1075 for mkprop in mkprops:
1076 if mkprop.name == nm:
1077 break
1078 else:
1079 mkprop = None
1080 if mkprop is None:
1081 break
1082 if _typmap[rutyp.__class__] != mkprop.type:
1083 break
1084 else:
1085 return view.ordered(1)
1086 # need to create or restructure the mk view
1087 # id comes first, so MK will order it for us
1088 self.db.dirty = 1
1089 s = ["%s[id:I" % self.classname]
1090 for nm, rutyp in self.ruprops.items():
1091 mktyp = _typmap[rutyp.__class__]
1092 s.append('%s:%s' % (nm, mktyp))
1093 if mktyp == 'V':
1094 s[-1] += ('[fid:I]')
1095 s.append('_isdel:I,activity:I,creation:I,creator:I]')
1096 v = self.db._db.getas(','.join(s))
1097 self.db.commit()
1098 return v.ordered(1)
1099 def getview(self, RW=0):
1100 return self.db._db.view(self.classname).ordered(1)
1101 def getindexview(self, RW=0):
1102 return self.db._db.view("_%s" % self.classname).ordered(1)
1104 def _fetchML(sv):
1105 l = []
1106 for row in sv:
1107 if row.fid:
1108 l.append(str(row.fid))
1109 return l
1111 def _fetchPW(s):
1112 p = password.Password()
1113 p.unpack(s)
1114 return p
1116 def _fetchLink(n):
1117 return n and str(n) or None
1119 def _fetchDate(n):
1120 return date.Date(time.gmtime(n))
1122 _converters = {
1123 hyperdb.Date : _fetchDate,
1124 hyperdb.Link : _fetchLink,
1125 hyperdb.Multilink : _fetchML,
1126 hyperdb.Interval : date.Interval,
1127 hyperdb.Password : _fetchPW,
1128 hyperdb.Boolean : lambda n: n,
1129 hyperdb.Number : lambda n: n,
1130 hyperdb.String : str,
1131 }
1133 class FileName(hyperdb.String):
1134 isfilename = 1
1136 _typmap = {
1137 FileName : 'S',
1138 hyperdb.String : 'S',
1139 hyperdb.Date : 'I',
1140 hyperdb.Link : 'I',
1141 hyperdb.Multilink : 'V',
1142 hyperdb.Interval : 'S',
1143 hyperdb.Password : 'S',
1144 hyperdb.Boolean : 'I',
1145 hyperdb.Number : 'I',
1146 }
1147 class FileClass(Class):
1148 ''' like Class but with a content property
1149 '''
1150 default_mime_type = 'text/plain'
1151 def __init__(self, db, classname, **properties):
1152 properties['content'] = FileName()
1153 if not properties.has_key('type'):
1154 properties['type'] = hyperdb.String()
1155 Class.__init__(self, db, classname, **properties)
1157 def get(self, nodeid, propname, default=_marker, cache=1):
1158 x = Class.get(self, nodeid, propname, default, cache)
1159 if propname == 'content':
1160 if x.startswith('file:'):
1161 fnm = x[5:]
1162 try:
1163 x = open(fnm, 'rb').read()
1164 except Exception, e:
1165 x = repr(e)
1166 return x
1168 def create(self, **propvalues):
1169 content = propvalues['content']
1170 del propvalues['content']
1171 newid = Class.create(self, **propvalues)
1172 if not content:
1173 return newid
1174 nm = bnm = '%s%s' % (self.classname, newid)
1175 sd = str(int(int(newid) / 1000))
1176 d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1177 if not os.path.exists(d):
1178 os.makedirs(d)
1179 nm = os.path.join(d, nm)
1180 open(nm, 'wb').write(content)
1181 self.set(newid, content = 'file:'+nm)
1182 mimetype = propvalues.get('type', self.default_mime_type)
1183 self.db.indexer.add_text((self.classname, newid, 'content'), content,
1184 mimetype)
1185 def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1186 action1(fnm)
1187 self.rollbackaction(undo)
1188 return newid
1190 def index(self, nodeid):
1191 Class.index(self, nodeid)
1192 mimetype = self.get(nodeid, 'type')
1193 if not mimetype:
1194 mimetype = self.default_mime_type
1195 self.db.indexer.add_text((self.classname, nodeid, 'content'),
1196 self.get(nodeid, 'content'), mimetype)
1198 class IssueClass(Class, roundupdb.IssueClass):
1199 ''' The newly-created class automatically includes the "messages",
1200 "files", "nosy", and "superseder" properties. If the 'properties'
1201 dictionary attempts to specify any of these properties or a
1202 "creation" or "activity" property, a ValueError is raised.
1203 '''
1204 def __init__(self, db, classname, **properties):
1205 if not properties.has_key('title'):
1206 properties['title'] = hyperdb.String(indexme='yes')
1207 if not properties.has_key('messages'):
1208 properties['messages'] = hyperdb.Multilink("msg")
1209 if not properties.has_key('files'):
1210 properties['files'] = hyperdb.Multilink("file")
1211 if not properties.has_key('nosy'):
1212 # note: journalling is turned off as it really just wastes
1213 # space. this behaviour may be overridden in an instance
1214 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1215 if not properties.has_key('superseder'):
1216 properties['superseder'] = hyperdb.Multilink(classname)
1217 Class.__init__(self, db, classname, **properties)
1219 CURVERSION = 2
1221 class Indexer(indexer.Indexer):
1222 disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1223 def __init__(self, path, datadb):
1224 self.path = os.path.join(path, 'index.mk4')
1225 self.db = metakit.storage(self.path, 1)
1226 self.datadb = datadb
1227 self.reindex = 0
1228 v = self.db.view('version')
1229 if not v.structure():
1230 v = self.db.getas('version[vers:I]')
1231 self.db.commit()
1232 v.append(vers=CURVERSION)
1233 self.reindex = 1
1234 elif v[0].vers != CURVERSION:
1235 v[0].vers = CURVERSION
1236 self.reindex = 1
1237 if self.reindex:
1238 self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
1239 self.db.getas('index[word:S,hits[pos:I]]')
1240 self.db.commit()
1241 self.reindex = 1
1242 self.changed = 0
1243 self.propcache = {}
1245 def force_reindex(self):
1246 v = self.db.view('ids')
1247 v[:] = []
1248 v = self.db.view('index')
1249 v[:] = []
1250 self.db.commit()
1251 self.reindex = 1
1253 def should_reindex(self):
1254 return self.reindex
1256 def _getprops(self, classname):
1257 props = self.propcache.get(classname, None)
1258 if props is None:
1259 props = self.datadb.view(classname).structure()
1260 props = [prop.name for prop in props]
1261 self.propcache[classname] = props
1262 return props
1264 def _getpropid(self, classname, propname):
1265 return self._getprops(classname).index(propname)
1267 def _getpropname(self, classname, propid):
1268 return self._getprops(classname)[propid]
1270 def add_text(self, identifier, text, mime_type='text/plain'):
1271 if mime_type != 'text/plain':
1272 return
1273 classname, nodeid, property = identifier
1274 tbls = self.datadb.view('tables')
1275 tblid = tbls.find(name=classname)
1276 if tblid < 0:
1277 raise KeyError, "unknown class %r"%classname
1278 nodeid = int(nodeid)
1279 propid = self._getpropid(classname, property)
1280 ids = self.db.view('ids')
1281 oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
1282 if oldpos > -1:
1283 ids[oldpos].ignore = 1
1284 self.changed = 1
1285 pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
1287 wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
1288 words = {}
1289 for word in wordlist:
1290 if not self.disallows.has_key(word):
1291 words[word] = 1
1292 words = words.keys()
1294 index = self.db.view('index').ordered(1)
1295 for word in words:
1296 ndx = index.find(word=word)
1297 if ndx < 0:
1298 index.append(word=word)
1299 ndx = index.find(word=word)
1300 index[ndx].hits.append(pos=pos)
1301 self.changed = 1
1303 def find(self, wordlist):
1304 hits = None
1305 index = self.db.view('index').ordered(1)
1306 for word in wordlist:
1307 word = word.upper()
1308 if not 2 < len(word) < 26:
1309 continue
1310 ndx = index.find(word=word)
1311 if ndx < 0:
1312 return {}
1313 if hits is None:
1314 hits = index[ndx].hits
1315 else:
1316 hits = hits.intersect(index[ndx].hits)
1317 if len(hits) == 0:
1318 return {}
1319 if hits is None:
1320 return {}
1321 rslt = {}
1322 ids = self.db.view('ids').remapwith(hits)
1323 tbls = self.datadb.view('tables')
1324 for i in range(len(ids)):
1325 hit = ids[i]
1326 if not hit.ignore:
1327 classname = tbls[hit.tblid].name
1328 nodeid = str(hit.nodeid)
1329 property = self._getpropname(classname, hit.propid)
1330 rslt[i] = (classname, nodeid, property)
1331 return rslt
1333 def save_index(self):
1334 if self.changed:
1335 self.db.commit()
1336 self.changed = 0
1338 def rollback(self):
1339 if self.changed:
1340 self.db.rollback()
1341 self.db = metakit.storage(self.path, 1)
1342 self.changed = 0