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.

1 comment:

Unknown said...

You don't need to specify the column name when defining your fields:

e.g.:
user_id = Column('user_id', Integer, ForeignKey(User.user_id))

Can be just a little bit simpler:
user_id = Column(Integer, ForeignKey(User.user_id))

Post a Comment