New Actions — Camelot documentation

### Navigation

- index
- modules |
- next |
- previous |
- Camelot documentation »
- Camelot Enhancement Proposals »

# New Actions

status : approved

Note

This proposal is being implemented in the trunk, the functionallity described here is not yet available in the latest release.

## Introduction

Besides displaying and editing data, every application needs the functions to manipulate data or create reports. In the Camelot framework this is done through actions. Actions appear as buttons on the side of a form and a table. When the user clicks on an action button, a predefined function is called.

An action is available to show the address on a map

Every Action is build up with a set of Action Steps. An Action Step is a reusable part of an Action, such as for example, ask the user to select a file. Camelot comes with a set of standard Actions and Action Steps that are easily extended to manipulate data or create reports.

When defining Actions, a clear distinction should be made between things happening in the model thread (the manipulation or querying of data), and things happening in the gui thread (pop up windows or reports). The The Two Threads section gives more detail on this.

## Summary

In general, actions are defined by subclassing the standard Camelot `camelot.admin.action.Action` class

    from camelot.admin.action import Action
    from camelot.view.action_steps import PrintHtml
    from camelot.core.utils import ugettext_lazy as _
    from camelot.view.art import Icon

    class PrintReport( Action ):

        verbose_name = _('Print Report')
        icon = Icon('tango/16x16/actions/document-print.png')
        tooltip = _('Print a report with all the movies')

        def model_run( self, model_context ):
            yield PrintHtml( 'Hello World' )

Each action has two methods, `gui_run()` and `model_run()`, one of them should be reimplemented in the subclass to either run the action in the gui thread or to run the action in the model thread. The default `Action.gui_run()` behavior is to pop-up a `ProgressDialog` dialog and start the `model_run()` method in the model thread.

`model_run()` in itself is a generator, that can yield `ActionStep` objects back to the gui, such as a `PrintHtml`.

The action objects can than be used a an element of the actions list returned by the `ApplicationAdmin.get_actions()` method:

        def get_actions(self):
            from camelot.admin.action import OpenNewView
            from camelot_example.model import Movie

            new_movie_action = OpenNewView( self.get_related_admin(Movie) )
            new_movie_action.icon = Icon('tango/22x22/mimetypes/x-office-presentation.png')

            return [new_movie_action]

or be used in the `ObjectAdmin.list_actions` or `ObjectAdmin.form_actions` attributes.

The Add an import wizard to an application tutorial has a complete example of creating and using and action.

## What can happen inside `model_run()`

### `yield` events to the GUI

Actions need to be able to send their results back to the user, or ask the user for additional information. This is done with the `yield` statement.

Through `yield`, an Action Step is send to the GUI thread, where it creates user interaction, and sends it result back to the ‘model_thread’. The model_thread will be blocked while the action in the GUI thread takes place, eg

    yield PrintHtml( 'Hello World' )

Will pop up a print preview dialog in the GUI, and the model_run method will only continue when this dialog is closed.

Events that can be yielded to the GUI should be of type `camelot.admin.action.base.ActionStep`. Action steps are reusable parts of an action. Possible Action Steps that can be yielded to the GUI include:

> - `camelot.view.action_steps.change_object.ChangeObject`
> - `camelot.view.action_steps.print_preview.PrintPreview`
> - `camelot.view.action_steps.print_preview.PrintHtml`
> - `camelot.view.action_steps.print_preview.PrintJinjaTemplate`
> - `camelot.view.action_steps.open_file.OpenFile`
> - `camelot.view.action_steps.open_file.OpenStream`
> - `camelot.view.action_steps.open_file.OpenJinjaTemplate`
> - `camelot.view.action_steps.gui.Refresh`
> - `camelot.view.action_steps.gui.ShowPixmap`
> - `camelot.view.action_steps.gui.ShowChart`

### keep the user informed about progress

An `camelot.view.action_steps.update_progress.UpdateProgress` object can be yielded, to update the state of the progress dialog:

This should be done regulary to keep the user informed about the progres of the action:

    movie_count = Movie.query.count()

    report = '<table>'
    for i, movie in enumerate( Movie.query.all() ):
        report += '<tr><td>%s</td></tr>'%(movie.name)
        yield UpdateProgress( i, movie_count )
    report += '</table>'

    yield PrintHtml( report )

Should the user have pressed the Cancel button in the progress dialog, the next yield of an UpdateProgress object will raise a `camelot.core.exception.CancelRequest`.

### manipulation of the model

The most important purpose of an action is to query or manipulate the model, all such things can be done in the `model_run()` method, such as executing queries, manipulating files, etc.

Whenever a part of the model has been changed, it might be needed to inform the GUI about this, so that it can update itself, the easy way of doing so is by yielding an instance of `camelot.view.action_steps.orm.FlushSession` such as:

    movie.rating = 5
    yield FlushSession( model_context.session )

This will flush the session to the database, and at the same time update the GUI so that the flushed changes are shown to the user by updating the visualisation of the changed movie on every screen in the application that displays this object. Alternative updates that can be generated are :

> - `camelot.view.action_steps.orm.UpdateObject`, if one wants to inform the GUI an object is going to be updated.
> - `camelot.view.action_steps.orm.DeleteObject`, if one wants to inform the GUI an object is going to be deleted.
> - `camelot.view.action_steps.orm.CreateObject`, if one wants to inform the GUI an object has been created.

### raise exceptions

When an action fails, a normal Python `Exception` can be raised, which will pop-up an exception dialog to the user that displays a stack trace of the exception. In case no stack trace should be shown to the user, a `camelot.core.exception.UserException` should be raised. This will popup a friendly dialog :

When the `model_run()` method raises a `camelot.core.exception.CancelRequest`, a `GeneratorExit` or a `StopIteration` exception, these are ignored and nothing will be shown to the user.

### handle exceptions

In case an unexpected event occurs in the GUI, a `yield` statement will raise a `camelot.core.exception.GuiException`. This exception will propagate through the action an will be ignored unless handled by the developer.

### request information from the user

The pop-up of a dialog that presents the user with a number of options can be triggered from within the `model_run()` method. This happens by transferring an options object back and forth between the model_thread and the gui_thread. To transfer such an object, this object first needs to be defined:

    class Options( object ):

        def __init__(self):
            self.earliest_releasedate = datetime.date(2000, 1, 1)
            self.latest_releasedate = datetime.date.today()

        class Admin( ObjectAdmin ):
            form_display = [ 'earliest_releasedate', 'latest_releasedate' ]
            field_attributes = { 'earliest_releasedate':{'delegate':delegates.DateDelegate},
                                 'latest_releasedate':{'delegate':delegates.DateDelegate}, }

Than a `camelot.view.action_steps.change_object.ChangeObject` action step can be `yield` to present the options to the user and get the filled in values back :

            from camelot.view import action_steps
            options = NewProjectOptions()
            yield action_steps.UpdateProgress( text = 'Request information' )
            yield action_steps.ChangeObject( options )

Will show a dialog to modify the object:

When the user presses Cancel button of the dialog, the `yield` statement will raise a `camelot.core.exception.CancelRequest`.

Other ways of requesting information are :

> - `camelot.view.action_steps.NewObject`, to request the user to fill in a new form for an object of a specified class. This will return such a new object or None if the user canceled the operation.
> - `camelot.view.action_steps.select_file.SelectFile`, to request to select an existing file to process or a new file to save information.

## States and Modes

### States

The widget that is used to trigger an action can be in different states. A `camelot.admin.action.base.State` object is returned by the `camelot.admin.action.base.Action.get_state` method. Subclasses of Action can reimplement this method to change the State of an action button.

This allows to hide or disable the action button, depending on the objects selected or the current object being displayed.

### Modes

An action widget can be triggered in different modes, for example a print button can be triggered as Print or Export to PDF. The different modes of an action are specified as a list of `camelot.admin.action.base.Mode` objects.

To change the modes of an Action, either specify the `modes` attribute of an `Action` or specify the `modes` attribute of the `State` returned by the `Action.get_state()` method.

## Action Context

Depending on where an action was triggered, a different context will be available during its execution in `camelot.admin.action.base.Action.gui_run()` and `camelot.admin.action.base.Action.model_run()`.

The minimal context available in the GUI thread is :

class `camelot.admin.action.base.` `GuiContext`

  The GUI context in which an action is running. This object can contain references to widgets and other usefull information. This object cannot contain reference to anything database or model related, as those belong strictly to the `ModelContext`

  `progress_dialog`

    an instance of `QtGui.QProgressDialog` or `None`

  `mode_name`

    the name of the mode in which the action was triggered

  `model_context`

    a subclass of `ModelContext` to be used in `create_model_context()` as the type of object to return.

While the minimal contact available in the Model thread is:

class `camelot.admin.action.base.` `ModelContext`

  The Model context in which an action is running. The model context can contain reference to database sessions or other model related data. This object can not contain references to widgets as those belong strictly to the `GuiContext`.

  `mode_name`

    the name of the mode in which the action was triggered

### Application Actions

To enable Application Actions for a certain `ApplicationAdmin` overwrite its `ApplicationAdmin.get_actions()` method

    from camelot.admin.application_admin import ApplicationAdmin
    from camelot.admin.action import Action

    class GenerateReports( Action ):

        verbose_name = _('Generate Reports')

        def model_run( self, model_context):
            for i in range(10):
                yield UpdateProgress(i, 10)

    class MyApplicationAdmin( ApplicationAdmin )

        def get_actions( self ):
            return [GenerateReports(),]

An action specified here will receive an `ApplicationActionGuiContext` object as the gui_context argument of the the `gui_run()` method, and a `ApplicationActionModelContext` object as the model_context argument of the `model_run()` method.

class `camelot.admin.action.application_action.` `ApplicationActionGuiContext`

  The GUI context for an `camelot.admin.action.Action`. On top of the attributes of the `camelot.admin.action.base.GuiContext`, this context contains :

  `workspace`

    the the `camelot.view.workspace.DesktopWorkspace` of the application in which views can be opened or adapted.

  `admin`

    the application admin.

class `camelot.admin.action.application_action.` `ApplicationActionModelContext`

  The Model context for an `camelot.admin.action.Action`. On top of the attributes of the `camelot.admin.action.base.ModelContext`, this context contains :

  `admin`

    the application admin.

### Form Actions

To enable Form Actions for a certain `ObjectAdmin` or `EntityAdmin`, specify the `form_actions` attribute.

An action specified here will receive a `FormActionGuiContext` object as the gui_context argument of the `gui_run()` method, and a `FormActionModelContext` object as the model_context argument of the `model_run()` method.

class `camelot.admin.action.form_action.` `FormActionGuiContext`

  The context for an `Action` on a form. On top of the attributes of the `camelot.admin.action.application_action.ApplicationActionGuiContext`, this context contains :

  `widget_mapper`

    the `QtGui.QDataWidgetMapper` class that relates the form widget to the model.

class `camelot.admin.action.form_action.` `FormActionModelContext`

  On top of the attributes of the `camelot.admin.action.application_action.ApplicationActionModelContext`, this context contains :

  `current_row`

    the row in the list that is currently displayed in the form

  `collection_count`

    the number of objects that can be reached in the form.

  `selection_count`

    the number of objects displayed in the form, at most 1.

  `session`

    The session to which the objects in the list belong.

  The `selection_count` attribute allows the `model_run()` to quickly evaluate the size of the collection without calling the potetially time consuming method `get_collection()`.

  `FormActionModelContext.` `get_collection`(yield_per=None)

    Parameters: | yield_per – an integer number giving a hint on how many objects should fetched from the database at the same time.
    Returns: | a generator over the objects in the list

  `FormActionModelContext.` `get_object`()

    Returns: | the object currently displayed in the form, None if no object

    is displayed yet

  `FormActionModelContext.` `get_selection`(yield_per=None)

    Method to be compatible with a `camelot.admin.action.list_action.ListActionModelContext`, this allows creating a single Action to be used on a form and on list.

    Parameters: | yield_per – this parameter has no effect, it’s here only for compatibility with `camelot.admin.action.list_action.ListActionModelContext.get_selection()`
    Returns: | a generator that yields the current object displayed in the form and does not yield anything if no object is displayed yet in the form.

### List Actions

To enable List Actions for a certain `ObjectAdmin` or `EntityAdmin`, specify the `list_actions` attribute:

    list_actions = [ ChangeRatingAction() ]

This will result in a button being displayed on the table view.

An action specified here will receive a `ListActionGuiContext` object as the gui_context argument of th the `gui_run()` method, and a `ListActionModelContext` object as the model_context argument of the `model_run()` method.

class `camelot.admin.action.list_action.` `ListActionGuiContext`

  The context for an `Action` on a table view. On top of the attributes of the `camelot.admin.action.application_action.ApplicationActionGuiContext`, this context contains :

  `item_view`

    the `QtGui.QAbstractItemView` class that relates to the table view on which the widget will be placed.

class `camelot.admin.action.list_action.` `ListActionModelContext`

  On top of the attributes of the `camelot.admin.action.application_action.ApplicationActionModelContext`, this context contains :

  `selection_count`

    the number of selected rows.

  `collection_count`

    the number of rows in the list.

  `selected_rows`

    an ordered list with tuples of selected row ranges. the range is inclusive.

  `current_row`

    the current row in the list

  `session`

    The session to which the objects in the list belong.

  The `collection_count` and `selection_count` attributes allow the `model_run()` to quickly evaluate the size of the collection or the selection without calling the potentially time consuming methods `get_collection()` and `get_selection()`.

  `ListActionModelContext.` `get_collection`(yield_per=None)

    Parameters: | yield_per – an integer number giving a hint on how many objects should fetched from the database at the same time.
    Returns: | a generator over the objects in the list

  `ListActionModelContext.` `get_object`()

    Returns: | the object displayed in the current row or None

  `ListActionModelContext.` `get_selection`(yield_per=None)

    Parameters: | yield_per – an integer number giving a hint on how many objects should fetched from the database at the same time.
    Returns: | a generator over the objects selected

### Reusing List and Form actions

There is no need to define a different action subclass for form and list actions, as both their model_context have a get_selection method, a single action can be used both for the list and the form.

## Generate documents

Generating reports and documents is an important part of any application. Python and Qt provide various ways to generate documents. Each of them with its own advantages and disadvantages.

> Method | Advantages | Disadvantages
> PDF documents through reportlab | Perfect control over layout Excellent for mass creation of documents | Relatively steep learning curve User cannot edit document
> HTML | Easy to get started Print preview within Camelot No dependencies | Not much layout control User cannot edit document
> Docx Word documents | User can edit document | Proprietary format Word processor needed

Camelot leaves all options open to the developer.

Please have a look at Creating a Report with Camelot to get started with generating documents.

## Inspiration

> - Implementing actions as generators was made possible with the language functions of PEP 342.
> - The EuroPython talk of Erik Groeneveld inspired the use of these features. (http://ep2011.europython.eu/conference/talks/beyond-python-enhanched-generators)
> - Action steps were introduced to be able to take advantage of the new language features of PEP 380 in Python 3.3

### Navigation

- index
- modules |
- next |
- previous |
- Camelot documentation »
- Camelot Enhancement Proposals »

Comments
blog comments powered by Disqus