Code

Add Number and Boolean types to hyperdb.
[roundup.git] / roundup / backends / back_metakit.py
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()
43         
44             
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
115             
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
132         
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)
179     
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',
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
232         
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)
261     
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
289         
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 = {}
312         
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
330             
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
364                 
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))
415                             
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
426                     
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)
458                 
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
463                 
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
482             
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
598             
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
627         
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() 
690             
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() 
706         
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
714             
715             iv = v.filter(ff)
716             v = v.remapwith(iv)
717         
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
726             
727             iv = v.filter(ff)
728             v = v.remapwith(iv)
729         #print "filter regexs at %s" % time.time() 
730         
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() 
748             
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
758     
759     def hasnode(self, nodeid):
760         return int(nodeid) < self.maxid
761     
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.
790         
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
799         
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)
888     
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',
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)
979  
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)