Setup inicial de um projeto Django 1.3

Depois de trabalhar em alguns projetos com o framework Django, eu passei a seguir uma série de padrões no setup inicial de uma aplicação, esse post descreve detalhadamente todo o processo.

Inicialmente eu tenho um diretório /home/allisson/Projects que é onde eu crio os meus projetos em Django, vamos começar criando um novo projeto:

$ cd Projects/
$ django-admin.py startproject teste
$ cd teste/
$ chmod +x manage.py # assim posso rodar ./manage.py no lugar de python manage.py

Inicialmente temos apenas quatro arquivos no diretório teste, vamos criar mais alguns arquivos e diretórios:

$ mkdir apps # diretório das aplicações do projeto
$ mkdir templates # diretório dos templates
$ mkdir media # diretório para os uploads de arquivos
$ mkdir static # diretório para uso do aplicativo staticfiles
$ mkdir sitestatic # diretório para arquivos estáticos do site (img, css, js)
$ touch apps/__init__.py # criando o arquivo init para deixar o diretório como um módulo em python

O principal arquivo é o settings.py, inicialmente ele tem essa estrutura:

# Django settings for teste project.

DEBUG = True
TEMPLATE_DEBUG = DEBUG

ADMINS = (
    # ('Your Name', 'your_email@example.com'),
)

MANAGERS = ADMINS

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
        'NAME': '',                      # Or path to database file if using sqlite3.
        'USER': '',                      # Not used with sqlite3.
        'PASSWORD': '',                  # Not used with sqlite3.
        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
    }
}

# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# On Unix systems, a value of None will cause Django to use the same
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/Chicago'

# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'

SITE_ID = 1

# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True

# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale
USE_L10N = True

# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
MEDIA_ROOT = ''

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
MEDIA_URL = ''

# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = ''

# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'

# URL prefix for admin static files -- CSS, JavaScript and images.
# Make sure to use a trailing slash.
# Examples: "http://foo.com/static/admin/", "/static/admin/".
ADMIN_MEDIA_PREFIX = '/static/admin/'

# Additional locations of static files
STATICFILES_DIRS = (
    # Put strings here, like "/home/html/static" or "C:/www/django/static".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)

# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
#    'django.contrib.staticfiles.finders.DefaultStorageFinder',
)

# Make this unique, and don't share it with anybody.
SECRET_KEY = 'wrqj$_b0lg!gfp+xlnn#zs^5vh*f0=+2e=e@4*=)&*f*4_wt_f'

# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.Loader',
    'django.template.loaders.app_directories.Loader',
#     'django.template.loaders.eggs.Loader',
)

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
)

ROOT_URLCONF = 'teste.urls'

TEMPLATE_DIRS = (
    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
)

# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': True,
        },
    }
}

No começo desse arquivo eu insiro o seguinte código:

import sys
import os.path

PROJECT_PATH = os.path.dirname(os.path.abspath(__file__))

sys.path.insert(0, os.path.join(PROJECT_PATH, 'apps'))

Esse trecho de código serve para duas finalidades:

1) Armazenar na variável PROJECT_PATH o diretório atual do seu projeto, isso é bastante útil para setar os caminhos para diretórios de templates e etc.

2) O diretório apps/ é adicionado no PATH do python, dessa forma se eu tiver um aplicativo em apps/meuapp eu posso chamar meuapp.models no lugar de teste.apps.meuapp.models.

Alterando a configuração do banco de dados:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(PROJECT_PATH, 'teste.sqlite'),
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    }
}

Caso você alterasse o ‘NAME’ para ‘teste.sqlite’ rodaria sem problemas localmente (pelo ./manage.py runserver), o problema ocorre quando você faz o deploy dessa aplicação onde é necessário o caminho completo do banco sqlite para funcionar corretamente. Essa configuração vai funcionar para os dois casos.

Alterando o MEDIA_ROOT e MEDIA_URL:

# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
MEDIA_ROOT = os.path.join(PROJECT_PATH, 'media')

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
MEDIA_URL = '/media/'

O MEDIA_ROOT guarda os arquivos que os usuários enviam para o site, usei o PROJECT_PATH novamente pelos mesmos motivos do banco de dados, não importa pra onde eu mova o meu projeto ele sempre vai apontar para o diretório correto.

Alterando o STATIC_ROOT e STATIC_URL:

# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = os.path.join(PROJECT_PATH, 'static')

# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'

O STATIC_ROOT guarda os arquivos estáticos do projeto (imagens, css, js etc) que são processados pelo aplicativo staticfiles (basicamente o staticfiles coleta todos os arquivos estáticos e armazena nesse diretório).

Alterando o STATICFILES_DIRS:

# Additional locations of static files
STATICFILES_DIRS = (
    # Put strings here, like "/home/html/static" or "C:/www/django/static".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
    ('site', os.path.join(PROJECT_PATH, 'sitestatic')),
)

Ao adicionar o sitestatic/ com o prefixo site informamos ao aplicativo staticfiles que adicione os arquivos desse diretório na url /static/site/, se for criado um arquivo sitestatic/file.txt ele vai poder ser acesso via url /static/site/file.txt

Alterando o TEMPLATE_DIRS:

TEMPLATE_DIRS = (
    os.path.join(PROJECT_PATH, 'templates'),
)

Essas são as alterações básicas no settings.py, além disso eu adiciono o seguinte trecho no final:

# ==============================================================================
# Local settings here
# ==============================================================================

# Flag to serve static files under media/ and static/
SERVE_STATIC_FILES = True

# ==============================================================================
# Load settings_local.py if exists
# ==============================================================================
try:
    execfile(os.path.join(PROJECT_PATH, 'settings_local.py'), globals(), locals())
except IOError:
    pass

Aqui temos duas funcionalidades:

1) SERVE_STATIC_FILES é uma flag criada para servir ou não arquivos estáticos dos diretórios media/ e static/ pelo Django. Eu deixo como True porque em ambiente de desenvolvimento nem sempre queremos subir um Apache/Nginx para servir esses arquivos. Eu também já vi gente usar o DEBUG como flag para isso, mas eu prefiro setar essa flag porque pode ocorrer que eu queira o DEBUG ativo e os arquivos estáticos não sendo servidos pelo Django.

2) Se existir um arquivo settings_local.py carregue o conteúdo dele, isso é bastante útil para modificações pessoais, por exemplo, eu quero rodar o mysql no lugar do sqlite, então eu vou e crio um arquivo settings_local.py com o seguinte conteúdo:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
        'NAME': 'teste', # Or path to database file if using sqlite3.
        'USER': 'root',                      # Not used with sqlite3.
        'PASSWORD': 'root',                  # Not used with sqlite3.
        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
    }
}

Quando eu rodar o ./manage.py runserver ele vai usar o mysql no lugar o sqlite que foi definido no settings.py.

Agora porque isso é útil? Quando for compartilhar esse código com outras pessoas via algum scm (git/mercurial/subversion), você vai deixar o settings.py padrão e caso queira fazer alguma modificação pessoal basta criar um settings_local.py e fica tudo resolvido. Claro que ao usar um scm você deve colocar o settings_local.py no ignore do scm, evitando que ele seja enviado por acidente para o repositório.

Alterando o urls.py:

from django.conf.urls.defaults import patterns, include, url
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.conf import settings

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
)

if settings.SERVE_STATIC_FILES:
    urlpatterns += patterns('',
        (r'^media/(?P<path>.*)$', 'django.views.static.serve', 
            {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}),
    )
    # staticfiles
    urlpatterns += staticfiles_urlpatterns()

Nesse urls.py eu adicionei a rota para aplicação admin (lembre de adicionar o admin na lista de apps do seu projeto) e no final do arquivo eu checo se a flag SERVE_STATIC_FILES é True, caso seja eu adiciono as rotas para servir arquivos estáticos pelas urls /media/ e /static/

Para configurações de deploy eu crio um arquivo settings_production.py:

# Load settings first
try:
    from settings import *
except ImportError:
    pass
 
# Now override any of them
DEBUG = False
SERVE_STATIC_FILES = False

Esse arquivo importa todas as configurações do settings.py e a partir disso você pode sobrescrever alterações de forma semelhante ao settings_local.py, a diferença é que o settings_production.py deve ser enviado ao repositório scm. Nesse exemplo eu desliguei as flags de DEBUG e SERVE_STATIC_FILES.

Para parte de testes eu crio um arquivo runtests:

#!/bin/sh
./manage.py test meuapp

Depois eu deixo esse arquivo com permissão de execução:

$ chmod +x runtests

O problema quando você roda o ./manage.py test é que ele vai testar os seus aplicativos e praticamente todo o framework Django e aplicativos de terceiros.

Claro que você não necessita rodar os testes do Django e de aplicativos de terceiros, os únicos testes importantes são os dos seus aplicativos.

O comando ./manage.py test meuapp roda os testes para o aplicativo meuapp, agora imagine que você tem vinte aplicativos no seu projeto, se você for rodar esse comando vai ser difícil lembrar de todos os aplicativos, então pra resolver isso eu crio um arquivo runtests onde eu sempre fico atualizando o comando a medida que novos aplicativos são criados ou removidos.

E por final eu crio um arquivo requirements.txt:

Django==1.3
MySQL-python>=1.2.2
PIL>=1.1.7

Nesse arquivo eu informo as depêndencias do meu projeto, nesse caso eu coloquei como dependências o Django versão 1.3, o MySQL-python versão 1.2.2 ou maior e o PIL (python-imaging) versão 1.1.7 ou maior.

Quando alguem baixar o código do projeto, basta rodar o comando:

$ pip install -r requirements.txt

Com esse comando o pip vai ler esse arquivo e instalar todas as bibliotecas listadas nele.

É isso aí pessoal, esse é o meu setup básico para novas aplicações em Django 1.3.