Visit Based Sessions

CherryPy 3 Sessions

Often you want to associate some information like the preferred language or other settings with individual visitors of your website. CherryPy sessions provides a very convenient way of doing this in your TurboGears application.

To enable CherryPy 3 sessions, you just have to set tools.sessions.on = True in your project’s configs/app.cfg file. There are a few other config settings, primarily tools.sessions.storage_type for configuring where sessions are stored (ram for storing in memory only, file for storing session data in files and postgresql for using a PostGreSQL database).

However, there are also some issues with CherryPy 3 sessions:

  • you can only import cherrypy.session after the server has started
  • the RamSession does not work with mod_wsgi
  • the FileSession is slow, uses locks and needs a writable directory
  • the PostgresqlSession uses its own database connection and table, instead of using the already existing database session and visit table

To overcome the first issue, you can just import cherrypy instead of from cherrypy importing session, and then always refer to the session as cherrypy.session in your controllers. Another solution is to set up the session object manually. You can also implement your own method for storing sessions. That’s not too difficult:

Visit Based Sessions with SQLAlchemy

The fact that TurboGears already comes with a mechanism for tracking visits the website in a database table suggests to use the same table for storing session data as well. You will find its definition in your model.py file, and you only need to add one line to set up an additional session_data column, like this (if you’re using SQLAlchemy):

visits_table = Table('visit', metadata,
    Column('visit_key', String(40), primary_key=True),
    Column('session_data', LargeBinary),
    Column('created', DateTime, nullable=False, default=datetime.now),
    Column('expiry', DateTime)
)

If you already created your visit table, you’ll need to add the column manually or just drop it and create it anew with the additional column.

The following module shows how you can make use of this additional column. You can store it as sessions.py in the package root of your TurboGears project and import it somewhere, e.g. in the __init__.py file in the package root of your TurboGears project:

import sessions

You need to activate it in your configs/app.cfg:

tools.sessions.on = True
tools.sessions.storage_type = 'visit'

Doing so also solves the first problem mentioned above, i.e. you can import the CherryPy session object in your controller and use it in the same way as is possible with the CherryPy request and response objects:

from cherrypy import request, response, session

So here is the code for our sessions.py module:

# -*- coding: utf-8 -*-

"""Add Visit based sessions to CherryPy and activate them.

The default CherryPy 3 sessions have some disadvantages:

* you can only import cherrypy.session after the server has started
* the RamSession does not work with mod_wsgi
* the FileSession is slow, uses locks and needs a writable directory
* the PostgresqlSession uses its own database connection and table,
  instead of using the existing database session and visit table

This SQLAlchemy based module tries to fix these issues by storing
the session data into the visit database table that's used by
TurboGears anyway to track visits to your page.

"""

from datetime import datetime
try:
    import cPickle as pickle
except ImportError:
    import pickle

import cherrypy

from turbogears import config
from turbogears.database import session
from turbogears.util import load_class
from turbogears.visit import current, Visit


class VisitSession(cherrypy.lib.sessions.Session):
    """Implementation of a visit based backend for sessions.

    The CherryPy config must have the following settings to use this::

        tools.sessions.on = True
        tools.sessions.storage_type = "visit"

    The Visit table must have a column for storing the sessions::

        Column('session_data', LargeBinary)

    """

    pickle_protocol = pickle.HIGHEST_PROTOCOL

    def __init__(self, id=None, **kwargs):
        super(VisitSession, self).__init__(id, **kwargs)
        visit_class_path = config.get('visit.saprovider.model',
            'turbogears.visit.savisit.TG_Visit')
        visit_class = load_class(visit_class_path)
        if not visit_class:
            raise ValueError("No visit class found for %s" % visit_class_path)
        self.visit_class = visit_class

    @classmethod
    def setup(cls, **kwargs):
        for k, v in kwargs.items():
            setattr(cls, k, v)

    @property
    def visit(self):
        """The current Visit object."""
        visit = current()
        if isinstance(visit, Visit):
            visit = self.visit_class.lookup_visit(visit.key)
        return visit

    def _exists(self):
        return bool(self.visit)

    def _load(self):
        visit = self.visit
        if visit:
            data = visit.session_data
            data = data and pickle.loads(data) or {}
            return data, visit.expiry

    def _save(self, expiration_time):
        visit = self.visit
        if visit:
            data = self._data
            data = data and pickle.dumps(data, self.pickle_protocol) or None
            session.begin()
            visit.session_data = data
            if expiration_time > visit.expiry:
                visit.expiry = expiration_time
            session.commit()

    def _delete(self):
        visit = self.visit
        if visit:
            visit.session_data = None

    def acquire_lock(self):
        self.locked = True

    def release_lock(self):
        self.locked = False

    def clean_up(self):
        visit_class = self.visit_class
        session.begin()
        session.query(visit_class).filter(
            visit_class.expiry < datetime.now()).update(
            values=dict(session_data=None), synchronize_session=False)
        session.commit()


# Initialize CherryPy

if not hasattr(cherrypy.lib, 'VisitSession'):
    cherrypy.lib.sessions.VisitSession = VisitSession
if not hasattr(cherrypy, 'session'):
    VisitSession.setup()
    cherrypy.session = cherrypy._ThreadLocalProxy('session')

Visit Based Sessions with SQLObject

If you’re using SQLObject instead of SQLAlchemy, you can proceed similarly.

Here you can use a PickleCol for storing the session data in the visit table. Extend the Visit class in your model.py file as follows:

class Visit(SQLObject):
    """A visit to your site."""

    visit_key = StringCol(length=40, alternateID=True,
        alternateMethodName='by_visit_key')
    session_data = PickleCol()
    created = DateTimeCol(default=datetime.now)
    expiry = DateTimeCol()

    def __init__(self, **kw):
        if 'session_data' not in kw:
            kw['session_data'] = None
        SQLObject.__init__(self, **kw)

    @classmethod
    def lookup_visit(cls, visit_key):
        try:
            return cls.by_visit_key(visit_key)
        except SQLObjectNotFound:
            return None

The sessions.py file must be adapted for SQLObject like this:

# -*- coding: utf-8 -*-

"""Add Visit based sessions to CherryPy and activate them.

The default CherryPy 3 sessions have some disadvantages:

* you can only import cherrypy.session after the server has started
* the RamSession does not work with mod_wsgi
* the FileSession is slow, uses locks and needs a writable directory
* the PostgresqlSession uses its own database connection and table,
  instead of using the existing database session and visit table

This SQLObject based module tries to fix these issues by storing
the session data into the visit database table that's used by
TurboGears anyway to track visits to your page.

"""

from datetime import datetime
try:
    import cPickle as pickle
except ImportError:
    import pickle

import cherrypy

from sqlobject.sqlbuilder import Update, AND

from turbogears import config
from turbogears.database import PackageHub
from turbogears.util import load_class
from turbogears.visit import current, Visit

hub = PackageHub('turbogears.visit')
__connection__ = hub


class VisitSession(cherrypy.lib.sessions.Session):
    """Implementation of a visit based backend for sessions.

    The CherryPy config must have the following settings to use this::

        tools.sessions.on = True
        tools.sessions.storage_type = "visit"

    The Visit table must have a column for storing the sessions::

        session_data = PickleCol()

    """

    pickle_protocol = pickle.HIGHEST_PROTOCOL

    def __init__(self, id=None, **kwargs):
        super(VisitSession, self).__init__(id, **kwargs)
        visit_class_path = config.get('visit.soprovider.model',
            'turbogears.visit.sovisit.TG_Visit')
        visit_class = load_class(visit_class_path)
        if not visit_class:
            raise ValueError("No visit class found for %s" % visit_class_path)
        self.visit_class = visit_class

    @classmethod
    def setup(cls, **kwargs):
        for k, v in kwargs.items():
            setattr(cls, k, v)

    @property
    def visit(self):
        """The current Visit object."""
        visit = current()
        if isinstance(visit, Visit):
            visit = self.visit_class.lookup_visit(visit.key)
        return visit

    def _exists(self):
        return bool(self.visit)

    def _load(self):
        visit = self.visit
        if visit:
            return visit.session_data or {}, visit.expiry

    def _save(self, expiration_time):
        visit = self.visit
        if visit:
            hub.begin()
            visit.session_data = self._data
            if expiration_time > visit.expiry:
                visit.expiry = expiration_time
            hub.commit()
            hub.end()

    def _delete(self):
        visit = self.visit
        if visit:
            visit.session_data = None

    def acquire_lock(self):
        self.locked = True

    def release_lock(self):
        self.locked = False

    def clean_up(self):
        if hub:
            hub.begin()
            visit_class = self.visit_class
            try:
                update = Update(visit_class.q,
                    {visit_class.q.session_data.fieldName: None},
                    where=(visit_class.q.expiry < datetime.now()))
                conn = hub.getConnection()
                try:
                    conn.query(conn.sqlrepr(update))
                    hub.commit()
                except:
                    hub.rollback()
                    raise
            finally:
                hub.end()


# Initialize CherryPy

if not hasattr(cherrypy.lib, 'VisitSession'):
    cherrypy.lib.sessions.VisitSession = VisitSession
if not hasattr(cherrypy, 'session'):
    VisitSession.setup()
    cherrypy.session = cherrypy._ThreadLocalProxy('session')