Sunday, June 29, 2014

Django Recipe: Custom Url Dispatcher for views.

While refactoring our Django application I felt a need to make url.py less loaded and more readable.

The idea is to attach the information on view regarding the action it performs. This action name will be the part of the url while requesting the view. Views will be abstracted in a callable class extending 'BaseView' (similar to the one’s provided by Django). A 'dispatch' decorator will be applied on the new class view methods. 

Here is the sample HomeView of a feeds display application:

class HomeView(BaseView):

  @dispatch(request_type='GET', action='default')
  def getAllfeeds(self, request):
    template_file_path = 'templates/main.html'
    tmpl = os.path.join(os.path.dirname(__file__), template_file_path)
    return render_to_response(tmpl, {'feeds': Feed.objects.all()})

  @dispatch(request_type='POST', action='add')
  def addFeed(self, request):
    new_feed = Feed(title=request.POST.get('title'),
                    description=request.POST.get('description'))
    new_feed.save()
    return HttpResponse('Added Feed.')

Notice the dispatch decorator: param 'request_type' will be the http request method and ‘action’ will be a part of url while requesting a view.

And we’ll have single entry for all views within HomeView.
urlpatterns = patterns('',
    url(r'^(?P<action>\w{0,50})$', HomeView(), name='home')
)

A url for addfeed will be:
customapp.com/myapp/feeds/add

Using this approach I've noticed following benefits:

  • Views have become even more discrete. Otherwise we had multiple if..else or switch statements or  write multiple urls.py entries.
  • Views are more readable.
  • Urls.py is less loaded and hence more readable. 
  • Since I’ve a Base view class. This has provided me the power to implement pre and cleanup logic.

Here is the crux:

Dispatch decorator will save the request_type and action information inside each view method.

def dispatch(request_type, action):
  """Decorator to transform the view class methods to be treated 
     Django views.
  """
  def dispatcher(handler_to_call):
    def WrappedFunc(*args, **kwargs):
      return handler_to_call(*args, **kwargs)
    setattr(WrappedFunc, 'request_type', request_type)
    setattr(WrappedFunc, 'action', action)
    return WrappedFunc
  return dispatcher

‘BaseView’ is a callable class. At runtime we’ll inspect the members of the class and take out the member call relevant action and request type.

class BaseView(object):

  def __call__(self, request, *args, **kwargs):
    """Dispatch the url to relevant handler.

    Args:
      request: django.http.HttpRequest representing the request being handled.
      *args: args for method.
      **kwargs: keyword args for method.

    Returns:
      Returns response from the called handler.
      Returns HttpResponseNotFound when AttributeError or TypeError.
    """

    if not kwargs.get('action'):
      kwargs['action'] = 'default'

    callee = None;

    def is_view(member):
      return inspect.ismethod(member) and request.method.upper() == getattr(
         member, 'request_type', False) and kwargs['action'] == getattr(
         member, 'action', False)

    members = inspect.getmembers(self, predicate=is_view)

    for member_name, _ in members:
      callee = getattr(self, member_name)

    del kwargs['action']
    if not callee:
      return HttpResponseNotFound()
    else:
      return callee(request, **kwargs)

You can clone the Django app here: https://github.com/rohit0286/DjangoCustomURLDispatcherApp

Hope this helps.



No comments: