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!