changeset 610:8dac4b68072b

refactor RenamePage and DeletePage action to use ActionBase base class, move rename page code to PageEditor
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Thu, 04 May 2006 15:50:11 +0200
parents a188f23ba223
children 219ffcdc211a
files MoinMoin/PageEditor.py MoinMoin/action/DeletePage.py MoinMoin/action/PackagePages.py MoinMoin/action/RenamePage.py MoinMoin/action/__init__.py
diffstat 5 files changed, 344 insertions(+), 210 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/PageEditor.py	Mon May 01 02:03:45 2006 +0200
+++ b/MoinMoin/PageEditor.py	Thu May 04 15:50:11 2006 +0200
@@ -446,6 +446,57 @@
         page = backto and Page(self.request, backto) or self
         page.send_page(self.request, msg=_('Edit was cancelled.'))
 
+    def renamePage(self, newpagename, comment=None):
+        """
+        Rename the current version of the page (making a backup before deletion
+        and keeping the backups, logs and attachments).
+        
+        @param comment: Comment given by user
+        @rtype: unicode
+        @return: success flag, error message
+        """
+        _ = self._
+        if not newpagename:
+            return False, _("You can't rename to an empty pagename.")
+
+        newpage = PageEditor(self.request, newpagename)
+
+        pageexists_error = _("""'''A page with the name {{{'%s'}}} already exists.'''
+
+Try a different name.""") % (newpagename,)
+
+        # Check whether a page with the new name already exists
+        if newpage.exists(includeDeleted=1):
+            return False, pageexists_error
+        
+        # Get old page text
+        savetext = self.get_raw_body()
+
+        oldpath = self.getPagePath(check_create=0)
+        newpath = newpage.getPagePath(check_create=0)
+
+        # Rename page
+
+        # NOTE: might fail if another process created newpagename just
+        # NOW, while you read this comment. Rename is atomic for files -
+        # but for directories, rename will fail if the directory
+        # exists. We should have global edit-lock to avoid this.
+        # See http://docs.python.org/lib/os-file-dir.html
+        try:
+            os.rename(oldpath, newpath)
+            self.error = None
+            # Save page text with a comment about the old name
+            savetext = u"## page was renamed from %s\n%s" % (self.page_name, savetext)
+            newpage.saveText(savetext, 0, comment=comment)
+            return True, None
+        except OSError, err:
+            # Try to understand what happened. Maybe its better to check
+            # the error code, but I just reused the available code above...
+            if newpage.exists(includeDeleted=1):
+                return False, pageexists_error
+            else:
+                return False, _('Could not rename page because of file system error: %s.') % unicode(err)
+
     def deletePage(self, comment=None):
         """
         Delete the current version of the page (making a backup before deletion
@@ -453,10 +504,10 @@
         
         @param comment: Comment given by user
         @rtype: unicode
-        @return: error message
+        @return: success flag, error message
         """
         _ = self._
-        
+        success = True
         try:
             # First save a final backup copy of the current page
             # (recreating the page allows access to the backups again)
@@ -472,6 +523,7 @@
                     raise err
         except self.SaveError, message:
             # XXX Error handling
+            success = False
             msg = "SaveError has occured in PageEditor.deletePage. We need locking there."
         
         # reset page object
@@ -495,7 +547,7 @@
             key = formatter_name
             cache = caching.CacheEntry(self.request, arena, key)
             cache.remove()
-        return msg
+        return success, msg
 
     def _sendNotification(self, comment, emails, email_lang, revisions, trivial):
         """
--- a/MoinMoin/action/DeletePage.py	Mon May 01 02:03:45 2006 +0200
+++ b/MoinMoin/action/DeletePage.py	Thu May 04 15:50:11 2006 +0200
@@ -4,72 +4,77 @@
 
     This action allows you to delete a page.
 
-    @copyright: 2004 by Jürgen Hermann <jh@web.de>
+    @copyright: 2006 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
-import os
-from MoinMoin import config, wikiutil
+from MoinMoin import wikiutil
 from MoinMoin.PageEditor import PageEditor
+from MoinMoin.action import ActionBase
+
+class DeletePage(ActionBase):
+    """ Delete page action
+
+    Note: the action name is the class name
+    """
+    def __init__(self, pagename, request):
+        ActionBase.__init__(self, pagename, request)
+        self.use_ticket = True
+        _ = self._
+        self.form_trigger = 'delete'
+        self.form_trigger_label = _('Delete')
+
+    def is_allowed(self):
+        may = self.request.user.may
+        return may.write(self.pagename) and may.delete(self.pagename)
+    
+    def check_condition(self):
+        _ = self._
+        if not self.page.exists():
+            return _('This page is already deleted or was never created!')
+        else:
+            return None
+        
+    def do_action(self):
+        """ Delete pagename """
+        form = self.form
+        comment = form.get('comment', [u''])[0]
+        comment = wikiutil.clean_comment(comment)
+        
+        # Create a page editor that does not do editor backups, because
+        # delete generates a "deleted" version of the page.
+        self.page = PageEditor(self.request, self.pagename, do_editor_backup=0)
+        success, msg = self.page.deletePage(comment)
+        return success, msg
+
+    def get_form_html(self, buttons_html):
+        _ = self._
+        d = {
+            'pagename': self.pagename,
+            'comment_label': _("Optional reason for the deletion"),
+            'buttons_html': buttons_html,
+            'querytext': _('Really delete this page?'),
+        }
+        return '''
+<strong>%(querytext)s</strong>
+<table>
+    <tr>
+        <td class="label"><label>%(comment_label)s</label></td>
+        <td class="content">
+            <input type="text" name="comment" maxlength="80">
+        </td>
+    </tr>
+    <tr>
+        <td></td>
+        <td class="buttons">
+            %(buttons_html)s
+        </td>
+    </tr>
+</table>
+''' % d
 
 
 def execute(pagename, request):
-    _ = request.getText
-    actname = __name__.split('.')[-1]
-    # Create a page editor that does not do editor backups, because
-    # delete generates a "deleted" version of the page.
-    page = PageEditor(request, pagename, do_editor_backup=0)
-
-    # be extra paranoid in dangerous actions
-    if actname in request.cfg.actions_excluded \
-            or not request.user.may.write(pagename) \
-            or not request.user.may.delete(pagename):
-        return page.send_page(request,
-            msg = _('You are not allowed to delete this page.'))
-
-    # check whether page exists at all
-    if not page.exists():
-        return page.send_page(request,
-            msg = _('This page is already deleted or was never created!'))
-
-    # check whether the user clicked the delete button
-    if request.form.has_key('button') and request.form.has_key('ticket'):
-        # check whether this is a valid deletion request (make outside
-        # attacks harder by requiring two full HTTP transactions)
-        if not wikiutil.checkTicket(request.form['ticket'][0]):
-            return page.send_page(request,
-                msg = _('Please use the interactive user interface to delete pages!'))
+    """ Glue code for actions """
+    DeletePage(pagename, request).render()
 
-        # Delete the page
-        comment = request.form.get('comment', [u''])[0]
-        comment = wikiutil.clean_comment(comment)
-        msg = page.deletePage(comment)
-
-        return page.send_page(request, msg=msg)
-
-    # send deletion form
-    ticket = wikiutil.createTicket()
-    querytext = _('Really delete this page?')
-    button = _('Delete')
-    comment_label = _("Optional reason for the deletion")
-
-    # TODO: this form sucks, redesign like RenamePage
-    formhtml = '''
-<form method="post" action="">
-<strong>%(querytext)s</strong>
-<input type="hidden" name="action" value="%(actname)s">
-<input type="hidden" name="ticket" value="%(ticket)s">
-<input type="submit" name="button" value="%(button)s">
-<p>
-%(comment_label)s<br>
-<input type="text" name="comment" size="60" maxlength="80">
-</form>''' % {
-    'querytext': querytext,
-    'actname': actname,
-    'ticket': ticket,
-    'button': button,
-    'comment_label': comment_label,
-}
-
-    return page.send_page(request, msg=formhtml)
-
--- a/MoinMoin/action/PackagePages.py	Mon May 01 02:03:45 2006 +0200
+++ b/MoinMoin/action/PackagePages.py	Thu May 04 15:50:11 2006 +0200
@@ -4,6 +4,8 @@
 
     This action allows you to package pages.
 
+    TODO: use ActionBase class
+
     @copyright: 2005 MoinMoin:AlexanderSchremmer
     @license: GNU GPL, see COPYING for details.
 """
--- a/MoinMoin/action/RenamePage.py	Mon May 01 02:03:45 2006 +0200
+++ b/MoinMoin/action/RenamePage.py	Thu May 04 15:50:11 2006 +0200
@@ -4,161 +4,71 @@
 
     This action allows you to rename a page.
 
-    Based on the DeletePage action by Jürgen Hermann <jh@web.de>
-
-    @copyright: 2002-2004 Michael Reinsch <mr@uue.org>
+    @copyright: 2002-2004 Michael Reinsch <mr@uue.org>,
+                2006 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
 import os
 from MoinMoin import wikiutil
+from MoinMoin.Page import Page
 from MoinMoin.PageEditor import PageEditor
+from MoinMoin.action import ActionBase
 
-class RenamePage:
+class RenamePage(ActionBase):
     """ Rename page action
 
     Note: the action name is the class name
     """
     def __init__(self, pagename, request):
-        self.request = request
-        self.pagename = pagename
-        self.page = PageEditor(request, pagename)
-        self.newpage = None
-        self.error = ''
-
-    def allowed(self):
-        """ Check if user is allowed to do this
-
-        This could be a standard method of the base action class, doing
-        the filtering by action class name and cfg.actions_excluded.
-        """
-        may = self.request.user.may
-        return (not self.__class__.__name__ in self.request.cfg.actions_excluded and
-                may.write(self.pagename) and may.delete(self.pagename))
-    
-    def render(self):
-        """ Render action
-
-        This action return a wiki page with optional message, or
-        redirect to new page.
-        """
-        _ = self.request.getText
-        form = self.request.form
-        
-        if form.has_key('cancel'):
-            # User canceled
-            return self.page.send_page(self.request)
+        ActionBase.__init__(self, pagename, request)
+        self.use_ticket = True
+        _ = self._
+        self.form_trigger = 'rename'
+        self.form_trigger_label = _('Rename Page')
 
-        # Validate user rights and page state. If we get error here, we
-        # return an error message, without the rename form.
-        error = None
-        if not self.allowed():
-            error = _('You are not allowed to rename pages in this wiki!')
-        elif not self.page.exists():
-            error = _('This page is already deleted or was never created!')
-        if error:
-            # Send page with an error message
-            return self.page.send_page(self.request, msg=error)
+    def is_allowed(self):
+        may = self.request.user.may
+        return may.write(self.pagename) and may.delete(self.pagename)
+    
+    def check_condition(self):
+        _ = self._
+        if not self.page.exists():
+            return _('This page is already deleted or was never created!')
+        else:
+            return None
 
-        # Try to rename. If we get an error here, we return the error
-        # message with a rename form.
-        elif (form.has_key('rename') and form.has_key('newpagename') and
-              form.has_key('ticket')):
-            # User replied to the rename form with all required items
-            self.rename()
-            if not self.error:
-                self.request.http_redirect(self.newpage.url(self.request))
-                return self.request.finish()    
-
-        # A new form request, or form has missing data, or rename
-        # failed. Return a new form with optional error.
-        return self.page.send_page(self.request, msg=self.makeform())
-
-    def rename(self):
-        """ Rename pagename and return the new page """
-        _ = self.request.getText
-        form = self.request.form
-        
-        # Require a valid ticket. Make outside attacks harder by
-        # requiring two full HTTP transactions
-        if not wikiutil.checkTicket(form['ticket'][0]):
-            self.error = _('Please use the interactive user interface to rename pages!')
-            return
-
-        # Get new name from form and normalize.
+    def do_action(self):
+        """ Rename this page to "pagename" """
+        _ = self._
+        form = self.form
+        newpagename = form.get('newpagename', [u''])[0]
+        newpagename = self.request.normalizePagename(newpagename)
         comment = form.get('comment', [u''])[0]
         comment = wikiutil.clean_comment(comment)
-        newpagename = form.get('newpagename')[0]
-        newpagename = self.request.normalizePagename(newpagename)
-
-        # Name might be empty after normalization. To save translation
-        # work for this extreme case, we just use "EmptyName".
-        if not newpagename:
-            newpagename = "EmptyName"
-
-        # Valid new name
-        newpage = PageEditor(self.request, newpagename)
-
-        # Check whether a page with the new name already exists
-        if newpage.exists(includeDeleted=1):
-            return self.pageExistsError(newpagename)
-        
-        # Get old page text
-        savetext = self.page.get_raw_body()
-
-        oldpath = self.page.getPagePath(check_create=0)
-        newpath = newpage.getPagePath(check_create=0)
-
-        # Rename page
 
-        # NOTE: might fail if another process created newpagename just
-        # NOW, while you read this comment. Rename is atomic for files -
-        # but for directories, rename will fail if the directory
-        # exists. We should have global edit-lock to avoid this.
-        # See http://docs.python.org/lib/os-file-dir.html
-        try:
-            os.rename(oldpath, newpath)
-            self.newpage = newpage
-            self.error = None
-            # Save page text with a comment about the old name
-            savetext = u"## page was renamed from %s\n%s" % (self.pagename, savetext)
-            newpage.saveText(savetext, 0, comment=comment)
-        except OSError, err:
-            # Try to understand what happened. Maybe its better to check
-            # the error code, but I just reused the available code above...
-            if newpage.exists(includeDeleted=1):
-                return self.pageExistsError(newpagename)
-            else:
-                self.error = _('Could not rename page because of file system'
-                             ' error: %s.') % unicode(err)
-                        
-    def makeform(self):
-        """ Display a rename page form
+        self.page = PageEditor(self.request, self.pagename)
+        success, msg = self.page.renamePage(newpagename, comment)
+        self.newpagename = newpagename # keep there for finish
+        return success, msg
 
-        The form might contain an error that happened when trying to rename.
-        """
-        from MoinMoin.widget.dialog import Dialog
-        _ = self.request.getText
+    def do_action_finish(self, success):
+        if success:
+            url = Page(self.request, self.newpagename).url(self.request)
+            self.request.http_redirect(url)
+            self.request.finish()
+        else:
+            self.render_msg(self.make_form())
 
-        error = ''
-        if self.error:
-            error = u'<p class="error">%s</p>\n' % self.error
-
+    def get_form_html(self, buttons_html):
+        _ = self._
         d = {
-            'error': error,
-            'action': self.__class__.__name__,
-            'ticket': wikiutil.createTicket(),
             'pagename': self.pagename,
-            'rename': _('Rename Page'),
-            'cancel': _('Cancel'),
             'newname_label': _("New name"),
             'comment_label': _("Optional reason for the renaming"),
+            'buttons_html': buttons_html,
         }
-        form = '''
-%(error)s
-<form method="post" action="">
-<input type="hidden" name="action" value="%(action)s">
-<input type="hidden" name="ticket" value="%(ticket)s">
+        return '''
 <table>
     <tr>
         <td class="label"><label>%(newname_label)s</label></td>
@@ -175,23 +85,13 @@
     <tr>
         <td></td>
         <td class="buttons">
-            <input type="submit" name="rename" value="%(rename)s">
-            <input type="submit" name="cancel" value="%(cancel)s">
+            %(buttons_html)s
         </td>
     </tr>
 </table>
-</form>''' % d
-        
-        return Dialog(self.request, content=form)        
-    
-    def pageExistsError(self, pagename):
-        _ = self.request.getText
-        self.error = _("""'''A page with the name {{{'%s'}}} already exists.'''
+''' % d
 
-Try a different name.""") % (pagename,)    
-
-    
 def execute(pagename, request):
     """ Glue code for actions """
     RenamePage(pagename, request).render()
-    
+
--- a/MoinMoin/action/__init__.py	Mon May 01 02:03:45 2006 +0200
+++ b/MoinMoin/action/__init__.py	Thu May 04 15:50:11 2006 +0200
@@ -2,13 +2,188 @@
 """
     MoinMoin - Extension Action Package
 
-    @copyright: 2000 by Richard Jones <richard@bizarsoftware.com.au>
-    @copyright: 2000, 2001, 2002 by Jürgen Hermann <jh@web.de>  
+    Additionally to the usual stuff, we provide an ActionBase class here with
+    some of the usual base functionality for an action, like checking
+    actions_excluded, making and checking tickets, rendering some form,
+    displaying errors and doing stuff after an action.
+    
+    @copyright: 2006 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
 from MoinMoin.util import pysupport
+from MoinMoin import wikiutil
+from MoinMoin.Page import Page
 
 # create a list of extension actions from the subpackage directory
 extension_actions = pysupport.getPackageModules(__file__)
 modules = extension_actions
+
+class ActionBase:
+    """ action base class with some generic stuff to inherit
+
+    Note: the action name is the class name of the derived class
+    """
+    def __init__(self, pagename, request):
+        self.request = request
+        self.form = request.form
+        self.cfg = request.cfg
+        self._ = _ = request.getText
+        self.pagename = pagename
+        self.actionname = self.__class__.__name__
+        self.use_ticket = False # set this to True if you want to use a ticket
+        self.user_html = '''Just checking.''' # html fragment for make_form
+        self.form_cancel = "cancel" # form key for cancelling action
+        self.form_cancel_label = _("Cancel") # label for the cancel button
+        self.form_trigger = "doit" # form key for triggering action (override with e.g. 'rename')
+        self.form_trigger_label = _("Do it.") # label for the trigger button
+        self.page = Page(request, pagename)
+        self.error = ''
+
+    # CHECKS -----------------------------------------------------------------
+    def is_excluded(self):
+        """ Return True if action is excluded """
+        return self.actionname in self.cfg.actions_excluded
+    
+    def is_allowed(self):
+        """ Return True if action is allowed (by ACL) """
+        return True
+    
+    def check_condition(self):
+        """ Check if some other condition is not allowing us to do that action,
+            return error msg or None if there is no problem.
+
+            You can use this to e.g. check if a page exists.
+        """
+        return None
+    
+    def ticket_ok(self):
+        """ Return True if we check for tickets and there is some valid ticket
+            in the form data or if we don't check for tickets at all.
+            Use this to make sure someone really used the web interface.
+        """
+        if not self.use_ticket:
+            return True
+        # Require a valid ticket. Make outside attacks harder by
+        # requiring two full HTTP transactions
+        ticket = self.form.get('ticket', [''])[0]
+        return wikiutil.checkTicket(ticket)
+    
+    # UI ---------------------------------------------------------------------
+    def get_form_html(self, buttons_html):
+        """ Override this to assemble the inner part of the form,
+            for convenience we give him some pre-assembled html for the buttons.
+        """
+        _ = self._
+        prompt = _("Execute action %(actionname)s?") % {'actionname': self.actionname}
+        return "<p>%s</p>%s" % (prompt, buttons_html)
+
+    def make_buttons(self):
+        """ return a list of form buttons for the action form """
+        return [
+            (self.form_trigger, self.form_trigger_label),
+            (self.form_cancel, self.form_cancel_label),
+        ]
+
+    def make_form(self):
+        """ Make some form html for later display.
+
+        The form might contain an error that happened when trying to do the action.
+        """
+        from MoinMoin.widget.dialog import Dialog
+        _ = self._
+
+        if self.error:
+            error_html = u'<p class="error">%s</p>\n' % self.error
+        else:
+            error_html = ''
+
+        buttons = self.make_buttons()
+        buttons_html = []
+        for button in buttons:
+            buttons_html.append('<input type="submit" name="%s" value="%s">' % button)
+        buttons_html = "".join(buttons_html)
+        
+        if self.use_ticket:
+            ticket_html = '<input type="hidden" name="ticket" value="%s">' % wikiutil.createTicket()
+        else:
+            ticket_html = ''
+            
+        d = {
+            'error_html': error_html,
+            'actionname': self.actionname,
+            'pagename': self.pagename,
+            'ticket_html': ticket_html,
+            'user_html': self.get_form_html(buttons_html),
+        }
+
+        form_html = '''
+%(error_html)s
+<form method="post" action="">
+<input type="hidden" name="action" value="%(actionname)s">
+%(ticket_html)s
+%(user_html)s
+</form>''' % d
+        
+        return Dialog(self.request, content=form_html)
+
+    def render_msg(self, msg):
+        """ Called to display some message (can also be the action form) """
+        self.page.send_page(self.request, msg=msg)
+
+    def render_success(self, msg):
+        """ Called to display some message when the action succeeded """
+        self.page.send_page(self.request, msg=msg)
+
+    def render_cancel(self):
+        """ Called when user has hit the cancel button """
+        self.page.send_page(self.request) # we don't tell user he has hit cancel :)
+        
+    def render(self):
+        """ Render action - this is the main function called by action's
+            execute() function.
+
+            We usually render a form here, check for posted forms, etc.
+        """
+        _ = self._
+        form = self.form
+        
+        if form.has_key(self.form_cancel):
+            self.render_cancel()
+            return
+
+        # Validate allowance, user rights and other conditions.
+        error = None
+        if self.is_excluded():
+            error = _('Action %(actionname)s is excluded in this wiki!') % {'actionname': self.actionname }
+        elif not self.is_allowed():
+            error = _('You are not allowed to use action %(actionname)s on this page!') % {'actionname': self.actionname }
+        if error is None:
+            error = self.check_condition()
+        if error:
+            self.render_msg(error)
+        elif form.has_key(self.form_trigger): # user hit the trigger button
+            if self.ticket_ok():
+                success, self.error = self.do_action()
+            else:
+                success = False
+                self.error = _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': self.actionname }
+            self.do_action_finish(success)
+        else:
+            # Return a new form
+            self.render_msg(self.make_form())
+
+    # Executing the action ---------------------------------------------------
+    def do_action(self):
+        """ Do the action and either return error msg or None, if there was no error. """
+        return None
+
+    # AFTER the action -------------------------------------------------------
+    def do_action_finish(self, success):
+        """ Override this to handle success or failure (with error in self.error) of your action.
+        """
+        if success:
+            self.render_success(self.error)
+        else:
+            self.render_msg(self.make_form()) # display the form again
+