SaltyCrane Blog — Notes on JavaScript and web development

Hello Saltycrane

Well, I posted a "goodbye" post on my Blogger blog. I suppose I ought to write a "hello" post here on my new Django blog. Hmm, I'm not quite sure what to write. Some may be wondering where I came up with the name "saltycrane.com" for my domain name. Well it's a sort of personal code name brainstormed by my friend, Bill, who happens to use Python for big screen productions. I like the name, but I just hope it isn't too creative for such a dry blog.


Plans

As I mentioned in my "goodbye" post, I am starting a new web development job at a small startup company. I am very excited about learning and making use of the technology that captured me over that past couple years. I will be doing a lot more Ruby and Rails now so I hope to post notes as I learn that. I also still plan to refine this Django blog and will post about that whenever I make updates. That's about it!

Transition: Bye Blogger, Bo*ing

I've come to a transition point in my blogging and professional life. As for blogging, I am saying goodbye to Blogger and moving on to my new Django blog located at http://www.saltycrane.com/blog/. If everything goes according to plan, this will be my last post here on Blogger. My new blog is pretty much finished, so I might as well start using it for real. Undoubtedly, I will have to make fixes and tweaks, but that's the best way to learn, I suppose. Hopefully, it will serve me as well as Blogger has. To be honest, I hope it will serve me better than Blogger (that's why I'm moving, right?).

Which brings me to my second transition. I am also transitioning out of embedded software and my almost ten year career in the Aerospace/Defense industry to a small web 2.0 startup company. My current company has been great, but kind of like my new blog, I am hoping for something even better. Instead of using C as my primary language, I will be using Python, Django, Ruby, and Rails. It's true I don't have much Ruby or Rails experience, but I plan to learn fast. I am currently in Chapter 5 of Why's (poignant) (and free) (and very funny) guide to Ruby and am starting on my newly purchased Agile Web Development with Rails, 3rd edition Beta. You can expect I will start posting a lot more on Ruby and Rails on my new blog. Hopefully I can add some insightful impressions as a devoted Pythonista. (Though, I don't too much care for that term.)

Well, I'm off! Please come along!

UPDATE 2008/8/6: It looks like I get to mostly stick with Python and Django instead of Ruby and Rails. Cool.

Django Blog Project #9: Migrating Blogger posts with Beautiful Soup

Last post, I talked about adding comments to my new sample blog application. This was about the last basic feature I needed to add before I started actually using it for real. Of course there are still a number of features I'd like to add, such as automatic syntax highlighting with Pygments, and incorporating django-tagging and some more intersting views, not to mention comment moderation. But I think those will have to wait-- I want to start using my new blog for real sometime.

So for the past few days, I've been working on my Beautiful Soup screen scraper script to copy all my Blogger posts over to my new Django blog. Initial results came quickly (it's pretty cool to see such a huge data dump after only a few lines of Beautiful Soup'ing) but the details (especially with the comments) kind of slowed me down. I've finally got everything copied over to my satisfaction. Below is the script I used to do it. Note, I realize it's not pretty-- just a one time use hack. But hopefully someone else doing the same thing might find it useful.

#!/usr/bin/env python

import datetime
import os
import re
import urllib2
from BeautifulSoup import BeautifulSoup
from myblogapp.models import Post, LegacyComment
from django.contrib.comments.models import FreeComment

URL = ''.join([
        'http://iwiwdsmi.blogspot.com/search?',
        'updated-min=2006-01-01T00%3A00%3A00-08%3A00&'
        'updated-max=2009-01-01T00%3A00%3A00-08%3A00&',
        'max-results=1000'
        ])
html = urllib2.urlopen(URL).read()
soup = BeautifulSoup(html)

for post in soup.html.body.findAll('div', {'class': 'post'}):
    print
    print '--------------------------------------------------------------'

    # save the post title and permalink
    h3 = post.find('h3', {'class': 'post-title'})
    post_href = h3.find('a')['href']
    post_title = h3.find('a').string
    post_slug = os.path.basename(post_href).rstrip('.html')
    print post_slug
    print post_href
    print post_title

    # save the post body
    div = post.find('div', {'class': 'post-body'})
    [toremove.extract() for toremove in div.findAll('script')]
    [toremove.extract() for toremove in div.findAll('span', {'id': 'showlink'})]
    [toremove.extract() for toremove in div.findAll('div', {'style': 'clear: both;'})]
    [toremove.parent.extract() for toremove in div.findAll(text='#fullpost{display:none;}')]
    post_body = ''.join([str(item)
                         for item in div.contents
                         ]).rstrip()
    post_body = re.sub(r"iwiwdsmi\.blogspot\.com/(\d{4}/\d{2}/[\w\-]+)\.html", 
                       r"www.saltycrane.com/blog/\1/", 
                       post_body)

    # count number of highlighted code sections 
    highlight = div.findAll('div', {'class': 'highlight'})
    if highlight:
        hl_count += len(highlight)
        hl_list.append(post_title)

    # save the timestamp
    a = post.find('a', {'class': 'timestamp-link'})
    try:
        post_timestamp = a.string
    except:
        match = re.search(r"\.com/(\d{4})/(\d{2})/", post_href)
        if match:
            year = match.group(1)
            month = match.group(2)
        post_timestamp = "%s/01/%s 11:11:11 AM" % (month, year)
    print post_timestamp

    # save the tags (this is ugly, i know)
    if 'error' in post_title.lower():
        post_tags = ['error']
    else:
        post_tags = []
    span = post.find('span', {'class': 'post-labels'})
    if span:
        a = span.findAll('a', {'rel': 'tag'})
    else:
        a = post.findAll('a', {'rel': 'tag'})
    post_tags = ' '.join([tag.string for tag in a] + post_tags)
    if not post_tags:
        post_tags = 'untagged'
    print post_tags

    # add Post object to new blog
    if True:
        p = Post()
        p.title = post_title
        p.body = post_body
        p.date_created = datetime.datetime.strptime(post_timestamp, "%m/%d/%Y %I:%M:%S %p")
        p.date_modified = p.date_created
        p.tags = post_tags
        p.slug = post_slug
        p.save()

    # check if there are comments
    a = post.find('a', {'class': 'comment-link'})
    if a:
        comm_string = a.string.strip()
    else:
        comm_string = "0"
    if comm_string[0] != "0":
        print
        print "COMMENTS:"

        # get the page with comments
        html_single = urllib2.urlopen(post_href).read()
        soup_single = BeautifulSoup(html_single)

        # get comments
        comments = soup_single.html.body.find('div', {'class': 'comments'})
        cauth_list = comments.findAll('dt')
        cbody_list = comments.findAll('dd', {'class': 'comment-body'})
        cdate_list = comments.findAll('span', {'class': 'comment-timestamp'})

        if not len(cauth_list)==len(cbody_list)==len(cdate_list):
            raise "didn't get all comment data"

        for auth, body, date in zip(cauth_list, cbody_list, cdate_list):
            
            # create comment in database
            lc = LegacyComment()
            lc.body = str(body.p)

            # find author
            lc.author = "Anonymous"
            auth_a = auth.findAll('a')[-1]
            auth_no_a = auth.contents[2]
            if auth_a.string:
                lc.author = auth_a.string
            elif auth_no_a:
                match = re.search(r"\s*([\w\s]*\w)\s+said", str(auth_no_a))
                if match:
                    lc.author = match.group(1)
            print lc.author

            # find website
            try:
                lc.website = auth_a['href']
            except KeyError:
                lc.website = ''
            print lc.website

            # other info
            lc.date_created = datetime.datetime.strptime(
                date.a.string.strip(), "%m/%d/%Y %I:%M %p")
            print lc.date_created
            lc.date_modified = lc.date_created
            lc.post_id = p.id
            lc.save()

I also made some changes to my Django blog code as I migrated my Blogger posts. The main addition was a LegacyComment model along with the associated views and templates. My Blogger comments consisted of HTML markup, but I didn't want to allow arbitrary HTML in my new comments for fear of cross site scripting. So I separated my legacy Blogger comments from my new Django site comments.



models.py

Here are my model changes. I added a LegacyComment class which contains pertinent comment attributes and a ForeignKey to the post that it belongs to. I also added a lc_count (for legacy comment count) field to the Post class which stores the number of comments for the post. It is updated by the save() method in the LegacyComment class every time a comment is saved. Hmmm, I just realized the count will be wrong if I ever edit these comments. Well, since these are legacy comments, hopefully I won't have to edit them.

~/src/django/myblogsite/myblogapp/models.py:
import re
from django.db import models

class Post(models.Model):
    title = models.CharField(maxlength=200)
    slug = models.SlugField(maxlength=100)
    date_created = models.DateTimeField() #auto_now_add=True)
    date_modified = models.DateTimeField()
    tags = models.CharField(maxlength=200)
    body = models.TextField()
    body_html = models.TextField(editable=False, blank=True)
    lc_count = models.IntegerField(default=0, editable=False)

    def get_tag_list(self):
        return re.split(" ", self.tags)

    def get_absolute_url(self):
        return "/blog/%d/%02d/%s/" % (self.date_created.year,
                                      self.date_created.month,
                                      self.slug)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ["-date_created"]

    class Admin:
        pass

class LegacyComment(models.Model):
    author = models.CharField(maxlength=60)
    website = models.URLField(core=False)
    date_created = models.DateTimeField()
    date_modified = models.DateTimeField()
    body = models.TextField()
    post = models.ForeignKey(Post)

    def save(self):
        p = Post.objects.get(id=self.post.id)
        p.lc_count += 1
        p.save()
        super(LegacyComment, self).save()

    class Meta:
        ordering = ["date_created"]

    class Admin:
        pass


views.py

Here is an excerpt from my views.py file showing the changes:

~/src/django/myblogsite/myblogapp/views.py:
import re
from datetime import datetime
from django.shortcuts import render_to_response
from myblogsite.myblogapp.models import Post, LegacyComment

MONTH_NAMES = ('', 'January', 'Feburary', 'March', 'April', 'May', 'June', 'July',
               'August', 'September', 'October', 'November', 'December')
MAIN_TITLE = "Sofeng's Blog 0.0.7"

def frontpage(request):
    posts, pagedata = init()
    posts = posts[:5]
    pagedata.update({'post_list': posts,
                     'subtitle': '',})
    return render_to_response('listpage.html', pagedata)

def singlepost(request, year, month, slug2):
    posts, pagedata = init()
    post = posts.get(date_created__year=year,
                            date_created__month=int(month),
                            slug=slug2,)
    legacy_comments = LegacyComment.objects.filter(post=post.id)
    pagedata.update({'post': post,
                     'lc_list': legacy_comments,})
    return render_to_response('singlepost.html', pagedata)


Templates

In the list page template I used the truncatewords_html template filter to show a 50 word post summary on the list pages instead of the full post. I also added the legacy comment count with the Django free comment count to display the total number of comments.

Excerpt from ~/src/django/myblogsite/templates/listpage.html:
{% block main %}
  <br>
  {% for post in post_list %}
    <h4><a href="/blog/{{ post.date_created|date:"Y/m" }}/{{ post.slug }}/">
        {{ post.title }}</a>
    </h4>
    {{ post.body|truncatewords_html:"50" }}
    <a href="{{ post.get_absolute_url }}">Read more...</a><br>
    <br>
    <hr>
    <div class="post_footer">
      {% ifnotequal post.date_modified.date post.date_created.date %}
        Last modified: {{ post.date_modified.date }}<br>
      {% endifnotequal %}
      Date created: {{ post.date_created.date }}<br>
      Tags: 
      {% for tag in post.get_tag_list %}
        <a href="/blog/tag/{{ tag }}/">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
      {% endfor %}
      <br>

      {% get_free_comment_count for myblogapp.post post.id as comment_count %}
      <a href="{{ post.get_absolute_url }}#comments">
        {{ comment_count|add:post.lc_count }} 
        Comment{{ comment_count|add:post.lc_count|pluralize}}</a>

    </div>
    <br>
  {% endfor %}
{% endblock %}

In the single post template, I added the display of the Legacy comments in addition to the Django free comments.

Excerpt from ~/src/django/myblogsite/templates/singlepost.html:
 <a name="comments"></a>
  {% if lc_list %}
    <h4>{{ lc_list|length }} Legacy Comment{{lc_list|length|pluralize}}</h4>
  {% endif %}
  {% for legacy_comment in lc_list %}
    <br>
    <a name="lc{{ legacy_comment.id }}" href="#lc{{ legacy_comment.id }}">
      #{{ forloop.counter }}</a>
    {% if legacy_comment.website %}
      <a href="{{ legacy_comment.website }}">
        <b>{{ legacy_comment.author|escape }}</b></a> 
    {% else %}
      <b>{{ legacy_comment.author|escape }}</b>
    {% endif %}
    commented,
    on {{ legacy_comment.date_created|date:"F j, Y" }} 
    at {{ legacy_comment.date_created|date:"P" }}:
    {{ legacy_comment.body }}
  {% endfor %}
  <br>

That's it. Hopefully, I can start using my new blog soon. Please browse around on the new Django site and let me know if you run across any problems. When everything looks to be OK, I'll start posting only on my new Django site.

Here is a snapshot screenshot of version 0.0.8:


The live site can be viewed at: http://saltycrane.com/blog


Related posts:
  Django Blog Project #1: Creating a basic blog
  Django Blog Project #2: Deploying at Webfaction
  Django Blog Project #3: Using CSS and Template Inheritance
  Django Blog Project #4: Adding post metadata
  Django Blog Project #5: YUI CSS and serving static media
  Django Blog Project #6: Creating standard blog views
  Django Blog Project #7: Adding a simple Atom feed
  Django Blog Project #8: Adding basic comment functionality

On the quest for consistent keybindings

I'm trying to get consistent keybindings in the applications which I use the most starting with Emacs, then bash/screen, conkeror (my web browser), and others like KPDF and OpenOffice. I haven't got complete consistency, but I'm making progress. Here are my notes on setting the keybindings for a few of the applications. Maybe when I find the perfect setup, I will write about my complete configuration. Until then, here are some sparse notes. Note, for some reason, I've mixed in vi-style, h-j-k-l, movement keybindings with the standard emacs keybindings. I use control+h,j,k,l to move around by character or line and meta+h,j,k,l to move by word or page. I've been using this for several months and it works pretty well-- much better than using pgup/pgdown and the arrow keys.



Emacs

Here is part of my ~/.emacs:

;; buffer switching
(iswitchb-mode t)
(global-set-key "\C-b" 'switch-to-buffer)

;; movement key bindings (use h-j-k-l for movement like vi)
(global-set-key "\C-l" 'forward-char)
(global-set-key "\C-h" 'backward-char)
(global-set-key "\C-j" 'next-line)
(global-set-key "\C-k" 'previous-line)
(global-set-key "\M-l" 'forward-word)
(global-set-key "\M-h" 'backward-word)
(global-set-key "\M-j" 'scroll-up)
(global-set-key "\M-k" 'scroll-down)

;; rebind displaced movement key bindings
(global-set-key "\C-p" 'kill-line)
(global-set-key "\M-v" 'downcase-word)
(global-set-key "\M-b" 'recenter)


Readline (bash)

To change the keybindings for readline (bash), I edited my ~/.inputrc. Luckily, readline is very similar to Emacs and there are a lot of commands that I didn't know about. Here are a few examples. For a full list of commands see the readline manpage.

# ~/.inputrc
"\C-h": backward-char
"\C-l": forward-char
"\M-h": backward-word
"\M-l": forward-word


GNU Screen

Here are some keybindings I use in GNU Screen. I use C-b to switch windows like Emacs buffers. I enter copy mode by pressing M-k and exit by pressing C-g. The "-m" means the keybinding is for copy mode only. "stuff" is used to stuff some stuff into the input buffer. "^" is used for Ctrl and "^[" is used for Meta (Alt).

# ~/.screenrc
bindkey "^B" select         # like emacs switch buffers
bindkey "^[k" eval "copy" "stuff ^b"
bindkey "^k" eval "copy" "stuff k"
bindkey -m "^[j" stuff ^f   # move down one page
bindkey -m "^[k" stuff ^b   # move up one page
bindkey -m "^[h" stuff b    # move backward word
bindkey -m "^[l" stuff w    # move forward by word
bindkey -m "^j" stuff j     # move down one line
bindkey -m "^k" stuff k     # move up one line
bindkey -m "^h" stuff h     # move back one character
bindkey -m "^l" stuff l     # move forward one character
bindkey -m "^e" stuff $     # move to end of line
bindkey -m "^a" stuff 0     # move to beginning of line
bindkey -m "^f" stuff ^s    # incremental search forward
bindkey -m "^ " stuff " "   # set mark
bindkey -m "^g" stuff q     # exit copy mode


Conkeror

My ~/.conkerorrc keybindings section is pretty long, so I won't include it. But again, I use C-b to switch buffers, and Ctrl and Meta plus h-j-k-l to navigate.



OpenOffice.org

This is directly from Appendix A Keyboard Shortcuts in the Getting Started Guide. Unfortunately, OpenOffice doesn't allow me to use the Meta (Alt) key for custom keybindings so I can't use my usual Emacs keybindings. Apparently, Peter in this forum thread had the same lament. If anyone knows how to fix this, please let me know.

To adapt shortcut keys to your needs, use the Customize dialog, as described below.
    1) Select Tools > Customize > Keyboard. The Customize dialog (Figure 1) opens.
    2) To have the shortcut key assignment available in all components of OpenOffice.org select the
       OpenOffice.org button.
    3) Next select the required function from the Category and Function lists.
    4) Now select the desired shortcut keys in the Shortcut keys list and click the Modify button at
       the upper right.
    5) Click OK to accept the change. Now the chosen shortcut keys will execute the function
       chosen in step 3 above whenever they are pressed.


KPDF

I like KPDF better than Evince. I haven't tried anything else. To change some keyboard shortcuts in KPDF go to "Settings", "Configure Shortcuts..."

Django Blog Project #8: Adding basic comment functionality with Django Free Comments

Update 2009-05-08: The notes here apply to Django 0.96 and not Django 1.0 or later. In particular, FreeComment has been changed to Comment in Django 1.0. For current documentation, please go here instead.

This post describes how I added basic commenting functionality using Django's Free Comments. Note, this built-in functionality does not support comment moderation (used to counteract spam) or other nice features. I tried to use David Bennett's comment_utils, but I couldn't get it to work. So for now, I'm just going to use Django Free Comments and hopefully add in moderation and other features later.

I basically followed the instructions on the wiki: Using Django's Free Comments. See there for more information. It was pretty easy to set up.


settings.py

In my settings.py file, I added django.contrib.comments to the list of installed apps.

Excerpt from ~/src/django/myblogsite/settings.py:
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.admin',
    'django.contrib.comments',
    'iwiwdsmi.myblogapp',
)


urls.py

I modified my urls.py to use the comment module's URLConf.

~/src/django/myblogsite/urls.py:
from django.conf.urls.defaults import *
from django.contrib.comments.models import FreeComment
from iwiwdsmi.myblogapp.views import *
from iwiwdsmi.feeds import *

feeds = {
    'latest': LatestPosts,
}

urlpatterns = patterns(
    '',
    (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': '/home/sofeng/src/django/myblogsite/media'}),

    (r'^admin/', include('django.contrib.admin.urls')),
    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', 
     {'feed_dict': feeds}),
    (r'^comments/', include('django.contrib.comments.urls.comments')),
    
    (r'^myview1/$', myview1),
    (r'^blog/$', frontpage),
    (r'^blog/(\d{4})/(\d{2})/([\w\-]+)/$', singlepost),
    (r'^blog/(\d{4})/$', yearview),
    (r'^blog/(\d{4})/(\d{2})/$', monthview),
    (r'^blog/tag/([\w\-]+)/$', tagview),
)


python manage.py syncdb

To install the comments model, I ran python manage.py syncdb:

$ cd ~/src/django/myblogsite
$ python manage.py syncdb


List page template

Then I modified my list page template to display the number of comments for each post:

~/src/django/myblogsite/templates/listpage.html:
{% extends "base.html" %}

{% load comments %}

{% block title %}
  {{ main_title }}
  {% if subtitle %}:{% endif %}
  {{ subtitle }}
{% endblock %}

{% block header1 %}
  {% if subtitle %}
    <a href="/blog/">{{ main_title }}</a>
  {% else %}
    {{ main_title }}
  {% endif %}
{% endblock %}

{% block header2 %}
  {{ subtitle }}
{% endblock %}

{% block main %}
  {% for post in post_list %}
    <h3><a href="/blog/{{ post.date_created|date:"Y/m" }}/{{ post.slug }}/">
        {{ post.title }}</a>
    </h3>
    {{ post.body }}
    <hr>
    <div class="post_footer">
      {% ifnotequal post.date_modified.date post.date_created.date %}
        Last modified: {{ post.date_modified.date }}<br>
      {% endifnotequal %}
      Date created: {{ post.date_created.date }}<br>
      Tags: 
      {% for tag in post.get_tag_list %}
        <a href="/blog/tag/{{ tag }}/">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
      {% endfor %}
      <br>
      {% get_free_comment_count for myblogapp.post post.id as comment_count %}
      <a href="{{ post.get_absolute_url }}">{{ comment_count }} Comment{{ comment_count|pluralize}}</a>
    </div>
    <br>
  {% endfor %}
{% endblock %}

Oh, I forgot to mention, I implemented a get_absolute_url method in my Post model. This is the preferred way to specify the url to the detail view of my Post object. The code for the method is shown below:

~/src/django/myblogsite/myblogapp/models.py:
import re
from django.db import models

class Post(models.Model):
    title = models.CharField(maxlength=200)
    slug = models.SlugField(maxlength=100)
    date_created = models.DateTimeField() #auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    tags = models.CharField(maxlength=200)
    body = models.TextField()

    def get_tag_list(self):
        return re.split(" ", self.tags)

    def get_absolute_url(self):
        return "/blog/%d/%02d/%s/" % (self.date_created.year,
                                      self.date_created.month,
                                      self.slug)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ["-date_created"]

    class Admin:
        pass


Detail page template

Then I modified my single post, or detail page, template:

~/src/django/myblogsite/templates/singlepost.html:
{% extends "base.html" %}

{% load comments %}

{% block title %}
  {{ main_title }}: {{ post.title }}
{% endblock %}

{% block header1 %}
  <a href="/blog/">{{ main_title }}</a>
{% endblock %}

{% block main %}
  <h3>{{ post.title }}</h3>
  {{ post.body }}
  <hr>
  <div class="post_footer">
    {% ifnotequal post.date_modified.date post.date_created.date %}
      Last modified: {{ post.date_modified.date }}<br>
    {% endifnotequal %}
    Date created: {{ post.date_created.date }}<br>
    Tags: 
    {% for tag in post.get_tag_list %}
      <a href="/blog/tag/{{ tag }}/">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
    {% endfor %}
  </div>
  <br>
  {% get_free_comment_list for myblogapp.post post.id as comment_list %}
  {% get_free_comment_count for myblogapp.post post.id as comment_count %}
  {% if comment_list %}
    <h4>{{ comment_count }} Comment{{ comment_count|pluralize}}</h4>
  {% endif %}
  {% for comment in comment_list %}
    <a href="#c{{ comment.id }}">#{{ forloop.counter }}</a>
    <b>{{ comment.person_name|escape }}</b> commented, 
      on {{ comment.submit_date|date:"F j, Y" }} at {{ comment.submit_date|date:"P" }}:
    {{ comment.comment|escape|urlizetrunc:40|linebreaks }}
  {% endfor %}
  <br>
  <h4>Post a comment</h4>
  {% free_comment_form for myblogapp.post post.id %}

{% endblock %}


Create templates for comment preview and comment posted

At this point, I can view these template changes on my blog list view or detail view. However, if I try to add a comment, I will get a TemplateDoesNotExist exception. I need two new templates-- one for the comment preview and one for the page just after the comment is posted. I just copied these from the wiki. Probably, I should dress these up a little to match my site. For now, I'll use the generic ones. I put them in a new directory called comments in my templates directory.

~/src/django/myblogsite/templates/comments/free_preview.html:
<h1>Preview your comment</h1>

<form action="/comments/postfree/" method="post">
    {% if comment_form.has_errors %}
        <p><strong style="color: red;">Please correct the following errors.</strong></p>
    {% else %}
        <div class="comment">
        {{ comment.comment|escape|urlizetrunc:"40"|linebreaks }}
        <p class="date small">Posted by <strong>{{ comment.person_name|escape }}</strong></p>
        </div>

        <p><input type="submit" name="post" value="Post public comment" /></p>

        <h1>Or edit it again</h1>
    {% endif %}

    {% if comment_form.person_name.errors %}
        {{ comment_form.person_name.html_error_list }}
    {% endif %}

    <p><label for="id_person_name">Your name:</label> {{ comment_form.person_name }}</p>

    {% if comment_form.comment.errors %}
        {{ comment_form.comment.html_error_list }}
    {% endif %}

    <p>
        <label for="id_comment">Comment:</label>
        <br />
        {{ comment_form.comment }}
    </p>
    
    <input type="hidden" name="options" value="{{ options }}" />
    <input type="hidden" name="target" value="{{ target }}" />
    <input type="hidden" name="gonzo" value="{{ hash }}" />

    <p>
        <input type="submit" name="preview" value="Preview revised comment" />
    </p>
</form>

~/src/django/myblogsite/templates/comments/posted.html:
<h1>Comment posted successfully</h1>

<p>Thanks for contributing.</p>

{% if object %}
    <ul>
        <li><a href="{{ object.get_absolute_url }}">View your comment</a></li>
    </ul>
{% endif %}


Add some comments

I'm done. I added some comments and saw them show up on my page.



Upload, update, restart server

I uploaded to webfaction, updated my Mercurial repository, and restarted the Apache server. Everything's good.

Here is a snapshot screenshot of a detail page with some comments:


The live site can be viewed at: http://saltycrane.com/blog


Related posts:
  Django Blog Project #1: Creating a basic blog
  Django Blog Project #2: Deploying at Webfaction
  Django Blog Project #3: Using CSS and Template Inheritance
  Django Blog Project #4: Adding post metadata
  Django Blog Project #5: YUI CSS and serving static media
  Django Blog Project #6: Creating standard blog views
  Django Blog Project #7: Adding a simple Atom feed

Django Blog Project #7: Adding a simple Atom feed

Setting up a basic Atom feed for my new blog was not too difficult. I followed the instructions in the Django documentation and created a basic Atom feed for my 10 latest posts. There were a couple of things I couldn't figure out: 1) how to create a feed for a specific tag, and 2) how to redirect my feed to FeedBurner. I know these things are not that hard, but I want to get my new blog working as soon as possible so I can start using it. If anyone knows how to do these things, I'd be grateful for your feedback. Regarding item 1, I plan to replace my tagging functionality hack with the django-tagging application so maybe I will wait until then to add tag-specific feeds. Regarding item 2, I read about using .htaccess to do redirects, but I couldn't figure out how to do a feed redirect at Webfaction.

Anyways, here are the steps I took to create a basic feed of my latests posts. Refer to the Django syndication feed framework documentation for more details.



Modify the URLConf
Per the documentation, I modified my ~/src/django/myblogsite/urls.py:
from django.conf.urls.defaults import *
from iwiwdsmi.myblogapp.views import *
from iwiwdsmi.feeds import *

feeds = {
    'latest': LatestPosts,
}

urlpatterns = patterns(
    '',
    (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': '/home/sofeng/src/django/myblogsite/media'}),

    (r'^admin/', include('django.contrib.admin.urls')),
    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', 
     {'feed_dict': feeds}),    
    
    (r'^blog/$', frontpage),
    (r'^blog/(\d{4,4})/(\d{2,2})/([\w\-]+)/$', singlepost),
    (r'^blog/(\d{4,4})/$', yearview),
    (r'^blog/(\d{4,4})/(\d{2,2})/$', monthview),
    (r'^blog/tag/([\w\-]+)/$', tagview),
)


Create a feeds.py file

I created the following file at ~/src/django/myblogsite/feeds.py. Note, I factored out my main title, "Sofeng's Blog 0.0.6", into the variable MAIN_TITLE in my views.py file and used it here. The class attributes, title and description are strings describing the feed.

from django.contrib.syndication.feeds import Feed
from django.utils.feedgenerator import Atom1Feed
from iwiwdsmi.myblogapp.models import Post
from iwiwdsmi.myblogapp.views import MAIN_TITLE

class LatestPosts(Feed):
    title = MAIN_TITLE
    link = "/blog/"
    description = MAIN_TITLE + ": Latest posts"
    feed_type = Atom1Feed
    
    def items(self):
        return Post.objects.order_by('-date_created')[:10]


Create feed templates

I created two feed templates which are used to display the title and body for each post. They each only contain one line.

~/src/django/myblogsite/templates/feeds/latest_title.html:
{{ obj.title }}

~/src/django/myblogsite/templates/feeds/latest_description.html:
{{ obj.body }}


Display a link to the new feed

I added a link to my new feed in my base.html template.

Excerpt from ~/src/django/myblogsite/templates/base.html:
        <h4>FEEDS</h4>
          <a href="/feeds/latest/" rel="alternate" type="application/rss+xml"><img alt="" style="vertical-align:middle;border:0" src="http://www.feedburner.com/fb/images/pub/feed-icon16x16.png"/></a>
        <a href="/feeds/latest/">Subscribe to Atom feed</a><br>


Change site name

When I first created my Django project, I installed the django.contrib.sites app (by default), but I did not change my site name, so my feed used the site example.com instead of my real site. Here is how I changed my site name:

  1. I went to the admin page at http://127.0.0.1:8000/admin/
  2. I clicked on "Sites", then on "example.com".
  3. I changed "example.com" to "saltycrane.com" and clicked the "Save" button.


Deploy and Test

I pushed my changes to the Webfaction server, updated my repository, and restarted the Apache server. Then I pointed Firefox at http://saltycrane.com/blog/ and clicked on my new "Subscribe to Atom feed" link. I subscribed to the feed using "Google" and chose "Add to Google Reader" and saw my new feed in Google Reader.
Here is a screenshot of my feed in my Google Reader:


Extra links on redirection for my reference:
Webfaction knowledgebase article on redirection
Blog article on redirecting feeds to FeedBurner

Related posts:
  Django Blog Project #1: Creating a basic blog
  Django Blog Project #2: Deploying at Webfaction
  Django Blog Project #3: Using CSS and Template Inheritance
  Django Blog Project #4: Adding post metadata
  Django Blog Project #5: YUI CSS and serving static media
  Django Blog Project #6: Creating standard blog views
  Django Blog Project #8: Adding basic comment functionality with Django Free Comments

Django Blog Project #6: Creating standard blog views

My last django post actually got a little bit of publicity-- a mention on Python reddit, and a mention in the Yahoo User Interface blog In the Wild for June 20 article. So I feel a little pressure to produce intelligent, engaging content so as not to disappoint the hordes of new readers I must now have. Unfortunately, the topic of this post is pretty dry. Goodbye, new readers. I'll try to find you another time.

The topic of this post is creating standard blog views. So far, I've created a simple blog application that allows me to log in to an Admin interface and enter blog posts, I added some post metadata such as date and tags, and, last time, I added YUI stylesheets to create a cross browser layout solution. However, until now, my trying-so-hard-to-be-a-real website only had a single front page view with a list of all the blog posts. I needed to add other views, such as a single post view, an archive view, and a tag view. Here are the changes I made to implement these views.

Warning: I am a newbie to Django and web development. I've created this blog series to document my journey in creating a blog application using Django. It does not necessarily represent Django best practices. To the extent of my knowledge, I try to follow Django conventions and philosophies, but I know I must still have a number of violations, particularly in this latest revision. Hopefully, as I learn better ways of doing things, I can post corrections. If you have corrections, please leave a comment.

For a much better example of a Django blog application, see Adam Gomaa's Django blog site source code repository. Though I don't understand everything in there, I've referenced it a number of times while creating my blog site. Update 2008-06-29: I just discovered that James Bennett, a Django contributer, also has blog application source code available online. Seeing as he has written a book on Django, this <sarcasm>might</sarcasm> be a good place to look as well.



models.py

In my models.py file, I added a slug field to store the last part of the url and a method called get_tag_list to return the tags for the post as a list.

~/src/django/myblogsite/myblogapp/models.py:
import re
from django.db import models

class Post(models.Model):
    title = models.CharField(maxlength=200)
    slug = models.SlugField(maxlength=100)
    date_created = models.DateTimeField()
    date_modified = models.DateTimeField(auto_now=True)
    tags = models.CharField(maxlength=200)
    body = models.TextField()

    def get_tag_list(self):
        return re.split(" ", self.tags)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ["-date_created"]

    class Admin:
        pass


views.py

In my views.py file, to the frontpage view, I added a single post view, an archive view by year, an archive view by month, and a tag view. I also added a couple functions to create an archive index and a tag index.

~/src/django/myblogsite/myblogapp/views.py:
import re
from datetime import datetime
from django.shortcuts import render_to_response
from myblogsite.myblogapp.models import Post

MONTH_NAMES = ('', 'January', 'Feburary', 'March', 'April', 'May', 'June', 'July',
               'August', 'September', 'October', 'November', 'December')

def frontpage(request):
    posts, pagedata = init()
    pagedata.update({'subtitle': '',})
    return render_to_response('listpage.html', pagedata)

def singlepost(request, year, month, slug2):
    posts, pagedata = init()
    post = posts.get(date_created__year=year,
                            date_created__month=int(month),
                            slug=slug2,)
    pagedata.update({'post': post})
    return render_to_response('singlepost.html', pagedata)
    
def yearview(request, year):
    posts, pagedata = init()
    posts = posts.filter(date_created__year=year)
    pagedata.update({'post_list': posts,
                     'subtitle': 'Posts for %s' % year})
    return render_to_response('listpage.html', pagedata)

def monthview(request, year, month):
    posts, pagedata = init()
    posts = posts.filter(date_created__year=year)
    posts = posts.filter(date_created__month=int(month))
    pagedata.update({'post_list': posts,
                     'subtitle': 'Posts for %s %s' % (MONTH_NAMES[int(month)], year),})
    return render_to_response('listpage.html', pagedata)

def tagview(request, tag):
    allposts, pagedata = init()
    posts = []
    for post in allposts:
        tags = re.split(' ', post.tags)
        if tag in tags:
            posts.append(post)
    pagedata.update({'post_list': posts,
                     'subtitle': "Posts tagged '%s'" % tag,})
    return render_to_response('listpage.html', pagedata)

def init():
    posts = Post.objects.all()
    tag_data = create_tag_data(posts)
    archive_data = create_archive_data(posts)
    pagedata = {'version': '0.0.5',
                'post_list': posts,
                'tag_counts': tag_data,
                'archive_counts': archive_data,}
    return posts, pagedata

def create_archive_data(posts):
    archive_data = []
    count = {}
    mcount = {}
    for post in posts:
        year = post.date_created.year
        month = post.date_created.month
        if year not in count:
            count[year] = 1
            mcount[year] = {}
        else:
            count[year] += 1
        if month not in mcount[year]:
            mcount[year][month] = 1
        else:
            mcount[year][month] += 1
    for year in sorted(count.iterkeys(), reverse=True):
        archive_data.append({'isyear': True,
                             'year': year, 
                             'count': count[year],})
        for month in sorted(mcount[year].iterkeys(), reverse=True):
            archive_data.append({'isyear': False,
                                 'yearmonth': '%d/%02d' % (year, month),
                                 'monthname': MONTH_NAMES[month], 
                                 'count': mcount[year][month],})
    return archive_data

def create_tag_data(posts):
    tag_data = []
    count = {}
    for post in posts:
        tags = re.split(" ", post.tags)
        for tag in tags:
            if tag not in count:
                count[tag] = 1
            else:
                count[tag] += 1
    for tag, count in sorted(count.iteritems(), key=lambda(k, v): (v, k), reverse=True):
        tag_data.append({'tag': tag,
                         'count': count,})
    return tag_data


Templates
I made the following template changes:
  • I modified the sidebar in base.html to display an archive index and a tag index.
  • I replaced frontpage.html with a more generic listpage.html template used for displaying the frontpage, archives, and tag views.
  • I added a singlepost.html template for displaying a single post view.
  • In all the templates, I created links to navigate among the different views.
~/src/django/myblogsite/templates/base.html:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" 
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.2/build/reset-fonts-grids/reset-fonts-grids.css">
  <link rel="stylesheet" type="text/css" href="/site_media/css/mystyle.css">
  <title>{% block title %}Sofeng's Blog {{ version }}{% endblock %}</title>
</head>

<body>
  <div id="doc" class="yui-t4">

    <div id="hd">
      <h1>
        <img align="center" src="/site_media/image/crane.jpg">
        {% block header1 %}Sofeng's Blog {{ version }}{% endblock %}
      </h1>
      <h2>{% block header2 %}{% endblock %}</h2>
    </div>

    <div id="bd">
      <div id="yui-main">
        <div class="yui-b">
          {% block main %}{% endblock %}
        </div>
      </div>
    
      <div class="yui-b" id="sidebar">
        {% block sidebar %}
        <h4>ABOUT</h4>
        <p>This is my new blog created using <a href="http://www.djangoproject.com">Django</a>,
          a <a href="http://www.python.org">Python</a> web framework. This site is under 
          construction. My current blog is located at: 
          <a href="http://iwiwdsmi.blogspot.com">http://iwiwdsmi.blogspot.com</a>.
        </p>
        <br>
        <h4>TAGS</h4>
        {% for line in tag_counts %}
            <a href="/blog/tag/{{ line.tag }}/">{{ line.tag }}</a> ({{ line.count }})<br>
        {% endfor %}
        <br>
        <h4>ARCHIVE</h4>
        {% for line in archive_counts %}
          {% if line.isyear %}
            {% if not forloop.first %}
              <br>
            {% endif %}
              <a href="/blog/{{ line.year }}/">{{ line.year }}</a> ({{ line.count }})<br>
          {% else %}
                <a href="/blog/{{ line.yearmonth }}/">{{ line.monthname }}</a> ({{ line.count }})<br>
          {% endif %}
        {% endfor %}
        <br>
        {% endblock %}
      </div>
    </div>

    <div id="ft">
      Created with <a href="http://www.djangoproject.com/">Django</a>.<br>
      Hosted by <a href="http://www.webfaction.com/">Webfaction</a>.
    </div>
  </div>

</body>
</html>
~/src/django/myblogsite/templates/listpage.html:
{% extends "base.html" %}

{% block title %}
  Sofeng's Blog {{ version }}
  {% if subtitle %}:{% endif %}
  {{ subtitle }}
{% endblock %}

{% block header1 %}
  {% if subtitle %}
    <a href="/blog/">Sofeng's Blog {{ version }}</a>
  {% else %}
    Sofeng's Blog {{ version }}
  {% endif %}
{% endblock %}

{% block header2 %}
  {{ subtitle }}
{% endblock %}

{% block main %}
  {% for post in post_list %}
    <h3><a href="/blog/{{ post.date_created|date:"Y/m" }}/{{ post.slug }}/">
        {{ post.title }}</a>
    </h3>
    {{ post.body }}
    <hr>
    <div class="post_footer">
      {% ifnotequal post.date_modified.date post.date_created.date %}
        Last modified: {{ post.date_modified.date }}<br>
      {% endifnotequal %}
      Date created: {{ post.date_created.date }}<br>
      Tags: 
      {% for tag in post.get_tag_list %}
        <a href="/blog/tag/{{ tag }}/">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
      {% endfor %}
    </div>
    <br>
  {% endfor %}
{% endblock %}
~/src/django/myblogsite/templates/singlepost.html:
{% extends "base.html" %}

{% block title %}
  Sofeng's Blog {{ version }}: {{ post.title }}
{% endblock %}

{% block header1 %}
  <a href="/blog/">Sofeng's Blog {{ version }}</a>
{% endblock %}

{# to fix IE #}
{% block header2 %} {% endblock %}

{% block main %}
    <h3>{{ post.title }}</h3>
    {{ post.body }}
    <hr>
    <div class="post_footer">
      {% ifnotequal post.date_modified.date post.date_created.date %}
        Last modified: {{ post.date_modified.date }}<br>
      {% endifnotequal %}
      Date created: {{ post.date_created.date }}<br>
      Tags: 
      {% for tag in post.get_tag_list %}
        <a href="/blog/tag/{{ tag }}/">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
      {% endfor %}
    </div>
    <br>
{% endblock %}


urls.py

As the last step in my bottom-up approach, I modified my urls.py and urls_webfaction.py.

~/src/django/myblogsite/urls_webfaction.py:
from django.conf.urls.defaults import *
from myblogsite.myblogapp.views import *

urlpatterns = patterns(
    '',
    (r'^admin/', include('django.contrib.admin.urls')),
    (r'^myview1/$', myview1),
    (r'^blog/$', frontpage),
    (r'^blog/(\d{4,4})/(\d{2,2})/([\w\-]+)/$', singlepost),
    (r'^blog/(\d{4,4})/$', yearview),
    (r'^blog/(\d{4,4})/(\d{2,2})/$', monthview),
    (r'^blog/tag/([\w\-]+)/$', tagview),
)


Finish

I uploaded, updated, and restarted the Apache server as usual.


Here is a snapshot screenshot of the site:

The live site is located at: http://saltycrane.com/blog/

Related posts:
  Django Blog Project #1: Creating a basic blog
  Django Blog Project #2: Deploying at Webfaction
  Django Blog Project #3: Using CSS and Template Inheritance
  Django Blog Project #4: Adding post metadata
  Django Blog Project #5: YUI CSS and serving static media
  Django Blog Project #7: Adding a simple Atom feed
  Django Blog Project #8: Adding basic comment functionality

How to get the current date and time in Python

Here is an example of how to get the current date and time using the datetime module in Python:

import datetime

now = datetime.datetime.now()

print
print "Current date and time using str method of datetime object:"
print str(now)

print
print "Current date and time using instance attributes:"
print "Current year: %d" % now.year
print "Current month: %d" % now.month
print "Current day: %d" % now.day
print "Current hour: %d" % now.hour
print "Current minute: %d" % now.minute
print "Current second: %d" % now.second
print "Current microsecond: %d" % now.microsecond

print
print "Current date and time using strftime:"
print now.strftime("%Y-%m-%d %H:%M")

print
print "Current date and time using isoformat:"
print now.isoformat()

Results:
Current date and time using str method of datetime object:
2014-09-26 16:34:40.278298

Current date and time using instance attributes:
Current year: 2014
Current month: 9
Current day: 26
Current hour: 16
Current minute: 34
Current second: 40
Current microsecond: 278298

Current date and time using strftime:
2014-09-26 16:34

Current date and time using isoformat:
2014-09-26T16:34:40.278298

Directly from the time module documentation, here are more options to use with strftime:

DirectiveMeaningNotes
%aLocale's abbreviated weekday name.
%ALocale's full weekday name.
%bLocale's abbreviated month name.
%BLocale's full month name.
%cLocale's appropriate date and time representation.
%dDay of the month as a decimal number [01,31].
%HHour (24-hour clock) as a decimal number [00,23].
%IHour (12-hour clock) as a decimal number [01,12].
%jDay of the year as a decimal number [001,366].
%mMonth as a decimal number [01,12].
%MMinute as a decimal number [00,59].
%pLocale's equivalent of either AM or PM.(1)
%SSecond as a decimal number [00,61].(2)
%UWeek number of the year (Sunday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Sunday are considered to be in week 0.(3)
%wWeekday as a decimal number [0(Sunday),6].
%WWeek number of the year (Monday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Monday are considered to be in week 0.(3)
%xLocale's appropriate date representation.
%XLocale's appropriate time representation.
%yYear without century as a decimal number [00,99].
%YYear with century as a decimal number.
%ZTime zone name (no characters if no time zone exists).
%%A literal "%" character.


See also:

Django Blog Project #5: YUI CSS and serving static media

I wrote about adding some CSS to my new blog in my post #3. However, I got the CSS layout code from a book that was a couple of years old (CSS Cookbook, 2006). I created a 2 column fixed width layout, but it was not centered. It turns out creating a robust, cross-browser solution to centering a page layout is not trivial, even in 2008.

I happened to notice that the Django Book used YUI style sheets for styling its online book. The Yahoo User Interface Library (YUI) consists mostly of a Javascript Library, but it also includes "several core CSS resources". Doing a search among Django blogs using django blog search revealed that YUI was a pretty popular choice for CSS among Django users. Watching the 42 minute YUI CSS introductory video gave me a better understanding of the current state of CSS and browser compatability and how to use YUI to easily create consistent layouts across a variety of browsers.

So in this post I'll touch on how I used the YUI CSS library to restyle my slowly-getting-to-be-a-fledgling site. I'll also note how I setup Django and Webfaction to serve the static CSS media files. (What, I need a special setup to use external CSS stylesheets? you ask. I know, it suprised me too.) Finally, I made some small updates to my model to use more reasonable model attribute field types.


Setup YUI-based template

In my base.html template file, I removed all the CSS in between the <style></style> tags and replaced it with a link to the YUI concatenated, minified reset+fonts+grids aggregate file. I also added a link to my to-be-described-later local CSS stylesheet, mystyle.css. In the body section, I added a number of div blocks to create the required structure for the YUI library. The following code creates a fixed 750 pixel total width layout with a 180 pixel side column on the right and a full width header and footer. Not coming from a web background, I had thought this type of thing would be easier. I guess this complexity contributes to the demand for tools such as Dreamweaver.

~/src/django/myblogsite/templates/base.html:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" 
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.2/build/reset-fonts-grids/reset-fonts-grids.css">
  <link rel="stylesheet" type="text/css" href="/site_media/css/mystyle.css">
  <title>{% block title %}Sofeng's Blog Version {{ VERSION }}{% endblock %}</title>
</head>

<body>
  <div id="doc" class="yui-t4">
    <div id="hd">
      <h1>Sofeng's Blog Version {{ VERSION }}</h1>
    </div>
    <div id="bd">
      <div id="yui-main">
        <div class="yui-b">
          {% block main %}{% endblock %}
        </div>
      </div>
      <div class="yui-b" id="sidebar">
        {% block sidebar %}
        <h3>ABOUT</h3>
        <p>This is my new blog created using <a href="http://www.djangoproject.com">Django</a>,
          a <a href="http://www.python.org">Python</a> web framework. This site is under 
          construction. My current blog is located at: 
          <a href="http://iwiwdsmi.blogspot.com">http://iwiwdsmi.blogspot.com</a>.
        </p>
        {% endblock %}
      </div>
    </div>
    <div id="ft">
      A <a href="http://www.djangoproject.com/">Django</a> site.
    </div>
  </div>
</body>
</html>

Create local mystyle.css stylesheet

mystyle.css contains the CSS code to customize the style (fonts, colors, spacing, etc.) of my site. Currently it is just a copy of YUI's Base library with a few minor modifications. (YUI's Reset library removes all browser specific styling (located in browser.css or similar). Then the YUI Base library adds back the common element styles that we are familiar with. This provides consistency across browsers.) I saved this file at ~/src/django/myblogsite/media/css/mystyle.css.


Serve static CSS media files (development server)

Django doesn't serve static media files such as CSS, JavaScript, and images by default. The reason is that Django is meant to create dynamically generated webpages and serving static media is best left to a tool designed for this task such as Apache. However, during development, it is convenient to have the Django development server (python manage.py runserver) serve the static media files. To do this, I used these instructions from Django. This amounted to adding a new line to my URLConf (urls.py).

~/src/django/myblogsite/urls.py:
from django.conf.urls.defaults import *
from myblogsite.myblogapp.views import *

urlpatterns = patterns(
    '',
    (r'^admin/', include('django.contrib.admin.urls')),
    (r'^myview1/$', myview1),
    (r'^blog/$', frontpage),
    (r'^site_media/(?P.*)$', 'django.views.static.serve', {'document_root': '/home/sofeng/src/django/myblogsite/media'}),
)

Serve static CSS media files (Webfaction server)

To serve my static CSS file at Webfaction, I followed these Webfaction knowledgebase instructions:

  1. I went to the Webfaction Control Panel
  2. Under the ">Domains/websites" menu, I selected "Applications"
  3. I selected the icon with the plus to add a new App.
  4. I filled in the following fields:
    "Name": myblogsite_media "App type": Symbolic link "Extra info": /home/sofeng/webapps/django/myblogsite/media
  5. Under the ">Domains/websites" menu, I selected "Websites"
  6. I clicked on the pencil icon to edit my website
  7. Under "Site apps", I selected the icon with the plus to add a new site app.
  8. I selected "myblogsite_media" as the App and entered /site_media as the "URL path".

In my settings_webfaction.py file I updated the MEDIA_ROOT and MEDIA_URL variables. (Did I forget to mention I created a separate settings file for Webfaction. I point to the settings_webfaction.py file in my ~/webapps/django/apache2/conf/httpd.conf.) I also created a new file, urls_webfaction.py that doesn't have the above modification and I point to this URLConf in my settings_webfaction.py. I know there has got to be a better way to do this, but my tired brain hasn't figured it out yet.

In ~/src/django/myblogsite/settings_webfaction.py, I modified the following lines:
MEDIA_ROOT = '/home/sofeng/webapps/django/myblogsite/media/'
MEDIA_URL = '/site_media/'
ROOT_URLCONF = 'myblogsite.urls_webfaction'

Upload project and deploy
On my local machine:
$ pushwebf
On Webfaction machine:
$ hg update -C
$ ~/webapps/django/apache2/bin/restart

Here is a snapshot screenshot of the site:
The live site can be viewed at: http://saltycrane.com/blog/

Related posts:
  Django Blog Project #1: Creating a basic blog
  Django Blog Project #2: Deploying at Webfaction
  Django Blog Project #3: Using CSS and Template Inheritance
  Django Blog Project #4: Adding post metadata
  Django Blog Project #6: Creating standard blog views

Django Blog Project #4: Adding post metadata

I've created a basic blog using SQLite as a backend, deployed it at a shared hosting provider, Webfaction, and added a little CSS for style. Now what? Well, while I say I've created a basic blog, I should say it's a not-quite-yet-basic blog. All it has is some text, which I call Posts, separated by <hr> tags. The next step is to add some post metadata, such as a title, date of creation and tags.


Modify the Post model

To create this new metadata, I first modified the Post model in ~/src/django/myblogsite/myblogapp/models.py:

from django.db import models

class Post(models.Model):
    title = models.CharField(maxlength=500)
    date_created = models.DateField()
    tags = models.CharField(maxlength=200)
    body = models.TextField()

    def __str__(self):
        return self.title

    class Meta:
        ordering = ["-id"]

    class Admin:
        pass

I added new attributes: title, date_created, and tags. The __str__ method is used to identify a Post instance by its title. This is useful when working in the Administration page. The class Meta: is used to reverse sort the Posts by "id".

Correction 7/6/2008: For the Post's body field, I previously used the line: body = models.CharField(maxlength=999999). However, thanks to Myles's comment below, I've changed this to use the more appropriate TextField.


python manage.py syncdb

Whoops, running python manage.py syncdb doesn't actually sync the model with the database. It is only used to initially create the database tables. To add the new columns to my table, I would have to enter the SQL commands directly. Though it is not super difficult, and would be good experience since all those hiring managers seem to want SQL experience, I took the lazy route and just replaced my database. (Not like I had much in there anyways.) For your reference, the section Making Changes to a Database Schema in Chapter 5 of The Django Book describes how to solve my problem without replacing my database.

$ cd ~/src/django/myblogsite
$ rm mydatabase.sqlite3 
$ python manage.py syncdb 
Creating table auth_message
Creating table auth_group
Creating table auth_user
Creating table auth_permission
Creating table django_content_type
Creating table django_session
Creating table django_site
Creating table django_admin_log
Creating table myblogapp_post

You just installed Django's auth system, which means you don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (Leave blank to use sofeng'): sofeng
E-mail address: [email protected]
Password: 
Password (again): 
Superuser created successfully.
Installing index for auth.Message model
Installing index for auth.Permission model
Installing index for admin.LogEntry model
Loading 'initial_data' fixtures...
No fixtures found.

Oh yeah, I forgot gotta recreate my superuser.


Modify views.py slightly

In versions 0.0.1 and 0.0.2 of my new blog, I passed a list of post bodies to my template. But actually, I could have just passed a list of the Post objects and accessed the body attribute of each post in the template. This saves a line of unecessary code, and provides accessiblity to all the attributes of the Post object. (I also factored out my blog version number from the base.html template.)

~/src/django/myblogsite/myblogapp/views.py:
from django.shortcuts import render_to_response
from myblogsite.myblogapp.models import Post

def frontpage(request):
    posts = Post.objects.all()
    return render_to_response('frontpage.html', 
                              {'post_list': posts,
                               'VERSION': '0.0.3'})
Correction 7/6/2008: I previously had from myblogapp.models import Post on the second line. This works, but is inconsistent with my urls.py below and can (and did for me) cause subtle errors in the future. I corrected the line to read: from myblogsite.myblogapp.models import Post.

Modify my frontpage.html template

This is pretty self-explanatory. I access the the Post object's attributes using the "." (dot).

~/src/django/myblogsite/templates/frontpage.html:
{% extends "base.html" %}

{% block main %}
  {% for post in post_list %}
    <h2>{{ post.title }}</h2>
    {{ post.body }}
    <hr>
    <div class="post_footer">Created: {{ post.date_created }} | Tags: {{ post.tags }}</div>
  {% endfor %}
{% endblock %}

A little CSS

In between the <style> tags in ~/src/django/myblogsite/templates/base.html:

.post_footer {
  font-family: Verdana, Arial, sans-serif;
  font-size:70%;
}
hr {
  border: 0;
  color: gray;
  background-color: gray;
  height: 1px;
}

Start development server and add a couple posts

I started the development server and added a couple of posts in the Admin site:

  • $ cd ~/src/django/myblogsite
    $ python manage.py runserver
  • I visited http://127.0.0.1:8000/admin, logged in, and added a couple posts.

Upload to Webfaction server

pushwebf is my alias for hg push --remotecmd /home/sofeng/bin/hg ssh://[email protected]/webapps/django/myblogsite

$ pushwebf
[email protected]'s password:
pushing to ssh://[email protected]/webapps/django/myblogsite
searching for changes
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 7 changesets with 20 changes to 10 files

Deploy
Logged into Webfaction:
$ cd ~/webapps/django/myblogsite
$ hg update -C
9 files updated, 0 files merged, 4 files removed, 0 files unresolved
$ ~/webapps/django/apache2/bin/restart

Here is a snapshot screenshot of version 0.0.3

The live site can be viewed at http://saltycrane.com/blog/



Related posts:
  Django Blog Project #1: Creating a basic blog
  Django Blog Project #2: Deploying at Webfaction
  Django Blog Project #3: Using CSS and Template Inheritance
  Django Blog Project #5: YUI CSS and serving static media