ea76be7dd458e5d735766245ec452615430591d5
1 # $Id: test_memorydb.py,v 1.4 2004-11-03 01:34:21 richard Exp $
2 '''Implement an in-memory hyperdb for testing purposes.
3 '''
5 import shutil
7 from roundup import hyperdb
8 from roundup import roundupdb
9 from roundup import security
10 from roundup import password
11 from roundup import configuration
12 from roundup.backends import back_anydbm
13 from roundup.backends import indexer_dbm
14 from roundup.backends import indexer_common
15 from roundup.hyperdb import *
17 def new_config():
18 config = configuration.CoreConfig()
19 config.DATABASE = "db"
20 #config.logging = MockNull()
21 # these TRACKER_WEB and MAIL_DOMAIN values are used in mailgw tests
22 config.MAIL_DOMAIN = "your.tracker.email.domain.example"
23 config.TRACKER_WEB = "http://tracker.example/cgi-bin/roundup.cgi/bugs/"
24 return config
26 def create(journaltag, create=True):
27 db = Database(new_config(), journaltag)
29 # load standard schema
30 schema = os.path.join(os.path.dirname(__file__),
31 '../share/roundup/templates/classic/schema.py')
32 vars = dict(globals())
33 vars['db'] = db
34 execfile(schema, vars)
35 initial_data = os.path.join(os.path.dirname(__file__),
36 '../share/roundup/templates/classic/initial_data.py')
37 vars = dict(db=db, admin_email='admin@test.com',
38 adminpw=password.Password('sekrit'))
39 execfile(initial_data, vars)
41 # load standard detectors
42 dirname = os.path.join(os.path.dirname(__file__),
43 '../share/roundup/templates/classic/detectors')
44 for fn in os.listdir(dirname):
45 if not fn.endswith('.py'): continue
46 vars = {}
47 execfile(os.path.join(dirname, fn), vars)
48 vars['init'](db)
50 '''
51 status = Class(db, "status", name=String())
52 status.setkey("name")
53 priority = Class(db, "priority", name=String(), order=String())
54 priority.setkey("name")
55 keyword = Class(db, "keyword", name=String(), order=String())
56 keyword.setkey("name")
57 user = Class(db, "user", username=String(), password=Password(),
58 assignable=Boolean(), age=Number(), roles=String(), address=String(),
59 supervisor=Link('user'),realname=String(),alternate_addresses=String())
60 user.setkey("username")
61 file = FileClass(db, "file", name=String(), type=String(),
62 comment=String(indexme="yes"), fooz=Password())
63 file_nidx = FileClass(db, "file_nidx", content=String(indexme='no'))
64 issue = IssueClass(db, "issue", title=String(indexme="yes"),
65 status=Link("status"), nosy=Multilink("user"), deadline=Date(),
66 foo=Interval(), files=Multilink("file"), assignedto=Link('user'),
67 priority=Link('priority'), spam=Multilink('msg'),
68 feedback=Link('msg'))
69 stuff = Class(db, "stuff", stuff=String())
70 session = Class(db, 'session', title=String())
71 msg = FileClass(db, "msg", date=Date(),
72 author=Link("user", do_journal='no'),
73 files=Multilink('file'), inreplyto=String(),
74 messageid=String(), summary=String(),
75 content=String(),
76 recipients=Multilink("user", do_journal='no')
77 )
78 '''
79 if create:
80 db.user.create(username="fred", roles='User',
81 password=password.Password('sekrit'), address='fred@example.com')
83 db.security.addPermissionToRole('User', 'Email Access')
84 '''
85 db.security.addPermission(name='Register', klass='user')
86 db.security.addPermissionToRole('User', 'Web Access')
87 db.security.addPermissionToRole('Anonymous', 'Email Access')
88 db.security.addPermissionToRole('Anonymous', 'Register', 'user')
89 for cl in 'issue', 'file', 'msg', 'keyword':
90 db.security.addPermissionToRole('User', 'View', cl)
91 db.security.addPermissionToRole('User', 'Edit', cl)
92 db.security.addPermissionToRole('User', 'Create', cl)
93 for cl in 'priority', 'status':
94 db.security.addPermissionToRole('User', 'View', cl)
95 '''
96 return db
98 class cldb(dict):
99 def close(self):
100 pass
102 class BasicDatabase(dict):
103 ''' Provide a nice encapsulation of an anydbm store.
105 Keys are id strings, values are automatically marshalled data.
106 '''
107 def __getitem__(self, key):
108 if key not in self:
109 d = self[key] = {}
110 return d
111 return super(BasicDatabase, self).__getitem__(key)
112 def exists(self, infoid):
113 return infoid in self
114 def get(self, infoid, value, default=None):
115 return self[infoid].get(value, default)
116 def getall(self, infoid):
117 return self[infoid]
118 def set(self, infoid, **newvalues):
119 self[infoid].update(newvalues)
120 def list(self):
121 return self.keys()
122 def destroy(self, infoid):
123 del self[infoid]
124 def commit(self):
125 pass
126 def close(self):
127 pass
128 def updateTimestamp(self, sessid):
129 pass
130 def clean(self):
131 pass
133 class Sessions(BasicDatabase):
134 name = 'sessions'
136 class OneTimeKeys(BasicDatabase):
137 name = 'otks'
139 class Indexer(indexer_dbm.Indexer):
140 def __init__(self, db):
141 indexer_common.Indexer.__init__(self, db)
142 self.reindex = 0
143 self.quiet = 9
144 self.changed = 0
146 def load_index(self, reload=0, wordlist=None):
147 # Unless reload is indicated, do not load twice
148 if self.index_loaded() and not reload:
149 return 0
150 self.words = {}
151 self.files = {'_TOP':(0,None)}
152 self.fileids = {}
153 self.changed = 0
155 def save_index(self):
156 pass
158 class Database(hyperdb.Database, roundupdb.Database):
159 """A database for storing records containing flexible data types.
161 Transaction stuff TODO:
163 - check the timestamp of the class file and nuke the cache if it's
164 modified. Do some sort of conflict checking on the dirty stuff.
165 - perhaps detect write collisions (related to above)?
166 """
167 def __init__(self, config, journaltag=None):
168 self.config, self.journaltag = config, journaltag
169 self.classes = {}
170 self.items = {}
171 self.ids = {}
172 self.journals = {}
173 self.files = {}
174 self.security = security.Security(self)
175 self.stats = {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0,
176 'filtering': 0}
177 self.sessions = Sessions()
178 self.otks = OneTimeKeys()
179 self.indexer = Indexer(self)
182 def filename(self, classname, nodeid, property=None, create=0):
183 shutil.copyfile(__file__, __file__+'.dummy')
184 return __file__+'.dummy'
186 def filesize(self, classname, nodeid, property=None, create=0):
187 return len(self.getnode(classname, nodeid)[property or 'content'])
189 def post_init(self):
190 pass
192 def refresh_database(self):
193 pass
195 def getSessionManager(self):
196 return self.sessions
198 def getOTKManager(self):
199 return self.otks
201 def reindex(self, classname=None, show_progress=False):
202 pass
204 def __repr__(self):
205 return '<memorydb instance at %x>'%id(self)
207 def storefile(self, classname, nodeid, property, content):
208 self.files[classname, nodeid, property] = content
210 def getfile(self, classname, nodeid, property):
211 return self.files[classname, nodeid, property]
213 def numfiles(self):
214 return len(self.files)
216 #
217 # Classes
218 #
219 def __getattr__(self, classname):
220 """A convenient way of calling self.getclass(classname)."""
221 if self.classes.has_key(classname):
222 return self.classes[classname]
223 raise AttributeError, classname
225 def addclass(self, cl):
226 cn = cl.classname
227 if self.classes.has_key(cn):
228 raise ValueError, cn
229 self.classes[cn] = cl
230 self.items[cn] = cldb()
231 self.ids[cn] = 0
233 # add default Edit and View permissions
234 self.security.addPermission(name="Create", klass=cn,
235 description="User is allowed to create "+cn)
236 self.security.addPermission(name="Edit", klass=cn,
237 description="User is allowed to edit "+cn)
238 self.security.addPermission(name="View", klass=cn,
239 description="User is allowed to access "+cn)
241 def getclasses(self):
242 """Return a list of the names of all existing classes."""
243 l = self.classes.keys()
244 l.sort()
245 return l
247 def getclass(self, classname):
248 """Get the Class object representing a particular class.
250 If 'classname' is not a valid class name, a KeyError is raised.
251 """
252 try:
253 return self.classes[classname]
254 except KeyError:
255 raise KeyError, 'There is no class called "%s"'%classname
257 #
258 # Class DBs
259 #
260 def clear(self):
261 self.items = {}
263 def getclassdb(self, classname):
264 """ grab a connection to the class db that will be used for
265 multiple actions
266 """
267 return self.items[classname]
269 #
270 # Node IDs
271 #
272 def newid(self, classname):
273 self.ids[classname] += 1
274 return str(self.ids[classname])
275 def setid(self, classname, id):
276 self.ids[classname] = id
278 #
279 # Nodes
280 #
281 def addnode(self, classname, nodeid, node):
282 self.getclassdb(classname)[nodeid] = node
284 def setnode(self, classname, nodeid, node):
285 self.getclassdb(classname)[nodeid] = node
287 def getnode(self, classname, nodeid, db=None):
288 if db is not None:
289 return db[nodeid]
290 d = self.getclassdb(classname)
291 if nodeid not in d:
292 raise IndexError(nodeid)
293 return d[nodeid]
295 def destroynode(self, classname, nodeid):
296 del self.getclassdb(classname)[nodeid]
298 def hasnode(self, classname, nodeid):
299 return nodeid in self.getclassdb(classname)
301 def countnodes(self, classname, db=None):
302 return len(self.getclassdb(classname))
304 #
305 # Journal
306 #
307 def addjournal(self, classname, nodeid, action, params, creator=None,
308 creation=None):
309 if creator is None:
310 creator = self.getuid()
311 if creation is None:
312 creation = date.Date()
313 self.journals.setdefault(classname, {}).setdefault(nodeid,
314 []).append((nodeid, creation, creator, action, params))
316 def setjournal(self, classname, nodeid, journal):
317 self.journals.setdefault(classname, {})[nodeid] = journal
319 def getjournal(self, classname, nodeid):
320 return self.journals.get(classname, {}).get(nodeid, [])
322 def pack(self, pack_before):
323 TODO
325 #
326 # Basic transaction support
327 #
328 def commit(self, fail_ok=False):
329 pass
331 def rollback(self):
332 TODO
334 def close(self):
335 pass
337 class Class(back_anydbm.Class):
338 def getnodeids(self, db=None, retired=None):
339 d = self.db.getclassdb(self.classname)
340 if retired is None:
341 return d.keys()
342 return [k for k in d if d[k].get(self.db.RETIRED_FLAG, False) == retired]
344 class FileClass(back_anydbm.Class):
345 def __init__(self, db, classname, **properties):
346 if not properties.has_key('content'):
347 properties['content'] = hyperdb.String(indexme='yes')
348 if not properties.has_key('type'):
349 properties['type'] = hyperdb.String()
350 back_anydbm.Class.__init__(self, db, classname, **properties)
352 def getnodeids(self, db=None, retired=None):
353 return self.db.getclassdb(self.classname).keys()
355 # deviation from spec - was called ItemClass
356 class IssueClass(Class, roundupdb.IssueClass):
357 # Overridden methods:
358 def __init__(self, db, classname, **properties):
359 """The newly-created class automatically includes the "messages",
360 "files", "nosy", and "superseder" properties. If the 'properties'
361 dictionary attempts to specify any of these properties or a
362 "creation" or "activity" property, a ValueError is raised.
363 """
364 if not properties.has_key('title'):
365 properties['title'] = hyperdb.String(indexme='yes')
366 if not properties.has_key('messages'):
367 properties['messages'] = hyperdb.Multilink("msg")
368 if not properties.has_key('files'):
369 properties['files'] = hyperdb.Multilink("file")
370 if not properties.has_key('nosy'):
371 # note: journalling is turned off as it really just wastes
372 # space. this behaviour may be overridden in an instance
373 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
374 if not properties.has_key('superseder'):
375 properties['superseder'] = hyperdb.Multilink(classname)
376 Class.__init__(self, db, classname, **properties)
378 # vim: set et sts=4 sw=4 :