Actions

Introduction

Besides displaying and editing data, every application needs the functions to manipulate data or create reports. In Camelot this is done through actions. Actions can appear as buttons on the side of a form or a table, as icons in a toolbar or as icons in the home workspace.

../_images/new_view_address.png

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:

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 :

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 :

../_images/user_exception.png

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 PyQt4 import QtGui
        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:

../_images/change_object.png

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 :

Issue SQLAlchemy statements

Camelot itself only manipulates the database through objects of the ORM for the sake of make no difference between objects mapped to the database and plain old python objects. But for performance reasons, it is often desired to do manipulations directly through SQLAlchemy ORM or Core queries :

        model_context.session.query( BatchJobType ).update( values = {'name':'accounting audit'},
                                                            synchronize_session = 'evaluate' )

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 when gui_run() is called :

class camelot.admin.action.base.GuiContext[source]

The GUI context in which an action is running. This object can contain references to widgets and other useful 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 when model_run() is called :

class camelot.admin.action.base.ModelContext[source]

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[source]

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 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[source]

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.

session

the active session

Form Actions

A form action has access to the object currently visible on the form.

class BurnToDisk( Action ):
    
    verbose_name = _('Burn to disk')
    
    def model_run( self, model_context ):
        yield action_steps.UpdateProgress( 0, 3, _('Formatting disk') )
        time.sleep( 0.7 )
        yield action_steps.UpdateProgress( 1, 3, _('Burning movie') )
        time.sleep( 0.7 )
        yield action_steps.UpdateProgress( 2, 3, _('Finishing') )
        time.sleep( 0.5 )

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

        #
        # create a list of actions available for the user on the form view
        #
        form_actions = [BurnToDisk()]
../_images/new_view_movie.png

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[source]

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.

view

a camelot.view.controls.view.AbstractView class that represents the view in which the action is triggered.

class camelot.admin.action.form_action.FormActionModelContext[source]

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)[source]
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()[source]
Returns:the object currently displayed in the form, None if no object

is displayed yet

FormActionModelContext.get_selection(yield_per=None)[source]

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

A list action has access to both all the rows displayed in the table (called the collection) and the rows selected by the user (called the selection) :

class ChangeRatingAction( Action ):
    """Action to print a list of movies"""
    
    verbose_name = _('Change Rating')
    
    def model_run( self, model_context ):
        #
        # the model_run generator method yields various ActionSteps
        #
        options = Options()
        yield ChangeObject( options )
        if options.only_selected:
            iterator = model_context.get_selection()
        else:
            iterator = model_context.get_collection()
        for movie in iterator:
            yield UpdateProgress( text = u'Change %s'%unicode( movie ) )
            movie.rating = min( 5, max( 0, (movie.rating or 0 ) + options.change ) )
        #
        # FlushSession will write the changes to the database and inform
        # the GUI
        #
        yield FlushSession( model_context.session )

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

        #
        # the action buttons that should be available in the list view
        #
        list_actions = [ChangeRatingAction()]

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

../_images/table_view_movie.png

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[source]

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.

view

a camelot.view.controls.view.AbstractView class that represents the view in which the action is triggered.

field_attributes

a dictionary with the field attributes of the list. This dictionary will be filled in case if the list displayed is related to a field on another object. For example, the list of addresses of Person will have the field attributes of the Person.addresses field when displayed on the Person form.

class camelot.admin.action.list_action.ListActionModelContext[source]

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.

field_attributes

The field attributes of the field to which the list relates, for example the attributes of Person.addresses if the list is the list of addresses of the Person.

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)[source]
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()[source]
Returns:the object displayed in the current row or None
ListActionModelContext.get_selection(yield_per=None)[source]
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.

Inspiration