Customizing Django Comments: Remove Unwanted Fields

I recently added comments to a new Django site that I’m working on (Wantbox.com = craigslist[::-1] + stack overflow). Comments pose an interesting problem as they can have a number of “parents”. In my case, the parent might be a user’s “Want” a respondent’s “Have” or possibly another “Comment.”

In the process of researching the best way to architect Wantbox’s comments app, I read about “polymorphic associations“, “exclusive arcs” and Django’s ContentType framework. Using this knowledge, I contemplated recreating the comment wheel, since I wanted my comment form to just be a simple “Stack Overflow-type” comment-only field and not the larger “WordPress-type” name/email/website/comment.

As I explored Django’s comments framework deeper, I realized that recreating another comment app was a waste of my time and my end product would be far less feature rich than Django’s bundled commenting system. Below are my modifications which allowed me to quickly and easily twist Django comments into what I needed.

My Django Comment Modifications:

To customize the default comment form and comment list display, I created a “comments” directory in my root “templates” directory and simply overrode the two default comment templates “form.html” and “list.html”.

My custom “/templates/comments/form.html”:

{% load comments i18n %}
{% if user.is_authenticated %}
   <form action="{% comment_form_target %}" method="post">
        {% csrf_token %}
        {% if next %}<input name="next" type="hidden" value="{{ next }}" />{% endif %}
        {% for field in form %}
            {% if field.is_hidden %}
                {{ field }}
            {% else %}
                {% if field.name != "name" and field.name != "email" and field.name != "url" %}
                    {% if field.errors %}{{ field.errors }}{% endif %}
                    {{ field }}
                {% endif %}
            {% endif %}
        {% endfor %}
        <input class="submit-post" name="post" type="submit" value="{% trans " />
   </form>
{% else %}
    I'm sorry, but you must be <a href="javascript:alert('send to login page')">logged in</a> to submit comments.
{% endif %}

Which is only slightly different from the default Django comments form.html, primarily suppressing the display of the not-wanted and not-required “name”, “email” and “url” input fields.

My custom “/templates/comments/list.html”:

<div class="comment_start"></div>
{% for comment in comment_list %}
   <div class="comment">
      {{ comment.comment }}
      (from <a href="javascript:alert('show user profile/stats')">{{ comment.user }}</a> - {{ comment.submit_date|timesince }} ago)
   </div>
{% endfor %}

In the template where I want to invoke the comments form, I first call {% load comments %} and then {% render_comment_form for [object] %} to show the form, or {% render_comment_list for [object] %} to generate a list of the comments on the object (replace [object] with your appropriate object name). So easy.

This solution is working great for me, and still giving me all the other “free” stuff that comes with django comments (moderation, flagging, feeds, polymorphic associations, helpful template tags, etc…). The moral of this story: don’t recreate the wheel when an hour or two of research can give it to you for free.

NOTE: This blog post is based on my Stack Overflow answer to Ignacio’s question “How to extend the comments framework (django) by removing unnecesary fields?

Django 1.2 Tutorials: Django by Example

Looking for some good Django 1.2 tutorials? Check out Django by Example by “andreai.avk”. I just discovered them myself via the django-users group on googlegroups.com.

When I searched a few months ago for good (and current) Django tutorials, I never ran across this site. Hopefully adding a couple links will help bump it up the Google rankings. The simple blog tutorial is currently ranked #103 for the search “django 1.2 tutorials”, but no other page from the site is in the top 500.

Topics covered include: Django admin customization, comment notification and moderation, thumbnail creation, searching and filtering, and automated testing. Current demos include a To-Do App, a Simple Blog, a Photo Organizing and Sharing App and a Simple Forum.

Let the link juice trickle.

Django Documentation for the iPhone

Have you ever searched the App Store for Django apps? Don’t bother, as of this post there is only one that will come up: Django Documentation. Since it was only 99 cents, I bought it a few weeks back and I wouldn’t recommend that you do the same. It is simply the online Django documentation “formatted” for the iPhone and iPod Touch.

Guess what? If you use your iPhone to browse to the Django documentation page with your free Mobile Safari app you’ll also get the Django documentation “formatted” for you iPhone or iPod Touch, and you’ll also get the ability to bookmark pages, easily go forward and back in your page history and zoom in and out on text.

For the past couple weeks I’ve had the paid app sitting side-by-side with a Safari bookmark on my iPhone:

Paid app on far right, standard bookmark to the left of it, new bookmark to the left of that

My only problem with adding the Safari bookmark to the home screen? The ugly default icon it creates. For some reason, it *really* bothers me. Sure, it’s an accurate representation of the page it bookmarks, but it doesn’t make for a very good icon.

What I have done — and you can see it immediately to the left of the Safari bookmark — is create a front page to the standard Django docs so when you bookmark it, it makes a presentable icon suitable for the iPhone. The downside is that when you click on this icon, you go to that front page and then have to click the huge “dj” button to get to the docs. For me, however, this is worth it.

If you care about the appearance of your iPhone icons and want to have the Django docs on your own phone, do the following:

  1. open Mobile Safari
  2. browse to http://be73.com
  3. click the “+” icon on the bottom of the screen
  4. click the “Add to Home Screen” button
  5. click “Add”

Done. May my fellow OCD suffers rest easily tonight.

UPDATE: In the comments Phillip Bosch points out that I can use a more standard <link rel=”apple-touch-icon” href=”django-icon.png”> to produce an even better icon. Using this, I have iframed the django docs, so clicking on the icon goes directly there while still producing a nice iPhone/touch-friendly icon. Thanks Phillip!

Show a Custom 403 Forbidden Error Page in Django

Creating a custom 404 Page Not Found error page is so easy in Django (all you do is put your own template named “404.html” at the root of your templates directory) that I naturally assumed doing the same for a 403 Forbidden error page would be just as easy. Unfortunately it is not.

After searching around for quite a while last night, I found bits and pieces that I have modified slightly and republished below in a unambiguous step-by-step tutorial (see the “Source and Other Resources” section at the end of the post for a few of the source posts).

The method posted below leverages some custom Django middleware code. Please, if you have a better, more elegant solution I’d love to hear about it in the comments.

Create the Middleware

  1. Create a directory at the root of your project called “middleware”
  2. Add a file named “__init__.py” to this directory
  3. Create a file named “http.py” in this directory with the following contents:
  4. from django.conf import settings
    from django.http import HttpResponseForbidden
    from django.template import RequestContext,Template,loader,TemplateDoesNotExist
    from django.utils.importlib import import_module
    
    """
    # Middleware to allow the display of a 403.html template when a
    # 403 error is raised.
    """
    
    class Http403(Exception):
        pass
    
    class Http403Middleware(object):
        def process_exception(self, request, exception):
            from http import Http403
    
            if not isinstance(exception, Http403):
                # Return None so django doesn't re-raise the exception
                return None
    
            try:
                # Handle import error but allow any type error from view
                callback = getattr(import_module(settings.ROOT_URLCONF),'handler403')
                return callback(request,exception)
            except (ImportError,AttributeError):
                # Try to get a 403 template
                try:
                    # First look for a user-defined template named "403.html"
                    t = loader.get_template('403.html')
                except TemplateDoesNotExist:
                    # If a template doesn't exist in the projct, use the following hardcoded template
                    t = Template("""{% load i18n %}
                     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
                            "http://www.w3.org/TR/html4/strict.dtd">
                     <html>
                     <head>
                         <title>{% trans "403 ERROR: Access denied" %}</title>
                     </head>
                     <body>
                         <h1>{% trans "Access Denied (403)" %}</h1>
                         {% trans "We're sorry, but you are not authorized to view this page." %}
                     </body>
                     </html>""")
    
                # Now use context and render template
                c = RequestContext(request, {
                      'message': exception.message
                 })
    
                return HttpResponseForbidden(t.render(c))
    
  5. You should now have this:
    • /myproject/middleware/
      • __init__.py
      • http.py

Modify Your Project’s “settings.py”

  1. Add ” ‘myproject.middleware.http.Http403Middleware’, ” to your MIDDLEWARE_CLASSES

Create a Custom “403.html” page

  1. Put it at the root of your template directory
  2. Sample content: (note: assumes you’ve already defined a “base.html” template)
{% extends "base.html" %}
{% block title %} | Access Denied{% endblock %}

{% block content %}
<h1>Access Denied</h1>
<span>We're sorry, but you are not authorized to view this page (Error: 403)</span>
{% endblock content %}

Raise a 403 Error

  1. In the file where you want to raise the 403 add this at the top: (I used it in my project’s “view.py” file)
  2. from myproject.middleware.http import Http403
  3. Raise the 403
  4. if request.user.id != object.user.id:
        raise Http403

That’s it, you now have a Django template to handle 403 Forbidden errors. I’m sure there’s a way for your front-end, production web server to do the same, but I haven’t explored that yet.

Source and Other Resources

  1. A middleware solution is here (HT Felipe ‘chronos’ Prenholato)
  2. A middleware solution is here (HT Glen Zangirolami)
  3. A potential decorator solution is here (HT Magus)

Getting Started with virtualenv (Isolated Python Environments)

Like South, virtualenv is a helper utility that I put off using for too long. Looking back, it is so easy to get up and running (just like South, see below) that there is no reason for you to hold off like I did.

In a nutshell, virtualenv is a tool for creating isolated Python environments. This is particularly useful if you host multiple Django projects on a single dev box. As an example, virtualenv allows you to easily work on one site built on Django 1.1 and django-registration 0.7  and another one built on Django 1.2 with django-registration 0.8.

It is also invaluable if you want to deploy a Django project to a shared host where you don’t have root access to the main “site-packages” directory. Once you create a virtualenv for your project, an isolated copy of Python and “site-packages” is created which you own and can write to.

Basic virtualenv Start-up Steps

  1. sudo pip install virtualenv
    (or, sudo easy_install virtualenv if you don’t use pip)
    (or, easy_install --install-dir ~/site-packages/ virtualenv on a shared host)
  2. mkdir ~/virtualenvs   (a directory for your isolated environments)
  3. virtualenv ~/virtualenvs/mysite.com --no-site-packages
    (--no-site-packages isolates your environment from the main site-packages directory)
  4. cd ~/virtualenvs/mysite.com/bin
  5. source activate  (activates your new environment)

That’s it, you now have a dedicated Python environment for your mysite.com project with it’s own “site-packages” directory (~/virtualenvs/mysite.com/lib/python2.5/site-packages/) where you can install any version of Django or Django app without messing with your other projects. To exit your virtualenv just type “deactivate”.

A helper alias:

  1. vi ~/.bash_aliases
    alias ams='source ~/virtualenvs/mysite.com/bin/activate'
    
  2. source ~/.bash_aliases  (to activate the aliases)

Now you can run “ams” to quickly activate your “mysite.com” environment. For more in-depth information, check out the virtualenv documentation or a couple other informative blog posts.

Debian 5
Python 2.5.2
virtualenv 1.4.9

Getting Started with South (Django Database Migrations)

If you have worked on a project in Django, you have undoubtedly discovered that ‘syncdb’ is great at turning your ‘models.py’ files into real database tables but not so great at taking your modified models and altering your database with the new definitions. In fact, the Django documentation is pretty clear about this: “Syncdb will not alter existing tables”!

South is a Django project which solves this problem by providing “consistent, easy-to-use, database-agnostic migrations for Django applications.” Below are the most basic steps for getting South up and running in your project.

INSTALLING SOUTH

  1. pip install south     (if you’re lucky, otherwise RTFM)
  2. add ‘south’ to your project’s INSTALLED_APPS
  3. run ‘syncdb’     (before you create your own models)
  4. note: this is the last time you’ll run ‘syncdb’

YOUR FIRST MIGRATION

  1. create a new app and create your initial ‘models.py’ file for it
  2. add your app to your project’s INSTALLED_APPS
  3. run ‘python manage.py schemamigration myapp –initial’      (creates your initial migration, note: those are two dashes hyphens before initial)
  4. run ‘python manage.py migrate myapp’     (uses this initial migration to create your app’s DB tables)

MIGRATING A CHANGED MODEL

  1. modify your app’s models.py file    (e.g., add a new column somewhere)
  2. run ‘python manage.py schemamigration myapp –auto’    (creates a new migration, note: those are two dashes hyphens before auto)
  3. run ‘python manage.py migrate myapp’    (applies this new migration)

That’s it, the very bare bones steps to getting up and running with South. Obviously you’ll either need to run these commands in the same directory as ‘manage.py’ or change the commands to point to it.

One last thing…

AN OPTIONAL SHELL FUNCTION TO HELP OUT

  1. vi ~/.bash_aliases    (there’s probably a better home for this, but this is where I put mine)
  2. add:
    function mig() {
        python manage.py schemamigration "$@" --auto;
        python manage.py migrate "$@";
    }
  3. source .bash_aliases    (to activate the changes)
  4. now you can run ‘mig myapp’ whenever you change the model for ‘myapp’

If you want to be able to run this shell function anywhere, put the full path to your project’s ‘manage.py’ file in the function body. Otherwise, you’ll have to run it in the same directory as the relevant ‘manage.py’.

For South’s more advanced goodness, check out the documentation.

South 0.7
Django 1.2.1
Python 2.5.2.

Aptana Studio and “Undefined variable from import: DoesNotExist”

In my journey from Java hacker to a Django developer, I’ve test driven a bunch of Python IDEs (ranging from true dev tools like Aptana, Eclipse/PyDev, PyCharm to simple editors like Notepad++).

Recent I was adding some django-profile code I found via Scot Hacker’s very helpful “django-profiles: The Missing Manual“. When I added this bit of code for a custom form object:

    def __init__(self, *args, **kwargs):
        super(ProfileForm, self).__init__(*args, **kwargs)
        try:
            self.fields['email'].initial = self.instance.user.email
        except User.DoesNotExist:
            pass

Aptana would call out “DoesNotExist” with the error: “Undefined variable from import: DoesNotExist”. Thanks to Google and the DjangoBot I learned that this is because “DoesNotExist” is added by the metaclass.

Here’s the fix

  1. Open up Aptana Studio (I’m v2.0.3 btw)
  2. Open Window > Preferences > PyDev > Editor > Code Analysis
  3. Select the “Undefined” tab
  4. Add DoesNotExist at the end of the “Consider the following names as globals” list
  5. Apply and restart

Error gone. In the immortal words of ubernostrum: “Bah. It’s not like metaclasses are *that* hard to statically figure out.” I’ll take your word for that, James.

New Django site: polurls.com

polurls | the political blog aggregatorTwo of my great passions (OK…obsessions) are web tech and politics. It was just a matter of time before I mashed them together.

Recently, I’ve been diving head first into Django, looking to complement my Java-based toolset — honed via ParentShack.com and Sharenik.com — with some Python/Django ones.

Last week I launched polurls.com, a political blog aggregator which is not only my first live Django site but also the spawn of my politics and tech love. Think of it as the popurls of politics. A site which aggregates left-leaning political blogs on polurls.com/blue, right-leaning blogs on polurls.com/red and the whole spectrum of political blogs on polurls.com/purple.

My hope is that by showing conservative, progressive and centrist blogs side-by-side that polurls visitors will get a truly balanced take on the latest political news.

I’ve already found the site to be a very quick and interesting way to scan the latest political news. I’m eating my own dog food and loving the taste!

I’d love feedback either in the comments or on twitter (@mfournier or @polurls).

Specify a custom manager for the Django admin interface

As I was running through the weblog example in James Bennett’s excellent “Practical Django Projects (2nd Ed)” book I ran across a problem. In the book, we are asked to create a custom manager for the Entry model. Instead of pulling all entries, the custom manager only pull the entries that have been marked as LIVE (and ignores the ones that are marked HIDDEN or DRAFT).

This custom manager is set as the _default_manager (by defining it first in the Entry class), which is fine, except for the fact that the Django admin interface “defaults” to using the default manager of a class. In the admin, I want to see and edit ALL objects, not just the live ones.

To further complicate things, we create a custom tag that takes any type of content and displays the most recent elements of it. This tag is model agnostic and so uses the _default_manager of the model that it is looking at. So, for the Entry model, the default manager needs to remain the custom one which shows only LIVE results.

Luckily, there is a fairly simple way to tell the admin interface to not use the default manager. Simply change your ModelAdmin class in you admin.py file from something like this:

class EntryAdmin(admin.ModelAdmin):
    prepopulated_fields = { 'slug': ['title'] }

To something like this:

class EntryAdmin(admin.ModelAdmin):
     prepopulated_fields = { 'slug': ['title'] }
     def queryset(self, request):
         return Entry.objects

And make sure that your Entry model has its managers defined thusly:

# Give the Entry model two managers. NOTE: the first one is the default!
live = LiveEntryManager()   # _default_manager #
objects = models.Manager()

Now everywhere that you use Entry.live.all() or Entry._default_manager.all() You’ll pull only the LIVE results while the admin interface will show all of the LIVE, DRAFT and HIDDEN results.

(Let me know in the comments any unspeakable horrors this solution might stir up ;)

Make Firefox look (and act) like Google Chrome

Chrome logoI love almost everything about Google’s Chrome browser: the startup speed, the clean and simple interface, the javascript processing speed, the extensions (especially not having to reboot when you add extensions!) and the tabs-as-processes.

What I don’t love, however, is the lack of a built-in master password mechanism for hiding and protecting all of my most sensitive site credentials. A quick visit to “Options > Personal Stuff > Show saved passwords” and you’ll see all the typical passwords I use at my favorite sites.

I do use LastPass, but my Firefox experience tells me that relying on extensions to provide the required core functionality can result in frustration around browser update time. Granted, I’m using a bunch of Firefox extensions to mimic Chrome, but these are just for cosmetics. If any of them go at my next update, it’s not the end of my browsing world.

My secondary concerns with Chrome are its more bloated RAM use and its lack of a good LeechBlock alternative for keeping me on task. I could probably live with these, however, but I can’t live with the master password oversight.

So until Chrome has a master password, I’ve customized Firefox to look and act like it:

FF as Chrome


  1. Get and install the “Chromifox Basic” theme for Firefox:
    https://addons.mozilla.org/en-US/firefox/addon/8782
  2. Get and install the “Omnibar” extension:
    https://addons.mozilla.org/en-US/firefox/addon/8823
  3. Get and install the “Tiny Menu” extension:
    https://addons.mozilla.org/en-US/firefox/addon/1455
  4. Get and install the “Toolbar Buttons” extension:
    https://addons.mozilla.org/en-US/firefox/addon/2377
  5. Restart Firefox
  6. Right click on your Firefox menubar and click “customize”. Move the newly minimized menu to the far right, add toolbar buttons for “Toggle the Bookmark Toolbar” | “Open Add-ons Manager” | “Print this page” and move the new “omnibar” to the right of the forward/back/reload buttons
  7. Optionally, remove the excessive location bar icons (RSS, bookmark star) by creating a userChrome.css file (located here in my Windows 7 install: C:\Users\Mitch\AppData\Roaming\Mozilla\Firefox\Profiles\vggf8vt5.default\chrome) and adding the following:
    /* Remove the Bookmark star from the location bar */
    #star-button {
    display: none !important; }
    
    /* Remove the feed button from the location bar */
    #feed-button {
    display: none !important;}
  8. Restart Firefox again

Voila! Many of the benefits of Chrome, all in the feature-rich (aka: master password containing) container of Firefox!