SaltyCrane Blog — Notes on JavaScript and web development

How to set up Django with MySql on Ubuntu Hardy

Here are my notes on installing Django with MySql. Almost all of this was taken from Zeth's article: Baby Steps with Django - part 2 database setup.

Install Django and MySql

Note: during the installation of mysql-server, you will be prompted for a root password. Use this in the section below.

$ sudo apt-get install python-django
$ sudo apt-get install mysql-server
$ sudo apt-get install python-mysqldb
Set up a MySql database and user

Note, use the password you entered when installing MySql

$ mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.0.51a-3ubuntu5.1 (Ubuntu)

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> CREATE DATABASE django_db;
Query OK, 1 row affected (0.01 sec)

mysql> GRANT ALL ON django_db.* TO 'djangouser'@'localhost' IDENTIFIED BY 'mypassword';
Query OK, 0 rows affected (0.03 sec)

mysql> quit
Bye
Create a Django Project
$ django-admin startproject mysite
Edit the Django database settings
Edit mysite/settings.py:
DATABASE_ENGINE = 'mysql'           # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'.
DATABASE_NAME = 'django_db'             # Or path to database file if using sqlite3.
DATABASE_USER = 'djangouser'             # Not used with sqlite3.
DATABASE_PASSWORD = 'mypassword'         # Not used with sqlite3.
DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
Use Django to create the database tables
$ cd mysite
$ 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

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'):    
E-mail address: [email protected]
Password: 
Password (again): 
Superuser created successfully.
Installing index for auth.Message model
Installing index for auth.Permission model
Loading 'initial_data' fixtures...
No fixtures found.
Run the development server
$ python manage.py runserver
Validating models...
0 errors found.

Django version 0.96.1, using settings 'mysite.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
[30/Jul/2008 16:37:23] "GET / HTTP/1.1" 404 2053

Point your browser at http://127.0.0.1:8000 and you should see the Django It worked! page.

Install wmii snapshot

These are my notes for installing wmii snapshot in my home directory on Ubuntu 8.04 Hardy.

  • Download the latest snapshot (wmii+ixp-20080520) from the wmii homepage and save it to ~/incoming.
  • Untar the snapshot.
    $ cd ~/incoming
    $ tar -zxvf wmii+ixp-20080520.tgz
  • Install prerequisites:
    $ sudo apt-get install build-essential xorg-dev
    $ sudo apt-get install dwm-tools
  • Build: (I left the defaults for everything during make config except for the path. I changed this to a temporary directory, ~/tmp/wmii.)
    $ cd ~/incoming/wmii+ixp-20080520
    $ make config
    $ make 
    $ make install
  • Move bin, etc, lib, and share to a new wmii directory.
    $ mkdir ~/lib/wmii
    $ mv ~/tmp/wmii/* ~/lib/wmii
  • Create links in my ~/bin directory:
    $ cd ~/bin
    $ ln -s ../lib/wmii/bin/wihack wihack
    $ ln -s ../lib/wmii/bin/wmii9rc wmii9rc
    $ ln -s ../lib/wmii/bin/wmii9menu wmii9menu
    $ ln -s ../lib/wmii/bin/wmii.rc wmii.rc
    $ ln -s ../lib/wmii/bin/wmii.sh wmii.sh
  • Create a .xinitrc script:
    xmodmap ~/.Xmodmap
    gnome-screensaver&
    urxvt&
    
    until wmii; do
        true
    done
  • And link ~/.xsession to it:
    $ ln -s ~/.xinitrc ~/.xsession
  • Log out of the current window manager and then select X client script as the session and log in.

New PC setup notes

Here is how I set up my new work PC with Xubuntu, wmii, and conkeror using the wubi windows installer and my home mercurial repository of dot files. Notes: ti is a python script I use to interface with the remote machine that holds my mercurial repositories. I also have another python script which concatenates my configuration files in ~/etc with my local machine-specific configuration files in ~/local/localmachine/etc and puts them in my home directory. crayola is my home machine. cotton is my new work machine. (Actually I haven't changed the name yet.)

  • I used the wubi installer to install Xubuntu as part of the Windows filesystem.
  • I restarted the computer and selected to boot into Xubuntu. (I actually had to restart twice because the first time it was configuring stuff.)
  • I ran the Update Manager.
  • I installed Mercurial:
    $ sudo apt-get install mercurial
  • I cloned my home directory repository and set it up:
    $ cd /home
    $ sudo mv sofeng sofeng-old
    $ sudo hg clone ssh://[email protected]//home/sofeng
    $ sudo mkdir -p ~/local/cotton/etc
    $ sudo mkdir -p ~/local/cotton/bin
    $ sudo chown -R sofeng:sofeng /home/sofeng
    $ cp ~/local/crayola/etc/.* ~/local/cotton/etc
    $ . ~/.bashrc
  • I installed some stuff:
    $ sudo apt-get install wmii
    $ sudo apt-get install rxvt-unicode
    $ sudo apt-get install emacs-snapshot
    $ sudo apt-get install lastfm
    $ sudo apt-get install conky
  • I logged out of Xfce and logged into wmii. Mod4+Enter to get a urxvt terminal.
  • I cloned my conkeror repository:
    $ cd ~/lib
    $ ti clone conk

Emacs ruby-mode

To install ruby-mode for emacs, I followed the directions at the digital sanitation engineering blog. I pretty much did exactly the same thing-- I don't know why I wrote my own post-- I suppose it is good just for my records.

sofeng@crayola:~/etc
$ export SITE_LISP=~/etc/.emacs.d/site-lisp
sofeng@crayola:~/etc
$ svn export http://svn.ruby-lang.org/repos/ruby/trunk/misc $SITE_LISP/ruby
A    /home/sofeng/etc/.emacs.d/site-lisp/ruby
A    /home/sofeng/etc/.emacs.d/site-lisp/ruby/rubydb2x.el
A    /home/sofeng/etc/.emacs.d/site-lisp/ruby/rubydb3x.el
A    /home/sofeng/etc/.emacs.d/site-lisp/ruby/ruby-mode.el
A    /home/sofeng/etc/.emacs.d/site-lisp/ruby/ruby-electric.el
A    /home/sofeng/etc/.emacs.d/site-lisp/ruby/inf-ruby.el
A    /home/sofeng/etc/.emacs.d/site-lisp/ruby/README
A    /home/sofeng/etc/.emacs.d/site-lisp/ruby/ruby-style.el
Exported revision 18207.
sofeng@crayola:~/etc
$ emacs -batch -f batch-byte-complile $SITE_LISP/ruby
Loading 00debian-vars...
No /etc/mailname. Reverting to default...
Loading /etc/emacs/site-start.d/50dictionaries-common.el (source)...
Skipping dictionaries-common setup for emacs-snapshot
Loading /etc/emacs/site-start.d/50psvn.el (source)...
Symbol's function definition is void: batch-byte-complile
sofeng@crayola:~/etc
$ ll ~/etc/.emacs.d/site-lisp/ruby
total 92
-rw-r--r-- 1 sofeng sofeng   369 2008 04/10 04:36 README
-rw-r--r-- 1 sofeng sofeng 14393 2008 01/06 07:49 inf-ruby.el
-rw-r--r-- 1 sofeng sofeng  6747 2007 02/12 15:01 ruby-electric.el
-rw-r--r-- 1 sofeng sofeng 44473 2008 07/21 17:51 ruby-mode.el
-rw-r--r-- 1 sofeng sofeng  1798 2008 04/22 06:15 ruby-style.el
-rw-r--r-- 1 sofeng sofeng  4481 2007 02/12 15:01 rubydb2x.el
-rw-r--r-- 1 sofeng sofeng  4613 2007 02/12 15:01 rubydb3x.el

Then added the following to my .emacs:

(add-to-list 'load-path "~/.emacs.d/site-lisp/ruby")
(autoload 'ruby-mode "ruby-mode"
    "Mode for editing ruby source files")
(add-to-list 'auto-mode-alist '("\\.rb$" . ruby-mode))
(add-to-list 'interpreter-mode-alist '("ruby" . ruby-mode))
(autoload 'run-ruby "inf-ruby"
    "Run an inferior Ruby process")
(autoload 'inf-ruby-keys "inf-ruby"
    "Set local key defs for inf-ruby in ruby-mode")
(add-hook 'ruby-mode-hook
    '(lambda ()
        (inf-ruby-keys)))
;; If you have Emacs 19.2x or older, use rubydb2x                              
(autoload 'rubydb "rubydb3x" "Ruby debugger" t)
;; uncomment the next line if you want syntax highlighting                     
(add-hook 'ruby-mode-hook 'turn-on-font-lock)

How to search C code for division or sqrt

The following Python script searches through C code for division or sqrt and prints the line of code and the line number. It skips C comments. To use, run python find_divides.py filename.c

#!/usr/bin/python

"""find_divides.py

usage: python find_divides.py filename
"""

import re
import sys

def main():
    filename = sys.argv[1]
    text = open(filename).read()
    lines = text.splitlines()
    lines = ["%4d: %s" % (i, line) for (i, line) in enumerate(lines)]
    text = "\n".join(lines)
    text = remove_comments_and_strings(text)

    for line in text.splitlines():
        if ("/" in line) or ("sqrt" in line):
            print line

def remove_comments_and_strings(text):
    """ remove c-style comments and strings
        text: blob of text with comments (can include newlines)
        returns: text with comments and strings removed
    """
    pattern = r"""
                            ##  --------- COMMENT ---------
           /\*              ##  Start of /* ... */ comment
           [^*]*\*+         ##  Non-* followed by 1-or-more *'s
           (                ##
             [^/*][^*]*\*+  ##
           )*               ##  0-or-more things which don't start with /
                            ##    but do end with '*'
           /                ##  End of /* ... */ comment
         |                  ##  -OR-  various things which aren't comments:
           (                ## 
                            ##  ------ " ... " STRING ------
             "              ##  Start of " ... " string
             (              ##
               \\.          ##  Escaped char
             |              ##  -OR-
               [^"\\]       ##  Non "\ characters
             )*             ##
             "              ##  End of " ... " string
           |                ##  -OR-
                            ##
                            ##  ------ ' ... ' STRING ------
             '              ##  Start of ' ... ' string
             (              ##
               \\.          ##  Escaped char
             |              ##  -OR-
               [^'\\]       ##  Non '\ characters
             )*             ##
             '              ##  End of ' ... ' string
           |                ##  -OR-
                            ##
                            ##  ------ ANYTHING ELSE -------
             (.              ##  Anything other char
             [^/"'\\]*)      ##  Chars which doesn't start a comment, string
           )                ##    or escape
    """
    regex = re.compile(pattern, re.VERBOSE|re.MULTILINE|re.DOTALL)
    goodstuff = [m.group(5) for m in regex.finditer(text) if m.group(5)]
    return "".join(goodstuff)

if __name__ == "__main__":
    main()

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