Code

added ability to restore retired nodes
authorkedder <kedder@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sun, 16 Mar 2003 22:24:56 +0000 (22:24 +0000)
committerkedder <kedder@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sun, 16 Mar 2003 22:24:56 +0000 (22:24 +0000)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1593 57a73879-2fb5-44c3-a270-3262357dd7e2

CHANGES.txt
doc/design.txt
roundup/admin.py
roundup/backends/back_anydbm.py
roundup/backends/back_metakit.py
roundup/backends/rdbms_common.py
test/test_db.py

index 1ce4dce5d000a92bc3ef27f1745df69a1393a3d8..aafba74e8166a811b8952a2609eb3ca7968abd61 100644 (file)
@@ -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:
index 5d7ac09cd88a9ad8a203aea4285b8eaa631bd52f..3422a661a227040899744d15d4590020e4376f27 100644 (file)
@@ -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
 ~~~~~~~~~~~~~~~~
index 1f2e4d00b5afe4932e905b4979e25b0246ce1c92..ad2910ad0cc15070f9cb40e4226cfb0f4234e4c0 100644 (file)
@@ -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.
index 2607654ea55e48a652e4036ab52bfaf7abf310e3..258b4daf1ee7e947db0eeb0735ff9a06afcc71c5 100644 (file)
@@ -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.
         '''
index fec52eb95579bc27887062c38f31dc3bc0b8e21e..8eae6946c454d0ad967735273f7a349ef859a049 100755 (executable)
@@ -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
index b3a9c9ca47c06d9c8e11c4b87889d8f3120261f2..1c60cc7097d61abc8d764eb722eaa1d060f5c91c 100644 (file)
@@ -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
         '''
index 43ed8b18970c7f60dd6d0aa5c1dd0d1fa3fec316..e6860945038bc569623361e30ef7efece4a5b2ef 100644 (file)
@@ -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',