Monday, 15 September 2008

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.

11 comments:

ldm616 said...

Hi Brett,
Probably a dumb question, but I'm getting the following error with the Akismet code and not sure why. Maybe the validation is failing per your note?

UnboundLocalError at /comments/post/

local variable 'akismet_api' referenced before assignment

Request Method: POST
Request URL: http://127.0.0.1:8000/comments/post/
Exception Type: UnboundLocalError
Exception Value:

local variable 'akismet_api' referenced before assignment

I tried hard-coding the key and url as follows but still no luck.

akismet_api = Akismet(key="75aa1e10b9d6",
blog_url="http://127.0.0.1:8000")

The failure occurs on the next line:

if akismet_api.verify_key():

TIA for any suggestions.

Brett said...

Not a dumb question. if akismet_api.verify_key() should have been indented

ldm616 said...

I found that I also had to use user_agent (vs user-agent) and I also needed to add 'comment_author' : comment.user_name to akismet_data. I was then able to successfully test it using user_name = 'viagra-test-123'.

Hope this helps.

Brett, Thanks again for this. I'd have gone nuts by now were it not for your notes.

Brett said...

Well done. I never actually got Akismet to work - it wasn't critical so I left it. Amended the post with your corrections

W.A.S.T.E. said...

4th Paragraph: verbose render_comment_form tag works when written: "{% render_comment_form for coltrane.entry object.id %}"

waltbrad said...

With the mail function, I just wanted to say a couple of things:

1) Pretty small detail, but on the mail manager code it should be comment.user_name, not comment.person_name.

2) With the above correction, when I tried to use the code I got an error about localuser.domain not existing. From a WebFaction forum
I found I had to add SERVER_EMAIL = 'someuser@domain.com' to my settings. Then the emails I got were from that address.

3) Then I got strange Delivery Notification Failures. Bad destination email address 'invalid domain "": no dot found' I finally figured out that the ADMIN and MANAGERS in my settings were not formatted as tuples. Why there were not set correctly I do not know. That may have just been my oversight. But, in case anyone else has that probelm...

Great service Brett. I would be totally lost without this blog.

Brett said...

Thanks Waltbrad, updated with your correction.

Gurur said...

verify_key() didn't work because "blog_url="http:/%s/"" should be "blog_url="http://%s/""

Note two //

Brett said...

Thanks Gurur. Fixed

rbbeltran.09 said...

hey .. it would be better if we can download working source code in every chapter.

niceguydave said...

Thanks for posting this - going through exactly the same issue myself - helps a lot!

Hedged Down