Recent Comments

Elasticsearch & Django Tutorial -- V is for View

As we've already mentioned, here at OpenCrowd, we use Elasticsearch to provide search on top of our application development. The previous article in this series covered the "models" used in a simple blogging engine. This article will cover the "views".

In views.py, we're going to define the entry points to viewing a list of blog posts, creating a new blog post, viewing a specific blog post, and also handling user comments.

blog_detail and blog_list are really the work-horse functions of this application, and if you look at them you notice how simple they. It's deceptive, because a lot of the logic of building up the context for rendering is done with templatetags instead of in the views. I decided to do it this way, so that the logic of /how/ things get rendered is together in one place (in the tags). Deciding on /what/ gets rendered is up to the functions defined in views.py

def blog_detail(request, parent, pk):
    post = get_post(parent, pk)
    return render_to_response('simple/blogpost_detail.html',
                              {'object': post},
                              context_instance=RequestContext(request))

def blog_list(request):
    posts = get_posts()
    return render_to_response('simple/blogpost_list.html',
                              {'object_list': posts},
                              context_instance=RequestContext(request))

In the blog_edit function, we need to have some more logic since we need to handle the form input. Essentially, we have to handle all the parts of using Django's forms ourselves as we can't leverage the excellent ModelForm class since we're not using anything close to resembling a Django model. In blog_edit, we build up a regular Form object from the supplied request object. If it's valid, we then build a new dictionary from the cleaned_data of the Form. This dictionary is then passed along to the index_post function from models.py. comment_edit behaves similar, just with some different named functions.

def blog_edit(request, parent=None, pk=None):
    if parent and pk:
        post = get_post(parent, pk)
        author = post.author
    else:
        author = get_author(name=request.user.username)[0]
        post = dict(author=author)
    if request.method == 'POST':
        form = BlogForm(request.POST, initial=post)
        if form.is_valid():
            post['author'] = author
            post['title'] = form.cleaned_data['title']
            post['body'] = form.cleaned_data['body']
            post['body_clean'] = form.cleaned_data['body']
            post['body_lower'] = form.cleaned_data['body']
            post['updated_on'] = datetime.utcnow()
            if 'created_on' not in post:
                post['created_on'] = datetime.utcnow()
                blog_id = index_post(author, post)
                return HttpResponseRedirect(reverse('blogpost_detail', args=(author.get_meta().id, blog_id)))
    else:
        form = BlogForm(initial=post)
        return render_to_response('simple/blogpost_form.html',
                                  {'object': post, 'form': form},
                                  context_instance=RequestContext(request))

For comment_edit, things are a little more complicated. Partly, this is due to needing to have some sort of control over what comments can get added (for this, I decided to use the akismet library & service). Additionally, we need some additional logic to handle storing what bit of data the comment is referring to. In this simple engine, a comment can be made on a blog post, or on another comment.

def comment_edit(request, reference_type, reference_to):
    referent = None
    while reference_type != 'post':
        # chase back to a blog post
        referent = get_reference(reference_to, reference_type)
        reference_to = referent.reference_to
        reference_type = referent.reference_type
    referent = get_reference(reference_to, reference_type)
    if request.method == 'POST':
        form = CommentForm(data=request.POST, reference_type=reference_type, reference_to=reference_to)
        if form.is_valid():
            comment = dict(comment=form.cleaned_data['comment'],
                           pingback=form.cleaned_data['pingback'],
                           comment_author=form.cleaned_data['name'],
                           created_on=datetime.utcnow(),
                           reference_type=reference_type,
                           reference_to=reference_to)
            comment['is_spam'] = False
            if referent:
                comment['post_author'] = referent.author
                comment['post'] = referent.get_meta().id
                comment['post_title'] = referent.title
            else:
                referent = get_reference(reference_to, reference_type)
                comment['post_author'] = referent.author
                comment['post'] = referent.get_meta().id
                comment['post_title'] = referent.title
            index_comment(comment)
        return HttpResponseRedirect(reverse('blogpost_detail', args=(referent.author, referent.get_meta().id)))
    return HttpResponseRedirect(reverse('blogpost_detail', args=(referent.author, referent.get_meta().id)))

For simplicity's sake, I left out the parts of comment_edit that reference akismet and determine if a comment is spam. You can see the full code in the github repository. The trickiest part of the function is where we have try and determine the "root" object we are referring to -- in our case, the root object will be a "post". This method, while clunky, is straight-forward to understand and implement while also allowing us to have threaded comments.

For a "real" system, you would want to have either a better method of nesting comments, or make heavy use of caching to prevent needless rendering of deeply nested comments that haven't changed.

In the next (and final) part, we will step through the template tag functions that provide the context needed to render the templates.

Comments