In my previous post I explained that I wanted to explore new non-php web development frameworks like Rails and Django. So I decided to build a blog engine with each of them and see which framework felt the best. I also decided that if I am going to embark on a relatively time consuming learning project, that I wanted to expand my scope and experiment with Twitter Bootstrap and MongoDB as well.

I have used Python several times in the past, and have never touched Ruby outside of a Hello World rails tutorial. I also have some peers that hate rails (even though they like Ruby), so I decided to start on the Python side of the fence. After some research and discussion with peers I decided to start with Pyramid rather than Django.

Pyramid seems to provide more flexibility when break ingaway from the convention, and considering I wanted to use Mongo instead of SQL this felt like a great place to start. After completing this exercise I found that I really enjoyed working with Pyramid(and Mongo), so much so that I pushed my code to my VPS and went public with my blog. This also delayed my Django and Rails trails as I had to prepare to deploy a production version of my Pyramid site. I will start the Rails version soon, but for now here are the steps I took to create the Pyramid version.

In order to get started I followed my usual routine for a python project on my Mac (you can find help for these steps all over the web):

  1. Check my python version
  2. Create a virtual environment called myBlog in my src directory using virtualenv
  3. Git Init my project (of course)
  4. Install Pyramid in my virtualenv
[me@mymac:~/src]$   virtualenv myBlog
New python executable in myBlog/bin/python
Installing setuptools............done.
Installing pip...............done.

[me@mymac:~/src]$ cd myBlog

[me@mymac:~/src/myBlog]$ ls
bin     include lib

[me@mymac:~/src/myBlog]$ ./bin/pip install pyramid
'Downloading/unpacking pyramid
  Downloading pyramid-1.3.2.tar.gz (2.4Mb): 2.4Mb downloaded
  Running setup.py egg_info for package pyramid

Now I have a development environment setup for working with Pyramid, but I still need to setup my app. Pyramid has setup scaffolds(called Paster templates) similar to Rails. In the Pyramid documentation I found a setup scaffold for mongoDB. The only problem with this template is that it uses URL traversal instead of URL matching. I am familiar with URL mapping, but this is a learning exercise so why not use traversal and learn a new method? So I installed this template and setup my app:

[me@mymac:~/src/myBlog]$ ./bin/pip install pyramid_mongodb
Downloading/unpacking pyramid-mongodb
  Downloading pyramid_mongodb-1.3.tar.gz (54Kb): 54Kb downloaded
  Running setup.py egg_info for package pyramid-mongodb

[me@mymac:~/src/myBlog]$ ./bin/pcreate -t
Available scaffolds:
  alchemy:          Pyramid SQLAlchemy project using url dispatch
  pyramid_mongodb:  pyramid MongoDB project
  starter:          Pyramid starter project
  zodb:             Pyramid ZODB project using traversal

[me@mymac:~/src/myBlog]$ ./bin/pcreate -t pyramid_mongodb my_blog
Creating directory /Users/me/src/myBlog/my_blog
  Recursing into +package+
    Creating /Users/me/src/myBlog/my_blog/my_blog/
    .........
Welcome to Pyramid.  Sorry for the convenience.

[me@mymac:~/src/myBlog]$ cd my_blog/
[me@mymac:~/src/myBlog/my_blog]$ ../bin/python setup.py develop
running develop
running egg_info
creating my_blog.egg-info
Now we are ready to roll, let's turn on our new site:
the reload option allows us to change
our code and the site will detect and restart
[me@mymac:~/src/myBlog/my_blog]$ ../bin/pserve development.ini --reload
Starting subprocess with file monitor
Starting server in PID 25805.
serving on 0.0.0.0:6543 view at http://127.0.0.1:6543
And here is our new Mongo based site:

Next I went and downloaded the latest version ofTwitter Bootstrap and added all the code into my /static folder. I then used the bootstrap Hero example and modified it to create a new template in my /templates directory. A great tutorial/rundown on Twitter Bootstrap is available on nettuts.

Now the site is ready to start building. First I needed to design a schema for my blog posts. I plan to build an admin site, but for now I am just setting up the read-only site, therefore I am going to add a few test entries in mongo directly. For my posts I need a title, unique url, tags, active flag so I can edit before posting, post date, category, and author.

[me@mymac:~/src/myBlog/my_blog]$ mongo
MongoDB shell version: 2.0.4
connecting to: test

Now let's setup our post structure by creating a javascript object:

> var postExample = {'title': 'My Post Example',
...                  'author': 'Me',
...                  'tags': ['mongodb', 'test', 'tag1', 'tag2'],
...                  'postDate': new Date(),
...                  'url': 'my_post_example',
...                  'category': 'programming',
...                  'active': true,
...                  'postBody': 'This is a test post'};
> postExample
{
    "title" : "My Post Example",
    "author" : "Me",
    "tags" : [
        "mongodb",
        "test",
        "tag1",
        "tag2"
    ],
    "postDate" : ISODate("2012-08-12T21:36:26.218Z"),
    "url" : "my_post_example",
    "category" : "programming",
    "active" : true,
    "postBody" : "This is a test post"
}
> db.blogExample.save(postExample);
> db.blogExample.find();
{ "_id" : ObjectId("5028220e61419e477bf0dcab"), "title" : "My Post Example", "author" : "Me", "tags" : [ "mongodb", "test", "tag1", "tag2" ], "postDate" : ISODate("2012-08-12T21:36:26.218Z"), "url" : "my_post_example", "category" : "programming", "active" : true, "postBody" : "This is a test post" }
>

You can see we also saved the entry to the blogExample collection with the db.blogExample.save(object); command. Now we can serve this entry on our site. To do so we need to create our root view that serves our entries in newest to oldest order. But before we do that we need to design our URL routes. So for each single blog we need /post/{url_of_post} and for categories we need /category/{category_name}. So after researching Traversal, I modified my resources.py file like so:

class Root(object):
    __name__ = ''
    __parent__ = None

    def __init__(self, request):
        pass

    def __getitem__(self, key):
        if key == 'post':
            return Post()
        elif key == 'category':
            return Category()
        raise KeyError


class Post(object):
    __name__ = ''
    __parent__ = Root

    def __init__(self):
        pass

    def __getitem__(self, key):
        if key:
            return PostName(key)
        raise KeyError

class PostName(object):
    def __init__(self, name):
        self.__name__ = name


class Category(object):
    __name__ = ''
    __parent__ = Root

    def __init__(self):
        pass

    def __getitem__(self, key):
        if key:
            return CategoryName(key)
        raise KeyError

class CategoryName(object):
    def __init__(self, name):
        self.__name__ = name

Now we have the URL structure setup, so we can start building our views. I started by creating a views folder and then created an empty __init__.py file inside it so that I can organize my views into multiple files. The __init__.py file in our app's root directory has this line: config.scan('my_blog'). This means that it will scan our project for views, enabling us to create a new file with more views.

Here is our root.py file in our views directory, this defines our root view, and also aliases /home to our root view. This view file also defines our 404 view for any page that doesn't match our url routes.

from pyramid.view import view_config
import my_blog.resources
from my_blof.blogdata import BlogData
from pyramid.httpexceptions import HTTPFound

#this line defines our root view
@view_config(context='my_blog:resources.Root', renderer='my_blog:templates/home_entry_list.pt')
#this line also attaches /home to our root view
@view_config(context='my_blog:resources.Root', renderer='my_blog:templates/home_entry_list.pt', name='home')
def my_view(request):
    #get mongoDB posts
    blog_data = BlogData(request)
    posts = blog_data.get_recent_posts(10, 1)

    entries = []
    for post in posts:
        postDate = post[u'postDate'].strftime("%B %d %Y")
        entry = {'title': post[u'title'],
               'url': post[u'url'],
               'date': postDate,
                        'author': post[u'author'],
                        'blog_body': strip_tags(post[u'postText'][:1000]) + '...',
                        'tags': post[u'tags']}
        entries.append(entry)
    return {'cur_page': 'home', 'page_title': 'Welcome to Brett\'s Blog', 'entries': entries}

#this handles our 404 not found view
@view_config(context='pyramid.httpexceptions.HTTPNotFound', renderer='my_blog:templates/404_error.pt')
def not_found(request):
    return{'message': 'Error 404, Page Not Found',  'cur_page': '', 'page_title': 'Requested Page Not Found'}

The BlogData object is a class that handles all of our fetches from mongoDB, here is the contents:

class BlogData(object):
    def __init__(self, request):
        self.settings = request.registry.settings
        self.collection = request.db['blogExample']


    #get a list of  blog blogPosts, handle pagination
    def get_recent_posts(self, num_of_entries, page, titles_only=False):
        row = num_of_entries * (page - 1)
        entries = self.collection.find({'active': True}).sort('_id', -1)[row: row + num_of_entries]
        if titles_only:
            titles = []
            for entry in entries:
                this_entry = {'title': entry['title'], 'url': entry['url']}
                titles.append(this_entry)
            return titles
        else:
            return entries

    #get a single post by the url
    def get_post_by_url(self, url):
        post = self.collection.find_one({'url':  url})
        return post

    def get_recent_posts_by_category(self, category, num_of_entries, page):
        row = num_of_entries * (page - 1)
        entries = self.collection.find({'category': category, 'active': True}).sort('_id', -1)[row: row + num_of_entries]
        return entries

This object can get us a list of posts, a list of just the titles, a list of posts for a specific category, and a single post via the url entry. This should be sufficient for now. You can see in my view, that our root view pulls up the home_entry_list.pt template which looks like this:


<div metal:fill-slot="main_content">
  <div tal:repeat="entry entries" class="hero-unit">
            <h3><a href="/post/${structure: entry.url}">${structure: entry.title}</a></h3>
            <h4>${structure: entry.date}</h4>
            <p tal:content="structure: entry.blog_body"></p>

            <p><a class="btn btn-primary" href="/post/${structure: entry.url}">Read Post ยป</a></p>
            <p></p><h4><a href="/post/${structure: entry.url}#disqus_thread"></a></h4><p></p>
            <i class="icon-tags icon-large"></i> <span tal:repeat="tag entry.tags"><a href="/tag/${structure: tag}" class="label label-success">${structure: tag</a>
</span>
          </div>
  </div>
</metal:main>

This template injects itself into the structure.pt template, which is my site's main shell with head, header, footer, sidebar and main content. This was the only part of the project I really hated. Chameleon templates were a pain to work with, simple stuff is pretty easy but breaking my HTML layout into multiple templates was a pain that I never quite figured out. Having worked with Magento, Wordpress and Drupal templates in the past made me want to create a layout template that pulls in a head, header, main, etc template. I couldn't figure out how to do this, so I ended up with a single structure.pt template file that had all my elements. I am looking forward to finding a better template solution.

Now all that is left is the views for the categories and the individual posts. I created a post.py file in my views directory and defined them like so:

from pyramid.view import view_config, render_view
import my_blog.resources
from my_blog.blogdata import BlogData
from pyramid.httpexceptions import HTTPNotFound

#note the context, this view handles the /post and /post/{url} views
@view_config(context='my_blog:resources.Post',  renderer='my_blog:templates/post.pt')
@view_config(context='my_blog:resources.PostName', name='', renderer='my_blog:templates/post.pt')
def post(context, request):
    #get mongoDB posts
    blog_data = BlogData(request)
    #context.__name__ is our {url} and this comes from resources.py
    post = blog_data.get_post_by_url(context.__name__)

    #throw a NotFound Exception if the {url} doesnt exist, this will kick off our 404 view
    if post is None:
        raise HTTPNotFound('notfound').exception
   #convert the date to a human readable string
    postDate = post[u'postDate'].strftime("%B %d %Y")
   #return our found view to the template
    return {'cur_page': '',
          'page_title': post[u'title'],
          'title': post[u'title'],
          'category': post[u'category'],
          'author': post[u'author'],
          'url': post[u'url'],
          'date': postDate,
          'blog_body': post[u'postText'],
          'tags': post[u'tags']}


#these views define our category views
@view_config(context='my_blog:resources.Category',  renderer='my_blog:templates/home_entry_list.pt')
@view_config(context='my_blog:resources.CategoryName',  name='', renderer='my_blog:templates/home_entry_list.pt')
def category_view(context, request):
     #get mongoDB posts
    blog_data = BlogData(request)
    posts = blog_data.get_recent_posts_by_category(context.__name__, 10, 1)

    entries = []
    for post in posts:
        postDate = post[u'postDate'].strftime("%B %d %Y")
        entry = {'title': post[u'title'],
               'url': post[u'url'],
               'date': postDate,
                        'author': post[u'author'],
                        'blog_body': strip_tags(post[u'postText'][:1000]) + '...',  #notice we limit our post_body to 1000 chars on the pages that list multiple posts
                        'tags': post[u'tags']}
        entries.append(entry)
    if not entries:
        raise HTTPNotFound('notfound').exception
    return {'cur_page': 'home', 'page_title': 'Category: ' + context.__name__, 'entries': entries}

That sums it up, I will dive into the admin page later on as I get more time. Next up Rails!

Update: I have uploaded some code to github here. This has an admin page that partially works (posts but not pages). This also pulls a user from the mongodb for authentication. This has disqus/google admin and an html editor in it. Leave a comment below if you have any questions

comments powered by Disqus