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