Rumifying your models - A tutorial

In this tutorial we will see how we can use Rum to rapidly create a CRUD interface for the objects of an existing domain model. Well, not existing actually since we’ll be defining it right now, but take note how the model objects are totally agnostic about Rum’s existence, which means we could have rummified an existing model. After completing this tutorial you should be able to configure a Rum instance with your own models.

Assumptions

This tutorial assumes you already have Rum and the two other plugins installed inside a virtualenv as described in the Installation document. It also assumes you are using python 2.5 or have manually installed PySqlite since we will be using a sqlite database. Finally, it also assumes you have some basic SQLAlchemy knowledge, in particular that you know how to use the ORM layer up to the very basics. If this is not the case, please refer to the SQLAlchemy Documentation (which is one of the best ones in the Python world) in case you get stuck.

Create your domain model

In this tutorial we’ll be modeling the movie rental system of Jollywood’s largest movie store. Our clients are usually actors and directors, although other persons can rent movies too. Since Jollywoods stars are ones of the most self-centered creatures on earth, they tend to forget what their buddies are up to, let alone what the movies they haven’t worked on are about (sometimes not even these!). In order to help them we will store a brief synopsis about each movie. Ah, for some reason, Jollywood’s directors tend to break a lot of chairs, in fact, the’re a whole chair industry flourishing in Jollywood just because of this, so we’ll also keep track of how many chairs they’ve broken (this is because we get some extra income selling this information to the chair industry, but I’m digressing now...)

After a couple of informal UML sessions, we end up with this model.

../_images/model.png

Ok, now lets code... We’ll be using SQLAlchemy’s declarative extensionn since the code looks cleaner and we haven’t got such a complex model that we need to go down to the lower ORM levels. Open up your editor and type (or paste) the following code into a file named model.py:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
import datetime

from sqlalchemy import Column, ForeignKey, Table, PrimaryKeyConstraint
from sqlalchemy.types import *
from sqlalchemy.orm import relation
from sqlalchemy.ext.declarative import declarative_base

Model = declarative_base()

class Person(Model):
    __tablename__ = "person"

    id = Column('id', Integer, primary_key=True)
    name = Column('name', Unicode, nullable=False)
    type = Column('type', String(50), nullable=False)
    age = Column('age', Integer)

    __mapper_args__ = {
        'polymorphic_on':type,
        'polymorphic_identity': 'Person'
        }

    def __unicode__(self):
        return self.name


class Actor(Person):
    __tablename__ = "actor"

    id = Column('id', Integer, ForeignKey('person.id'), primary_key=True)
    oscars_won = Column('oscars_won', Integer, default=0)
    __mapper_args__ = {
        'polymorphic_identity': 'Actor',
        }


class Director(Person):
    __tablename__ = "director"

    id = Column('id', Integer, ForeignKey('person.id'), primary_key=True)
    chairs_broken = Column('chairs_broken', Integer, default=0)
    __mapper_args__ = {
        'polymorphic_identity': 'Director',
        }


class Genre(Model):
    __tablename__ = "genre"

    id = Column('id', Integer, primary_key=True)
    name = Column('name', Unicode, nullable=False)
    
    def __unicode__(self):
        return self.name

# This is a database table we won't map to a class since it's only an
# implementation detail required to create many-to-many associations in a
# relational database.
_actor_movie = Table('actor_movie', Model.metadata,
    Column('actor_id', Integer, ForeignKey('actor.id')),
    Column('movie_id', Integer, ForeignKey('movie.id')),
    PrimaryKeyConstraint('movie_id', 'actor_id'),
    )


class Movie(Model):
    __tablename__ = "movie"

    id = Column('id', Integer, primary_key=True)
    title = Column('title', Unicode, nullable=False)
    filmed_on = Column('filmed_on', Date)
    genre_id = Column('genre_id', Integer, ForeignKey('genre.id'))
    director_id = Column('director_id', Integer, ForeignKey('director.id'))
    synopsis = Column('synopsis', Unicode)
    
    genre = relation('Genre', backref='movies')
    director = relation('Director', backref='movies')
    actors = relation('Actor', secondary=_actor_movie, backref='movies')

    def __unicode__(self):
        ret = self.title
        if self.filmed_on:
            ret += " (%d)" % self.filmed_on.year
        return ret

class Rental(Model):
    __tablename__ = "rental"
    id = Column('id', Integer, primary_key=True)
    person_id = Column('person_id', Integer, ForeignKey('person.id'),
                       nullable=False)
    movie_id = Column('movie_id', Integer, ForeignKey('movie.id'),
                      nullable=False)
    date = Column('date', DateTime)
    due_date = Column('due_date', DateTime)

    movie = relation('Movie', backref='rentals')
    person = relation('Person', backref='rentals')

    def is_overtime(self):
        return self.due_date > datetime.datetime.now()

    def __unicode__(self):
        return u"%s -> %s" % (self.movie, self.person)

We’ll now create some test data from an interactive Python shell which will also help us to test the model is properly mapped (this is no substitute for unittests! but since this is a tutorial we might get away with it)

>>> import model
>>> from sqlalchemy import create_engine
>>> from sqlalchemy.orm import create_session
>>> engine = create_engine('sqlite:///movie.db')
>>> model.Model.metadata.create_all(engine)
>>> session = create_session(bind=engine)
>>> actor = model.Actor(name=u'Sean Jollystar')
>>> director = model.Director(name=u'Lucy Jollygood')
>>> genre = model.Genre(name=u'Comedy')
>>> movie = model.Movie(title=u'Jolly Summer', director=director,
...                     actors=[actor], genre=genre)
>>> session.add(movie) # SA will take care of saving all the object graph
>>> session.flush()

We’ve just created an Actor, a Director and a Movie which will include them both in the credits. These have been saved in the movie.db sqlite database in the current directory. Let’s do a simple query to confirm it.

>>> session.clear() # removes the objects we've created before from memory
...                 # so we make sure they're actually fetched from the db
>>> session.query(model.Person).all()
[<model.Director object at ...>, <model.Actor object at ...>]

Great, they’re saved. Notice that we have asked the session to retrieve us all objects of kind Person. Since both Actor and Director are a Person (ie: they inherit from the Person class) the session retrieves both Lucy and Sean.

Note

Do not confuse this session with an HTTP session. “Session”, in SQLAlchemy parlance, is an object that tracks changes to the in-memory objects and shuttles them to the underlying database. It is also used to retrieve objects from the database, delete them and save new ones we create.

Configure Rum to create an interface for your model

Rum will try to gather as much information about your domain models by introspecing the classes so a functional (albeit probably not very usable) interface can be created with the minimum amount of code.

Model introspection can be avoided completely, or partially, by giving hints to Rum about what each attribute in the model actually is. This allows a very powerful and flexible way to fine-tune how the resulting interface will look and behave like.

However, this tutorial will not cover how to customize Rum, only the basic setup needed to create a Rum instance and plug it into a WSGI server. For detailed information on how Rum can be customized please refer to the Customizing the way Rum interprets models document.

Ok, enough talk, here is what the code needed to interface our model looks like. Paste the following code into a file called app.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import sys
import logging
from optparse import OptionParser

from sqlalchemy import create_engine
from paste.deploy import loadserver

from rum import RumApp

from model import Model, Person, Genre, Actor, Director, Movie, Rental

#
# A parser for command line options
#
parser = OptionParser()
parser.add_option('', '--dburl',
                  dest='url',
                  help='SQLAlchemy database uri (eg: postgres:///somedatabase)',
                  default='sqlite:///rum_demo.db')
parser.add_option('-d', '--debug',
                  dest='debug',
                  help='Turn on debug mode',
                  default=False,
                  action='store_true')

#
# Makes the app
#
def load_app(url, debug=False):
    models = [Person, Genre, Actor, Director, Movie, Rental]
    return RumApp({
        'debug': debug,
        'rum.repositoryfactory': {
            'use': 'sqlalchemy',
            'models': models,
            'sqlalchemy.url': url,
            'session.transactional': True,
        },
        'rum.viewfactory': {
            'use': 'toscawidgets',
        }
    })

#
# Main calling point
#
def main(argv=None):
    logging.basicConfig(level=logging.INFO, stream=sys.stderr)
    opts, args = parser.parse_args(argv)
    Model.metadata.create_all(bind=create_engine(opts.url))
    app = load_app(opts.url, opts.debug)
    server = loadserver('egg:Paste#http')
    try:
        server(app)
    except (KeyboardInterrupt, SystemExit):
        print "Bye!"

if __name__ == '__main__':
    sys.exit(main(sys.argv))

Some of the code you just saw is boiler-plate needed to parse command line arguments in a sane way. The gist is at line 29 in the load_app() function. In this function we instantiate a rum.RumApp and configure it. Configuration is passed in a dictionary (can also be a path to an .ini file with a special syntax, see Deploying a Rum application for details)

The dictionary under rum.repositoryfactory configures the plugin we will use to access our model. In this case we say that we want the sqlalchemy plugin (which lives inside RumAlchemy) and we pass it some specific configuration keys:

models
is a list of the domain model classes we want an interface for.
sqlalchemy.url.
is the url of the database those models are persisted in (we will pass the sqlite:///movie.db url we used before in the interactive session).
session.transactional
tells SQLAlchemy that the session should use a database transaction so we can make sure data is never left in an inconsistent state if an error occurs while serving a request

Note

It is possible to pass a callable as the session_factory key if we need a specially configured SQLAlchemy session.

The dictionary under rum.viewfactory configures the plugin we will use to generate views for our models. In this case we use toscawidgets which lives inside tw.rum.

Finally, the main() function at line 47 is the one that will be called when we execute the script. It configures the logging subsystem, parses the command-line arguments, creates the database tables if they don’t exist yet, loads a WSGI server using PasteDeploy’s API and finally starts the server to serve the rum.RumApp load_app() returns.

Launch Rum

Now can see Rum in action, execute the script and pass it the URL of the database we’ve created before:

$ python app.py --dburl sqlite:///movie.db

A HTTP server should start listening at port 8080 in the loopback interface. If you now visit http://localhost:8080/actors you should see something like this:

../_images/actors.png

Now go ahead and use the interface. Create some objects, delete them, create a bunch of them and paginate them, build queries to filter them, etc...

Summary

In this tutorial we have seen how we can configure Rum to generate a basic CRUD interface with very little code. We have also seen how plugins are activated and configured.

However, we are probably not very satisfied with the way the interface looks like at this point. For example, we need to have HTML in the synopsis field of movie since we’d like to link some concepts to their wikipedia entries since Jollywoodians are not the most literate folks around.

Read on into Customizing the way Rum interprets models to see how you can help Rum to better understand our models.