1 from roundup import hyperdb, date, password, roundupdb
2 import metakit
3 import re, marshal, os, sys, weakref, time, calendar
4 from roundup.indexer import Indexer
6 _instances = weakref.WeakValueDictionary()
8 def Database(config, journaltag=None):
9 if _instances.has_key(id(config)):
10 db = _instances[id(config)]
11 old = db.journaltag
12 db.journaltag = journaltag
13 try:
14 delattr(db, 'curuserid')
15 except AttributeError:
16 pass
17 return db
18 else:
19 db = _Database(config, journaltag)
20 _instances[id(config)] = db
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._classes = []
29 self.dirty = 0
30 self.__RW = 0
31 self._db = self.__open()
32 self.indexer = Indexer(self.config.DATABASE)
33 os.umask(0002)
34 def post_init(self):
35 if self.indexer.should_reindex():
36 self.reindex()
38 def reindex(self):
39 for klass in self.classes.values():
40 for nodeid in klass.list():
41 klass.index(nodeid)
42 self.indexer.save_index()
45 # --- defined in ping's spec
46 def __getattr__(self, classname):
47 if classname == 'curuserid':
48 try:
49 self.curuserid = x = int(self.classes['user'].lookup(self.journaltag))
50 except KeyError:
51 x = 0
52 return x
53 return self.getclass(classname)
54 def getclass(self, classname):
55 return self.classes[classname]
56 def getclasses(self):
57 return self.classes.keys()
58 # --- end of ping's spec
59 # --- exposed methods
60 def commit(self):
61 if self.dirty:
62 if self.__RW:
63 self._db.commit()
64 for cl in self.classes.values():
65 cl._commit()
66 self.indexer.save_index()
67 else:
68 raise RuntimeError, "metakit is open RO"
69 self.dirty = 0
70 def rollback(self):
71 if self.dirty:
72 for cl in self.classes.values():
73 cl._rollback()
74 self._db.rollback()
75 self.dirty = 0
76 def clear(self):
77 for cl in self.classes.values():
78 cl._clear()
79 def hasnode(self, classname, nodeid):
80 return self.getclass(clasname).hasnode(nodeid)
81 def pack(self, pack_before):
82 pass
83 def addclass(self, cl):
84 self.classes[cl.classname] = cl
85 def addjournal(self, tablenm, nodeid, action, params):
86 tblid = self.tables.find(name=tablenm)
87 if tblid == -1:
88 tblid = self.tables.append(name=tablenm)
89 # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
90 self.hist.append(tableid=tblid,
91 nodeid=int(nodeid),
92 date=int(time.time()),
93 action=action,
94 user = self.curuserid,
95 params = marshal.dumps(params))
96 def gethistory(self, tablenm, nodeid):
97 rslt = []
98 tblid = self.tables.find(name=tablenm)
99 if tblid == -1:
100 return rslt
101 q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
102 i = 0
103 userclass = self.getclass('user')
104 for row in q:
105 try:
106 params = marshal.loads(row.params)
107 except ValueError:
108 print "history couldn't unmarshal %r" % row.params
109 params = {}
110 usernm = userclass.get(str(row.user), 'username')
111 dt = date.Date(time.gmtime(row.date))
112 rslt.append((i, dt, usernm, _actionnames[row.action], params))
113 i += 1
114 return rslt
116 def close(self):
117 import time
118 now = time.time
119 start = now()
120 for cl in self.classes.values():
121 cl.db = None
122 #self._db.rollback()
123 #print "pre-close cleanup of DB(%d) took %2.2f secs" % (self.__RW, now()-start)
124 self._db = None
125 #print "close of DB(%d) took %2.2f secs" % (self.__RW, now()-start)
126 self.classes = {}
127 try:
128 del _instances[id(self.config)]
129 except KeyError:
130 pass
131 self.__RW = 0
133 # --- internal
134 def __open(self):
135 self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
136 self.fastopen = 0
137 if os.path.exists(db):
138 dbtm = os.path.getmtime(db)
139 pkgnm = self.config.__name__.split('.')[0]
140 schemamod = sys.modules.get(pkgnm+'.dbinit', None)
141 if schemamod:
142 if os.path.exists(schemamod.__file__):
143 schematm = os.path.getmtime(schemamod.__file__)
144 if schematm < dbtm:
145 # found schema mod - it's older than the db
146 self.fastopen = 1
147 else:
148 # can't find schemamod - must be frozen
149 self.fastopen = 1
150 else:
151 self.__RW = 1
152 if not self.fastopen:
153 self.__RW = 1
154 db = metakit.storage(db, self.__RW)
155 hist = db.view('history')
156 tables = db.view('tables')
157 if not self.fastopen:
158 if not hist.structure():
159 hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
160 if not tables.structure():
161 tables = db.getas('tables[name:S]')
162 self.tables = tables
163 self.hist = hist
164 return db
165 def isReadOnly(self):
166 return self.__RW == 0
167 def getWriteAccess(self):
168 if self.journaltag is not None and self.__RW == 0:
169 #now = time.time
170 #start = now()
171 self._db = None
172 #print "closing the file took %2.2f secs" % (now()-start)
173 #start = now()
174 self._db = metakit.storage(self.dbnm, 1)
175 self.__RW = 1
176 self.hist = self._db.view('history')
177 self.tables = self._db.view('tables')
178 #print "getting RW access took %2.2f secs" % (now()-start)
180 _STRINGTYPE = type('')
181 _LISTTYPE = type([])
182 _CREATE, _SET, _RETIRE, _LINK, _UNLINK = range(5)
184 _actionnames = {
185 _CREATE : 'create',
186 _SET : 'set',
187 _RETIRE : 'retire',
188 _LINK : 'link',
189 _UNLINK : 'unlink',
190 }
192 _marker = []
194 _ALLOWSETTINGPRIVATEPROPS = 0
196 class Class:
197 privateprops = None
198 def __init__(self, db, classname, **properties):
199 self.db = weakref.proxy(db)
200 self.classname = classname
201 self.keyname = None
202 self.ruprops = properties
203 self.privateprops = { 'id' : hyperdb.String(),
204 'activity' : hyperdb.Date(),
205 'creation' : hyperdb.Date(),
206 'creator' : hyperdb.Link('user') }
207 self.auditors = {'create': [], 'set': [], 'retire': []} # event -> list of callables
208 self.reactors = {'create': [], 'set': [], 'retire': []} # ditto
209 view = self.__getview()
210 self.maxid = 1
211 if view:
212 self.maxid = view[-1].id + 1
213 self.uncommitted = {}
214 self.rbactions = []
215 # people reach inside!!
216 self.properties = self.ruprops
217 self.db.addclass(self)
218 self.idcache = {}
220 # default is to journal changes
221 self.do_journal = 1
223 def enableJournalling(self):
224 '''Turn journalling on for this class
225 '''
226 self.do_journal = 1
228 def disableJournalling(self):
229 '''Turn journalling off for this class
230 '''
231 self.do_journal = 0
233 # --- the roundup.Class methods
234 def audit(self, event, detector):
235 l = self.auditors[event]
236 if detector not in l:
237 self.auditors[event].append(detector)
238 def fireAuditors(self, action, nodeid, newvalues):
239 for audit in self.auditors[action]:
240 audit(self.db, self, nodeid, newvalues)
241 def fireReactors(self, action, nodeid, oldvalues):
242 for react in self.reactors[action]:
243 react(self.db, self, nodeid, oldvalues)
244 def react(self, event, detector):
245 l = self.reactors[event]
246 if detector not in l:
247 self.reactors[event].append(detector)
248 # --- the hyperdb.Class methods
249 def create(self, **propvalues):
250 rowdict = {}
251 rowdict['id'] = newid = self.maxid
252 self.maxid += 1
253 ndx = self.getview(1).append(rowdict)
254 propvalues['#ISNEW'] = 1
255 try:
256 self.set(str(newid), **propvalues)
257 except Exception:
258 self.maxid -= 1
259 raise
260 return str(newid)
262 def get(self, nodeid, propname, default=_marker, cache=1):
263 # default and cache aren't in the spec
264 # cache=0 means "original value"
266 view = self.getview()
267 id = int(nodeid)
268 if cache == 0:
269 oldnode = self.uncommitted.get(id, None)
270 if oldnode and oldnode.has_key(propname):
271 return oldnode[propname]
272 ndx = self.idcache.get(id, None)
273 if ndx is None:
274 ndx = view.find(id=id)
275 if ndx < 0:
276 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
277 self.idcache[id] = ndx
278 try:
279 raw = getattr(view[ndx], propname)
280 except AttributeError:
281 raise KeyError, propname
282 rutyp = self.ruprops.get(propname, None)
283 if rutyp is None:
284 rutyp = self.privateprops[propname]
285 converter = _converters.get(rutyp.__class__, None)
286 if converter:
287 raw = converter(raw)
288 return raw
290 def set(self, nodeid, **propvalues):
291 isnew = 0
292 if propvalues.has_key('#ISNEW'):
293 isnew = 1
294 del propvalues['#ISNEW']
295 if not propvalues:
296 return
297 if propvalues.has_key('id'):
298 raise KeyError, '"id" is reserved'
299 if self.db.journaltag is None:
300 raise DatabaseError, 'Database open read-only'
301 view = self.getview(1)
302 # node must exist & not be retired
303 id = int(nodeid)
304 ndx = view.find(id=id)
305 if ndx < 0:
306 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
307 row = view[ndx]
308 if row._isdel:
309 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
310 oldnode = self.uncommitted.setdefault(id, {})
311 changes = {}
313 for key, value in propvalues.items():
314 # this will raise the KeyError if the property isn't valid
315 # ... we don't use getprops() here because we only care about
316 # the writeable properties.
317 if _ALLOWSETTINGPRIVATEPROPS:
318 prop = self.ruprops.get(key, None)
319 if not prop:
320 prop = self.privateprops[key]
321 else:
322 prop = self.ruprops[key]
323 converter = _converters.get(prop.__class__, lambda v: v)
324 # if the value's the same as the existing value, no sense in
325 # doing anything
326 oldvalue = converter(getattr(row, key))
327 if value == oldvalue:
328 del propvalues[key]
329 continue
331 # check to make sure we're not duplicating an existing key
332 if key == self.keyname:
333 iv = self.getindexview(1)
334 ndx = iv.find(k=value)
335 if ndx == -1:
336 iv.append(k=value, i=row.id)
337 if not isnew:
338 ndx = iv.find(k=oldvalue)
339 if ndx > -1:
340 iv.delete(ndx)
341 else:
342 raise ValueError, 'node with key "%s" exists'%value
344 # do stuff based on the prop type
345 if isinstance(prop, hyperdb.Link):
346 link_class = prop.classname
347 # if it isn't a number, it's a key
348 if type(value) != _STRINGTYPE:
349 raise ValueError, 'link value must be String'
350 try:
351 int(value)
352 except ValueError:
353 try:
354 value = self.db.getclass(link_class).lookup(value)
355 except (TypeError, KeyError):
356 raise IndexError, 'new property "%s": %s not a %s'%(
357 key, value, prop.classname)
359 if not self.db.getclass(link_class).hasnode(value):
360 raise IndexError, '%s has no node %s'%(link_class, value)
362 setattr(row, key, int(value))
363 changes[key] = oldvalue
365 if self.do_journal and prop.do_journal:
366 # register the unlink with the old linked node
367 if oldvalue:
368 self.db.addjournal(link_class, value, _UNLINK, (self.classname, str(row.id), key))
370 # register the link with the newly linked node
371 if value:
372 self.db.addjournal(link_class, value, _LINK, (self.classname, str(row.id), key))
374 elif isinstance(prop, hyperdb.Multilink):
375 if type(value) != _LISTTYPE:
376 raise TypeError, 'new property "%s" not a list of ids'%key
377 link_class = prop.classname
378 l = []
379 for entry in value:
380 if type(entry) != _STRINGTYPE:
381 raise ValueError, 'new property "%s" link value ' \
382 'must be a string'%key
383 # if it isn't a number, it's a key
384 try:
385 int(entry)
386 except ValueError:
387 try:
388 entry = self.db.getclass(link_class).lookup(entry)
389 except (TypeError, KeyError):
390 raise IndexError, 'new property "%s": %s not a %s'%(
391 key, entry, prop.classname)
392 l.append(entry)
393 propvalues[key] = value = l
395 # handle removals
396 rmvd = []
397 for id in oldvalue:
398 if id not in value:
399 rmvd.append(id)
400 # register the unlink with the old linked node
401 if self.do_journal and prop.do_journal:
402 self.db.addjournal(link_class, id, _UNLINK, (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, (self.classname, str(row.id), key))
416 sv = getattr(row, key)
417 i = 0
418 while i < len(sv):
419 if str(sv[i].fid) in rmvd:
420 sv.delete(i)
421 else:
422 i += 1
423 for id in adds:
424 sv.append(fid=int(id))
425 changes[key] = oldvalue
428 elif isinstance(prop, hyperdb.String):
429 if value is not None and type(value) != _STRINGTYPE:
430 raise TypeError, 'new property "%s" not a string'%key
431 setattr(row, key, value)
432 changes[key] = oldvalue
433 if hasattr(prop, 'isfilename') and prop.isfilename:
434 propvalues[key] = os.path.basename(value)
435 if prop.indexme:
436 self.db.indexer.add_text((self.classname, nodeid, key), value, 'text/plain')
438 elif isinstance(prop, hyperdb.Password):
439 if not isinstance(value, password.Password):
440 raise TypeError, 'new property "%s" not a Password'% key
441 setattr(row, key, str(value))
442 changes[key] = str(oldvalue)
443 propvalues[key] = str(value)
445 elif value is not None and isinstance(prop, hyperdb.Date):
446 if not isinstance(value, date.Date):
447 raise TypeError, 'new property "%s" not a Date'% key
448 setattr(row, key, int(calendar.timegm(value.get_tuple())))
449 changes[key] = str(oldvalue)
450 propvalues[key] = str(value)
452 elif value is not None and isinstance(prop, hyperdb.Interval):
453 if not isinstance(value, date.Interval):
454 raise TypeError, 'new property "%s" not an Interval'% key
455 setattr(row, key, str(value))
456 changes[key] = str(oldvalue)
457 propvalues[key] = str(value)
459 elif value is not None and isinstance(prop, hyperdb.Number):
460 setattr(row, key, int(value))
461 changes[key] = oldvalue
462 propvalues[key] = value
464 elif value is not None and isinstance(prop, hyperdb.Boolean):
465 bv = value != 0
466 setattr(row, key, bv)
467 changes[key] = oldvalue
468 propvalues[key] = value
470 oldnode[key] = oldvalue
472 # nothing to do?
473 if not propvalues:
474 return
475 if not propvalues.has_key('activity'):
476 row.activity = int(time.time())
477 if isnew:
478 if not row.creation:
479 row.creation = int(time.time())
480 if not row.creator:
481 row.creator = self.db.curuserid
483 self.db.dirty = 1
484 if self.do_journal:
485 if isnew:
486 self.db.addjournal(self.classname, nodeid, _CREATE, {})
487 else:
488 self.db.addjournal(self.classname, nodeid, _SET, changes)
490 def retire(self, nodeid):
491 view = self.getview(1)
492 ndx = view.find(id=int(nodeid))
493 if ndx < 0:
494 raise KeyError, "nodeid %s not found" % nodeid
495 row = view[ndx]
496 oldvalues = self.uncommitted.setdefault(row.id, {})
497 oldval = oldvalues['_isdel'] = row._isdel
498 row._isdel = 1
499 if self.do_journal:
500 self.db.addjournal(self.classname, nodeid, _RETIRE, {})
501 iv = self.getindexview(1)
502 ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
503 if ndx > -1:
504 iv.delete(ndx)
505 self.db.dirty = 1
506 def history(self, nodeid):
507 if not self.do_journal:
508 raise ValueError, 'Journalling is disabled for this class'
509 return self.db.gethistory(self.classname, nodeid)
510 def setkey(self, propname):
511 if self.keyname:
512 if propname == self.keyname:
513 return
514 raise ValueError, "%s already indexed on %s" % (self.classname, self.keyname)
515 # first setkey for this run
516 self.keyname = propname
517 iv = self.db._db.view('_%s' % self.classname)
518 if self.db.fastopen and iv.structure():
519 return
520 # very first setkey ever
521 self.db.getWriteAccess()
522 self.db.dirty = 1
523 iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
524 iv = iv.ordered(1)
525 #XXX
526 # print "setkey building index"
527 for row in self.getview():
528 iv.append(k=getattr(row, propname), i=row.id)
529 self.db.commit()
530 def getkey(self):
531 return self.keyname
532 def lookup(self, keyvalue):
533 if type(keyvalue) is not _STRINGTYPE:
534 raise TypeError, "%r is not a string" % keyvalue
535 iv = self.getindexview()
536 if iv:
537 ndx = iv.find(k=keyvalue)
538 if ndx > -1:
539 return str(iv[ndx].i)
540 else:
541 view = self.getview()
542 ndx = view.find({self.keyname:keyvalue, '_isdel':0})
543 if ndx > -1:
544 return str(view[ndx].id)
545 raise KeyError, keyvalue
546 def find(self, **propspec):
547 """Get the ids of nodes in this class which link to the given nodes.
549 'propspec' consists of keyword args propname={nodeid:1,}
550 'propname' must be the name of a property in this class, or a
551 KeyError is raised. That property must be a Link or
552 Multilink property, or a TypeError is raised.
554 Any node in this class whose propname property links to any of the
555 nodeids will be returned. Used by the full text indexing, which knows
556 that "foo" occurs in msg1, msg3 and file7; so we have hits on these
557 issues:
559 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
561 """
562 propspec = propspec.items()
563 for propname, nodeid in propspec:
564 # check the prop is OK
565 prop = self.ruprops[propname]
566 if (not isinstance(prop, hyperdb.Link) and
567 not isinstance(prop, hyperdb.Multilink)):
568 raise TypeError, "'%s' not a Link/Multilink property"%propname
570 vws = []
571 for propname, ids in propspec:
572 if type(ids) is _STRINGTYPE:
573 ids = {ids:1}
574 prop = self.ruprops[propname]
575 view = self.getview()
576 if isinstance(prop, hyperdb.Multilink):
577 view = view.flatten(getattr(view, propname))
578 def ff(row, nm=propname, ids=ids):
579 return ids.has_key(str(row.fid))
580 else:
581 def ff(row, nm=propname, ids=ids):
582 return ids.has_key(str(getattr(row, nm)))
583 ndxview = view.filter(ff)
584 vws.append(ndxview.unique())
586 # handle the empty match case
587 if not vws:
588 return []
590 ndxview = vws[0]
591 for v in vws[1:]:
592 ndxview = ndxview.union(v)
593 view = view.remapwith(ndxview)
594 rslt = []
595 for row in view:
596 rslt.append(str(row.id))
597 return rslt
600 def list(self):
601 l = []
602 for row in self.getview().select(_isdel=0):
603 l.append(str(row.id))
604 return l
605 def count(self):
606 return len(self.getview())
607 def getprops(self, protected=1):
608 # protected is not in ping's spec
609 allprops = self.ruprops.copy()
610 if protected and self.privateprops is not None:
611 allprops.update(self.privateprops)
612 return allprops
613 def addprop(self, **properties):
614 for key in properties.keys():
615 if self.ruprops.has_key(key):
616 raise ValueError, "%s is already a property of %s" % (key, self.classname)
617 self.ruprops.update(properties)
618 self.db.getWriteAccess()
619 self.db.fastopen = 0
620 view = self.__getview()
621 self.db.commit()
622 # ---- end of ping's spec
623 def filter(self, search_matches, filterspec, sort, group):
624 # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
625 # filterspec is a dict {propname:value}
626 # sort and group are lists of propnames
628 where = {'_isdel':0}
629 mlcriteria = {}
630 regexes = {}
631 orcriteria = {}
632 for propname, value in filterspec.items():
633 prop = self.ruprops.get(propname, None)
634 if prop is None:
635 prop = self.privateprops[propname]
636 if isinstance(prop, hyperdb.Multilink):
637 if type(value) is not _LISTTYPE:
638 value = [value]
639 # transform keys to ids
640 u = []
641 for item in value:
642 try:
643 item = int(item)
644 except (TypeError, ValueError):
645 item = int(self.db.getclass(prop.classname).lookup(item))
646 if item == -1:
647 item = 0
648 u.append(item)
649 mlcriteria[propname] = u
650 elif isinstance(prop, hyperdb.Link):
651 if type(value) is not _LISTTYPE:
652 value = [value]
653 # transform keys to ids
654 u = []
655 for item in value:
656 try:
657 item = int(item)
658 except (TypeError, ValueError):
659 item = int(self.db.getclass(prop.classname).lookup(item))
660 if item == -1:
661 item = 0
662 u.append(item)
663 if len(u) == 1:
664 where[propname] = u[0]
665 else:
666 orcriteria[propname] = u
667 elif isinstance(prop, hyperdb.String):
668 # simple glob searching
669 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
670 v = v.replace('?', '.')
671 v = v.replace('*', '.*?')
672 regexes[propname] = re.compile(v, re.I)
673 elif propname == 'id':
674 where[propname] = int(value)
675 elif isinstance(prop, hyperdb.Boolean):
676 if type(value) is _STRINGTYPE:
677 bv = value.lower() in ('yes', 'true', 'on', '1')
678 else:
679 bv = value
680 where[propname] = bv
681 elif isinstance(prop, hyperdb.Number):
682 where[propname] = int(value)
683 else:
684 where[propname] = str(value)
685 v = self.getview()
686 #print "filter start at %s" % time.time()
687 if where:
688 v = v.select(where)
689 #print "filter where at %s" % time.time()
691 if mlcriteria:
692 # multilink - if any of the nodeids required by the
693 # filterspec aren't in this node's property, then skip
694 # it
695 def ff(row, ml=mlcriteria):
696 for propname, values in ml.items():
697 sv = getattr(row, propname)
698 for id in values:
699 if sv.find(fid=id) == -1:
700 return 0
701 return 1
702 iv = v.filter(ff)
703 v = v.remapwith(iv)
705 #print "filter mlcrit at %s" % time.time()
707 if orcriteria:
708 def ff(row, crit=orcriteria):
709 for propname, allowed in crit.items():
710 val = getattr(row, propname)
711 if val not in allowed:
712 return 0
713 return 1
715 iv = v.filter(ff)
716 v = v.remapwith(iv)
718 #print "filter orcrit at %s" % time.time()
719 if regexes:
720 def ff(row, r=regexes):
721 for propname, regex in r.items():
722 val = getattr(row, propname)
723 if not regex.search(val):
724 return 0
725 return 1
727 iv = v.filter(ff)
728 v = v.remapwith(iv)
729 #print "filter regexs at %s" % time.time()
731 if sort or group:
732 sortspec = []
733 rev = []
734 for propname in group + sort:
735 isreversed = 0
736 if propname[0] == '-':
737 propname = propname[1:]
738 isreversed = 1
739 try:
740 prop = getattr(v, propname)
741 except AttributeError:
742 continue
743 if isreversed:
744 rev.append(prop)
745 sortspec.append(prop)
746 v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
747 #print "filter sort at %s" % time.time()
749 rslt = []
750 for row in v:
751 id = str(row.id)
752 if search_matches is not None:
753 if search_matches.has_key(id):
754 rslt.append(id)
755 else:
756 rslt.append(id)
757 return rslt
759 def hasnode(self, nodeid):
760 return int(nodeid) < self.maxid
762 def labelprop(self, default_to_id=0):
763 ''' Return the property name for a label for the given node.
765 This method attempts to generate a consistent label for the node.
766 It tries the following in order:
767 1. key property
768 2. "name" property
769 3. "title" property
770 4. first property from the sorted property name list
771 '''
772 k = self.getkey()
773 if k:
774 return k
775 props = self.getprops()
776 if props.has_key('name'):
777 return 'name'
778 elif props.has_key('title'):
779 return 'title'
780 if default_to_id:
781 return 'id'
782 props = props.keys()
783 props.sort()
784 return props[0]
785 def stringFind(self, **requirements):
786 """Locate a particular node by matching a set of its String
787 properties in a caseless search.
789 If the property is not a String property, a TypeError is raised.
791 The return is a list of the id of all nodes that match.
792 """
793 for propname in requirements.keys():
794 prop = self.properties[propname]
795 if isinstance(not prop, hyperdb.String):
796 raise TypeError, "'%s' not a String property"%propname
797 requirements[propname] = requirements[propname].lower()
798 requirements['_isdel'] = 0
800 l = []
801 for row in self.getview().select(requirements):
802 l.append(str(row.id))
803 return l
805 def addjournal(self, nodeid, action, params):
806 self.db.addjournal(self.classname, nodeid, action, params)
808 def index(self, nodeid):
809 ''' Add (or refresh) the node to search indexes '''
810 # find all the String properties that have indexme
811 for prop, propclass in self.getprops().items():
812 if isinstance(propclass, hyperdb.String) and propclass.indexme:
813 # index them under (classname, nodeid, property)
814 self.db.indexer.add_text((self.classname, nodeid, prop),
815 str(self.get(nodeid, prop)))
817 # --- used by Database
818 def _commit(self):
819 """ called post commit of the DB.
820 interested subclasses may override """
821 self.uncommitted = {}
822 self.rbactions = []
823 self.idcache = {}
824 def _rollback(self):
825 """ called pre rollback of the DB.
826 interested subclasses may override """
827 for action in self.rbactions:
828 action()
829 self.rbactions = []
830 self.uncommitted = {}
831 self.idcache = {}
832 def _clear(self):
833 view = self.getview(1)
834 if len(view):
835 view[:] = []
836 self.db.dirty = 1
837 iv = self.getindexview(1)
838 if iv:
839 iv[:] = []
840 def rollbackaction(self, action):
841 """ call this to register a callback called on rollback
842 callback is removed on end of transaction """
843 self.rbactions.append(action)
844 # --- internal
845 def __getview(self):
846 db = self.db._db
847 view = db.view(self.classname)
848 mkprops = view.structure()
849 if mkprops and self.db.fastopen:
850 return view.ordered(1)
851 # is the definition the same?
852 for nm, rutyp in self.ruprops.items():
853 for mkprop in mkprops:
854 if mkprop.name == nm:
855 break
856 else:
857 mkprop = None
858 if mkprop is None:
859 print "%s missing prop %s (%s)" % (self.classname, nm, rutyp.__class__.__name__)
860 break
861 if _typmap[rutyp.__class__] != mkprop.type:
862 print "%s - prop %s (%s) has wrong mktyp (%s)" % (self.classname, nm, rutyp.__class__.__name__, mkprop.type)
863 break
864 else:
865 return view.ordered(1)
866 # need to create or restructure the mk view
867 # id comes first, so MK will order it for us
868 self.db.getWriteAccess()
869 self.db.dirty = 1
870 s = ["%s[id:I" % self.classname]
871 for nm, rutyp in self.ruprops.items():
872 mktyp = _typmap[rutyp.__class__]
873 s.append('%s:%s' % (nm, mktyp))
874 if mktyp == 'V':
875 s[-1] += ('[fid:I]')
876 s.append('_isdel:I,activity:I,creation:I,creator:I]')
877 v = self.db._db.getas(','.join(s))
878 self.db.commit()
879 return v.ordered(1)
880 def getview(self, RW=0):
881 if RW and self.db.isReadOnly():
882 self.db.getWriteAccess()
883 return self.db._db.view(self.classname).ordered(1)
884 def getindexview(self, RW=0):
885 if RW and self.db.isReadOnly():
886 self.db.getWriteAccess()
887 return self.db._db.view("_%s" % self.classname).ordered(1)
889 def _fetchML(sv):
890 l = []
891 for row in sv:
892 if row.fid:
893 l.append(str(row.fid))
894 return l
896 def _fetchPW(s):
897 p = password.Password()
898 p.unpack(s)
899 return p
901 def _fetchLink(n):
902 return n and str(n) or None
904 def _fetchDate(n):
905 return date.Date(time.gmtime(n))
907 _converters = {
908 hyperdb.Date : _fetchDate,
909 hyperdb.Link : _fetchLink,
910 hyperdb.Multilink : _fetchML,
911 hyperdb.Interval : date.Interval,
912 hyperdb.Password : _fetchPW,
913 hyperdb.Boolean : lambda n: n,
914 hyperdb.Number : lambda n: n,
915 }
917 class FileName(hyperdb.String):
918 isfilename = 1
920 _typmap = {
921 FileName : 'S',
922 hyperdb.String : 'S',
923 hyperdb.Date : 'I',
924 hyperdb.Link : 'I',
925 hyperdb.Multilink : 'V',
926 hyperdb.Interval : 'S',
927 hyperdb.Password : 'S',
928 hyperdb.Boolean : 'I',
929 hyperdb.Number : 'I',
930 }
931 class FileClass(Class):
932 ' like Class but with a content property '
933 default_mime_type = 'text/plain'
934 def __init__(self, db, classname, **properties):
935 properties['content'] = FileName()
936 if not properties.has_key('type'):
937 properties['type'] = hyperdb.String()
938 Class.__init__(self, db, classname, **properties)
939 def get(self, nodeid, propname, default=_marker, cache=1):
940 x = Class.get(self, nodeid, propname, default, cache)
941 if propname == 'content':
942 if x.startswith('file:'):
943 fnm = x[5:]
944 try:
945 x = open(fnm, 'rb').read()
946 except Exception, e:
947 x = repr(e)
948 return x
949 def create(self, **propvalues):
950 content = propvalues['content']
951 del propvalues['content']
952 newid = Class.create(self, **propvalues)
953 if not content:
954 return newid
955 if content.startswith('/tracker/download.php?'):
956 self.set(newid, content='http://sourceforge.net'+content)
957 return newid
958 nm = bnm = '%s%s' % (self.classname, newid)
959 sd = str(int(int(newid) / 1000))
960 d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
961 if not os.path.exists(d):
962 os.makedirs(d)
963 nm = os.path.join(d, nm)
964 open(nm, 'wb').write(content)
965 self.set(newid, content = 'file:'+nm)
966 mimetype = propvalues.get('type', self.default_mime_type)
967 self.db.indexer.add_text((self.classname, newid, 'content'), content, mimetype)
968 def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
969 remove(fnm)
970 self.rollbackaction(undo)
971 return newid
972 def index(self, nodeid):
973 Class.index(self, nodeid)
974 mimetype = self.get(nodeid, 'type')
975 if not mimetype:
976 mimetype = self.default_mime_type
977 self.db.indexer.add_text((self.classname, nodeid, 'content'),
978 self.get(nodeid, 'content'), mimetype)
980 class IssueClass(Class, roundupdb.IssueClass):
981 # Overridden methods:
982 def __init__(self, db, classname, **properties):
983 """The newly-created class automatically includes the "messages",
984 "files", "nosy", and "superseder" properties. If the 'properties'
985 dictionary attempts to specify any of these properties or a
986 "creation" or "activity" property, a ValueError is raised."""
987 if not properties.has_key('title'):
988 properties['title'] = hyperdb.String(indexme='yes')
989 if not properties.has_key('messages'):
990 properties['messages'] = hyperdb.Multilink("msg")
991 if not properties.has_key('files'):
992 properties['files'] = hyperdb.Multilink("file")
993 if not properties.has_key('nosy'):
994 properties['nosy'] = hyperdb.Multilink("user")
995 if not properties.has_key('superseder'):
996 properties['superseder'] = hyperdb.Multilink(classname)
997 Class.__init__(self, db, classname, **properties)