Anatomy of a Django Project
Mark Lavin - DjangoCon 2014
@DrOhYes
Introduction
Technical Director at Caktus Group in Carborro, NC
Co-Author of "Lightweight Django" for O'Reilly
Started dozens of Django projects and worked on existing code
bases for dozens more. I've seen a lot of methods of
code/project/settings project configurations. Some good.
Some bad.
I'm here to answer one central question...
What is a Django project?
A related question is "what is a Django app?" and I'll touch on that as well.
Apps provide a piece of functionality or interaction.
Some apps are reusable and some of them aren't like those
custom Lego pieces.
Many apps can't really run on their own and need other apps
and some global (framework) configuration.
For this talk apps are any Python package which is added to the
INSTALLED_APPS setting. Usually it will have a models.py though
that's no longer a requirement in Django 1.7
A project configures and glues apps together.
This is a high level idea but most people first encounter a "project"
via startproject.
startproject
user@host:$ mkvirtualenv myproject
New python executable in myproject/bin/python
Installing setuptools, pip...done.
(myproject)user@host:$ pip install django
Downloading/unpacking django
...
Successfully installed django
Cleaning up...
(myproject)user@host:$ django-admin.py startproject myproject
(myproject)user@host:$ tree myproject/
myproject/
├── manage.py
└── myproject
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
1 directory, 5 files
So what is the project? The outer directory? The myproject package?
Let's look at each file at see what fits the description of a "a collection of settings".
manage.py
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
django-admin.py
#!/usr/bin/env python
from django.core import management
if __name__ == "__main__":
management.execute_from_command_line()
This doesn't look like settings. This is a thin wrapper around execute_from_command_line
The only project specfic thing it does is set DJANGO_SETTINGS_MODULE to use
the new settings file.
Compare this to django-admin. If we weren't so lazy we could set this
varible ourself and just use django-admin.
No manage.py
(myproject)user@host:$ cd myproject/
(myproject)user@host:$ add2virtualenv .
(myproject)user@host:$ echo "export DJANGO_SETTINGS_MODULE=myproject.settings"
>> $VIRTUAL_ENV/bin/postactivate
(myproject)user@host:$ echo "unset DJANGO_SETTINGS_MODULE"
>> $VIRTUAL_ENV/bin/postdeactivate
(myproject)user@host:$ deactivate
user@host:$ workon myproject
(myproject)user@host:$ rm manage.py
(myproject)user@host:$ django-admin.py runserver
If you are using virtualenv/virtualenv-wrapper this is easy to do.
Step 1: Add current project path to the virtualenv path
Step 2: Set the variable when the virtualenv is activated
Step 3: Unset the variable when the virtualenv is deactivated
deactivate/reactivate to see it work
manage.py clearly isn't needed. It really only sets an environment variable.
manage.py is not part of this mythical "project".
This inner "myproject" must be the project.
settings.py
"""
Django settings for myproject project.
For more information on this file, see
https://docs.djangoproject.com/en/1.6/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.6/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
...
This is definitely a collection of settings. No question that
whatever a Django project is. This is part of it.
Note that BASE_DIR says "Build paths inside the project like this"
and points to the parent directory of this file. manage.py was in
that folder...maybe it is part of the project?
urls.py
from django.conf.urls import patterns, include, url
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Examples:
# url(r'^$', 'myproject.views.home', name='home'),
# url(r'^blog/', include('blog.urls')),
url(r'^admin/', include(admin.site.urls)),
)
This is something between code and configuration. Routing
URLs to callables or delegating that responsibilty to another
set of these mappings (include).
This is based on 1.6 (admin lines are needed in 1.7)
1.8 will deprecate patterns and passing strings in to url
wsgi.py
"""
WSGI config for myproject project.
It exposes the WSGI callable as a module-level variable named
``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
Similar to manage.py. Configures one environment variable and
calls one function. Isn't really part of the Django project. This is
an artifact the WSGI deployment.
settings.py (Revisited)
...
ROOT_URLCONF = 'myproject.urls'
WSGI_APPLICATION = 'myproject.wsgi.application'
...
The ROOT_URLCONF and WSGI_APPLICATION are configured by the
settings. These don't have to live where they are. They can
live anywhere on the PYTHONPATH as long as these references
are updated.
Here you can see the settings is the only thing that really
stands alone and the other files live in a namespace constructed
entirely out of convention.
So what are some questions about Django projects?
Common Questions
Where should site-wide templates/static resources live?
Should apps live inside the project?
Can a project have views?
Can a project have models?
These all have one answer...
There is no such thing as a Django project. There
are only settings files. Any given project/deployment is likely
to have more than on set of settings.
TEMPLATE_DIRS
By default this will be discovered by the loader before any app
template directories. Can be used to override app templates or
add additional templates. This is empty by default.
Django does not care how many of these there are (can be more than one)
or where they live on the file-system (you have to give the full path).
STATICFILES_DIRS
Nearly identical to templates.
By default this will be discovered by the loader before any app
static directories. This is empty by default.
Django does not care how many of these there are (can be more than one)
or where they live on the file-system (you have to give the full path).
Django only cares that the app is on your PYTHONPATH.
Namespaces are optional.
A better question might be: should my "project" and my app be
in the same code repository.
If namespacing under the app makes sense to you do it.
If not, then don't.
Zen of Python would say "flat is better than nested."
Proposed alternative layouts
startproject Templates
django-admin.py startproject newproject --template=<path-to-tempalte>
These are just sketches/skeletons but they can be made into
full examples.
App/Project Layout
project_name
├── project_name
│ ├── __init__.py
│ ├── admin.py
│ ├── models.py
│ ├── static
│ │ ├── css
│ │ ├── img
│ │ └── js
│ ├── templates
│ ├── tests.py
│ └── views.py
├── settings.py
├── urls.py
└── wsgi.py
No manage.py
Top level dir is not a package
- which caused double import problems pre 1.4
project_name namespace for settings/urls/wsgi is removed
- could cause name collisions if multiple project on the PATH
inner project_name package is now an app
App/Project Settings
...
BASE_DIR = os.path.dirname(__file__)
...
INSTALLED_APPS = (
'{{ project_name }}',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
)
...
ROOT_URLCONF = 'urls'
WSGI_APPLICATION = 'wsgi.application'
...
BASE_DIR removes one parent directory reference
newproject app is the first installed app so that its templates/static
are discovered first by the loader.
ROOT_URLCONF and WSGI_APPLICATION updated to remove the namespace.
Micro Project Layout
project_name
└── project_name.py
Micro Project
import os
import sys
from django.conf.urls import url
from django.http import HttpResponse
os.environ.setdefault("DJANGO_SETTINGS_MODULE", __name__)
DEBUG = True
SECRET_KEY = 'foo'
ROOT_URLCONF = [
url(r'^$', lambda x: HttpResponse('Hello World'), ),
]
if __name__ == "__main__":
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
Something like this has been done a number of times by now.
Please don't do this but please know that this can be done.
There are more realistic ways to make this work without impossible
circular imports.
Turnkey App Layout
project_name/
├── project_name
│ ├── __init__.py
│ ├── admin.py
│ ├── models.py
│ ├── static
│ ├── templates
│ ├── tests.py
│ ├── views.py
│ └── wsgi.py
└── setup.py
Run a single app project (micro-service).
Usable as both a reusable app and standalone WSGI application.
Turn-key App wsgi.py
from django.conf import settings
if not settings.configured:
# Default settings
settings.configure(
ROOT_URLCONF='{{ project_name }}.urls',
INSTALLED_APPS=(
'{{ project_name }}',
...
),
)
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
Uses only the urls from the app.
Other settings might be configured by the OS variables, config
file parsing, etc
Turn-key App Deployment
(turn-key)user@host:$ pip install project_name gunicorn
(turn-key)user@host:$ gunicorn project_name.wsgi
Again this requires sane defaults with some configuration via
OS (or config file).
Has the ability for full settings override.
I would love to set this for blog/CMS apps which are already
blur the line between app/project
Please stop worrying and debating about this and let's make
cool stuff with Django.
Django settings define a "project" and a "project" **may** have more
than one set of settings.
Settings point to a ROOT_URLCONF which again there **can** be more
than one.
The myproject package is a namespacing convention and is in
no way required.
Photo Credits
https://www.flickr.com/photos/bdesham/2432400623
https://www.flickr.com/photos/toomuchdew/4074047756
https://www.flickr.com/photos/calavera/65098350/
https://www.flickr.com/photos/starsalive/4498944830/
https://www.flickr.com/photos/macbeck/3985839229/
https://www.flickr.com/photos/alexblanck/5557452102/
https://www.flickr.com/photos/cdevers/5609962642/