Using Django and MongoDB to Build a Blog

Mihalis Tsoukalos

Issue #238, February 2014

MongoEngine is an ORM for working with MongoDB from Python.

This article shows how to create a simple blog site using the MongoDB Document Database and the Django Web framework.

Mongo Basics

MongoDB is an open-source document-oriented database, not a traditional relational database, written in C++ by Dwight Merriman and Eliot Horowitz. Being a document database does not mean storing Microsoft Word documents, but rather it means storing semi-structured data. You can input arbitrary binary JSON objects (BSON) into a MongoDB database. It runs on UNIX machines as well as Windows and supports replication and sharding.

Your Linux distribution probably includes a MongoDB package, so go ahead an install it if you have not done so already. Alternatively, you can download a precompiled binary or get the MongoDB source code from www.mongodb.org and compile it yourself.

On a Debian 7 system, you can install MongoDB with the following command:

# apt-get install mongodb

After installing MongoDB, start the MongoDB server process with:

# service mongodb start

Similarly, you can stop the running MongoDB server with:

# service mongodb stop

After installation, type mongo --version in your UNIX shell to find the MongoDB version you are using, and type mongo to enter the MongoDB shell and check whether the MongoDB server process is running.

By default, the MongoDB server process listens to localhost using the 27017 port. You can change it if you want, but if both the MongoDB server and the Django installation are on the same machine, it is more secure to leave it as it is.

The configuration file for MongoDB is /etc/mongodb.conf. Nevertheless, if you want to run multiple MongoDB servers on the same UNIX machine, you can bypass the /etc/mongodb.conf file and utilize command-line options that allow you to use a different port number, a different IP or even a different MongoDB configuration file.

Figure 1. MongoDB Terminology

Figure 1 shows the most useful MongoDB terms in relation to their respective SQL terms.

Starting the MongoDB server process (mongod) on a Linux machine without any parameters and without root privileges should generate output similar to the following:

$ mongod
mongod --help for help and startup options
Fri Sep 27 23:21:33 [initandlisten] MongoDB starting : 
 ↪pid=7991 port=27017 dbpath=/data/db/ 64-bit host=mail
Fri Sep 27 23:21:33 [initandlisten] db version v2.0.6, 
 ↪pdfile version 4.5
Fri Sep 27 23:21:33 [initandlisten] git version: nogitversion
Fri Sep 27 23:21:33 [initandlisten] build info: Linux z6 
 ↪3.8-trunk-amd64 #1 SMP Debian 3.8.3-1~experimental.1 
 ↪x86_64 BOOST_LIB_VERSION=1_49
Fri Sep 27 23:21:33 [initandlisten] options: {}
Fri Sep 27 23:21:33 [initandlisten] exception in initAndListen: 
 ↪10296 dbpath (/data/db/) does not exist, terminating
Fri Sep 27 23:21:33 dbexit: 
Fri Sep 27 23:21:33 [initandlisten] shutdown: going to close 
 ↪listening sockets...
Fri Sep 27 23:21:33 [initandlisten] shutdown: going to 
 ↪flush diaglog...
Fri Sep 27 23:21:33 [initandlisten] shutdown: going to 
 ↪close sockets...
Fri Sep 27 23:21:33 [initandlisten] shutdown: waiting 
 ↪for fs preallocator...
Fri Sep 27 23:21:33 [initandlisten] shutdown: lock for 
 ↪final commit...
Fri Sep 27 23:21:33 [initandlisten] shutdown: final commit...
Fri Sep 27 23:21:33 [initandlisten] shutdown: closing all files...
Fri Sep 27 23:21:33 [initandlisten] closeAllFiles() finished
Fri Sep 27 23:21:33 dbexit: really exiting now

Django Basics

Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design. It allows you to build Web applications quickly. Instagram, Mozilla and Pinterest use Django.

Simply put, Django is a collection of libraries written in Python. In order to create a site using Django, you basically write Python code that uses the Django libraries. If you already have a good working knowledge of Python, you have to understand only how the Django libraries work.

Django follows a slightly changed version of the MVC (Model View Controller) design pattern called Model Template View (MTV). The MTV handles the Controller work by the core, and all the other work is done in Models, Templates and Views. According to Django's philosophy, what is truly important is not terminology but getting things done.

On a Debian 7 system, you can install Django with the following command:

# apt-get install python-django

To make sure that everything works as expected, type the following Django command, which prints the version of Django:

# django-admin version
1.5.1

How Python and Django Communicate with MongoDB

You will need a Python module called PyMongo to talk to MongoDB from Python. On a Debian 7 system, you can install it as follows:

# apt-get install python-pymongo
Reading package lists... Done
Building dependency tree    
Reading state information... Done
The following extra packages will be installed:
  python-bson python-bson-ext python-gridfs python-pymongo-ext
The following NEW packages will be installed:
  python-bson python-bson-ext python-gridfs python-pymongo 
  ↪python-pymongo-ext
0 upgraded, 5 newly installed, 0 to remove and 0 not upgraded.
Need to get 212 kB of archives.
After this operation, 928 kB of additional disk space will be used.
Do you want to continue [Y/n]?

The following instructional Python code (saved as connect.py) connects to a MongoDB database, prints the available databases of the mongodb://localhost:27017 server and closes the MongoDB connection:

import pymongo
# Open the MongoDB connection
connMongo = pymongo.Connection('mongodb://localhost:27017')
# Print the available MongoDB databases
print connMongo.database_names()
# Close the MongoDB connection
connMongo.close()

You can run the small Python script as follows:

$ python connect.py 
[u'LJ', u'local', u'test']

The output shows that at the time of running the script, three databases exist called LJ, local and test. Although PyMongo will not be used directly in the rest of the article, it is useful to know about it for testing and troubleshooting purposes.

Generally speaking, Django has a wrapper for every relational database it supports, but Mongo is a non-relational database, so you need some external help. You need the MongoEngine Python package in order to utilize MongoDB. Other options are Ming, MongoKit, django-mongodb and django-nonrel. In my opinion, MongoEngine is the best option.

MongoEngine is an object-document mapper made for MongoDB, following Django's ORM style. You can install it by executing this command:

# apt-get install python-mongoengine

MongoEngine is based on PyMongo, and that's why you need to know some basic things about PyMongo.

For those of you who are familiar with Django, you should know that when you are using MongoEngine, you lose both the Django Admin panel and the python manage.py syncdb command. Losing the Django Admin panel is a major drawback, but MongoDB offers features that relational databases cannot provide.

The Problem

Imagine you registered a new domain to host your personal site. The site also will have a blog. Instead of using a CMS, such as Joomla! or WordPress, for creating the blog, you want more control over the site, so you decide to use Django for creating the blog and MongoDB for storing the blog data.

The nice thing about this solution is that if you already are familiar with Django, it will not take more than two hours to develop, test and deliver a complete version of the blog site.

Note: the solution presented here tries to be as Django-“native” as possible. The only thing different from the usual Django way is the use of MongoDB.

The Solution

If you try to access a MongoDB that does not already exist, MongoDB will create it. The same happens if you try to write to a MongoDB collection (table) that does not exist. So, you do not need to execute any commands on MongoDB, but you should be very careful not to have any typos in your code.

Do the following steps on Django.

1) Create a new project called LJ:

$ django-admin.py startproject LJ
$ cd LJ

The manage.py script is created for every Django project and is a wrapper around django-admin.py. You do not need to make any changes to it.

2) Run the test development Web server to see if everything is okay:

$ python manage.py runserver

By going to http://localhost:8000/ (or http://127.0.0.1:8000/), you will see Figure 2.

Figure 2. The Test Development Web Server

The test development server restarts automatically every time you make changes to the Django project.

3) Create the app for the blog called LJblog:

$ python manage.py startapp LJblog

4) Add the name of the LJblog app to the list of the INSTALLED_APPS in the LJ/settings.py file. If you do not “install” the app, you will not be able to use it. So, the INSTALLED_APPS variable should have the following values:

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'LJblog',
    'django_extensions',
)

As you can see here, it also is required to install a package called django-extensions. If your Linux distribution does not provide a ready-to-install package, visit the Django Extensions site for instructions about installing it.

4) Many other changes had to be made in the LJ/settings.py file. The following diff output shows the changes:

$ diff settings.py{,.orig}
3,5d2
< import os
< PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
< 
14a12,23
> DATABASES = {
>   'default': {
>       'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 
                                         # 'mysql', 'sqlite3' or 
                                         # 'oracle'.
>       'NAME': '',      # Or path to database file if using sqlite3.
>       # The following settings are not used with sqlite3:
>       'USER': '',
>       'PASSWORD': '',
>       'HOST': '',      # Empty for localhost through domain sockets 
                         # or '127.0.0.1' for localhost through TCP.
>       'PORT': '',      # Set to empty string for default.
>   }
> }
> 
44c53
< MEDIA_ROOT = os.path.join(PROJECT_ROOT, '..', 'media')
---
> MEDIA_ROOT = ''
49c58
< MEDIA_URL = '/media/'
---
> MEDIA_URL = ''
55c64
< STATIC_ROOT = os.path.join(PROJECT_ROOT, '..', 'static')
---
> STATIC_ROOT = ''
63d71
< 	os.path.join(PROJECT_ROOT, 'static'),
103d110
< 	os.path.join(PROJECT_ROOT, 'templates'),
148,159d154
< 
< AUTHENTICATION_BACKENDS = (
<     'mongoengine.django.auth.MongoEngineBackend',
< )
< 
< SESSION_ENGINE = 'mongoengine.django.sessions'
< 
< MONGO_DATABASE_NAME = 'LJ_blog'
< 
< from mongoengine import connect
< connect(MONGO_DATABASE_NAME)
< 

Note: the name of the MongoDB database is defined and stored in the MONGO_DATABASE_NAME variable.

5) The contents of the LJ/urls.py file should be the following:

from django.conf.urls import patterns, include, url
from django.conf import settings
from LJblog.views import PostListView

urlpatterns = patterns('',
	url(r'^$', PostListView.as_view(), name='list'),
    url(r'^post/', include('LJblog.urls'))
)

6) Inside the LJ/LJ directory, you need to create two directories, called static and templates, and copy some files and directories inside them. The project uses the Twitter Bootstrap set of tools for creating Web sites and Web applications.

The following output shows the full contents of the static directory:

$ ls -lR static/
total 0
drwxr-xr-x@ 6 mtsouk  staff  204 Sep 21 14:13 bootstrap

static//bootstrap:
total 0
drwxr-xr-x@ 6 mtsouk  staff  204 Jan  5  2013 css
drwxr-xr-x@ 4 mtsouk  staff  136 Jan  5  2013 img
drwxr-xr-x@ 4 mtsouk  staff  136 Jan  5  2013 js

static//bootstrap/css:
total 544
-rwxr-xr-x@ 1 mtsouk  staff   21751 Jan  5  2013 
 ↪bootstrap-responsive.css
-rwxr-xr-x@ 1 mtsouk  staff   16553 Jan  5  2013 
 ↪bootstrap-responsive.min.css
-rwxr-xr-x@ 1 mtsouk  staff  124223 Jan  5  2013 
 ↪bootstrap.css
-rwxr-xr-x@ 1 mtsouk  staff  103314 Jan  5  2013 
 ↪bootstrap.min.css

static//bootstrap/img:
total 56
-rwxr-xr-x@ 1 mtsouk  staff   8777 Jan  5  2013 
 ↪glyphicons-halflings-white.png
-rwxr-xr-x@ 1 mtsouk  staff  12799 Jan  5  2013 
 ↪glyphicons-halflings.png

static//bootstrap/js:
total 184
-rwxr-xr-x@ 1 mtsouk  staff  58516 Jan  5  2013 
 ↪bootstrap.js
-rwxr-xr-x@ 1 mtsouk  staff  31596 Jan  5  2013 
 ↪bootstrap.min.js

The templates directory contains two files, as the output of the ls -lR command shows:

$ ls -lR templates/
total 16
-rwxr-xr-x@ 1 mtsouk  staff  1389 Sep 25 21:25 base.html
-rwxr-xr-x@ 1 mtsouk  staff   148 Jan  5  2013 messages.html

The base.html file contains project-specific information that you can change.

7) The connection to the MongoDB database happens inside the LJ/settings.py file. Django needs the following two commands to connect to a MongoDB database:

from mongoengine import connect
connect(MONGO_DATABASE_NAME)

8) Next, you should create four HTML files inside the templates/LJblog directory. They are the displayed Web pages for the Create, Read, Update and Delete operations:

  • create.html

  • detail.html

  • list.html

  • update.html

The selected filenames must match the parameters found inside the LJblog/urls.py file.

9) The contents of the LJblog/urls.py file are the following:

from django.conf.urls import patterns, url
from views import PostCreateView, PostDetailView, 
 ↪PostUpdateView, PostDeleteView

urlpatterns = patterns('',
    url(r'^add/$', PostCreateView.as_view(), name='create'),
    url(r'^(?P<pk>[\w\d]+)/$', PostDetailView.as_view(), 
    ↪name='detail'),
    url(r'^(?P<pk>[\w\d]+)/edit/$', PostUpdateView.as_view(), 
    ↪name='update'),
    url(r'^(?P<pk>[\w\d]+)/delete/$', PostDeleteView.as_view(), 
    ↪name='delete'),
)

10) Next, you need to edit the models.py file. This file is where data models are defined.

Using Django's ORM (Object-Relational Mapper) is one of the project's goals. ORM allows the Python classes that were defined inside models.py to access the selected database without requiring you to deal with the database directly. ORM is a major advantage of Django.

The Post Python class is defined as follows:

class Post(Document):
    user = ReferenceField(User, reverse_delete_rule=CASCADE)
    title = StringField(max_length=200, required=True)
    text = StringField(required=True)
    text_length = IntField()
    date_modified = DateTimeField(default=datetime.now)
    is_published = BooleanField()

As you will see later in this article, the Post MongoDB table has a direct connection to the Post Python class.

11) Then, you need to edit the forms.py file. The forms.py file allows Django to access user-submitted form data.

12) Last but not least, you should edit the views.py file. This file includes the functions that handle data as well as various other things.

The directory structure of the project as well as the included files are shown in Figure 3. You also will notice files ending with .pyc. These are byte-code files created by the Python interpreter that are executed by the Python virtual machine.

Figure 3. The Directory Structure of the Django Project

You can examine the contents of the LJ_blog collection using MongoDB commands:

> use LJ_blog;
switched to db LJ_blog
> show collections;
post
system.indexes
> db.post.find();
{ "_id" : ObjectId("523d83de8491973b242e2772"), "title" : 
 ↪"First blog entry!", "text" : "This is my first 
 ↪blog entry.\r\Mihalis Tsoukalos", "text_length" : 47, 
 ↪"date_modified" : ISODate("2013-09-21T06:32:46.289Z"), 
 ↪"is_published" : true }
{ "_id" : ObjectId("523d83f88491973b242e2773"), "title" : 
 ↪"Another post", "text" : "Just another blog post!", 
 ↪"text_length" : 23, "date_modified" : 
 ↪ISODate("2013-09-21T06:33:12.321Z"), "is_published" : true }
{ "_id" : ObjectId("523d86f58491973b9e3c8c78"), "title" : 
 ↪"Just another test!", "text" : "Just another test!\r\nLJ", 
 ↪"text_length" : 22, "date_modified" : 
 ↪ISODate("2013-09-21T06:45:57.092Z"), "is_published" : true }
>

Note: every time you insert a BSON document in MongoDB, MongoDB automatically generates a new field called _id. The _id field acts as the primary key and is always 12 bytes long.

Now, you should check that everything is fine by running the test development server and trying to connect to http://localhost:8000/:

$ python manage.py runserver
Validating models...

0 errors found
September 21, 2013 - 07:25:07
Django version 1.5.1, using settings 'LJ.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

If everything is okay, you will see something similar to Figure 4 after visiting http://127.0.0.1:8000/ or http://localhost:8000/.

Figure 4. The LJblog application is up and running.

While using the application, the output from the test development server is updated and will look similar to the following:

[25/Sep/2013 12:18:28] "GET / HTTP/1.1" 200 1320
[25/Sep/2013 12:18:28] "GET /static/bootstrap/css/bootstrap.min.css 
 ↪HTTP/1.1" 200 103314
[25/Sep/2013 12:18:28] "GET /static/bootstrap/js/bootstrap.min.js 
 ↪HTTP/1.1" 200 31596
[25/Sep/2013 12:18:28] "GET /static/bootstrap/css/
↪bootstrap-responsive.css HTTP/1.1" 200 21751
[25/Sep/2013 12:18:32] "GET / HTTP/1.1" 200 1320
[25/Sep/2013 12:18:33] "GET /post/add/ HTTP/1.1" 200 1823
[25/Sep/2013 12:18:34] "GET /?all_posts HTTP/1.1" 200 1320
[25/Sep/2013 16:01:10] "GET /post/5243295f8491976bd8f016d0/edit/ 
 ↪HTTP/1.1" 200 1841
[25/Sep/2013 16:01:18] "GET /post/5243295f8491976bd8f016d0/delete/ 
 ↪HTTP/1.1" 302 0

The output is useful for debugging purposes, especially when you don't get the expected results on your Web browser.

If you want to delete the LJ_blog collection in order to start your blog from scratch, use the following command with care:

> db.post.drop()
true
> db.post.find();
> show collections;
system.indexes

Deploying the Django Web Site to a Production Server

Explaining the full deployment process is outside the scope of this article, but I want to give some useful tips. When you try to run your Django project on a production server, keep the following things in mind:

1) Turn off Debug mode inside LJ/settings.py:

DEBUG = False
TEMPLATE_DEBUG = DEBUG

2) Change the ADMINS setting inside LJ/settings.py to something useful:

ADMINS = (
    ('Mihalis', 'someEmail@Domain.GR'),
)

3) Install and activate the mod_python Apache module.

4) You can use mod_wsgi instead of mod_python.

Why Use MongoDB Instead of a Relational Database?

You may wonder why you should use a NoSQL database, such as MongoDB, instead of a traditional DBMS like MySQL or PostgreSQL. Although it would be possible to use a relational database, here are the reasons for preferring MongoDB:

  • MongoDB is generally faster.

  • MongoDB is better for high-volume traffic sites.

  • MongoDB supports sharding. Sharding (aka horizontal partitioning) is the process of separating a single database across a cluster of machines.

  • MongoDB supports replication.

  • Your data schema may change without downtime when using MongoDB.

  • Depending on the application, it may feel more natural to develop in a document-oriented database.

  • MongoDB has an easy-to-use protocol for storing large files and file metadata called GridFS.

Summary

As I've explained here, MongoDB and Django can indeed work together. However, two things are missing to be 100% Django-native: support for the Django Admin Panel and support for the syncdb command.

A link to the full code for this project is listed in the Resources section of this article.

Acknowledgement

I would like to thank Josh Ourisman for answering some questions that I had while writing this article.

Mihalis Tsoukalos is a UNIX administrator who also focuses on programming, databases and mathematics. You can reach him at tsoukalos@sch.gr and @mactsouk (Twitter). Contact him if you are looking for a UNIX system administrator.