2009-04-12

Design Pattern for Reusable Apps: Getting into Details

This article is an attempt to explain a technique for creating reusable and overridable apps that were partly mentioned in my last post.

First of all, let's remember the presentation by James Bennett about reusable apps from DjangoCon 2008:



Also the upgraded version Real World Django by Jacob Kaplan-Moss presented at PyCon 2009 should be checked.

Imagine, that you have a bunch of predefined apps somewhere under your python path. Some of them have models with relations to models from other apps. You will surely want to reuse those apps for different projects. You'll want to be able to activate the necessary set of apps. Probably sometimes you'll want to use an extended version of an existing app. As shown in the presentations given above, reusable apps should allow you easy overrides. Although all apps should be either under python path directly or under a package, nothing should be set in stone. The method get_model() should be used to import models from installed apps. But apps consist of more components than just models. There are urls, views, forms, template tags, context processors, middlewares, etc.

Let's have a look at a simple example. Two reusable apps could be events and venues where events would contain an Event model with a foreign key to Venue model from the venues app. In order to make both models overridable, I'll put the definitions in abstract base classes in the files base.py. The files models.py will import from base.py and create the leaf classes which will be the models to use further in forms and elsewhere.


  • venues

    • __init__.py

    • base.py
      from django.db import models

      class VenueBase(models.Model):
      title = models.CharField(...)
      street_address = models.CharField(...)
      postal_code = models.CharField(...)
      city = models.CharField(...)
      country = models.CharField(...)

      class Meta:
      abstract = True


    • models.py
      from venues.base import *

      class Venue(VenueBase):
      pass





  • events

    • __init__.py

    • base.py
      from django.db import models

      Venue = models.get_model("venues", "Venue")

      class EventBase(models.Model):
      title = models.CharField(...)
      venue = models.ForeignKey(Venue)
      from_date = models.DateField(...)
      to_date = models.DateField(...)

      class Meta:
      abstract = True



    • models.py
      from events.base import *

      class Event(EventBase):
      pass






Venues (as well as events) will probably have some urls, views, and forms which should be extensible too.


  • venues

    • ...

    • forms.py
      from django.db import models
      from django.forms import ModelForm
      Venue = models.get_model("venues", "Venue")

      class VenueForm(ModelForm):
      class Meta:
      model = Venue



    • views.py

      from django.db import models
      from django.utils import importlib
      from django.shortcuts import render_to_response

      # importing the form depending on the path of installed app
      venues_app = models.get_app("venues")
      venues_forms = importlib.import_module(venues_app.__name__[:-6] + "forms")
      VenueForm = venues_forms.VenueForm

      def add_venue(request):
      if request.method == "POST":
      form = VenueForm(request.POST, request.FILES)
      if form.is_valid():
      form.save()
      else:
      form = VenueForm()
      return render_to_response("venues/add_venue.html", {
      "form": form,
      })

      def change_venue(request, pk):
      ...



    • urls.py

      from django.conf.urls.defaults import *
      from django.db import models
      from django.utils import importlib

      # importing the form depending on the path of installed app
      venues_app = models.get_app("venues")
      venues_views = importlib.import_module(venues_app.__name__[:-6] + "views")

      urlpatterns = patterns('',
      ...
      url(r'^add/$', venues_views.add_venue, name='add_venue'),
      url(r'^(\d+)/$', venues_views.change_venue, name='change_venue'),
      ...
      )






You might ask why such a strange and tricky importing is done in the views and urls. If we know the path to the app, it is not going to change or something. Even James Bennett himself didn't see any advantages of importing forms and views dynamically when closing ticket #10703 for Django:
I don't really see the utility of this – a properly-written Django application is just a Python module, and is importable the same as any other Python module. It's not like that module is suddenly going to have a different path (and if it does, you're doing something wrong).


The answer hides in the overriding apps.

The combo of current events and venues might fit most of your projects. But then you might need a specific venues app with additional features for a specific project. Note, that you probably don't want to change or recreate the events app just because of the relation to the venues. The app and model names can be still the same, just put the new app under your new project and include the new app into INSTALLED_APPS instead of the original app. Project name will serve as a namespace for distinguishing the original app and the overridden app.

If you want to extend just the model, but not the other components, your specific app might look like this:


  • myproject

    • venues

      • __init__.py

      • models.py
        from venues.base import VenueBase

        class Venue(VenueBase):
        description = models.TextField(...)



      • forms.py
        from venues.forms import *



      • views.py
        from venues.views import *



      • urls.py
        from venues.urls import *







If the model is OK as is, but you need an additional behavior for the form, like saving an image for each venue, you can extend the form instead:


  • myproject

    • venues

      • __init__.py

      • models.py
        from venues.models import *



      • forms.py
        from django import forms
        from venues.forms import VenueForm as VenueFormBase

        class VenueForm(VenueFormBase):
        image = forms.ImageField(...)
        def save(self, *args, **kwargs):
        super(VenueForm, self).save(*args, **kwargs)
        ...



      • views.py
        from venues.views import *



      • urls.py
        from venues.urls import *







If models and forms are alright, but you want to add an additional view, you can do that too:


  • myproject

    • venues

      • __init__.py

      • models.py
        from venues.models import *



      • forms.py
        from venues.forms import *



      • views.py
        from venues.views import *

        def delete_venue(request, pk):
        ...



      • urls.py
        from venues.urls import *

        urlpatterns += patterns('',
        url(r'^(\d+)/delete/$', venues_views.delete_venue, name='delete_venue'),
        )







As you see from the case examples above, you don't have to duplicate any code of the original app. Still you can now enhance or overwrite just specific parts that you need very flexibly. The events could be extended in the same manner. If the original extensible apps live under a specific package, then the imports in the extending app should be changed appropriately.

Ufff.. That's about all what I wanted to explain this time. If you have any questions, suggestions or disagree with some concepts, please write a comment bellow.

If you reside in Berlin or going to be here next Wednesday by accident or so, don't miss the Django meetup at Schleusenkrug at 19:30.

Also have a nice Easter Holiday!

Django Easter Egg

2 comments:

  1. This looks great! Does this approach supersede what you wrote about earlier, with custom Creator constructor classes?

    ReplyDelete
  2. Actually both approaches supplement each other. If you extend two models where a relation should be automatically defined between them, then the creator can be used. BTW, a similar automagical relation creation might be also implemented using signals.

    ReplyDelete