New Comment Notification by E-mail

This little trick certainly isn't anything special. Several other folks have posted about how to have Django send out an e-mail automagically when a new comment has been posted on your site. However, a lot of these other posts seem to be pre-Django 1.0. Some groovy changes took place with the signal system slightly before Django 1.0 was released, so I thought I'd share my method of having Django notify me by e-mail when someone posts a comment on my sites.

My code follows:

from django.db.models.signals import post_save
from django.core.mail import mail_admins
from django.contrib.comments.models import Comment

def notify_of_comment(sender, instance, **kwargs):
    message = 'A new comment has been posted.\n'
    message += instance.get_as_text()

    mail_admins('New Comment', message)

post_save.connect(notify_of_comment, sender=Comment)

Nice and simple, right? Right. All this does is it creates a callback function called notify_of_comment and waits for Django to signal that a Comment object has been saved. Any time that signal is intercepted, the callback will send off an e-mail to anyone in the settings.ADMINS list.

You should be able to put such code anywhere in your project so long as that anywhere is loaded when your project starts up. I usually put signal interceptors in an __init__.py file somewhere, though they should work just as well in a models.py or urls.py file.

I realize that the django-comment-utils project is capable of notifying when a comment has been posted, but I didn't want much else from the project. That is why I did it this way ;)

Adding Captcha To Django's Built-in Comments

I recently had a good friend of mine ask for some help in adding a captcha field to his comments form. I gave him some pointers, but before he could put them into action he had to leave for a Thanksgiving roadtrip home. I didn't give much mind to the idea of putting captchas on my own site since it's not all that popular amongst spammers yet. When I woke up this morning, however, I found myself with a few spare minutes to see if my pointers were correct.

Some of the ideas I shared with my friend turned out to not work very well. As I tinkered about trying to get things to work on my own site, I think I came up with a relatively efficient way of doing things.

Installing The Captcha

The captcha field I use is quite simple and effective. I originally got it from http://django.agami.at/media/captcha/, but the project seems to be unmaintained now. Along the road to Django 1.0, some changes were made to the way form fields work, and there is a minor change required in the base code for this captcha field if you want it to work. Alternatively, you can use a copy of the field that I'm currently using.

All you need to do is extract the captcha directory somewhere on your PYTHONPATH. The author recommends putting it in django.contrib, but I usually just place it straight on the PYTHONPATH so all I need to do is from captcha import CaptchaField instead of from django.contrib.captcha import CaptchaField. Minor details...

Adding The Captcha

The first thing you'll want to do after installing the captcha field is add the field itself to your comments form. Instead of subclassing the built-in django.contrib.comments.forms.CommentForm form, I simply decorated the constructor of the form as such:

1
2
3
4
5
6
7
8
9
from django.contrib.comments.forms import CommentForm
from captcha import CaptchaField

def add_captcha(func):
    def wrapped(self, *args, **kwargs):
        func(self, *args, **kwargs)
        self.fields['security_code'] = CaptchaField()
    return wrapped
CommentForm.__init__ = add_captcha(CommentForm.__init__)

This adds a field called security_code to the CommentForm, and it works the same way as if you had done something like this:

1
2
3
4
5
6
7
from django import forms
from captcha import CaptchaField

class MyCommentForm(forms.Form):
    name = forms.CharField()
    ...
    security_code = CaptchaField()

You can put the decorating snippet from above anywhere you'd like so long as the module you put it in is loaded at some point in your project. I usually put this sort of magic in my main urls.py file so it's harder to forget about when I debug things.

Fixing the Form

The first problem with this little trick seems to be that the CaptchaField is rendered as unsafe HTML in the default form.html template in the built-in comments application. That just means that, instead of seeing the captcha, you will see the HTML necessary to render the CaptchaField directly on the page, like this:

<input type="hidden" name="security_code" value="captcha.caZ1SqQ" />
<img src="/static/captchas/caZ1SqQ/0656f09d3974850397dd4c4974f23a35.gif"
 alt="" /><br /><input type="text" name="security_code"
 id="id_security_code" />

To fix that, you can apply the safe filter to the field and make the template look something like this:

{% load comments i18n %}
<form action="{% comment_form_target %}" method="post">
<table>
{% for field in form %}
    {% if field.is_hidden %}
    {{ field }}
    {% else %}
    <tr{% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
        <th>{{ field.label_tag }}:</th>
        <td {% if field.errors %} class="error"{% endif %}>
            {{ field|safe }} &nbsp;
            {% if field.errors %}{{ field.errors|join:"; " }}{% endif %}
        </td>
    </tr>
    {% endif %}
{% endfor %}
</table>
<p class="submit">
    <input type="submit" name="post" class="submit-post"
     value="{% trans "Post" %}" />
    <input type="submit" name="preview" class="submit-preview"
     value="{% trans "Preview" %}" />

</form>

Notice the {{ field|safe }} in there. Also note that I prefer the table layout for the comment form over the default mode. If you change your form template as I have done, you should put the updated copy in your own project's template directory. It belongs in templates/comments/form.html, assuming that your templates directory is called templates. You'll probably also want to check out the preview.html template for the django.contrib.comments application. I changed mine to look like this:

{% extends "comments/base.html" %}
{% load i18n %}

{% block title %}{% trans "Preview your comment" %}{% endblock %}

{% block content %}
    {% load comments %}
    {% if form.errors %}
    <h1>{% blocktrans count form.errors|length as counter %}Please
     correct the error below{% plural %}Please correct the errors below
     {% endblocktrans %}</h1>
    {% else %}
    <h1>{% trans "Preview your comment" %}</h1>
        <blockquote>{{ comment|linebreaks }}</blockquote>

        {% trans "and" %} <input type="submit" name="submit"
         class="submit-post" value="{% trans "Post your comment" %}"
         id="submit" /> {% trans "or make changes" %}:

    {% endif %}

    {% include 'comments/form.html' %}
{% endblock %}

See how I just use the include tag to pull in the comments/form.html template I mentioned above? Saves a lot of typing and potential for problems... If you update the preview.html template, you should save your copy in templates/comments/preview.html, assuming your templates directory is called templates.

Testing It Out

At this point, you should be able to try out your newly installed captcha-fied comments. If it doesn't work, please comment on this article and perhaps we can figure out the problem!

Project Release: django-watermark 0.1.0-pre1

I've found myself in many situations where I have several images that need a watermark applied to them. Applying a watermark to more than 5 images at a time is annoying at best, even if you do have macros to do the mundane stuff for you. I don't like to do things that annoy me.

Recently, I found myself yet again confronted with a "please watermark these images" sort of situation, and I decided to take a more logical approach to the problem: program it. The result of my efforts is called django-watermark and is available on Google Code and PyPI. To be perfectly honest, I actually ganked most of the useful code for this application from a generous soul on the Internet.

This seems to be working fine for me. I think the positioning for watermarks might need more work, but there are several options to hold people over for the time being. Please let me know what your thoughts are.

ReStructured Text

I've been working on updating all of my articles to use ReStructured text formatting these past couple of days. Basically ever since I started using Django on my personal sites, I used Textile formatting to make my articles look pretty, and it treated me well. However, I found myself with a few extra minutes to do some research this weekend, and the topic I happened to research was the difference between ReStructured text, Markdown, and Textile.

After my brief research into these utilities, I decided it would be worth my while to convert all of my articles and other Textile-formatted content to use ReStructured text instead. Most of this is due to personal preferences for certain conventions--very little technical reasoning came into play when I made the decision to switch (so don't ask me why I abandoned one for the other if you expect a technical pro/con list). Suffice it to say that I consider the ReST syntax to be superior to that of Textile.

In the conversion process, I realized that I wrote a whole lot more in my articles than I thought I did! It took much longer than expected, but the work is done now, and all of the articles should be updated. Most of you probably won't be able to tell much of a difference one way or another, but if you do find a problem with the formatting of any of my articles, please notify me!

Pendulum Screenshots!

For anyone who's interested, you can now see what Pendulum can look like out of the box (with the addition of a little CSS). The color scheme is kinda fugly, but that's okay because it's not a real site!

If you want to see the screenshots, head on over to the project page.

Project Release: django-reploc 0.1.0-pre1

Here's yet another application to help you make your site more useful to your users. django-reploc is a "representative locator" for Django-powered Web sites. It uses the Google Maps API to offer your users an interactive map of your representatives, vendors, friends, houses, etc.

I built this application to be a dealer locator for one of my clients. The application provides a way for you to create "representatives," and each representative may have one or more locations. For example, say you are a producer of clothing and apparel, ranging from shoes to pants to t-shirts to sweaters to hats to bags and so on. Perhaps your bigger representatives (or vendors) carry your products in all of their retail branches. reploc gives you a way to display each of these locations for the one vendor on a map for your users.

In addition to that, while the bigger vendors might carry your products in all locations, perhaps some locations only carry particular lines of your products. For example, a vendor location in Miami, Florida might carry a good deal of your sandals and none of your sweatshirts, while a vendor location in Bangor, Maine might not bother with any sandals but keeps sweatshirts in stock year-round. reploc offers a way for you to describe which product lines each location carries by the use of "attributes".

However, it can be used for much more than just that. Pretty much any location that you can show on Google Maps (is there anything that can't be shown on Google Maps anymore?) can be displayed with this application.

Furthermore, you can search for locations within a certain radius of an address that your users specify! A nice circle is drawn on the map, making it visually easier to understand what is and isn't within range. To make things even easier, the locations that do not fall within the radius of the given address are hidden from the map.

Finally, once a user finds a location within the radius of the address they specify, they are able to get driving directions to help them get right where they need to go!

w00t!

To learn more, go to the project page: http://code.google.com/p/django-reploc/

Setup a favicon.ico in Django

Up until a couple weeks ago, I had never installed a FavIcon on any of my Django sites. I never really thought about it until one day I enabled the SEND_BROKEN_LINK_EMAILS setting for one of my sites. As soon as I did that, I was able to track down links to broken pages very quickly. It also notified me that I didn't have a favicon.ico file setup anywhere on my site, and there are a great many programs out there that look for this file automatically.

At first I tried to go through Apache to get this working, but I'm no Apache guru so I was less than successful in taking this route. A couple of days ago I figured out a little trick to make the missing favicon.ico file stop sending me "broken link" e-mails hundreds of times a day. The solution? Put a favicon on my site, of course!

The approach I took was to simple add some information to my main urls.py file. Here's the line straight from my URLconf:

(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),

Hah! Simple isn't it? This way Apache is still handling the actual serving of the static image file--Django just handles the redirect. Ever since I added this line to my URLconf, I've not received one "broken link" e-mail pertaining to the missing favicon.ico file. That leads me to believe that most applications can understand the redirect and plug the actual image file where it belongs.

Oh, and for those of you who might be curious... My favicon.ico is actually just a PNG image that I renamed to favicon.ico. Again, most things seem to understand this (but I could be wrong).

See below for a more complete example of my URLconf

from django.conf.urls.defaults import *

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^robots\.txt$', 'django.views.generic.simple.direct_to_template', {'template': 'robots.txt', 'mimetype': 'text/plain'}),
    (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
    (r'^$', 'django.views.generic.simple.direct_to_template', {'template': 'base_home.html'}),
)

Project Release: django-axes 0.1-pre

Ever curious to see information about failed login attempts on your site? This morning I threw together a small, simple Django application that allows you to do just that. I'm calling this project django-axes, which is pronounced as "jango access".

I've only tested it on my own site, so it must still undergo some more in-depth testing before I can call it a stable application. From my testing, however, it works exactly as I expect it to.

For more information, check out the project homepage on Google Code.

django-pendulum news

I've made several fun improvements to my Pendulum Django application. Perhaps the most noteworthy for most people is the addition of a default jQuery-powered date picker for adding and updating entries. I was hesitant to make anything like this be required because some people might prefer controls other than the one I chose. However, I tried to make it easy to override this default date picker if you so desire. Hopefully someone will report on how easy/difficult it is.

Also, I've added django-pendulum to the PyPI, which is Python's little package repository. You can think of it as an apt-get repository for those of you familiar with Debian/Ubuntu Linux or derivatives. There is a utility called easy_install, which obviously makes it easy to install packages that are found in the PyPI. The command to install django-pendulum with easy_install is easy_install django-pendulum. Good stuff!

There have been various other changes to the code, and I have some more changes planned. We'll see how long it takes me to get around to making these changes...