Model-View-Controller

Introduction

Here is an example how to use the Model-View-Controller paradigm with Webware. I'll also show how to integrate unit testing into the development.

Requirements

Let's assume the task is to model a basket with apples. You can either add an apple or empty the basket. The basket can hold at most 10 apples.

We'd like to have a web interface for adding apples and emptying the basket. The interface should show the contents of the basket and be able to react to the most common errors (e.g. emptying an empty basket or adding an apple to a full basket).

Design

I'll use three classes: Basket, Basket Controller and Basket Page. The basket (i.e. model) is responsible for administering the data structure and persistence. The controller is responsible for high-level operations on the basket and the translation of exceptions to user messages, and the page (i.e. view) is responsible for presenting the user interface (containing details on the basket) and forwarding user requests to the controller.

The Basket

# file basket.py
import UserList
import fileinput

NoSuchApple = "No such apple!"
BasketEmpty = "The basket is empty"
BasketFull = "The basket is full"


class Basket (UserList.UserList):

    def __init__(self, filename):
        """initialize UserList data structures, set maximum size"""
        UserList.UserList.__init__(self)
        self.max_size = 10
        self.filename = filename
        self.load()

    def putAppleInside(self, a_type):
        """add an apple if the basket is not full"""
        if len(self) == self.max_size:
            raise BasketFull
        self.append(a_type)

    def removeApple(self, a_type):
        """remove an apple of a certain kind, if available"""
        if not self:
            raise BasketEmpty
        if a_type in self:
            self.remove(a_type)
        else:
            raise NoSuchApple

    def empty(self):
        num = len(self)
        if not num:
            raise BasketEmpty
        self.data = []
        self.save()
        return num

    def load(self):
        """load data from text file."""
        self.data = []
        for line in fileinput.input(self.filename):
            self.append(line[:-1])

    def save(self):
        """save data to text file"""
        f = open(self.filename, "w")
        for line in self:
            f.write(line + "\n")
        f.close()

Testing the Basket

The Basket class can be tested e.g. with the following lines:

def testBasket():
    testfile = "basket.txt"
    b = Basket(testfile)

    # test putAppleInside
    b.putAppleInside("jonagold")
    assert len(b) == 1
    assert b[0] == "jonagold"
    b.putAppleInside("granny smith")
    assert  len(b) == 2
    assert b[0] == "jonagold"
    assert b[1] == "granny smith"

    # test empty
    b.empty()
    assert len(b) == 0

    # lots of tests removed...

if __name__ == "__main__":
    testBasket()

How to Execute the Tests

For smaller projects, I usually write tests like this and insert them into the file that is tested. Using the Emacs Python mode, I type Ctrl-C Ctrl-C each time I changed something. This key combination starts the Python interpreter and executes testBasket(). If you are using SciTe instead of Emacs as your editor, you can use the F5 key for this.

If you do not use Emacs, SciTe or a Python IDE, you can run the tests from the commandline via:

python basket.py

or doubleclick it, if you use e.g Windows Explorer.

Larger projects need some more thought - you might e.g. want to switch to the unittest-Framework and / or write a test module which runs all tests that are available. With Emacs, you can import this module as above in every source file, otherwise you can start it as standalone program.

The Basket Controller

As a next step, you add a controller which knows how to modify the basket and report the results to the (not yet implemented) view class:

# file basket_ctl.py
import basket

class BasketController:

    def __init__(self, a_basket):
        self.basket = a_basket

    def awake(self):
        """reload the basket - necessary if someone else has
        put apples inside in the meantime"""
        self.basket.load()

    def addApple(self, name):
        """add an apple and return a string describing if it
        was successful"""
        if not name:
            return "You did not provide a name"
        try:
            self.basket.putAppleInside(name)
            self.basket.save()
            return "Added apple"
        except basket.BasketFull:
            return "The basket is full!"

    def emptyBasket(self):
        try:
            apples = self.basket.empty()
            return "The basket contained " + \
                `apples` + " apples. It's empty now."
        except basket.BasketEmpty:
            return "The basket is already empty!"

For testing this class, you do not need any knowledge about Webware data structures like requests, transactions, sessions etc. The class is not very complex, so the tests are rather straightforward (which means I won't present them here...).

The Basket Page

Finally, the Webware page is written. It communicates with themodel (to present data) and the controller (to modify data). It also translates the Webware specific data structures (transactions, requests and so on) to Webware independent structures that are understood by the controller:

# file BasketPage.py
from WebKit.Page import Page

import basket
import basket_ctl

class BasketPage (Page):
    """Usually, you derive your pages from a customized
    SitePage. I won't do this here."""

    def __init__(self):
        """create model and controller objects"""
        Page.__init__(self)
        self.basket = basket.Basket("basket.txt")
        self.ctl = basket_ctl.BasketController(self.basket)

    def awake(self, transaction):
        """make sure everything is up to date"""
        Page.awake(self, transaction)
        self.ctl.awake()

    def writeBody(self):
        """present the contents of the basket and some
        means to control it"""
        self.writeln("<ul>")
        for item in self.basket:
            self.writeln("<li>%s</li>" % item)
        self.writeln("</ul>")
        self.writeln("<form>")
        self.writeln('<input type=text name="apple">')
        self.writeln('<input type=submit name="_action_" value="Add apple">')
        self.writeln('<input type=submit name="_action_" value="Empty basket">')

    def addApple(self):
        """extract the kind of apple and forward the request to the
        controller which tells you what to respond"""
        req = self.request()
        self.show_response(self.ctl.addApple(req.field("apple")))

    def emptyBasket(self):
        """forward the request to empty the basket to the
        controller"""
        self.show_response(self.ctl.emptyBasket())

    def show_response(self, response):
        """a helper method to present the response"""
        self.writeln(response)
        self.writeln('<p><a href="BasketPage">Back to overview</a></p>')

    # these are the usual, webware specific methods for
    # easing the use of forms

    def actions(self):
        """return the list of all actions that can be called"""
        return Page.actions(self) + ["addApple", "emptyBasket"]

    def methodNameForAction(self, name):
        """return the names for the actions"""
        return {"Add apple": "addApple",
                "Empty basket": "emptyBasket"}[name]

As you can see, this page does not contain much logic. Most of the issues will be about the layout - and this is something that cannot be automated anyway ;-)

-- AlbertBrandl - 09 Nov 2001