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()
48 # --- defined in ping's spec
49 def __getattr__(self, classname):
50 if classname == 'curuserid':
51 if self.journaltag is None:
52 return None
54 try:
55 self.curuserid = x = int(self.classes['user'].lookup(self.journaltag))
56 except KeyError:
57 if self.journaltag == 'admin':
58 self.curuserid = x = 1
59 else:
60 x = 0
61 return x
62 elif classname == 'transactions':
63 return self.dirty
64 return self.getclass(classname)
65 def getclass(self, classname):
66 try:
67 return self.classes[classname]
68 except KeyError:
69 raise KeyError, 'There is no class called "%s"'%classname
70 def getclasses(self):
71 return self.classes.keys()
72 # --- end of ping's spec
73 # --- exposed methods
74 def commit(self):
75 if self.dirty:
76 self._db.commit()
77 for cl in self.classes.values():
78 cl._commit()
79 self.indexer.save_index()
80 self.dirty = 0
81 def rollback(self):
82 if self.dirty:
83 for cl in self.classes.values():
84 cl._rollback()
85 self._db.rollback()
86 self._db = None
87 self._db = metakit.storage(self.dbnm, 1)
88 self.hist = self._db.view('history')
89 self.tables = self._db.view('tables')
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') }
249 self.auditors = {'create': [], 'set': [], 'retire': []} # event -> list of callables
250 self.reactors = {'create': [], 'set': [], 'retire': []} # ditto
251 view = self.__getview()
252 self.maxid = 1
253 if view:
254 self.maxid = view[-1].id + 1
255 self.uncommitted = {}
256 self.rbactions = []
257 # people reach inside!!
258 self.properties = self.ruprops
259 self.db.addclass(self)
260 self.idcache = {}
262 # default is to journal changes
263 self.do_journal = 1
265 def enableJournalling(self):
266 '''Turn journalling on for this class
267 '''
268 self.do_journal = 1
270 def disableJournalling(self):
271 '''Turn journalling off for this class
272 '''
273 self.do_journal = 0
275 # --- the roundup.Class methods
276 def audit(self, event, detector):
277 l = self.auditors[event]
278 if detector not in l:
279 self.auditors[event].append(detector)
280 def fireAuditors(self, action, nodeid, newvalues):
281 for audit in self.auditors[action]:
282 audit(self.db, self, nodeid, newvalues)
283 def fireReactors(self, action, nodeid, oldvalues):
284 for react in self.reactors[action]:
285 react(self.db, self, nodeid, oldvalues)
286 def react(self, event, detector):
287 l = self.reactors[event]
288 if detector not in l:
289 self.reactors[event].append(detector)
290 # --- the hyperdb.Class methods
291 def create(self, **propvalues):
292 self.fireAuditors('create', None, propvalues)
293 rowdict = {}
294 rowdict['id'] = newid = self.maxid
295 self.maxid += 1
296 ndx = self.getview(1).append(rowdict)
297 propvalues['#ISNEW'] = 1
298 try:
299 self.set(str(newid), **propvalues)
300 except Exception:
301 self.maxid -= 1
302 raise
303 return str(newid)
305 def get(self, nodeid, propname, default=_marker, cache=1):
306 # default and cache aren't in the spec
307 # cache=0 means "original value"
309 view = self.getview()
310 id = int(nodeid)
311 if cache == 0:
312 oldnode = self.uncommitted.get(id, None)
313 if oldnode and oldnode.has_key(propname):
314 return oldnode[propname]
315 ndx = self.idcache.get(id, None)
316 if ndx is None:
317 ndx = view.find(id=id)
318 if ndx < 0:
319 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
320 self.idcache[id] = ndx
321 try:
322 raw = getattr(view[ndx], propname)
323 except AttributeError:
324 raise KeyError, propname
325 rutyp = self.ruprops.get(propname, None)
326 if rutyp is None:
327 rutyp = self.privateprops[propname]
328 converter = _converters.get(rutyp.__class__, None)
329 if converter:
330 raw = converter(raw)
331 return raw
333 def set(self, nodeid, **propvalues):
334 isnew = 0
335 if propvalues.has_key('#ISNEW'):
336 isnew = 1
337 del propvalues['#ISNEW']
338 if not isnew:
339 self.fireAuditors('set', nodeid, propvalues)
340 if not propvalues:
341 return propvalues
342 if propvalues.has_key('id'):
343 raise KeyError, '"id" is reserved'
344 if self.db.journaltag is None:
345 raise hyperdb.DatabaseError, 'Database open read-only'
346 view = self.getview(1)
347 # node must exist & not be retired
348 id = int(nodeid)
349 ndx = view.find(id=id)
350 if ndx < 0:
351 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
352 row = view[ndx]
353 if row._isdel:
354 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
355 oldnode = self.uncommitted.setdefault(id, {})
356 changes = {}
358 for key, value in propvalues.items():
359 # this will raise the KeyError if the property isn't valid
360 # ... we don't use getprops() here because we only care about
361 # the writeable properties.
362 if _ALLOWSETTINGPRIVATEPROPS:
363 prop = self.ruprops.get(key, None)
364 if not prop:
365 prop = self.privateprops[key]
366 else:
367 prop = self.ruprops[key]
368 converter = _converters.get(prop.__class__, lambda v: v)
369 # if the value's the same as the existing value, no sense in
370 # doing anything
371 oldvalue = converter(getattr(row, key))
372 if value == oldvalue:
373 del propvalues[key]
374 continue
376 # check to make sure we're not duplicating an existing key
377 if key == self.keyname:
378 iv = self.getindexview(1)
379 ndx = iv.find(k=value)
380 if ndx == -1:
381 iv.append(k=value, i=row.id)
382 if not isnew:
383 ndx = iv.find(k=oldvalue)
384 if ndx > -1:
385 iv.delete(ndx)
386 else:
387 raise ValueError, 'node with key "%s" exists'%value
389 # do stuff based on the prop type
390 if isinstance(prop, hyperdb.Link):
391 link_class = prop.classname
392 # must be a string or None
393 if value is not None and not isinstance(value, type('')):
394 raise ValueError, 'property "%s" link value be a string'%(
395 key)
396 # Roundup sets to "unselected" by passing None
397 if value is None:
398 value = 0
399 # if it isn't a number, it's a key
400 try:
401 int(value)
402 except ValueError:
403 try:
404 value = self.db.getclass(link_class).lookup(value)
405 except (TypeError, KeyError):
406 raise IndexError, 'new property "%s": %s not a %s'%(
407 key, value, prop.classname)
409 if (value is not None and
410 not self.db.getclass(link_class).hasnode(value)):
411 raise IndexError, '%s has no node %s'%(link_class, value)
413 setattr(row, key, int(value))
414 changes[key] = oldvalue
416 if self.do_journal and prop.do_journal:
417 # register the unlink with the old linked node
418 if oldvalue:
419 self.db.addjournal(link_class, value, _UNLINK,
420 (self.classname, str(row.id), key))
422 # register the link with the newly linked node
423 if value:
424 self.db.addjournal(link_class, value, _LINK,
425 (self.classname, str(row.id), key))
427 elif isinstance(prop, hyperdb.Multilink):
428 if value is not None and type(value) != _LISTTYPE:
429 raise TypeError, 'new property "%s" not a list of ids'%key
430 link_class = prop.classname
431 l = []
432 if value is None:
433 value = []
434 for entry in value:
435 if type(entry) != _STRINGTYPE:
436 raise ValueError, 'new property "%s" link value ' \
437 'must be a string'%key
438 # if it isn't a number, it's a key
439 try:
440 int(entry)
441 except ValueError:
442 try:
443 entry = self.db.getclass(link_class).lookup(entry)
444 except (TypeError, KeyError):
445 raise IndexError, 'new property "%s": %s not a %s'%(
446 key, entry, prop.classname)
447 l.append(entry)
448 propvalues[key] = value = l
450 # handle removals
451 rmvd = []
452 for id in oldvalue:
453 if id not in value:
454 rmvd.append(id)
455 # register the unlink with the old linked node
456 if self.do_journal and prop.do_journal:
457 self.db.addjournal(link_class, id, _UNLINK,
458 (self.classname, str(row.id), key))
460 # handle additions
461 adds = []
462 for id in value:
463 if id not in oldvalue:
464 if not self.db.getclass(link_class).hasnode(id):
465 raise IndexError, '%s has no node %s'%(
466 link_class, id)
467 adds.append(id)
468 # register the link with the newly linked node
469 if self.do_journal and prop.do_journal:
470 self.db.addjournal(link_class, id, _LINK,
471 (self.classname, str(row.id), key))
473 sv = getattr(row, key)
474 i = 0
475 while i < len(sv):
476 if str(sv[i].fid) in rmvd:
477 sv.delete(i)
478 else:
479 i += 1
480 for id in adds:
481 sv.append(fid=int(id))
482 changes[key] = oldvalue
483 if not rmvd and not adds:
484 del propvalues[key]
486 elif isinstance(prop, hyperdb.String):
487 if value is not None and type(value) != _STRINGTYPE:
488 raise TypeError, 'new property "%s" not a string'%key
489 if value is None:
490 value = ''
491 setattr(row, key, value)
492 changes[key] = oldvalue
493 if hasattr(prop, 'isfilename') and prop.isfilename:
494 propvalues[key] = os.path.basename(value)
495 if prop.indexme and value is not None:
496 self.db.indexer.add_text((self.classname, nodeid, key),
497 value, 'text/plain')
499 elif isinstance(prop, hyperdb.Password):
500 if value is not None and not isinstance(value, password.Password):
501 raise TypeError, 'new property "%s" not a Password'% key
502 if value is None:
503 value = ''
504 setattr(row, key, str(value))
505 changes[key] = str(oldvalue)
506 propvalues[key] = str(value)
508 elif isinstance(prop, hyperdb.Date):
509 if value is not None and not isinstance(value, date.Date):
510 raise TypeError, 'new property "%s" not a Date'% key
511 if value is None:
512 setattr(row, key, 0)
513 else:
514 setattr(row, key, int(calendar.timegm(value.get_tuple())))
515 changes[key] = str(oldvalue)
516 propvalues[key] = str(value)
518 elif isinstance(prop, hyperdb.Interval):
519 if value is not None and not isinstance(value, date.Interval):
520 raise TypeError, 'new property "%s" not an Interval'% key
521 if value is None:
522 setattr(row, key, '')
523 else:
524 setattr(row, key, str(value))
525 changes[key] = str(oldvalue)
526 propvalues[key] = str(value)
528 elif isinstance(prop, hyperdb.Number):
529 if value is None:
530 value = 0
531 try:
532 v = int(value)
533 except ValueError:
534 raise TypeError, "%s (%s) is not numeric" % (key, repr(value))
535 setattr(row, key, v)
536 changes[key] = oldvalue
537 propvalues[key] = value
539 elif isinstance(prop, hyperdb.Boolean):
540 if value is None:
541 bv = 0
542 elif value not in (0,1):
543 raise TypeError, "%s (%s) is not boolean" % (key, repr(value))
544 else:
545 bv = value
546 setattr(row, key, bv)
547 changes[key] = oldvalue
548 propvalues[key] = value
550 oldnode[key] = oldvalue
552 # nothing to do?
553 if not propvalues:
554 return propvalues
555 if not propvalues.has_key('activity'):
556 row.activity = int(time.time())
557 if isnew:
558 if not row.creation:
559 row.creation = int(time.time())
560 if not row.creator:
561 row.creator = self.db.curuserid
563 self.db.dirty = 1
564 if self.do_journal:
565 if isnew:
566 self.db.addjournal(self.classname, nodeid, _CREATE, {})
567 self.fireReactors('create', nodeid, None)
568 else:
569 self.db.addjournal(self.classname, nodeid, _SET, changes)
570 self.fireReactors('set', nodeid, oldnode)
572 return propvalues
574 def retire(self, nodeid):
575 if self.db.journaltag is None:
576 raise hyperdb.DatabaseError, 'Database open read-only'
577 self.fireAuditors('retire', nodeid, None)
578 view = self.getview(1)
579 ndx = view.find(id=int(nodeid))
580 if ndx < 0:
581 raise KeyError, "nodeid %s not found" % nodeid
582 row = view[ndx]
583 oldvalues = self.uncommitted.setdefault(row.id, {})
584 oldval = oldvalues['_isdel'] = row._isdel
585 row._isdel = 1
586 if self.do_journal:
587 self.db.addjournal(self.classname, nodeid, _RETIRE, {})
588 if self.keyname:
589 iv = self.getindexview(1)
590 ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
591 if ndx > -1:
592 iv.delete(ndx)
593 self.db.dirty = 1
594 self.fireReactors('retire', nodeid, None)
595 def history(self, nodeid):
596 if not self.do_journal:
597 raise ValueError, 'Journalling is disabled for this class'
598 return self.db.getjournal(self.classname, nodeid)
599 def setkey(self, propname):
600 if self.keyname:
601 if propname == self.keyname:
602 return
603 raise ValueError, "%s already indexed on %s" % (self.classname, self.keyname)
604 prop = self.properties.get(propname, None)
605 if prop is None:
606 prop = self.privateprops.get(propname, None)
607 if prop is None:
608 raise KeyError, "no property %s" % propname
609 if not isinstance(prop, hyperdb.String):
610 raise TypeError, "%s is not a String" % propname
611 # first setkey for this run
612 self.keyname = propname
613 iv = self.db._db.view('_%s' % self.classname)
614 if self.db.fastopen and iv.structure():
615 return
616 # very first setkey ever
617 self.db.dirty = 1
618 iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
619 iv = iv.ordered(1)
620 # print "setkey building index"
621 for row in self.getview():
622 iv.append(k=getattr(row, propname), i=row.id)
623 self.db.commit()
624 def getkey(self):
625 return self.keyname
626 def lookup(self, keyvalue):
627 if type(keyvalue) is not _STRINGTYPE:
628 raise TypeError, "%r is not a string" % keyvalue
629 iv = self.getindexview()
630 if iv:
631 ndx = iv.find(k=keyvalue)
632 if ndx > -1:
633 return str(iv[ndx].i)
634 else:
635 view = self.getview()
636 ndx = view.find({self.keyname:keyvalue, '_isdel':0})
637 if ndx > -1:
638 return str(view[ndx].id)
639 raise KeyError, keyvalue
641 def destroy(self, id):
642 view = self.getview(1)
643 ndx = view.find(id=int(id))
644 if ndx > -1:
645 if self.keyname:
646 keyvalue = getattr(view[ndx], self.keyname)
647 iv = self.getindexview(1)
648 if iv:
649 ivndx = iv.find(k=keyvalue)
650 if ivndx > -1:
651 iv.delete(ivndx)
652 view.delete(ndx)
653 self.db.destroyjournal(self.classname, id)
654 self.db.dirty = 1
656 def find(self, **propspec):
657 """Get the ids of nodes in this class which link to the given nodes.
659 'propspec' consists of keyword args propname={nodeid:1,}
660 'propname' must be the name of a property in this class, or a
661 KeyError is raised. That property must be a Link or
662 Multilink property, or a TypeError is raised.
664 Any node in this class whose propname property links to any of the
665 nodeids will be returned. Used by the full text indexing, which knows
666 that "foo" occurs in msg1, msg3 and file7; so we have hits on these
667 issues:
669 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
671 """
672 propspec = propspec.items()
673 for propname, nodeid in propspec:
674 # check the prop is OK
675 prop = self.ruprops[propname]
676 if (not isinstance(prop, hyperdb.Link) and
677 not isinstance(prop, hyperdb.Multilink)):
678 raise TypeError, "'%s' not a Link/Multilink property"%propname
680 vws = []
681 for propname, ids in propspec:
682 if type(ids) is _STRINGTYPE:
683 ids = {ids:1}
684 prop = self.ruprops[propname]
685 view = self.getview()
686 if isinstance(prop, hyperdb.Multilink):
687 view = view.flatten(getattr(view, propname))
688 def ff(row, nm=propname, ids=ids):
689 return ids.has_key(str(row.fid))
690 else:
691 def ff(row, nm=propname, ids=ids):
692 return ids.has_key(str(getattr(row, nm)))
693 ndxview = view.filter(ff)
694 vws.append(ndxview.unique())
696 # handle the empty match case
697 if not vws:
698 return []
700 ndxview = vws[0]
701 for v in vws[1:]:
702 ndxview = ndxview.union(v)
703 view = view.remapwith(ndxview)
704 rslt = []
705 for row in view:
706 rslt.append(str(row.id))
707 return rslt
710 def list(self):
711 l = []
712 for row in self.getview().select(_isdel=0):
713 l.append(str(row.id))
714 return l
715 def count(self):
716 return len(self.getview())
717 def getprops(self, protected=1):
718 # protected is not in ping's spec
719 allprops = self.ruprops.copy()
720 if protected and self.privateprops is not None:
721 allprops.update(self.privateprops)
722 return allprops
723 def addprop(self, **properties):
724 for key in properties.keys():
725 if self.ruprops.has_key(key):
726 raise ValueError, "%s is already a property of %s" % (key, self.classname)
727 self.ruprops.update(properties)
728 self.db.fastopen = 0
729 view = self.__getview()
730 self.db.commit()
731 # ---- end of ping's spec
732 def filter(self, search_matches, filterspec, sort, group):
733 # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
734 # filterspec is a dict {propname:value}
735 # sort and group are lists of propnames
736 # sort and group are (dir, prop) where dir is '+', '-' or None
737 # and prop is a prop name or None
739 where = {'_isdel':0}
740 mlcriteria = {}
741 regexes = {}
742 orcriteria = {}
743 for propname, value in filterspec.items():
744 prop = self.ruprops.get(propname, None)
745 if prop is None:
746 prop = self.privateprops[propname]
747 if isinstance(prop, hyperdb.Multilink):
748 if type(value) is not _LISTTYPE:
749 value = [value]
750 # transform keys to ids
751 u = []
752 for item in value:
753 try:
754 item = int(item)
755 except (TypeError, ValueError):
756 item = int(self.db.getclass(prop.classname).lookup(item))
757 if item == -1:
758 item = 0
759 u.append(item)
760 mlcriteria[propname] = u
761 elif isinstance(prop, hyperdb.Link):
762 if type(value) is not _LISTTYPE:
763 value = [value]
764 # transform keys to ids
765 u = []
766 for item in value:
767 try:
768 item = int(item)
769 except (TypeError, ValueError):
770 item = int(self.db.getclass(prop.classname).lookup(item))
771 if item == -1:
772 item = 0
773 u.append(item)
774 if len(u) == 1:
775 where[propname] = u[0]
776 else:
777 orcriteria[propname] = u
778 elif isinstance(prop, hyperdb.String):
779 # simple glob searching
780 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
781 v = v.replace('?', '.')
782 v = v.replace('*', '.*?')
783 regexes[propname] = re.compile(v, re.I)
784 elif propname == 'id':
785 where[propname] = int(value)
786 elif isinstance(prop, hyperdb.Boolean):
787 if type(value) is _STRINGTYPE:
788 bv = value.lower() in ('yes', 'true', 'on', '1')
789 else:
790 bv = value
791 where[propname] = bv
792 elif isinstance(prop, hyperdb.Number):
793 where[propname] = int(value)
794 else:
795 where[propname] = str(value)
796 v = self.getview()
797 #print "filter start at %s" % time.time()
798 if where:
799 v = v.select(where)
800 #print "filter where at %s" % time.time()
802 if mlcriteria:
803 # multilink - if any of the nodeids required by the
804 # filterspec aren't in this node's property, then skip
805 # it
806 def ff(row, ml=mlcriteria):
807 for propname, values in ml.items():
808 sv = getattr(row, propname)
809 for id in values:
810 if sv.find(fid=id) == -1:
811 return 0
812 return 1
813 iv = v.filter(ff)
814 v = v.remapwith(iv)
816 #print "filter mlcrit at %s" % time.time()
818 if orcriteria:
819 def ff(row, crit=orcriteria):
820 for propname, allowed in crit.items():
821 val = getattr(row, propname)
822 if val not in allowed:
823 return 0
824 return 1
826 iv = v.filter(ff)
827 v = v.remapwith(iv)
829 #print "filter orcrit at %s" % time.time()
830 if regexes:
831 def ff(row, r=regexes):
832 for propname, regex in r.items():
833 val = getattr(row, propname)
834 if not regex.search(val):
835 return 0
836 return 1
838 iv = v.filter(ff)
839 v = v.remapwith(iv)
840 #print "filter regexs at %s" % time.time()
842 if sort or group:
843 sortspec = []
844 rev = []
845 for dir, propname in group, sort:
846 if propname is None: continue
847 isreversed = 0
848 if dir == '-':
849 isreversed = 1
850 try:
851 prop = getattr(v, propname)
852 except AttributeError:
853 print "MK has no property %s" % propname
854 continue
855 propclass = self.ruprops.get(propname, None)
856 if propclass is None:
857 propclass = self.privateprops.get(propname, None)
858 if propclass is None:
859 print "Schema has no property %s" % propname
860 continue
861 if isinstance(propclass, hyperdb.Link):
862 linkclass = self.db.getclass(propclass.classname)
863 lv = linkclass.getview()
864 lv = lv.rename('id', propname)
865 v = v.join(lv, prop, 1)
866 if linkclass.getprops().has_key('order'):
867 propname = 'order'
868 else:
869 propname = linkclass.labelprop()
870 prop = getattr(v, propname)
871 if isreversed:
872 rev.append(prop)
873 sortspec.append(prop)
874 v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
875 #print "filter sort at %s" % time.time()
877 rslt = []
878 for row in v:
879 id = str(row.id)
880 if search_matches is not None:
881 if search_matches.has_key(id):
882 rslt.append(id)
883 else:
884 rslt.append(id)
885 return rslt
887 def hasnode(self, nodeid):
888 return int(nodeid) < self.maxid
890 def labelprop(self, default_to_id=0):
891 ''' Return the property name for a label for the given node.
893 This method attempts to generate a consistent label for the node.
894 It tries the following in order:
895 1. key property
896 2. "name" property
897 3. "title" property
898 4. first property from the sorted property name list
899 '''
900 k = self.getkey()
901 if k:
902 return k
903 props = self.getprops()
904 if props.has_key('name'):
905 return 'name'
906 elif props.has_key('title'):
907 return 'title'
908 if default_to_id:
909 return 'id'
910 props = props.keys()
911 props.sort()
912 return props[0]
913 def stringFind(self, **requirements):
914 """Locate a particular node by matching a set of its String
915 properties in a caseless search.
917 If the property is not a String property, a TypeError is raised.
919 The return is a list of the id of all nodes that match.
920 """
921 for propname in requirements.keys():
922 prop = self.properties[propname]
923 if isinstance(not prop, hyperdb.String):
924 raise TypeError, "'%s' not a String property"%propname
925 requirements[propname] = requirements[propname].lower()
926 requirements['_isdel'] = 0
928 l = []
929 for row in self.getview().select(requirements):
930 l.append(str(row.id))
931 return l
933 def addjournal(self, nodeid, action, params):
934 self.db.addjournal(self.classname, nodeid, action, params)
936 def index(self, nodeid):
937 ''' Add (or refresh) the node to search indexes '''
938 # find all the String properties that have indexme
939 for prop, propclass in self.getprops().items():
940 if isinstance(propclass, hyperdb.String) and propclass.indexme:
941 # index them under (classname, nodeid, property)
942 self.db.indexer.add_text((self.classname, nodeid, prop),
943 str(self.get(nodeid, prop)))
945 def export_list(self, propnames, nodeid):
946 ''' Export a node - generate a list of CSV-able data in the order
947 specified by propnames for the given node.
948 '''
949 properties = self.getprops()
950 l = []
951 for prop in propnames:
952 proptype = properties[prop]
953 value = self.get(nodeid, prop)
954 # "marshal" data where needed
955 if value is None:
956 pass
957 elif isinstance(proptype, hyperdb.Date):
958 value = value.get_tuple()
959 elif isinstance(proptype, hyperdb.Interval):
960 value = value.get_tuple()
961 elif isinstance(proptype, hyperdb.Password):
962 value = str(value)
963 l.append(repr(value))
964 return l
966 def import_list(self, propnames, proplist):
967 ''' Import a node - all information including "id" is present and
968 should not be sanity checked. Triggers are not triggered. The
969 journal should be initialised using the "creator" and "creation"
970 information.
972 Return the nodeid of the node imported.
973 '''
974 if self.db.journaltag is None:
975 raise hyperdb.DatabaseError, 'Database open read-only'
976 properties = self.getprops()
978 d = {}
979 view = self.getview(1)
980 for i in range(len(propnames)):
981 value = eval(proplist[i])
982 propname = propnames[i]
983 prop = properties[propname]
984 if propname == 'id':
985 newid = value
986 value = int(value)
987 elif isinstance(prop, hyperdb.Date):
988 value = int(calendar.timegm(value))
989 elif isinstance(prop, hyperdb.Interval):
990 value = str(date.Interval(value))
991 d[propname] = value
992 view.append(d)
993 creator = d.get('creator', None)
994 creation = d.get('creation', None)
995 self.db.addjournal(self.classname, newid, 'create', {}, creator, creation)
996 return newid
998 # --- used by Database
999 def _commit(self):
1000 """ called post commit of the DB.
1001 interested subclasses may override """
1002 self.uncommitted = {}
1003 self.rbactions = []
1004 self.idcache = {}
1005 def _rollback(self):
1006 """ called pre rollback of the DB.
1007 interested subclasses may override """
1008 for action in self.rbactions:
1009 action()
1010 self.rbactions = []
1011 self.uncommitted = {}
1012 self.idcache = {}
1013 def _clear(self):
1014 view = self.getview(1)
1015 if len(view):
1016 view[:] = []
1017 self.db.dirty = 1
1018 iv = self.getindexview(1)
1019 if iv:
1020 iv[:] = []
1021 def rollbackaction(self, action):
1022 """ call this to register a callback called on rollback
1023 callback is removed on end of transaction """
1024 self.rbactions.append(action)
1025 # --- internal
1026 def __getview(self):
1027 db = self.db._db
1028 view = db.view(self.classname)
1029 mkprops = view.structure()
1030 if mkprops and self.db.fastopen:
1031 return view.ordered(1)
1032 # is the definition the same?
1033 for nm, rutyp in self.ruprops.items():
1034 for mkprop in mkprops:
1035 if mkprop.name == nm:
1036 break
1037 else:
1038 mkprop = None
1039 if mkprop is None:
1040 break
1041 if _typmap[rutyp.__class__] != mkprop.type:
1042 break
1043 else:
1044 return view.ordered(1)
1045 # need to create or restructure the mk view
1046 # id comes first, so MK will order it for us
1047 self.db.dirty = 1
1048 s = ["%s[id:I" % self.classname]
1049 for nm, rutyp in self.ruprops.items():
1050 mktyp = _typmap[rutyp.__class__]
1051 s.append('%s:%s' % (nm, mktyp))
1052 if mktyp == 'V':
1053 s[-1] += ('[fid:I]')
1054 s.append('_isdel:I,activity:I,creation:I,creator:I]')
1055 v = self.db._db.getas(','.join(s))
1056 self.db.commit()
1057 return v.ordered(1)
1058 def getview(self, RW=0):
1059 return self.db._db.view(self.classname).ordered(1)
1060 def getindexview(self, RW=0):
1061 return self.db._db.view("_%s" % self.classname).ordered(1)
1063 def _fetchML(sv):
1064 l = []
1065 for row in sv:
1066 if row.fid:
1067 l.append(str(row.fid))
1068 return l
1070 def _fetchPW(s):
1071 p = password.Password()
1072 p.unpack(s)
1073 return p
1075 def _fetchLink(n):
1076 return n and str(n) or None
1078 def _fetchDate(n):
1079 return date.Date(time.gmtime(n))
1081 _converters = {
1082 hyperdb.Date : _fetchDate,
1083 hyperdb.Link : _fetchLink,
1084 hyperdb.Multilink : _fetchML,
1085 hyperdb.Interval : date.Interval,
1086 hyperdb.Password : _fetchPW,
1087 hyperdb.Boolean : lambda n: n,
1088 hyperdb.Number : lambda n: n,
1089 hyperdb.String : str,
1090 }
1092 class FileName(hyperdb.String):
1093 isfilename = 1
1095 _typmap = {
1096 FileName : 'S',
1097 hyperdb.String : 'S',
1098 hyperdb.Date : 'I',
1099 hyperdb.Link : 'I',
1100 hyperdb.Multilink : 'V',
1101 hyperdb.Interval : 'S',
1102 hyperdb.Password : 'S',
1103 hyperdb.Boolean : 'I',
1104 hyperdb.Number : 'I',
1105 }
1106 class FileClass(Class):
1107 ' like Class but with a content property '
1108 default_mime_type = 'text/plain'
1109 def __init__(self, db, classname, **properties):
1110 properties['content'] = FileName()
1111 if not properties.has_key('type'):
1112 properties['type'] = hyperdb.String()
1113 Class.__init__(self, db, classname, **properties)
1114 def get(self, nodeid, propname, default=_marker, cache=1):
1115 x = Class.get(self, nodeid, propname, default, cache)
1116 if propname == 'content':
1117 if x.startswith('file:'):
1118 fnm = x[5:]
1119 try:
1120 x = open(fnm, 'rb').read()
1121 except Exception, e:
1122 x = repr(e)
1123 return x
1124 def create(self, **propvalues):
1125 content = propvalues['content']
1126 del propvalues['content']
1127 newid = Class.create(self, **propvalues)
1128 if not content:
1129 return newid
1130 nm = bnm = '%s%s' % (self.classname, newid)
1131 sd = str(int(int(newid) / 1000))
1132 d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1133 if not os.path.exists(d):
1134 os.makedirs(d)
1135 nm = os.path.join(d, nm)
1136 open(nm, 'wb').write(content)
1137 self.set(newid, content = 'file:'+nm)
1138 mimetype = propvalues.get('type', self.default_mime_type)
1139 self.db.indexer.add_text((self.classname, newid, 'content'), content, mimetype)
1140 def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1141 action1(fnm)
1142 self.rollbackaction(undo)
1143 return newid
1144 def index(self, nodeid):
1145 Class.index(self, nodeid)
1146 mimetype = self.get(nodeid, 'type')
1147 if not mimetype:
1148 mimetype = self.default_mime_type
1149 self.db.indexer.add_text((self.classname, nodeid, 'content'),
1150 self.get(nodeid, 'content'), mimetype)
1152 class IssueClass(Class, roundupdb.IssueClass):
1153 # Overridden methods:
1154 def __init__(self, db, classname, **properties):
1155 """The newly-created class automatically includes the "messages",
1156 "files", "nosy", and "superseder" properties. If the 'properties'
1157 dictionary attempts to specify any of these properties or a
1158 "creation" or "activity" property, a ValueError is raised."""
1159 if not properties.has_key('title'):
1160 properties['title'] = hyperdb.String(indexme='yes')
1161 if not properties.has_key('messages'):
1162 properties['messages'] = hyperdb.Multilink("msg")
1163 if not properties.has_key('files'):
1164 properties['files'] = hyperdb.Multilink("file")
1165 if not properties.has_key('nosy'):
1166 # note: journalling is turned off as it really just wastes
1167 # space. this behaviour may be overridden in an instance
1168 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1169 if not properties.has_key('superseder'):
1170 properties['superseder'] = hyperdb.Multilink(classname)
1171 Class.__init__(self, db, classname, **properties)
1173 CURVERSION = 1
1175 class Indexer(indexer.Indexer):
1176 disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1177 def __init__(self, path, datadb):
1178 self.db = metakit.storage(os.path.join(path, 'index.mk4'), 1)
1179 self.datadb = datadb
1180 self.reindex = 0
1181 v = self.db.view('version')
1182 if not v.structure():
1183 v = self.db.getas('version[vers:I]')
1184 self.db.commit()
1185 v.append(vers=CURVERSION)
1186 self.reindex = 1
1187 elif v[0].vers != CURVERSION:
1188 v[0].vers = CURVERSION
1189 self.reindex = 1
1190 if self.reindex:
1191 self.db.getas('ids[tblid:I,nodeid:I,propid:I]')
1192 self.db.getas('index[word:S,hits[pos:I]]')
1193 self.db.commit()
1194 self.reindex = 1
1195 self.changed = 0
1196 self.propcache = {}
1197 def force_reindex(self):
1198 v = self.db.view('ids')
1199 v[:] = []
1200 v = self.db.view('index')
1201 v[:] = []
1202 self.db.commit()
1203 self.reindex = 1
1204 def should_reindex(self):
1205 return self.reindex
1206 def _getprops(self, classname):
1207 props = self.propcache.get(classname, None)
1208 if props is None:
1209 props = self.datadb.view(classname).structure()
1210 props = [prop.name for prop in props]
1211 self.propcache[classname] = props
1212 return props
1213 def _getpropid(self, classname, propname):
1214 return self._getprops(classname).index(propname)
1215 def _getpropname(self, classname, propid):
1216 return self._getprops(classname)[propid]
1218 def add_text(self, identifier, text, mime_type='text/plain'):
1219 if mime_type != 'text/plain':
1220 return
1221 classname, nodeid, property = identifier
1222 tbls = self.datadb.view('tables')
1223 tblid = tbls.find(name=classname)
1224 if tblid < 0:
1225 raise KeyError, "unknown class %r"%classname
1226 nodeid = int(nodeid)
1227 propid = self._getpropid(classname, property)
1228 pos = self.db.view('ids').append(tblid=tblid,nodeid=nodeid,propid=propid)
1230 wordlist = re.findall(r'\b\w{3,25}\b', text)
1231 words = {}
1232 for word in wordlist:
1233 word = word.upper()
1234 if not self.disallows.has_key(word):
1235 words[word] = 1
1236 words = words.keys()
1238 index = self.db.view('index').ordered(1)
1239 for word in words:
1240 ndx = index.find(word=word)
1241 if ndx < 0:
1242 ndx = index.append(word=word)
1243 hits = index[ndx].hits
1244 if len(hits)==0 or hits.find(pos=pos) < 0:
1245 hits.append(pos=pos)
1246 self.changed = 1
1248 def find(self, wordlist):
1249 hits = None
1250 index = self.db.view('index').ordered(1)
1251 for word in wordlist:
1252 if not 2 < len(word) < 26:
1253 continue
1254 ndx = index.find(word=word)
1255 if ndx < 0:
1256 return {}
1257 if hits is None:
1258 hits = index[ndx].hits
1259 else:
1260 hits = hits.intersect(index[ndx].hits)
1261 if len(hits) == 0:
1262 return {}
1263 if hits is None:
1264 return {}
1265 rslt = {}
1266 ids = self.db.view('ids').remapwith(hits)
1267 tbls = self.datadb.view('tables')
1268 for i in range(len(ids)):
1269 hit = ids[i]
1270 classname = tbls[hit.tblid].name
1271 nodeid = str(hit.nodeid)
1272 property = self._getpropname(classname, hit.propid)
1273 rslt[i] = (classname, nodeid, property)
1274 return rslt
1275 def save_index(self):
1276 if self.changed:
1277 self.db.commit()
1278 self.changed = 0