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