From b18794219b853ea867f4e4638453ae4c7a36112f Mon Sep 17 00:00:00 2001 From: kedder Date: Sun, 16 Mar 2003 22:24:56 +0000 Subject: [PATCH] added ability to restore retired nodes git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1593 57a73879-2fb5-44c3-a270-3262357dd7e2 --- CHANGES.txt | 1 + doc/design.txt | 21 ++++++++++++------ roundup/admin.py | 24 ++++++++++++++++++++- roundup/backends/back_anydbm.py | 29 ++++++++++++++++++++----- roundup/backends/back_metakit.py | 37 ++++++++++++++++++++++++++++---- roundup/backends/rdbms_common.py | 30 +++++++++++++++++++++++--- test/test_db.py | 5 ++++- 7 files changed, 126 insertions(+), 21 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1ce4dce..aafba74 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -44,6 +44,7 @@ Feature: - added support for searching on ranges of dates (see doc/user_guide.txt in chapter "Searching Page" for details) - role names made case insensitive +- added ability to restore retired nodes Fixed: diff --git a/doc/design.txt b/doc/design.txt index 5d7ac09..3422a66 100644 --- a/doc/design.txt +++ b/doc/design.txt @@ -308,7 +308,8 @@ Here is the interface provided by the hyperdatabase:: The 'journaltag' is a token that will be attached to the journal entries for any edits done on the database. If 'journaltag' is None, the database is opened in read-only mode: the Class.create(), - Class.set(), and Class.retire() methods are disabled. + Class.set(), Class.retire(), and Class.restore() methods are + disabled. """ def __getattr__(self, classname): @@ -379,6 +380,12 @@ Here is the interface provided by the hyperdatabase:: reuse the values of their key properties. """ + def restore(self, nodeid): + '''Restpre a retired node. + + Make node available for all operations like it was before retirement. + ''' + def history(self, itemid): """Retrieve the journal of edits on a particular item. @@ -793,7 +800,7 @@ There are two kinds of detectors: 2. a reactor is triggered just after an item has been modified When the Roundup database is about to perform a -``create()``, ``set()``, or ``retire()`` +``create()``, ``set()``, ``retire()``, or ``restore`` operation, it first calls any *auditors* that have been registered for that operation on that class. Any auditor may raise a *Reject* exception @@ -814,14 +821,14 @@ register detectors on a given class of items:: def audit(self, event, detector): """Register an auditor on this class. - 'event' should be one of "create", "set", or "retire". + 'event' should be one of "create", "set", "retire", or "restore". 'detector' should be a function accepting four arguments. """ def react(self, event, detector): """Register a reactor on this class. - 'event' should be one of "create", "set", or "retire". + 'event' should be one of "create", "set", "retire", or "restore". 'detector' should be a function accepting four arguments. """ @@ -842,7 +849,7 @@ For a ``set()`` operation, newdata contains only the names and values of properties that are about to be changed. -For a ``retire()`` operation, newdata is None. +For a ``retire()`` or ``restore()`` operation, newdata is None. Reactors are called with the arguments:: @@ -859,8 +866,8 @@ newly-created item and ``olddata`` is None. For a ``set()`` operation, ``olddata`` contains the names and previous values of properties that were changed. -For a ``retire()`` operation, ``itemid`` is the -id of the retired item and ``olddata`` is None. +For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of +the retired or restored item and ``olddata`` is None. Detector Example ~~~~~~~~~~~~~~~~ diff --git a/roundup/admin.py b/roundup/admin.py index 1f2e4d0..ad2910a 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.42 2003-03-06 07:33:29 richard Exp $ +# $Id: admin.py,v 1.43 2003-03-16 22:24:54 kedder Exp $ '''Administration commands for maintaining Roundup trackers. ''' @@ -886,6 +886,28 @@ Command help: raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() return 0 + def do_restore(self, args): + '''Usage: restore designator[,designator]* + Restore the retired node specified by designator. + + The given nodes will become available for users again. + ''' + if len(args) < 1: + raise UsageError, _('Not enough arguments supplied') + designators = args[0].split(',') + for designator in designators: + try: + classname, nodeid = hyperdb.splitDesignator(designator) + except hyperdb.DesignatorError, message: + raise UsageError, message + try: + self.db.getclass(classname).restore(nodeid) + except KeyError: + raise UsageError, _('no such class "%(classname)s"')%locals() + except IndexError: + raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() + return 0 + def do_export(self, args): '''Usage: export [class[,class]] export_dir Export the database to colon-separated-value files. diff --git a/roundup/backends/back_anydbm.py b/roundup/backends/back_anydbm.py index 2607654..258b4da 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.111 2003-03-10 00:22:20 richard Exp $ +#$Id: back_anydbm.py,v 1.112 2003-03-16 22:24:54 kedder 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 @@ -57,8 +57,9 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): The 'journaltag' is a token that will be attached to the journal entries for any edits done on the database. If 'journaltag' is None, the database is opened in read-only mode: the Class.create(), - Class.set(), and Class.retire() methods are disabled. - ''' + Class.set(), Class.retire(), and Class.restore() methods are + disabled. + ''' self.config, self.journaltag = config, journaltag self.dir = config.DATABASE self.classes = {} @@ -710,8 +711,8 @@ class Class(hyperdb.Class): # do the db-related init stuff db.addclass(self) - self.auditors = {'create': [], 'set': [], 'retire': []} - self.reactors = {'create': [], 'set': [], 'retire': []} + self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []} + self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []} def enableJournalling(self): '''Turn journalling on for this class @@ -1319,6 +1320,24 @@ class Class(hyperdb.Class): self.fireReactors('retire', nodeid, None) + def restore(self, nodeid): + '''Restpre a retired node. + + Make node available for all operations like it was before retirement. + ''' + if self.db.journaltag is None: + raise DatabaseError, 'Database open read-only' + + self.fireAuditors('restore', nodeid, None) + + node = self.db.getnode(self.classname, nodeid) + del node[self.db.RETIRED_FLAG] + self.db.setnode(self.classname, nodeid, node) + if self.do_journal: + self.db.addjournal(self.classname, nodeid, 'restored', None) + + self.fireReactors('restore', nodeid, None) + def is_retired(self, nodeid, cldb=None): '''Return true if the node is retired. ''' diff --git a/roundup/backends/back_metakit.py b/roundup/backends/back_metakit.py index fec52eb..8eae694 100755 --- a/roundup/backends/back_metakit.py +++ b/roundup/backends/back_metakit.py @@ -1,4 +1,4 @@ -# $Id: back_metakit.py,v 1.41 2003-03-10 20:24:30 kedder Exp $ +# $Id: back_metakit.py,v 1.42 2003-03-16 22:24:54 kedder Exp $ ''' Metakit backend for Roundup, originally by Gordon McMillan. @@ -279,12 +279,13 @@ class _Database(hyperdb.Database, roundupdb.Database): _STRINGTYPE = type('') _LISTTYPE = type([]) -_CREATE, _SET, _RETIRE, _LINK, _UNLINK = range(5) +_CREATE, _SET, _RETIRE, _LINK, _UNLINK, _RESTORE = range(6) _actionnames = { _CREATE : 'create', _SET : 'set', _RETIRE : 'retire', + _RESTORE : 'restore', _LINK : 'link', _UNLINK : 'unlink', } @@ -307,8 +308,8 @@ class Class: 'creator' : hyperdb.Link('user') } # event -> list of callables - self.auditors = {'create': [], 'set': [], 'retire': []} - self.reactors = {'create': [], 'set': [], 'retire': []} + self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []} + self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []} view = self.__getview() self.maxid = 1 @@ -676,6 +677,34 @@ class Class: self.db.dirty = 1 self.fireReactors('retire', nodeid, None) + def restore(self, nodeid): + '''Restpre a retired node. + + Make node available for all operations like it was before retirement. + ''' + if self.db.journaltag is None: + raise hyperdb.DatabaseError, 'Database open read-only' + self.fireAuditors('restore', nodeid, None) + view = self.getview(1) + ndx = view.find(id=int(nodeid)) + if ndx < 0: + raise KeyError, "nodeid %s not found" % nodeid + + row = view[ndx] + oldvalues = self.uncommitted.setdefault(row.id, {}) + oldval = oldvalues['_isdel'] = row._isdel + row._isdel = 0 + + if self.do_journal: + self.db.addjournal(self.classname, nodeid, _RESTORE, {}) + if self.keyname: + iv = self.getindexview(1) + ndx = iv.find(k=getattr(row, self.keyname),i=row.id) + if ndx > -1: + iv.delete(ndx) + self.db.dirty = 1 + self.fireReactors('restore', nodeid, None) + def is_retired(self, nodeid): view = self.getview(1) # node must exist & not be retired diff --git a/roundup/backends/rdbms_common.py b/roundup/backends/rdbms_common.py index b3a9c9c..1c60cc7 100644 --- a/roundup/backends/rdbms_common.py +++ b/roundup/backends/rdbms_common.py @@ -1,4 +1,4 @@ -# $Id: rdbms_common.py,v 1.43 2003-03-14 02:51:25 richard Exp $ +# $Id: rdbms_common.py,v 1.44 2003-03-16 22:24:55 kedder Exp $ ''' Relational database (SQL) backend common code. Basics: @@ -894,8 +894,8 @@ class Class(hyperdb.Class): # do the db-related init stuff db.addclass(self) - self.auditors = {'create': [], 'set': [], 'retire': []} - self.reactors = {'create': [], 'set': [], 'retire': []} + self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []} + self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []} def schema(self): ''' A dumpable version of the schema that we can store in the @@ -1473,9 +1473,33 @@ class Class(hyperdb.Class): if __debug__: print >>hyperdb.DEBUG, 'retire', (self, sql, nodeid) self.db.cursor.execute(sql, (1, nodeid)) + if self.do_journal: + self.db.addjournal(self.classname, nodeid, 'retired', None) self.fireReactors('retire', nodeid, None) + def restore(self, nodeid): + '''Restpre a retired node. + + Make node available for all operations like it was before retirement. + ''' + if self.db.journaltag is None: + raise DatabaseError, 'Database open read-only' + + self.fireAuditors('restore', nodeid, None) + + # 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, 'restore', (self, sql, nodeid) + self.db.cursor.execute(sql, (0, nodeid)) + if self.do_journal: + self.db.addjournal(self.classname, nodeid, 'restored', None) + + self.fireReactors('restore', nodeid, None) + def is_retired(self, nodeid): '''Return true if the node is rerired ''' diff --git a/test/test_db.py b/test/test_db.py index 43ed8b1..e686094 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: test_db.py,v 1.76 2003-03-10 18:16:42 kedder Exp $ +# $Id: test_db.py,v 1.77 2003-03-16 22:24:56 kedder Exp $ import unittest, os, shutil, time @@ -286,6 +286,9 @@ class anydbmDBTestCase(MyTestCase): self.db.commit() self.assertEqual(self.db.status.get('1', 'name'), b) self.assertNotEqual(a, self.db.status.list()) + # try to restore retired node + self.db.status.restore('1') + self.assertEqual(a, self.db.status.list()) def testSerialisation(self): nid = self.db.issue.create(title="spam", status='1', -- 2.30.2