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.
{% 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>
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.
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.
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.
<!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>
{% extends "test_app/base.html" %}
{% block content %}
{% load comments %}
<h1>Comment on your favourite photo</h1>
<ul>
{% for a in object_list %}
{% get_comment_count for test_app.photo_gallery a.pk as cc %}
<li><a href="{{ a.pk }}/">{{ a }}</a> ({{ cc }} comment{{ cc|pluralize }})</li>
{% endfor %}
</ul>
{% endblock %}
The get_comment_count tag fetches the number of comments for each object (photo) of the photo_gallery model.
{% extends "test_app/base.html" %}
{% block content %}
{% load comments %}
<h1>Give feedback/comments for your favourite photo</h1>
<p><strong>{{ object.name }}</strong></p>
<img src='{{ SITE_MEDIA }}/images/{{ object.pic.url }}' alt='{{ object.name }}' width="30%" />
{% get_comment_list for test_app.photo_gallery object.pk as comment_list %}
{% if comment_list %}
<h2>Comments</h2>
{% for comment in comment_list %}
<h4>{{ comment.name|escape }} at {{ comment.submit_date|date:"r"}}</h4>
<p>{{ comment.comment|escape|urlizetrunc:"100"|linebreaks }}</p>
<hr>
{% endfor %}
{% endif %}
{% if request.user.is_authenticated %}
<h2>Leave a comment</h2>
{% render_comment_form for test_app.photo_gallery object.pk %}
{% endif %}
{% endblock %}
The get_comment_list tag fetches all the comments bound to a particular object (photo) based on it's primary key (pk). Most of this is almost stock comment based except for the request.user.is_authenticated.
Since we added TEMPLATE_CONTEXT_PROCESSORS in our settings.py, it gives us the functionality to access the request object in our templates.
We check if the user is authenticated and display the comment form accordingly. The render_comment_form tag displays the form by rendering the comments/form.html.
Note
Without the request.user.is_authenticated functionality, we can accept comments from anonymous users also.
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.
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)
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.
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).
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.
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.
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.
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.
[theju@localhost ~]$ django-admin.py startproject comments_reg_users
[theju@localhost ~]$ cd comments_reg_users
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"
)
'django.contrib.admin',
'django.contrib.comments',
'test_app',
[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
[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.
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.