Code

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