I try my best to write reusable Django apps. Now I\'m puzzled how to put them all together to get the final project.
Here is an example of what I mean: I have a pict
I am a Django newbie and I faced the same problem. It is shameful how there's so much buzz about reusable apps in the Django community, but not a single authoritative reference on how to connect them in a project without hard coded dependencies.
I have been working on a new Django project and I wanted to set up my models to avoid hard coding as much as possible. This is the pattern that I used.
Layout
project_root/
core/
models/
mixinmodels1.py
mixinmodels2.py
...
utils.py
...
app1/
models/
__init__.py
base.py
basemixins.py
mixins.py
concrete.py
/signals
__init__.py
handlers.py
utils.py
...
app2/
...
...
App models
base.py : This module implements only the 'reason for existence' of that app in abstract classes.
Rules are:
This module usually only imports from the core app. It usually does not import anything from other apps in the same project.
An exception to the above rule is when the 'reason for existence' assumes the existence of another app. For example, a group app assumes that there is an user app somewhere. In this case, the way to link them is:
# project_root/settings.py
AUTH_USER_MODEL = 'app_label.UserModel'
# project_root/groups/models/base.py
from django.conf import settings
and then using settings.AUTH_USER_MODEL to refer to the user model
Use this pattern for all apps, not just the user app. For example, you should also do
# project_root/settings.py
GROUP_MODEL = 'app_label.GroupModel'
If using the above pattern, only assume the functionality provided by base.py of the other app
to which you are linking. Do not assume the functionality of elaborate concrete classes (I will discuss where to put concrete classes shortly)
Of course, importing from django, third party apps and python packages is allowed.
Make sure that the assumptions that you make in base.py of any app is rock solid and would not
change much in the future. A good example is django-registration by James Bennett. Its an old app, but its appeal did not wane because it made rock solid assumptions. Since good reusable apps do one thing well, it is not difficult to find that set of assumptions.
basemixins.py: This module should implements plugs to the concrete models of that app. A 'plug' to a model M is any model that contains a foreign key to the model M. For example:
# project_root/groups/models/basemixins.py
from django.conf import settings
from django.db import models
class BaseOwnedByGroup(models.Model):
"""
This is a plug to the group model. Use this
to implement ownership like relations with
the group model
"""
owner = models.ForeignKey(settings.GROUP_MODEL,
related_name = '%(app_label)s_%(class)s_owner',
verbose_name = 'owner')
# functionality and manager definitions go here.
class Meta:
abstract = True
app_label = 'groups'
BaseOwnedByGroup is a 'plug' to the group model. The rules here are the same as 'base.py`
basemixins.py, only assume functionality provided by base.py.core, django, third party apps and python packages.mixins.py : This module should be used for two purposes
To define elaborate 'plugs', which assumes the functionality of the elaborate concrete classes but not the relationships with other apps. The elaborate plugs should ideally inherit one of the 'base plugs' defined in basemixins.py.
To define mixin models (that are not 'plugs') which can be used by the concrete classes of that app.
concrete.py : This module should be used to define (you guessed it) concrete classes of that apps and to set up relationships with other apps. In short, this module assumes your project and all the
functionality you want to provide in it.
Relationships to other apps should be set up as follows:
To establish a one to one or many to one relationship with model M of app another_app, do the following:
# project_root/another_app/utils.py
def plug_to_M_factory(version_label):
"""
This is a factory method which returns
the plug to model M specified by
version_label
"""
if version_label == 'first_version':
from another_app.models.basemixins import BasePlugToM
return BasePlugToM
if version_label == 'second_version':
from another_app.models.mixins import PlugToM
return PlugToM
...
# project_root/groups/models/concrete.py
from groups.models.base import BaseGroup
from another_app.utils import plug_to_M_factory
PlugToMClass = plug_to_M_factory(version_label = 'second_version')
class ConcreteGroup(BaseGroup, PlugToMClass):
# define your concrete model
class Meta:
app_label = 'groups'
To establish a many to many relationship, the recommended way is to use a through model. Inherit the correct plug in the through model in the exact same way (as we did in the ConcreteGroup model)
signals : While setting up relationships, often we have to perform operations on a model M of app app1, when model N of app app2 changes. You can use signals to handle that. Your handlers can assume the functionality of the concrete sender, but often they don't need to. Assumption of the base version of the sender in base.py is enough. This is why it is a good idea to always use settings.ModelName to refer to a concrete model. You can extract out the model class from the settings.ModelName string by either using ContentType or using a project wide get_model_for_settings function as follows:
# project_root/project/utils.py
from django.db.models import get_model
from django.core.exceptions import ImproperlyConfigured
def get_model_from_settings(model_string):
"""
Takes a string of the form 'app_label.model' as input, returns the
appropriate model class if it can find it.
"""
try:
app_label, model_name = model_string.split('.')
except ValueError:
raise ImproperlyConfigured("function argument must be of the "
"form 'app_label.model_name', got '%s'" % model_string)
model = get_model(app_label, model_name)
if model is None:
raise ImproperlyConfigured("function argument refers to model "
"'%s' that has not been installed" % model_string)
return model
core : The core app is a special app which stores project wide mixin functions.
These mixins should not assume anything about any other app. Only exception to this rule is mixins that rely on base functionality of settings.AUTH_USER_MODEL. This is because you can safely assume that most projects will have an user model.
Of course imports from django, third party and python packages are still allowed
Remember that all base.py and basemixins.py modules are allowed to import from core.
Finally for everything to work as intended, import your models in models/__init__.py of every app.
The advantages that I find from following this scheme is:
The models are reusable. Anyone can use the abstract base models and mixins to design their own concrete model. base.py, basemixins.py and related factory methods can be bundled together with a bare bones concrete model and shipped in a reusable app.
The apps are extendable. All mixins are versioned and there is a clear inheritance scheme.
The apps are loosely coupled. External mixins are accessed via factory methods and external models are referred to using django.conf.settings.
The apps are self contained. Any changes in an app will most likely break that app and that app only. Other apps will most likely remain unscathed. Even if external apps break, the place where this could happen is clearly marked.
I have been using this scheme to reduce coupling between my apps. I am not an experienced Django programmer, and I have a lot to learn, so any feedback is appreciated.