Monday, 22 September 2008

10. Finishing the Code-Sharing Application

This is part 10 of a series of posts on James Bennett's excellent Practical Django Projects. The table of contents and explanation can be found here

Finally, we're on the home stretch. This chapter builds on our knowledge of templates and also gives a few new examples of custom template tags, so without further ado lets finish the app.

On p188 you need to modify the save method of the bookmark model to
#cab.models.py
...
  def save(self, force_insert=False, force_update=False):
      if not self.id:
          self.date = datetime.datetime.now()
      super(Bookmark, self).save(force_insert, force_update)
The force_insert and force_update keywords are new to 1.0. I'm not sure why they are specifically required in this model but I got an error without them and certainly the documentation shows that you include them if you override the save method. I guess this means that wherever you have overridden the save method you would include these arguments as best practice, but you can take a shortcut and write it like this:
#cab.models.py
...
  def save(self, **kwargs):
      if not self.id:
          self.date = datetime.datetime.now()
      super(Bookmark, self).save(**kwargs)
This will make your code safe if any of these model save method arguments are changed or added to, so go and add them to your rating and snippet models as well.

On p191 def user_bookmarks(request) is missing request in the object_list method call:
#cab.views.bookmarks.py
...
def user_bookmarks(request):
 return object_list(request, queryset=Bookmark.objects.filter(user__pk=request.user.id),
  template_name='cab/user_bookmarks.html',
  paginate_by=20)
Also on p191 the most_bookmarked method is missing a } after params and should read:
return self.extra(select={ 'score': subquery % params },
 order_by=['-score'])
After writing up the bookmarks templatetags code make yourself a simple bookmarks list template user_bookmarks.html once you get to p198 and the start of the User Rating System.
{% for bookmark in object_list %}
 <p><a href="{{ bookmark.snippet.get_absolute_url }}">{{ bookmark.snippet.title }}</a></p>
{% endfor %}

On p204 the template code for the ratings tag:
{% load snippets %}
{% if_rated user object %}
{% get_rating user snippet as rating %}
should read
{% load snippets %}
{% if_rated user object %}
{% get_rating user object as rating %}
This is because the snippet_detail.html template generic view uses object as the default template context.

That's it. You should now have a working basic snippets application with bookmarking and ratings working, and hopefully feel more comfortable working with Django. The final chapter of the book talks about re-usable apps, and gives some code but is focused on techniques rather than a fully working app, so I won't do any notes on it. I might write one more post just to summarize my own experiences with the book and Django as someone who has gone through it without any prior Django experience, but essentially that's all I have to offer on Practical Django Projects. I hope you've enjoyed it as much as I did, and I am looking forward to James releasing his sample code which should be the icing on the cake if you want to take any of these sample apps further.

9. Form Processing in the Code-Sharing Application

This is part 9 of a series of posts on James Bennett's excellent Practical Django Projects. The table of contents and explanation can be found here

The chapter is the first serious coverage of forms. As Django 1.x has been released there is no old and newforms just forms, so on p171 and p176 and wherever else from django import newforms as forms is used, just use from django import forms.

There is really nothing else to cover in this chapter as it all just works.

Friday, 19 September 2008

Mark Ramm: Django and learning from Zope 2

Mark Ramm gave an interesting talk (now up on youtube) at DjangoCon about learning from the experiences of Zope 2. As someone who hit the Z productivity wall after developing a site with Zope 2 I really appreciated what he had to say but disagree with the reasons for the decline of Zope 2 and what it means for Django. The first part of the talk he summarizes as:
"basically an argument that monolithic frameworks can lead to community fragmentation by increasing the cost of switching and by creating an attitude of “uninformed superiority” on both sides of the divide."
Well yes - I can't argue with that, but I don't think that being monolithic is necessarily Zopes problem.

Firstly, Zope was way ahead of the cgi curve, but in my opinion too far ahead, so like a lot of trailblazing products only got a niche audience for it's product, and also had to change directions a few times. Django by contrast had the advantage of being able to look at other frameworks and make some very careful design decisions which I think it ultimately is why it has become much more successful.

The biggest single design decision that zope is paying the price for is zodb.

A native object database is the most obvious solution for storing object data, but compared to sql, who in the wider world is actually willing to put their data in them. Given that no OODMS vendor has made any real dent in the sql world, it's very hard to pitch a large project which has zodb at the core. By using an ORM mapper Django made a pragmatic decision that keeps a lot of stakeholders happy, and there are a lot of people who know SQL, so if the ORM doesn't do it for you, then just bypass it and query the data directly. Zope by contrast can connect to sql databases, but it's clear zodb rules in the Zope world.

The other fundamental reason why Django is succeeding where Zope hasn't is a philosophical one. As Jacob is quoted as saying, Django is about "a way to get shit done", and from a design perspective the developers have been willing to take shortcuts to achieve that goal. Zope on the other hand has relied on others to develop the get shit done part (Grok) and focused on making Zope a framework that can compete with more complete enterprise frameworks. In other words Django has focused on a lower common demoninator.

Mark also talks about the trade-off between tightly integrated components and loosely coupled components, and it's here that Django needs to play close attention but not panic. I would argue that as long as Django does not get too far behind with any given core component, then it is much better to be able to control the direction of those components then be ahead of the game. Tight integration is one of  Djangos key competitive advantages, which can only be a weakness if one of the components gets too far behind it's key competitors.

Loose coupling I would argue can be just as problematic for the advancement of Django. Ultimately if you can just just drop one part in over another it becomes like a wine-tasting convention, with every  wine maker arguing that their wine is the definitive representation of the grape variety when all you want is a reasonable tasting drop at a reasonable price. Common blend wines might not have the features of a more expensive drop, but it has an easy appeal for a wide audience, and can benefit from the experiences of developing the more expensive specialized wine. Django's ORM and Templates need to pay attention and learn from SQL Alchemy and Jinja, but it doesn't mean they need to be replaced by them.

The key to Django's ongoing success I think will be something quite simple but hard to replicate, and that is a benevolent group of core developers who can stick to their philosophical roots and achieve that balance between technical excellence, pragmatism, and a healthy ability to accept criticism when it's due.   That and retaining that newsroom sensibility of being able to communicate with an audience through the website, excellent documentation, and now with DjangoCon, are what should ensure a healthy future for the framework.

Monday, 15 September 2008

8. A Social Code-Sharing Site

This is part 8 of a series of posts on James Bennett's excellent Practical Django Projects. The table of contents and explanation can be found here

Building and testing the models for the snippets app is fairly straightforward. The only changes you need to make are to separate out your admin classes into the separate admin.py file which I've shown previously, so type up all the code up to p154 and register your admin classes.
#cab.admin.py
from django.contrib import admin
from cab.models import Snippet, Language

admin.site.register(Language)
admin.site.register(Snippet)

Testing the Snippets Application

Here we syncdb and add some Language and Snippet objects. When adding a Language object the language_code corresponds to the short names on the lexers page, and I guess you can pick a mime_type that fits the language. In my case I just used python as the language_code and mime_type text/x-python.

Initial Views for Snippets and Languages

Because there is no sample code it is necessary for us to actually create the templates snippet_list.html and snippet_detail.html. Because the templates are trivial my suggestion is to just create the most basic template to begin with, put all the available model variables in it, and then come back to it later if you want to pretty it up. I'll write up one very basic one for snippet_list.html as a basic example.

Before we create the template I want to mention the {% debug %} tag. This is an extremely handy tag to insert when you want to work out what environment, variables, data etc is being made available to your template. There is a catch however. The debug tag displays actual objects with html tags around them, so you don't actually see some of the information you might want to see. For example in my snippets_list.html I was looking at the 'object_list' key generated by debug and only seeing an empty list instead of 'object_list':[<Snippet: Example Snippet>].

So heres the simplest template for your snippet_list.html which you add in above or below the example block of code from p156 that uses the page variables for pagination.
  {% for snippet in object_list %}
  <p><a href="{{ snippet.get_absolute_url }}">{{ snippet.title }}</a></p>
 {% endfor %}
The next bunch of templates I've just filled with placeholders {% debug %} and/or the actual variables like <p>{{ object.title }}</p>.

And that's really it for this chapter. Everything works fine with no further modification needed, so it's onto the next major feature of Django in the next chapter. Forms.

7. Finishing the Weblog

This is part 7 of a series of posts on James Bennett's excellent Practical Django Projects. The table of contents and explanation can be found here

Firstly, the comments framework has been re-written for 1.0. The documentation for it can be found here if you need to refer to it. That out of the way lets get into the meat of the chapter.

There is now only one model for comments instead of two separate models - one for free comments and one for registered comments. I assume the new combined model will cover both use-cases.

On p124 in the URLConf file the pattern you should use is ( r'^comments/', include('django.contrib.comments.urls')) dropping off the extra '.comments'. On p125 the free_comment_form is used. Replace that with the simplest comment form {% render_comment_form for object %}, or you can be more explicit and use {% render_comment_form for coltrane.entry object.id %}, which makes it clearer that you're referring to the entry model and passing the primary key for the entry table which is the field id. I'm unsure as to whether these are functionally equivalent, but they appear to be.

Next up on p125 you build a free_preview.html template, but you might have already discovered that the new comments gives you one for free! So to modify it you copy the preview.html template from the django.contrib.comments.templates.comments directory into your templates.comments directory. Modify it so that it {% extends "base.html"} and use the block title from the book example. All content in the {% block content %} should come from the copied template, but you can add back in the poster's name like this:
<h2>Post a comment</h2>
Here's how your comment will look:

{{ form.cleaned_data.name }} said:

...
Cleaned_data references a post validation version of the form data. There will be more about form validation in later chapters. You could also reference the pre-cleaning version {{ form.data.name }}. Because your comment hasn't been posted you can't display the submit date. There is a timestamp available but no corresponding template filter to display it nicely, so we'll have to leave that out.

On p127 add markup to your installed apps and modify your custom preview.html template
{{ comment|markdown:"safe" }}
to make it work. Also on p127 is a posted.html template. Again there is a default template for this you can use. Modifying it to include the example <a href="{{ object.get_absolute_url }}"> doesn't work as expected, as the url only references the comment itself, so change it to <a href="{{ comment.content_object.get_absolute_url }}"> which will link back to the weblog entry. On p128 the variable {{ comment.person_name }} is now {{ comment.name }} and on p129 the tag get_free_comment_count is now just get_comment_count.

Comment Moderation

The new comments app includes some anti-spam techniques such as a honeypot formfield that is hidden to the viewer by css but not to a prospective spam bot, but the other techniques described still seem worth implementing. I managed to implement the Akismet code but it fails to verify my key (Note: 17-Nov-08 ldm616 provided a solution, I have amended the code example). The code that is added to your models.py has a fair amount of changes to it so I'll include all the added code to coltrane.models rather than just the changed bits.
#coltrane.models.py
from akismet.akismet import Akismet
from django.contrib.comments.signals import comment_will_be_posted
from django.contrib.sites.models import Site
from django.db.models import signals
from django.utils.encoding import smart_str
...
def moderate_comment(sender,**kwargs):
    comment = kwargs['comment']
    if not comment.id:
        entry = comment.content_object
    delta = datetime.datetime.now() - entry.pub_date
    if delta.days > 30:
        comment.is_public = False
    else:
        akismet_api = Akismet(key=settings.AKISMET_API_KEY,
             blog_url="http://%s/" % Site.objects.get_current().domain)
       if akismet_api.verify_key():
           akismet_data = { 'comment_type': 'comment',
                'referrer': '',
                'user_ip': comment.ip_address,
                'user_agent': '',
                'comment_author': comment.user_name }
           if akismet_api.comment_check(smart_str(comment.comment), akismet_data,  build_data=True):
               comment.is_public = False

comment_will_be_posted.connect(moderate_comment)

The changes to the code are due to the re-factor of signals in Django and also the implementation of signals in the new comments app. A signal receiver function should take just the sender, and then an arbitrary list of keyword arguments (sender, **kwargs). I could have just added instance = kwargs['comment'] and left the other references to instance alone, but seeing as the comments app passes it in to the function as 'comment' I prefer to change them. Where the code refers to the actual blog entry that the comment attaches to (entry = instance.get_content_object() ) I did two things. Firstly I looked at the code for the comment class and noticed that there is an attribute content_object available in the new class. Secondly I altered my code to load up the debugger to have a look at the object when actually posting a comment to see what is actually in the comment object when it is used in this context.
#coltrane.models.py
def moderate_comment(sender,**kwargs):
    comment = kwargs['comment']
    import pdb
    pdb.set_trace()
    ...
The debugger pdb will drop your runserver into a commandline when you post a comment where you can introspect the comment object. From the commandline you can do dir(comment) to see what functions and attributes the comment object has. So dir(comment) revealed a list of attributes and methods of which content_object is one of them. When I typed comment.content_object it prints the object that the comment is attached to so I know that is what I want for the reworked moderate_comment function. You can distinguish the functions from the attributes in the dir list by the fact that they will have action verbs such as get, save, set etc in the name. If you want help with the debugger, watch Eric Holscher's screencasts on the topic which is a really good overview of pdb and debugging in general.

Lastly, the new comments app defines its own signal functions and the most appropriate one for us to use is comment_will_be_posted so we connect that to the receiver function with

comment_will_be_posted.connect(moderate_comment).

Email Notification of Comments

As per the admonition on p136 I tested my email settings and used a gmail account which requires a secure TLS connection.
>>> from django.core import mail
>>> mail.send_mail('Subject','Message','from@address',['to@address'])
After determining that my email worked I added the mail_managers code replacing instance with comment, and get_content_object() with content_object
from django.core.mail import mail_managers
email_body = "%s posted a new comment on the entry '%s'."
mail_managers("New comment posted",
 email_body % (comment.user_name,
 comment.content_object))

Adding Feeds

No modification of code is needed in this section. Everything worked as per the books examples. Once you've implemented the LatestEntries and Categories feed you can view them in your browser at http://localhost:8000/feeds and http://localhost:8000/feeds/categories/your_category_here/. Of course you need to have populated some entries with [your_category_here] to see them in the feed.

So that's it for the weblog. Of course there is a fair bit to do still, like fill out some of the other standard templates, and design a css for it - but really the hard work is done, so it's on to Chapter 8 and the social code sharing site.

Saturday, 6 September 2008

6. Templates for the Weblog

Writing the Link Model

This is part 6 of a series of posts on James Bennett's excellent Practical Django Projects. The table of contents and explanation can be found here.

Read through and implement the all the templates up to p111 where the book covers Custom Tags. If you try running your base site at http://localhost:8000/weblog/ you'll probably get some template errors so we need to go back and fix up some url files. I updated my post for chapter 5 on 5-Sep-08, so if you missed that bit go back and read the end of that post, but here's the updated urls.categories.py
from django.conf.urls.defaults import *
from coltrane.models import Category

urlpatterns = patterns('',
  (r'^$', 'django.views.generic.list_detail.object_list',
  { 'queryset': Category.objects.all() },'coltrane_category_list'),
  url(r'^(?P[-\w]+)/$',
     'coltrane.views.category_detail',name='coltrane_category_detail'),
)
Note I've added a name for the category list view called coltrane_category_list which matches what is the base.html template is looking for.

Next go and do the same with coltrane_tag_list which is also missing from urls.tags.py.

There is also a new way in 1.0 (possibly still in development version) that you can deal with these errors a little better. You'll probably have noticed that if you use a variable in the template that can't be displayed because you've mistyped it or the data isn't there then it will silently ignore it. If we are using the url tag however, we do get an error if it can't do a reverse lookup on your named URLconf. But lets say you wanted to put that in the tags list view for later development and wanted to include it now in the base template as a placeholder for your layout.

Here's how you can do that in 1.x:

{% url coltrane_tag_list as tag_list %}
{% if tag_list %}
<li id="main-nav-tags">
<a href="{{ tag_list }}">Tags</a>
</li>
{% endif %}

Now the url reverse lookup will silently fail instead of producing an error.

Going back and playing with some of the templates there are a few other little things that have changed.

You might have noticed that your blog entries are converting from markdown to html but are displaying the html tags like <p> in the view. In one section of the chapter the {{ object.body_html }} is written correctly like this {{ object.body_html|safe }} but in the other on p110 the safe is dropped off. The default in 1.0 is to auto-escape html unless you turn the behaviour off using a {% autoescape off %} {% endautoescape %} block, so you need to add a safe filter to any html output.

Extending the Template System with Custom Tags

In this section we learn how to write custom tags for templates, and on p120 there is discussion about using default managers whenever possible. Unfortunately for us this conflicts with the problem overriding the admin manager that I discussed in the last post on Chapter 5, and until there is a simple way to override the manager for admin it is easier for us to declare a manager specifically for the Entry class. Once you've finished writing up the examples in this chapter alter your coltrane_tags.py render function to accomodate the live manager for the Entry model.
    def render(self, context):
        manager = self.model._default_manager
        if self.model.__name__ == 'Entry':
            manager = self.model.live
        context[self.varname] = manager.all()[:self.num]
        return ''
That's it for this chapter. On a final note for this post, I heard the author James Bennett on episode 36 of the excellent Django podcast, This week in django, talking about the book among other things. The good news is he will be working on making the source code available real soon. I look forward to it though I'm enjoying puzzling my way through the code changes.

Onto chapter 7 and the comments system which should be interesting as it has undergone a complete re-write.

Wednesday, 3 September 2008

5. Expanding the Weblog

Writing the Link Model

This is part 5 of a series of posts on James Bennett's excellent Practical Django Projects. The table of contents and explanation can be found here.

I'll move straight into the material here as it follows on from the previous chapter expanding the weblog application.

On p78 remember to move the prepopulate argument to your admin class which is created on p79. Update your admin.py and add in the LinkAdmin class in addition to your CategoryAdmin and EntryAdmin.
#admin.py

from coltrane.models import Category, Entry, Link
...
class LinkAdmin(admin.ModelAdmin):
    prepopulated_fields = {"slug":("title",)}

admin.site.register(Link, LinkAdmin)
...
Installing the pydelicious module on p79 also requires the feedparser module from
http://code.google.com/p/feedparser/

Setting Up Views for Categories

I still had some redundant code left over in the views.py from Chapter 4. The entries_index function was replaced by the generic view archive_index in the last chapter so remove this and the Entry import.

On p87 there is a minor type with an extra unwanted ) in extra_context={ 'category': category })

Cleaning Up the URLConf

This section breaks the urls.py into separate files. When you delete the urls.py delete your urls.pyc as well - I don't think it matters in this case as python imports the urls folder modules first, but good to get in the habit. I updated my tests.py to reflect the new urls layout.
from coltrane import admin, models, views
from coltrane.urls import categories, entries, links, tags

Handling Live Entries

Custom managers for your models are covered in this section. On p94 here are two lines that define the managers.
 live = LiveEntryManager()
 objects = models.Manager()
The order is important because the first manager becomes the default manager for the class. Normally this would be great but in 1.x this won't work with admin as the admin model will also use the default manager. This means that if you set your status to draft, your entries will disappear from the admin interface; not what you wanted.

In principle what you want to be able to do is define which manager the Admin class will use in your custom EntryAdmin class, but in practice this is tricky to do at the moment because there are a lot of functions to override.

I suspect this behaviour will be changed at some point and there is a ticket already that relates to this: http://code.djangoproject.com/ticket/7510

So for now just switch the order around to put objects = models.Manager() first.

Finally, mention was made in Chapter 4 about decoupling the Category model urls but there was no code example. So update your Category model like this:
    def get_absolute_url(self):
        return ('coltrane_category_detail', (),
             { 'slug': self.slug })
    get_absolute_url = models.permalink(get_absolute_url)
In your urls.categories.py you'll need to update your urlpatterns to this:
urlpatterns = patterns('',
    (r'^$', 'django.views.generic.list_detail.object_list',
    { 'queryset': Category.objects.all() }),
    (r'^(?P<slug>[-\w]+)/$',
        'coltrane.views.category_detail',{},
        'coltrane_category_detail'),
)
Because you aren't using a generic view for this you don't need to pass in an info_dict like your url.links.py so just pass an empty dictionary {}.

There is one new method in 1.0 for naming your urlpatterns to avoid bumping into other patterns that might use coltrane.views.category_detail. It seems like good practice to use that anyway here, so re-write the code like this.
urlpatterns = patterns('',
    (r'^$', 'django.views.generic.list_detail.object_list',
    { 'queryset': Category.objects.all() }),
    url(r'^(?P<slug>[-\w]+)/$',
        'coltrane.views.category_detail',
         name='coltrane_category_detail'),
)
Now you can write a proper template for category_detail.html if you wish. Remember that your view is a list so you need a {% for category in object_list %} block. Viewing the category detail view from the admin doesn't make a lot of sense but I guess you would normally view it in the context of an entry. The main thing to watch out for here is when testing the view to make sure you're are actually using a category that is attached to a live entry, but for now lets get onto chapter 6.

Hedged Down