Your browser does not support JavaScript!
Upvotes Anonymous 2 User 0

MVC with CherryPy and Jinja2 (updated)

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).
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:
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;
}
Questions?
Feel free to comment below, or find me on Twitter @jasonprogrammer.
Was this helpful?

Comments

Byanonymous Anonymous 0 User 0
great template!
Byjasonj Anonymous 0 User 0
Thanks for the feedback! Glad to hear you like it.

Leave a Comment

I agree to the Terms of Service
Design © 2015, Downranked, LLC.,
Original user code contributions under MIT License