SaltyCrane Blog — Notes on JavaScript and web development

Notes on users and groups on Linux

Here are some notes on some basic tasks dealing with users, groups, and permissions on Ubuntu Linux. All these commands (except passwd) are done as root. If you are not root, prepend sudo to all the commands.

  • Add a user, sofeng
    # adduser sofeng
  • Change your password:
    $ passwd
  • Create a developer group
    # addgroup developer
  • Add user sofeng to the developer group
    # adduser sofeng developer
  • Give developer group sudo power:
    Add the following line to /etc/sudoers:
    %developer ALL=(ALL) ALL
  • Change owner to sofeng, and group to developer, of directory, mydir, and all its subdirectories:
    # chown -R sofeng:developer mydir
  • Change permisions of directory, mydir, and all its subdirectories, to be writable by the group, developer:
    # chmod -R g+w mydir

Python recursion example to navigate tree data

Here is a simple Python example using recursion to navigate a nested Python data structure. Each node in the data structure contains 0 or more children. In this simple example, I look at each node and print the "text" indented according to the nesting level within the data structure.

Update 2008-09-15: Nihiliad posted an improvement to my example in the comments. It is much simpler. I have updated my example below.

Nihiliad's (improved) method
data = {'count': 2,
        'text': '1',
        'kids': [{'count': 3,
                  'text': '1.1',
                  'kids': [{'count': 1,
                            'text': '1.1.1',
                            'kids': [{'count':0,
                                      'text': '1.1.1.1',
                                      'kids': []}]},
                           {'count': 0,
                            'text': '1.1.2',
                            'kids': []},
                           {'count': 0,
                            'text': '1.1.3',
                            'kids': []}]},
                 {'count': 0,
                  'text': '1.2',
                  'kids': []}]}

def traverse(data):
    print ' ' * traverse.level + data['text']
    for kid in data['kids']:
        traverse.level += 1
        traverse(kid)
        traverse.level -= 1

if __name__ == '__main__':
    traverse.level = 1
    traverse(data)

Results:

 1
  1.1
   1.1.1
    1.1.1.1
   1.1.2
   1.1.3
  1.2
My original (inferior) method
def outer(data):
    class Namespace: pass
    ns = Namespace()
    ns.level = 1
    
    def inner(data):
        print ' ' * ns.level + data['text']
        if data['count'] > 0:
            ns.level += 1
            for kid in data['kids']:
                inner(kid)
            ns.level -= 1

    inner(data)

if __name__ == '__main__':
    outer(data)

On using Python, the Digg API, and simplejson

Here are some quick notes on using the Digg API with a Python script. Note, there is a Python toolkit for Digg but I just used urllib2 and the Digg API endpoints for the sake of simplicity.

I wanted the output in JSON format so I specified the response type as JSON. To decode JSON directly to a Python data structure, I used simplejson.

Here is a simple example which returns the JSON output for the Digg story Dell vs. Apple: This Time it's Personal which has a "clean title" of Dell_vs_Apple_This_Time_it_s_Personal.

#!/usr/bin/env python

import urllib2

APPKEY = 'http%3A%2F%2Fwww.example.com'
story_clean_title = 'Dell_vs_Apple_This_Time_it_s_Personal'
url = ''.join([
        'http://services.digg.com',
        '/story/%s' % story_clean_title,
        '?appkey=%s' % APPKEY,
        '&type;=json',
        ])
json = urllib2.urlopen(url).read()
print json

Results:

{"timestamp":1219168025,"total":"1","offset":0,"stories":[{"id":"8038250","link":"http:\/\/www.businessweek.com\/magazine\/content\/08_34\/b4097022701166.htm?campaign_id=rss_daily","submit_date":1219047878,"diggs":763,"comments":198,"title":"Dell vs. Apple: This Time it's Personal","description":"Now Bucher is again squaring off against his former company. He's spearheading an ambitious plan at Dell (DELL) to break Apple's dominant hold on the digital entertainment market.","promote_date":1219095692,"status":"popular","media":"news","user":{"name":"msaleem","icon":"http:\/\/digg.com\/users\/msaleem\/l.png","registered":1126518985,"profileviews":136052,"fullname":"Muhammad Saleem"},"topic":{"name":"Apple","short_name":"apple"},"container":{"name":"Technology","short_name":"technology"},"thumbnail":{"originalwidth":370,"originalheight":245,"contentType":"image\/jpeg","src":"http:\/\/digg.com\/apple\/Dell_vs_Apple_This_Time_it_s_Personal\/t.jpg","width":80,"height":80},"href":"http:\/\/digg.com\/apple\/Dell_vs_Apple_This_Time_it_s_Personal"}],"count":1}

Here is a slightly less simple example which returns the comments for the same story above. It uses simplejson to decode the Digg story JSON data and get the story ID which is then used to get the comment data.

#!/usr/bin/env python

import simplejson
import urllib2
from pprint import pprint

APPKEY = 'http%3A%2F%2Fwww.example.com'

def main():
    story_clean_title = 'Dell_vs_Apple_This_Time_it_s_Personal'

    # get story
    json = get_json('/story/%s' % story_clean_title)
    pydata = simplejson.loads(json)
    story_id = pydata['stories'][0]['id']

    # get comments
    json = get_json('/story/%s/comments' % story_id)
    pydata = simplejson.loads(json)
    pprint(pydata)

def get_json(endpoint):
    """ returns json data for requested digg endpoint 
    """
    url = ''.join([
            'http://services.digg.com',
            endpoint,
            '?appkey=%s' % APPKEY,
            '&type;=json',
            ])
    return urllib2.urlopen(url).read()

if __name__ == '__main__':
    main()

Results:

{u'comments': [{u'content': u"For those who are having trouble understanding what this is about.  This is not about creating a competing closed platform.  This is about creating a standard platform for selling music online.  Imagine iTunes but with the ability to add other music stores into it.  So when you want to buy a song, you can browse this music store or that music store.  And all will work with your mp3 player.  Whether it is a basic one that mounts as an external usb drive, or one that is able to sync the songs up the way iTunes does it with the iPod.  And we know it is gonna be good because the guy that is driving it worked at Apple, so he knows what quality is.  I'm sure it will have the ability to import or export your music library to iTunes if you so choose.  But the point is that it is trying to create a standard that any device maker can follow.  And hopefully, it will have no drm.  Otherwise it's as useless as iTunes with its drm.\n\nRemember the same thing happened when windows 3.1 came along and immediately sold as many as 10x the number of pc's compared to mac's at the time.  Anytime there is an open standard, no matter how good the closed standard is, the open one wins because that means lower prices for consumers since any manufacturer can use it.  Apple thrives on closed standards when it comes to selling their products because it enables them to lock their customers in.  And before they know it, they're locked into it and can't get out of it without great expense.\n\nI know that the Apple fanboi's are gonna bury me for this, but I'll say it anyways.  It was nice being on top.  But you can't be on top forever.",
                u'date': 1219163527,
                u'down': 0,
                u'id': 17963749,
                u'level': 0,
                u'replies': 0,
                u'replyto': None,
                u'root': 17963749,
                u'story': 8038250,
                u'up': 1,
                u'user': u'pyrates'},
               {u'content': u'@thinkdifferent: I should of specified its a $1 cheaper when you buy the full album. Single tracks are the same price but albums are usually about $8.99. ',
                u'date': 1219159148,
                u'down': 0,
                u'id': 17961525,
                u'level': 0,
                u'replies': 0,
                u'replyto': None,
                u'root': 17961525,
                u'story': 8038250,
                u'up': 1,
                u'user': u'mrgermy'},
               {u'content': u'Zune XPS w/ Vista Ultimate combo deal.',
                u'date': 1219146354,
                u'down': 0,
                u'id': 17956943,
                u'level': 0,
                u'replies': 0,
                u'replyto': None,
                u'root': 17956943,
                u'story': 8038250,
                u'up': 1,
                u'user': u'hurdboy'},
               {u'content': u'Digg: "Dell vs. Apple: This Time it\'s Personal"\nBusiness Week: "Bucher says his quest to challenge Apple is all business and not personal."\n\nBurried as inacurate.',
                u'date': 1219134278,
                u'down': 0,
                u'id': 17954900,
                u'level': 0,
                u'replies': 0,
                u'replyto': None,
                u'root': 17954900,
                u'story': 8038250,
                u'up': 1,
                u'user': u'KAMiKAZOW'},
               {u'content': u'Dell is junk and Apple is overpriced',
                u'date': 1219117388,
                u'down': 0,
                u'id': 17950148,
                u'level': 0,
                u'replies': 0,
                u'replyto': None,
                u'root': 17950148,
                u'story': 8038250,
                u'up': 1,
                u'user': u'DeuceDiggalow'},
               {u'content': u'Dell, all you have to do to kick apple in the jewels is get your act together with Ubuntu.\n\nAmarok will do the rest.',
                u'date': 1219112434,
                u'down': 2,
                u'id': 17948056,
                u'level': 0,
                u'replies': 4,
                u'replyto': None,
                u'root': 17948056,
                u'story': 8038250,
                u'up': 3,
                u'user': u'ethana2'},
               {u'content': u'Apple = Overpriced, under-featured, but pretty\nDell = priced right, full-featured, and awesome looking',
                u'date': 1219105961,
                u'down': 8,
                u'id': 17945330,
                u'level': 0,
                u'replies': 2,
                u'replyto': None,
                u'root': 17945330,
                u'story': 8038250,
                u'up': 3,
                u'user': u'freesf'},
               {u'content': u'Go Dell! Best of luck to you :)',
                u'date': 1219104145,
                u'down': 0,
                u'id': 17944493,
                u'level': 0,
                u'replies': 0,
                u'replyto': None,
                u'root': 17944493,
                u'story': 8038250,
                u'up': 0,
                u'user': u'Sabre24q7'},
               {u'content': u'Actually the Dell DJ wasnt that bad for its time. Obviously now its looks very dated and far from the best now, but at its time it was a decent player and had pretty good sound quality. It also had more features than the ipod does even today.\n\n Creative was the people who made it and it was a very basic player but it did what it was suppose to and had a lot of room on it for a cheap price. ',
                u'date': 1219104085,
                u'down': 2,
                u'id': 17944474,
                u'level': 0,
                u'replies': 1,
                u'replyto': None,
                u'root': 17944474,
                u'story': 8038250,
                u'up': 1,
                u'user': u'jsc315'},
               {u'content': u"I'm not saying this will or won't succeed. I'm just pointing out something people seem to be missing.\n\nJust because Dell customers aren't pompous and loudmouthed doesn't mean they aren't loyal and happy customers. There are a LOT of Dell users that like the company and what the products they make.",
                u'date': 1219102724,
                u'down': 1,
                u'id': 17943861,
                u'level': 0,
                u'replies': 1,
                u'replyto': None,
                u'root': 17943861,
                u'story': 8038250,
                u'up': 2,
                u'user': u'Urkel'}],
 u'count': 10,
 u'offset': 0,
 u'timestamp': 1219168299,
 u'total': u'55'}

http://www.saltycrane.com is my new OpenID

Open ID is a new technology that allows you to use one set of login credentials to access many sites. This is good because you don't have to remember yet another password or go through yet another Web 2.0 community registration process.

Simon Willison wrote a simple, clear explanation on how to turn your blog in to an OpenID. I followed his instructions, and now http://www.saltycrane.com/ is my new Open ID! I look forward to putting it to good use. (In case you're wondering, yes, I am planning to add Open ID support to this site. Mr. Willison has also written a django-openid library to "enable your Django application to act as an OpenID consumer".)

Somewhere on your Python path

As I install new python packages, I sometimes see instructions which say something like "check out the code, and place it somewhere on your Python path". These are very simple instructions, but since it is not automatic like a Windows installer, or Ubuntu's package management system, it causes me to pause. Where on my Python path should I put it? I could put all my packages in random places and update my PYTHONPATH environment variable every time. I also thought about putting new packages in Python's site-packages directory. This is probably a good option. However, I tend to like to have all my important stuff in my home directory so I can easier maintain it across multiple machines. (Also, I forget where the site-packages lives (it is /usr/lib/python2.5/site-packages on Ubuntu Hardy).) So my solution was to create my own python-packages directory in ~/lib. I set the PYTHONPATH in my ~/.bashrc as follows:

export PYTHONPATH=$HOME/lib/python-packages
and then put all my Python packages here.



Update: Some Python packages are distributed with other stuff besides the actual Python package. To handle these cases, I created a dist directory inside my python-packages directory, and created symbolic links from the actual package directory in dist to python-packages. Finally, I made the entire python-packages directory a Mercurial repository so I can finely control my Python environment and easily maintain it across multiple machines. Here's what my ~/lib/python-packages currently looks like:

drwxr-xr-x 9 sofeng sofeng 4096 2008 08/05 21:40 dist
drwxr-xr-x 4 sofeng sofeng 4096 2008 08/05 21:31 django_openidconsumer
lrwxrwxrwx 1 sofeng sofeng   43 2008 08/05 21:31 elementtree -> dist/elementtree-1.2.6-20050316/elementtree
lrwxrwxrwx 1 sofeng sofeng   31 2008 08/05 21:31 openid -> dist/python-openid-1.2.0/openid
lrwxrwxrwx 1 sofeng sofeng   31 2008 08/05 21:31 openid2.2 -> dist/python-openid-2.2.1/openid
lrwxrwxrwx 1 sofeng sofeng   27 2008 08/05 21:41 pygments -> dist/Pygments-0.10/pygments
lrwxrwxrwx 1 sofeng sofeng   29 2008 08/05 21:31 urljr -> dist/python-urljr-1.0.1/urljr
lrwxrwxrwx 1 sofeng sofeng   29 2008 08/05 21:31 yadis -> dist/python-yadis-1.1.0/yadis

Update 2008-09-14: Here is a post on the django-developers mailing list by Kevin Teague which explains the large number of technologies related to Python package management and deployment including PyPi, Distutils, Eggs, Easy Install, VirtualEnv, and Buildout. Kevin admits that package management and deployment is an area in Python where there is room for a great deal of improvemnt. He notes that the symlinking method that I use can work for simple needs, but it fails for more complicated use cases, such as tracking package dependencies. The new Virtualenv and Buildout technologies seem to be interesting-- I will have to check them out when I have time. I found this link via Simon Willison

Update 2008-09-24: Ian Bicking, author of Virtualenv, has just released pyinstall which seems to be an improved easy_install. I have not tried it yet, but I believe Ian Bicking writes good code.

Update 2008-10-24:Glyph Lefkowitz, lead architect of Twisted, suggests using twisted.python.modules for solving Python Path Programming Problems. This looks like something I could use in my current project-- I just wish I understood it.

Update 2008-12-16:

Django Blog Project #11: Migrating from Django 0.96 to SVN Trunk

I've been using the Django 0.96 release for this blog, but I've been thinking about switching to the SVN trunk version since it is recommended by the Django community. Django 1.0 alpha was released a couple weeks ago, so now seems like a good time to migrate.

Here are the changes I had to make. There were suprisingly few changes required-- probably because I'm not using a lot of the Django functionality. For a complete list of changes, see the Backwards-incompatible changes documentation.

Note, I am using trunk revision 8210. (2 weeks post Alpha).

Model changes

The admin definitions have been decoupled from the model definitions. Also, the prepopulate_from database field has been moved to the new admin class. See here for more information. Also maxlength was changed to max_length.

~/src/django/myblogsite/myblogapp/models.py:
import re
from django.db import models
from django.contrib.auth.models import User
from django.contrib import admin

class Post(models.Model):
    author = models.ForeignKey(User)
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200,
                            prepopulate_from=['title'],
                            unique_for_month='date_created')
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    tags = models.CharField(max_length=200, help_text="Space separated.")
    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 PostAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ('title',)}

admin.site.register(Post, PostAdmin)
Modify URLConf

See here for more information.

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

admin.autodiscover()

feeds = {
    'latest': LatestPosts,
}

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

    (r'^admin/', include('django.contrib.admin.urls')),
    (r'^admin/(.*)', admin.site.root),
    (r'^feeds/(?P.*)/$', 'django.contrib.syndication.views.feed', 
     {'feed_dict': feeds}),
    (r'^comments/', include('django.contrib.comments.urls.comments')),
    
    (r'^myview1/$', myview1),
    (r'^$', rootview),
    (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),
)
Use the safe template filter

In the Django templates, Django SVN now escapes HTML by default to protect against cross-site scripting. To display my HTML blog posts, I needed to use the safe filter.

Excerpt from ~/src/django/myblogsite/templates/listpage.html:
    {{ post.body|safe|truncatewords_html:"50" }}
Use new admin templates

Finally, I copied over the new Django SVN templates from trunk/django/contrib/admin/templates.

Notes on moving Ubuntu Wubi to a standard ext3 partition using LVPM

Here are my notes for moving my wubi Ubuntu install to a dedicated ext3 partition. I used the Loopmounted Virtual Partition Manager (LVPM) to do the transfer. From the webpage:

The Loopmounted Virtual Partition Manager allows users to upgrade their existing Wubi or Lubi installation to a standard Ubuntu system by transferring all data, settings, and applications from the original install to a dedicated partition. The advantages of upgrading using LVPM are better disk performance and reliability, and the ability to replace the original operating system with Ubuntu.

Add 2 new partitions

My hard disk had only 1 partition containing Windows XP.

  • Boot to Xubuntu 8.04 Hardy Heron Live CD
  • Go to System, Partition Editor
  • Click the existing partition, and "Resize".
  • Click "Apply".
    • Note 1: the first time I tried this, I got ERROR: Extended record needed (1712>1024), not yet supported. Please try to free less space. So I slid my partition divider a little more to the right and tried again and it worked.
    • Note 2: This took a few minutes when it worked.
  • In the unallocated section, create a new partition. I made this my swap partition. It was 1065 MB, Primary Partition, Filesystem: linux-swap. It became sda2. Click "Add".
  • Then I created my main Linux partition. Used the remaining size, filesystem: ext3, Primary Partition. This is sda3. Click "Add".
  • Click "Apply".
Install and run LVPM
  • Boot into the Wubi Ubuntu installation.
  • Go to http://sourceforge.net/project/showfiles.php?group_id=198821, download, save, install, and run it.
  • Select "Transfer" and then the new main linux partition (sda3)
  • Reboot
  • When I selected Ubuntu in the Grub boot loader, I got the following error mesage: Error 17: Cannot mount selected partition.
  • To fix this, I hit "e" to edit the command, and changed root ()/ubuntu/disks to root (hd0,2). Then I hit "b" to boot. hd0 means the first hard disk, and 2 means the 3rd partition.
  • After booting into the new ext3 Ubuntu install, I edited the /boot/grub/menu.lst file to change root ()/ubuntu/disks to root (hd0,2)
  • All pau.

How to use gnip-python to retrieve activity from Twitter, Delicious, Digg, etc.

  • Create an account at http://www.gnipcentral.com/
  • Download gnip-python from github.com.
  • Unpack it:
    $ tar -zxvf gnip-gnip-python-028364a70bd40dda0069ecdd3e7f6fff23bb985e.tar.gz
    
  • Move it to your example directory:
    $ mkdir ~/src/python/gnip-example
    $ mv gnip-gnip-python-028364a70bd40dda0069ecdd3e7f6fff23bb985e/*.py ~/src/python/gnip-example
  • Create an example file called ~/src/python/gnip-example/gnip-example.py:
    #!/usr/bin/env python
    
    from gnip import *
    
    gnip = Gnip("[email protected]", "yourpassword")
    
    for publisher in ["twitter", "digg", "delicious"]:
        activities = gnip.get_publisher_activities(publisher)
        print
        print publisher
        for activity in activities[:5]:
            print activity
    
  • Run it:
    $ python gnip-example.py

    And get the following results:
    twitter
    [derricklo, 2008-08-01T22:49:59+00:00, tweet, http://twitter.com/derricklo/statuses/875165550]
    [sam_metal, 2008-08-01T22:50:01+00:00, tweet, http://twitter.com/sam_metal/statuses/875165564]
    [lalatina, 2008-08-01T22:49:59+00:00, tweet, http://twitter.com/lalatina/statuses/875165544]
    [Nochipra, 2008-08-01T22:50:01+00:00, tweet, http://twitter.com/Nochipra/statuses/875165562]
    [jmcgaha, 2008-08-01T22:50:01+00:00, tweet, http://twitter.com/jmcgaha/statuses/875165556]
    
    digg
    [DAlexopoulos, 2008-08-01T18:50:08+00:00, submission, http://digg.com/health/CLA_Conjugated_Linoleic_Acid_Explained]
    [EradicateIV, 2008-08-01T18:50:05+00:00, submission, http://digg.com/pc_games/Pittco_Iron_Storm_9]
    [vivianpetman, 2008-08-01T18:49:58+00:00, submission, http://digg.com/business_finance/Dub_Me_Now_Turns_Business_Cards_into_Bits_and_Bytes_Small]
    [portia7896, 2008-08-01T18:49:53+00:00, submission, http://digg.com/world_news/Radioactive_water_yum]
    [hhdepot, 2008-08-01T18:49:52+00:00, submission, http://digg.com/2008_us_elections/Why_WE_are_democrats_in_2008_Original_Video]
    
    delicious
    [hanasama1, 2008-08-01T22:49:55+00:00, delicious, http://www.birchmere.com/]
    [Chrmftcotl, 2008-08-01T22:49:54+00:00, delicious, http://www.shaunlow.com/a-definitive-stumbleupon-guide-driving-traffic-to-websites/]
    [shankar, 2008-08-01T22:49:53+00:00, delicious, http://www.guardian.co.uk/books/2008/jul/26/salmanrushdie.bookerprize]
    [grzyweasel, 2008-08-01T22:49:53+00:00, delicious, http://patterntap.com/]
    [metaffect, 2008-08-01T22:50:05+00:00, delicious, http://dean.edwards.name/]
    Pretty cool.

This is just scratching the surface of what you can do with gnip. You can also filter by time or user. Or get XML output. Or you can publish to gnip yourself. See the gnip-python README for more python examples or the Gnip API for more detailed information. Also, here is a list of Gnip publishers.

If you get a ImportError: No module named iso8601 error, install iso8601.

Django Blog Project #10: Adding support for multiple authors

Here is a quick post on how I added support for multiple users on my blog.

Modfiy the model
Excerpt from ~/src/django/myblogsite/myblogapp/models.py:
import re
from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
    author = models.ForeignKey(User)
    title = models.CharField(maxlength=200)
    slug = models.SlugField(maxlength=200,
                            prepopulate_from=['title'],
                            unique_for_month='date_created')
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    tags = models.CharField(maxlength=200, help_text="Space separated.")
    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
Update the database
  • List the SQL commands Django would use the create the database tables:
    $ cd ~/src/django/myblogsite/
    $ python manage.py sqlall myblogapp
    BEGIN;
    CREATE TABLE "myblogapp_post" (
        "id" integer NOT NULL PRIMARY KEY,
        "author_id" integer NOT NULL REFERENCES "auth_user" ("id"),
        "title" varchar(200) NOT NULL,
        "slug" varchar(200) NOT NULL,
        "date_created" datetime NOT NULL,
        "date_modified" datetime NOT NULL,
        "tags" varchar(200) NOT NULL,
        "body" text NOT NULL,
        "body_html" text NOT NULL,
        "lc_count" integer NOT NULL
    );  
    CREATE INDEX myblogapp_post_author_id ON "myblogapp_post" ("author_id");
    CREATE INDEX myblogapp_post_slug ON "myblogapp_post" ("slug");
    COMMIT;
  • Enter the sqlite shell:
    $ sqlite3 mydatabase.sqlite3

    and enter the following statement:
    sqlite> ALTER TABLE myblogapp_post ADD COLUMN author_id integer REFERENCES auth_user (id);
    sqlite> .exit
Update the template
Excerpt from ~/src/django/myblogsite/templates/singlepost.html:
  <h3>{{ post.title }}</h3>
  {{ post.body }}
  <hr>
  <div class="post_footer">
    Author: {{ post.author.first_name }}<br>
    Date created: {{ post.date_created.date }}<br>
    {% ifnotequal post.date_modified.date post.date_created.date %}
      Last modified: {{ post.date_modified.date }}<br>
    {% endifnotequal %}
    Tags: 
    {% for tag in post.get_tag_list %}
      <a href="/blog/tag/{{ tag }}/">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
    {% endfor %}
    <br>
    <a href="/admin/myblogapp/post/{{ post.id }}">Edit post</a>
  </div>

Now you should be able to go in to the Admin interface select a user to associate with each post. Unfortunately, it does not automatically associate the logged in user with the post.

Here is a snapshot screenshot of what I'm calling version 0.1.1. Yeah, I know, I skipped 0.1.0-- I consider that to be the point where I said Goodbye Blogger and Hello Saltycrane.

How to set the default directory in Claws Mail

I've decided to use Claws Mail because it looked pretty good and mainly because I respect Adam Gomaa's opinion a lot. My first customization is to change the default directory used to store the messages. I couldn't find this in the documentation, but found the solution after Googling. The solution is: edit the path in your ~/.claws-mail/folderlist.xml file. Now to figure out how to show new message notifications in my wmii status bar!