From: richard Date: Fri, 2 Apr 2004 05:58:45 +0000 (+0000) Subject: Export and import now include journals (incompatible with export < 0.7) X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=f936769ed5c88e79308f4c541d7a366ebb028478;p=roundup.git Export and import now include journals (incompatible with export < 0.7) Need to check setting of activity in RDBMS imports. Metakit import is quite possibly very busted in setjournal() - I didn't even try to figure how to *clear the previous journal* for the journal being imported. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@2246 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/CHANGES.txt b/CHANGES.txt index 0b321a8..e04a1cc 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,7 @@ Fixed: places (thanks Toby Sargeant) - MySQL and Postgresql use BOOL/BOOLEAN for Boolean types - OTK generation was busted (thanks Stuart D. Gathman) +- export and import now include journals (incompatible with export < 0.7) 2004-03-27 0.7.0b2 diff --git a/roundup/admin.py b/roundup/admin.py index e89f864..6c3502e 100644 --- a/roundup/admin.py +++ b/roundup/admin.py @@ -16,7 +16,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: admin.py,v 1.63 2004-03-21 23:39:08 richard Exp $ +# $Id: admin.py,v 1.64 2004-04-02 05:58:43 richard Exp $ '''Administration commands for maintaining Roundup trackers. ''' @@ -1029,8 +1029,10 @@ Command help: # do all the classes specified for classname in classes: cl = self.get_class(classname) + f = open(os.path.join(dir, classname+'.csv'), 'w') writer = rcsv.writer(f, rcsv.colon_separated) + properties = cl.getprops() propnames = properties.keys() propnames.sort() @@ -1038,15 +1040,18 @@ Command help: fields.append('is retired') writer.writerow(fields) - # all nodes for this class (not using list() 'cos it doesn't - # include retired nodes) - - for nodeid in self.db.getclass(classname).getnodeids(): - # get the regular props - writer.writerow (cl.export_list(propnames, nodeid)) + # all nodes for this class + for nodeid in cl.getnodeids(): + writer.writerow(cl.export_list(propnames, nodeid)) # close this file f.close() + + # export the journals + jf = open(os.path.join(dir, classname+'-journals.csv'), 'w') + journals = rcsv.writer(jf, rcsv.colon_separated) + map(journals.writerow, cl.export_journals()) + jf.close() return 0 def do_import(self, args): @@ -1054,8 +1059,8 @@ Command help: Import a database from the directory containing CSV files, one per class to import. - The files must define the same properties as the class (including having - a "header" line with those property names.) + The files must define the same properties as the class (including + having a "header" line with those property names.) The imported nodes will have the same nodeid as defined in the import file, thus replacing any existing content. @@ -1071,33 +1076,37 @@ Command help: from roundup import hyperdb for file in os.listdir(args[0]): + classname, ext = os.path.splitext(file) # we only care about CSV files - if not file.endswith('.csv'): + if ext != '.csv' or classname.endswith('-journals'): continue - f = open(os.path.join(args[0], file)) - - # get the classname - classname = os.path.splitext(file)[0] + cl = self.get_class(classname) # ensure that the properties and the CSV file headings match - cl = self.get_class(classname) + f = open(os.path.join(args[0], file)) reader = rcsv.reader(f, rcsv.colon_separated) file_props = None maxid = 1 - # loop through the file and create a node for each entry for r in reader: if file_props is None: file_props = r continue - # do the import and figure the current highest nodeid maxid = max(maxid, int(cl.import_list(file_props, r))) + f.close() + + # import the journals + f = open(os.path.join(args[0], classname + '-journals.csv')) + reader = rcsv.reader(f, rcsv.colon_separated) + cl.import_journals(reader) + f.close() # set the id counter print 'setting', classname, maxid+1 self.db.setid(classname, str(maxid+1)) + return 0 def do_pack(self, args): diff --git a/roundup/backends/back_anydbm.py b/roundup/backends/back_anydbm.py index 6fb115d..0d3d15f 100644 --- a/roundup/backends/back_anydbm.py +++ b/roundup/backends/back_anydbm.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: back_anydbm.py,v 1.139 2004-03-19 04:47:59 richard Exp $ +#$Id: back_anydbm.py,v 1.140 2004-04-02 05:58:43 richard Exp $ '''This module defines a backend that saves the hyperdatabase in a database chosen by anydbm. It is guaranteed to always be available in python versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several @@ -487,6 +487,14 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): self.transactions.append((self.doSaveJournal, (classname, nodeid, action, params, creator, creation))) + def setjournal(self, classname, nodeid, journal): + '''Set the journal to the "journal" list.''' + if __debug__: + print >>hyperdb.DEBUG, 'setjournal', (self, classname, nodeid, + journal) + self.transactions.append((self.doSetJournal, (classname, nodeid, + journal))) + def getjournal(self, classname, nodeid): ''' get the journal for id @@ -685,6 +693,17 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): db[nodeid] = marshal.dumps(l) + def doSetJournal(self, classname, nodeid, journal): + l = [] + for nodeid, journaldate, journaltag, action, params in journal: + # serialise the parameters now if necessary + if isinstance(params, type({})): + if action in ('set', 'create'): + params = self.serialise(classname, params) + l.append((nodeid, journaldate, journaltag, action, params)) + db = self.getCachedJournalDB(classname) + db[nodeid] = marshal.dumps(l) + def doDestroyNode(self, classname, nodeid): if __debug__: print >>hyperdb.DEBUG, 'doDestroyNode', (self, classname, nodeid) @@ -926,103 +945,6 @@ class Class(hyperdb.Class): return newid - def export_list(self, propnames, nodeid): - ''' Export a node - generate a list of CSV-able data in the order - specified by propnames for the given node. - ''' - properties = self.getprops() - l = [] - for prop in propnames: - proptype = properties[prop] - value = self.get(nodeid, prop) - # "marshal" data where needed - if value is None: - pass - elif isinstance(proptype, hyperdb.Date): - value = value.get_tuple() - elif isinstance(proptype, hyperdb.Interval): - value = value.get_tuple() - elif isinstance(proptype, hyperdb.Password): - value = str(value) - l.append(repr(value)) - - # append retired flag - l.append(repr(self.is_retired(nodeid))) - - return l - - def import_list(self, propnames, proplist): - ''' Import a node - all information including "id" is present and - should not be sanity checked. Triggers are not triggered. The - journal should be initialised using the "creator" and "created" - information. - - Return the nodeid of the node imported. - ''' - if self.db.journaltag is None: - raise DatabaseError, 'Database open read-only' - properties = self.getprops() - - # make the new node's property map - d = {} - newid = None - for i in range(len(propnames)): - # Figure the property for this column - propname = propnames[i] - - # Use eval to reverse the repr() used to output the CSV - value = eval(proplist[i]) - - # "unmarshal" where necessary - if propname == 'id': - newid = value - continue - elif propname == 'is retired': - # is the item retired? - if int(value): - d[self.db.RETIRED_FLAG] = 1 - continue - elif value is None: - d[propname] = None - continue - - prop = properties[propname] - if isinstance(prop, hyperdb.Date): - value = date.Date(value) - elif isinstance(prop, hyperdb.Interval): - value = date.Interval(value) - elif isinstance(prop, hyperdb.Password): - pwd = password.Password() - pwd.unpack(value) - value = pwd - d[propname] = value - - # get a new id if necessary - if newid is None: - newid = self.db.newid(self.classname) - - # add the node and journal - self.db.addnode(self.classname, newid, d) - - # extract the journalling stuff and nuke it - if d.has_key('creator'): - creator = d['creator'] - del d['creator'] - else: - creator = None - if d.has_key('creation'): - creation = d['creation'] - del d['creation'] - else: - creation = None - if d.has_key('activity'): - del d['activity'] - if d.has_key('actor'): - del d['actor'] - self.db.addjournal(self.classname, newid, 'create', {}, creator, - creation) - return newid - def get(self, nodeid, propname, default=_marker, cache=1): '''Get the value of a property on an existing node of this class. @@ -1993,6 +1915,147 @@ class Class(hyperdb.Class): for react in self.reactors[action]: react(self.db, self, nodeid, oldvalues) + # + # import / export support + # + def export_list(self, propnames, nodeid): + ''' Export a node - generate a list of CSV-able data in the order + specified by propnames for the given node. + ''' + properties = self.getprops() + l = [] + for prop in propnames: + proptype = properties[prop] + value = self.get(nodeid, prop) + # "marshal" data where needed + if value is None: + pass + elif isinstance(proptype, hyperdb.Date): + value = value.get_tuple() + elif isinstance(proptype, hyperdb.Interval): + value = value.get_tuple() + elif isinstance(proptype, hyperdb.Password): + value = str(value) + l.append(repr(value)) + + # append retired flag + l.append(repr(self.is_retired(nodeid))) + + return l + + def import_list(self, propnames, proplist): + ''' Import a node - all information including "id" is present and + should not be sanity checked. Triggers are not triggered. The + journal should be initialised using the "creator" and "created" + information. + + Return the nodeid of the node imported. + ''' + if self.db.journaltag is None: + raise DatabaseError, 'Database open read-only' + properties = self.getprops() + + # make the new node's property map + d = {} + newid = None + for i in range(len(propnames)): + # Figure the property for this column + propname = propnames[i] + + # Use eval to reverse the repr() used to output the CSV + value = eval(proplist[i]) + + # "unmarshal" where necessary + if propname == 'id': + newid = value + continue + elif propname == 'is retired': + # is the item retired? + if int(value): + d[self.db.RETIRED_FLAG] = 1 + continue + elif value is None: + d[propname] = None + continue + + prop = properties[propname] + if isinstance(prop, hyperdb.Date): + value = date.Date(value) + elif isinstance(prop, hyperdb.Interval): + value = date.Interval(value) + elif isinstance(prop, hyperdb.Password): + pwd = password.Password() + pwd.unpack(value) + value = pwd + d[propname] = value + + # get a new id if necessary + if newid is None: + newid = self.db.newid(self.classname) + + # add the node and journal + self.db.addnode(self.classname, newid, d) + return newid + + def export_journals(self): + '''Export a class's journal - generate a list of lists of + CSV-able data: + + nodeid, date, user, action, params + + No heading here - the columns are fixed. + ''' + properties = self.getprops() + r = [] + for nodeid in self.getnodeids(): + for nodeid, date, user, action, params in self.history(nodeid): + date = date.get_tuple() + if action == 'set': + for propname, value in params.items(): + prop = properties[propname] + # make sure the params are eval()'able + if value is None: + pass + elif isinstance(prop, Date): + value = value.get_tuple() + elif isinstance(prop, Interval): + value = value.get_tuple() + elif isinstance(prop, Password): + value = str(value) + params[propname] = value + l = [nodeid, date, user, action, params] + r.append(map(repr, l)) + return r + + def import_journals(self, entries): + '''Import a class's journal. + + Uses setjournal() to set the journal for each item.''' + properties = self.getprops() + d = {} + for l in entries: + l = map(eval, l) + nodeid, date, user, action, params = l + r = d.setdefault(nodeid, []) + if action == 'set': + for propname, value in params.items(): + prop = properties[propname] + if value is None: + pass + elif isinstance(prop, Date): + value = date.Date(value) + elif isinstance(prop, Interval): + value = date.Interval(value) + elif isinstance(prop, Password): + pwd = password.Password() + pwd.unpack(value) + value = pwd + params[propname] = value + r.append((nodeid, date.Date(date), user, action, params)) + + for nodeid, l in d.items(): + self.db.setjournal(self.classname, nodeid, l) + class FileClass(Class, hyperdb.FileClass): '''This class defines a large chunk of data. To support this, it has a mandatory String property "content" which is typically saved off diff --git a/roundup/backends/back_metakit.py b/roundup/backends/back_metakit.py index 17a47e9..1de4f53 100755 --- a/roundup/backends/back_metakit.py +++ b/roundup/backends/back_metakit.py @@ -1,4 +1,4 @@ -# $Id: back_metakit.py,v 1.69 2004-03-24 05:39:47 richard Exp $ +# $Id: back_metakit.py,v 1.70 2004-04-02 05:58:45 richard Exp $ '''Metakit backend for Roundup, originally by Gordon McMillan. Known Current Bugs: @@ -211,6 +211,21 @@ class _Database(hyperdb.Database, roundupdb.Database): action=action, user = creator, params = marshal.dumps(params)) + + def setjournal(self, tablenm, nodeid, journal): + '''Set the journal to the "journal" list.''' + tblid = self.tables.find(name=tablenm) + if tblid == -1: + tblid = self.tables.append(name=tablenm) + for nodeid, date, user, action, params in journal: + # tableid:I,nodeid:I,date:I,user:I,action:I,params:B + self.hist.append(tableid=tblid, + nodeid=int(nodeid), + date=date, + action=action, + user=user, + params=marshal.dumps(params)) + def getjournal(self, tablenm, nodeid): ''' get the journal for id ''' @@ -1434,108 +1449,6 @@ class Class(hyperdb.Class): self.db.indexer.add_text((self.classname, nodeid, prop), str(self.get(nodeid, prop))) - def export_list(self, propnames, nodeid): - ''' Export a node - generate a list of CSV-able data in the order - specified by propnames for the given node. - ''' - properties = self.getprops() - l = [] - for prop in propnames: - proptype = properties[prop] - value = self.get(nodeid, prop) - # "marshal" data where needed - if value is None: - pass - elif isinstance(proptype, hyperdb.Date): - value = value.get_tuple() - elif isinstance(proptype, hyperdb.Interval): - value = value.get_tuple() - elif isinstance(proptype, hyperdb.Password): - value = str(value) - l.append(repr(value)) - - # append retired flag - l.append(repr(self.is_retired(nodeid))) - - return l - - def import_list(self, propnames, proplist): - ''' Import a node - all information including "id" is present and - should not be sanity checked. Triggers are not triggered. The - journal should be initialised using the "creator" and "creation" - information. - - Return the nodeid of the node imported. - ''' - if self.db.journaltag is None: - raise hyperdb.DatabaseError, 'Database open read-only' - properties = self.getprops() - - d = {} - view = self.getview(READWRITE) - for i in range(len(propnames)): - value = eval(proplist[i]) - if not value: - continue - - propname = propnames[i] - if propname == 'id': - newid = value = int(value) - elif propname == 'is retired': - # is the item retired? - if int(value): - d['_isdel'] = 1 - continue - elif value is None: - d[propname] = None - continue - - prop = properties[propname] - if isinstance(prop, hyperdb.Date): - value = int(calendar.timegm(value)) - elif isinstance(prop, hyperdb.Interval): - value = date.Interval(value).serialise() - elif isinstance(prop, hyperdb.Number): - value = int(value) - elif isinstance(prop, hyperdb.Boolean): - value = int(value) - elif isinstance(prop, hyperdb.Link) and value: - value = int(value) - elif isinstance(prop, hyperdb.Multilink): - # we handle multilinks separately - continue - d[propname] = value - - # possibly make a new node - if not d.has_key('id'): - d['id'] = newid = self.maxid - self.maxid += 1 - - # save off the node - view.append(d) - - # fix up multilinks - ndx = view.find(id=newid) - row = view[ndx] - for i in range(len(propnames)): - value = eval(proplist[i]) - propname = propnames[i] - if propname == 'is retired': - continue - prop = properties[propname] - if not isinstance(prop, hyperdb.Multilink): - continue - sv = getattr(row, propname) - for entry in value: - sv.append((int(entry),)) - - self.db.dirty = 1 - creator = d.get('creator', 0) - creation = d.get('creation', 0) - self.db.addjournal(self.classname, str(newid), _CREATE, {}, creator, - creation) - return newid - # --- used by Database def _commit(self): ''' called post commit of the DB. @@ -1644,6 +1557,166 @@ class Class(hyperdb.Class): tablename = "_%s.%s"%(self.classname, self.key) return self.db._db.view("_%s" % tablename).ordered(1) + # + # import / export + # + def export_list(self, propnames, nodeid): + ''' Export a node - generate a list of CSV-able data in the order + specified by propnames for the given node. + ''' + properties = self.getprops() + l = [] + for prop in propnames: + proptype = properties[prop] + value = self.get(nodeid, prop) + # "marshal" data where needed + if value is None: + pass + elif isinstance(proptype, hyperdb.Date): + value = value.get_tuple() + elif isinstance(proptype, hyperdb.Interval): + value = value.get_tuple() + elif isinstance(proptype, hyperdb.Password): + value = str(value) + l.append(repr(value)) + + # append retired flag + l.append(repr(self.is_retired(nodeid))) + + return l + + def import_list(self, propnames, proplist): + ''' Import a node - all information including "id" is present and + should not be sanity checked. Triggers are not triggered. The + journal should be initialised using the "creator" and "creation" + information. + + Return the nodeid of the node imported. + ''' + if self.db.journaltag is None: + raise hyperdb.DatabaseError, 'Database open read-only' + properties = self.getprops() + + d = {} + view = self.getview(READWRITE) + for i in range(len(propnames)): + value = eval(proplist[i]) + if not value: + continue + + propname = propnames[i] + if propname == 'id': + newid = value = int(value) + elif propname == 'is retired': + # is the item retired? + if int(value): + d['_isdel'] = 1 + continue + elif value is None: + d[propname] = None + continue + + prop = properties[propname] + if isinstance(prop, hyperdb.Date): + value = int(calendar.timegm(value)) + elif isinstance(prop, hyperdb.Interval): + value = date.Interval(value).serialise() + elif isinstance(prop, hyperdb.Number): + value = int(value) + elif isinstance(prop, hyperdb.Boolean): + value = int(value) + elif isinstance(prop, hyperdb.Link) and value: + value = int(value) + elif isinstance(prop, hyperdb.Multilink): + # we handle multilinks separately + continue + d[propname] = value + + # possibly make a new node + if not d.has_key('id'): + d['id'] = newid = self.maxid + self.maxid += 1 + + # save off the node + view.append(d) + + # fix up multilinks + ndx = view.find(id=newid) + row = view[ndx] + for i in range(len(propnames)): + value = eval(proplist[i]) + propname = propnames[i] + if propname == 'is retired': + continue + prop = properties[propname] + if not isinstance(prop, hyperdb.Multilink): + continue + sv = getattr(row, propname) + for entry in value: + sv.append((int(entry),)) + + self.db.dirty = 1 + return newid + + def export_journals(self): + '''Export a class's journal - generate a list of lists of + CSV-able data: + + nodeid, date, user, action, params + + No heading here - the columns are fixed. + ''' + properties = self.getprops() + r = [] + for nodeid in self.getnodeids(): + for nodeid, date, user, action, params in self.history(nodeid): + date = date.get_tuple() + if action == 'set': + for propname, value in params.items(): + prop = properties[propname] + # make sure the params are eval()'able + if value is None: + pass + elif isinstance(prop, Date): + value = value.get_tuple() + elif isinstance(prop, Interval): + value = value.get_tuple() + elif isinstance(prop, Password): + value = str(value) + params[propname] = value + l = [nodeid, date, user, action, params] + r.append(map(repr, l)) + return r + + def import_journals(self, entries): + '''Import a class's journal. + + Uses setjournal() to set the journal for each item.''' + properties = self.getprops() + d = {} + for l in entries: + l = map(eval, l) + nodeid, date, user, action, params = l + r = d.setdefault(nodeid, []) + if action == 'set': + for propname, value in params.items(): + prop = properties[propname] + if value is None: + pass + elif isinstance(prop, Date): + value = date.Date(value) + elif isinstance(prop, Interval): + value = date.Interval(value) + elif isinstance(prop, Password): + pwd = password.Password() + pwd.unpack(value) + value = pwd + params[propname] = value + r.append((nodeid, date.Date(date), user, action, params)) + + for nodeid, l in d.items(): + self.db.setjournal(self.classname, nodeid, l) + def _fetchML(sv): l = [] for row in sv: diff --git a/roundup/backends/rdbms_common.py b/roundup/backends/rdbms_common.py index 684a04c..657b477 100644 --- a/roundup/backends/rdbms_common.py +++ b/roundup/backends/rdbms_common.py @@ -1,4 +1,4 @@ -# $Id: rdbms_common.py,v 1.87 2004-03-31 07:25:14 richard Exp $ +# $Id: rdbms_common.py,v 1.88 2004-04-02 05:58:45 richard Exp $ ''' Relational database (SQL) backend common code. Basics: @@ -679,10 +679,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): self.arg, self.arg) self.sql(sql, (entry, nodeid)) - # make sure we do the commit-time extra stuff for this node - self.transactions.append((self.doSaveNode, (classname, nodeid, node))) - - def setnode(self, classname, nodeid, values, multilink_changes): + def setnode(self, classname, nodeid, values, multilink_changes={}): ''' Change the specified node. ''' if __debug__: @@ -736,7 +733,27 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): print >>hyperdb.DEBUG, 'setnode', (self, sql, vals) self.cursor.execute(sql, vals) - # now the fun bit, updating the multilinks ;) + # we're probably coming from an import, not a change + if not multilink_changes: + for name in mls: + prop = props[name] + value = values[name] + + t = '%s_%s'%(classname, name) + + # clear out previous values for this node + # XXX numeric ids + self.sql('delete from %s where nodeid=%s'%(t, self.arg), + (nodeid,)) + + # insert the values for this node + for entry in values[name]: + sql = 'insert into %s (linkid, nodeid) values (%s,%s)'%(t, + self.arg, self.arg) + # XXX numeric ids + self.sql(sql, (entry, nodeid)) + + # we have multilink changes to apply for col, (add, remove) in multilink_changes.items(): tn = '%s_%s'%(classname, col) if add: @@ -752,9 +769,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): # XXX numeric ids self.sql(sql, (int(nodeid), int(removeid))) - # make sure we do the commit-time extra stuff for this node - self.transactions.append((self.doSaveNode, (classname, nodeid, values))) - sql_to_hyperdb_value = { hyperdb.String : str, hyperdb.Date : lambda x:date.Date(str(x).replace(' ', '.')), @@ -887,11 +901,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): 'link' or 'unlink' -- 'params' is (classname, nodeid, propname) 'retire' -- 'params' is None ''' - # serialise the parameters now if necessary - if isinstance(params, type({})): - if action in ('set', 'create'): - params = self.serialise(classname, params) - # handle supply of the special journalling parameters (usually # supplied on importing an existing database) if creator: @@ -904,7 +913,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): journaldate = date.Date() # create the journal entry - cols = ','.join('nodeid date tag action params'.split()) + cols = 'nodeid,date,tag,action,params' if __debug__: print >>hyperdb.DEBUG, 'addjournal', (nodeid, journaldate, @@ -913,6 +922,22 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): self.save_journal(classname, cols, nodeid, journaldate, journaltag, action, params) + def setjournal(self, classname, nodeid, journal): + '''Set the journal to the "journal" list.''' + # clear out any existing entries + self.sql('delete from %s__journal where nodeid=%s'%(classname, + self.arg), (nodeid,)) + + # create the journal entry + cols = 'nodeid,date,tag,action,params' + + for nodeid, journaldate, journaltag, action, params in journal: + if __debug__: + print >>hyperdb.DEBUG, 'setjournal', (nodeid, journaldate, + journaltag, action, params) + self.save_journal(classname, cols, nodeid, journaldate, + journaltag, action, params) + def getjournal(self, classname, nodeid): ''' get the journal for id ''' @@ -1024,12 +1049,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): # clear the cache self.clearCache() - def doSaveNode(self, classname, nodeid, node): - ''' dummy that just generates a reindex event - ''' - # return the classname, nodeid so we reindex this content - return (classname, nodeid) - def sql_close(self): if __debug__: print >>hyperdb.DEBUG, '+++ close database connection +++' @@ -1253,114 +1272,6 @@ class Class(hyperdb.Class): # XXX numeric ids return str(newid) - def export_list(self, propnames, nodeid): - ''' Export a node - generate a list of CSV-able data in the order - specified by propnames for the given node. - ''' - properties = self.getprops() - l = [] - for prop in propnames: - proptype = properties[prop] - value = self.get(nodeid, prop) - # "marshal" data where needed - if value is None: - pass - elif isinstance(proptype, hyperdb.Date): - value = value.get_tuple() - elif isinstance(proptype, hyperdb.Interval): - value = value.get_tuple() - elif isinstance(proptype, hyperdb.Password): - value = str(value) - l.append(repr(value)) - l.append(repr(self.is_retired(nodeid))) - return l - - def import_list(self, propnames, proplist): - ''' Import a node - all information including "id" is present and - should not be sanity checked. Triggers are not triggered. The - journal should be initialised using the "creator" and "created" - information. - - Return the nodeid of the node imported. - ''' - if self.db.journaltag is None: - raise DatabaseError, 'Database open read-only' - properties = self.getprops() - - # make the new node's property map - d = {} - retire = 0 - newid = None - for i in range(len(propnames)): - # Use eval to reverse the repr() used to output the CSV - value = eval(proplist[i]) - - # Figure the property for this column - propname = propnames[i] - - # "unmarshal" where necessary - if propname == 'id': - newid = value - continue - elif propname == 'is retired': - # is the item retired? - if int(value): - retire = 1 - continue - elif value is None: - d[propname] = None - continue - - prop = properties[propname] - if value is None: - # don't set Nones - continue - elif isinstance(prop, hyperdb.Date): - value = date.Date(value) - elif isinstance(prop, hyperdb.Interval): - value = date.Interval(value) - elif isinstance(prop, hyperdb.Password): - pwd = password.Password() - pwd.unpack(value) - value = pwd - d[propname] = value - - # get a new id if necessary - if newid is None: - newid = self.db.newid(self.classname) - - # add the node and journal - self.db.addnode(self.classname, newid, d) - - # retire? - if retire: - # use the arg for __retired__ to cope with any odd database type - # conversion (hello, sqlite) - sql = 'update _%s set __retired__=%s where id=%s'%(self.classname, - self.db.arg, self.db.arg) - if __debug__: - print >>hyperdb.DEBUG, 'retire', (self, sql, newid) - self.db.cursor.execute(sql, (1, newid)) - - # extract the extraneous journalling gumpf and nuke it - if d.has_key('creator'): - creator = d['creator'] - del d['creator'] - else: - creator = None - if d.has_key('creation'): - creation = d['creation'] - del d['creation'] - else: - creation = None - if d.has_key('activity'): - del d['activity'] - if d.has_key('actor'): - del d['actor'] - self.db.addjournal(self.classname, newid, 'create', {}, creator, - creation) - return newid - _marker = [] def get(self, nodeid, propname, default=_marker, cache=1): '''Get the value of a property on an existing node of this class. @@ -2246,6 +2157,159 @@ class Class(hyperdb.Class): for react in self.reactors[action]: react(self.db, self, nodeid, oldvalues) + # + # import / export support + # + def export_list(self, propnames, nodeid): + ''' Export a node - generate a list of CSV-able data in the order + specified by propnames for the given node. + ''' + properties = self.getprops() + l = [] + for prop in propnames: + proptype = properties[prop] + value = self.get(nodeid, prop) + # "marshal" data where needed + if value is None: + pass + elif isinstance(proptype, hyperdb.Date): + value = value.get_tuple() + elif isinstance(proptype, hyperdb.Interval): + value = value.get_tuple() + elif isinstance(proptype, hyperdb.Password): + value = str(value) + l.append(repr(value)) + l.append(repr(self.is_retired(nodeid))) + return l + + def import_list(self, propnames, proplist): + ''' Import a node - all information including "id" is present and + should not be sanity checked. Triggers are not triggered. The + journal should be initialised using the "creator" and "created" + information. + + Return the nodeid of the node imported. + ''' + if self.db.journaltag is None: + raise DatabaseError, 'Database open read-only' + properties = self.getprops() + + # make the new node's property map + d = {} + retire = 0 + newid = None + for i in range(len(propnames)): + # Use eval to reverse the repr() used to output the CSV + value = eval(proplist[i]) + + # Figure the property for this column + propname = propnames[i] + + # "unmarshal" where necessary + if propname == 'id': + newid = value + continue + elif propname == 'is retired': + # is the item retired? + if int(value): + retire = 1 + continue + elif value is None: + d[propname] = None + continue + + prop = properties[propname] + if value is None: + # don't set Nones + continue + elif isinstance(prop, hyperdb.Date): + value = date.Date(value) + elif isinstance(prop, hyperdb.Interval): + value = date.Interval(value) + elif isinstance(prop, hyperdb.Password): + pwd = password.Password() + pwd.unpack(value) + value = pwd + d[propname] = value + + # get a new id if necessary + if newid is None or not self.hasnode(newid): + newid = self.db.newid(self.classname) + self.db.addnode(self.classname, newid, d) + else: + # update + self.db.setnode(self.classname, newid, d) + + # retire? + if retire: + # use the arg for __retired__ to cope with any odd database type + # conversion (hello, sqlite) + sql = 'update _%s set __retired__=%s where id=%s'%(self.classname, + self.db.arg, self.db.arg) + if __debug__: + print >>hyperdb.DEBUG, 'retire', (self, sql, newid) + self.db.cursor.execute(sql, (1, newid)) + return newid + + def export_journals(self): + '''Export a class's journal - generate a list of lists of + CSV-able data: + + nodeid, date, user, action, params + + No heading here - the columns are fixed. + ''' + properties = self.getprops() + r = [] + for nodeid in self.getnodeids(): + for nodeid, date, user, action, params in self.history(nodeid): + date = date.get_tuple() + if action == 'set': + for propname, value in params.items(): + prop = properties[propname] + # make sure the params are eval()'able + if value is None: + pass + elif isinstance(prop, Date): + value = value.get_tuple() + elif isinstance(prop, Interval): + value = value.get_tuple() + elif isinstance(prop, Password): + value = str(value) + params[propname] = value + l = [nodeid, date, user, action, params] + r.append(map(repr, l)) + return r + + def import_journals(self, entries): + '''Import a class's journal. + + Uses setjournal() to set the journal for each item.''' + properties = self.getprops() + d = {} + for l in entries: + l = map(eval, l) + nodeid, jdate, user, action, params = l + r = d.setdefault(nodeid, []) + if action == 'set': + for propname, value in params.items(): + prop = properties[propname] + if value is None: + pass + elif isinstance(prop, Date): + value = date.Date(value) + elif isinstance(prop, Interval): + value = date.Interval(value) + elif isinstance(prop, Password): + pwd = password.Password() + pwd.unpack(value) + value = pwd + params[propname] = value + r.append((nodeid, date.Date(jdate), user, action, params)) + + for nodeid, l in d.items(): + self.db.setjournal(self.classname, nodeid, l) + class FileClass(Class, hyperdb.FileClass): '''This class defines a large chunk of data. To support this, it has a mandatory String property "content" which is typically saved off