klowner.com

I've been developing small wikiesque app for Django recently, and since I'm a fan of human-friendly URIs I had some difficulty coming up with a DRY method for finding the primary model associated with any given view within my project. Of course, not every view is directly associated with a single model, but in most cases you can probably inspect each of your views and conclude that there is a single primary model instance which is most directly related.

In this example, I'll explain the method which I've decided to use in order to take any requested URI path and rather than resolve it into a view, we will resolve it to a specific model instance. This greatly simplifies the process of creating sub-views which have access to the parent view's primary model instance.

We start with a very simple example of a view.

# myapp/views.py
from django.shortcuts import render_to_response, get_object_or_404
from models import Product

def product_show(request, product_slug=None):
    product = get_object_or_404(Product, slug=product_slug)

    return render_to_response(
        ['myapp/product/show.html'],
        {'product': product}
        )


For this example, we'll say that this view is called by the url /product/(?P<product_slug>[^/]+)/. Now, let's say I've developed a WikiPage model which uses Django's generic relations to attach itself to any other model on the site. If you were to add a url pattern for wiki_show which would behave in such a way that /products/my_pretty_pony/ideas/ would call the wiki_show view which displays a WikiPage named "ideas" which is also attached to a Product named my_pretty_pony.

The problem we have then is that wiki_show needs to know not only the name of the WikiPage we're interested in, but also the parent class type (which is Product), as well as the name of the parent item.

Now the need for being able to specify any URL and then obtain an associated model instance becomes a bit more obvious. In this example, we'll develop a method which allows us to resolve the path /products/my_pretty_pony/ into the instance <Product: my_pretty_pony>.

Now, it's time for...

The Plan

I ended up writing a fairly simple view decorator which handles most of this issue, and is a fairly DRY way to accomplish it.

# wiki/decorators.py

from functools import wraps
class model_instance(object):
    def __init__(self, resolver_method=None):
        self.resolver_method = resolver_method

    def __call__(self, f):
        @wraps(f)
        def model_instance_wrapped(*args, **kwargs):
            return f(*args, **kwargs)
        wrapped.model_instance_method = self.resolver_method
        return wrapped

    @staticmethod
    def resolve(request, path):
        from django.core.urlresolvers import resolve
        view_func, args, kwargs = resolve(path)
        instance = None
        if (hasattr(view_func, 'model_instance_method') and
                callable(view_func.model_instance_method)):
            instance = view_func.model_instance_method(request, *args, **kwargs)
        return instance

It's a fairly simple decorator, but notice that it tacks on the provided resolver_method as an attribute to the wrapped function. Let's go back to our original product_show view, and modify it so that it uses our new model_instance decorator.

# myapp/views.py
from django.shortcuts import render_to_response, get_object_or_404
from models import Product
from wiki.decorators import model_instance

def product_instance_resolver(request, product_slug=None):
    return get_object_or_404(Product, slug=product_slug)

@model_instance(product_instance_resolver)
def product_show(request, product_slug=None):
    product = get_object_or_404(Product, slug=product_slug)

    return render_to_response(
        ['myapp/product/show.html'],
        {'product': product}
        )



The important things to note is that the resolver method which is passed to the model_instance decorator must accept the same args as the view method. At this point, the decorator accomplishes our primary goal.

Now anywhere within our Django project we can simply import the model_instance decorator and resolve paths directly into a model instance, assuming we've decorated that associated view.

# wiki/views.py
from wiki.decorators import model_instance

def wiki_show(request, slug=None):
    # Take the current request path, and move up one level to the parent
    # ie. 'products/my_pretty_pony/notepad/' becomes
    # 'products/my_pretty_pony/'
    request_path = request.META.get('PATH_INFO').lstrip('/')
    path_items = filter(lambda x:x, request_path.split('/'))
    parent_path = "%s/" % (u"/".join(path_items[:-1]))

    instance = model_instance.resolve(request, parent_path)

    # etc...



instance should now be <Product: my_pretty_pony>! Problem solved. My method for stripping the last item of the request path probably isn't ideal, you could of course use various os.path methods as well.

Additional Ideas

To avoid repetition in your views which use the decorator, you could of course add additional functionality to the wrapper method which uses the resolver_method to provide the model instance to the view method itself.

If you see anything that you feel should be corrected or needs better explanation, please don't hesitate to send me an e-mail.