MVC with CherryPy and Jinja2

Want to build an MVC application on top of CherryPy and Jinja2?

You can download the code I use as a foundation. The code I wrote is released under the MIT license.

I've included libraries such as jQuery, Underscore, and VueJS, along with some code from the CherryPy tools website. These libraries are included under their own licenses.

Download

Prerequisites

This tutorial assumes you have installed Python, CherryPy, and Jinja2. I'm using PyCharm as an IDE.

Folder Structure (Models, Views, and Controllers)

I've included jQuery, UnderscoreJS, and VueJS, as a matter of personal preference, but they are not required.

The directory and file names are lowercase since I use Linux primarily (and case matters).

shows CherryPyMVC directory structure, open in PyCharm

The Base Controller

Here are the contents of base.py, which provides a base class for the Controllers:

import inspect
import cherrypy
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
from controllers.jinjahelper import JinjaHelper
from models.loghelper import Logger


class BaseController:
    def render_template(self, path, template_vars=None):
        template_vars = template_vars if template_vars else {}
        try:
            session = cherrypy.session

            # set variables in the page so you know whether or not a user is logged in. I
            # use this because I have different menus defined in the layout, and a user
            # will see a different menu when they're logged in. This code is optional;
            # remove it if you have different code for handling sessions.
            template_vars['logged_in'] = '0'
            template_vars['isUserLoggedIn'] = False
            if session:
                if 'logged_in_user' in session and session['logged_in_user'] is not None:
                    template_vars['isUserLoggedIn'] = True
                    template_vars['logged_in'] = '1'
                    template_vars['logged_in_user'] = session['logged_in_user']
                    template_vars['logged_in_username'] = session['logged_in_username']

            # set our base path for loading templates, and load the template view file
            jh = JinjaHelper(cherrypy.site['base_path'])
            tpl = jh.get_template(path)

            if not tpl:
                Logger.error('Error rendering template: ' + path, None, True)

            return tpl.render(template_vars)
        except Exception as ex:
            Logger.error('Error rendering template', ex, True)

The render_template function uses Jinja2 to help render pages.

The Home Controller

Here's the code for home.py, a sample controller to show a home page:

import cherrypy
from controllers.base import BaseController


class HomeController(BaseController):
    @cherrypy.expose
    def index(self):
        return self.render_template('home/index.html')

Running CherryPy

To run the CherryPy server, we have a run.py:

import cherrypy
import os
from controllers.home import *
from site_config import SiteConfig
from controllers.auth import require, member_of, name_is
from models.loghelper import Logger
from models.dbtool import DbTool


# this method returns HTML when a 404 (page not found error) is encountered.
# You'll probably want to return custom HTML using Jinja2.
def error_page_404(status, message, traceback, version):
    a = cherrypy.request
    b = cherrypy.url()
    return "404 Error!"


# this returns an HTML error message when an exception is thrown in your code in production.
# This to avoid showing a stack trace with sensitive information.
def handle_error():
    cherrypy.response.status = 500
    cherrypy.response.body = [
        "<html><body>Sorry, an error occured</body></html>".encode()
    ]


class RootController:
    @cherrypy.expose
    def index(self, *args, **kwargs):
        c = HomeController()
        return c.index()


def start_server():
    cherrypy.site = {
        'base_path': os.getcwd()
    }

    # make sure the directory for storing session files exists
    session_dir = cherrypy.site['base_path'] + "/sessions"

    if not os.path.exists(session_dir):
        os.makedirs(session_dir)

    # this is where I initialize a custom tool for connecting to the database, once for each
    # request. Edit models/dbtool.py and uncomment the tools.db lines below to use this.
    # cherrypy.tools.db = DbTool()

    server_config = {
        # This tells CherryPy what host and port to run the site on (e.g. localhost:3005/)
        # Feel free to set this to whatever you'd like.
        'server.socket_host': '0.0.0.0',
        'server.socket_port': 3005,

        'error_page.404': error_page_404,
        'engine.autoreload.on': False,

        # this indicates that we want file-based sessions (not stored in RAM, which is the default)
        # the advantage of this is that you can stop/start CherryPy and your sessions will continue
        'tools.sessions.on': True,
        'tools.sessions.storage_type': "file",
        'tools.sessions.storage_path': session_dir,
        'tools.sessions.timeout': 180,

        # this is a custom tool for handling authorization (see auth.py)
        'tools.auth.on': True,
        'tools.auth.priority': 52,
        'tools.sessions.locking': 'early'

        # uncomment the below line to use the tool written to connect to the database
        # 'tools.db.on': True
    }

    if SiteConfig.is_prod:
        server_config['request.error_response'] = handle_error

    cherrypy.config.update(server_config)

    # this will let us access localhost:3005/Home or localhost:3005/Home/Index
    cherrypy.tree.mount(HomeController(), '/Home')

    # this will map localhost:3005/
    cherrypy.tree.mount(RootController(), '/', {
        '/': {
            'tools.staticdir.root': os.getcwd()
        },
        '/static': {
            'tools.staticdir.on': True,
            'tools.staticdir.dir': 'static',

            # we don't need to initialize the database for static files served by CherryPy
            # 'tools.db.on': False
        }
    })

    # if you're using a separate WSGI server (e.g. Nginx + uWsgi) in prod, then let CherryPy know
    if SiteConfig.is_prod:
        cherrypy.server.unsubscribe()

    cherrypy.engine.start()

    # this return value is used by the WSGI server in prod
    return cherrypy.tree


try:
    application = start_server()
except Exception as ex:
    Logger.error('Error during run', ex)

This works whether you're using CherryPy standalone, or using a WSGI layer such as uWSGI on top of an HTTP server such as nginx.

Configuration

To configure certain site settings, there's a site_config.py:

import os

class SiteConfig:
    # whether or not we're running in production (I determine this via the path)
    # we use this to determine whether or not to show stack traces when errors occur
    is_prod = os.path.abspath("./").startswith("/srv")
    home = '/srv/www/mysite.com/application' if is_prod else os.path.abspath("./")

There are many ways to configure things, so tweak things as needed.

Site Template

The layout.html file uses Jinja2 syntax to provide a site template (since we'd like an HTML template that can be reused across different pages):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/static/css/site.css"/>
    <title>MVC with CherryPy</title>
    {% block head %}{% endblock %}
</head>
<body>

<div id="top-section">
</div>

<div id="middle-section">
{% block body %}{% endblock %}
</div>

</body>
</html>

Index Page

The views/home/index.html file contains some sample text to be used on the home page:

{% extends "layout.html" %}

{% block head %}
<link rel="stylesheet" type="text/css" href="/static/css/views/home/index.css"/>
{% endblock %}

{% block body %}
<div id="welcome-header">
  Hello!
  <div id="welcome-text">
      This is content from Views/Home/Index.html. <br/><br/>
      The outer template is Views/Layout.html.
  </div>
</div>
{% endblock %}

Here's a view of the sample page:

viewing the resulting Hello page in a browser

Styles

The included CSS is just there to provide a starting point. The Site.css is minimal:

body {
    font-family: sans-serif;
}

div {
    -webkit-box-sizing: border-box; /* safari, chrome */
    -moz-box-sizing: border-box; /* firefox */
    box-sizing: border-box; /* ie8+, opera */
}

#top-section
{
    height: 130px;
}

#middle-section
{
    background-color: #7887AB;
    min-height: 500px;
    margin: 0;
    padding: 100px 0;
}

Comments

Leave a comment

What color are green eyes? (spam prevention)
Submit
Code under MIT License unless otherwise indicated.
© 2020, Downranked, LLC.