Day 2-3: Back to Django

August 05, 2016

The last couple of days have been relatively productive. Thanks to GitHub's excellent API documentation, I've got to the stage where I can reliably authenticate a user with my application via Oauth, and use their key to pull basic user info such as their private email address and GitHub ID. In this post I'll talk briefly about the setup of the application and some of the design choices I've made so far.

I've written the server element of the project using Django 1.10. I went with Django mainly because of my familiarity with the ecosystem and the ease of setting up REST endpoints with Django REST Framework (DRF), which is definitely something I'll want to do further down the line.

Layout

The project layout is slightly different from the standard created by Django's startproject command. For simplicity's sake, I prefer to put settings.py, wsgi.py etc in the root directory of the Django application, rather than having them in a separate, named folder. As a result, my tree looks like this:

.
├── alexandria_server
│   ├── permissions
│   │   ├── migrations
│   │   │   └── 0001_initial.py
│   │   ├── models.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── docker-compose.yml
├── Dockerfile
├── manage.py
└── requirements.txt

All of the configuration and deployment code lives at the project root, along with manage.py. The application root (alexandria_server) contains settings, base URL configuration and WSGI info, as well as any app directories.

Configuration

It was an interesting experience trying to see how few of Django's built-in modules were necessary for it to work. I've built Django projects before without many of the contrib apps, but I think this was the first time that I'd completely removed the auth app. Since the only login will be through GitHub initially, the user model in the application can be very basic and thus doesn't require the password hashing and other helpful features that Django auth provides.

Here's my extremely minimal settings.py file currently. Initially, DRF was pretty unhappy with my removal of the auth app, as it tries to assign an AnonymousUser object to the request on an unauthenticated call. However, the UNAUTHENTICATED_USER setting can be changed to another object, or None, which circumvents this issue.

"""
Django settings for alexandria_server project.

Generated by 'django-admin startproject' using Django 1.10.
"""

import os
import dj_database_url

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

SECRET_KEY = os.environ.get('SECRET_KEY')

if os.environ.get('DEBUG'):
    DEBUG = True
else:
    DEBUG = False

GITHUB_CLIENT_ID = os.environ.get('GITHUB_CLIENT_ID')
GITHUB_CLIENT_SECRET = os.environ.get('GITHUB_CLIENT_SECRET')

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    # Core
    'django.contrib.contenttypes',
    'django.contrib.messages',
    # Third-party
    'rest_framework',
    # Internal
    'alexandria_server.permissions',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'alexandria_server.urls'

WSGI_APPLICATION = 'alexandria_server.wsgi.application'


# Database
DATABASES = {
    'default': dj_database_url.parse(os.environ.get('DATABASE_URL')),
}

# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

# DRF
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [],
    'DEFAULT_PERMISSION_CLASSES': [],
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
    ),
    'UNAUTHENTICATED_USER': None
}

Only a couple of interesting things worth pointing out here. Firstly, as you can see, most of the default Django apps are gone. I'm not entirely sure if I need contenttypes or messages either, so those may disappear.

I'm using Kenneth Reitz's dj-database-url to easily populate the configuration for the default database. I like this approach because it makes it trivially easy to set your DB locally, or in the cloud, simply by changing the DATABASE_URL environment variable.

Finally, I'm going for the classic 12 factor app approach to settings, so all variables and secrets are pulled in from environment variables. I prefer this tactic to the alternative of having a settings.py file per environment, perhaps not checked in to source control. I feel like it gives more flexibility but also more consistency between environments.

Database

My go-to database choice for Django has always been Postgres. They play together very nicely, and Postgres has a host of awesome features which are fully supported in Django that no other database has. It's also extremely easy, thanks to Docker, to run a Postgres database locally in a container. Here's the setup in my docker-compose.yml:

db:
    image: postgres:9.5
    environment:
        - POSTGRES_PASSWORD=some_password
        - POSTGRES_USER=some_user

It's amazing to be able to spin up a local database with only 5 lines of configuration code.


In a separate post, I'll talk about the Oauth flow in more detail. If you have any questions or comments about project layout, or configuring a basic Django setup, let me know in the comments.