2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
18 """This module defines a backend that saves the hyperdatabase in a
19 database chosen by anydbm. It is guaranteed to always be available in python
20 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
21 serious bugs, and is not available)
23 __docformat__
= 'restructuredtext'
25 import os
, marshal
, re
, weakref
, string
, copy
, time
, shutil
, logging
27 from roundup
.anypy
.dbm_
import anydbm
, whichdb
29 from roundup
import hyperdb
, date
, password
, roundupdb
, security
, support
30 from roundup
.support
import reversed
31 from roundup
.backends
import locking
32 from roundup
.i18n
import _
34 from roundup
.backends
.blobfiles
import FileStorage
35 from roundup
.backends
.sessions_dbm
import Sessions
, OneTimeKeys
38 from roundup
.backends
.indexer_xapian
import Indexer
40 from roundup
.backends
.indexer_dbm
import Indexer
42 def db_exists(config
):
43 # check for the user db
44 for db
in 'nodes.user nodes.user.db'.split():
45 if os
.path
.exists(os
.path
.join(config
.DATABASE
, db
)):
50 shutil
.rmtree(config
.DATABASE
)
55 class Database(FileStorage
, hyperdb
.Database
, roundupdb
.Database
):
56 """A database for storing records containing flexible data types.
58 Transaction stuff TODO:
60 - check the timestamp of the class file and nuke the cache if it's
61 modified. Do some sort of conflict checking on the dirty stuff.
62 - perhaps detect write collisions (related to above)?
64 def __init__(self
, config
, journaltag
=None):
65 """Open a hyperdatabase given a specifier to some storage.
67 The 'storagelocator' is obtained from config.DATABASE.
68 The meaning of 'storagelocator' depends on the particular
69 implementation of the hyperdatabase. It could be a file name,
70 a directory path, a socket descriptor for a connection to a
71 database over the network, etc.
73 The 'journaltag' is a token that will be attached to the journal
74 entries for any edits done on the database. If 'journaltag' is
75 None, the database is opened in read-only mode: the Class.create(),
76 Class.set(), Class.retire(), and Class.restore() methods are
79 FileStorage
.__init__(self
, config
.UMASK
)
80 self
.config
, self
.journaltag
= config
, journaltag
81 self
.dir = config
.DATABASE
83 self
.cache
= {} # cache of nodes loaded or created
84 self
.stats
= {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0,
86 self
.dirtynodes
= {} # keep track of the dirty nodes by class
87 self
.newnodes
= {} # keep track of the new nodes by class
88 self
.destroyednodes
= {}# keep track of the destroyed nodes by class
89 self
.transactions
= []
90 self
.indexer
= Indexer(self
)
91 self
.security
= security
.Security(self
)
92 os
.umask(config
.UMASK
)
95 lockfilenm
= os
.path
.join(self
.dir, 'lock')
96 self
.lockfile
= locking
.acquire_lock(lockfilenm
)
97 self
.lockfile
.write(str(os
.getpid()))
101 """Called once the schema initialisation has finished.
103 # reindex the db if necessary
104 if self
.indexer
.should_reindex():
107 def refresh_database(self
):
108 """Rebuild the database
112 def getSessionManager(self
):
113 return Sessions(self
)
115 def getOTKManager(self
):
116 return OneTimeKeys(self
)
118 def reindex(self
, classname
=None, show_progress
=False):
120 classes
= [self
.getclass(classname
)]
122 classes
= self
.classes
.values()
123 for klass
in classes
:
125 for nodeid
in support
.Progress('Reindex %s'%klass
.classname
,
129 for nodeid
in klass
.list():
131 self
.indexer
.save_index()
134 return '<back_anydbm instance at %x>'%id(self
)
139 def __getattr__(self
, classname
):
140 """A convenient way of calling self.getclass(classname)."""
141 if classname
in self
.classes
:
142 return self
.classes
[classname
]
143 raise AttributeError, classname
145 def addclass(self
, cl
):
147 if cn
in self
.classes
:
149 self
.classes
[cn
] = cl
151 # add default Edit and View permissions
152 self
.security
.addPermission(name
="Create", klass
=cn
,
153 description
="User is allowed to create "+cn
)
154 self
.security
.addPermission(name
="Edit", klass
=cn
,
155 description
="User is allowed to edit "+cn
)
156 self
.security
.addPermission(name
="View", klass
=cn
,
157 description
="User is allowed to access "+cn
)
159 def getclasses(self
):
160 """Return a list of the names of all existing classes."""
161 return sorted(self
.classes
)
163 def getclass(self
, classname
):
164 """Get the Class object representing a particular class.
166 If 'classname' is not a valid class name, a KeyError is raised.
169 return self
.classes
[classname
]
171 raise KeyError('There is no class called "%s"'%classname
)
177 """Delete all database contents
179 logging
.getLogger('hyperdb').info('clear')
180 for cn
in self
.classes
:
181 for dummy
in 'nodes', 'journals':
182 path
= os
.path
.join(self
.dir, 'journals.%s'%cn
)
183 if os
.path
.exists(path
):
185 elif os
.path
.exists(path
+'.db'): # dbm appends .db
186 os
.remove(path
+'.db')
188 path
= os
.path
.join(os
.getcwd(), self
.dir, '_ids')
189 if os
.path
.exists(path
):
191 elif os
.path
.exists(path
+'.db'): # dbm appends .db
192 os
.remove(path
+'.db')
194 def getclassdb(self
, classname
, mode
='r'):
195 """ grab a connection to the class db that will be used for
198 return self
.opendb('nodes.%s'%classname
, mode
)
200 def determine_db_type(self
, path
):
201 """ determine which DB wrote the class file
204 if os
.path
.exists(path
):
205 db_type
= whichdb(path
)
207 raise hyperdb
.DatabaseError(_("Couldn't identify database type"))
208 elif os
.path
.exists(path
+'.db'):
209 # if the path ends in '.db', it's a dbm database, whether
210 # anydbm says it's dbhash or not!
214 def opendb(self
, name
, mode
):
215 """Low-level database opener that gets around anydbm/dbm
218 # figure the class db type
219 path
= os
.path
.join(os
.getcwd(), self
.dir, name
)
220 db_type
= self
.determine_db_type(path
)
222 # new database? let anydbm pick the best dbm
223 # in Python 3+ the "dbm" ("anydbm" to us) module already uses the
224 # whichdb() function to do this
225 if not db_type
or hasattr(anydbm
, 'whichdb'):
227 logging
.getLogger('hyperdb').debug(
228 "opendb anydbm.open(%r, 'c')"%path
)
229 return anydbm
.open(path
, 'c')
231 # in Python <3 it anydbm was a little dumb so manually open the
232 # database with the correct module
234 dbm
= __import__(db_type
)
236 raise hyperdb
.DatabaseError(_("Couldn't open database - the "
237 "required module '%s' is not available")%db_type
)
239 logging
.getLogger('hyperdb').debug(
240 "opendb %r.open(%r, %r)"%(db_type
, path
, mode
))
241 return dbm
.open(path
, mode
)
246 def newid(self
, classname
):
247 """ Generate a new id for the given class
249 # open the ids DB - create if if doesn't exist
250 db
= self
.opendb('_ids', 'c')
252 newid
= db
[classname
] = str(int(db
[classname
]) + 1)
254 # the count() bit is transitional - older dbs won't start at 1
255 newid
= str(self
.getclass(classname
).count()+1)
256 db
[classname
] = newid
260 def setid(self
, classname
, setid
):
261 """ Set the id counter: used during import of database
263 # open the ids DB - create if if doesn't exist
264 db
= self
.opendb('_ids', 'c')
265 db
[classname
] = str(setid
)
271 def addnode(self
, classname
, nodeid
, node
):
272 """ add the specified node to its class's db
274 # we'll be supplied these props if we're doing an import
275 if 'creator' not in node
:
276 # add in the "calculated" properties (dupe so we don't affect
277 # calling code's node assumptions)
279 node
['creator'] = self
.getuid()
280 node
['actor'] = self
.getuid()
281 node
['creation'] = node
['activity'] = date
.Date()
283 self
.newnodes
.setdefault(classname
, {})[nodeid
] = 1
284 self
.cache
.setdefault(classname
, {})[nodeid
] = node
285 self
.savenode(classname
, nodeid
, node
)
287 def setnode(self
, classname
, nodeid
, node
):
288 """ change the specified node
290 self
.dirtynodes
.setdefault(classname
, {})[nodeid
] = 1
292 # can't set without having already loaded the node
293 self
.cache
[classname
][nodeid
] = node
294 self
.savenode(classname
, nodeid
, node
)
296 def savenode(self
, classname
, nodeid
, node
):
297 """ perform the saving of data specified by the set/addnode
300 logging
.getLogger('hyperdb').debug('save %s%s %r'%(classname
, nodeid
, node
))
301 self
.transactions
.append((self
.doSaveNode
, (classname
, nodeid
, node
)))
303 def getnode(self
, classname
, nodeid
, db
=None, cache
=1):
304 """ get a node from the database
306 Note the "cache" parameter is not used, and exists purely for
307 backward compatibility!
310 cache_dict
= self
.cache
.setdefault(classname
, {})
311 if nodeid
in cache_dict
:
313 logging
.getLogger('hyperdb').debug('get %s%s cached'%(classname
, nodeid
))
314 self
.stats
['cache_hits'] += 1
315 return cache_dict
[nodeid
]
318 self
.stats
['cache_misses'] += 1
319 start_t
= time
.time()
320 logging
.getLogger('hyperdb').debug('get %s%s'%(classname
, nodeid
))
322 # get from the database and save in the cache
324 db
= self
.getclassdb(classname
)
326 raise IndexError("no such %s %s"%(classname
, nodeid
))
328 # check the uncommitted, destroyed nodes
329 if (classname
in self
.destroyednodes
and
330 nodeid
in self
.destroyednodes
[classname
]):
331 raise IndexError("no such %s %s"%(classname
, nodeid
))
334 res
= marshal
.loads(db
[nodeid
])
336 # reverse the serialisation
337 res
= self
.unserialise(classname
, res
)
339 # store off in the cache dict
341 cache_dict
[nodeid
] = res
344 self
.stats
['get_items'] += (time
.time() - start_t
)
348 def destroynode(self
, classname
, nodeid
):
349 """Remove a node from the database. Called exclusively by the
350 destroy() method on Class.
352 logging
.getLogger('hyperdb').info('destroy %s%s'%(classname
, nodeid
))
354 # remove from cache and newnodes if it's there
355 if (classname
in self
.cache
and nodeid
in self
.cache
[classname
]):
356 del self
.cache
[classname
][nodeid
]
357 if (classname
in self
.newnodes
and nodeid
in self
.newnodes
[classname
]):
358 del self
.newnodes
[classname
][nodeid
]
360 # see if there's any obvious commit actions that we should get rid of
361 for entry
in self
.transactions
[:]:
362 if entry
[1][:2] == (classname
, nodeid
):
363 self
.transactions
.remove(entry
)
365 # add to the destroyednodes map
366 self
.destroyednodes
.setdefault(classname
, {})[nodeid
] = 1
368 # add the destroy commit action
369 self
.transactions
.append((self
.doDestroyNode
, (classname
, nodeid
)))
370 self
.transactions
.append((FileStorage
.destroy
, (self
, classname
, nodeid
)))
372 def serialise(self
, classname
, node
):
373 """Copy the node contents, converting non-marshallable data into
376 properties
= self
.getclass(classname
).getprops()
378 for k
, v
in node
.iteritems():
379 if k
== self
.RETIRED_FLAG
:
383 # if the property doesn't exist then we really don't care
384 if k
not in properties
:
387 # get the property spec
390 if isinstance(prop
, hyperdb
.Password
) and v
is not None:
392 elif isinstance(prop
, hyperdb
.Date
) and v
is not None:
394 elif isinstance(prop
, hyperdb
.Interval
) and v
is not None:
400 def unserialise(self
, classname
, node
):
401 """Decode the marshalled node data
403 properties
= self
.getclass(classname
).getprops()
405 for k
, v
in node
.iteritems():
406 # if the property doesn't exist, or is the "retired" flag then
407 # it won't be in the properties dict
408 if k
not in properties
:
412 # get the property spec
415 if isinstance(prop
, hyperdb
.Date
) and v
is not None:
417 elif isinstance(prop
, hyperdb
.Interval
) and v
is not None:
418 d
[k
] = date
.Interval(v
)
419 elif isinstance(prop
, hyperdb
.Password
) and v
is not None:
420 p
= password
.Password()
427 def hasnode(self
, classname
, nodeid
, db
=None):
428 """ determine if the database has a given node
431 cache
= self
.cache
.setdefault(classname
, {})
435 # not in the cache - check the database
437 db
= self
.getclassdb(classname
)
440 def countnodes(self
, classname
, db
=None):
443 # include the uncommitted nodes
444 if classname
in self
.newnodes
:
445 count
+= len(self
.newnodes
[classname
])
446 if classname
in self
.destroyednodes
:
447 count
-= len(self
.destroyednodes
[classname
])
449 # and count those in the DB
451 db
= self
.getclassdb(classname
)
452 return count
+ len(db
)
456 # Files - special node properties
457 # inherited from FileStorage
462 def addjournal(self
, classname
, nodeid
, action
, params
, creator
=None,
464 """ Journal the Action
467 'create' or 'set' -- 'params' is a dictionary of property values
468 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
469 'retire' -- 'params' is None
471 'creator' -- the user performing the action, which defaults to
475 logging
.getLogger('hyperdb').debug('addjournal %s%s %s %r %s %r'%(classname
,
476 nodeid
, action
, params
, creator
, creation
))
478 creator
= self
.getuid()
479 self
.transactions
.append((self
.doSaveJournal
, (classname
, nodeid
,
480 action
, params
, creator
, creation
)))
482 def setjournal(self
, classname
, nodeid
, journal
):
483 """Set the journal to the "journal" list."""
485 logging
.getLogger('hyperdb').debug('setjournal %s%s %r'%(classname
,
487 self
.transactions
.append((self
.doSetJournal
, (classname
, nodeid
,
490 def getjournal(self
, classname
, nodeid
):
491 """ get the journal for id
493 Raise IndexError if the node doesn't exist (as per history()'s
499 # add any journal entries for transactions not committed to the
501 for method
, args
in self
.transactions
:
502 if method
!= self
.doSaveJournal
:
504 (cache_classname
, cache_nodeid
, cache_action
, cache_params
,
505 cache_creator
, cache_creation
) = args
506 if cache_classname
== classname
and cache_nodeid
== nodeid
:
507 if not cache_creator
:
508 cache_creator
= self
.getuid()
509 if not cache_creation
:
510 cache_creation
= date
.Date()
511 res
.append((cache_nodeid
, cache_creation
, cache_creator
,
512 cache_action
, cache_params
))
514 # attempt to open the journal - in some rare cases, the journal may
517 db
= self
.opendb('journals.%s'%classname
, 'r')
518 except anydbm
.error
, error
:
519 if str(error
) == "need 'c' or 'n' flag to open new db":
520 raise IndexError('no such %s %s'%(classname
, nodeid
))
521 elif error
.args
[0] != 2:
522 # this isn't a "not found" error, be alarmed!
525 # we have unsaved journal entries, return them
527 raise IndexError('no such %s %s'%(classname
, nodeid
))
529 journal
= marshal
.loads(db
[nodeid
])
533 # we have some unsaved journal entries, be happy!
535 raise IndexError('no such %s %s'%(classname
, nodeid
))
538 # add all the saved journal entries for this node
539 for nodeid
, date_stamp
, user
, action
, params
in journal
:
540 res
.append((nodeid
, date
.Date(date_stamp
), user
, action
, params
))
543 def pack(self
, pack_before
):
544 """ Delete all journal entries except "create" before 'pack_before'.
546 pack_before
= pack_before
.serialise()
547 for classname
in self
.getclasses():
550 db_name
= 'journals.%s'%classname
551 path
= os
.path
.join(os
.getcwd(), self
.dir, classname
)
552 db_type
= self
.determine_db_type(path
)
553 db
= self
.opendb(db_name
, 'w')
556 # get the journal for this db entry
557 journal
= marshal
.loads(db
[key
])
559 last_set_entry
= None
560 for entry
in journal
:
562 (nodeid
, date_stamp
, self
.journaltag
, action
,
564 # if the entry is after the pack date, _or_ the initial
565 # create entry, then it stays
566 if date_stamp
> pack_before
or action
== 'create':
570 db
[key
] = marshal
.dumps(l
)
572 logging
.getLogger('hyperdb').info('packed %d %s items'%(packed
,
575 if db_type
== 'gdbm':
581 # Basic transaction support
583 def commit(self
, fail_ok
=False):
584 """ Commit the current transactions.
586 Save all data changed since the database was opened or since the
587 last commit() or rollback().
589 fail_ok indicates that the commit is allowed to fail. This is used
590 in the web interface when committing cleaning of the session
591 database. We don't care if there's a concurrency issue there.
593 The only backend this seems to affect is postgres.
595 logging
.getLogger('hyperdb').info('commit %s transactions'%(
596 len(self
.transactions
)))
598 # keep a handle to all the database files opened
602 # now, do all the transactions
604 for method
, args
in self
.transactions
:
605 reindex
[method(*args
)] = 1
607 # make sure we close all the database files
608 for db
in self
.databases
.itervalues():
612 # clear the transactions list now so the blobfile implementation
613 # doesn't think there's still pending file commits when it tries
614 # to access the file data
615 self
.transactions
= []
617 # reindex the nodes that request it
618 for classname
, nodeid
in [k
for k
in reindex
if k
]:
619 self
.getclass(classname
).index(nodeid
)
621 # save the indexer state
622 self
.indexer
.save_index()
626 def clearCache(self
):
627 # all transactions committed, back to normal
631 self
.destroyednodes
= {}
632 self
.transactions
= []
634 def getCachedClassDB(self
, classname
):
635 """ get the class db, looking in our cache of databases for commit
637 # get the database handle
638 db_name
= 'nodes.%s'%classname
639 if db_name
not in self
.databases
:
640 self
.databases
[db_name
] = self
.getclassdb(classname
, 'c')
641 return self
.databases
[db_name
]
643 def doSaveNode(self
, classname
, nodeid
, node
):
644 db
= self
.getCachedClassDB(classname
)
646 # now save the marshalled data
647 db
[nodeid
] = marshal
.dumps(self
.serialise(classname
, node
))
649 # return the classname, nodeid so we reindex this content
650 return (classname
, nodeid
)
652 def getCachedJournalDB(self
, classname
):
653 """ get the journal db, looking in our cache of databases for commit
655 # get the database handle
656 db_name
= 'journals.%s'%classname
657 if db_name
not in self
.databases
:
658 self
.databases
[db_name
] = self
.opendb(db_name
, 'c')
659 return self
.databases
[db_name
]
661 def doSaveJournal(self
, classname
, nodeid
, action
, params
, creator
,
663 # serialise the parameters now if necessary
664 if isinstance(params
, type({})):
665 if action
in ('set', 'create'):
666 params
= self
.serialise(classname
, params
)
668 # handle supply of the special journalling parameters (usually
669 # supplied on importing an existing database)
672 journaldate
= creation
.serialise()
674 journaldate
= date
.Date().serialise()
676 # create the journal entry
677 entry
= (nodeid
, journaldate
, journaltag
, action
, params
)
679 db
= self
.getCachedJournalDB(classname
)
681 # now insert the journal entry
690 db
[nodeid
] = marshal
.dumps(l
)
692 def doSetJournal(self
, classname
, nodeid
, journal
):
694 for nodeid
, journaldate
, journaltag
, action
, params
in journal
:
695 # serialise the parameters now if necessary
696 if isinstance(params
, type({})):
697 if action
in ('set', 'create'):
698 params
= self
.serialise(classname
, params
)
699 journaldate
= journaldate
.serialise()
700 l
.append((nodeid
, journaldate
, journaltag
, action
, params
))
701 db
= self
.getCachedJournalDB(classname
)
702 db
[nodeid
] = marshal
.dumps(l
)
704 def doDestroyNode(self
, classname
, nodeid
):
705 # delete from the class database
706 db
= self
.getCachedClassDB(classname
)
710 # delete from the database
711 db
= self
.getCachedJournalDB(classname
)
716 """ Reverse all actions from the current transaction.
718 logging
.getLogger('hyperdb').info('rollback %s transactions'%(
719 len(self
.transactions
)))
721 for method
, args
in self
.transactions
:
722 # delete temporary files
723 if method
== self
.doStoreFile
:
724 self
.rollbackStoreFile(*args
)
728 self
.destroyednodes
= {}
729 self
.transactions
= []
734 if self
.lockfile
is not None:
735 locking
.release_lock(self
.lockfile
)
736 self
.lockfile
.close()
740 class Class(hyperdb
.Class
):
741 """The handle to a particular class of nodes in a hyperdatabase."""
743 def enableJournalling(self
):
744 """Turn journalling on for this class
748 def disableJournalling(self
):
749 """Turn journalling off for this class
755 def create(self
, **propvalues
):
756 """Create a new node of this class and return its id.
758 The keyword arguments in 'propvalues' map property names to values.
760 The values of arguments must be acceptable for the types of their
761 corresponding properties or a TypeError is raised.
763 If this class has a key property, it must be present and its value
764 must not collide with other key strings or a ValueError is raised.
766 Any other properties on this class that are missing from the
767 'propvalues' dictionary are set to None.
769 If an id in a link or multilink property does not refer to a valid
770 node, an IndexError is raised.
772 These operations trigger detectors and can be vetoed. Attempts
773 to modify the "creation" or "activity" properties cause a KeyError.
775 if self
.db
.journaltag
is None:
776 raise hyperdb
.DatabaseError(_('Database open read-only'))
777 self
.fireAuditors('create', None, propvalues
)
778 newid
= self
.create_inner(**propvalues
)
779 self
.fireReactors('create', newid
, None)
782 def create_inner(self
, **propvalues
):
783 """ Called by create, in-between the audit and react calls.
785 if 'id' in propvalues
:
786 raise KeyError('"id" is reserved')
788 if self
.db
.journaltag
is None:
789 raise hyperdb
.DatabaseError(_('Database open read-only'))
791 if 'creation' in propvalues
or 'activity' in propvalues
:
792 raise KeyError('"creation" and "activity" are reserved')
794 newid
= self
.db
.newid(self
.classname
)
796 # validate propvalues
797 num_re
= re
.compile('^\d+$')
798 for key
, value
in propvalues
.iteritems():
805 raise ValueError('node with key "%s" exists'%value
)
807 # try to handle this property
809 prop
= self
.properties
[key
]
811 raise KeyError('"%s" has no property "%s"'%(self
.classname
,
814 if value
is not None and isinstance(prop
, hyperdb
.Link
):
815 if type(value
) != type(''):
816 raise ValueError('link value must be String')
817 link_class
= self
.properties
[key
].classname
818 # if it isn't a number, it's a key
819 if not num_re
.match(value
):
821 value
= self
.db
.classes
[link_class
].lookup(value
)
822 except (TypeError, KeyError):
823 raise IndexError('new property "%s": %s not a %s'%(
824 key
, value
, link_class
))
825 elif not self
.db
.getclass(link_class
).hasnode(value
):
826 raise IndexError('%s has no node %s'%(link_class
,
830 propvalues
[key
] = value
832 # register the link with the newly linked node
833 if self
.do_journal
and self
.properties
[key
].do_journal
:
834 self
.db
.addjournal(link_class
, value
, 'link',
835 (self
.classname
, newid
, key
))
837 elif isinstance(prop
, hyperdb
.Multilink
):
840 if not hasattr(value
, '__iter__'):
841 raise TypeError('new property "%s" not an iterable of ids'%key
)
843 # clean up and validate the list of links
844 link_class
= self
.properties
[key
].classname
847 if type(entry
) != type(''):
848 raise ValueError('"%s" multilink value (%r) '\
849 'must contain Strings'%(key
, value
))
850 # if it isn't a number, it's a key
851 if not num_re
.match(entry
):
853 entry
= self
.db
.classes
[link_class
].lookup(entry
)
854 except (TypeError, KeyError):
855 raise IndexError('new property "%s": %s not a %s'%(
856 key
, entry
, self
.properties
[key
].classname
))
859 propvalues
[key
] = value
863 if not self
.db
.getclass(link_class
).hasnode(nodeid
):
864 raise IndexError('%s has no node %s'%(link_class
,
866 # register the link with the newly linked node
867 if self
.do_journal
and self
.properties
[key
].do_journal
:
868 self
.db
.addjournal(link_class
, nodeid
, 'link',
869 (self
.classname
, newid
, key
))
871 elif isinstance(prop
, hyperdb
.String
):
872 if type(value
) != type('') and type(value
) != type(u
''):
873 raise TypeError('new property "%s" not a string'%key
)
875 self
.db
.indexer
.add_text((self
.classname
, newid
, key
),
878 elif isinstance(prop
, hyperdb
.Password
):
879 if not isinstance(value
, password
.Password
):
880 raise TypeError('new property "%s" not a Password'%key
)
882 elif isinstance(prop
, hyperdb
.Date
):
883 if value
is not None and not isinstance(value
, date
.Date
):
884 raise TypeError('new property "%s" not a Date'%key
)
886 elif isinstance(prop
, hyperdb
.Interval
):
887 if value
is not None and not isinstance(value
, date
.Interval
):
888 raise TypeError('new property "%s" not an Interval'%key
)
890 elif value
is not None and isinstance(prop
, hyperdb
.Number
):
894 raise TypeError('new property "%s" not numeric'%key
)
896 elif value
is not None and isinstance(prop
, hyperdb
.Boolean
):
900 raise TypeError('new property "%s" not boolean'%key
)
902 # make sure there's data where there needs to be
903 for key
, prop
in self
.properties
.iteritems():
904 if key
in propvalues
:
907 raise ValueError('key property "%s" is required'%key
)
908 if isinstance(prop
, hyperdb
.Multilink
):
912 self
.db
.addnode(self
.classname
, newid
, propvalues
)
914 self
.db
.addjournal(self
.classname
, newid
, 'create', {})
918 def get(self
, nodeid
, propname
, default
=_marker
, cache
=1):
919 """Get the value of a property on an existing node of this class.
921 'nodeid' must be the id of an existing node of this class or an
922 IndexError is raised. 'propname' must be the name of a property
923 of this class or a KeyError is raised.
925 'cache' exists for backward compatibility, and is not used.
927 Attempts to get the "creation" or "activity" properties should
933 # get the node's dict
934 d
= self
.db
.getnode(self
.classname
, nodeid
)
936 # check for one of the special props
937 if propname
== 'creation':
940 if not self
.do_journal
:
941 raise ValueError('Journalling is disabled for this class')
942 journal
= self
.db
.getjournal(self
.classname
, nodeid
)
946 # on the strange chance that there's no journal
948 if propname
== 'activity':
951 if not self
.do_journal
:
952 raise ValueError('Journalling is disabled for this class')
953 journal
= self
.db
.getjournal(self
.classname
, nodeid
)
955 return self
.db
.getjournal(self
.classname
, nodeid
)[-1][1]
957 # on the strange chance that there's no journal
959 if propname
== 'creator':
962 if not self
.do_journal
:
963 raise ValueError('Journalling is disabled for this class')
964 journal
= self
.db
.getjournal(self
.classname
, nodeid
)
966 num_re
= re
.compile('^\d+$')
967 value
= journal
[0][2]
968 if num_re
.match(value
):
971 # old-style "username" journal tag
973 return self
.db
.user
.lookup(value
)
975 # user's been retired, return admin
978 return self
.db
.getuid()
979 if propname
== 'actor':
982 if not self
.do_journal
:
983 raise ValueError('Journalling is disabled for this class')
984 journal
= self
.db
.getjournal(self
.classname
, nodeid
)
986 num_re
= re
.compile('^\d+$')
987 value
= journal
[-1][2]
988 if num_re
.match(value
):
991 # old-style "username" journal tag
993 return self
.db
.user
.lookup(value
)
995 # user's been retired, return admin
998 return self
.db
.getuid()
1000 # get the property (raises KeyErorr if invalid)
1001 prop
= self
.properties
[propname
]
1003 if propname
not in d
:
1004 if default
is _marker
:
1005 if isinstance(prop
, hyperdb
.Multilink
):
1012 # return a dupe of the list so code doesn't get confused
1013 if isinstance(prop
, hyperdb
.Multilink
):
1014 return d
[propname
][:]
1018 def set(self
, nodeid
, **propvalues
):
1019 """Modify a property on an existing node of this class.
1021 'nodeid' must be the id of an existing node of this class or an
1022 IndexError is raised.
1024 Each key in 'propvalues' must be the name of a property of this
1025 class or a KeyError is raised.
1027 All values in 'propvalues' must be acceptable types for their
1028 corresponding properties or a TypeError is raised.
1030 If the value of the key property is set, it must not collide with
1031 other key strings or a ValueError is raised.
1033 If the value of a Link or Multilink property contains an invalid
1034 node id, a ValueError is raised.
1036 These operations trigger detectors and can be vetoed. Attempts
1037 to modify the "creation" or "activity" properties cause a KeyError.
1039 if self
.db
.journaltag
is None:
1040 raise hyperdb
.DatabaseError(_('Database open read-only'))
1042 self
.fireAuditors('set', nodeid
, propvalues
)
1043 oldvalues
= copy
.deepcopy(self
.db
.getnode(self
.classname
, nodeid
))
1044 for name
, prop
in self
.getprops(protected
=0).iteritems():
1045 if name
in oldvalues
:
1047 if isinstance(prop
, hyperdb
.Multilink
):
1048 oldvalues
[name
] = []
1050 oldvalues
[name
] = None
1051 propvalues
= self
.set_inner(nodeid
, **propvalues
)
1052 self
.fireReactors('set', nodeid
, oldvalues
)
1055 def set_inner(self
, nodeid
, **propvalues
):
1056 """ Called by set, in-between the audit and react calls.
1061 if 'creation' in propvalues
or 'activity' in propvalues
:
1062 raise KeyError, '"creation" and "activity" are reserved'
1064 if 'id' in propvalues
:
1065 raise KeyError, '"id" is reserved'
1067 if self
.db
.journaltag
is None:
1068 raise hyperdb
.DatabaseError(_('Database open read-only'))
1070 node
= self
.db
.getnode(self
.classname
, nodeid
)
1071 if self
.db
.RETIRED_FLAG
in node
:
1073 num_re
= re
.compile('^\d+$')
1075 # if the journal value is to be different, store it in here
1078 # list() propvalues 'cos it might be modified by the loop
1079 for propname
, value
in list(propvalues
.items()):
1080 # check to make sure we're not duplicating an existing key
1081 if propname
== self
.key
and node
[propname
] != value
:
1087 raise ValueError('node with key "%s" exists'%value
)
1089 # this will raise the KeyError if the property isn't valid
1090 # ... we don't use getprops() here because we only care about
1091 # the writeable properties.
1093 prop
= self
.properties
[propname
]
1095 raise KeyError('"%s" has no property named "%s"'%(
1096 self
.classname
, propname
))
1098 # if the value's the same as the existing value, no sense in
1100 current
= node
.get(propname
, None)
1101 if value
== current
:
1102 del propvalues
[propname
]
1104 journalvalues
[propname
] = current
1106 # do stuff based on the prop type
1107 if isinstance(prop
, hyperdb
.Link
):
1108 link_class
= prop
.classname
1109 # if it isn't a number, it's a key
1110 if value
is not None and not isinstance(value
, type('')):
1111 raise ValueError('property "%s" link value be a string'%(
1113 if isinstance(value
, type('')) and not num_re
.match(value
):
1115 value
= self
.db
.classes
[link_class
].lookup(value
)
1116 except (TypeError, KeyError):
1117 raise IndexError('new property "%s": %s not a %s'%(
1118 propname
, value
, prop
.classname
))
1120 if (value
is not None and
1121 not self
.db
.getclass(link_class
).hasnode(value
)):
1122 raise IndexError('%s has no node %s'%(link_class
,
1125 if self
.do_journal
and prop
.do_journal
:
1126 # register the unlink with the old linked node
1127 if propname
in node
and node
[propname
] is not None:
1128 self
.db
.addjournal(link_class
, node
[propname
], 'unlink',
1129 (self
.classname
, nodeid
, propname
))
1131 # register the link with the newly linked node
1132 if value
is not None:
1133 self
.db
.addjournal(link_class
, value
, 'link',
1134 (self
.classname
, nodeid
, propname
))
1136 elif isinstance(prop
, hyperdb
.Multilink
):
1139 if not hasattr(value
, '__iter__'):
1140 raise TypeError('new property "%s" not an iterable of'
1142 link_class
= self
.properties
[propname
].classname
1145 # if it isn't a number, it's a key
1146 if type(entry
) != type(''):
1147 raise ValueError('new property "%s" link value '
1148 'must be a string'%propname
)
1149 if not num_re
.match(entry
):
1151 entry
= self
.db
.classes
[link_class
].lookup(entry
)
1152 except (TypeError, KeyError):
1153 raise IndexError('new property "%s": %s not a %s'%(
1155 self
.properties
[propname
].classname
))
1158 propvalues
[propname
] = value
1160 # figure the journal entry for this property
1165 if propname
in node
:
1172 # register the unlink with the old linked node
1173 if self
.do_journal
and self
.properties
[propname
].do_journal
:
1174 self
.db
.addjournal(link_class
, id, 'unlink',
1175 (self
.classname
, nodeid
, propname
))
1181 if not self
.db
.getclass(link_class
).hasnode(id):
1182 raise IndexError('%s has no node %s'%(link_class
,
1186 # register the link with the newly linked node
1187 if self
.do_journal
and self
.properties
[propname
].do_journal
:
1188 self
.db
.addjournal(link_class
, id, 'link',
1189 (self
.classname
, nodeid
, propname
))
1193 # figure the journal entry
1196 l
.append(('+', add
))
1198 l
.append(('-', remove
))
1200 journalvalues
[propname
] = tuple(l
)
1202 elif isinstance(prop
, hyperdb
.String
):
1203 if value
is not None and type(value
) != type('') and type(value
) != type(u
''):
1204 raise TypeError('new property "%s" not a '
1207 self
.db
.indexer
.add_text((self
.classname
, nodeid
, propname
),
1210 elif isinstance(prop
, hyperdb
.Password
):
1211 if not isinstance(value
, password
.Password
):
1212 raise TypeError('new property "%s" not a '
1213 'Password'%propname
)
1214 propvalues
[propname
] = value
1216 elif value
is not None and isinstance(prop
, hyperdb
.Date
):
1217 if not isinstance(value
, date
.Date
):
1218 raise TypeError('new property "%s" not a '
1220 propvalues
[propname
] = value
1222 elif value
is not None and isinstance(prop
, hyperdb
.Interval
):
1223 if not isinstance(value
, date
.Interval
):
1224 raise TypeError('new property "%s" not an '
1225 'Interval'%propname
)
1226 propvalues
[propname
] = value
1228 elif value
is not None and isinstance(prop
, hyperdb
.Number
):
1232 raise TypeError('new property "%s" not '
1235 elif value
is not None and isinstance(prop
, hyperdb
.Boolean
):
1239 raise TypeError('new property "%s" not '
1242 node
[propname
] = value
1248 # update the activity time
1249 node
['activity'] = date
.Date()
1250 node
['actor'] = self
.db
.getuid()
1252 # do the set, and journal it
1253 self
.db
.setnode(self
.classname
, nodeid
, node
)
1256 self
.db
.addjournal(self
.classname
, nodeid
, 'set', journalvalues
)
1260 def retire(self
, nodeid
):
1263 The properties on the node remain available from the get() method,
1264 and the node's id is never reused.
1266 Retired nodes are not returned by the find(), list(), or lookup()
1267 methods, and other nodes may reuse the values of their key properties.
1269 These operations trigger detectors and can be vetoed. Attempts
1270 to modify the "creation" or "activity" properties cause a KeyError.
1272 if self
.db
.journaltag
is None:
1273 raise hyperdb
.DatabaseError(_('Database open read-only'))
1275 self
.fireAuditors('retire', nodeid
, None)
1277 node
= self
.db
.getnode(self
.classname
, nodeid
)
1278 node
[self
.db
.RETIRED_FLAG
] = 1
1279 self
.db
.setnode(self
.classname
, nodeid
, node
)
1281 self
.db
.addjournal(self
.classname
, nodeid
, 'retired', None)
1283 self
.fireReactors('retire', nodeid
, None)
1285 def restore(self
, nodeid
):
1286 """Restpre a retired node.
1288 Make node available for all operations like it was before retirement.
1290 if self
.db
.journaltag
is None:
1291 raise hyperdb
.DatabaseError(_('Database open read-only'))
1293 node
= self
.db
.getnode(self
.classname
, nodeid
)
1294 # check if key property was overrided
1297 id = self
.lookup(node
[key
])
1301 raise KeyError("Key property (%s) of retired node clashes "
1302 "with existing one (%s)" % (key
, node
[key
]))
1303 # Now we can safely restore node
1304 self
.fireAuditors('restore', nodeid
, None)
1305 del node
[self
.db
.RETIRED_FLAG
]
1306 self
.db
.setnode(self
.classname
, nodeid
, node
)
1308 self
.db
.addjournal(self
.classname
, nodeid
, 'restored', None)
1310 self
.fireReactors('restore', nodeid
, None)
1312 def is_retired(self
, nodeid
, cldb
=None):
1313 """Return true if the node is retired.
1315 node
= self
.db
.getnode(self
.classname
, nodeid
, cldb
)
1316 if self
.db
.RETIRED_FLAG
in node
:
1320 def destroy(self
, nodeid
):
1323 WARNING: this method should never be used except in extremely rare
1324 situations where there could never be links to the node being
1327 WARNING: use retire() instead
1329 WARNING: the properties of this node will not be available ever again
1331 WARNING: really, use retire() instead
1333 Well, I think that's enough warnings. This method exists mostly to
1334 support the session storage of the cgi interface.
1336 if self
.db
.journaltag
is None:
1337 raise hyperdb
.DatabaseError(_('Database open read-only'))
1338 self
.db
.destroynode(self
.classname
, nodeid
)
1340 def history(self
, nodeid
):
1341 """Retrieve the journal of edits on a particular node.
1343 'nodeid' must be the id of an existing node of this class or an
1344 IndexError is raised.
1346 The returned list contains tuples of the form
1348 (nodeid, date, tag, action, params)
1350 'date' is a Timestamp object specifying the time of the change and
1351 'tag' is the journaltag specified when the database was opened.
1353 if not self
.do_journal
:
1354 raise ValueError('Journalling is disabled for this class')
1355 return self
.db
.getjournal(self
.classname
, nodeid
)
1358 def hasnode(self
, nodeid
):
1359 """Determine if the given nodeid actually exists
1361 return self
.db
.hasnode(self
.classname
, nodeid
)
1363 def setkey(self
, propname
):
1364 """Select a String property of this class to be the key property.
1366 'propname' must be the name of a String property of this class or
1367 None, or a TypeError is raised. The values of the key property on
1368 all existing nodes must be unique or a ValueError is raised. If the
1369 property doesn't exist, KeyError is raised.
1371 prop
= self
.getprops()[propname
]
1372 if not isinstance(prop
, hyperdb
.String
):
1373 raise TypeError('key properties must be String')
1377 """Return the name of the key property for this class or None."""
1380 # TODO: set up a separate index db file for this? profile?
1381 def lookup(self
, keyvalue
):
1382 """Locate a particular node by its key property and return its id.
1384 If this class has no key property, a TypeError is raised. If the
1385 'keyvalue' matches one of the values for the key property among
1386 the nodes in this class, the matching node's id is returned;
1387 otherwise a KeyError is raised.
1390 raise TypeError('No key property set for '
1391 'class %s'%self
.classname
)
1392 cldb
= self
.db
.getclassdb(self
.classname
)
1394 for nodeid
in self
.getnodeids(cldb
):
1395 node
= self
.db
.getnode(self
.classname
, nodeid
, cldb
)
1396 if self
.db
.RETIRED_FLAG
in node
:
1398 if self
.key
not in node
:
1400 if node
[self
.key
] == keyvalue
:
1404 raise KeyError('No key (%s) value "%s" for "%s"'%(self
.key
,
1405 keyvalue
, self
.classname
))
1407 # change from spec - allows multiple props to match
1408 def find(self
, **propspec
):
1409 """Get the ids of nodes in this class which link to the given nodes.
1411 'propspec' consists of keyword args propname=nodeid or
1412 propname={nodeid:1, }
1413 'propname' must be the name of a property in this class, or a
1414 KeyError is raised. That property must be a Link or
1415 Multilink property, or a TypeError is raised.
1417 Any node in this class whose 'propname' property links to any of
1418 the nodeids will be returned. Examples::
1420 db.issue.find(messages='1')
1421 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1423 for propname
, itemids
in propspec
.iteritems():
1424 # check the prop is OK
1425 prop
= self
.properties
[propname
]
1426 if not isinstance(prop
, hyperdb
.Link
) and not isinstance(prop
, hyperdb
.Multilink
):
1427 raise TypeError("'%s' not a Link/Multilink "
1428 "property"%propname
)
1430 # ok, now do the find
1431 cldb
= self
.db
.getclassdb(self
.classname
)
1434 for id in self
.getnodeids(db
=cldb
):
1435 item
= self
.db
.getnode(self
.classname
, id, db
=cldb
)
1436 if self
.db
.RETIRED_FLAG
in item
:
1438 for propname
, itemids
in propspec
.iteritems():
1439 if type(itemids
) is not type({}):
1440 itemids
= {itemids
:1}
1442 # special case if the item doesn't have this property
1443 if propname
not in item
:
1449 # grab the property definition and its value on this item
1450 prop
= self
.properties
[propname
]
1451 value
= item
[propname
]
1452 if isinstance(prop
, hyperdb
.Link
) and value
in itemids
:
1455 elif isinstance(prop
, hyperdb
.Multilink
):
1468 def stringFind(self
, **requirements
):
1469 """Locate a particular node by matching a set of its String
1470 properties in a caseless search.
1472 If the property is not a String property, a TypeError is raised.
1474 The return is a list of the id of all nodes that match.
1476 for propname
in requirements
:
1477 prop
= self
.properties
[propname
]
1478 if not isinstance(prop
, hyperdb
.String
):
1479 raise TypeError("'%s' not a String property"%propname
)
1480 requirements
[propname
] = requirements
[propname
].lower()
1482 cldb
= self
.db
.getclassdb(self
.classname
)
1484 for nodeid
in self
.getnodeids(cldb
):
1485 node
= self
.db
.getnode(self
.classname
, nodeid
, cldb
)
1486 if self
.db
.RETIRED_FLAG
in node
:
1488 for key
, value
in requirements
.iteritems():
1491 if node
[key
] is None or node
[key
].lower() != value
:
1500 """ Return a list of the ids of the active nodes in this class.
1504 cldb
= self
.db
.getclassdb(cn
)
1506 for nodeid
in self
.getnodeids(cldb
):
1507 node
= self
.db
.getnode(cn
, nodeid
, cldb
)
1508 if self
.db
.RETIRED_FLAG
in node
:
1516 def getnodeids(self
, db
=None, retired
=None):
1517 """ Return a list of ALL nodeids
1519 Set retired=None to get all nodes. Otherwise it'll get all the
1520 retired or non-retired nodes, depending on the flag.
1524 # start off with the new nodes
1525 if self
.classname
in self
.db
.newnodes
:
1526 res
.extend(self
.db
.newnodes
[self
.classname
])
1530 db
= self
.db
.getclassdb(self
.classname
)
1535 # remove the uncommitted, destroyed nodes
1536 if self
.classname
in self
.db
.destroyednodes
:
1537 for nodeid
in self
.db
.destroyednodes
[self
.classname
]:
1541 # check retired flag
1542 if retired
is False or retired
is True:
1545 node
= self
.db
.getnode(self
.classname
, nodeid
, db
)
1546 is_ret
= self
.db
.RETIRED_FLAG
in node
1547 if retired
== is_ret
:
1555 def _filter(self
, search_matches
, filterspec
, proptree
,
1556 num_re
= re
.compile('^\d+$')):
1557 """Return a list of the ids of the active nodes in this class that
1558 match the 'filter' spec, sorted by the group spec and then the
1561 "filterspec" is {propname: value(s)}
1563 "sort" and "group" are (dir, prop) where dir is '+', '-' or None
1564 and prop is a prop name or None
1566 "search_matches" is a sequence type or None
1568 The filter must match all properties specificed. If the property
1569 value to match is a list:
1571 1. String properties must match all elements in the list, and
1572 2. Other properties must match any of the elements in the list.
1575 start_t
= time
.time()
1579 # optimise filterspec
1581 props
= self
.getprops()
1583 MULTILINK
= 'spec:multilink'
1584 STRING
= 'spec:string'
1586 INTERVAL
= 'spec:interval'
1587 OTHER
= 'spec:other'
1589 for k
, v
in filterspec
.iteritems():
1590 propclass
= props
[k
]
1591 if isinstance(propclass
, hyperdb
.Link
):
1592 if type(v
) is not type([]):
1596 # the value -1 is a special "not set" sentinel
1600 l
.append((LINK
, k
, u
))
1601 elif isinstance(propclass
, hyperdb
.Multilink
):
1602 # the value -1 is a special "not set" sentinel
1603 if v
in ('-1', ['-1']):
1605 elif type(v
) is not type([]):
1607 l
.append((MULTILINK
, k
, v
))
1608 elif isinstance(propclass
, hyperdb
.String
) and k
!= 'id':
1609 if type(v
) is not type([]):
1612 # simple glob searching
1613 v
= re
.sub(r
'([\|\{\}\\\.\+\[\]\(\)])', r
'\\\1', v
)
1614 v
= v
.replace('?', '.')
1615 v
= v
.replace('*', '.*?')
1616 l
.append((STRING
, k
, re
.compile(v
, re
.I
)))
1617 elif isinstance(propclass
, hyperdb
.Date
):
1619 date_rng
= propclass
.range_from_raw(v
, self
.db
)
1620 l
.append((DATE
, k
, date_rng
))
1622 # If range creation fails - ignore that search parameter
1624 elif isinstance(propclass
, hyperdb
.Interval
):
1626 intv_rng
= date
.Range(v
, date
.Interval
)
1627 l
.append((INTERVAL
, k
, intv_rng
))
1629 # If range creation fails - ignore that search parameter
1632 elif isinstance(propclass
, hyperdb
.Boolean
):
1633 if type(v
) == type(""):
1635 if type(v
) != type([]):
1639 if type(val
) is type(''):
1640 bv
.append(propclass
.from_raw (val
))
1643 l
.append((OTHER
, k
, bv
))
1646 if type(v
) != type([]):
1648 l
.append((OTHER
, k
, [str(int(val
)) for val
in v
]))
1650 elif isinstance(propclass
, hyperdb
.Number
):
1651 if type(v
) != type([]):
1654 except AttributeError :
1656 l
.append((OTHER
, k
, [float(val
) for val
in v
]))
1660 # now, find all the nodes that are active and pass filtering
1662 cldb
= self
.db
.getclassdb(cn
)
1665 # TODO: only full-scan once (use items())
1666 for nodeid
in self
.getnodeids(cldb
):
1667 node
= self
.db
.getnode(cn
, nodeid
, cldb
)
1668 if self
.db
.RETIRED_FLAG
in node
:
1671 for t
, k
, v
in filterspec
:
1672 # handle the id prop
1678 # get the node value
1679 nv
= node
.get(k
, None)
1683 # now apply the property filter
1685 # link - if this node's property doesn't appear in the
1686 # filterspec's nodeid list, skip it
1688 elif t
== MULTILINK
:
1689 # multilink - if any of the nodeids required by the
1690 # filterspec aren't in this node's property, then skip
1692 nv
= node
.get(k
, [])
1694 # check for matching the absence of multilink values
1698 # othewise, make sure this node has each of the
1708 match
= v
.search(nv
)
1709 elif t
== DATE
or t
== INTERVAL
:
1714 if v
.from_value
<= nv
and v
.to_value
>= nv
:
1717 if v
.from_value
<= nv
:
1720 # straight value comparison for the other types
1725 matches
.append([nodeid
, node
])
1727 # filter based on full text search
1728 if search_matches
is not None:
1731 if v
[0] in search_matches
:
1735 # add sorting information to the proptree
1736 JPROPS
= {'actor':1, 'activity':1, 'creator':1, 'creation':1}
1739 children
= proptree
.sortable_children()
1741 dir = pt
.sort_direction
1743 assert (dir and prop
)
1744 propclass
= props
[prop
]
1746 is_pointer
= isinstance(propclass
,(hyperdb
.Link
,
1751 # cache the opened link class db, if needed.
1753 # cache the linked class items too
1756 for entry
in matches
:
1759 # handle the properties that might be "faked"
1760 # also, handle possible missing properties
1765 # force lookup of the special journal prop
1766 v
= self
.get(itemid
, prop
)
1768 # the node doesn't have a value for this
1771 if isinstance(propclass
, hyperdb
.Multilink
):
1775 pt
.sort_ids
.append(v
)
1777 pt
.sort_result
.append(v
)
1780 # missing (None) values are always sorted first
1782 pt
.sort_ids
.append(v
)
1784 pt
.sort_result
.append(v
)
1787 if isinstance(propclass
, hyperdb
.Link
):
1788 lcn
= propclass
.classname
1789 link
= self
.db
.classes
[lcn
]
1790 key
= link
.orderprop()
1791 child
= pt
.propdict
[key
]
1794 # open the link class db if it's not already
1796 lcldb
= self
.db
.getclassdb(lcn
)
1797 lcache
[v
] = self
.db
.getnode(lcn
, v
, lcldb
)
1799 child
.propdict
[key
].sort_ids
.append(r
)
1801 child
.propdict
[key
].sort_ids
.append(v
)
1802 pt
.sort_ids
.append(v
)
1804 r
= propclass
.sort_repr(pt
.parent
.cls
, v
, pt
.name
)
1805 pt
.sort_result
.append(r
)
1807 # if we opened the link class db, close it now
1808 if lcldb
is not None:
1814 # pull the id out of the individual entries
1815 matches
= [entry
[-2] for entry
in matches
]
1817 self
.db
.stats
['filtering'] += (time
.time() - start_t
)
1821 """Get the number of nodes in this class.
1823 If the returned integer is 'numnodes', the ids of all the nodes
1824 in this class run from 1 to numnodes, and numnodes+1 will be the
1825 id of the next node to be created in this class.
1827 return self
.db
.countnodes(self
.classname
)
1829 # Manipulating properties:
1831 def getprops(self
, protected
=1):
1832 """Return a dictionary mapping property names to property objects.
1833 If the "protected" flag is true, we include protected properties -
1834 those which may not be modified.
1836 In addition to the actual properties on the node, these
1837 methods provide the "creation" and "activity" properties. If the
1838 "protected" flag is true, we include protected properties - those
1839 which may not be modified.
1841 d
= self
.properties
.copy()
1843 d
['id'] = hyperdb
.String()
1844 d
['creation'] = hyperdb
.Date()
1845 d
['activity'] = hyperdb
.Date()
1846 d
['creator'] = hyperdb
.Link('user')
1847 d
['actor'] = hyperdb
.Link('user')
1850 def addprop(self
, **properties
):
1851 """Add properties to this class.
1853 The keyword arguments in 'properties' must map names to property
1854 objects, or a TypeError is raised. None of the keys in 'properties'
1855 may collide with the names of existing properties, or a ValueError
1856 is raised before any properties have been added.
1858 for key
in properties
:
1859 if key
in self
.properties
:
1860 raise ValueError(key
)
1861 self
.properties
.update(properties
)
1863 def index(self
, nodeid
):
1864 """ Add (or refresh) the node to search indexes """
1865 # find all the String properties that have indexme
1866 for prop
, propclass
in self
.getprops().iteritems():
1867 if isinstance(propclass
, hyperdb
.String
) and propclass
.indexme
:
1868 # index them under (classname, nodeid, property)
1870 value
= str(self
.get(nodeid
, prop
))
1872 # node has been destroyed
1874 self
.db
.indexer
.add_text((self
.classname
, nodeid
, prop
), value
)
1877 # import / export support
1879 def export_list(self
, propnames
, nodeid
):
1880 """ Export a node - generate a list of CSV-able data in the order
1881 specified by propnames for the given node.
1883 properties
= self
.getprops()
1885 for prop
in propnames
:
1886 proptype
= properties
[prop
]
1887 value
= self
.get(nodeid
, prop
)
1888 # "marshal" data where needed
1891 elif isinstance(proptype
, hyperdb
.Date
):
1892 value
= value
.get_tuple()
1893 elif isinstance(proptype
, hyperdb
.Interval
):
1894 value
= value
.get_tuple()
1895 elif isinstance(proptype
, hyperdb
.Password
):
1897 l
.append(repr(value
))
1899 # append retired flag
1900 l
.append(repr(self
.is_retired(nodeid
)))
1904 def import_list(self
, propnames
, proplist
):
1905 """ Import a node - all information including "id" is present and
1906 should not be sanity checked. Triggers are not triggered. The
1907 journal should be initialised using the "creator" and "created"
1910 Return the nodeid of the node imported.
1912 if self
.db
.journaltag
is None:
1913 raise hyperdb
.DatabaseError(_('Database open read-only'))
1914 properties
= self
.getprops()
1916 # make the new node's property map
1919 for i
in range(len(propnames
)):
1920 # Figure the property for this column
1921 propname
= propnames
[i
]
1923 # Use eval to reverse the repr() used to output the CSV
1924 value
= eval(proplist
[i
])
1926 # "unmarshal" where necessary
1927 if propname
== 'id':
1930 elif propname
== 'is retired':
1931 # is the item retired?
1933 d
[self
.db
.RETIRED_FLAG
] = 1
1939 prop
= properties
[propname
]
1940 if isinstance(prop
, hyperdb
.Date
):
1941 value
= date
.Date(value
)
1942 elif isinstance(prop
, hyperdb
.Interval
):
1943 value
= date
.Interval(value
)
1944 elif isinstance(prop
, hyperdb
.Password
):
1945 pwd
= password
.Password()
1950 # get a new id if necessary
1952 newid
= self
.db
.newid(self
.classname
)
1954 # add the node and journal
1955 self
.db
.addnode(self
.classname
, newid
, d
)
1958 def export_journals(self
):
1959 """Export a class's journal - generate a list of lists of
1962 nodeid, date, user, action, params
1964 No heading here - the columns are fixed.
1966 properties
= self
.getprops()
1968 for nodeid
in self
.getnodeids():
1969 for nodeid
, date
, user
, action
, params
in self
.history(nodeid
):
1970 date
= date
.get_tuple()
1973 for propname
, value
in params
.iteritems():
1974 if propname
not in properties
:
1975 # property no longer in the schema
1978 prop
= properties
[propname
]
1979 # make sure the params are eval()'able
1982 elif isinstance(prop
, hyperdb
.Date
):
1983 # this is a hack - some dates are stored as strings
1984 if not isinstance(value
, type('')):
1985 value
= value
.get_tuple()
1986 elif isinstance(prop
, hyperdb
.Interval
):
1987 # hack too - some intervals are stored as strings
1988 if not isinstance(value
, type('')):
1989 value
= value
.get_tuple()
1990 elif isinstance(prop
, hyperdb
.Password
):
1992 export_data
[propname
] = value
1993 params
= export_data
1994 r
.append([repr(nodeid
), repr(date
), repr(user
),
1995 repr(action
), repr(params
)])
1998 def import_journals(self
, entries
):
1999 """Import a class's journal.
2001 Uses setjournal() to set the journal for each item."""
2002 properties
= self
.getprops()
2005 nodeid
, jdate
, user
, action
, params
= tuple(map(eval, l
))
2006 r
= d
.setdefault(nodeid
, [])
2008 for propname
, value
in params
.iteritems():
2009 prop
= properties
[propname
]
2012 elif isinstance(prop
, hyperdb
.Date
):
2013 value
= date
.Date(value
)
2014 elif isinstance(prop
, hyperdb
.Interval
):
2015 value
= date
.Interval(value
)
2016 elif isinstance(prop
, hyperdb
.Password
):
2017 pwd
= password
.Password()
2020 params
[propname
] = value
2021 r
.append((nodeid
, date
.Date(jdate
), user
, action
, params
))
2023 for nodeid
, l
in d
.iteritems():
2024 self
.db
.setjournal(self
.classname
, nodeid
, l
)
2026 class FileClass(hyperdb
.FileClass
, Class
):
2027 """This class defines a large chunk of data. To support this, it has a
2028 mandatory String property "content" which is typically saved off
2029 externally to the hyperdb.
2031 The default MIME type of this data is defined by the
2032 "default_mime_type" class attribute, which may be overridden by each
2033 node if the class defines a "type" String property.
2035 def __init__(self
, db
, classname
, **properties
):
2036 """The newly-created class automatically includes the "content"
2037 and "type" properties.
2039 if 'content' not in properties
:
2040 properties
['content'] = hyperdb
.String(indexme
='yes')
2041 if 'type' not in properties
:
2042 properties
['type'] = hyperdb
.String()
2043 Class
.__init__(self
, db
, classname
, **properties
)
2045 def create(self
, **propvalues
):
2046 """ Snarf the "content" propvalue and store in a file
2048 # we need to fire the auditors now, or the content property won't
2049 # be in propvalues for the auditors to play with
2050 self
.fireAuditors('create', None, propvalues
)
2052 # now remove the content property so it's not stored in the db
2053 content
= propvalues
['content']
2054 del propvalues
['content']
2056 # make sure we have a MIME type
2057 mime_type
= propvalues
.get('type', self
.default_mime_type
)
2059 # do the database create
2060 newid
= self
.create_inner(**propvalues
)
2062 # store off the content as a file
2063 self
.db
.storefile(self
.classname
, newid
, None, content
)
2066 self
.fireReactors('create', newid
, None)
2070 def get(self
, nodeid
, propname
, default
=_marker
, cache
=1):
2071 """ Trap the content propname and get it from the file
2073 'cache' exists for backwards compatibility, and is not used.
2075 poss_msg
= 'Possibly an access right configuration problem.'
2076 if propname
== 'content':
2078 return self
.db
.getfile(self
.classname
, nodeid
, None)
2079 except IOError, strerror
:
2080 # XXX by catching this we don't see an error in the log.
2081 return 'ERROR reading file: %s%s\n%s\n%s'%(
2082 self
.classname
, nodeid
, poss_msg
, strerror
)
2083 if default
is not _marker
:
2084 return Class
.get(self
, nodeid
, propname
, default
)
2086 return Class
.get(self
, nodeid
, propname
)
2088 def set(self
, itemid
, **propvalues
):
2089 """ Snarf the "content" propvalue and update it in a file
2091 self
.fireAuditors('set', itemid
, propvalues
)
2093 # create the oldvalues dict - fill in any missing values
2094 oldvalues
= copy
.deepcopy(self
.db
.getnode(self
.classname
, itemid
))
2095 for name
, prop
in self
.getprops(protected
=0).iteritems():
2096 if name
in oldvalues
:
2098 if isinstance(prop
, hyperdb
.Multilink
):
2099 oldvalues
[name
] = []
2101 oldvalues
[name
] = None
2103 # now remove the content property so it's not stored in the db
2105 if 'content' in propvalues
:
2106 content
= propvalues
['content']
2107 del propvalues
['content']
2109 # do the database update
2110 propvalues
= self
.set_inner(itemid
, **propvalues
)
2114 # store and possibly index
2115 self
.db
.storefile(self
.classname
, itemid
, None, content
)
2116 if self
.properties
['content'].indexme
:
2117 mime_type
= self
.get(itemid
, 'type', self
.default_mime_type
)
2118 self
.db
.indexer
.add_text((self
.classname
, itemid
, 'content'),
2120 propvalues
['content'] = content
2123 self
.fireReactors('set', itemid
, oldvalues
)
2126 def index(self
, nodeid
):
2127 """ Add (or refresh) the node to search indexes.
2129 Use the content-type property for the content property.
2131 # find all the String properties that have indexme
2132 for prop
, propclass
in self
.getprops().iteritems():
2133 if prop
== 'content' and propclass
.indexme
:
2134 mime_type
= self
.get(nodeid
, 'type', self
.default_mime_type
)
2135 self
.db
.indexer
.add_text((self
.classname
, nodeid
, 'content'),
2136 str(self
.get(nodeid
, 'content')), mime_type
)
2137 elif isinstance(propclass
, hyperdb
.String
) and propclass
.indexme
:
2138 # index them under (classname, nodeid, property)
2140 value
= str(self
.get(nodeid
, prop
))
2142 # node has been destroyed
2144 self
.db
.indexer
.add_text((self
.classname
, nodeid
, prop
), value
)
2146 # deviation from spec - was called ItemClass
2147 class IssueClass(Class
, roundupdb
.IssueClass
):
2148 # Overridden methods:
2149 def __init__(self
, db
, classname
, **properties
):
2150 """The newly-created class automatically includes the "messages",
2151 "files", "nosy", and "superseder" properties. If the 'properties'
2152 dictionary attempts to specify any of these properties or a
2153 "creation" or "activity" property, a ValueError is raised.
2155 if 'title' not in properties
:
2156 properties
['title'] = hyperdb
.String(indexme
='yes')
2157 if 'messages' not in properties
:
2158 properties
['messages'] = hyperdb
.Multilink("msg")
2159 if 'files' not in properties
:
2160 properties
['files'] = hyperdb
.Multilink("file")
2161 if 'nosy' not in properties
:
2162 # note: journalling is turned off as it really just wastes
2163 # space. this behaviour may be overridden in an instance
2164 properties
['nosy'] = hyperdb
.Multilink("user", do_journal
="no")
2165 if 'superseder' not in properties
:
2166 properties
['superseder'] = hyperdb
.Multilink(classname
)
2167 Class
.__init__(self
, db
, classname
, **properties
)
2169 # vim: set et sts=4 sw=4 :