This is the third post in a series exploring how to create a basic tagging application, and how to do so in the Django framework so that I may better understand Django as well as other technologies and development approaches that are at least somewhat new to me. For convenience, here are the first two posts:
- Implementing Tagging in a Django Application
- Toward a RESTful Approach to Django Applications
This time, I want to revisit REST, and hopefully come up with something more usable from the last post.
REST redux
I sincerely thank those who took the time to either suggest improvements to the code from last week’s post, or who corrected some misconceptions I had. Ultimately, I didn’t get the comment I had hoped for–something along the lines of “hey dummy, there’s a much easier way to do this…” so I will forge ahead with what I have.
David Larlet from biologeek.com suggested using a view prefix to significantly clean up the urls.py code from my previous post, making it easier to understand:
from django.core.management import _get_table_list, _get_installed_models models = _get_installed_models( _get_table_list() ) for model in models: urlpatterns += patterns(‘tagging_exploration.tagging.views.’ + model.__name__, (r’^’ + model.__name__ + ‘/$’, ‘dispatch’), (r’^’ + model.__name__ + ‘/creator/$’, ‘create’), (r’^’ + model.__name__ + ‘/(?P<id>d+)/$’, ‘dispatch’), (r’^’ + model.__name__ + ‘/(?P<id>d+)/editor/$’, ‘edit’), )
You might also notice from this, that I changed my naming conventions, because I realized that in my effort to figure out how to make the views more resource oriented, I had named a few Python methods with nouns instead of verbs, which doesn’t make much sense. So now, the former ‘creator’ and ‘editor’ views that dispatch to the real implementations are now named ‘create’ and ‘edit’ respectively. The names are a little more consistent now, and perhaps it’s a little clearer what’s happening: the URL patterns are being matched to view methods that are surrogates for the nested methods of the resources that I really want to get to.
While I’m in the mood to clean up last week’s mess, lets try to figure out a way to mitigate the complexity of this approach and generalize it a little more. In my previous attempt to create urlpatterns automatically, I was actually creating urlpatterns for every model in every installed application in this Django project, which isn’t really what I wanted to do. So, I read through some Django source, and borrowed heavily from it to figure out how to limit this to just the applications that I want to be RESTful. To begin, I modified the Django project level settings.py file to separate out the RESTful applications:
RESTFUL_APPS = ( ‘tagging_exploration.tagging’, ) INSTALLED_APPS = RESTFUL_APPS + ( ‘django.contrib.auth’, ‘django.contrib.contenttypes’, ‘django.contrib.sessions’, ‘django.contrib.sites’, ‘django.contrib.admin’, )
If you are playing along at home, you’ll have to restart your Django development server for this to take effect. You can now get access to the RESTFUL_APPS setting from urls.py with code like this:
from django.conf import settings print str(settings.RESTFUL_APPS)
I also thought it was pretty lousy to pollute urls.py with a lot of Python hacking, and I wanted the view prefix I added above to be dynamically populated along with the model classes, so, I moved that code to a new module in the Django project root called restful_urls.py (at the same level as urls.py) and replaced it with this code in urls.py:
from restful_urls import get_restful_patterns urlpatterns += get_restful_patterns()
Here is the restful_urls.py module updated to load only the models in the RESTFUL_APPS and generalize the view prefix:
from django.conf.urls.defaults import * from django.core.management import _get_table_list from django.db import models from django.conf import settings def get_restful_patterns(): urlpatterns = [] # get restful applications restful_modules = [] for app in settings.RESTFUL_APPS: restful_modules.append(app + ‘.models’) apps = filter(lambda x: restful_modules.count(x.__name__) > 0, models.get_apps()) # get models for restful applications for app in apps: app_models = [] for model in models.get_models(app): app_models.append(model) restful_models = set([m for m in app_models if m._meta.db_table in _get_table_list()]) # configure URLs for restful models for model in restful_models: app_label = ‘.’.join(app.__name__.split(‘.’)[:-1]) resources_class = None try: resource_module_name = app_label + ‘.views.’ + model.__name__ resource_module = __import__(resource_module_name, globals(), locals(), [model.__name__]) resources_class = vars(resource_module)[model.__name__] except Exception, e: pass # move along, nothing to see here if resources_class: class_info = {‘resources_class’: resources_class, ‘model_class’ : model } urlpatterns += patterns(app_label + ‘.views.’ + model.__name__, (r’^’ + model.__name__ + ‘/$’, ‘dispatch’, class_info), (r’^’ + model.__name__ + ‘/creator/$’, ‘create’, class_info), (r’^’ + model.__name__ + ‘/(?P<id>d+)/$’, ‘dispatch’, class_info), (r’^’ + model.__name__ + ‘/(?P<id>d+)/editor/$’, ‘edit’, class_info), ) return urlpatterns
As I already mentioned, the code to limit the models is based on code in the Django source, in particular, the _get_installed_models(table_list) method in django.core.management.py. If you’re going to steal, steal from the best.
Also, notice the extra context information I am now passing to the views. This change facilitates further simplifying and generalizing the view code, Items.py, from last week’s post. To begin that work, I decided to apply some good ol’ fashioned object orientation to Items.py, and created a Resources.py module based on the same structure as Items.py and copied over the dispatcher class into it. And, to make it easier to override methods, I decided to put the module level GET and POST methods inside a “Resources” class which also contains the inner classes for the member class, the Creator class, etc. Here is the current version of Resources.py:
from django.views.generic.list_detail import object_list from django.views.generic.create_update import create_object, delete_object from django.http import HttpResponseNotFound class Resources: def __init__(self): pass def GET(self, request, model_class=None): return object_list(request, queryset=model_class.objects.all()) def POST(self, request, model_class=None): # to help with initial debugging… return HttpResponseNotFound(‘POST: Resources’) class Creator: def GET(self, request): # to help with initial debugging… return HttpResponseNotFound(‘GET: Resource creator’) class Resources_Member : def __init__(self, model_class): self.model_class = model_class def GET(self, request, id): # to help with initial debugging… return HttpResponseNotFound(‘GET: Resource’) def PUT(self, request, id): # to help with initial debugging… return HttpResponseNotFound(‘PUT: Resource’) def DELETE(self, request, id): return delete_object(request, model=self.model_class, object_id=id, post_delete_redirect=’/’ + self.model_class.__name__ + ‘/’ ) class Editor: def GET(self, request, id): # to help with initial debugging… return HttpResponseNotFound(‘GET: Resource editor ‘) def getMemberClass(resources_class=Resources): return getattr(resources_class, resources_class.__name__ + ‘_Member’) class Resource_Dispatcher: def __init__(self, resources_class, model_class): self.resources_class = resources_class self.model_class = model_class def dispatch(self, request, id=None): response = None if id: member_class = getMemberClass(self.resources_class) member_item = member_class(self.model_class) if request.method.lower() == ‘get’: response = member_item.GET(request, id) else: if request.has_key(‘_action’): if request.POST[‘_action’].lower() == ‘put’: response = member_item.PUT(request, id) elif request.POST[‘_action’].lower() == ‘delete’: response = member_item.DELETE(request, id) else: if request.method.lower() == ‘get’: response = self.resources_class().GET(request, self.model_class) else: response = self.resources_class().POST(request, self.model_class) if response: return response else: raise Http404 def create(request, resources_class=Resources, model_class=None): return resources_class().Creator().GET(request) def edit(request, id, resources_class=Resources, model_class=None): member_class = getMemberClass(resources_class) return member_class(model_class).Editor().GET(request, id) def dispatch(request, id=None, resources_class=Resources, model_class=None): dispatcher = Resource_Dispatcher(resources_class, model_class) return dispatcher.dispatch(request, id)
Again, I moved the dispatcher logic into a class to make it easier to override it in the future if I want. This also follows the general pattern I have been using of keeping the view methods that are directly called on incoming requests as a layer of indirection to the true resource method implementations.
I also cleaned up a multitude of return statements within the Resource_Dispatcher.dispatch method that probably hurt more than a few people’s eyes. I then made capitalization a little more consistent and made string checking a little more robust by forcing all strings to lower case before comparing values. And, because my new Items.py class will be overriding Resources.py, and because within Resources.py, I have default implementations for all the methods that the dispatch method expects to find, I don’t need to worry about “looking before I leap” in the dispatch method, keeping its logic clean. Instead, I am guaranteed to have default implementations of all the methods that will provide appropriate debugging messages if they are called. Eventually, once I get this approach more nailed down, just raising Http404 for any unimplemented methods may be sufficient.
Also, look at the Resources.GET and Resources_Member.DELETE methods; by passing in the appropriate class information as part of the extra context information in restful_urls.py, and also by utilizing generic views, I now have some basic, but usable, default implementations of these RESTful methods, so a developer can start out simply writing the appropriate templates that Django looks for by convention, and override these resource methods later if needed. I won’t implement all the RESTful methods right away however, because I don’t want to suggest that the way that I have defined, say, the DELETE method is in any way a recommended default implementation of these methods. I don’t think I know Django’s generic views well enough yet to be able to suggest best practices for using them.
Now I can implement my Items class as a subclass of Resource and override its methods as needed. In fact, once all the generic views are implemented, the only requirement would be a minimal view module like this for Items:
from tagging_exploration.tagging.models import Items from tagging_exploration.tagging.views.Resources import Resources, Resource_Dispatcher, edit, create, dispatch class Items(Resources): class Items_Member (Resources.Resources_Member): pass
That’s it, and a smarter Python developer could probably make this easier still. But for now, all I did was import the corresponding model class, as well as my various RESTful classes and methods, then I subclassed the Resources and Resources_Member classes, which by convention, are expected to be named after the corresponding model class and be named “<model class name>_Member” respectively. Here’s a slightly more interesting example that overrides a few methods at a few different levels to verify that this approach basically works:
from tagging_exploration.tagging.models import Items from tagging_exploration.tagging.views.Resources import Resources, Resource_Dispatcher, edit, create, dispatch from django.http import HttpResponseNotFound class Items(Resources): def GET(request, resources_class=Items, model_class=Items): return HttpResponseNotFound(‘GET: Items list ‘) class Items_Member (Resources.Resources_Member): class Editor: # demonstrate overriding inner class methods def GET(self, request, id): # to help with initial debugging… return HttpResponseNotFound(‘GET: Items editor ‘)
So, now to implement this solution, you only need to copy restful_urls.py to your Django project root and then copy Resources.py into a views directory for each RESTful application. Configure settings.py and urls.py accordingly, and then subclass Resources as needed.
With these changes, I feel a little bit better about my approach to making Django more RESTful. I’ve hidden much of the complexity I introduced last week, and I’ve taken advantage of some of Django’s conventions and niceties that I felt I previously either ignored or subverted. Ultimately though, I still feel that the Django developers could do a much better job of implementing pure REST in an easier and better integrated way, or perhaps with a little more work I could refactor this out into a middleware contribution. (Do I hear an idea for next week’s post?) But, either way, I will continue to use this method for now, because I feel I’ve learned a lot from it, and I suspect it has more to teach me.
future posts
If you’ve actually followed this series of posts, then I should address the fact that the emphasis of the series is changing. Initially, I thought implementing a tagging application would be my primary focus while exploring things like REST would be minor diversions. Just the opposite has happened, as I am realizing that implementing tagging is a smaller problem that I thought it might be, while the minor diversions are not as minor as I had anticipated.
To do justice to these tangential topics, and to learn about them as thoroughly as I can in the limited time I can devote to them, I would like to take the time to fully explore these diversions, like I have with REST, even if it means that the series will go on longer and not be as tightly focused as it might otherwise be. Throughout, I will still be adding to the source code and coming back to the notion that it all the posts apply to a common application being developed.
I also encountered something interesting earlier this week while checking out recent submissions to Django Snippets–which if you haven’t visited yet, you definitely must. Someone has already tackled tagging in Django! (How do I keep missing these things?) And, even though the project seems to just be starting, its already well ahead of anything I would likely have come up with. This briefly took the wind out of my sails, but I quickly realized that this is actually a great opportunity. I can still blog about implementing this tagging contribution in what I have started, and do so less naively than I would have before starting this series. Also, I eventually wanted to explore various user interface approaches to tagging anyway, and by using this contributed application, I can explore this and other aspects of the application more fully.
The tagging application itself pointed me to some areas of Django I didn’t know existed, like generic relations, which is very cool. If I understand it correctly, it allows the tagging application to be used with your application’s model to tag anything you want. So, using the tagging application will be a learning opportunity in a number of ways, and hopefully make this series more interesting too.
resources
- source code for this post
- Django Documentation | URL Dispatcher: Passing extra options to view functions
- Django | Documentation | Model examples | Generic relations
- Django Generic Relations Made Easier by Net-X.
- Django Snippets
- django-tagging: A generic tagging application for Django.