Django is a web framework for quickly creating scalable fault torrent web applications using python.

#Getting Started

You're gonna need to have python 3 installed. I also recommend a package manager such as poetry to maintain project dependencies. After you've got both those you can bootstrap a new project named foo in the cwd.

poetry init
poetry add django
poetry run django-admin startproject foo .

This should've generated a directory structure like:

.
├── foo
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── poetry.lock
└── pyproject.toml
  • poetry.lock and pyproject.toml are both used by poetry to declare this as a python project.
  • manage.py is a administrative script that gives you control over your application.
  • foo is the main package for your webapp. Settings go into settings.py and routes are configured in urls.py.

You can start a basic web server for your application by running:

./manage.py runserver

#Apps

Django adopts a pluggable system reliant on python modules called apps. You can create a new app using manage.py.

./manage.py startapp bar

This should've created a new package in your root directory called bar.

bar
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py
  • views.py is where we define handlers for requests. I.E. where we decide responses for requests.
  • tests.py is where we create application tests. I won't be covering it in these notes but feel free to look here and here.
  • models.py is where we declare the database tables and schema for our app.
  • migrations is where django will automatically place recipes for updating databse changes. You should commit this directory into VC.
  • apps.py is where you configure this application
  • admin.py is where you can register this apps models for use in the admin dashboard.

Once you've created a new app (or installed one from somewhere else) navigate to settings.py and add the apps import path to INSTALLED_APPS.

INSTALLED_APPS = [
  ...,
  'bar',
]

Now Django will make sure to use the apps settings and build it's migrations when running from manage.py.

#Views

Django supports both class and function based views. The class views come in useful when you want to extend some existing logic or share some handling across multiple views. I'll go more into this later.

More often though you'll be writing django views as functions. Let's try making a view in our views.py file for bar.

from django.http import HttpResponse

def home(req):
  print(req)
  return HttpResponse('hello world')

This simple view prints the request we get and then returns a simple response that says hello world. Now we need to connect this view to some path on our site. Go to urls.py and add a path for home.

from bar.views import home

urlpatterns = [
  ...,
  Path('home/', home),
]

Now if you start the server and goto localhost:8000/home it should print out hello world.

#Decorators for Filtering

Django has a bunch of shortcut decorators for automatically handling requests that don't have the right structure or format. For example:

from django.views.decorators.http import require_POST

@require_POST
def my_awesome_view(req):
  return HttpResponse('you're in')

Now only post requests make it into the body of the view. Others are immeadiately discarded.

#Grouping Urls

However here we've imported a view from an app and routed it manually. Your apps are likely to have dozens of views and already have a shared understanding of how they should be mapped. You can use the include function to include an entire group of views in a subpath.

Lets add another view to views.py.

def baz(req):
  return HttpResponse('baz bag bam boom')

And lets add a urls.py file in bar which sets up the routes for the bar app.

from .views import home, baz
from django.urls import path, include

urlpatterns = [
    path('home/', home),
    path('baz/', baz),
]

And lets change our previous route for home in foo/urls.py to include these urlpatterns under a new path.

from django.urls import path, include

urlpatterns = [
  ...,
  path('bar/', include('bar.urls')),
]

Now we can goto localhost:8000/bar/home to see the home view or localhost:8000/bar/baz to see the baz view. You can nest includes like this for as many apps as you want.

#Namespacing Routes

You can give a view a name attribute to reference it in templates or with the reverse function.

urlpatterns = [
    path('home/', home, name='home'),
    path('baz/', baz, name='baz),
]

Now we can run reverse('home') to get the URL for the home route, we can also supply args or parameters for the request to the URL. There's also a resolve function to convert a URL path to a view function.

For some added privacy when working with common names such as home or login you can define a variable in your apps urls.py file to add another namespace for the reverse function to work.

app_name = 'my-super-cool-app'
urlpatterns = [
  ...,
]

Now each route with a name in urlpatterns requires a my-super-cool-app: prefix when reversing. So reverse('home') is now reverse('my-super-cool-app:home').

#Class Views

Django supports generic class based views for automating a lot of the boilerplate in creating a view. For example you can automatically generate a login page from the appropriate User model. Each of these generic views has a .as_view() function to turn them into function views when using them in a path.

#Templates

Django has it's own mustache like [templating][templates] engine with tags and filters. See here for a list of available tags and filters. Django also supports letting you use other templating engines.

There's a list of lookup rules for where django looks for a template. By default it'll look in a templates directory in the root of your project and in every subproject directory.

NOTE: Each templates import path is from the root of the directory in it's lookup path. If you put a template at bar/templates/foo.html you import it with foo.html. It's recommended to namespace templates with the app their for, so for bar put your templates in bar/templates/bar/foo.html and import as bar/foo.html.

The general work flow for loading a template file and evaluating it is:

from django.http import HttpResponse
from django.template import loader

def my_view(req):
  t = loader.get_template(bar/index.html')
  return HttpResponse(t.render({}, req))

First we load the template, then we evaluate the template with a context dictionary and our request. This process is so common that there's a shortcut function to automatically fetch the template and evaluate it into a HTTP Response.

from django.shortcuts import render

def my_view(req):
  return render(req, 'bar/index.html', {})

#Forms

Django lets you define forms using plug and play python objects. This powerful paradigm also gives you automatic verifiers. Lets try it out. First we create a subclass of django.forms.form.

from django import forms

class NameForm(forms.Form):
  name = forms.CharField(label='Your name', max_length=100)

Now in our views we instantiate this form:

from .forms import NameForm

from django.shortcuts import render
from django.http import HttpResponseRedirect

def my_view(req):
  if req.method == 'POST':
    form = NameForm(req.POST)
    if form.is_valid()
      return HttpResponseRedirect('/thanks/')
    else:
      form = NameForm()

    return render(request, 'name.html', {'form': form})

And in our template we can just insert our form instance directly and django will automatically include the fields alongside their current data and any validation error messages etc.

<form action="/my-view/" method="post">
  {% csrf_token %}
  {{ form }}
  <input type="submit" value="Submit">
</form>

Did you catch what we did there? It was kind of a lot all at once so let's go over it in detail. First we create a form class. This class supports a bunch of form fields that each have their own form widgets and options.

Then in our view we check if the user is POSTing, if so we take the post data and populate a new form instance with it. We then check whether all the data is OK, if it is then we redirect to a success page, otherwise we return the original page with our updated form. If the user wasn't posting to begin with we return an empty form.

Then finally in the templates we include the forms fields in a form tag alongisde a CSRF token and submission button.

#Models

A models is djangos abstraction over your database and the data within it. A django model is just a class (deriving from django.db.models.Model) which contains a bunch of fields. Each field has a bunch of shared-options and some unique ones. The API is strikingly similair to Forms.

from django.db import models

class Musician(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    instrument = models.CharField(max_length=100)

class Album(models.Model):
    artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)
    release_date = models.DateField()
    num_stars = models.IntegerField()

You can define relations between models using ForeignKey, OneToOneField and ManyToManyField.

Each model class can also define a Meta subclass to specify options that the model can support. An exhaustive list of such options can be found here. The most important ones are:

  • abstract, when true a table won't be made for this model. Only non-abstract subclasses of this model will have generated tables. See here.
  • base_manager_name the name of the manager from which you can query instances of this model from the db. Defaults to objects.
  • ordering, how objects of this model should be ordered by default.
  • verbose_name and verbose_name_plural. Human readable name for the model, eg. pizza and pizzas.
  • proxy, makes this model like an abstract model except one table is made for this model and that same table is used by classes that extend this model. This is useful when you want to define methods or accessors for a model but only in a unique circumstance. See here.

#Inheritance

By default when you inherit a non-proxy, non-abstract model, django will create an implicit OneToOneField connecting each submodel to it's parent model. You can access the fields in the parent table from the child table directly, even though they'll exist in seperate databse tables. See more here.

#Methods

Like with any other class you can define methods on a model and it'll be available in model instances from a REPL or templates.

class MyUser(models.Model):
  first_name = models.CharField(max_length=50)
  last_name = models.CharField(max_length=50)

  @property
  def full_name(self):
    return f"{self.first_name} {self.last_name}"

#Migrations

Every time you make a change to any of the models in your django app, you need to migrate those changes to your database. Django will warn you if you try to run the server while having pending migrations.

./manage.py makemigrations
./manage.py migrate

#Managers

Managers are how you can interface with your database from a model. They let you create new model objects and query existing models.

For example:

# You can create a new model by creating a new instance
p = Pizza(name='super meat lovers')
# You access and modify fields just like regular python.
p.name = 'EXTRA' + p.name
# django won't save changes automatically, explicitly call save()
# to persist modifications to the db.

# You can use a manager to access the database in one step.
# The create method automatically saves a new row.
Pizza.objects.create(name='vegan supreme')

# You can get all entries as a lazy QuerySet with:
Pizza.objects.all() #=> [Pizza(...), ...]

# You can refine a queryset with filter and exclude.
# django lets you use kwarg arguments seperated by double
# underscores to specify query parameters like this.
Author.objects.all().filter(birthday__year__lt=2005)
Author.objects.filter(birthday__year__lt=2005)

# QuerySets are immutable, each refinement produces a new
# query set.
q1 = Author.objects.all()
q2 = q1.filter(name__exact="RealName")
q1 == q2 #=> False

# You can extract values from a query set with an array lookup.
# if the value doesn't None will be returned.
q2[0] #=> None
# A stricter way to get the first result of a query set is with the
# get() method.
q2.get() # Error Author.DoesNotExist
# You can also call .get() on the manager directly.

# Every model has a pk field which refers to the primary key for
# the model.
Autho.objects.get(pk=1)

# Let's create a few authors
john = Author.objects.create(name="John")
paul = Author.objects.create(name="Paul")
george = Author.objects.create(name="George")
ringo = Author.objects.create(name="Ringo")
entry.authors.add(john, paul, george, ringo)

For a list of supported field lookups with querysets, see here. For lookups that can span across OneToOne or ManyToMany fields, see here.

#F-Expressions

F-Expressions are a way to compare values of a model field with another field in the same model.

from django.db.models import F

Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks'))

Here we get all the objects in Entry where the number of comments they have is larger than the number of pingback they have. Both these two fields are part of the model itself.

#Q-Expressions

Multiple calls to filter or exclude in a QuerySet has different behaviour depending on how the call is made. See here. By default q.filter(A,B) filters in all rows that match A and B at the same time whereas q.filter(A).filter(B) filters in all rows that match A or match B but not necessarily both at the same time.

Q-Expressions give you a way to build more complex queries without requiring them to be Anded together in q.filter(A,B). A Q expression just holds a query such as:

Q(question__startswith='Who')

You can then OR this query with another query such as:

Q(question__startswith='Who') | Q(question__startswith='What')

Now when we pass this to our models manager:

Poll.objects.get(Q(question__startswith='Who') | Q(question__startswith='What'))

We get a poll object that starts with Who or starts with What.