Saturday 12 February 2011

Using Flask and ToscaWidgets

What is Flask?

Flask is yet another web framework for Python. It is based on Werkzeug for its controller functions, Jinja2 for the views and can use the excellent SQLAlchemy ORM for the M part of the MVC triad. Above all, Flask is a lightweight and fun framework to develop with. Although it lacks some of the features that Django and Turbogears have out-of-the-box, the real beauty of Flask is that it is simple to add extra functionality as needed, without it interfering with pre-existing components.

Introducing ToscaWidgets

ToscaWidgets is a Python widget library and has many easy to use widgets such as date pickers, captcha forms and trees. The most compelling reason to use ToscaWidgets for me is to use the Sprox form generation library. As a web developer you find half your working life is spent with forms, it's such an important yet undeniably tedious part of the job. Sprox introspects your SQLAlchemy models and automatically generates your forms, making a dreary task much more manageable.

A Load of CRUD

The Flask website has a great quickstart tutorial you can use to get you started so I am not going to re-invent the wheel here, it also details the Flask-SQLALchemy package which you will obviously need here.

Before we can use Sprox we need to wrap the ToscaWidgets middleware into the main Flask WSGI app. In the file where you define your Flask application do something similar to this:

from tw.api import make_middleware

# Create the WSGI app
app = Flask(__name__)
app.debug = True

# Insert ToscaWidgets Middleware
app.wsgi_app = make_middleware(app.wsgi_app, stack_registry=True)

There are several configuration options here for the middleware detailed on the ToscaWidgets website but the important one here is stack_registry=True without this, the middleware will not stack correctly and ToscaWidgets will not run correctly.

Once this runs correctly we can start using Sprox to rapidly knock together some CRUD (Create Records, Update, Delete) functionality for our little app.

I created a separate module for each of my models and I am going to use an image model for this example so I can show you how easy it is to upload images into a database using this setup.

The forms module:

from myapp.models.items import Image
from sprox.dojo.formbase import DojoEditableForm,  DojoAddRecordForm
from flask import url_for
from myapp.database import db_session
from formencode import validators
from sprox.tablebase import TableBase
from sprox.fillerbase import TableFiller,EditFormFiller
import formencode

class UniqueSlug(formencode.FancyValidator):

    model = None

    def _to_python(self, value, state):
        if not self.model is None:
            values = db_session.query(self.model.slug).first()
            if values is None:
                return value
        else:
            raise NameError('model is not defined')
        if unicode(value) in values:
            raise formencode.Invalid('That slug already exists', value, state)
        return value

class FormDefaults(DojoAddRecordForm):
    __omit_fields__ = ['id', 'discriminator', 'create_date', 'update_date']

class ImageForm(FormDefaults):
    __model__ = Image
    slug = UniqueSlug(model=Image)
    image = validators.FieldStorageUploadConverter(if_missing=None)

class EditImageForm(DojoEditableForm):
    __model__ = Image
    __omit_fields__ = ['id', 'slug']
    image = validators.FieldStorageUploadConverter(if_missing=None)

class EditImageFormFiller(EditFormFiller):
    __model__ = Image

class ImageList(TableBase):
    __model__ = Image
    __xml_fields__ = ['image']
    __omit_fields__ = ['alt_text']

class ImageListFiller(TableFiller):
    __model__ = Image

    def __actions__(self, obj):
        """Override this function to define how action links should be displayed for the given record."""
        primary_fields = self.__provider__.get_primary_fields(self.__entity__)
        pklist = '/'.join(map(lambda x: str(getattr(obj, x)), primary_fields))
        value = '<div><div><a class="edit_link" href="'+str(url_for('edit_image', id=pklist))+'" style="text-decoration:none">edit</a>'\
              '</div><div>'\
              '<form method="POST" action="'+str(url_for('delete_image', id=pklist))+'" class="button-to">'\
            '<input type="hidden" name="_method" value="DELETE" />'\
            '<input class="delete-button" onclick="return confirm(\'Are you sure?\');" value="delete" type="submit" '\
            'style="background-color: transparent; float:left; border:0; color: #286571; display: inline; margin: 0; padding: 0;"/>'\
        '</form>'\
        '</div></div>'
        return value

    def image(self, obj):
        image = ', '.join(['<img width="100px" height="75px" src="/image/'+str(obj.slug)+'" alt="'+str(obj.alt_text)+'" />'])
        return image.join(('<div>', '</div>'))

create_image_form = ImageForm(db_session)
edit_image_form = EditImageForm(db_session)
edit_image_form_filler = EditImageFormFiller(db_session)
image_list = ImageList(db_session)
image_list_filler = ImageListFiller(db_session) 

This should be fairly self explanatory to anybody who has read the sometimes sparse Sprox documentation. Note here that I have overridden the __actions__() method, this method creates the edit/delete links in the table. Sprox is used mainly with TurboGears so the default URLs work with TurboGears but need modifying to work with Flask. Also I have created a custom validator to ensure that the slug is always unique. This functionality probably exists out-of-the-box but it serves a purpose here as a demonstration of how to do it.

Also note that we are setting the flag if_missing=None on the image field validators. This is because in the controller we are passing request.form to the form's validate method, file uploads are stored in the request.files structure. We will always get a validation error about the image filed being empty. If you need to validate file uploads, and I strongly recommend you do, then do it separately from Sprox and Formencode using the Flask-Upload package.

Next create some controllers that handle listing, creating, editing and deleting.

### Image CRUD controllers
@admin.route('/images')
def list_image():
    value = image_list_filler.get_value()
    return render_template('admin_list.html', list=image_list, name='Images', value=value, create_url=url_for('create_image'))

@admin.route('/edit_image/<id>', methods=['GET', 'POST'])
def edit_image(id):

    if request.method == 'POST':
        try:
            edit_image_form.validate(request.form)
        except Invalid as error:
            flash(error.msg)
            return redirect(url_for('create_image'))
        file = request.files['image']
        image = db_session.query(Image).get(id)
        image.alt_text = request.form['alt_text']

        if not file.filename is None:
            image.image = file.read()

        db_session.commit()
        return redirect(url_for('list_image'))

    value = edit_image_form_filler.get_value(values={'id': id})
    return render_template('admin_edit.html', form=edit_image_form, name='Edit Image', value=value)

@admin.route('/create_image', methods=['GET', 'POST'])
def create_image():
    
    if request.method == 'POST':
        try:
            create_image_form.validate(request.form)
        except Invalid as error:
            flash(error.msg)
            return redirect(url_for('create_image'))
        file = request.files['image']
        image = Image(slug=request.form['slug'], alt_text=request.form['alt_text'], image=file.read())
        db_session.add(image)
        db_session.commit()
        flash('Image saved')
        return redirect(url_for('list_image'))
    return render_template('admin_form.html', form=create_image_form, name='Create Image')

@admin.route('/delete_image/<id>', methods=['GET', 'POST'])
def delete_image(id):
    image = db_session.query(Image).get(id)

    if image is None:
        flash('Image not found')
    else:
        db_session.delete(image)
        db_session.commit()
        flash('Image deleted')

    return redirect(url_for('list_image'))

Just create some templates that display the forms and you are done!

No comments:

Post a Comment