Code

- more fixes to CGI form handling
[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)
37     def post_init(self):
38         if self.indexer.should_reindex():
39             self.reindex()
41     def reindex(self):
42         for klass in self.classes.values():
43             for nodeid in klass.list():
44                 klass.index(nodeid)
45         self.indexer.save_index()
47     # --- defined in ping's spec
48     def __getattr__(self, classname):
49         if classname == 'curuserid':
50             if self.journaltag is None:
51                 return None
53             try:
54                 self.curuserid = x = int(self.classes['user'].lookup(self.journaltag))
55             except KeyError:
56                 if self.journaltag == 'admin':
57                     self.curuserid = x = 1
58                 else:
59                     x = 0
60             return x
61         elif classname == 'transactions':
62             return self.dirty
63         return self.getclass(classname)
64     def getclass(self, classname):
65         try:
66             return self.classes[classname]
67         except KeyError:
68             raise KeyError, 'There is no class called "%s"'%classname
69     def getclasses(self):
70         return self.classes.keys()
71     # --- end of ping's spec 
72     # --- exposed methods
73     def commit(self):
74         if self.dirty:
75             self._db.commit()
76             for cl in self.classes.values():
77                 cl._commit()
78             self.indexer.save_index()
79         self.dirty = 0
80     def rollback(self):
81         if self.dirty:
82             for cl in self.classes.values():
83                 cl._rollback()
84             self._db.rollback()
85             self._db = None
86             self._db = metakit.storage(self.dbnm, 1)
87             self.hist = self._db.view('history')
88             self.tables = self._db.view('tables')
89             self.indexer.rollback()
90             self.indexer.datadb = self._db
91         self.dirty = 0
92     def clearCache(self):
93         for cl in self.classes.values():
94             cl._commit()
95     def clear(self):
96         for cl in self.classes.values():
97             cl._clear()
98     def hasnode(self, classname, nodeid):
99         return self.getclass(classname).hasnode(nodeid)
100     def pack(self, pack_before):
101         mindate = int(calendar.timegm(pack_before.get_tuple()))
102         i = 0
103         while i < len(self.hist):
104             if self.hist[i].date < mindate and self.hist[i].action != _CREATE:
105                 self.hist.delete(i)
106             else:
107                 i = i + 1
108     def addclass(self, cl):
109         self.classes[cl.classname] = cl
110         if self.tables.find(name=cl.classname) < 0:
111             self.tables.append(name=cl.classname)
112     def addjournal(self, tablenm, nodeid, action, params, creator=None,
113                 creation=None):
114         tblid = self.tables.find(name=tablenm)
115         if tblid == -1:
116             tblid = self.tables.append(name=tablenm)
117         if creator is None:
118             creator = self.curuserid
119         else:
120             try:
121                 creator = int(creator)
122             except TypeError:
123                 creator = int(self.getclass('user').lookup(creator))
124         if creation is None:
125             creation = int(time.time())
126         elif isinstance(creation, date.Date):
127             creation = int(calendar.timegm(creation.get_tuple()))
128         # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
129         self.hist.append(tableid=tblid,
130                          nodeid=int(nodeid),
131                          date=creation,
132                          action=action,
133                          user = creator,
134                          params = marshal.dumps(params))
135     def getjournal(self, tablenm, nodeid):
136         rslt = []
137         tblid = self.tables.find(name=tablenm)
138         if tblid == -1:
139             return rslt
140         q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
141         if len(q) == 0:
142             raise IndexError, "no history for id %s in %s" % (nodeid, tablenm)
143         i = 0
144         #userclass = self.getclass('user')
145         for row in q:
146             try:
147                 params = marshal.loads(row.params)
148             except ValueError:
149                 print "history couldn't unmarshal %r" % row.params
150                 params = {}
151             #usernm = userclass.get(str(row.user), 'username')
152             dt = date.Date(time.gmtime(row.date))
153             #rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
154             rslt.append((nodeid, dt, str(row.user), _actionnames[row.action], params))
155         return rslt
156             
157     def destroyjournal(self, tablenm, nodeid):
158         nodeid = int(nodeid)
159         tblid = self.tables.find(name=tablenm)
160         if tblid == -1:
161             return 
162         i = 0
163         hist = self.hist
164         while i < len(hist):
165             if hist[i].tableid == tblid and hist[i].nodeid == nodeid:
166                 hist.delete(i)
167             else:
168                 i = i + 1
169         self.dirty = 1
170         
171     def close(self):
172         for cl in self.classes.values():
173             cl.db = None
174         self._db = None
175         if self.lockfile is not None:
176             locking.release_lock(self.lockfile)
177         if _dbs.has_key(self.config.DATABASE):
178             del _dbs[self.config.DATABASE]
179         if self.lockfile is not None:
180             self.lockfile.close()
181             self.lockfile = None
182         self.classes = {}
183         self.indexer = None
185     # --- internal
186     def __open(self):
187         if not os.path.exists(self.config.DATABASE):
188             os.makedirs(self.config.DATABASE)
189         self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
190         lockfilenm = db[:-3]+'lck'
191         self.lockfile = locking.acquire_lock(lockfilenm)
192         self.lockfile.write(str(os.getpid()))
193         self.lockfile.flush()
194         self.fastopen = 0
195         if os.path.exists(db):
196             dbtm = os.path.getmtime(db)
197             pkgnm = self.config.__name__.split('.')[0]
198             schemamod = sys.modules.get(pkgnm+'.dbinit', None)
199             if schemamod:
200                 if os.path.exists(schemamod.__file__):
201                     schematm = os.path.getmtime(schemamod.__file__)
202                     if schematm < dbtm:
203                         # found schema mod - it's older than the db
204                         self.fastopen = 1
205                 else:
206                      # can't find schemamod - must be frozen
207                     self.fastopen = 1
208         db = metakit.storage(db, 1)
209         hist = db.view('history')
210         tables = db.view('tables')
211         if not self.fastopen:
212             if not hist.structure():
213                 hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
214             if not tables.structure():
215                 tables = db.getas('tables[name:S]')
216             db.commit()
217         self.tables = tables
218         self.hist = hist
219         return db
220         
221 _STRINGTYPE = type('')
222 _LISTTYPE = type([])
223 _CREATE, _SET, _RETIRE, _LINK, _UNLINK = range(5)
225 _actionnames = {
226     _CREATE : 'create',
227     _SET : 'set',
228     _RETIRE : 'retire',
229     _LINK : 'link',
230     _UNLINK : 'unlink',
233 _marker = []
235 _ALLOWSETTINGPRIVATEPROPS = 0
237 class Class:    
238     privateprops = None
239     def __init__(self, db, classname, **properties):
240         #self.db = weakref.proxy(db)
241         self.db = db
242         self.classname = classname
243         self.keyname = None
244         self.ruprops = properties
245         self.privateprops = { 'id' : hyperdb.String(),
246                               'activity' : hyperdb.Date(),
247                               'creation' : hyperdb.Date(),
248                               'creator'  : hyperdb.Link('user') }
250         # event -> list of callables
251         self.auditors = {'create': [], 'set': [], 'retire': []}
252         self.reactors = {'create': [], 'set': [], 'retire': []}
254         view = self.__getview()
255         self.maxid = 1
256         if view:
257             self.maxid = view[-1].id + 1
258         self.uncommitted = {}
259         self.rbactions = []
261         # people reach inside!!
262         self.properties = self.ruprops
263         self.db.addclass(self)
264         self.idcache = {}
266         # default is to journal changes
267         self.do_journal = 1
269     def enableJournalling(self):
270         '''Turn journalling on for this class
271         '''
272         self.do_journal = 1
274     def disableJournalling(self):
275         '''Turn journalling off for this class
276         '''
277         self.do_journal = 0
278         
279     # --- the roundup.Class methods
280     def audit(self, event, detector):
281         l = self.auditors[event]
282         if detector not in l:
283             self.auditors[event].append(detector)
284     def fireAuditors(self, action, nodeid, newvalues):
285         for audit in self.auditors[action]:
286             audit(self.db, self, nodeid, newvalues)
287     def fireReactors(self, action, nodeid, oldvalues):
288         for react in self.reactors[action]:
289             react(self.db, self, nodeid, oldvalues)
290     def react(self, event, detector):
291         l = self.reactors[event]
292         if detector not in l:
293             self.reactors[event].append(detector)
295     # --- the hyperdb.Class methods
296     def create(self, **propvalues):
297         self.fireAuditors('create', None, propvalues)
298         rowdict = {}
299         rowdict['id'] = newid = self.maxid
300         self.maxid += 1
301         ndx = self.getview(1).append(rowdict)
302         propvalues['#ISNEW'] = 1
303         try:
304             self.set(str(newid), **propvalues)
305         except Exception:
306             self.maxid -= 1
307             raise
308         return str(newid)
309     
310     def get(self, nodeid, propname, default=_marker, cache=1):
311         # default and cache aren't in the spec
312         # cache=0 means "original value"
314         view = self.getview()        
315         id = int(nodeid)
316         if cache == 0:
317             oldnode = self.uncommitted.get(id, None)
318             if oldnode and oldnode.has_key(propname):
319                 return oldnode[propname]
320         ndx = self.idcache.get(id, None)
321         if ndx is None:
322             ndx = view.find(id=id)
323             if ndx < 0:
324                 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
325             self.idcache[id] = ndx
326         try:
327             raw = getattr(view[ndx], propname)
328         except AttributeError:
329             raise KeyError, propname
330         rutyp = self.ruprops.get(propname, None)
331         if rutyp is None:
332             rutyp = self.privateprops[propname]
333         converter = _converters.get(rutyp.__class__, None)
334         if converter:
335             raw = converter(raw)
336         return raw
337         
338     def set(self, nodeid, **propvalues):
339         isnew = 0
340         if propvalues.has_key('#ISNEW'):
341             isnew = 1
342             del propvalues['#ISNEW']
343         if not isnew:
344             self.fireAuditors('set', nodeid, propvalues)
345         if not propvalues:
346             return propvalues
347         if propvalues.has_key('id'):
348             raise KeyError, '"id" is reserved'
349         if self.db.journaltag is None:
350             raise hyperdb.DatabaseError, 'Database open read-only'
351         view = self.getview(1)
353         # node must exist & not be retired
354         id = int(nodeid)
355         ndx = view.find(id=id)
356         if ndx < 0:
357             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
358         row = view[ndx]
359         if row._isdel:
360             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
361         oldnode = self.uncommitted.setdefault(id, {})
362         changes = {}
364         for key, value in propvalues.items():
365             # this will raise the KeyError if the property isn't valid
366             # ... we don't use getprops() here because we only care about
367             # the writeable properties.
368             if _ALLOWSETTINGPRIVATEPROPS:
369                 prop = self.ruprops.get(key, None)
370                 if not prop:
371                     prop = self.privateprops[key]
372             else:
373                 prop = self.ruprops[key]
374             converter = _converters.get(prop.__class__, lambda v: v)
375             # if the value's the same as the existing value, no sense in
376             # doing anything
377             oldvalue = converter(getattr(row, key))
378             if  value == oldvalue:
379                 del propvalues[key]
380                 continue
381             
382             # check to make sure we're not duplicating an existing key
383             if key == self.keyname:
384                 iv = self.getindexview(1)
385                 ndx = iv.find(k=value)
386                 if ndx == -1:
387                     iv.append(k=value, i=row.id)
388                     if not isnew:
389                         ndx = iv.find(k=oldvalue)
390                         if ndx > -1:
391                             iv.delete(ndx)
392                 else:
393                     raise ValueError, 'node with key "%s" exists'%value
395             # do stuff based on the prop type
396             if isinstance(prop, hyperdb.Link):
397                 link_class = prop.classname
398                 # must be a string or None
399                 if value is not None and not isinstance(value, type('')):
400                     raise ValueError, 'property "%s" link value be a string'%(
401                         key)
402                 # Roundup sets to "unselected" by passing None
403                 if value is None:
404                     value = 0   
405                 # if it isn't a number, it's a key
406                 try:
407                     int(value)
408                 except ValueError:
409                     try:
410                         value = self.db.getclass(link_class).lookup(value)
411                     except (TypeError, KeyError):
412                         raise IndexError, 'new property "%s": %s not a %s'%(
413                             key, value, prop.classname)
415                 if (value is not None and
416                         not self.db.getclass(link_class).hasnode(value)):
417                     raise IndexError, '%s has no node %s'%(link_class, value)
419                 setattr(row, key, int(value))
420                 changes[key] = oldvalue
421                 
422                 if self.do_journal and prop.do_journal:
423                     # register the unlink with the old linked node
424                     if oldvalue:
425                         self.db.addjournal(link_class, value, _UNLINK,
426                             (self.classname, str(row.id), key))
428                     # register the link with the newly linked node
429                     if value:
430                         self.db.addjournal(link_class, value, _LINK,
431                             (self.classname, str(row.id), key))
433             elif isinstance(prop, hyperdb.Multilink):
434                 if value is not None and type(value) != _LISTTYPE:
435                     raise TypeError, 'new property "%s" not a list of ids'%key
436                 link_class = prop.classname
437                 l = []
438                 if value is None:
439                     value = []
440                 for entry in value:
441                     if type(entry) != _STRINGTYPE:
442                         raise ValueError, 'new property "%s" link value ' \
443                             'must be a string'%key
444                     # if it isn't a number, it's a key
445                     try:
446                         int(entry)
447                     except ValueError:
448                         try:
449                             entry = self.db.getclass(link_class).lookup(entry)
450                         except (TypeError, KeyError):
451                             raise IndexError, 'new property "%s": %s not a %s'%(
452                                 key, entry, prop.classname)
453                     l.append(entry)
454                 propvalues[key] = value = l
456                 # handle removals
457                 rmvd = []
458                 for id in oldvalue:
459                     if id not in value:
460                         rmvd.append(id)
461                         # register the unlink with the old linked node
462                         if self.do_journal and prop.do_journal:
463                             self.db.addjournal(link_class, id, _UNLINK,
464                                 (self.classname, str(row.id), key))
466                 # handle additions
467                 adds = []
468                 for id in value:
469                     if id not in oldvalue:
470                         if not self.db.getclass(link_class).hasnode(id):
471                             raise IndexError, '%s has no node %s'%(
472                                 link_class, id)
473                         adds.append(id)
474                         # register the link with the newly linked node
475                         if self.do_journal and prop.do_journal:
476                             self.db.addjournal(link_class, id, _LINK,
477                                 (self.classname, str(row.id), key))
479                 # perform the modifications on the actual property value
480                 sv = getattr(row, key)
481                 i = 0
482                 while i < len(sv):
483                     if str(sv[i].fid) in rmvd:
484                         sv.delete(i)
485                     else:
486                         i += 1
487                 for id in adds:
488                     sv.append(fid=int(id))
490                 # figure the journal entry
491                 l = []
492                 if adds:
493                     l.append(('+', adds))
494                 if rmvd:
495                     l.append(('-', rmvd))
496                 if l:
497                     changes[key] = tuple(l)
498                 #changes[key] = oldvalue
500                 if not rmvd and not adds:
501                     del propvalues[key]
502                     
503             elif isinstance(prop, hyperdb.String):
504                 if value is not None and type(value) != _STRINGTYPE:
505                     raise TypeError, 'new property "%s" not a string'%key
506                 if value is None:
507                     value = ''
508                 setattr(row, key, value)
509                 changes[key] = oldvalue
510                 if hasattr(prop, 'isfilename') and prop.isfilename:
511                     propvalues[key] = os.path.basename(value)
512                 if prop.indexme:
513                     self.db.indexer.add_text((self.classname, nodeid, key),
514                         value, 'text/plain')
516             elif isinstance(prop, hyperdb.Password):
517                 if value is not None and not isinstance(value, password.Password):
518                     raise TypeError, 'new property "%s" not a Password'% key
519                 if value is None:
520                     value = ''
521                 setattr(row, key, str(value))
522                 changes[key] = str(oldvalue)
523                 propvalues[key] = str(value)
525             elif isinstance(prop, hyperdb.Date):
526                 if value is not None and not isinstance(value, date.Date):
527                     raise TypeError, 'new property "%s" not a Date'% key
528                 if value is None:
529                     setattr(row, key, 0)
530                 else:
531                     setattr(row, key, int(calendar.timegm(value.get_tuple())))
532                 changes[key] = str(oldvalue)
533                 propvalues[key] = str(value)
535             elif isinstance(prop, hyperdb.Interval):
536                 if value is not None and not isinstance(value, date.Interval):
537                     raise TypeError, 'new property "%s" not an Interval'% key
538                 if value is None:
539                     setattr(row, key, '')
540                 else:
541                     setattr(row, key, str(value))
542                 changes[key] = str(oldvalue)
543                 propvalues[key] = str(value)
544                 
545             elif isinstance(prop, hyperdb.Number):
546                 if value is None:
547                     value = 0
548                 try:
549                     v = int(value)
550                 except ValueError:
551                     raise TypeError, "%s (%s) is not numeric" % (key, repr(value))
552                 setattr(row, key, v)
553                 changes[key] = oldvalue
554                 propvalues[key] = value
556             elif isinstance(prop, hyperdb.Boolean):
557                 if value is None:
558                     bv = 0
559                 elif value not in (0,1):
560                     raise TypeError, "%s (%s) is not boolean" % (key, repr(value))
561                 else:
562                     bv = value 
563                 setattr(row, key, bv)
564                 changes[key] = oldvalue
565                 propvalues[key] = value
567             oldnode[key] = oldvalue
569         # nothing to do?
570         if not propvalues:
571             return propvalues
572         if not propvalues.has_key('activity'):
573             row.activity = int(time.time())
574         if isnew:
575             if not row.creation:
576                 row.creation = int(time.time())
577             if not row.creator:
578                 row.creator = self.db.curuserid
579             
580         self.db.dirty = 1
581         if self.do_journal:
582             if isnew:
583                 self.db.addjournal(self.classname, nodeid, _CREATE, {})
584                 self.fireReactors('create', nodeid, None)
585             else:
586                 self.db.addjournal(self.classname, nodeid, _SET, changes)
587                 self.fireReactors('set', nodeid, oldnode)
589         return propvalues
590     
591     def retire(self, nodeid):
592         if self.db.journaltag is None:
593             raise hyperdb.DatabaseError, 'Database open read-only'
594         self.fireAuditors('retire', nodeid, None)
595         view = self.getview(1)
596         ndx = view.find(id=int(nodeid))
597         if ndx < 0:
598             raise KeyError, "nodeid %s not found" % nodeid
599         row = view[ndx]
600         oldvalues = self.uncommitted.setdefault(row.id, {})
601         oldval = oldvalues['_isdel'] = row._isdel
602         row._isdel = 1
603         if self.do_journal:
604             self.db.addjournal(self.classname, nodeid, _RETIRE, {})
605         if self.keyname:
606             iv = self.getindexview(1)
607             ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
608             if ndx > -1:
609                 iv.delete(ndx)
610         self.db.dirty = 1
611         self.fireReactors('retire', nodeid, None)
613     def history(self, nodeid):
614         if not self.do_journal:
615             raise ValueError, 'Journalling is disabled for this class'
616         return self.db.getjournal(self.classname, nodeid)
618     def setkey(self, propname):
619         if self.keyname:
620             if propname == self.keyname:
621                 return
622             raise ValueError, "%s already indexed on %s"%(self.classname,
623                 self.keyname)
624         prop = self.properties.get(propname, None)
625         if prop is None:
626             prop = self.privateprops.get(propname, None)
627         if prop is None:
628             raise KeyError, "no property %s" % propname
629         if not isinstance(prop, hyperdb.String):
630             raise TypeError, "%s is not a String" % propname
632         # first setkey for this run
633         self.keyname = propname
634         iv = self.db._db.view('_%s' % self.classname)
635         if self.db.fastopen and iv.structure():
636             return
638         # very first setkey ever
639         self.db.dirty = 1
640         iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
641         iv = iv.ordered(1)
642         for row in self.getview():
643             iv.append(k=getattr(row, propname), i=row.id)
644         self.db.commit()
646     def getkey(self):
647         return self.keyname
649     def lookup(self, keyvalue):
650         if type(keyvalue) is not _STRINGTYPE:
651             raise TypeError, "%r is not a string" % keyvalue
652         iv = self.getindexview()
653         if iv:
654             ndx = iv.find(k=keyvalue)
655             if ndx > -1:
656                 return str(iv[ndx].i)
657         else:
658             view = self.getview()
659             ndx = view.find({self.keyname:keyvalue, '_isdel':0})
660             if ndx > -1:
661                 return str(view[ndx].id)
662         raise KeyError, keyvalue
664     def destroy(self, id):
665         view = self.getview(1)
666         ndx = view.find(id=int(id))
667         if ndx > -1:
668             if self.keyname:
669                 keyvalue = getattr(view[ndx], self.keyname)
670                 iv = self.getindexview(1)
671                 if iv:
672                     ivndx = iv.find(k=keyvalue)
673                     if ivndx > -1:
674                         iv.delete(ivndx)
675             view.delete(ndx)
676             self.db.destroyjournal(self.classname, id)
677             self.db.dirty = 1
678         
679     def find(self, **propspec):
680         """Get the ids of nodes in this class which link to the given nodes.
682         'propspec' consists of keyword args propname={nodeid:1,}   
683         'propname' must be the name of a property in this class, or a
684                    KeyError is raised.  That property must be a Link or
685                    Multilink property, or a TypeError is raised.
687         Any node in this class whose propname property links to any of the
688         nodeids will be returned. Used by the full text indexing, which knows
689         that "foo" occurs in msg1, msg3 and file7; so we have hits on these
690         issues:
692             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
694         """
695         propspec = propspec.items()
696         for propname, nodeid in propspec:
697             # check the prop is OK
698             prop = self.ruprops[propname]
699             if (not isinstance(prop, hyperdb.Link) and
700                     not isinstance(prop, hyperdb.Multilink)):
701                 raise TypeError, "'%s' not a Link/Multilink property"%propname
703         vws = []
704         for propname, ids in propspec:
705             if type(ids) is _STRINGTYPE:
706                 ids = {int(ids):1}
707             else:
708                 d = {}
709                 for id in ids.keys():
710                     d[int(id)] = 1
711                 ids = d
712             prop = self.ruprops[propname]
713             view = self.getview()
714             if isinstance(prop, hyperdb.Multilink):
715                 def ff(row, nm=propname, ids=ids):
716                     sv = getattr(row, nm)
717                     for sr in sv:
718                         if ids.has_key(sr.fid):
719                             return 1
720                     return 0
721             else:
722                 def ff(row, nm=propname, ids=ids):
723                     return ids.has_key(getattr(row, nm))
724             ndxview = view.filter(ff)
725             vws.append(ndxview.unique())
727         # handle the empty match case
728         if not vws:
729             return []
731         ndxview = vws[0]
732         for v in vws[1:]:
733             ndxview = ndxview.union(v)
734         view = self.getview().remapwith(ndxview)
735         rslt = []
736         for row in view:
737             rslt.append(str(row.id))
738         return rslt
739             
741     def list(self):
742         l = []
743         for row in self.getview().select(_isdel=0):
744             l.append(str(row.id))
745         return l
747     def count(self):
748         return len(self.getview())
750     def getprops(self, protected=1):
751         # protected is not in ping's spec
752         allprops = self.ruprops.copy()
753         if protected and self.privateprops is not None:
754             allprops.update(self.privateprops)
755         return allprops
757     def addprop(self, **properties):
758         for key in properties.keys():
759             if self.ruprops.has_key(key):
760                 raise ValueError, "%s is already a property of %s"%(key,
761                     self.classname)
762         self.ruprops.update(properties)
763         self.db.fastopen = 0
764         view = self.__getview()
765         self.db.commit()
766     # ---- end of ping's spec
768     def filter(self, search_matches, filterspec, sort=(None,None),
769             group=(None,None)):
770         # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
771         # filterspec is a dict {propname:value}
772         # sort and group are (dir, prop) where dir is '+', '-' or None
773         #                    and prop is a prop name or None
774         where = {'_isdel':0}
775         mlcriteria = {}
776         regexes = {}
777         orcriteria = {}
778         for propname, value in filterspec.items():
779             prop = self.ruprops.get(propname, None)
780             if prop is None:
781                 prop = self.privateprops[propname]
782             if isinstance(prop, hyperdb.Multilink):
783                 if type(value) is not _LISTTYPE:
784                     value = [value]
785                 # transform keys to ids
786                 u = []
787                 for item in value:
788                     try:
789                         item = int(item)
790                     except (TypeError, ValueError):
791                         item = int(self.db.getclass(prop.classname).lookup(item))
792                     if item == -1:
793                         item = 0
794                     u.append(item)
795                 mlcriteria[propname] = u
796             elif isinstance(prop, hyperdb.Link):
797                 if type(value) is not _LISTTYPE:
798                     value = [value]
799                 # transform keys to ids
800                 u = []
801                 for item in value:
802                     try:
803                         item = int(item)
804                     except (TypeError, ValueError):
805                         item = int(self.db.getclass(prop.classname).lookup(item))
806                     if item == -1:
807                         item = 0
808                     u.append(item)
809                 if len(u) == 1:
810                     where[propname] = u[0]
811                 else:
812                     orcriteria[propname] = u
813             elif isinstance(prop, hyperdb.String):
814                 # simple glob searching
815                 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
816                 v = v.replace('?', '.')
817                 v = v.replace('*', '.*?')
818                 regexes[propname] = re.compile(v, re.I)
819             elif propname == 'id':
820                 where[propname] = int(value)
821             elif isinstance(prop, hyperdb.Boolean):
822                 if type(value) is _STRINGTYPE:
823                     bv = value.lower() in ('yes', 'true', 'on', '1')
824                 else:
825                     bv = value
826                 where[propname] = bv
827             elif isinstance(prop, hyperdb.Date):
828                 t = date.Date(value).get_tuple()
829                 where[propname] = int(calendar.timegm(t))
830             elif isinstance(prop, hyperdb.Interval):
831                 where[propname] = str(date.Interval(value))
832             elif isinstance(prop, hyperdb.Number):
833                 where[propname] = int(value)
834             else:
835                 where[propname] = str(value)
836         v = self.getview()
837         #print "filter start at  %s" % time.time() 
838         if where:
839             v = v.select(where)
840         #print "filter where at  %s" % time.time() 
842         if mlcriteria:
843             # multilink - if any of the nodeids required by the
844             # filterspec aren't in this node's property, then skip it
845             def ff(row, ml=mlcriteria):
846                 for propname, values in ml.items():
847                     sv = getattr(row, propname)
848                     for id in values:
849                         if sv.find(fid=id) == -1:
850                             return 0
851                 return 1
852             iv = v.filter(ff)
853             v = v.remapwith(iv)
855         #print "filter mlcrit at %s" % time.time() 
856         
857         if orcriteria:
858             def ff(row, crit=orcriteria):
859                 for propname, allowed in crit.items():
860                     val = getattr(row, propname)
861                     if val not in allowed:
862                         return 0
863                 return 1
864             
865             iv = v.filter(ff)
866             v = v.remapwith(iv)
867         
868         #print "filter orcrit at %s" % time.time() 
869         if regexes:
870             def ff(row, r=regexes):
871                 for propname, regex in r.items():
872                     val = getattr(row, propname)
873                     if not regex.search(val):
874                         return 0
875                 return 1
876             
877             iv = v.filter(ff)
878             v = v.remapwith(iv)
879         #print "filter regexs at %s" % time.time() 
880         
881         if sort or group:
882             sortspec = []
883             rev = []
884             for dir, propname in group, sort:
885                 if propname is None: continue
886                 isreversed = 0
887                 if dir == '-':
888                     isreversed = 1
889                 try:
890                     prop = getattr(v, propname)
891                 except AttributeError:
892                     print "MK has no property %s" % propname
893                     continue
894                 propclass = self.ruprops.get(propname, None)
895                 if propclass is None:
896                     propclass = self.privateprops.get(propname, None)
897                     if propclass is None:
898                         print "Schema has no property %s" % propname
899                         continue
900                 if isinstance(propclass, hyperdb.Link):
901                     linkclass = self.db.getclass(propclass.classname)
902                     lv = linkclass.getview()
903                     lv = lv.rename('id', propname)
904                     v = v.join(lv, prop, 1)
905                     if linkclass.getprops().has_key('order'):
906                         propname = 'order'
907                     else:
908                         propname = linkclass.labelprop()
909                     prop = getattr(v, propname)
910                 if isreversed:
911                     rev.append(prop)
912                 sortspec.append(prop)
913             v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
914         #print "filter sort   at %s" % time.time() 
915             
916         rslt = []
917         for row in v:
918             id = str(row.id)
919             if search_matches is not None:
920                 if search_matches.has_key(id):
921                     rslt.append(id)
922             else:
923                 rslt.append(id)
924         return rslt
925     
926     def hasnode(self, nodeid):
927         return int(nodeid) < self.maxid
928     
929     def labelprop(self, default_to_id=0):
930         ''' Return the property name for a label for the given node.
932         This method attempts to generate a consistent label for the node.
933         It tries the following in order:
934             1. key property
935             2. "name" property
936             3. "title" property
937             4. first property from the sorted property name list
938         '''
939         k = self.getkey()
940         if  k:
941             return k
942         props = self.getprops()
943         if props.has_key('name'):
944             return 'name'
945         elif props.has_key('title'):
946             return 'title'
947         if default_to_id:
948             return 'id'
949         props = props.keys()
950         props.sort()
951         return props[0]
953     def stringFind(self, **requirements):
954         """Locate a particular node by matching a set of its String
955         properties in a caseless search.
957         If the property is not a String property, a TypeError is raised.
958         
959         The return is a list of the id of all nodes that match.
960         """
961         for propname in requirements.keys():
962             prop = self.properties[propname]
963             if isinstance(not prop, hyperdb.String):
964                 raise TypeError, "'%s' not a String property"%propname
965             requirements[propname] = requirements[propname].lower()
966         requirements['_isdel'] = 0
967         
968         l = []
969         for row in self.getview().select(requirements):
970             l.append(str(row.id))
971         return l
973     def addjournal(self, nodeid, action, params):
974         self.db.addjournal(self.classname, nodeid, action, params)
976     def index(self, nodeid):
977         ''' Add (or refresh) the node to search indexes '''
978         # find all the String properties that have indexme
979         for prop, propclass in self.getprops().items():
980             if isinstance(propclass, hyperdb.String) and propclass.indexme:
981                 # index them under (classname, nodeid, property)
982                 self.db.indexer.add_text((self.classname, nodeid, prop),
983                                 str(self.get(nodeid, prop)))
985     def export_list(self, propnames, nodeid):
986         ''' Export a node - generate a list of CSV-able data in the order
987             specified by propnames for the given node.
988         '''
989         properties = self.getprops()
990         l = []
991         for prop in propnames:
992             proptype = properties[prop]
993             value = self.get(nodeid, prop)
994             # "marshal" data where needed
995             if value is None:
996                 pass
997             elif isinstance(proptype, hyperdb.Date):
998                 value = value.get_tuple()
999             elif isinstance(proptype, hyperdb.Interval):
1000                 value = value.get_tuple()
1001             elif isinstance(proptype, hyperdb.Password):
1002                 value = str(value)
1003             l.append(repr(value))
1004         return l
1005         
1006     def import_list(self, propnames, proplist):
1007         ''' Import a node - all information including "id" is present and
1008             should not be sanity checked. Triggers are not triggered. The
1009             journal should be initialised using the "creator" and "creation"
1010             information.
1012             Return the nodeid of the node imported.
1013         '''
1014         if self.db.journaltag is None:
1015             raise hyperdb.DatabaseError, 'Database open read-only'
1016         properties = self.getprops()
1018         d = {}
1019         view = self.getview(1)
1020         for i in range(len(propnames)):
1021             value = eval(proplist[i])
1022             propname = propnames[i]
1023             prop = properties[propname]
1024             if propname == 'id':
1025                 newid = value
1026                 value = int(value)
1027             elif isinstance(prop, hyperdb.Date):
1028                 value = int(calendar.timegm(value))
1029             elif isinstance(prop, hyperdb.Interval):
1030                 value = str(date.Interval(value))
1031             d[propname] = value
1032         view.append(d)
1033         creator = d.get('creator', None)
1034         creation = d.get('creation', None)
1035         self.db.addjournal(self.classname, newid, 'create', {}, creator,
1036             creation)
1037         return newid
1039     # --- used by Database
1040     def _commit(self):
1041         """ called post commit of the DB.
1042             interested subclasses may override """
1043         self.uncommitted = {}
1044         self.rbactions = []
1045         self.idcache = {}
1046     def _rollback(self):  
1047         """ called pre rollback of the DB.
1048             interested subclasses may override """
1049         for action in self.rbactions:
1050             action()
1051         self.rbactions = []
1052         self.uncommitted = {}
1053         self.idcache = {}
1054     def _clear(self):
1055         view = self.getview(1)
1056         if len(view):
1057             view[:] = []
1058             self.db.dirty = 1
1059         iv = self.getindexview(1)
1060         if iv:
1061             iv[:] = []
1062     def rollbackaction(self, action):
1063         """ call this to register a callback called on rollback
1064             callback is removed on end of transaction """
1065         self.rbactions.append(action)
1066     # --- internal
1067     def __getview(self):
1068         db = self.db._db
1069         view = db.view(self.classname)
1070         mkprops = view.structure()
1071         if mkprops and self.db.fastopen:
1072             return view.ordered(1)
1073         # is the definition the same?
1074         for nm, rutyp in self.ruprops.items():
1075             for mkprop in mkprops:
1076                 if mkprop.name == nm:
1077                     break
1078             else:
1079                 mkprop = None
1080             if mkprop is None:
1081                 break
1082             if _typmap[rutyp.__class__] != mkprop.type:
1083                 break
1084         else:
1085             return view.ordered(1)
1086         # need to create or restructure the mk view
1087         # id comes first, so MK will order it for us
1088         self.db.dirty = 1
1089         s = ["%s[id:I" % self.classname]
1090         for nm, rutyp in self.ruprops.items():
1091             mktyp = _typmap[rutyp.__class__]
1092             s.append('%s:%s' % (nm, mktyp))
1093             if mktyp == 'V':
1094                 s[-1] += ('[fid:I]')
1095         s.append('_isdel:I,activity:I,creation:I,creator:I]')
1096         v = self.db._db.getas(','.join(s))
1097         self.db.commit()
1098         return v.ordered(1)
1099     def getview(self, RW=0):
1100         return self.db._db.view(self.classname).ordered(1)
1101     def getindexview(self, RW=0):
1102         return self.db._db.view("_%s" % self.classname).ordered(1)
1103     
1104 def _fetchML(sv):
1105     l = []
1106     for row in sv:
1107         if row.fid:
1108             l.append(str(row.fid))
1109     return l
1111 def _fetchPW(s):
1112     p = password.Password()
1113     p.unpack(s)
1114     return p
1116 def _fetchLink(n):
1117     return n and str(n) or None
1119 def _fetchDate(n):
1120     return date.Date(time.gmtime(n))
1122 _converters = {
1123     hyperdb.Date   : _fetchDate,
1124     hyperdb.Link   : _fetchLink,
1125     hyperdb.Multilink : _fetchML,
1126     hyperdb.Interval  : date.Interval,
1127     hyperdb.Password  : _fetchPW,
1128     hyperdb.Boolean   : lambda n: n,
1129     hyperdb.Number    : lambda n: n,
1130     hyperdb.String    : str,
1131 }                
1133 class FileName(hyperdb.String):
1134     isfilename = 1            
1136 _typmap = {
1137     FileName : 'S',
1138     hyperdb.String : 'S',
1139     hyperdb.Date   : 'I',
1140     hyperdb.Link   : 'I',
1141     hyperdb.Multilink : 'V',
1142     hyperdb.Interval  : 'S',
1143     hyperdb.Password  : 'S',
1144     hyperdb.Boolean   : 'I',
1145     hyperdb.Number    : 'I',
1147 class FileClass(Class):
1148     ''' like Class but with a content property
1149     '''
1150     default_mime_type = 'text/plain'
1151     def __init__(self, db, classname, **properties):
1152         properties['content'] = FileName()
1153         if not properties.has_key('type'):
1154             properties['type'] = hyperdb.String()
1155         Class.__init__(self, db, classname, **properties)
1157     def get(self, nodeid, propname, default=_marker, cache=1):
1158         x = Class.get(self, nodeid, propname, default, cache)
1159         if propname == 'content':
1160             if x.startswith('file:'):
1161                 fnm = x[5:]
1162                 try:
1163                     x = open(fnm, 'rb').read()
1164                 except Exception, e:
1165                     x = repr(e)
1166         return x
1168     def create(self, **propvalues):
1169         content = propvalues['content']
1170         del propvalues['content']
1171         newid = Class.create(self, **propvalues)
1172         if not content:
1173             return newid
1174         nm = bnm = '%s%s' % (self.classname, newid)
1175         sd = str(int(int(newid) / 1000))
1176         d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1177         if not os.path.exists(d):
1178             os.makedirs(d)
1179         nm = os.path.join(d, nm)
1180         open(nm, 'wb').write(content)
1181         self.set(newid, content = 'file:'+nm)
1182         mimetype = propvalues.get('type', self.default_mime_type)
1183         self.db.indexer.add_text((self.classname, newid, 'content'), content,
1184             mimetype)
1185         def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1186             action1(fnm)
1187         self.rollbackaction(undo)
1188         return newid
1190     def index(self, nodeid):
1191         Class.index(self, nodeid)
1192         mimetype = self.get(nodeid, 'type')
1193         if not mimetype:
1194             mimetype = self.default_mime_type
1195         self.db.indexer.add_text((self.classname, nodeid, 'content'),
1196                     self.get(nodeid, 'content'), mimetype)
1197  
1198 class IssueClass(Class, roundupdb.IssueClass):
1199     ''' The newly-created class automatically includes the "messages",
1200         "files", "nosy", and "superseder" properties.  If the 'properties'
1201         dictionary attempts to specify any of these properties or a
1202         "creation" or "activity" property, a ValueError is raised.
1203     '''
1204     def __init__(self, db, classname, **properties):
1205         if not properties.has_key('title'):
1206             properties['title'] = hyperdb.String(indexme='yes')
1207         if not properties.has_key('messages'):
1208             properties['messages'] = hyperdb.Multilink("msg")
1209         if not properties.has_key('files'):
1210             properties['files'] = hyperdb.Multilink("file")
1211         if not properties.has_key('nosy'):
1212             # note: journalling is turned off as it really just wastes
1213             # space. this behaviour may be overridden in an instance
1214             properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1215         if not properties.has_key('superseder'):
1216             properties['superseder'] = hyperdb.Multilink(classname)
1217         Class.__init__(self, db, classname, **properties)
1218         
1219 CURVERSION = 2
1221 class Indexer(indexer.Indexer):
1222     disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1223     def __init__(self, path, datadb):
1224         self.path = os.path.join(path, 'index.mk4')
1225         self.db = metakit.storage(self.path, 1)
1226         self.datadb = datadb
1227         self.reindex = 0
1228         v = self.db.view('version')
1229         if not v.structure():
1230             v = self.db.getas('version[vers:I]')
1231             self.db.commit()
1232             v.append(vers=CURVERSION)
1233             self.reindex = 1
1234         elif v[0].vers != CURVERSION:
1235             v[0].vers = CURVERSION
1236             self.reindex = 1
1237         if self.reindex:
1238             self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
1239             self.db.getas('index[word:S,hits[pos:I]]')
1240             self.db.commit()
1241             self.reindex = 1
1242         self.changed = 0
1243         self.propcache = {}
1245     def force_reindex(self):
1246         v = self.db.view('ids')
1247         v[:] = []
1248         v = self.db.view('index')
1249         v[:] = []
1250         self.db.commit()
1251         self.reindex = 1
1253     def should_reindex(self):
1254         return self.reindex
1256     def _getprops(self, classname):
1257         props = self.propcache.get(classname, None)
1258         if props is None:
1259             props = self.datadb.view(classname).structure()
1260             props = [prop.name for prop in props]
1261             self.propcache[classname] = props
1262         return props
1264     def _getpropid(self, classname, propname):
1265         return self._getprops(classname).index(propname)
1267     def _getpropname(self, classname, propid):
1268         return self._getprops(classname)[propid]
1270     def add_text(self, identifier, text, mime_type='text/plain'):
1271         if mime_type != 'text/plain':
1272             return
1273         classname, nodeid, property = identifier
1274         tbls = self.datadb.view('tables')
1275         tblid = tbls.find(name=classname)
1276         if tblid < 0:
1277             raise KeyError, "unknown class %r"%classname
1278         nodeid = int(nodeid)
1279         propid = self._getpropid(classname, property)
1280         ids = self.db.view('ids')
1281         oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
1282         if oldpos > -1:
1283             ids[oldpos].ignore = 1
1284             self.changed = 1
1285         pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
1286         
1287         wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
1288         words = {}
1289         for word in wordlist:
1290             if not self.disallows.has_key(word):
1291                 words[word] = 1
1292         words = words.keys()
1293         
1294         index = self.db.view('index').ordered(1)
1295         for word in words:
1296             ndx = index.find(word=word)
1297             if ndx < 0:
1298                 index.append(word=word)
1299                 ndx = index.find(word=word)
1300             index[ndx].hits.append(pos=pos)
1301             self.changed = 1
1303     def find(self, wordlist):
1304         hits = None
1305         index = self.db.view('index').ordered(1)
1306         for word in wordlist:
1307             word = word.upper()
1308             if not 2 < len(word) < 26:
1309                 continue
1310             ndx = index.find(word=word)
1311             if ndx < 0:
1312                 return {}
1313             if hits is None:
1314                 hits = index[ndx].hits
1315             else:
1316                 hits = hits.intersect(index[ndx].hits)
1317             if len(hits) == 0:
1318                 return {}
1319         if hits is None:
1320             return {}
1321         rslt = {}
1322         ids = self.db.view('ids').remapwith(hits)
1323         tbls = self.datadb.view('tables')
1324         for i in range(len(ids)):
1325             hit = ids[i]
1326             if not hit.ignore:
1327                 classname = tbls[hit.tblid].name
1328                 nodeid = str(hit.nodeid)
1329                 property = self._getpropname(classname, hit.propid)
1330                 rslt[i] = (classname, nodeid, property)
1331         return rslt
1333     def save_index(self):
1334         if self.changed:
1335             self.db.commit()
1336         self.changed = 0
1338     def rollback(self):
1339         if self.changed:
1340             self.db.rollback()
1341             self.db = metakit.storage(self.path, 1)
1342         self.changed = 0