Theju's tryst with life

Another Method: Comments for Authenticated Users

In my previous post, I got a comment from Joshua Works who gave an interesting suggestion of not constructing fields for the name, email and url in the form as opposed to my method of using javascript to populate them. Each method has it's use cases and I like both of them.

So how do we get this method done?

In the photo_gallery_detail.html, make the following changes.

  • Replace the render_comment_form tag with the get_comment_form and contruct the form for all the fields you require.
{% get_comment_form for object as form %}
<form action="{% comment_form_target %}" method="POST">
  {{ form.comment }}
  {{ form.content_type }}
  {{ form.object_pk }}
  {{ form.timestamp }}
  {{ form.security_hash }}
  <input type="submit" value="Add comment" id="id_submit" />
</form>
  • You don't need the javascript that we added to populate the data in the base.html under the templates/test_app directory.
  • The wrapper remains as it is. You might ask me why I still have the wrapper in place even though the field for the name isn't available and that it would be automatically filled. The reason is that a spoofer can force a different name (user_name) and email address (user_email) to the database.

Credit goes to Joshua Works for suggesting this idea. This is a lot better than the hidden fields idea that I suggested as a passing reference at the very end of previous blog post.

Part 2: Django Comments for Authenticated Users

In the previous part, we had seen how to setup the basic portions of the project. In this part, we are going to build on that and achieve using the comments framework to accept comments from registered users only.

Step - 2: Getting templates ready

The comments are bound to different apps through the use of content_type and object_pk in the templates. If you check up the stock templates in the django.contrib.comments templates directory you will see how the binding is done.

Create a directory called test_app under your templates directory and create 3 files and name them base.html, photo_gallery_list.html and photo_gallery_detail.html.

Contents of base.html

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Comment demo</title>
    {% block head %}
    {% endblock %}
  </head>
  <body>
    {% block content %}
    {% endblock %}
  </body>
</html>

Step-3: Getting your admin ready to upload photos

Django's admin is one of the umpteen reasons it is so popular. It makes some CRUD part very easy.

Create a new file in test_app called admin.py.

Contents of admin.py

from django.contrib import admin
from test_app.models import Photo_Gallery

class Photo_Gallery_Admin(admin.ModelAdmin):
    list_display = ["name","pic"]

admin.site.register(Photo_Gallery, Photo_Gallery_Admin)

Step-4: Fire up your dev-server

You might have a question if we are done yet...and the answer to that would be a NO!!! We are just firing up the server to check if we have done everything right until here.

Make sure to sync your tables before you fire up your dev-server.

[theju@localhost comments_reg_users]$ python manage.py syncdb
Creating table auth_permission
Creating table auth_group
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
Creating table django_admin_log
Creating table django_comments
Creating table django_comment_flags
Creating table test_app_photo_gallery

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 'theju'): admin
E-mail address: admin@vce.ac.in
Password:
Password (again):
Superuser created successfully.
Installing index for auth.Permission model
Installing index for auth.Message model
Installing index for admin.LogEntry model
Installing index for comments.Comment model
Installing index for comments.CommentFlag model

and then fire your dev server

[theju@localhost comments_reg_users]$ python manage.py runserver
Validating models...
0 errors found

Django version 1.1 pre-alpha, using settings 'comments_reg_users.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Open your browser and direct it to http://127.0.0.1:8000/admin/. Open the Photo Gallery tab and add a few photos. Open a new tab (don't log out from the admin) on your browser and check out http://127.0.0.1:8000/names/. Then click on any photo object and you'll see the comments form. Log out from the admin tab and refresh this page and you'll see it vanish.

All happy till now??? But you might ask "Does the registered user have to go through the pain of entering his name and email id everytime?" and "What is the guarantee that this form cannot be spoofed?" My reply to these questions would be "Hey, I didn't tell this was done yet!!!"

The answer to the first question would be to override the comments/form.html.

Step-5: Slightly customize your comments form

Edit the base.html under templates/test_app to look like below:

 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 <html lang="en">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <title>Comment demo</title>
     <script type="text/javascript">
       function disableReqdInputs(){
         var nameInput = document.getElementById('id_name');
         if (nameInput){
           nameInput.value = "{{ request.user.get_full_name }}";
           nameInput.readOnly=true;
         }
         var emailInput = document.getElementById('id_email');
         if (emailInput){
           emailInput.value = "{{ request.user.email }}";
           emailInput.readOnly=true;
         }
       }
     </script>
    {% block head %}
    {% endblock %}
  </head>
  <body onload="disableReqdInputs();">
  {% block content %}
  {% endblock %}
  </body>
</html>

The above javascript will make the name and email inputs readonly and also autofill the names as mentioned in the django.contrib.auth.

Note

You can also use jQuery to get most of your javascript work done too.

Now we need to answer the second question regarding form-spoofing. For this we need to write a wrapper around the default post_comment that will prevent invalid data or changed data (like name or email).

Step-6: Writing a wrapper around post_comment

When the comment gets posted we need to make sure that it is redirected first to our wrapper.

For this add the following line in the urls.py and make sure this line always comes above the ^comments/ line. The reason is that django's urlresolvers match URLs from top-to-bottom.

(r'^comments/post/', 'test_app.views.comment_post_wrapper'),

Next open the test_app.views file and write the wrapper like below:

from django.contrib.comments.views.comments import post_comment
from django.http import HttpResponse

def comment_post_wrapper(request):
    # Clean the request to prevent form spoofing
    if request.user.is_authenticated():
        if not (request.user.get_full_name() == request.POST['name'] or \
               request.user.email == request.POST['email']):
            return HttpResponse("You registered user...trying to spoof a form...eh?")
        return post_comment(request)
    return HttpResponse("You anonymous cheater...trying to spoof a form?")

That's it, we are done...just test out your application now and it should work. If you don't like your inputs to be readonly, you can always make them hidden by altering the comments/form.html. The wrapper still remains the same.

Conclusions

There is a very interesting ticket #8630 that deals with the customization of comments. The ticket has some nice docs courtesy of Carljm and is slated to be in by Django 1.1.

With that ticket, the inbuilt comments should become much more easier to customize.

Part 1: Django Comments for Authenticated Users

Off late, I have not been hanging in the #django channel...but got an opportunity to do so a couple of days back. A user (I forgot his IRC nick), wanted to use django.contrib.comments to accept comments from authenticated users only.

I suggested that he write his own form template that used the request.user.is_authenticated in an if tag. He was ok with it but didn't want the user to enter his name and email id again. Then I suggested he use hidden fields and he started talking of form spoofing and other advanced techniques. Almost giving up hope (I wonder how Magus- manages to keep his cool), I asked him to write a wrapper for post_comment that would get his job done. The user finally dropped the bomb by telling he was new to python and didn't know what a wrapper was.

Realizing that many folks use django because it makes their life easy and let's them get away without knowing too much of python, I decided to write a step-by-step procedure for accepting comments from authenticated users only.

I am writing this post as a series so that each post is not too boring or intimidating.

First few steps:

The first few steps are mundane and you can ignore these if you know how to setup a django project, the database and a test application.

Note

I use Fedora as my operating system and some steps and applications might not be available in your setup.

  • Start a django project.
[theju@localhost ~]$ django-admin.py startproject comments_reg_users
[theju@localhost ~]$ cd comments_reg_users
  • Set the database by editing the settings.py in the comments_reg_users directory.
import os
DATABASE_ENGINE = 'sqlite3'
DATABASE_NAME = 'abc.db'
MEDIA_ROOT = os.path.join(os.getcwd(),'site_media')
TEMPLATE_DIRS = (
    os.path.join(os.getcwd(),'templates'),
)
TEMPLATE_CONTEXT_PROCESSORS=(
   "django.core.context_processors.auth",
   "django.core.context_processors.request",
   "django.core.context_processors.media"
)
  • Add the following to the INSTALLED_APPS in the settings.py.
'django.contrib.admin',
'django.contrib.comments',
'test_app',
  • Create two directories namely templates and site_media in the comments_reg_users directory.
  • Create a new application (or use an existing one if you have).
[theju@localhost comments_reg_users]$ python manage.py startapp test_app
[theju@localhost comments_reg_users]$ cd test_app/
[theju@localhost test_app]$ ls
__init__.py  models.py  views.py
  • Create a model against which the comments will be tied. Let's do a mini photo gallery.
[theju@localhost test_app]$ emacs models.py
from django.db import models

# Create your models here.
class Photo_Gallery(models.Model):
    name = models.CharField(max_length=50)
    pic  = models.ImageField(upload_to='pics')

    def __unicode__(self):
        return self.name

Note

If you don't understand what we've done so far, it is recommended that you go through the django docs.

  • Edit the urls.py in the comments_reg_users directory like below.
from django.conf.urls.defaults import *
import os

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
  (r'^admin/(.*)', admin.site.root),
  (r'^comments/', include('django.contrib.comments.urls')),
  (r'^names/$', list_detail.object_list,
                dict(queryset=Photo_Gallery.objects.all())),
  (r'^names/(?P<object_id>\d+)/$', list_detail.object_detail,
                                   dict(queryset=Photo_Gallery.objects.all())),
  (r'^images/(?P<path>.*)$', 'django.views.static.serve',
                             {'document_root':  \
                               os.path.join(os.path.dirname(__file__),'site_media/')}),

)

We will be using generic views to display the data bound to the models ie the photos in our mini photo gallery. Nothing extra to be written. With this we are done with the first few steps.

Read the next part here.

Previous The number of posts are 8. The number of pages are 2.