A Mezzanine Tutorial

The Old Tutorial

I strongly recommend you bypass this article for the much clearer, up to date Mezzanine Tutorial Take 2.

...Or How This Site was Built

I had a couple things in mind when I started this work:

  1. Document my canonical mezzanine site startup steps, that I had been doing, but often forgot/missed a step
  2. Share some knowledge of Mezzanine customization
  3. Get feedback on how I've approached things compared to others
This build is tested and works for Ubuntu 12, for Django 1.4.3 and Mezzanine 1.3. Hopefully, it will work for future versions and is certainly adaptable to other OS's. The biggest "not future-proofed" is the "settings reorg" in Django 1.4. The Django 1.4 django-admin.py startproject command now puts settings.py and urls.py into an "appdir" under the project dir, unlike it's predecessors. Mezzanine 1.3 still uses the old form, and so does this guide.
The text of this walkthrough is meant to be copy/pastable into a shell or ssh window. Most of the file modifications are expressed as patch files using bash heredocs; the heredocs have some tweaks in them to work as such, e.g., $ chars backslash escaped, a hack for TAB chars, ... If you do wish to copy any of the heredocs herein into a file, do so with something like the following
cat >mypatchfile <<EOF

Oh, and credit where credit due: Ross Laird's "First Steps" series on Mezzanine got me started. Thanks Ross! By comparison to Ross's conversational writeup, this article takes a bang-bang approach.

So, on with the show.

  • The script below is crafted so that it can be copy/pasted into a shell. With that in mind, a few shell variables to control the build:

names and passwords for the script

name="me" # name for scm setup email="me@domain.tld" # email for scm setup pguser_password="pgdbpw" # postgres db user password project_name="rodmtech" # name of project dbuser=$project_name # project database name dbname=$project_name # project database user dbpass=$project_name # project database password

for TAB in heredocs, see


T=$(echo -e '\t')

  • The next bunch all require root privilege, either su'd to root or sudo privs. On a prebuilt system, many of these may be done already, but are included here for thoroughness if, say, you're setting up on a freshly installed system

system stuff, requiring sudo or su

Make sure your system is up to date...essential on a new install

sudo apt-get update sudo apt-get --yes upgrade

Check for 2.7 or better python

python -c 'import sys; print("ruh roh" if sys.version_info < (2, 7) else "scooby dooby doo")'

Setup industory standard scm tools (if you haven't already)

sudo apt-get --yes install mercurial git subversion

Just in case not already installed

sudo apt-get --yes install openssh-server curl

Install things needed for python package building, pip, virtualenv, virtualenvwrapper, fabric

sudo apt-get --yes install build-essential python-setuptools python-dev python-software-properties sudo easy_install pip # no pip package in ubuntu < 10, be sure with easy_install sudo pip install --upgrade virtualenv virtualenvwrapper sudo pip install --upgrade 'fabric>=1.0'

Install, setup postgresql for dbms, give postgres dbuser a passwored for localhost login

sudo apt-get --yes install postgresql postgresql-client libpq5 sudo -u postgres psql template1 <<EOF ALTER USER postgres WITH ENCRYPTED PASSWORD '$pguser_password'; EOF

Allow postgres password based logins locally

sudo -u postgres patch -p0 -b <<EOF --- /etc/postgresql/9.1/main/pg_hba.conf.orig${T}2013-01-21 17:38:53.213169701 -0800 +++ /etc/postgresql/9.1/main/pg_hba.conf${T}2013-01-21 17:39:18.873170239 -0800 @@ -87,7 +87,7 @@ # TYPE DATABASE USER ADDRESS METHOD

# "local" is for Unix domain socket connections only -local all all peer +local all all md5 # IPv4 local connections: host all all md5 # IPv6 local connections: EOF service postgresql restart

Install jpeg, png, freetype support, so Pillow (aka PIL) has jpg & png support

sudo apt-get install --yes libjpeg-dev zlib1g-dev libfreetype6-dev

  • Now we get to project specific work: sudo to the postgres user (which has privileges for user and db create: see /etc/postgresql/9.1/main/pg_hba.conf) and create the project db user and database
# Create the database we'll use
sudo -u postgres psql <<EOF
  LC_CTYPE = 'en_US.UTF-8' LC_COLLATE = 'en_US.UTF-8' TEMPLATE template0;

  • Before we get to actually building the site, make sure mercurial and git are configured, and setup virtualenv+virtualenvwrapper. (If you're not familar with any of these, become so. Certainly git, followed closely by mercurial, are becoming industry standard scm tools. If you want to be a Python expert, now's the time to get your head around virtualenv and virtualenvwrapper.) 
# id self for git, hg
echo "
username = $name <$email>
verbose = True
" >> $HOME/.hgrc
git config --global user.name "$name"
git config --global user.email "$email"

Setup virtualenvwrapper

mkdir ~/pyves cat >~/.virtualenvrc <<EOF export WORKON_HOME="$HOME/pyves" export PROJECT_HOME="$HOME/pyves" source /usr/local/bin/virtualenvwrapper.sh EOF source ~/.virtualenvrc

setup virtualenvwrapper at login

cat >>~/.bashrc <<EOF source $HOME/.virtualenvrc EOF

  • Now we're ready to do some real work: create a virtualenv for the site, use pip to install python packages, make a mezzanine site, populate it with some initial data (before which we patch the django postgresql backend so the fixture can be loaded), and set the site's urls to take the home page from Mezzanine CMS.

setup the site

Create a virtualenv

mkvirtualenv $project_name cdvirtualenv

Get mezzanine and friends

pip install mezzanine south django-compressor Pillow psycopg2

Create a mezzanine project

mezzanine-project $project_name cd $project_name pwd >../.project # so virtualenvwrapper "cdproject" alaias works hg init hg add . hg commit -m 'genesis'

Setup postgres db for dev work.

Note: Give db user createdb for unittest db create, not reco'd for production

Update local_settings.py with project db setup.

cat >local_settings.py <<EOF

DEBUG = True

DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql_psycopg2", "NAME": "$dbname", "USER": "$dbuser", "PASSWORD": "$dbpass", "HOST": "localhost", "PORT": "", } } EOF

Before we can load the initial data fixture, django backend for psycopg2

has an undeployed bug. See django ticket 12728

mkdir requirements/patches cat >requirements/patches/django.ticket-12728.patch <<EOF --- lib/python2.7/site-packages/django.orig/db/backends/init.py${T}2012-12-18 00:44:27.000000000 -0800 +++ lib/python2.7/site-packages/django/db/backends/init.py${T}2012-12-18 01:17:51.276110288 -0800 @@ -973,7 +973,7 @@ # If this is an m2m using an intermediate table, # we don't need to reset the sequence. if f.rel.through is None: - sequence_list.append({'table': f.m2m_db_table(), 'column': None}) + sequence_list.append({'table': f.m2m_db_table(), 'column': f.m2m_reverse_name()})

     return sequence_list

diff -u -r django.orig/db/backends/postgresql_psycopg2/operations.py django/db/backends/postgresql_psycopg2/operations.py --- lib/python2.7/site-packages/django.orig/db/backends/postgresql_psycopg2/operations.py${T}2012-12-18 00:44:27.000000000 -0800 +++ lib/python2.7/site-packages/django/db/backends/postgresql_psycopg2/operations.py${T}2012-12-18 01:19:19.120545886 -0800 @@ -133,12 +133,13 @@ break # Only one AutoField is allowed per model, so don't bother continuing. for f in model._meta.many_to_many: if not f.rel.through: + pk_column = f.m2m_reverse_name() output.append("%s setval(pg_get_serial_sequence('%s','%s'), coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \ (style.SQL_KEYWORD('SELECT'), style.SQL_TABLE(qn(f.m2m_db_table())), - style.SQL_FIELD('id'), - style.SQL_FIELD(qn('id')), - style.SQL_FIELD(qn('id')), + style.SQL_FIELD(pk_column), + style.SQL_FIELD(qn(pk_column)), + style.SQL_FIELD(qn(pk_column)), style.SQL_KEYWORD('IS NOT'), style.SQL_KEYWORD('FROM'), style.SQL_TABLE(qn(f.m2m_db_table())))) EOF cat requirements/patches/django.ticket-12728.patch | (cdsitepackages && patch -r - -N -b -p3) hg add requirements/patches hg commit -m 'postgresql django backend has a bug, patch for django ticket 12728, save patch for future deploys'

Create the schema, add some site pages and give it a test

python manage.py createdb --noinput --nodata mkdir fixtures curl -L https://bitbucket.org/rmorison/rodmtech/raw/tip/static/misc/rodmtech_net_initdata.tgz | tar xvzf - hg add fixtures python manage.py loaddata fixtures/rodmtech_net_init.json

Make the home page an editable page in the page tree (see urls.py)

Page can be created via /admin or a fixture, as above

patch -p0 <<EOF --- urls.py.orig${T}2012-12-30 22:05:04.000000000 -0800 +++ urls.py${T}2012-12-30 23:50:01.649895137 -0800 @@ -27,7 +27,7 @@ # one homepage pattern, so if you use a different one, comment this # one out.

  • url("^$", direct_to_template, {"template": "index.html"}, name="home"),
  • url("^$", direct_to_template, {"template": "index.html"}, name="home"),

    # HOMEPAGE AS AN EDITABLE PAGE IN THE PAGE TREE # --------------------------------------------- @@ -45,7 +45,7 @@ # "/.html" - so for this case, the template "pages/index.html" can # be used.

  • url("^$", "mezzanine.pages.views.page", {"slug": "/"}, name="home"),

  • url("^$", "mezzanine.pages.views.page", {"slug": "/"}, name="home"),

    # HOMEPAGE FOR A BLOG-ONLY SITE # ----------------------------- EOF hg commit -m 'initial site data and / mapped to CMS page'

    try the site; login to http://localhost:8000/admin as admin/default

    python manage.py runserver


  • On with template customization: get rid of left and bottom nav, site timezone and site title+tagline, pull mezzanine.base templates, customize base.html template, and get new static content we need: an img and some css customization:
# mod settings.py for site name, static, etc.
patch -p0 <<EOF
--- settings.py.orig${T}2013-02-12 22:25:23.246056760 -0800
+++ settings.py${T}2013-02-12 22:26:47.098472562 -0800
@@ -35,11 +35,11 @@
 # menus a page should appear in. Note that if a menu template is used
 # that doesn't appear in this setting, all pages will appear in it.

-# PAGE_MENU_TEMPLATES = ( -# (1, "Top navigation bar", "pages/menus/dropdown.html"), +PAGE_MENU_TEMPLATES = ( + (1, "Top navigation bar", "pages/menus/dropdown.html"), # (2, "Left-hand tree", "pages/menus/tree.html"), # (3, "Footer", "pages/menus/footer.html"), -# ) +)

# A sequence of fields that will be injected into Mezzanine's (or any # library's) models. Each item in the sequence is a four item sequence. @@ -98,7 +98,7 @@ # timezone as the operating system. # If running in a Windows environment this must be set to the same as your # system time zone. -TIME_ZONE = None +TIME_ZONE = "America/Los_Angeles"

# If you set this to True, Django will use timezone-aware datetimes. USE_TZ = True @@ -116,6 +116,9 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True

SITE_ID = 1 +SITE_TITLE = "rodmtech" +SITE_TAGLINE = "docs & thoughts" + EOF

start customizing: pull mezzanine.base templates, setup a static css/js/img

python manage.py collecttemplates mezzanine.core hg add templates hg commit -m 'mezzanine.core templates' templates patch -p0 <<EOF --- templates/base.html.orig${T}2013-02-12 22:01:32.274960952 -0800 +++ templates/base.html${T}2013-02-12 22:01:47.295035445 -0800 @@ -19,6 +19,7 @@ <link rel="stylesheet" href="{{ STATIC_URL }}css/bootstrap.css"> <link rel="stylesheet" href="{{ STATIC_URL }}css/bootstrap.responsive.css"> <link rel="stylesheet" href="{{ STATIC_URL }}css/mezzanine.css"> +<link rel="stylesheet" href="{{ STATIC_URL }}css/rodmtech.css"> {% ifinstalled cartridge.shop %} <link rel="stylesheet" href="{{ STATIC_URL }}css/cartridge.css"> {% endifinstalled %} @@ -87,9 +88,8 @@ <div class="container"> <div class="row">

-<div class="span2 left"> +<div class="span1 left"> {% block left_panel %} - <div class="panel tree">{% page_menu "pages/menus/tree.html" %}</div> {% endblock %} </div>

@@ -115,8 +115,6 @@

<footer> <div class="container"> -{% page_menu "pages/menus/footer.html" %} -<br style="clear:both"> <p> {% trans "Powered by" %} <a href="http://mezzanine.jupo.org">Mezzanine</a>{% ifinstalled cartridge.shop %}, EOF

tell hg only to ignore static/media...

patch -p0 <<EOF --- .hgignore.orig${T}Wed Feb 13 06:54:35 2013 -0800 +++ .hgignore${T}Wed Feb 13 10:33:51 2013 -0800 @@ -7,4 +7,4 @@ local_settings.py

syntax: regexp -^static/ +^static/media EOF hg commit -m 'tell hg only to ignore static/media' .hgignore

...then add static content to ./static/(css)|(js)|(img)

mkdir -p static/css static/js static/img curl -L https://bitbucket.org/rmorison/rodmtech/raw/tip/static/misc/rodmtech_net_initstatic.tgz | tar xvzf - hg add static/css static/js static/img

hg commit -m 'first customization: - customize base.html - some custom css - remove left & bottom menus - add title, tagline and tz to settings '

  • Homepage content is now delivered from the CMS data, but we can still customize a template for it: remove the right user panel (twitter feed) from the base.html "sitewide" template, add the found width for CMS page content, and on the home page add a recent blog post summary under the CMS content.
# customize the homepage template: append list of recent posts when on home page
python manage.py collecttemplates mezzanine.pages
hg add templates
hg commit -m 'mezzanine.pages templates'
patch -p0 <<EOF
--- templates/base.html${T}Mon Dec 31 01:09:11 2012 -0800
+++ templates/base.html${T}Mon Dec 31 01:42:39 2012 -0800
@@ -93,22 +99,12 @@
     {% endblock %}

-<div class="span7 middle"> +<div class="{% block main_span %}span9{% endblock %} middle"> {% block main %}{% endblock %} </div>

-<div class="span3 right"> - {% nevercache %} - {% include "includes/user_panel.html" %} - {% endnevercache %} - <div class="panel"> - {% block right_panel %} - {% ifinstalled mezzanine.twitter %} - {% include "twitter/tweets.html" %} - {% endifinstalled %} - {% endblock %} - </div> -</div> +{% block optional_right_panel %} +{% endblock %}

</div> </div> EOF patch -p0 <<EOF --- templates/pages/index.html.orig${T}2013-02-12 17:01:24.000000000 -0800 +++ templates/pages/index.html${T}2013-02-12 22:44:38.415784934 -0800 @@ -1,5 +1,7 @@

+{% load blog_tags i18n future %} + {% block main %} <!-- This template is provided as a custom template for the homepage, for @@ -7,4 +9,18 @@ free to modify it. --> {{ block.super }} +<hr/> +{% block blog_recent_posts %} +{% blog_recent_posts 5 as recent_posts %} +{% if recent_posts %} +<h3>{% trans "Recent Posts" %}</h3> +<ul class="unstyled recent-posts"> +{% for recent_post in recent_posts %} +<li><a href="{{ recent_post.get_absolute_url }}" + >{{ recent_post.title }}</a></li> +{% endfor %} +</ul> +{% endif %} +{% endblock %} + {% endblock %} EOF hg commit -m 'customize pages templates: - widen main content blocki '

  • We're going to use SyntaxHighlighter for display code from the site. There are other server side solutions that work quite well, e.g., Pagedown, but SyntaxHighlighter posed an interesting challenge to integrate, and has worked out reasonably well. We include the nice tinymce integration, syntaxhl, and customize tinymce additionally. Note that normally tinymce modules like syntaxhl normally must be in the tinymce tree, which is buried deep in site-packages. We don't want to go patching about there needlessly, a little sed patch fixes things up for us under our project tree.

    Note also that in our tinymce customization we turn off the inlinepopups plugin: tinymce has "issues" serving in this mode over ssl, found out the hard way, and was not interested in finding my own workaround. If you come across one...

    Oh, we also update builtin TWITTER feed settings for moi. Yes, we took twitter out of the base template, but we put it back to the home page (only) here. 
# add SyntaxHighligher to site, syntaxhl tinymce plugin, turn on other tinymce options
( cd /tmp && \
  curl http://alexgorbatchev.com/SyntaxHighlighter/download/download.php?sh_current \
       --output syntaxhighlighter.zip && \
  unzip syntaxhighlighter.zip \
mkdir static/syntaxhighlighter
cp -a /tmp/syntaxhighlighter_3.0.83/styles/ /tmp/syntaxhighlighter_3.0.83/scripts/ static/syntaxhighlighter/
( cd /tmp && \
  curl -L https://github.com/RichGuk/syntaxhl/archive/master.zip --output master.zip && \
  unzip master.zip \
mv /tmp/syntaxhl-master/ static/syntaxhl
curl -L https://bitbucket.org/rmorison/rodmtech/raw/tip/static/misc/rodmtech_net_tinymce_setup.tgz | tar xvzf -

must fix relative path in syntaxhl because we're loading external

sed --in-place \ -e "s?../../tiny_mce_popup.js?/static/grappelli/tinymce/jscripts/tiny_mce/tiny_mce_popup.js?" \ static/syntaxhl/dialog.htm

customize tinymce setup and twitter settings

patch -p0 <<EOF --- settings.py.orig${T}Wed Feb 13 06:54:35 2013 -0800 +++ settings.py${T}Wed Feb 13 07:14:56 2013 -0800 @@ -305,6 +305,23 @@


+##################### +# TINYMCE SETTTINGS # +##################### + +TINYMCE_SETUP_JS = "js/tinymce_setup.js" +RICHTEXT_ALLOWED_STYLES = ('font-size', 'color', 'background-color', 'font-family', 'height') + + +##################### +# TWITTER SETTTINGS # +##################### + +TWITTER_DEFAULT_QUERY_TYPE = "user" +TWITTER_DEFAULT_QUERY = "rodmtech" +TWITTER_DEFAULT_NUM_TWEETS = 5 + + ################### # DEPLOY SETTINGS # ################### EOF

include syntaxhighlighter scripts and css in base template

patch -p0 <<EOF --- templates/base.html.org${T}2013-02-13 06:51:01.268494885 -0800 +++ templates/base.html${T}2013-02-12 22:44:13.047659149 -0800 @@ -19,6 +19,8 @@ <link rel="stylesheet" href="{{ STATIC_URL }}css/bootstrap.css"> <link rel="stylesheet" href="{{ STATIC_URL }}css/bootstrap.responsive.css"> <link rel="stylesheet" href="{{ STATIC_URL }}css/mezzanine.css"> +<link rel="stylesheet" href="{{ STATIC_URL }}syntaxhighlighter/styles/shThemeDefault.css"> +<link rel="stylesheet" href="{{ STATIC_URL }}syntaxhighlighter/styles/shCore.css"> <link rel="stylesheet" href="{{ STATIC_URL }}css/rodmtech.css"> {% ifinstalled cartridge.shop %} <link rel="stylesheet" href="{{ STATIC_URL }}css/cartridge.css"> @@ -29,6 +31,10 @@ {% compress js %} <script src="{{ STATIC_URL }}mezzanine/js/{{ settings.JQUERY_FILENAME }}"></script> <script src="{{ STATIC_URL }}js/bootstrap.min.js"></script> +<script src="{{ STATIC_URL }}syntaxhighlighter/scripts/shCore.js"></script> +<script src="{{ STATIC_URL }}syntaxhighlighter/scripts/shBrushPython.js"></script> +<script src="{{ STATIC_URL }}syntaxhighlighter/scripts/shBrushBash.js"></script> +<script src="{{ STATIC_URL }}syntaxhighlighter/scripts/shBrushCpp.js"></script> <script> \$(function() { \$('.middleinput:text, textarea').addClass('xlarge'); @@ -121,6 +127,10 @@ </div> </footer>

+<script type="text/javascript"> + SyntaxHighlighter.all() +</script> +

</body> EOF hg add static hg commit -m 'Add SyntaxHighligher, syntaxhl tinymce plugin, turn on other tinymce options'

add tweets back to index.html (was in base.html)

patch -p0 <<EOF --- templates/pages/index.html.orig${T}2013-02-13 13:36:25.969114562 -0800 +++ templates/pages/index.html${T}2013-02-13 06:51:01.268494885 -0800 @@ -1,13 +1,11 @@

-{% load blog_tags i18n future %} +{% load mezzanine_tags blog_tags i18n future %}

+<!--CMS Page block--> +{% block main_span %}span6{% endblock %} {% block main %} -<!-- -This template is provided as a custom template for the homepage, for -when it is configured as an editable page in the navigation tree. Feel -free to modify it. ---> + {{ block.super }} <hr/> {% block blog_recent_posts %} @@ -24,3 +22,22 @@

{% endblock %} + + +<!--Right panel block--> +{% block optional_right_panel %} + +<div class="span3 right"> + {% nevercache %} + {% include "includes/user_panel.html" %} + {% endnevercache %} + <div class="panel"> + {% block right_panel %} + {% ifinstalled mezzanine.twitter %} + {% include "twitter/tweets.html" %} + {% endifinstalled %} + {% endblock %} + </div> +</div> + +{% endblock %} EOF hg commit -m 'add tweets back to index.html (was in base.html)'

  • And finally, remove the little toolbar from SyntaxHighlighter: it used to be somewhat usefull, but not so in the latest release
# remove syntaxhighlighter toolbar, doesn't do much in latest release
patch -b -p0 <<EOF
--- templates/base.html${T}Thu Feb 14 23:39:56 2013 -0800
+++ templates/base.html${T}Fri Feb 15 00:33:56 2013 -0800
@@ -128,7 +128,8 @@

<script type="text/javascript"> - SyntaxHighlighter.all() + SyntaxHighlighter.defaults['toolbar'] = false; + SyntaxHighlighter.all() </script>

{% include "includes/footer_scripts.html" %} EOF hg commit -m "remove syntaxhighlighter toolbar, doesn't do much in latest release"

  • Like the comment says...

sit back, stir with content and enjoy!

python manage.py runserver