SaltyCrane: blogprojecthttps://www.saltycrane.com/blog/2010-03-27T15:06:07-07:00Adding a per-post comments feed with Django 1.0
2010-03-27T15:06:07-07:00https://www.saltycrane.com/blog/2010/03/adding-post-comments-feed-django-10/<p>I've added an Atom feed for comments on a single post on this blog
(per a <a href="/blog/2008/02/backup-on-linux-rsnapshot-vs-rdiff/#c8451">
request in the comments</a>).
Here are my notes. I am using Django 1.0. (<em>Note: the Django feeds framework
has changed for 1.2. See the Django 1.2
<a href="http://docs.djangoproject.com/en/dev/releases/1.2/#updating-feeds">release
notes</a> for more information.</em>)
</p>
<p>Added to <code>/srv/SaltyCrane/iwiwdsmi/feeds.py</code>:</p>
<pre class="python">from django.contrib.comments.models import Comment
from django.contrib.syndication.feeds import Feed, FeedDoesNotExist
from django.utils.feedgenerator import Atom1Feed
from django.core.exceptions import ObjectDoesNotExist
from iwiwdsmi.myblogapp.models import Post
from iwiwdsmi.settings import SITE_NAME
class CommentsByPost(Feed):
title_template = "feeds/comments_title.html"
description_template = "feeds/comments_description.html"
feed_type = Atom1Feed
def get_object(self, bits):
if len(bits) != 1:
raise ObjectDoesNotExist
return Post.objects.get(id=bits[0])
def title(self, obj):
return '%s' % obj
def description(self, obj):
return 'Comments on "%s": %s' % (obj, SITE_NAME)
def link(self, obj):
if not obj:
raise FeedDoesNotExist
return obj.get_absolute_url() + "#comments"
def items(self, obj):
return Comment.objects.filter(object_pk=str(obj.id),
is_public=True,
is_removed=False,
).order_by('-submit_date')
def item_pubdate(self, item):
return item.submit_date</pre>
<p>Added to <code>/srv/SaltyCrane/iwiwdsmi/urls.py</code>:</p>
<pre class="python">from iwiwdsmi.feeds import CommentsByPost
feeds = {
'comments': CommentsByPost,
}
urlpatterns = patterns(
'',
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
{'feed_dict': feeds}),
)</pre>
<p>Added <code>/srv/SaltyCrane/iwiwdsmi/templates/feeds/comments_title.html</code>:</p>
<pre class="django">Comment by {{ obj.name|escape }}</pre>
<p>Added <code>/srv/SaltyCrane/iwiwdsmi/templates/feeds/comments_description.html</code>:</p>
<pre class="django">{% load markup %}
{{ obj.comment|markdown:"safe" }}</pre>
<p>Added to <code>/srv/SaltyCrane/iwiwdsmi/templates/myblogapp/singlepost.html</code>:</p>
<pre class="django"><a href="/feeds/comments/{{ post.id }}/">
<img src="http://saltycrane.s3.amazonaws.com/image/icon_feed_orange_14x14_1.png" style="border: 0pt none ; vertical-align: middle;" alt="feed icon">
Comments feed for this post</a></pre>
Notes on switching my Djangos to mod_wsgi
2009-10-22T00:44:17-07:00https://www.saltycrane.com/blog/2009/10/notes-switching-my-djangos-mod_wsgi/<p>I'm slowly trying to make my Django web servers conform to current
best practices. I've
<a href="/blog/2009/04/notes-using-nginx-mod_python-and-django/">
set up an Nginx reverse proxy for serving static files</a>,
<a href="/blog/2009/05/notes-using-pip-and-virtualenv-django/">
started using virtualenv to isolate my Python environments</a>, and
<a href="/blog/2009/08/notes-migrating-blog-sqlite-postgresql/">
migrated my database to PostgreSQL</a>. I ultimately want to implement
<a href="http://bretthoerner.com/blog/2008/oct/27/using-nginx-memcached-module-django/">
memcached+Nginx caching</a> in my reverse proxy, but the next task on my to-do list
is switching from mod_python to <a href="http://code.google.com/p/modwsgi/">mod_wsgi</a>.
</p>
<p>
Within the past year (or maybe before), mod_wsgi has become the
<a href="http://docs.djangoproject.com/en/dev/howto/deployment/modwsgi/">
preferred</a>
<a href="http://simonwillison.net/2009/Apr/1/modwsgi/">method</a>
for serving Django applications. I also originally thought
<a href="http://collingrady.wordpress.com/2009/01/06/mod_python-versus-mod_wsgi/">
switching
from mod_python</a> to mod_wsgi would save me some much needed memory on my
256MB VPS. But after trying it out, running with a single Apache process
in each case, the memory footprint was about the same. Even switching
from mod_wsgi's embedded mode to daemon mode didn't make a significant difference.
Likely the performance is better with mod_wsgi, though.
</p>
<p>Here are my notes on installing mod_wsgi.</p>
<h4>Configuration References</h4>
<ul>
<li>Django docs: <a href="http://docs.djangoproject.com/en/dev/howto/deployment/modwsgi/#howto-deployment-modwsgi">
How to use Django with Apache and mod_wsgi</a></li>
<li>mod_wsgi docs: <a href="http://code.google.com/p/modwsgi/wiki/QuickConfigurationGuide">
Quick Configuration Guide</a></li>
<li>A good blog article: <a href="http://bretthoerner.com/blog/2008/oct/09/configs-nginx-and-apache-mod_wsgi/">
Configs for nginx and Apache with mod_wsgi</a></li>
</ul>
<h4>Advice from mod_wsgi author Graham Dumpleton</h4>
<ul>
<li><a href="http://blog.dscpl.com.au/2009/03/load-spikes-and-excessive-memory-usage.html">
Load spikes and excessive memory usage in mod_python</a></li>
<li><a href="http://blog.dscpl.com.au/2008/12/using-modwsgi-when-developing-django.html">
Using mod_wsgi when developing Django sites</a></li>
<li><a href="http://groups.google.com/group/django-users/browse_thread/thread/6d670b0fa7c0d733/4ff111a1f00f7629?q=worker+daemon#4ff111a1f00f7629">
http://groups.google.com/group/django-users/browse_thread/thread/6d670b0fa7c0d733/4ff111a1f00f7629?q=worker+daemon#4ff111a1f00f7629</a></li>
<li><a href="http://groups.google.com/group/modwsgi/browse_thread/thread/cb21864a97d44ee9/38716433921a48cb?q=worker+daemon#38716433921a48cb">
http://groups.google.com/group/modwsgi/browse_thread/thread/cb21864a97d44ee9/38716433921a48cb?q=worker+daemon#38716433921a48cb</a></li>
</ul>
<h4>Install mod_wsgi and apache mpm-worker</h4>
<p>I'm not 100% sure about prefork vs. worker mpm, but Graham Dumpleton
favors worker mpm.</p>
<pre>sudo apt-get install libapache2-mod-wsgi
sudo apt-get install apache2-mpm-worker</pre>
<h4>Create .wsgi application file</h4>
<p>My virtualenv is located at <code>/srv/python-environments/saltycrane</code>.
My Django settings files is at <code>/srv/SaltyCrane/iwiwdsmi/settings.py</code>.
</p>
<p><code>/srv/SaltyCrane/saltycrane.wsgi</code>:</p>
<pre class="python">import os
import sys
import site
site.addsitedir('/srv/python-environments/saltycrane/lib/python2.5/site-packages')
os.environ['DJANGO_SETTINGS_MODULE'] = 'iwiwdsmi.settings'
sys.path.append('/srv/SaltyCrane')
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()</pre>
<h4>Edit Apache's httpd.conf file</h4>
<p>I went back and forth between using embedded mode or daemon mode.
I've ended up with embedded mode for now since it seems to use a tad less
memory and is supposed to be a little bit faster. However, Graham Dumpleton seems to
recommend daemon mode for people on VPSs. I may change my mind again later.
To use daemon mode, I just need to uncomment the <code>WSGIDaemonProcess</code>
and <code>WSGIProcessGroup</code> lines.
I have <code>StartServers</code> set to 1 because I can only afford to have
one Apache process running.
This is assuming nginx is proxying requests to apache. For more
on my nginx setup, see <a href="/blog/2009/04/notes-using-nginx-mod_python-and-django/">
here</a>.</p>
<p>Edit <code>/etc/apache2/httpd.conf</code>:</p>
<pre>
<IfModule mpm_worker_module>
StartServers 1
ServerLimit 1
ThreadsPerChild 5
ThreadLimit 5
MinSpareThreads 5
MaxSpareThreads 5
MaxClients 5
MaxRequestsPerChild 500
</IfModule>
KeepAlive Off
NameVirtualHost 127.0.0.1:8080
Listen 8080
<VirtualHost 127.0.0.1:8080>
ServerName www.saltycrane.com
# WSGIDaemonProcess saltycrane.com processes=1 threads=5 display-name=%{GROUP}
# WSGIProcessGroup saltycrane.com
WSGIScriptAlias / /srv/SaltyCrane/saltycrane.wsgi
</VirtualHost>
<VirtualHost 127.0.0.1:8080>
ServerName supafu.com
# WSGIDaemonProcess supafu.com processes=1 threads=5 display-name=%{GROUP}
# WSGIProcessGroup supafu.com
WSGIScriptAlias / /srv/Supafu/supafu.wsgi
</VirtualHost>
<VirtualHost 127.0.0.1:8080>
ServerName handsoncards.com
# WSGIDaemonProcess handsoncards.com processes=1 threads=5 display-name=%{GROUP}
# WSGIProcessGroup handsoncards.com
WSGIScriptAlias / /srv/HandsOnCards/handsoncards.wsgi
</VirtualHost>
</pre>
<h4>Restart Apache</h4>
<pre>sudo /etc/init.d/apache2 restart</pre>
Notes on migrating this blog from SQLite to PostgreSQL using Django
2009-08-07T23:54:14-07:00https://www.saltycrane.com/blog/2009/08/notes-migrating-blog-sqlite-postgresql/<p>Here are my notes on migrating this blog from SQLite to PostgreSQL. For the parts
of my database that were in proper order, the migration was made very easy using
Django's
<a href="http://docs.djangoproject.com/en/dev/ref/django-admin/#dumpdata">./manage.py dumpdata</a>
and
<a href="http://docs.djangoproject.com/en/dev/ref/django-admin/#loaddata-fixture-fixture">
./manage.py loaddata</a> commands. However, the database tables used for storing
the comments on this blog were kind of screwed up because I had
<a href="/blog/2008/07/django-blog-project-9-migrating-blogger/">
previously migrated</a> them from my old Blogger blog. So I had to write
another (not so pretty) script for that.
</p>
<p>Thanks to
<a href="http://stevelord.tumblr.com/post/86674737/migrating-an-sqlite-based-django-app-to-postgresql">
this article</a> for showing me how to use Django's dumpdata and loaddata for
this migration.
</p>
<h4>Create a new Postgres database</h4>
<p>See <a href="/blog/2008/12/card-store-project-2-installing-satchmo-django-postgresql-and-apache-ubuntu-slicehost/#postgres">
my previous notes</a> for creating a Postgres database. I named the new
database "saltycrane_db" owned by "django_user" with password "my_password".
</p>
<h4>Clone and modify Django project</h4>
<ul>
<li>Clone my SaltyCrane project
<pre>hg clone SaltyCrane SaltyCraneNew</pre>
or
<pre>cp -rp SaltyCrane SaltyCraneNew</pre>
</li>
<li>Edit settings.py:
<pre>DATABASE_ENGINE = 'postgresql_psycopg2'
DATABASE_NAME = 'saltycrane_db'
DATABASE_USER = 'django_user'
DATABASE_PASSWORD = my_password'</pre>
</li>
<li>Create database tables
<pre>python manage.py syncdb</pre>
</li>
</ul>
<h4>Migrate data</h4>
<ul>
<li>Create JSON dumps from the existing SQLite database for my 3 Django apps (myblogapp, comments, and tagging):
<pre>cd /srv/SaltyCrane/iwiwdsmi
./manage.py dumpdata myblogapp > dump_myblogapp.json
./manage.py dumpdata comments > dump_comments.json
./manage.py dumpdata tagging > dump_tagging.json</pre>
</li>
<li>Load the JSON data dumps to the new Postgres database:
<pre>cd /srv/SaltyCraneNew/iwiwdsmi
./manage.py loaddata ../../SaltyCrane/iwiwdsmi/dump_tagging.json
./manage.py loaddata ../../SaltyCrane/iwiwdsmi/dump_myblogapp.json</pre>
</li>
</ul>
<p>However, loading comments didn't work because I had some missing fields so...</p>
<h4>Migration script</h4>
<p>...I wrote a migration script:</p>
<pre class="python">import setup_environment
import simplejson
from pprint import pprint
from django.contrib.comments.models import Comment
from django.contrib.contenttypes.models import ContentType
from iwiwdsmi.myblogapp.models import Post
JSON_FILENAME = "/srv/SaltyCrane/iwiwdsmi/dump_comments.json"
ct = ContentType.objects.get(name='post', app_label='myblogapp')
N_OLD_COMMENTS = 4000
def main():
delete_all_comments()
create_dummy_comments()
pydata = open_json_file(JSON_FILENAME)
save_items_to_database(pydata)
delete_dummy_comments()
def delete_all_comments():
""" Clears the database of all comments.
"""
comments = Comment.objects.all()
comments.delete()
print "All comments deleted."
def create_dummy_comments():
""" Create a bunch of filler dummy comments
"""
for i in range(N_OLD_COMMENTS):
c = Comment()
c.comment = "Filler comment."
c.content_type = ct
c.ip_address = None
c.is_public = False
c.is_removed = False
c.object_pk = 243
c.site_id = 1
c.user_email = "filler@email.com"
c.user_name = "Filler"
c.save()
print "Filler comments created."
def delete_dummy_comments():
comments = Comment.objects.filter(is_public=False)
comments.delete()
print "Dummy comments deleted."
def open_json_file(filename):
""" open the json file and return the python data structure
"""
json_fp = open(filename)
pydata = simplejson.load(json_fp)
return pydata
def save_items_to_database(pydata):
""" Process the python data structure and save to the new database
"""
for item in pydata:
pprint(item)
pk = item['pk']
item = item['fields']
if item['is_public'] and not item['is_removed']:
c = Comment.objects.get(id=pk)
c.comment = item['comment']
c.content_type = ct
c.ip_address = get_ip_address(item['ip_address'])
c.is_public = item['is_public']
c.is_removed = item['is_removed']
c.object_pk = item['object_pk']
c.site_id = 1
c.submit_date = item['submit_date']
c.user_id = item['user']
c.user_email = item['user_email']
c.user_name = item['user_name']
c.user_url = item['user_url']
c.save()
def get_ip_address(ip):
""" Handle bad input for IP addresses
"""
if ip == "" or ip == "unknown":
return None
else:
return ip
if __name__ == '__main__':
main()</pre>
Notes on using nginx with mod_python and Django
2009-04-28T00:26:11-07:00https://www.saltycrane.com/blog/2009/04/notes-using-nginx-mod_python-and-django/<p>Here are my notes on setting up
<a href="http://nginx.net/">nginx</a> as a reverse proxy with
Apache, mod_python, and Django on Ubuntu Intrepid. Nginx is used
to serve my static media files while all other requests are passed
on to the Apache/mod_python/Django web server.
</p>
<p>I realize
<a href="http://code.google.com/p/modwsgi/">mod_wsgi</a> has become the
<a href="http://simonwillison.net/2009/Apr/1/modwsgi/">preferred</a>
<a href="http://collingrady.wordpress.com/2009/01/06/mod_python-versus-mod_wsgi/">
way</a> to deploy Django, but I'm a little behind the times and am still
using mod_python. I hope to switch to mod_wsgi soon.
</p>
<p>I <a href="http://www.saltycrane.com/blog/2008/12/card-store-project-4-notes-using-amazons-cloudfront/">
have been using</a> Amazon's
<a href="http://aws.amazon.com/cloudfront/">CloudFront</a> service for
delivering my static media files. As far as I can tell, it has worked well.
My main reason for switching to nginx is so I can skip the extra step of
uploading to Amazon. Regarding my concern about the memory footprint of nginx,
it looks like it is using around 5mb with my two process configuration.
</p>
<p>My configuration parameters are shown below. I'm running two sites, SaltyCrane and
HandsOnCards on a <a href="http://www.slicehost.com/">Slicehost</a> 256mb plan.
</p>
<table width=550>
<tr>
<td><b>Description</b></td>
<td><b>HandsOnCards</b></td>
<td><b>SaltyCrane</b></td>
</tr>
<tr>
<td>Redirection</td>
<td><code class="code2">http://www.handsoncards.com</code> is redirected to <code class="code2">http://handsoncards.com</code></td>
<td><code class="code2">http://saltycrane.com</code> is redirected to <code class="code2">http://www.saltycrane.com</code></td>
</tr>
<tr>
<td>Static media filesystem path</td>
<td><code class="code2">/srv/HandsOnCards/handsoncards/static/</code></td>
<td><code class="code2">/srv/SaltyCrane/iwiwdsmi/media/</code></td>
</tr>
<tr>
<td>Static media web path</td>
<td><code class="code2">/site_media/</code></td>
<td><code class="code2">/site_media/</code></td>
</tr>
<tr>
<td>Django settings.py file location</td>
<td><code class="code2">/srv/HandsOnCards/handsoncards/settings.py</code></td>
<td><code class="code2">/srv/SaltyCrane/iwiwdsmi/settings.py</code></td>
</tr>
<tr>
<td>Additional Python packages path</td>
<td><code class="code2">/srv/python-packages</code></td>
<td><code class="code2">/srv/python-packages</code></td>
</tr>
</table>
<h4>Install nginx</h4>
<p>Some recommend installing nginx from source, but I took the easier route
and used Ubuntu's package manager.</p>
<pre>sudo apt-get install nginx</pre>
<h4>Nginx configuration</h4>
<p>Edit <code>/etc/nginx/nginx.conf</code>:</p>
<pre class="nginx">user www-data www-data;
worker_processes 2;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
sendfile on;
tcp_nopush on;
keepalive_timeout 3;
tcp_nodelay off;
gzip on;
gzip_comp_level 2;
gzip_proxied any;
gzip_types text/plain text/html text/css application/x-javascript text/xml
application/xml application/xml+rss text/javascript;
server {
listen 80;
server_name www.handsoncards.com;
rewrite ^/(.*) http://handsoncards.com/$1 permanent;
}
server {
listen 80;
server_name handsoncards.com;
access_log /var/log/nginx/handsoncards.com.access.log;
error_log /var/log/nginx/handsoncards.com.error.log;
location / {
proxy_pass http://127.0.0.1:8080/;
include /etc/nginx/proxy.conf;
}
location /site_media/ {
alias /srv/HandsOnCards/handsoncards/static/;
expires 24h;
}
}
server {
listen 80;
server_name saltycrane.com;
rewrite ^/(.*) http://www.saltycrane.com/$1 permanent;
}
server {
listen 80;
server_name www.saltycrane.com;
access_log /var/log/nginx/saltycrane.com.access.log;
error_log /var/log/nginx/saltycrane.com.error.log;
location / {
proxy_pass http://127.0.0.1:8080/;
include /etc/nginx/proxy.conf;
}
location /site_media/ {
alias /srv/SaltyCrane/iwidsmi/media/;
expires 24h;
}
}
}
</pre>
<p>Edit <code>/etc/nginx/proxy.conf</code>:</p>
<pre class="nginx">proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffers 32 4k;</pre>
<p>Restart Nginx</p>
<pre>sudo /etc/init.d/nginx restart</pre>
<h4>Install Apache</h4>
<p>I already had Apache and mod_python installed, but in case you don't:</p>
<pre>apt-get install apache2 apache2-mpm-prefork
apt-get install libapache2-mod-python</pre>
<h4>Apache Configuration</h4>
<p>Edit <code>/etc/apache2/httpd.conf</code>:</p>
<pre>MaxClients 2
MaxRequestsPerChild 350
KeepAlive Off
NameVirtualHost 127.0.0.1:8080
Listen 8080
<VirtualHost 127.0.0.1:8080>
ServerName www.saltycrane.com
<Location "/">
SetHandler python-program
PythonHandler django.core.handlers.modpython
SetEnv DJANGO_SETTINGS_MODULE iwiwdsmi.settings
PythonPath "['/srv/SaltyCrane', '/srv/python-packages'] + sys.path"
PythonDebug Off
</Location>
</VirtualHost>
<VirtualHost 127.0.0.1:8080>
ServerName handsoncards.com
<Location "/">
SetHandler python-program
PythonHandler django.core.handlers.modpython
SetEnv DJANGO_SETTINGS_MODULE handsoncards.settings
PythonPath "['/srv/HandsOnCards', '/srv/python-packages'] + sys.path"
PythonDebug Off
</Location>
</VirtualHost></pre>
<p>Edit <code>/etc/apache2/ports.conf</code> and comment out the following two lines:</p>
<pre>#NameVirtualHost *:80
#Listen 80</pre>
<p>Restart Apache</p>
<pre>sudo /etc/init.d/apache2 restart</pre>
<h4>Add Django's reverse proxy middleware</h4>
<p>Edit your <code>settings.py</code> file to include
<code class="code2">django.middleware.http.SetRemoteAddrFromForwardedFor</code> in
<code>MIDDLEWARE_CLASSES</code>. This allows your Django application to use the real
IP address of the client instead of <code>127.0.0.1</code> from your nginx proxy. It
sets Django's <code>request.META['REMOTE_ADDR']</code>
to be <code>request.META['HTTP_X_FORWARDED_FOR']</code> which we set above in nginx's
<code>proxy.conf</code>. For more information, see the
<a href="http://docs.djangoproject.com/en/dev/ref/middleware/#reverse-proxy-middleware">
Middleware reference</a> in the Django documentation.
</p>
<pre>MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.middleware.http.SetRemoteAddrFromForwardedFor',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
)</pre>
<h4>References</h4>
<ul>
<li><a href="http://bart.whahay.net/blog/2009/04/06/setting-up-nginx-django.html">
http://bart.whahay.net/blog/2009/04/06/setting-up-nginx-django.html</a> (my main reference)</li>
<li><a href="http://articles.slicehost.com/2009/3/5/ubuntu-intrepid-nginx-configuration">
http://articles.slicehost.com/2009/3/5/ubuntu-intrepid-nginx-configuration</a> (general nginx configuration)</li>
<li><a href="http://articles.slicehost.com/2009/3/6/ubuntu-intrepid-nginx-virtual-hosts-1">
http://articles.slicehost.com/2009/3/6/ubuntu-intrepid-nginx-virtual-hosts-1</a> (nginx virtual hosts configuration)</li>
<li><a href="http://forum.slicehost.com/comments.php?DiscussionID=2964">
http://forum.slicehost.com/comments.php?DiscussionID=2964</a> (explains the
difference between <code>root</code> and <code>alias</code> in nginx.conf.)</li>
</ul>
<h4>Errors</h4>
<ul>
<li>A 502 Bad Gateway from Nginx probably means there is something wrong with your Apache setup</li>
<li>An Internal Server Error from Apache means something is probably wrong with your Django
setup.</li>
</ul>
Django Blog Project #16: Adding URL redirects using the Blogger API
2008-09-12T22:53:36-07:00https://www.saltycrane.com/blog/2008/09/django-blog-project-16-adding-url-redirects-using-blogger-api/<p>I wanted to insert URL redirects on my old Blogger posts pointing to my new
blog articles.
A <a href="http://www.saltycrane.com/blog/2008/07/django-blog-project-9-migrating-blogger/#lc96">comment</a>
on my <a href="http://www.saltycrane.com/blog/2008/07/django-blog-project-9-migrating-blogger/">Migrating
Blogger Posts</a> post suggested that I use the
<a href="http://code.google.com/apis/blogger/developers_guide_python.html">(Python)
Blogger API</a>. This was a great suggestion. The Blogger API was well documented
and easy to use. Here is the script I used to insert the URL redirects on each
of my old Blogger posts.</p>
<pre class="python">from gdata import service
import re
import gdata
import atom
NEW_HTML = """
<script language='javascript'>
setTimeout('location.href="%s"', 2000);
</script>
<br><br>
<b>
<p>This is my OLD blog. I've copied this post over to my NEW blog at:</p>
<p><a href="%s">%s</a></p>
<p>You should be redirected in 2 seconds.</p>
</b>
<br><br>
"""
# authenticate
blogger_service = service.GDataService('myusername@gmail.com', 'mypassword')
blogger_service.service = 'blogger'
blogger_service.account_type = 'GOOGLE'
blogger_service.server = 'www.blogger.com'
blogger_service.ProgrammaticLogin()
# get list of blogs
query = service.Query()
query.feed = '/feeds/default/blogs'
feed = blogger_service.Get(query.ToUri())
# get blog id
blog_id = feed.entry[0].GetSelfLink().href.split("/")[-1]
# get all posts
query = service.Query()
query.feed = '/feeds/%s/posts/default' % blog_id
query.published_min = '2000-01-01'
query.published_max = '2009-01-01'
query.max_results = 1000
feed = blogger_service.Get(query.ToUri())
print feed.title.text
for entry in feed.entry:
# create link to article on new blog
new_link = re.sub(r'http://iwiwdsmi\.blogspot\.com/(.*)\.html',
r'http://www.saltycrane.com/blog/\1/',
entry.link[0].href)
print new_link
# update post
to_add = NEW_HTML % (new_link, new_link, new_link)
entry.content.text = to_add + entry.content.text
blogger_service.Put(entry, entry.GetEditLink().href)</pre>
Django Blog Project #15: New site logo
2008-09-11T01:12:50-07:00https://www.saltycrane.com/blog/2008/09/django-blog-project-15-new-site-logo/<p>I now have a new site logo design drawn by my wife, Angela! Doesn't it look great?
My previous logo was a crane picture I had just pulled from the web somewhere. So
it is nice to have a custom logo done for me. Luckily my wife is artistic and didn't
mind drawing it for me. I also made some minor changes to the title block to make things
look a little better up there. Now to figure out how to style the rest of the page.
</p>
<p>I also got a "memory over limit" warning from Webfaction this week. Over the weekend,
I had redirected all my old Blogger posts to this blog, so apparently the small increase in traffic brought to light some of my inefficient code. To help solve the
problem, I switched over to <a href="http://code.google.com/p/django-tagging/">django-tagging</a>.
This eliminated a bunch of my inefficient code and I appear to be within the
memory limits now. There is still another section of code I need to rework, but
this solves the problem for now. Django-tagging is pretty cool-- I haven't quite got everything working correctly, but I will be sure to write some notes on it when I get the time.
</p>
Django Blog Project #14: Running Django 1.0
2008-09-05T00:54:03-07:00https://www.saltycrane.com/blog/2008/09/django-blog-project-14-running-django-10/<p>I'm now running Django 1.0 for this blog. For those who haven't heard, Django 1.0
Final was released yesterday sometime. My last Django update had been shortly after
Beta 2 was released and updating to 1.0 Final didn't require any code changes for
me. (I think everything since then was supposed to be bug fixes.) (I also updated
my work project to 1.0 with no problem.) That's about all the content for this post.
Here are some links:</p>
<p>Here are my notes on my path to 1.0:</p>
<ul>
<li><a href="http://www.saltycrane.com/blog/2008/08/django-blog-project-11-migrate-django-096-svn-trunk/">
Django Blog Project #11: Migrating from Django 0.96 to SVN Trunk</a></li>
<li><a href="http://www.saltycrane.com/blog/2008/09/django-blog-project-13-updating-django-10-beta-2-new-comments-adding-markdown-support/">
Django Blog Project #13: Updating Django 1.0 Beta 2 New Comments and Adding Markdown support
</a></li>
</ul>
<br>
<p>And here are some Django 1.0 links:</p>
<ul>
<li><a href="http://docs.djangoproject.com/en/dev/releases/1.0/">Django 1.0 release
notes</a></li>
<li><a href="http://docs.djangoproject.com/en/dev/releases/1.0-porting-guide/">Django
0.96 to 1.0 Porting Guide</a></li>
<li><a href="http://code.djangoproject.com/wiki/BackwardsIncompatibleChanges">A more
detailed list of backwards incompatible changes</a></li>
<li><a href="http://docs.djangoproject.com/en/dev/">Django 1.0 documentation</a>
(This is a new documentation site)</li>
</ul>
<br>
Django Blog Project #13: Updating Django 1.0 Beta 2 New Comments and Adding Markdown support
2008-09-02T00:40:49-07:00https://www.saltycrane.com/blog/2008/09/django-blog-project-13-updating-django-10-beta-2-new-comments-adding-markdown-support/<p>I've updated to Django 1.0 Beta 2. One of the big items for this release was the
new commenting framework. I had been waiting for this, so I was excited to see it
was finally done.</p>
<p>I also added support for <a href="http://daringfireball.net/projects/markdown/">Markdown</a>
formatting of my comments. I actually could have added this earlier, but I only
recently learned that <a href="http://www.freewisdom.org/projects/python-markdown/Django">Django
has built-in support for Markdown</a>.
<h5>Update URLConf</h5>
<p>When I glanced over the changes for the new commenting framework, I missed this
change and I actually had to Google on my error message. Luckily, someone (I don't
remember where I found it now) had run into the same problem and saved me.</p>
<code>~/src/django/myblogsite/urls.py:</code>
<pre class="diff">--- a/urls.py Thu Aug 21 10:05:20 2008 -0500
+++ b/urls.py Mon Sep 01 22:34:16 2008 -0700
@@ -1,6 +1,6 @@
from django.conf.urls.defaults import *
from django.contrib import admin
-from django.contrib.comments.models import FreeComment
+from django.contrib.comments.models import Comment
from iwiwdsmi.myblogapp.views import *
from iwiwdsmi.feeds import *
from iwiwdsmi.views import *
@@ -19,7 +19,7 @@
(r'^admin/(.*)', admin.site.root),
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
{'feed_dict': feeds}),
- (r'^comments/', include('django.contrib.comments.urls.comments')),
+ (r'^comments/', include('django.contrib.comments.urls')),
(r'^$', rootview),</pre>
<h5>Update the database</h5>
<p>See the Django <a href="http://docs.djangoproject.com/en/dev/ref/contrib/comments/upgrade/#ref-contrib-comments-upgrade">
Upgrading from Django's previous comment system</a> guide for more complete information.</p>
<ul>
<li>I ran
<pre>$ cd ~/src/django/myblogsite
$ python manage.py syncdb</pre>
</li>
<li>Then I entered my sqlite3 shell:
<pre>$ sqlite3 mydatabase.sqlite3</pre>
and pasted the following at the command prompt:
<pre>BEGIN;
INSERT INTO django_comments
(content_type_id, object_pk, site_id, user_name, user_email, user_url,
comment, submit_date, ip_address, is_public, is_removed)
SELECT
content_type_id, object_id, site_id, person_name, '', '', comment,
submit_date, ip_address, is_public, approved
FROM comments_freecomment;
INSERT INTO django_comments
(content_type_id, object_pk, site_id, user_id, user_name, user_email,
user_url, comment, submit_date, ip_address, is_public, is_removed)
SELECT
content_type_id, object_id, site_id, user_id, '', '', '', comment,
submit_date, ip_address, is_public, is_removed
FROM comments_comment;
UPDATE django_comments SET user_name = (
SELECT username FROM auth_user
WHERE django_comments.user_id = auth_user.id
) WHERE django_comments.user_id is not NULL;
UPDATE django_comments SET user_email = (
SELECT email FROM auth_user
WHERE django_comments.user_id = auth_user.id
) WHERE django_comments.user_id is not NULL;
COMMIT;</pre>
then exited:
<pre>.exit</pre>
</li>
</ul>
<h5>Templates</h5>
<p>The rest of the changes were with the templates.</p>
<ul>
<li>I removed my old comments templates:
<pre>rm -rf ~/src/django/myblogsite/templates/comments</pre>
</li>
<li>I copied the new templates:
<pre>cp -r ~/lib/django_trunk/django/contrib/comments ~/src/django/myblogsite/templates</pre>
</li>
<li>I updated <code>~/src/django/myblogsite/templates/listpage.html</code>:
<pre class="diff">--- a/templates/listpage.html Thu Aug 21 10:05:20 2008 -0500
+++ b/templates/listpage.html Mon Sep 01 22:46:34 2008 -0700
@@ -47,7 +47,7 @@
{% endfor %}
|
- {% get_free_comment_count for myblogapp.post post.id as comment_count %}
+ {% get_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></pre>
</li>
<li>I updated <code>~/src/django/myblogsite/templates/singlepost.html</code>:
<pre class="diff">--- a/templates/singlepost.html Thu Aug 21 10:05:20 2008 -0500
+++ b/templates/singlepost.html Tue Sep 02 00:44:51 2008 -0700
@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% load comments %}
+{% load markup %}
{% block title %}
{{ main_title }}: {{ post.title }}
@@ -59,8 +60,8 @@
{% endfor %}
<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 %}
+ {% get_comment_list for myblogapp.post post.id as comment_list %}
+ {% get_comment_count for myblogapp.post post.id as comment_count %}
{% if comment_list %}
<h4>{{ comment_count }}
{% if lc_list %}New {% endif %}
@@ -69,13 +70,19 @@
{% for comment in comment_list %}
<br>
<a name="c{{ comment.id }}" 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 }}
+ <b>
+ {% if comment.url %}
+ <a href="{{ comment.url }}">{{ comment.name|escape }}</a>
+ {% else %}
+ {{ comment.name|escape }}
+ {% endif %}
+ </b> commented,
+ on {{ comment.submit_date|date:"F j, Y" }} at {{ comment.submit_date|date:"P" }}:
+ {{ comment.comment|markdown:"safe" }}
{% endfor %}
<br>
<h4>Post a comment</h4>
- {% free_comment_form for myblogapp.post post.id %}
+ {% render_comment_form for post %}
{% endblock %}
</pre>
</li>
</ul>
<h5>Add django.contrib.markup to INSTALLED_APPS</h5>
<p>To use Markdown, I added <code>django.contrib.markup</code> to my
<code>INSTALLED_APPS</code> in <code>settings.py</code>
<code>~/src/django/myblogsite/settings.py</code>:
<pre class="diff"> INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.admin',
'django.contrib.comments',
+ 'django.contrib.markup',
'iwiwdsmi.myblogapp',
)</pre>
<p>That was about it. I messed with the templates a little to try to make things
a little prettier. I'm not completely satisfied yet though. My next step is to
add <a href="http://code.google.com/p/django-openid/">django-openid</a> support.
Later on, I'd also like to add email notification and spam filtering.
</p>
<h5>Error messages:</h5>
<b>got an unexpected keyword argument 'core'</b>
<pre style="height:100px; overflow:auto">Validating models...
Unhandled exception in thread started by <function inner_run at 0x8675ca4>
Traceback (most recent call last):
File "/home/sofeng/lib/python-packages/django/core/management/commands/runserver.py", line 47, in inner_run
self.validate(display_num_errors=True)
File "/home/sofeng/lib/python-packages/django/core/management/base.py", line 122, in validate
num_errors = get_validation_errors(s, app)
File "/home/sofeng/lib/python-packages/django/core/management/validation.py", line 28, in get_validation_errors
for (app_name, error) in get_app_errors().items():
File "/home/sofeng/lib/python-packages/django/db/models/loading.py", line 128, in get_app_errors
self._populate()
File "/home/sofeng/lib/python-packages/django/db/models/loading.py", line 57, in _populate
self.load_app(app_name, True)
File "/home/sofeng/lib/python-packages/django/db/models/loading.py", line 72, in load_app
mod = __import__(app_name, {}, {}, ['models'])
File "/home/sofeng/src/django/mozblog/myblogapp/models.py", line 30, in <module>
class LegacyComment(models.Model):
File "/home/sofeng/src/django/mozblog/myblogapp/models.py", line 32, in LegacyComment
website = models.URLField(core=False)
File "/home/sofeng/lib/python-packages/django/db/models/fields/__init__.py", line 828, in __init__
CharField.__init__(self, verbose_name, name, **kwargs)
TypeError: __init__() got an unexpected keyword argument 'core'</pre>
<p>I removed the <code>core</code> argument from my models. This is an oldforms
related thing that has been removed.
<a href="http://code.djangoproject.com/wiki/BackwardsIncompatibleChanges#Removedoldformsvalidatorsandrelatedcode">
See here</a>
</p>
<b>Error while importing URLconf myblogsite.urls': cannot import name FreeComment</b>
<p>Comments have been refactored. See the
<a href="http://docs.djangoproject.com/en/dev/ref/contrib/comments/upgrade/">
Upgrading Guide</a>
</p>
Django Blog Project #12: Adding Pygments syntax highlighting
2008-08-20T14:55:29-07:00https://www.saltycrane.com/blog/2008/08/django-blog-project-12-adding-pygments-syntax-highlighting/<p>I've finally added automatic code highlighting to my blog. It
uses <a href="http://pygments.org/">Pygments</a> to do the
syntax highlighting and
<a href="http://www.crummy.com/software/BeautifulSoup/">Beautiful
Soup</a> to find all the <code><pre></code> blocks
to highlight. I still write my blog posts in HTML, but now
add a <code>class</code> attribute to my <code><pre></code>
tags to specify the Pygments lexer to use. For example,
for python code, I use:</p>
<pre><pre class="python">
import this
def demo():
pass</pre></pre>
<p>Which turns into:</p>
<pre class="python">import this
def demo():
pass</pre>
<p>I bought <a href="http://www.b-list.org/">James Bennett</a>'s book,
<a href="http://www.apress.com/book/view/1590599969">Practical
Django Projects</a> about a month ago and it has good information
about creating a blog with Django. It also documented techniques
for syntax highlighting which I used here. To summarize, I
added a new attribute, called <code>body_highlighted</code> to
my <code>Post</code> model. Then, I added a custom
<code>save()</code> method which parses my original HTML with
Beautiful Soup and highlights it with Pygments.</p>
<h5>Model changes</h5>
<p>Here is the relevant code in
<code>~/src/django/myblogsite/myblogapp/models.py</code>:</p>
<pre class="python">class Post(models.Model):
# ...
body = models.TextField()
body_highlighted = models.TextField(editable=False, blank=True)
def save(self):
self.body_highlighted = self.highlight_code(self.body)
super(Post, self).save()
def highlight_code(self, html):
soup = BeautifulSoup(html)
preblocks = soup.findAll('pre')
for pre in preblocks:
if pre.has_key('class'):
try:
code = ''.join([unicode(item) for item in pre.contents])
code = self.unescape_html(code)
lexer = lexers.get_lexer_by_name(pre['class'])
formatter = formatters.HtmlFormatter()
code_hl = highlight(code, lexer, formatter)
pre.replaceWith(BeautifulSoup(code_hl))
except:
pass
return unicode(soup)
def unescape_html(self, html):
html = html.replace('&lt;', '<')
html = html.replace('&gt;', '>')
html = html.replace('&amp;', '&')
return html</pre>
<p><em>Update 2010-04-09:</em> I added the <code>unescape_html</code>
method so that I could highlight Python code with regular expression
named groups. For example:
<pre class="python">m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+)", "Malcolm Reynolds")</pre>
With the new fix in place, I just need to escape the < and > characters
with &lt; and &gt; and the syntax highlighting will display
correctly. Before I made the fix, if I did not escape the characters, BeautifulSoup
would add closing tags to what it thought was my malformed HTML. So
instead of the above, it looked like this:
<pre class="python">m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+)", "Malcolm Reynolds")</pre>
If anyone knows of a better solution, please let me know.
</p>
<h5>Update the database</h5>
<ul>
<li>List the SQL commands Django would use:
<pre class="bash">$ cd ~/src/django/myblogsite/
$ python manage.py sqlall myblogapp</pre>
<pre>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_highlighted" text NOT NULL,
)
;
CREATE INDEX "myblogapp_post_author_id" ON "myblogapp_post" ("author_id");
CREATE INDEX "myblogapp_post_slug" ON "myblogapp_post" ("slug");
COMMIT;</pre>
</li>
<li>Enter the sqlite3 shell:
<pre class="bash">$ sqlite3 mydatabase.sqlite3</pre>
<br>
and enter the following statements:
<pre>sqlite> ALTER TABLE myblogapp_post ADD COLUMN body_highlighted text;
sqlite> .exit</pre>
</li>
</ul>
<h5>Update the template</h5>
<p>Here is the relevant code in
<code>~/src/django/myblogsite/templates/singlepost.html</code>:</p>
<pre class="django"> {% if post.body_highlighted %}
{{ post.body_highlighted|safe }}
{% else %}
{{ post.body|safe }}
{% endif %}</pre>
<h5>Add CSS for Pygments</h5>
<p>One last step is to add the CSS for Pygments.
Here is an excerpt from my <code>~/src/django/myblogsite/media/css/mystyle.css</code>:
</p>
<pre class="css" style="height: 300px; overflow: auto">/* PYGMENTS STYLE */
/* customized */
.c { color: #008040; font-style: italic } /* Comment */
.cm { color: #008040; font-style: italic } /* Comment.Multiline */
.cp { color: #BC7A00 } /* Comment.Preproc */
.c1 { color: #008040; font-style: italic } /* Comment.Single */
.cs { color: #008040; font-style: italic } /* Comment.Special */
.gd { color: grey; text-decoration: line-through } /* Generic.Deleted */
.gi { color: red; } /* Generic.Inserted */
.k { color: #000080; font-weight: bold } /* Keyword */
.kc { color: #000000; font-weight: bold } /* Keyword.Constant */
.kd { color: #000000; font-weight: bold } /* Keyword.Declaration */
.kp { color: #000000 } /* Keyword.Pseudo */
.kr { color: #000000; font-weight: bold } /* Keyword.Reserved */
.kt { color: #000000; font-weight: bold } /* Keyword.Type */
/* original settings */
.err { border: 1px solid #FF0000 } /* Error */
.o { color: #666666 } /* Operator */
.ge { font-style: italic } /* Generic.Emph */
.gr { color: #FF0000 } /* Generic.Error */
.gh { color: #000080; font-weight: bold } /* Generic.Heading */
.go { color: #808080 } /* Generic.Output */
.gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.gs { font-weight: bold } /* Generic.Strong */
.gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.gt { color: #0040D0 } /* Generic.Traceback */
.m { color: #666666 } /* Literal.Number */
.s { color: #BA2121 } /* Literal.String */
.na { color: #7D9029 } /* Name.Attribute */
.nb { color: #008000 } /* Name.Builtin */
.nc { color: #0000FF; font-weight: bold } /* Name.Class */
.no { color: #880000 } /* Name.Constant */
.nd { color: #AA22FF } /* Name.Decorator */
.ni { color: #999999; font-weight: bold } /* Name.Entity */
.ne { color: #D2413A; font-weight: bold } /* Name.Exception */
.nf { color: #0000FF } /* Name.Function */
.nl { color: #A0A000 } /* Name.Label */
.nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
.nt { color: #008000; font-weight: bold } /* Name.Tag */
.nv { color: #19177C } /* Name.Variable */
.ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
.w { color: #bbbbbb } /* Text.Whitespace */
.mf { color: #666666 } /* Literal.Number.Float */
.mh { color: #666666 } /* Literal.Number.Hex */
.mi { color: #666666 } /* Literal.Number.Integer */
.mo { color: #666666 } /* Literal.Number.Oct */
.sb { color: #BA2121 } /* Literal.String.Backtick */
.sc { color: #BA2121 } /* Literal.String.Char */
.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.s2 { color: #BA2121 } /* Literal.String.Double */
.se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
.sh { color: #BA2121 } /* Literal.String.Heredoc */
.si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
.sx { color: #008000 } /* Literal.String.Other */
.sr { color: #BB6688 } /* Literal.String.Regex */
.s1 { color: #BA2121 } /* Literal.String.Single */
.ss { color: #19177C } /* Literal.String.Symbol */
.bp { color: #008000 } /* Name.Builtin.Pseudo */
.vc { color: #19177C } /* Name.Variable.Class */
.vg { color: #19177C } /* Name.Variable.Global */
.vi { color: #19177C } /* Name.Variable.Instance */
.il { color: #666666 } /* Literal.Number.Integer.Long */</pre>
<br>
<p>All pau. Now we should have pretty syntax highlighted code!
(For those keeping track, this is now version 0.1.3 of my blog.)
</p>
Django Blog Project #11: Migrating from Django 0.96 to SVN Trunk
2008-08-04T17:38:03-07:00https://www.saltycrane.com/blog/2008/08/django-blog-project-11-migrate-django-096-svn-trunk/<p>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.
<a href="http://www.djangoproject.com/weblog/2008/jul/21/10-alpha/">
Django 1.0 alpha was released</a> a couple weeks ago, so now
seems like a good time to migrate. </p>
<p>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 <a href="http://code.djangoproject.com/wiki/BackwardsIncompatibleChanges">
Backwards-incompatible changes documentation</a>.
</p>
<p>Note, I am using trunk revision 8210. (2 weeks post Alpha).</p>
<h5>Model changes</h5>
<p>The admin definitions have been decoupled from the model
definitions. Also, the <code>prepopulate_from</code> database
field has been moved to the new admin class. See
<a href="http://code.djangoproject.com/wiki/BackwardsIncompatibleChanges#Changedprepopulate_fromtobedefinedintheAdminclassnotdatabasefieldclasses">
here</a> for more information. Also <code>maxlength</code>
was changed to <code>max_length</code>.
</p>
<code>~/src/django/myblogsite/myblogapp/models.py</code>:
<pre>import re
from django.db import models
from django.contrib.auth.models import User
<span style="color:red">from django.contrib import admin</span>
class Post(models.Model):
author = models.ForeignKey(User)
title = models.CharField(<span style="color:red">max_length</span>=200)
slug = models.SlugField(<span style="color:red">max_length</span>=200,
<del style="color:grey">prepopulate_from=['title'],</del>
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"]
<del style="color:grey"> class Admin:
pass</del>
<span style="color:red">class PostAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)}
admin.site.register(Post, PostAdmin)</span></pre>
<h5>Modify URLConf</h5>
<p>See <a href="http://code.djangoproject.com/wiki/BackwardsIncompatibleChanges#Mergednewforms-adminintotrunk">
here</a> for more information.</p>
<code>~/src/django/myblogsite/urls.py</code>:
<pre>from django.conf.urls.defaults import *
<span style="color:red">from django.contrib import admin</span>
from django.contrib.comments.models import FreeComment
from iwiwdsmi.myblogapp.views import *
from iwiwdsmi.feeds import *
from iwiwdsmi.views import *
<span style="color:red">admin.autodiscover()</span>
feeds = {
'latest': LatestPosts,
}
urlpatterns = patterns(
'',
(r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': '/home/sofeng/src/django/iwiwdsmi/media'}),
<del style="color:grey"> (r'^admin/', include('django.contrib.admin.urls')),</del>
<span style="color:red">(r'^admin/(.*)', admin.site.root),</span>
(r'^feeds/(?P<url>.*)/$', '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),
)</pre>
<h5>Use the <code>safe</code> template filter</h5>
<p>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 <code>safe</code> filter.</p>
Excerpt from <code>~/src/django/myblogsite/templates/listpage.html</code>:
<pre> {{ post.body<span style="color:red">|safe</span>|truncatewords_html:"50" }}</pre>
<h5>Use new admin templates</h5>
<p>Finally, I copied over the new Django SVN templates from
<code>trunk/django/contrib/admin/templates</code>.
Django Blog Project #10: Adding support for multiple authors
2008-08-01T12:38:01-07:00https://www.saltycrane.com/blog/2008/08/django-blog-project-10-adding-support-multiple-authors/<p>Here is a quick post on how I added support for multiple users on my blog.</p>
<h5>Modfiy the model</h5>
Excerpt from <code>~/src/django/myblogsite/myblogapp/models.py</code>:
<pre>import re
from django.db import models
<span style="color:red">from django.contrib.auth.models import User</span>
class Post(models.Model):
<span style="color:red">author = models.ForeignKey(User)</span>
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</pre>
<h5>Update the database</h5>
<ul>
<li>List the SQL commands Django would use the create the database tables:
<pre>$ cd ~/src/django/myblogsite/
$ python manage.py sqlall myblogapp</pre>
<pre>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;</pre>
</li>
<li>Enter the sqlite shell:
<pre>$ sqlite3 mydatabase.sqlite3</pre>
<br>
and enter the following statement:
<pre>sqlite> ALTER TABLE myblogapp_post ADD COLUMN author_id integer REFERENCES auth_user (id);
sqlite> .exit</pre>
</li>
</ul>
<h5>Update the template</h5>
Excerpt from <code>~/src/django/myblogsite/templates/singlepost.html</code>:
<pre> <h3>{{ post.title }}</h3>
{{ post.body }}
<hr>
<div class="post_footer">
<span style="color:red">Author: {{ post.author.first_name }}<br></span>
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></pre>
<p>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.</p>
<p>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
<a href="http://www.saltycrane.com/blog/2008/07/transition-bye-blogger-boing/">
Goodbye Blogger</a> and
<a href="http://www.saltycrane.com/blog/2008/07/hello-saltycrane/">Hello
Saltycrane</a>.
</p>
<img align="center" src="/site_media/image/version0.1.1.png">
Django Blog Project #9: Migrating Blogger posts with Beautiful Soup
2008-07-14T22:47:00-07:00https://www.saltycrane.com/blog/2008/07/django-blog-project-9-migrating-blogger/<p>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.</p>
<p>So for the past few days, I've been working on my
<a href="http://www.crummy.com/software/BeautifulSoup/">Beautiful Soup</a>
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.
</p>
<pre class="python" style="height: 400px; overflow:auto;">#!/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()
</pre>
<p>I also made some changes to my Django blog code as I migrated my Blogger posts.
The main addition was a <code>LegacyComment</code> 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
<a href="http://en.wikipedia.org/wiki/Cross-site_scripting">cross site
scripting</a>. So I separated my legacy Blogger comments from my new Django
site comments.</p>
<br /><br /><b>models.py</b><br />
<p>Here are my model changes. I added a <code>LegacyComment</code> class which
contains pertinent comment attributes and a <code>ForeignKey</code> to the
post that it belongs to. I also added a <code>lc_count</code> (for legacy
comment count) field to the <code>Post</code> class which stores the number
of comments for the post. It is updated by the <code>save()</code> method in
the <code>LegacyComment</code> 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.
</p>
<code>~/src/django/myblogsite/myblogapp/models.py</code>:
<pre style="height:300px; overflow:auto">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)
<span style="color:red">lc_count = models.IntegerField(default=0, editable=False)</span>
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
<span style="color:red">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</span>
</pre>
<br /><br /><b>views.py</b><br />
<p>Here is an excerpt from my views.py file showing the changes:</p>
<code>~/src/django/myblogsite/myblogapp/views.py</code>:
<pre>import re
from datetime import datetime
from django.shortcuts import render_to_response
from myblogsite.myblogapp.models import Post, <span style="color:red">LegacyComment</span>
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()
<span style="color:red">posts = posts[:5]
pagedata.update({'post_list': posts,
'subtitle': '',})</span>
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,)
<span style="color:red">legacy_comments = LegacyComment.objects.filter(post=post.id)
pagedata.update({'post': post,
'lc_list': legacy_comments,})</span>
return render_to_response('singlepost.html', pagedata)
</pre>
<br /><br /><b>Templates</b><br />
<p>In the list page template I used the <code>truncatewords_html</code> 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.</p>
Excerpt from <code>~/src/django/myblogsite/templates/listpage.html</code>:
<pre>{% 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<span style="color:red">|truncatewords_html:"50"</span> }}
<span style="color:red"><a href="{{ post.get_absolute_url }}">Read more...</a><br>
<br></span>
<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 }}<span style="color:red">#comments">
{{ comment_count|add:post.lc_count }}
Comment{{ comment_count|add:post.lc_count|pluralize}}</a></span>
</div>
<br>
{% endfor %}
{% endblock %}
</pre>
<p>In the single post template, I added the display of the Legacy comments in addition
to the Django free comments.</p>
Excerpt from <code>~/src/django/myblogsite/templates/singlepost.html</code>:
<pre> <span style="color:red"><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></span>
</pre>
<p>That's it. Hopefully, I can start using my new blog soon. Please browse around on
the <a href="http://saltycrane.com/blog">new Django site</a> 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.</p>
<p>Here is a snapshot screenshot of version 0.0.8:</p>
<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp1.blogger.com/_lZhqNsiakm4/SHw6-e-2w7I/AAAAAAAAAFo/V9SINeq8LVM/s1600-h/version0.0.8.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp1.blogger.com/_lZhqNsiakm4/SHw6-e-2w7I/AAAAAAAAAFo/V9SINeq8LVM/s320/version0.0.8.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5223114512985932722" /></a>
<br /><p>The live site can be viewed at: <a href="http://saltycrane.com/blog">http://saltycrane.com/blog</a>
</p>
<br />Related posts:<br />
<a href="http://www.saltycrane.com/blog/2008/05/django-new-blog-project/">
Django Blog Project #1: Creating a basic blog</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/first-django-webfaction-deployment/">
Django Blog Project #2: Deploying at Webfaction</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-3-using-css-and/">
Django Blog Project #3: Using CSS and Template Inheritance</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-4-adding-post/">
Django Blog Project #4: Adding post metadata</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-5-yui-css-and/">
Django Blog Project #5: YUI CSS and serving static media</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-6-creating-standard/">
Django Blog Project #6: Creating standard blog views</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-7-adding-simple/">
Django Blog Project #7: Adding a simple Atom feed</a><br />
<a href="http://www.saltycrane.com/blog/2008/07/django-blog-project8-adding-basic/">
Django Blog Project #8: Adding basic comment functionality</a><br />
<br />
Django Blog Project #8: Adding basic comment functionality with Django Free Comments
2008-07-06T22:32:00-07:00https://www.saltycrane.com/blog/2008/07/django-blog-project8-adding-basic/<p><em>Update 2009-05-08: The notes here apply to Django 0.96 and not
Django 1.0 or later. In particular, <code>FreeComment</code> has been changed
to <code>Comment</code> in Django 1.0. For current documentation, please go
<a href="http://docs.djangoproject.com/en/dev/ref/contrib/comments/#ref-contrib-comments-index">here</a> instead.</em></p>
<p>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
<a href="http://code.google.com/p/django-comment-utils/">comment_utils</a>, 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.</p>
<p>I basically followed the instructions on the wiki:
<a href="http://code.djangoproject.com/wiki/UsingFreeComment">
Using Django's Free Comments</a>. See there for more information.
It was pretty easy to set up.</p>
<br /><b>settings.py</b><br />
<p>In my settings.py file, I added <code>django.contrib.comments</code> to the
list of installed apps.</p>
Excerpt from <code>~/src/django/myblogsite/settings.py</code>:
<pre>INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.admin',
<span style="color:red">'django.contrib.comments',</span>
'iwiwdsmi.myblogapp',
)</pre>
<br /><br /><b>urls.py</b><br />
<p>I modified my urls.py to use the <code>comment</code> module's URLConf.</p>
<code>~/src/django/myblogsite/urls.py</code>:
<pre>from django.conf.urls.defaults import *
<span style="color:red">from django.contrib.comments.models import FreeComment</span>
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}),
<span style="color:red">(r'^comments/', include('django.contrib.comments.urls.comments')),</span>
(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),
)</pre>
<br /><br /><b>python manage.py syncdb</b><br />
<p>To install the comments model, I ran <code>python manage.py syncdb</code>:</p>
<pre>$ cd ~/src/django/myblogsite
$ python manage.py syncdb</pre>
<br /><br /><b>List page template</b><br />
<p>Then I modified my list page template to display the number of comments for
each post:</p>
<code>~/src/django/myblogsite/templates/listpage.html</code>:
<pre>{% extends "base.html" %}
<span style="color:red">{% load comments %}</span>
{% 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 %}
<span style="color:red"><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></span>
</div>
<br>
{% endfor %}
{% endblock %}</pre>
<p>Oh, I forgot to mention, I implemented a <code>get_absolute_url</code>
method in my <code>Post</code> model. This is the preferred way to
specify the url to the detail view of my <code>Post</code> object.
The code for the method is shown below:</p>
<code>~/src/django/myblogsite/myblogapp/models.py</code>:
<pre>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)
<span style="color:red">def get_absolute_url(self):
return "/blog/%d/%02d/%s/" % (self.date_created.year,
self.date_created.month,
self.slug)</span>
def __str__(self):
return self.title
class Meta:
ordering = ["-date_created"]
class Admin:
pass</pre>
<br /><br /><b>Detail page template</b><br />
<p>Then I modified my single post, or detail page, template:</p>
<code>~/src/django/myblogsite/templates/singlepost.html</code>:
<pre>{% extends "base.html" %}
<span style="color:red">{% load comments %}</span>
{% 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>
<span style="color:red">{% 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 %}</span>
{% endblock %}</pre>
<br /><br /><b>Create templates for comment preview and comment posted</b><br />
<p>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
<code>TemplateDoesNotExist</code> 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 <a href="http://code.djangoproject.com/wiki/UsingFreeComment">the
wiki</a>. 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 <code>comments</code> in my <code>templates</code> directory.
</p>
<code>~/src/django/myblogsite/templates/comments/free_preview.html</code>:
<pre style="height: 200px; overflow:auto; color:red"><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></pre>
<br /><code>~/src/django/myblogsite/templates/comments/posted.html</code>:
<pre style="color:red"><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 %}</pre>
<br /><br /><b>Add some comments</b><br />
<p>I'm done. I added some comments and saw them show up on my page.</p>
<br /><br /><b>Upload, update, restart server</b><br />
<p>I uploaded to webfaction, updated my Mercurial repository, and
restarted the Apache server. Everything's good.</p>
<p>Here is a snapshot screenshot of a detail page with some comments:</p>
<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp3.blogger.com/_lZhqNsiakm4/SHGppgh6d7I/AAAAAAAAAFg/O9zxZBvv-lg/s1600-h/version0.0.7.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp3.blogger.com/_lZhqNsiakm4/SHGppgh6d7I/AAAAAAAAAFg/O9zxZBvv-lg/s320/version0.0.7.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5220139973671024562" /></a>
<br /><p>The live site can be viewed at:
<a href="http://saltycrane.com/blog/">http://saltycrane.com/blog</a></p>
<br />Related posts:<br />
<a href="http://www.saltycrane.com/blog/2008/05/django-new-blog-project/">
Django Blog Project #1: Creating a basic blog</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/first-django-webfaction-deployment/">
Django Blog Project #2: Deploying at Webfaction</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-3-using-css-and/">
Django Blog Project #3: Using CSS and Template Inheritance</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-4-adding-post/">
Django Blog Project #4: Adding post metadata</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-5-yui-css-and/">
Django Blog Project #5: YUI CSS and serving static media</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-6-creating-standard/">
Django Blog Project #6: Creating standard blog views</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-7-adding-simple/">
Django Blog Project #7: Adding a simple Atom feed</a><br />
<br />
Django Blog Project #7: Adding a simple Atom feed
2008-06-30T00:54:00-07:00https://www.saltycrane.com/blog/2008/06/django-blog-project-7-adding-simple/<p>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
<a href="http://code.google.com/p/django-tagging/">django-tagging</a> 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.</p>
<p>Anyways, here are the steps I took to create a basic feed of my latests posts.
Refer to the <a href="http://www.djangoproject.com/documentation/0.96/syndication_feeds/#a-complex-example">Django
syndication feed framework documentation</a> for more details.
</p>
<br /><br /><strong>Modify the URLConf</strong><br />
Per the documentation, I modified my <code>~/src/django/myblogsite/urls.py</code>:
<pre>from django.conf.urls.defaults import *
from iwiwdsmi.myblogapp.views import *
from iwiwdsmi.feeds import *
<span style="color:red">feeds = {
'latest': LatestPosts,
}</span>
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')),
<span style="color:red">(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
{'feed_dict': feeds}),</span>
(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),
)</pre>
<br /><br /><strong>Create a <code>feeds.py</code> file</strong><br />
<p>I created the following file at <code>~/src/django/myblogsite/feeds.py</code>. Note,
I factored out my main title, "Sofeng's Blog 0.0.6", into the variable
<code>MAIN_TITLE</code> in my <code>views.py</code> file and used it here.
The class attributes, <code>title</code> and <code>description</code> are
strings describing the feed.</p>
<pre>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]</pre>
<br /><br /><strong>Create feed templates</strong><br />
<p>I created two feed templates which are used to display the title and body
for each post. They each only contain one line.</p>
<code>~/src/django/myblogsite/templates/feeds/latest_title.html</code>:
<pre>{{ obj.title }}</pre>
<br /><code>~/src/django/myblogsite/templates/feeds/latest_description.html</code>:
<pre>{{ obj.body }}</pre>
<br /><br /><strong>Display a link to the new feed</strong><br />
<p>I added a link to my new feed in my <code>base.html</code>
template.</p>
Excerpt from <code>~/src/django/myblogsite/templates/base.html</code>:
<pre> <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></pre>
<br /><br /><strong>Change site name</strong><br />
<p>When I first created my Django project, I installed the
<code>django.contrib.sites</code> app (by default), but I did not change
my site name, so my feed used the site <code>example.com</code> instead
of my real site. Here is how I changed my site name:</p>
<ol>
<li>I went to the admin page at
<a href="http://127.0.0.1:8000/admin/">http://127.0.0.1:8000/admin/</a>
</li>
<li>I clicked on "Sites", then on "example.com".</li>
<li>I changed "example.com" to "saltycrane.com" and clicked the "Save" button.</li>
</ol>
<br /><br /><strong>Deploy and Test</strong><br />
<p>I pushed my changes to the Webfaction server, updated my repository,
and restarted the Apache server. Then I pointed Firefox at
<a href="http://saltycrane.com/blog/">http://saltycrane.com/blog/</a>
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 <a href="http://www.google.com/reader/">Google Reader</a>.
<br />Here is a screenshot of my feed in my Google Reader:<br />
<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp3.blogger.com/_lZhqNsiakm4/SGiTFN4xkeI/AAAAAAAAAFY/as6MFBgHumI/s1600-h/version0.0.6_in_greader.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp3.blogger.com/_lZhqNsiakm4/SGiTFN4xkeI/AAAAAAAAAFY/as6MFBgHumI/s320/version0.0.6_in_greader.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5217581886145139170" /></a>
<br /><br />Extra links on redirection for my reference:<br />
<a href="https://help.webfaction.com/index.php?_m=knowledgebase&_a=viewarticle&kbarticleid=30&nav=0,19">Webfaction
knowledgebase article on redirection</a><br />
<a href="http://www.456bereastreet.com/archive/200608/redirecting_feeds_to_feedburner/">Blog
article on redirecting feeds to FeedBurner</a><br />
<br />Related posts:<br />
<a href="http://www.saltycrane.com/blog/2008/05/django-new-blog-project/">
Django Blog Project #1: Creating a basic blog</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/first-django-webfaction-deployment/">
Django Blog Project #2: Deploying at Webfaction</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-3-using-css-and/">
Django Blog Project #3: Using CSS and Template Inheritance</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-4-adding-post/">
Django Blog Project #4: Adding post metadata</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-5-yui-css-and/">
Django Blog Project #5: YUI CSS and serving static media</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-6-creating-standard/">
Django Blog Project #6: Creating standard blog views</a><br />
<a href="http://www.saltycrane.com/blog/2008/07/django-blog-project8-adding-basic/">
Django Blog Project #8: Adding basic comment functionality with Django Free Comments</a><br />
<br />
</p>
Django Blog Project #6: Creating standard blog views
2008-06-28T00:21:00-07:00https://www.saltycrane.com/blog/2008/06/django-blog-project-6-creating-standard/<p>My <a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-5-yui-css-and/">
last django post</a> actually got a little bit of publicity-- a mention on
<a href="http://www.reddit.com/r/Python/">Python reddit</a>, and a mention in the
<a href="http://yuiblog.com/blog">Yahoo User Interface blog</a>
<a href="http://yuiblog.com/blog/2008/06/20/wild-20080620/">
<em>In the Wild for June 20</em></a> 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.</p>
<p>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.</p>
<p><em>Warning</em>: 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.
</p>
<p>For a much better example of a Django blog application, see
<a href="http://adam.gomaa.us/">Adam Gomaa</a>'s Django blog site
<a href="http://hg.gomaa.us/agdj/">source code repository</a>. Though
I don't understand everything in there, I've referenced it a number of
times while creating my blog site. <em>Update 2008-06-29</em>: I just
discovered that <a href="http://www.b-list.org/about/">James Bennett</a>,
a Django contributer, also has
<a href="http://code.google.com/p/coltrane-blog/source/browse">blog
application source code available online</a>. Seeing as he
has <a href="http://www.apress.com/book/view/1590599969">written a
book on Django</a>, this <sarcasm>might</sarcasm> be a
good place to look as well.
</p>
<br /><br /><strong><code>models.py</code></strong><br />
<p>In my <code>models.py</code> file, I added a slug field to store the
last part of the url and a method called <code>get_tag_list</code>
to return the tags for the post as a list.</p>
<code>~/src/django/myblogsite/myblogapp/models.py</code>:
<pre>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</pre>
<br /><br /><strong><code>views.py</code></strong><br />
<p>In my <code>views.py</code> 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.</p>
<code>~/src/django/myblogsite/myblogapp/views.py</code>:
<pre style="height: 200px; overflow: auto;">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</pre>
<br /><br /><strong>Templates</strong><br />
I made the following template changes:
<ul>
<li>I modified the sidebar in <code>base.html</code> to display an archive index
and a tag index.</li>
<li>I replaced <code>frontpage.html</code> with a more generic
<code>listpage.html</code> template used for displaying the frontpage, archives,
and tag views.</li>
<li>I added a <code>singlepost.html</code> template for displaying a single
post view.</li>
<li>In all the templates, I created links to navigate among the different
views.</li>
</ul>
<code>~/src/django/myblogsite/templates/base.html</code>:
<pre style="height: 200px; overflow: auto;"><!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></pre>
<code>~/src/django/myblogsite/templates/listpage.html</code>:
<pre style="height: 200px; overflow: auto;">{% 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 %}</pre>
<code>~/src/django/myblogsite/templates/singlepost.html</code>:
<pre style="height: 200px; overflow: auto;">{% 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 %}</pre>
<br /><br /><strong><code>urls.py</code></strong><br />
<p>As the last step in my bottom-up approach, I modified my <code>urls.py</code>
and <code>urls_webfaction.py</code>.</p>
<code>~/src/django/myblogsite/urls_webfaction.py</code>:
<pre>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),
)</pre>
<br /><br /><strong>Finish</strong><br />
<p>I uploaded, updated, and restarted the Apache server as usual.</p>
<br />Here is a snapshot screenshot of the site:<br />
<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp3.blogger.com/_lZhqNsiakm4/SGXmzRHzpNI/AAAAAAAAAFQ/AVKA5d4I69A/s1600-h/version0.0.5.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp3.blogger.com/_lZhqNsiakm4/SGXmzRHzpNI/AAAAAAAAAFQ/AVKA5d4I69A/s320/version0.0.5.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5216829511822451922" /></a>
<br />The live site is located at:
<a href="http://saltycrane.com/blog/">http://saltycrane.com/blog/</a><br />
<br />Related posts:<br />
<a href="http://www.saltycrane.com/blog/2008/05/django-new-blog-project/">
Django Blog Project #1: Creating a basic blog</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/first-django-webfaction-deployment/">
Django Blog Project #2: Deploying at Webfaction</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-3-using-css-and/">
Django Blog Project #3: Using CSS and Template Inheritance</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-4-adding-post/">
Django Blog Project #4: Adding post metadata</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-5-yui-css-and/">
Django Blog Project #5: YUI CSS and serving static media</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-7-adding-simple/">
Django Blog Project #7: Adding a simple Atom feed</a><br />
<a href="http://www.saltycrane.com/blog/2008/07/django-blog-project8-adding-basic/">
Django Blog Project #8: Adding basic comment functionality</a><br />
<br />
Django Blog Project #5: YUI CSS and serving static media
2008-06-16T00:04:00-07:00https://www.saltycrane.com/blog/2008/06/django-blog-project-5-yui-css-and/<p>I wrote about adding some CSS to my new blog in my
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-3-using-css-and/">
post #3</a>. However, I got the CSS layout code from a book that
was a couple of years old (<em>CSS Cookbook, 2006</em>). 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.</p>
<p>I happened to notice
that the <a href="http://www.djangobook.com/">Django Book</a> used YUI style sheets
for styling its online book. The
<a href="http://developer.yahoo.com/yui/">Yahoo User Interface Library</a> (YUI) consists
mostly of a Javascript Library, but it also includes "several core CSS resources".
Doing a search among Django blogs using
<a href="http://www.google.com/coop/cse?cx=012993260288651340677%3A3fwy29agfd4">
django blog search</a> revealed that YUI was a pretty popular choice for CSS among
Django users. Watching the 42 minute
<a href="http://video.yahoo.com/watch/1373808">YUI CSS introductory video</a> 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.</p>
<p>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.</p>
<br /><b>Setup YUI-based template</b><br />
<p>In my <code>base.html</code> template file, I removed
all the CSS in between the <code><style></style></code> 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, <code>mystyle.css</code>.
In the body section, I added a number of <code>div</code> 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 <a href="http://www.adobe.com/products/dreamweaver/">Dreamweaver</a>.
</p>
<code>~/src/django/myblogsite/templates/base.html</code>:
<pre><!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></pre>
<br /><b>Create local mystyle.css stylesheet</b><br />
<p><code>mystyle.css</code> 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 <code>browser.css</code>
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
<code>~/src/django/myblogsite/media/css/mystyle.css</code>.
</p>
<br /><b>Serve static CSS media files (development server)</b><br />
<p>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 (<code>python manage.py
runserver</code>) serve the static media files. To do this, I used
<a href="http://www.djangoproject.com/documentation/0.96/static_files/">these instructions
from Django</a>.
This amounted to adding a new line to my URLConf (<code>urls.py</code>).
</p>
<code>~/src/django/myblogsite/urls.py</code>:
<pre>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<path>.*)$', 'django.views.static.serve', {'document_root': '/home/sofeng/src/django/myblogsite/media'}),
)</path></pre>
<br /><b>Serve static CSS media files (Webfaction server)</b><br />
<p>To serve my static CSS file at Webfaction, I followed
<a href="https://help.webfaction.com/index.php?_m=knowledgebase&_a=viewarticle&kbarticleid=98&nav=0,24">
these Webfaction knowledgebase instructions</a>:</p>
<ol>
<li>I went to the <a href="https://panel.webfaction.com/">Webfaction Control Panel</a></li>
<li>Under the ">Domains/websites" menu, I selected "Applications"</li>
<li>I selected the icon with the plus to add a new App.</li>
<li>I filled in the following fields:<br />
"Name": myblogsite_media
"App type": Symbolic link
"Extra info": /home/sofeng/webapps/django/myblogsite/media
</li>
<li>Under the ">Domains/websites" menu, I selected "Websites"</li>
<li>I clicked on the pencil icon to edit my website</li>
<li>Under "Site apps", I selected the icon with the plus to add a new site app.</li>
<li>I selected "myblogsite_media" as the App and entered <code>/site_media</code> as the
"URL path".</li>
</ol>
<p>In my <code>settings_webfaction.py</code> file I updated the <code>MEDIA_ROOT</code> and
<code>MEDIA_URL</code> variables. (Did I forget to mention I created a separate settings
file for Webfaction. I point to the <code>settings_webfaction.py</code> file in my
<code>~/webapps/django/apache2/conf/httpd.conf</code>.) I also created a new file,
<code>urls_webfaction.py</code> that doesn't have the above modification and I point to
this URLConf in my <code>settings_webfaction.py</code>. I know there has got to be a
better way to do this, but my tired brain hasn't figured it out yet.
</p>
In <code>~/src/django/myblogsite/settings_webfaction.py</code>, I modified the following lines:
<pre>MEDIA_ROOT = '/home/sofeng/webapps/django/myblogsite/media/'
MEDIA_URL = '/site_media/'
ROOT_URLCONF = 'myblogsite.urls_webfaction'</pre>
<br /><b>Upload project and deploy</b><br />
On my local machine:
<pre>$ pushwebf</pre>
On Webfaction machine:
<pre>$ hg update -C
$ ~/webapps/django/apache2/bin/restart</pre>
<br />Here is a snapshot screenshot of the site:<br />
<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp0.blogger.com/_lZhqNsiakm4/SFYQ6G9AGiI/AAAAAAAAAFI/vqAZqgeVQ08/s1600-h/version0.0.4.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp0.blogger.com/_lZhqNsiakm4/SFYQ6G9AGiI/AAAAAAAAAFI/vqAZqgeVQ08/s320/version0.0.4.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5212372209212856866" /></a>
The live site can be viewed at: <a href="http://saltycrane.com/blog/">http://saltycrane.com/blog/</a><br />
<br />Related posts:<br />
<a href="http://www.saltycrane.com/blog/2008/05/django-new-blog-project/">
Django Blog Project #1: Creating a basic blog</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/first-django-webfaction-deployment/">
Django Blog Project #2: Deploying at Webfaction</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-3-using-css-and/">
Django Blog Project #3: Using CSS and Template Inheritance</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-4-adding-post/">
Django Blog Project #4: Adding post metadata</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-6-creating-standard/">
Django Blog Project #6: Creating standard blog views</a><br />
<br />
Django Blog Project #4: Adding post metadata
2008-06-10T23:24:00-07:00https://www.saltycrane.com/blog/2008/06/django-blog-project-4-adding-post/<p>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 <code><hr></code> tags. The next step is to add some
post metadata, such as a title, date of creation and tags.</p>
<br /><b>Modify the <code>Post</code> model</b><br />
<p>To create this new metadata, I first modified the <code>Post</code>
model in <code>~/src/django/myblogsite/myblogapp/models.py</code>:</p>
<pre>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</pre>
<p>I added new attributes: <code>title</code>, <code>date_created</code>,
and <code>tags</code>. The <code>__str__</code> method is used
to identify a <code>Post</code> instance by its title. This is
useful when working in the Administration page. The <code>class Meta:</code>
is used to reverse sort the Posts by "id".</p>
<p><em>Correction 7/6/2008</em>: For the Post's body field, I previously
used the line: <code>body = models.CharField(maxlength=999999)</code>.
However, thanks to <a href="http://mylesbraithwaite.com/">Myles</a>'s
comment below, I've changed this to use the more appropriate <code>TextField</code>.</p>
<br /><b style="font-size:130%"><code>python manage.py syncdb</code></b><br />
<p>Whoops, running <code>python manage.py syncdb</code> 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 <a href="http://bp1.blogger.com/_lZhqNsiakm4/SE4dA5B21QI/AAAAAAAAAE4/Ek3yuLldw0Q/s1600-h/version0.0.2.png">
much in there</a> anyways.) For your reference,
the section <em>Making Changes to a Database Schema</em>
in <a href="http://www.djangobook.com/en/1.0/chapter05/">Chapter 5 of
The Django Book</a> describes how to solve my problem without
replacing my database.</p>
<pre>$ 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: my@email.address
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.</pre>
<p>Oh yeah, I forgot gotta recreate my superuser.</p>
<br /><b>Modify <code>views.py</code> slightly</b><br />
<p>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 <code>Post</code> 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 <code>Post</code> object. (I also
factored out my blog version number from the <code>base.html</code>
template.)</p>
<code>~/src/django/myblogsite/myblogapp/views.py</code>:
<pre>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'})</pre>
<em>Correction 7/6/2008</em>: I previously had <code>from myblogapp.models
import Post</code> on the second line. This works, but is inconsistent
with my <code>urls.py</code> below and can (and did for me) cause
subtle errors in the future. I corrected the line to read:
<code>from myblogsite.myblogapp.models import Post</code>.<br />
<br /><b>Modify my <code>frontpage.html</code> template</b><br />
<p>This is pretty self-explanatory. I access the the <code>Post</code>
object's attributes using the "<code>.</code>" (dot).</p>
<code>~/src/django/myblogsite/templates/frontpage.html</code>:
<pre>{% 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 %}</pre>
<br /><b>A little CSS</b><br />
<p>In between the <code><style></code> tags in <code>
~/src/django/myblogsite/templates/base.html</code>:
<pre>.post_footer {
font-family: Verdana, Arial, sans-serif;
font-size:70%;
}
hr {
border: 0;
color: gray;
background-color: gray;
height: 1px;
}</pre>
<br /><b>Start development server and add a couple posts</b><br />
</p><p>I started the development server and added a couple of posts in the Admin site:</p>
<ul>
<li>
<pre>$ cd ~/src/django/myblogsite
$ python manage.py runserver</pre>
</li>
<li>I visited <code>http://127.0.0.1:8000/admin</code>, logged in, and added a couple posts.
</li>
</ul>
<br /><b>Upload to Webfaction server</b><br />
<p><code>pushwebf</code> is my alias for
<code>hg push --remotecmd /home/sofeng/bin/hg ssh://sofeng@sofeng.webfactional.com/webapps/django/myblogsite</code>
</p>
<pre>$ pushwebf
sofeng@sofeng.webfactional.com's password:
pushing to ssh://sofeng@sofeng.webfactional.com/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</pre>
<br /><b>Deploy</b><br />
Logged into Webfaction:
<pre>$ cd ~/webapps/django/myblogsite
$ hg update -C
9 files updated, 0 files merged, 4 files removed, 0 files unresolved
$ ~/webapps/django/apache2/bin/restart</pre>
<p>Here is a snapshot screenshot of version 0.0.3</p>
<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp3.blogger.com/_lZhqNsiakm4/SE9wUjJ4HDI/AAAAAAAAAFA/T6eDSpQQPGk/s1600-h/version0.0.3.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp3.blogger.com/_lZhqNsiakm4/SE9wUjJ4HDI/AAAAAAAAAFA/T6eDSpQQPGk/s320/version0.0.3.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5210506792227314738" /></a>
<p>The live site can be viewed at <a href="http://saltycrane.com/blog/">http://saltycrane.com/blog/</a></p><br />
<br />Related posts:<br />
<a href="http://www.saltycrane.com/blog/2008/05/django-new-blog-project/">
Django Blog Project #1: Creating a basic blog</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/first-django-webfaction-deployment/">
Django Blog Project #2: Deploying at Webfaction</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-3-using-css-and/">
Django Blog Project #3: Using CSS and Template Inheritance</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-5-yui-css-and/">
Django Blog Project #5: YUI CSS and serving static media</a><br />
<br />
Django Blog project #3: Using CSS and Template Inheritance
2008-06-07T17:50:00-07:00https://www.saltycrane.com/blog/2008/06/django-blog-project-3-using-css-and/<p>Version 0.0.1 of my new blog had no style. For 0.0.2, I added a little
CSS to my Django template to make it look a little better.</p>
<b>Create new Django templates</b><br />
<p>I created two new Django templates, <code>base.html</code> and
<code>frontpage.html</code>. <code>base.html</code> contains
all the HTML and CSS that is common to all the web pages that
will be on this site. <code>frontpage.html</code>
is the template for the front page of my new blog. It uses the
object-oriented technique of <em>extending</em> my <code>base.html</code>
template. It uses the boilerplate code in <code>base.html</code>
and then fills in sections with content specific to the front page.
It is similar to server side includes, but more powerful because
specific content can be inserted anywhere instead of just at the
beginning or end. Django does have an
<em>include</em> mechanism which acts like server side includes,
but the <em>extension</em> method
(also called <em>Template Inheritance</em>) is the preferred way
of doing things.</p>
<br />Here is the code for <code>frontpage.html</code>:<br />
<pre>{% extends "base.html" %}
{% block main %}
{% for post in post_list %}
{{ post }}
<hr />
{% endfor %}
{% endblock %}</pre>
Here is the code for <code>base.html</code>:
<pre><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
body {
margin: 0;
padding: 0;
font-family: Georgia, Times, "Times New Roman", serif;
color: black;
width: 600px;
border-right: 1px solid black;
}
#header {
background-color: #666;
border-bottom: 1px solid #333;
}
#sidebarLeft {
float: left;
width: 160px;
margin-left: 10px;
padding-top: 1em;
}
#mainRight {
padding-top: 1em;
margin: 0 2em 0 200px;
}
#footer {
clear: both;
background-color: #ccc;
padding-bottom: 1em;
border-top: 1px solid #333;
padding-left: 200px;
}
h2.sidebar {
font-size: 120%;
}
</style>
<title>{% block title %}Sofeng's Blog Version 0.0.2{% endblock %}</title>
</head>
<body>
<div id="header">
<h1>Sofeng's Blog Version 0.0.2</h1>
</div>
<div id="sidebarLeft">
{% block sidebar %}
<h2 class="sidebar">ABOUT</h2>
<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 id="mainRight">
{% block main %}{% endblock %}
</div>
<div id="footer">
A <a href="http://www.djangoproject.com/">Django</a> site.
</div>
</body>
</html></pre>
<br /><b>Modify view to use new template</b><br />
<p>To use the new template, I created a new view called <code>frontpage</code>
in <code>~/src/django/myblogsite/myblogapp/views.py</code>:</p>
<pre>from django.shortcuts import render_to_response
from myblogsite.myblogapp.models import Post
def frontpage(request):
posts = Post.objects.all()
post_body_list = [post.body for post in posts]
return render_to_response('frontpage.html',
{'post_list': post_body_list})</pre>
<em>Correction 7/6/2008</em>: I previously had <code>from myblogapp.models
import Post</code> on the second line. This works, but is inconsistent
with my <code>urls.py</code> below and can (and did for me) cause
subtle errors in the future. I corrected the line to read:
<code>from myblogsite.myblogapp.models import Post</code>.<br />
<br /><b>Map an URL to the new view</b><br />
<p>To complete the change, I mapped the url <code>/blog</code> to the new
<code>frontpage</code> view in <code>~/src/django/myblogsite/urls.py</code>:
<pre>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),
)</pre>
<br /><b>Upload project to Webfaction server</b><br />
</p><p>I updated my <a href="http://www.saltycrane.com/blog/2008/06/first-django-webfaction-deployment/">
previous post</a> to include a section on installing Mercurial.
Here are my steps for pushing my changes to the server. It's a little more
complicated than it should be because I was mucking with the repository,
but future pushes should be much easier.</p>
<pre>$ hg pull -u --remotecmd /home/sofeng/bin/hg ssh://sofeng@sofeng.webfactional.com/webapps/django/myblogsite
sofeng@sofeng.webfactional.com's password:
pulling from ssh://sofeng@sofeng.webfactional.com/webapps/django/myblogsite
searching for changes
adding changesets
adding manifests
adding file changes
added 5 changesets with 4 changes to 5 files (+1 heads)
not updating, since new heads added
(run 'hg heads' to see heads, 'hg merge' to merge)
$ hg merge
local changed templates/base.html which remote deleted
use (c)hanged version or (d)elete? c
3 files updated, 0 files merged, 3 files removed, 0 files unresolved
(branch merge, don't forget to commit)
$ hg commit -m 'commit after merge'
$ hg push --remotecmd /home/sofeng/bin/hg ssh://sofeng@sofeng.webfactional.com/webapps/django/myblogsite
sofeng@sofeng.webfactional.com's password:
pushing to ssh://sofeng@sofeng.webfactional.com/webapps/django/myblogsite
searching for changes
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 3 changesets with 5 changes to 6 files</pre>
<br /><b>Deploy</b><br />
<p>With my cleanup of the mercurial repository, I didn't have to make any
file changes. I just updated the working directory of my project using
<code>hg update -C</code> and restarted the Apache web server.</p>
<pre>$ cd ~/webapps/django/myblogsite
$ hg update -C
5 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ ~/webapps/django/apache2/bin/restart</pre>
<p>Now, pointing my browser at <a href="http://saltycrane.com/blog/">
http://saltycrane.com/blog/</a>, shows new version 0.0.2 of my blog!</p>
<p>Here is a snapshot screenshot of my blog version 0.0.2:</p>
<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp1.blogger.com/_lZhqNsiakm4/SE4dA5B21QI/AAAAAAAAAE4/Ek3yuLldw0Q/s1600-h/version0.0.2.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp1.blogger.com/_lZhqNsiakm4/SE4dA5B21QI/AAAAAAAAAE4/Ek3yuLldw0Q/s320/version0.0.2.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5210133720060056834" /></a>
<br />Related posts:<br />
<a href="http://www.saltycrane.com/blog/2008/05/django-new-blog-project/">
Django Blog Project #1: Creating a basic blog</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/first-django-webfaction-deployment/">
Django Blog Project #2: Deploying at Webfaction</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-4-adding-post/">
Django Blog Project #4: Adding post metadata</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-5-yui-css-and/">
Django Blog Project #5: YUI CSS and serving static media</a><br />
<br />
Django Blog Project #2: Django Webfaction deployment
2008-06-06T11:12:00-07:00https://www.saltycrane.com/blog/2008/06/first-django-webfaction-deployment/<p>I mentioned in my <a href="http://www.saltycrane.com/blog/2008/05/django-new-blog-project/">
previous Django post</a> that I'd write about hosting my new Django blog site on Webfaction.
As promised, here it is.</p>
<p>I had some problems deploying my first site at Webfaction. First I got an error about pysqlite.
I tried using Postgresql, and then I got an error about something else when trying to do a <code>
python2.5 manage.py syncdb</code>. I think that they didn't set me up with a current
Django installation. Luckily the support was prompt and helpful. I ended up reinstalling
the Django application from the Webfaction Control Panel. Here are the steps I took
to deploy my less than fledgling site.</p>
<br /><b>Sign up for a shared hosting plan at Webfaction</b><br />
<p>I signed up for Webfaction's lowest plan at $9.50/month with no commitment.
<a href="http://www.webfaction.com/services/hosting">Here is their pricing plans.</a>
</p>
<br /><b>Play with new account</b><br />
<p>I got a welcome email within 24 hours with the details about my account.
<code>sofeng.webfactional.com</code> was a domain name which they provided. Visiting this
address in my browser showed my the <em>It worked!</em> Django new project page. I also ssh'd into
my shell account by using <code>ssh sofeng.webfactional.com</code>. The OS is
CentOS5 which, I think, is based on Red Hat. It was a little different than the
Ubuntu that I'm used to. I copied some of my configuration files over to make
me feel more at home. On my home machine:
<pre>$ scp .bashrc sofeng@sofeng.webfactional.com:
$ scp .screenrc sofeng@sofeng.webfactional.com:
$ scp .emacs sofeng@sofeng.webfactional.com:</pre>
</p>
<br /><b>Reinstall Django</b><br />
<p>Note, if your Webfaction account is set up properly with Django, you shouldn't
need to take these steps. Like I mentioned earlier, I had some problems with my
Django installation. So
I ended up reinstalling Django. It's not as hard as it sounds-- just a few clicks
in the Control Panel.</p>
<ol>
<li>In my browser, I went to the Webfaction Control Panel at
<a href="https://panel.webfaction.com/">https://panel.webfaction.com/</a>.
</li>
<li>Under "> Domains / websites", I selected "Applications".</li>
<li>I removed my current Django installation by clicking the icon with the minus sign.</li>
<li>I clicked the icon with the plus sign to add a new application.</li>
<li>I filled in the following information:
<blockquote>"Name:": django<br />
"App type:": Django 0.96.2/mod_python 3.3.1/Python 2.5</blockquote>
Then I clicked the "Create" button.
</li>
<li>Finally, I needed to specify which URL path I want to use with my Django application.<br />
Under "> Domains / websites", I selected "Websites".
</li>
<li>I selected the icon with the pencil to edit the website settings.</li>
<li>In the "Site apps:" section, I selected the icon with the plus sign to add a new App and path.
I selected "django" as my "App:" and entered "/" (without the quotes) in the "URL path" field.
Then I clicked "Update".
</li>
<li>After a couple minutes, I was able to view the Django <em>It worked!</em> page at
<code>http://sofeng.webfactional.com</code>.
</li>
</ol>
<br /><b>Upload my project</b><br />
<p>
<ol>
<li>The welcome message recommends using <code>sftp</code>, but I used <code>rsync</code> instead because I'm
more familiar with it:
<pre>sofeng@home:~ $ rsync -avz ~/src/django/myblogsite sofeng@sofeng.webfactional.com:webapps/django</pre>
</li>
</ol>
<br /><b>Deploy</b><br />
<ol>
<li>I ssh'd into my webfaction shell account:
<pre>sofeng@home:~ $ ssh sofeng@sofeng.webfactional.com</pre>
</li>
<li>I set the PYTHONPATH. (I actually put this in my .bash_profile)
<pre>sofeng@web36:~ $ export PYTHONPATH=$HOME/webapps/django</pre>
</li>
<li><pre>sofeng@web36:~ $ cd ~/webapps/django/myblogsite</pre></li>
<li>I tried to do a <code>python2.5 manage.py syncdb</code>, but got a database error:
<pre style="height: 40; overflow:auto">Traceback (most recent call last):
File "manage.py", line 11, in <module>
execute_manager(settings)
File "/home/sofeng/webapps/django/lib/python2.5/django/core/management.py", line 1672, in execute_manager
execute_from_command_line(action_mapping, argv)
File "/home/sofeng/webapps/django/lib/python2.5/django/core/management.py", line 1571, in execute_from_command_line
action_mapping[action](int(options.verbosity), options.interactive)
File "/home/sofeng/webapps/django/lib/python2.5/django/core/management.py", line 504, in syncdb
cursor = connection.cursor()
File "/home/sofeng/webapps/django/lib/python2.5/django/db/backends/sqlite3/base.py", line 58, in cursor
self.connection = Database.connect(**kwargs)
sqlite3.OperationalError: unable to open database file</module></pre>
</li>
<li>I needed to edit my <code>settings.py</code> file. I changed the following lines:
<pre>DATABASE_NAME = '/home/sofeng/webapps/django/myblogsite/mydatabase.sqlite3'
TEMPLATE_DIRS = (
'/home/sofeng/webapps/django/myblogsite/templates',
)</pre>
</li>
<li>I tried <code>python2.5 manage.py syncdb</code> again:
<pre>$ python2.5 manage.py syncdb
Loading 'initial_data' fixtures...
No fixtures found.</pre>
It worked.
</li>
<li>The next step was to set up the Apache configuration.
<pre>$ cd ~/webapps/django/apache2/conf</pre>
</li>
<li>I edited the following line in <code>httpd.conf</code>:
<pre> SetEnv DJANGO_SETTINGS_MODULE myblogsite.settings</pre>
</li>
<li>Then I restarted the Apache server:
<pre>$ ~/webapps/django/apache2/bin/restart</pre>
</li>
<li>In my browser, I went to <code>http://sofeng.webfactional.com/myview1</code>,
but got an error:
<pre>ImportError at /myview1/
No module named blog.models
Request Method: GET
Request URL: http://sofeng.webfactional.com/myview1/
Exception Type: ImportError
Exception Value: No module named blog.models
Exception Location: /home/sofeng/webapps/django/lib/python2.5/django/core/urlresolvers.py in _get_urlconf_module, line 177</pre>
</li>
<li>I guess I need my project on the Python path. I edited the following line in <code>httpd.conf</code>:
<pre> PythonPath "['/home/sofeng/webapps/django/myblogsite', '/home/sofeng/webapps/django', '/home/sofeng/webapps/django/lib/python2.5'] + sys.path"</pre>
</li>
<li>Then I went back to <code>http://sofeng.webfactional.com/myview1</code> and saw my page. Success!
</li>
</ol>
<br /><b>Install Mercurial</b><br />
</p><p>Update 6/7/2008: By default Webfaction doesn't come with Mercurial installed, but they do allow
you to do a user installation. I decided it would be useful to have Mercurial so I could
pull and merge changes with greater control. Here are my steps.This will do a user-wide
(as opposed to system-wide) installation of Mercurial in <code>~/bin</code> and <code>~/lib</code>.</p>
<ol>
<li><pre>sofeng@home:~/incoming $ wget http://www.selenic.com/mercurial/release/mercurial-1.0.1.tar.gz</pre></li>
<li><pre>sofeng@home:~/incoming $ scp mercurial-1.0.1.tar.gz sofeng@sofeng.webfactional.com:</pre></li>
<li><pre>sofeng@web36:~ $ mv mercurial-1.0.1.tar.gz ~/tmp</pre></li>
<li><pre>$ cd tmp</pre></li>
<li><pre>$ tar zxvf mercurial-1.0.1.tar.gz</pre></li>
<li><pre>$ cd mercurial-1.0.1</pre></li>
<li><pre>$ make install-home</pre></li>
<li><pre>$ export PYTHONPATH=$HOME/lib/python:$PYTHONPATH</pre>
Note: you will need to put this in your <code>.bashrc</code> instead of your
<code>.bash_profile</code> because Mercurial only executes <code>.bashrc</code>
on a remote <code>hg push</code> by default.
</li>
<li><pre>$ hg version
Mercurial Distributed SCM (version 1.0.1)
Copyright (C) 2005-2008 Matt Mackall <mpm> and others
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</mpm></pre>
</li>
</ol>
<br /><b>Other Errors</b><br />
<ul>
<li>No module name django.core.management<br />
<pre>$ python manage.py syncdb
Traceback (most recent call last):
File "manage.py", line 2, in ?
from django.core.management import execute_manager
ImportError: No module named django.core.management</pre>
You need to set the <code>PYTHONPATH</code>:
<pre>$ export PYTHONPATH=$HOME/webapps/django</pre>
You may get this if you use <code>python manage.py syncdb</code> instead of
<code>python2.5 manage.py syncdb</code>.
</li>
</ul>
<br />Related posts:<br />
<a href="http://www.saltycrane.com/blog/2008/05/django-new-blog-project/">
Django Blog Project #1: Creating a basic blog</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-3-using-css-and/">
Django Blog Project #3: Using CSS and Template Inheritance</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-4-adding-post/">
Django Blog Project #4: Adding post metadata</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-5-yui-css-and/">
Django Blog Project #5: YUI CSS and serving static media</a><br />
<br />
Django Blog Project #1: Creating a basic blog
2008-05-09T23:41:00-07:00https://www.saltycrane.com/blog/2008/05/django-new-blog-project/<p>It's been a while since my
<a href="http://www.saltycrane.com/blog/2007/11/django-project-3-creating-models/">
last post</a> on Django. I became very busy <del>but also found the
<a href="http://www.djangoproject.com/documentation/0.96/tutorial01/">Django
tutorial</a> to be somewhat dry</del><ins>and also somewhat lazy*</ins>.
Luckily, the official
<a href="http://www.djangobook.com/">Django Book</a> was published during this
time and it is more interesting to read. This post will be a very brief summary
of the first
6 chapters of the Django Book as I apply it towards the creation of a new blog
website. I highly recommend reading the book (I forgot to mention it is available
free online). Then after reading the first six chapters, I hope this post can
serve as kind of a refresher on how to put everything together.</p>
<p>As I mentioned, I decided to create my own blog site as my first Django project.
I know it is not the most original idea in the world, but I thought it would be
useful and a good learning experience. The following steps are my first cut
at my new blog (dubbed 0.0.1) and basically just document the basic concepts
of Django without providing much actual functionality.</p>
<p>I develop a model, a
template, and a view in accordance with Django's MTV (see also
<a href="http://en.wikipedia.org/wiki/Model-view-controller">MVC</a>)
development pattern. The model
is of a blog post (aptly named <code>Post</code>) and contains only one
attribute, the post body data (i.e. the actual text of the post). I should
add in other data such as the title, date, tags, etc. But in order to keep
things simple this first time around, it just has the post body. The model
is connected to a SQLite database and is updated using Django's excellent
admin interface. Finally, the model data is combined with a very basic template
which just displays a title (<em>My New Blog Version 0.0.1</em>), and all
the blog post bodies separated by a <hr>. Like I said, it's not
very useful at this point, but I think I understand the basic concepts and how
to put everything together much better now. The next step in my Django development
will be to create some
more interesting templates and views and add more useful data like titles and
dates.</p>
<p>I also have a couple of related plans:
</p><ul>
<li>Set up hosting: I've decided to use <a href="http://www.webfaction.com/">
WebFaction</a> for my hosting but I need to set up and upload my new,
almost-website there.
This will probably be the subject of my next Django post.
</li>
<li>Copy my Blogger posts over to my new site. I've already figured out how to
use <a href="http://www.crummy.com/software/BeautifulSoup/">Beautiful Soup</a>
to screen scrape my Blogger posts and import them into SQLite. Likely I will do
this further on in the process.
</li>
</ul>
<p></p>
<p>Here are the steps I took for my first cut at my new blog. Note, I'm
running on Ubuntu so that's why I have the <code>$</code> bash prompt
and use <code>/home/sofeng</code> paths in my examples.</p>
<p>*<em>Update 2008-10-09</em>: I realize my assessment of the tutorial
might have sounded critical-- in actuality, since I was job searching at the time, I was just
trying to avoid sounding lazy. For the record, I find the Django documentation to
be excellent and one of the highlights of the project.</p>
<h5>Create a new project</h5>
<p>The first thing to do after installing Django is to create a new project.
Luckily, it takes just one command to create the project.</p>
<ol>
<li>Create a new project
<pre>$ cd ~/src/django
$ django-admin.py startproject myblogsite</pre>
</li>
<li>Take a look at the site using the development server
<pre>$ python manage.py runserver</pre>
Then go to <a href="http://127.0.0.1:8000/">http://127.0.0.1:8000</a>
</li>
</ol>
<h5Set up the Django admin interface</h5>
<p>At first I thought the admin interface was kind of boring. However,
for my blog site, I will use the admin interface to enter new blog
posts.</p>
<ol>
<li>Edit <code>myblogsite/settings.py</code> to add the admin application to the
list of installed apps:
<pre>INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.admin',
)</pre>
</li>
<li>Install database tables for the admin interface:
<pre>$ python manage.py syncdb</pre>
At this point I was asked to create a superuser to log into the
admin interface. I answered "yes" and filled in the appropriate information.
<pre style="overflow: auto; height: 60px;">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
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: sofeng@sofeng.com
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.</pre>
</li>
<li>Edit <code>myblogsite/urls.py</code> to include the admin url.
<pre>from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^admin/', include('django.contrib.admin.urls')),
)</pre>
</li>
<li>Run the development server:
<pre>$ python manage.py runserver</pre>
Then go to <a href="http://127.0.0.1:8000/admin">http://127.0.0.1:8000/admin</a>
Log in and take a look around.
</li></ol>
<h5>Set up the SQLite3 database</h5>
<p>I chose SQLite because it is a lightweight, simple alternative to MySQL or
PostgreSQL. This makes it great for a development website.</p>
<ol>
<li>Edit the following section in the <code>myblogsite/settings.py</code>
file:
<pre>DATABASE_ENGINE = 'sqlite3'
DATABASE_NAME = '/home/sofeng/src/django/myblogsite/mydatabase.sqlite3'</pre>
The rest of the <code>DATABASE_</code> variables are not used
with SQLite.
</li>
<li>Test out the database configuration:
Run the shell:
<pre>$ python manage.py shell</pre>
Then type these commands:
<pre>>>> from django.db import connection
>>> cursor = connection.cursor()</pre>
If nothing happens, all is good.
See Table 5-2 in <a href="http://www.djangobook.com/en/1.0/chapter05/">
Chapter 5 of the Django Book</a> common database configuration error
messages.
</li>
</ol>
<h5>Create an App</h5>
<p>I think of an "app" as a piece of specific functionality of a website, whereas a
project corresponds to a particular website. There can be many apps in a project.
Also, apps can be used in more than one project. For more information about the
differences between projects and apps see <a href="http://www.djangobook.com/en/1.0/chapter05/">
Chapter 5 of the Django Book</a>.</p>
<ol>
<li>Create an app
<pre>$ cd ~/src/django/myblogsite
$ python manage.py startapp myblogapp</pre>
</li>
</ol>
<h5>Create a Model</h5>
<p>I created one model, the <code>Post</code> model. A model roughly
corresponds to a SQL table. And each attribute in that model corresponds to a table row.
I added the <code>class Admin:</code> so that my Post model would show up
in the Admin interface (where I can insert the data).</p>
<p></p>
<ol>
<li>Edit <code>myblogsite/myblogapp/models.py</code> to look like
the following:
<pre>from django.db import models
class Post(models.Model):
body = models.TextField()
# in the future I will add these other attributes
# title = models.CharField(maxlength=500)
# timestamp = models.CharField(maxlength=50)
# tags = models.CharField(maxlength=200)
class Admin:
pass</pre>
<em>Correction 7/6/2008</em>: For the Post's body field, I previously
used the line: <code>body = models.CharField(maxlength=999999)</code>.
However, thanks to <a href="http://mylesbraithwaite.com/">Myles</a>'s
comment in my <a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-4-adding-post/">post
#4</a>, I've changed this to use the more appropriate <code>TextField</code>.
</li>
</ol>
<h5>Install the Model</h5>
<p>After writing the Python model code, I needed to create the actual tables
in the SQLite database. The following steps include a couple of checks,
then I create the tables in the last step.</p>
<ol>
<li>Edit <code>myblogsite/settings.py</code> file again and add
the blog app to the list of installed apps:
<pre>INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'myblogsite.myblogapp',
)</pre>
</li>
<li>Try validating the model:
<pre>$ python manage.py validate</pre>
Which gives the following message:
<pre>0 errors found.</pre>
</li>
<li>Check the CREATE TABLE statements that Django will generate. Note,
the database won't be modified.
<pre>$ python manage.py sqlall myblogapp</pre>
Which yields the following:
<pre>BEGIN;
CREATE TABLE "myblogapp_post" (
"id" integer NOT NULL PRIMARY KEY,
"body" text NOT NULL
);
COMMIT;</pre>
<em>Correction 7/6/2008</em>: I've updated the results here to reflect
the correction I made to the model above.
</li>
<li>Now, actually create the tables in SQLite:
<pre>$ python manage.py syncdb</pre>
Which yields something like this:
<pre>Creating table blog_post
Loading 'initial_data' fixtures...
No fixtures found.</pre>
</li>
</ol>
<h5>Create some new data using the admin interface</h5>
<p>Now that I created the models and tied them to the admin interface,
I can start adding data using the admin interface.</p>
<ol>
<li>Start the development server again:
<pre>$ python manage.py runserver</pre>
Go to <a href="http://127.0.0.1:8000/admin">http://127.0.0.1:8000/admin</a> and log in.
</li>
<li>Under the "Blog" heading, click "Posts", then add some new posts using
"Add post" and the "Save" links. This will add data to the SQLite database.
</li>
</ol>
<h5>Create a template</h5>
<p>Now I will display the data I just created using a template and a view.
The template holds all the HTML code and some simple Django template code which
the view's Python code uses to customize the page.</p>
<ol>
<li>Create the file <code>myblogsite/templates/mytemplate.html</code> and
put the following inside:
<pre><html>
<head><title>Post</title></head>
<body>
<h1>My New Blog Version 0.0.1</h1>
{% for post in post_list %}
{{ post }}
<hr />
{% endfor %}
</body>
</html></pre>
</li>
<li>Edit <code>myblogsite/settings.py</code> again to instruct Django
where to find the template files.
<pre>TEMPLATE_DIRS = (
'/home/sofeng/src/django/myblogsite/templates',
)</pre>
Be sure to include the comma at the end.
</li>
</ol>
<h5>Create a view</h5>
<p>The view is where I will grab the data from my model and insert it into my
template.</p>
<ol>
<li>Create a new file <code>myblogsite/myblogapp/views.py</code> and put the following
inside:
<pre>from django.shortcuts import render_to_response
from myblogsite.myblogapp.models import Post
def myview(request):
posts = Post.objects.all()
post_body_list = [post.body for post in posts]
return render_to_response('mytemplate.html',
{'post_list': post_body_list})</pre>
<em>Correction 7/6/2008</em>: I previously had <code>from myblogapp.models
import Post</code> on the second line. This works, but is inconsistent
with my <code>urls.py</code> below and can (and did for me) cause
subtle errors in the future. I corrected the line to read:
<code>from myblogsite.myblogapp.models import Post</code>.
</li>
</ol>
<h5>Map an URL to the new view</h5>
<p>Finally, I map an URL to my newly created view.</p>
<ol>
<li>Edit <code>myblogsite/urls.py</code> so that it looks like:
<pre>from django.conf.urls.defaults import *
from myblogsite.myblogapp.views import myview
urlpatterns = patterns('',
(r'^admin/', include('django.contrib.admin.urls')),
(r'^myview/$', myview),
)</pre>
</li>
<li>Take a look at the new page:
Run the server:
<pre>$ python manage.py runserver</pre>
Then go to <code>http://127.0.0.1:8000/myview</code>
Visiting the url shows all the posts I entered through the admin interface. Nice.
Here is a snapshot screenshot of my new blog:<br /><br />
<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp0.blogger.com/_lZhqNsiakm4/SE4bH_WlzYI/AAAAAAAAAEo/GmhZrA-K8ms/s1600-h/version0.0.1.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp0.blogger.com/_lZhqNsiakm4/SE4bH_WlzYI/AAAAAAAAAEo/GmhZrA-K8ms/s320/version0.0.1.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5210131642993462658" /></a>
</li>
</ol>
<p>That's it for now. I tried to map out the basic steps for using Django's MTV
development pattern. Hopefully, in the future, I'll be able to add more useful
features to my new Django-powered blog.</p>
<br />Related posts:<br />
<a href="http://www.saltycrane.com/blog/2007/11/django-project-1-install/">
Install Django on Ubuntu</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/first-django-webfaction-deployment/">
Django Blog Project #2: Deploying at Webfaction</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-3-using-css-and/">
Django Blog Project #3: Using CSS and Template Inheritance</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-4-adding-post/">
Django Blog Project #4: Adding post metadata</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-5-yui-css-and/">
Django Blog Project #5: YUI CSS and serving static media</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-6-creating-standard/">
Django Blog Project #6: Creating standard blog views</a><br />
<a href="http://www.saltycrane.com/blog/2008/06/django-blog-project-7-adding-simple/">
Django Blog Project #7: Adding a simple Atom feed</a><br />
<a href="http://www.saltycrane.com/blog/2008/07/django-blog-project8-adding-basic/">
Django Blog Project #8: Adding basic comment functionality</a><br />
<br />