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!

Saturday 5 February 2011

Improving the Cart

Updating the Model

In my second update I will make some improvements to the shopping cart model, notably allowing multiple products to be added to the cart and fixing the remove() method so it actually works.

class Cart(object):
    """
    Shopping cart stores products in the session and persists to a database
    """
    # Stores the items
    __items = dict()

    # Constructor
    def __init__(self):
        # Get cartID from the session if it exists
        cartID = session.get('cartID', None)
        # If cartID is set then load an instance of the database model
        # populated with existing items
        if cartID:
            cart = CartModel()
            self.cartModel = cart.get(cartID)
        # If not then create a new instance and save self to the database
        # and set cartID in the session
        else:
            cart = CartModel(object=self, status=0)
            DBSession.add(cart)
            transaction.commit()
            session['cartID'] = cart.id
            self.cartModel = cart

    # Main methods
    def add(self, product, qty):
        # Save dictionary of product attributes
        items = dict({'id': product.id,
          'qty': qty,
          'description': product.name,
          'price': product.price})
        self.set_items(items)
        self.save()

    def remove(self, productID):
        # Remove an item
        items = self.get_items()
        if int(productID) in items:
            del items[int(productID)]

        self.save()

    def clear(self):
        # Clear items from session, but NOT database as we will need
        # a record of transactions
        session.delete()

    def save(self):
        # Persist changes
        session.save()
        self.cartModel.object = self
        transaction.commit()

    # Getters/Setters
    def set_items(self, items):
        self.__items[items['id']] = items
        session['items'] = self.__items

    def get_items(self):
        if session.get('items', None):
            self.__items = session['items']
        return self.__items


All I have done here is change the set_items() method to properly append the items dict to the self.__items dict. The remove method was puzzling to me coming from a background in PHP until I remembered that the keys in a Python dictionary can be any type (except another dictionary) and that the productID passed into it from the controller was a string and the key is an integer. In PHP this would not matter, but Python was throwing a KeyError, but explicitly casting productID to an integer solved this little issue.

Now the cart properly handles adding and removing products from it.

The Controller

I will briefly go over the controller functions in root.py. I think in my finished application I will move all cart related actions into a separate controller but for now when I am mainly just debugging my models I shall leave them in the root controller.

@expose()
    def add_to_cart(self, productID, quantity):
        # Get cart instance
        cart = self.get_cart()
        # create a new product object
        product = Product()
        # add product to cart
        cart.add(product.get(productID), quantity)
        # Feedback and redirect
        flash('Product added')
        redirect('/')

    @expose()
    def remove_from_cart(self, productID):
        # Get cart
        cart = self.get_cart()
        # Remove productID from cart
        cart.remove(productID)
        # Feedback and redirect
        flash('Product removed')
        redirect('/')

These are fairly self-explanatory, we simply call the instance of the cart add/remove and then set a flash message and redirect back to the home page.

In my next update I will bring in an address model and start creating some checkout logic so that the cart can do something useful.

Friday 4 February 2011

Building a Shopping Cart using TurboGears

Overview

This is my first foray into the world of Python and I am trying to build an e-commerce website. I have chosen TurboGears as my framework after messing about with Django and Web2Py it fits my needs well as a professional PHP developer with a lot of experience coding in Zend. The primary reason is that is uses the excellent SQLAlchemy ORM and unlike Django, follows a more traditional MVC design pattern.

After hours of fruitless searching for a decent tutorial that covers building a shopping cart I decided to take my experience as a Zend developer and try to do it in Python, like I would do it in PHP. I am documenting my progress to aid others, and also for the developer community to help me with my efforts. So here goes…

After setting up my models and tables, which I won't go into too much here, the main effort is building a shopping cart class that performs the following functions:
  • Add products to the cart
  • Remove products from the cart
  • Persist changes to both the web session and also to a database 

Cart Database Model

class CartModel(DeclarativeBase, Defaults):
  # Table name
  __tablename__ = 'carts'
  # Columns
  id = Column('id', Integer, primary_key=True, autoincrement=True)
  user_id = Column('user_id', Integer, ForeignKey(User.user_id))
  object = Column('object', PickleType)
  create_date = Column('create_date', DateTime, default=func.now())
  update_date = Column('update_date', DateTime, onupdate=func.now())
  status = Column('status', String)

  def get(self, cartID):
    query = DBSession.query(CartModel).get(cartID)
    return query

# Set relationships
CartModel.user = relation(User, primaryjoin=CartModel.user_id==User.user_id)

Basically this class saves a serialised copy of the cart object (which I will detail next) in the object field. SQLALchemy is great for this, the PickleType field will automatically serialise/unserialise Python objects. Doing this in PHP would be a pain in the backside. We also relate carts to users and define some metadata about the cart.


Shopping Cart Model

class Cart(object):
  __items = dict()

  def __init__(self):
    cartID = session.get('cartID', None)

    if cartID:
      cart = CartModel()
      self.cartModel = cart.get(cartID)
    else:
      cart = CartModel(object=self, status=0)
      DBSession.add(cart)
      transaction.commit()
      session['cartID'] = cart.id
      self.cartModel = cart

  def add(self, product, qty):
    items = dict(id=product.id, qty=qty, description=product.name, price=product.price)
    self.set_items(items)
    self.save()

  def remove(self, product):
    items = self.get_items()
    del items[product.id]
    self.set_items(items)
    self.save()

  def clear(self):
    session.delete()

  def save(self):
    session.save()
    self.cartModel.object = self
    transaction.commit()

  def set_items(self, items):
    self.__items[items['id']] = items['qty']
    session['items'] = items

  def get_items(self):
    if session.get('items', None):
      self.__items = session['items']
    return self.__items  

Here I have chosen to represent my cart items as a dict, self.__items the add and remove methods basically take attributes from the Product object that's passed to it and saves them to this dict.

The __init__() method is responsible for ensuring that the database and the session are synchronised. The cartID which is the primary key is saved in the session and if it exists then we create a CartModel object from this primary key, if not we create a new row and save the primary key in the session so subsequent actions all act on the same database row.

This is really in it infancy at the moment and I am not really sure if I can refactor certain functions to be more pythonic.

Your comments and improvements are more than welcome.