Add an import wizard to an application — Camelot documentation

### Navigation

- index
- modules |
- next |
- previous |
- Camelot documentation »
- Camelot Documentation contents »
- Tutorials »

# Add an import wizard to an application

Note

This tutorial depends on functionallity under development in the trunk. This functionallity is not yet available in the latest release.

In this tutorial we will add an import wizard to the movie database application created in the Creating a Movie Database Application tutorial.

We assume Camelot is properly installed and the movie database application is working.

## Introduction

Most applications need a way to import data. This data is often delivered in files generated by another application or company. To demonstrate this process we will build a wizard that allows the user to import cover images into the movie database. For each image the user selects, a new Movie will be created with the selected image as a cover image.

## Create an action

All user interaction in Camelot is handled through Actions. For actions that run in the context of the application, we use the Application Actions. We first create a file `importer.py` in the same directory as `application_admin.py`.

In this file we create subclass of `camelot.admin.action.Action` which will be the entry point of the import wizard:

    from camelot.admin.action import Action
    from camelot.core.utils import ugettext_lazy as _

    class ImportCovers( Action ):
        verbose_name = _('Import cover images')

        def model_run( self, model_context ):
            yield

So now we haven an `ImportCovers` action. Such an action has a `verbose_name` class attribute with the name of the action as shown to the user.

The most important method of the action is the `model_run` method, which will be triggered when the user clicks the action. This method should be a generator that yields an object whenever user interaction is required. Everything that happens inside the `model_run` method happens in a different thread than the GUI thread, so it will not block the GUI.

## Add the action to the GUI

Now the user needs to be able to trigger the action. We edit the `application_admin.py` file and make sure the `ImportCoversAction` is imported.

            from camelot_example.importer import ImportCovers

Then we add an instance of the `ImportCovers` action to the sections defined in the `get_sections` method of the `ApplicationAdmin`:

                    Section( _('Movies'),
                             self,
                             Icon('tango/22x22/mimetypes/x-office-presentation.png'),
                             items = [ Movie,
                                       Tag,
                                       VisitorReport,
                                       VisitorsPerDirector,
                                       ImportCovers() ]),

This will make sure the action pops up in the Movies section of the application.

## Select the files

To make the action do something usefull, we will implement its `model_run` method. Inside the `model_run` method, we can `yield` various `camelot.admin.action.base.ActionStep` objects to the GUI. An `ActionStep` is a part of the action that requires user interaction (the user answering a question). The result of this interaction is returned by the `yield` statement.

To ask the user for a number of image files to import, we will pop up a file selection dialog inside the `model_run` method:

        def model_run( self, model_context ):
            from camelot.view.action_steps import SelectFile, UpdateProgress, Refresh

            select_image_files = SelectFile( 'Image Files (*.png *.jpg);;All Files (*)' )
            select_image_files.single = False
            file_names = yield select_image_files
            file_count = len( file_names )

The `yield` statement returns a list of file names selected by the user.

## Create new movies

First make sure the `Movie` class has an `camelot.types.Image` field named `cover` which will store the image files.

        cover = Field(camelot.types.Image(upload_to='covers'))

Next we add to the `model_run` method the actual creation of new movies.

            import os
            from sqlalchemy import orm
            from camelot_example.model import Movie

            movie_mapper = orm.class_mapper( Movie )
            cover_property = movie_mapper.get_property( 'cover' )
            storage = cover_property.columns[0].type.storage

            for i, file_name in enumerate(file_names):
                yield UpdateProgress( i, file_count )
                title = os.path.splitext( os.path.basename( file_name ) )[0]
                stored_file = storage.checkin( file_name )
                movie = Movie( title = title )
                movie.cover = stored_file

            Movie.query.session.flush()

In this part of the code several things happen :

Store the images

In the first lines, we do some sqlalchemy magic to get access to the `storage` attribute of the `cover` field. This `storage` attribute is of type `camelot.core.files.storage.Storage`. The `Storage` represents the files managed by Camelot.

Create Movie objects

Then for each file, a new `Movie` object is created with as title the name of the file. For the `cover` attribute, the file is checked in into the `Storage`. This actually means the file is copied from its original directory to a directory managed by Camelot.

Write to the database

In the last line, the `session` is flushed and thus all changes are written to the database.

Keep the user informed

For each movie imported, a `camelot.view.action_steps.update_progress.UpdateProgress` object is `yield` to the GUI to inform the user of the import progress. Each time such an object is yielded, the progress bar is updated.

## Refresh the GUI

The last step of the `model_run` method will be to refresh the GUI. So if the user has the `Movies` table open when importing, this table will show the newly created movies.

            yield Refresh()

## Result

This is how the resulting `importer.py` file looks like :

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

    class ImportCovers( Action ):
        verbose_name = _('Import cover images')
        icon = Icon('tango/22x22/mimetypes/image-x-generic.png')

    # begin select files
        def model_run( self, model_context ):
            from camelot.view.action_steps import SelectFile, UpdateProgress, Refresh

            select_image_files = SelectFile( 'Image Files (*.png *.jpg);;All Files (*)' )
            select_image_files.single = False
            file_names = yield select_image_files
            file_count = len( file_names )
    # end select files
    # begin create movies
            import os
            from sqlalchemy import orm
            from camelot_example.model import Movie

            movie_mapper = orm.class_mapper( Movie )
            cover_property = movie_mapper.get_property( 'cover' )
            storage = cover_property.columns[0].type.storage

            for i, file_name in enumerate(file_names):
                yield UpdateProgress( i, file_count )
                title = os.path.splitext( os.path.basename( file_name ) )[0]
                stored_file = storage.checkin( file_name )
                movie = Movie( title = title )
                movie.cover = stored_file

            Movie.query.session.flush()
    # end create movies
    # begin refresh
            yield Refresh()
    # end refresh

## Unit tests

Once an action works, its important to keep it working as the development of the application continues. One of the advantages of working with generators for the user interaction, is that its easy to simulate the user interaction towards the `model_run()` method of the action. This is done by using the `send()` method of the generator that is returned when calling `model_run()` :

        def test_example_application_action( self ):
            from camelot_example.importer import ImportCovers
            from camelot_example.model import Movie
            # count the number of movies before the import
            movies = Movie.query.count()
            # create an import action
            action = ImportCovers()
            generator = action.model_run( None )
            select_file = generator.next()
            self.assertFalse( select_file.single )
            # pretend the user selected a file
            generator.send( [os.path.join( os.path.dirname(__file__), '..', 'camelot_example', 'media', 'covers', 'circus.jpg') ] )
            # continue the action till the end
            list( generator )
            # a movie should be inserted
            self.assertEqual( movies + 1, Movie.query.count() )

## Conclusion

We went through the basics of the action framework Camelot :

> - Subclassing a `camelot.admin.action.Action` class
> - Implementing the `model_run` method
> - `yield` `camelot.admin.action.base.ActionStep` objects to interact with the user
> - Add the `camelot.admin.action.base.Action` object to a `camelot.admin.section.Section` in the side pane

More `camelot.admin.action.base.ActionStep` classes can be found in the `camelot.view.action_steps` module.

### Navigation

- index
- modules |
- next |
- previous |
- Camelot documentation »
- Camelot Documentation contents »
- Tutorials »

Comments
blog comments powered by Disqus