Introspecting views in Django
Monday, February 15th, 2010I'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.
1 2 3 4 5 6 7 8 9 10 11 | # 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # 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.
