Adding Actions to Forms

Similar to Django’s Admin actions, pyramid_crud also provides a way to configure specific actions.

Introduction

What are actions?

An action in the context of this library is something you perform on a list of items that might change their state (or perform anything else, really). A good example would be publishing multiple articles at once or activating multiple users.

How do you configure actions?

Actions are configured by setting the actions parameter on your view. Possible values here are strings or callables. If a string is provided, a method of the same name is looked up on the view and used as the callable.

Each callable gets two arguments: The view and the query which selects the items for which the actions should be performed. Note that a query is used instead of a list of items so that you can refine it or directly perform actions on it. If you need a list, call .all on it or iterate over it.

So how do I create an action exactly?

Let’s work by example and take the same example Django does so we can directly see similarities and differences. Here’s our definition with which we start:

class Article(Base):
    id = Column(Integer, primary_key=True)
    title = Column(Text)
    body = Column(Text)
    status = Column(Enum('p', 'd', 'w'))

    def __unicode__(self):
        return self.title

Now that we have our model, let’s make a method to publish multiple articles at once:

def make_published(view, query):
    query.update({'status': 'p'})
    return True, None

Notice, how we don’t pass in the request as it can be accessed with view.request. The view is an instance of your subclassed CRUDView. The query is an instance of Query. Additionally, you can see that we return a pair here. The first value indicates success of the operation, the latter value is an optional response (see Returning Values From Actions for a detailed explanation).

Now you might want to have a nicer title than ‘Make Published’ (this title is assigned by default, replacing underscores with spaces and calling str.title() on the result). To achieve a custom title (that will appear in the list of items), assign a label to its info dict:

def make_published(view, query):
    query.update({'status': 'p'})
    return True, None
make_published.info = {'label': "Mark selected stories as published"}

And how do I add it to a view?

That’s easy. Here is a full configuration based on the model above:

class ArticleForm(ModelForm):
    class Meta:
        model = Article

class ArticleView(CRUDView):
    Form = ArticleForm
    url_path = '/articles'
    actions = [make_published]

See how we added the actions configuration directive? We gave it a list (with one item) of actions that should be available on this model.

And that’s it, now you have an additional action available at your disposal. Read on for some more information, including advanced techniques, differences and what’s missing in comparison to Django.

Advanced Techniques

Handling Errors

To handle exceptions, wrap your code in a try-except-else clause. You can then handle any exception and possibly log error messages and flash a message to the user. This allows you to shield the user from any application crashes and gives you the ability to examine the log for the cause of the error.

Nonetheless, you can still raise exceptions and they will be passed through in which case the section on Returning Values From Actions does not really apply (as no value is returned).

An example of an implementation that shields to user from exceptions might look like this:

def make_published(view, query):
    try:
        query.update({'status': 'p'})
    except:
        log.error("An error oucurred:\n%s" % format_traceback())
        self.request.session.flash("An error happened while publishing "
                                   "the article(s)")
        return False, None
    else:
        return True, None

This will inform the user of any failure and log the exact exception so you can investigate the problem. Note that with a perfect implementation, you would probably want to explicitly catch all possible exceptions and not use a catch-all. However, since this implementation doesn’t just ignore and instead log the exception, it is not too bad to have a catch all here.

Returning Values From Actions

As already noted above, it is recommended to wrap your code in try-except-else blocks and return the status as a boolean. The reason for this is to allow explicit changes in application behavior based on the result of your execution.

You always have to return a pair of (success, response) to indicate how you would like to proceed.

success must be a boolean value. If it is False it indicates that the action was not successful. In this case the redirect is raised which means it is considered an exception. Any optional transaction (e.g. pyramid_tm) will see this exception and abort the transaction. Afterwards the page is redirected. The response value is not used in this case, so it should always be None.

If success is True, it is assumed that the action was successful. In this case the redirect is returned and the transaction is committed. Note that this is a fine distinction between success and failure and the user does not see a difference (except error messages you might give out).

However, in the case of a successful response, you might also want to change the returned value into something else (maybe redirect somewhere else or return a whole new response). This can be done by setting the response paramater which can really be anything that is allowed to be returned from a view.

So for example if you wanted to direct to a completely different page, you could return an instance of HTTPFound that achieves this. On the other hand, you maybe want to create an intermediate response. In that case, you just need to return an instance of Response. You could create this by calling render_to_response if you want to render an intermediary view from a template. This is the technique the delete action uses.

Note

The more complex it gets, the more likely it is that a redirect to an actual view is much better than manually rendering or building your response. This allows you to factor out the code from your action into a separate view but has the drawback of an additional redirect and the need to keep all the formdata alive (e.g. in the session).

Actions as Methods on the View

Instead of having an external function, you can add your action directly to the view (in most cases the recommended way). For this, you just create a method on the view instead of a function:

class ArticleView(CRUDView):
    ...
    def make_published(self, query):
        try:
            query.update({'status': 'p'})
        except:
            return False, None
        else:
            return True, None
    make_published = {'info': "Mark selected stories as published"}

Note how we renamed view to self because as a method the view reference is now actually the own instance.

Instead of providing the action as a callable, you now use a string instead:

class ArticleView(CRUDView):
    actions = ['make_published']

This will look up the action as a method on the view and call it in the same manner.

Currently Unspported Features from Django

  • Site-wide actions: Currently it is not possible to add actions that are globally available. However, you can work around that by creating a custom subclass and modifiying the action list in the children during runtime, however, this is an unspported as of now and you might face some issues with mutability.
  • Disabling actions: This is currently not supported at all.
  • Runtime disabling/enabling of actions: While unspported, this is possible by overriding the _all_actions atribute. In the default implementation it behaves like a property but caches its result (using Pyramid’s reify decoartor). Take a look at the default implementation to see the format of the returned value.