Code

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