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 *
- venues
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 *
- venues
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'),
)
- venues
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!
This looks great! Does this approach supersede what you wrote about earlier, with custom Creator constructor classes?
ReplyDeleteActually 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