UP | HOME

Wed Dev Crash Course - Django, Heroku, Bootstrap

Table of Contents

This article teaches you how to make a traditional web application using Django.

Is begins with a "Quickstart" section to teach you how to make a simple app as quickly as possible. Then it moves on to cover several more important topics such as:

This article is written to be concise and practical. However is is not hand-holdy and assumes you already know the basics of web apps and are running a UNIXy environment.

I don't include anything that's not important so you will get the most out of this article if you work through it slowly and throughly, supplementing it with your own research in-case there is something you don't understand.

1 Quickstart

This section aims to get a basic app up and running and cover the most fundamental principles. We build the same app as in the official tutorial, but much more quickly. It takes certain shortcuts such as using incomplete HTML, but we will rectify that in a later section.

We will create a basic poll application. It consists of two parts:

  • a public site that lets people view polls and vote in them
  • an admin site that lets you add, change, and delete polls

1.1 Django project setup

1.1.1 Choose a name

Choose a name for your project:

  • avoid names of built-in Python or Django components
  • I recommend using only lowercase letters and underscores

1.1.2 Setup Python Environment

Make a directory for our new project, install django using pipenv and confirm:

export SITENAME=  # the name you chose
mkdir $SITENAME
cd $SITENAME
git init
cat <<EOF >.gitignore
__pycache__
venv
.env
staticfiles/
EOF
pipenv install django
pipenv shell
python -m django --version

๐Ÿ›ˆ don't forget to re-run pipenv shell when you work on the project in future as it sets up your environment.

1.1.3 Generate scaffold

Django has a command-line tool called django-admin. We use it to scaffold out our project:

django-admin startproject $SITENAME .

See that there is a script called manage.py. This is a wrapper for django-admin which sets some environment variables for your project. You will always use this from now on.

We will use manage.py now to test the Django development server:

python manage.py runserver

You will see an error about unapplied migrations. This is because Django has some default "apps" enabled by defualt (see $SITENAME/settings.py), and these require certain database structures.

๐Ÿ›ˆ to save you having to cancel and re-run runserver repeatedly I recommend opening an extra terminal so you can leave it running (don't forget to run pipenv shell).

1.1.4 Adding our database

We'll fix those migration errors now by adding a database. Before proceeding please:

  • setup PostgreSQL locally with peer authentication with this procedure
  • bookmark the the Django database docs
  • pipenv install psycopg2. If it fails you may need to install libpq-dev from your package manager

Now create your database using postgres's createdb command (replacing EDITME with a name):

createdb EDITME

Open $SITENAME/settings.py. You can see an array "DATABASES". By default it uses SQLite but we're building a Twelve-Factor app so we change it to PostgreSQL:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'EDITME',
    }
}

Now we create the database structures by running:

python manage.py migrate

Re-start your dev server and see that it no longer shows an error. Open it in your browser and marvel at what you see!

1.2 Understanding apps

We have now got our Django project up and running. Now we need an app. We already have several default apps enabled, see them in $SITENAME/settings.py.

Some apps have a web front-end, others are frameworks which can support other apps. The django.contrib.admin app does have a web front-end. View it with your dev server by adding /admin to the end of the URL.

If you want to try out the admin interface now; make a user to login as:

python manage.py createsuperuser

In the future you will want to familiarize yourself with Django Packages for getting off-the-shelf apps so I recommend bookmarking that.

1.3 Creating our app

We need to make our app for our poll application.

We'll create our poll app in the same directory as our manage.py file so that it can be imported as it's own top-level module, rather than a submodule of $SITENAME.

Django provides a command to template out an app. Use it to create an app named "polls":

python manage.py startapp polls

Take a look inside the polls directory it just created.

So that Django knows to include our app, edit $SITENAME/settings.py and add 'polls.apps.PollsConfig', to the INSTALLED_APPS array.

1.4 Making our app's database

1.4.1 models.py

The first thing we need is our data model for our app. Edit polls/models.py thus:

import datetime

from django.db import models
from django.utils import timezone

# Create your models here.

# a table "Question" with two fields: "question_text" and
# "pub_date". it also has a custom method was_published_recently()
class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text

    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

# a table "Choice" with a foreignkey field "question" pointing at an
# entry in the Question table, a character field "choice_text" and an
# integer field "votes"
class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return self.choice_text

Each of these classes represents a table in the database. If you understand relational databases this will make sense to you. You will want to bookmark the Django database docs.

The __str__() method we defined is not mandatory but it is used both in the interractive prompt and the admin interface to determine how the object represents itself. You may learn more about that here.

Our custom method was_published_recently() allows us to query if the question was published recently.

1.4.2 migration

Django can handle your database setup and upgrades. Run the following command to compute the SQL commands required to setup our database:

python manage.py makemigrations polls

The new migration is now stored on disk at polls/migrations/0001_initial.py. You may use this command to see the SQL code inside it:

python manage.py sqlmigrate polls 0001

Now that the migration is ready we may run it:

python manage.py migrate

1.4.3 confirm and add dummy data

Now we will use the django shell to see our models and add some dummy data to work with:

python manage.py shell

You now have a python shell with the correct variables to work with your app. Type these commands in the shell to learn how it works. Do not skip this because we need some data for the next section:

from polls.models import Choice, Question

# see that there are no questions yet
Question.objects.all()

# add a question
from django.utils import timezone
q = Question(question_text="What's new?", pub_date=timezone.now())
q.save()

# it has an id after being saved
q.id
q.question_text
q.pub_date
q.was_published_recently()

# changing values is simple
q.question_text = "What's up?"
q.save()

# see the list of questions
Question.objects.all()

# create three choices
q.choice_set.create(choice_text='Not much', votes=0)
q.choice_set.create(choice_text='The sky', votes=0)
c = q.choice_set.create(choice_text='Just hacking again', votes=0)

# because of the ForeignKey relation, we can query the question from
# the choice and the choice from the question
c.question
q.choice_set.all()

1.5 Add the code

1.5.1 views.py

Now that we have the database we can add in our Python code to make our app run!

In our poll application, weโ€™ll have the following four views:

  • index - this will list the questions
  • detail - this will show the question and a form full of available answers to vote on
  • results - shows the results of the vote so far
  • vote - this is where the form on the detail page points to. either re-displays the detail page if there was an error, or redirects to the results page if successful

Edit polls/views.py thus:

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse

# these imports are not used but we keep them here for reference
#from django.template import loader
#from django.http import HttpResponse

from .models import Question, Choice

# Create your views here.
def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

Read over that and learn how it works. Every function takes a HttpRequest object (called request) and returns a HttpResponse object. We are using a shortcut function render() which loads a template and returns a HttpResponse in one go.

get_object_or_404 is also a shortcut which we are using to return a 404 reponse if we can't find a Question that matches the question_โ€‹id. It has a sibling get_list_or_404().

The last view is doing form processing. Notice how it will re-display the previous page if the form data is invalid.

If you want to see how views can be setup without shortcuts see the official Django tutorial.

1.5.2 urls.py

There are two urls.py files we need to edit:

  • a project-wide urls.py, which is used to delegate control of certian paths to certain apps (for example the admin app and our polls app).
  • a urls.py in our app which maps URLs to views within the app.

First edit the project urls.py, at $SITENAME/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('polls/', include('polls.urls')),
]

Now make the urls.py for our app at polls/urls.py:

from django.urls import path

from . import views

app_name = 'polls'  # namespace
urlpatterns = [
    # example: /polls/
    path('', views.index, name='index'),

    # example: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),

    # example: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),

    # example: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

See the angle brackets <int:question_id>. What this does is take that section of the URL if it's an integer, and puts it in a variable of name question_โ€‹id, and pass it on to our function views.detail.

See how we set a name for each path. This is for Django's namespacing which means the URLs in our templates can point at the path's name rather than a hardcoded URL.

Read or bookmark the official url docs as they will be useful in future.

1.5.3 templates

Django uses the Django template language for it's templates. Make directories for the templates:

mkdir -p polls/templates/polls

n.b. see how we have polls/templates/polls. It's because the template loader looks for templates in several directories and merges them all together. This could cause different apps to accidentally use each-other's templates. So we namespace it.

Add polls/templates/polls/index.html

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

This template has variables and tags. Read or bookmark the Django template docs.

See how the URL namespacing works. {% url 'polls:detail' question.id %} translates to the URL of the "detail" path in polls/urls.py, with the question_id of question.id.

Add polls/templates/polls/detail.html

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>

Add polls/templates/polls/results.html

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

1.5.4 test it

Now it is time to test our new app.

๐Ÿ›ˆ having made so many changes you may need to restart your development server

If you remember how we setup our urls.py files you will know you need to go to /polls/ to see our app. Then you will see the index page listing the polls. Click into that and vote on your poll.

1.6 Setting up the admin interface

Remember the default app our project has called "admin"?

This app is very useful and you will likely use it with most of your projects, so we will now learn how to configure and use it properly.

If you didn't already, make a user to login as:

python manage.py createsuperuser

Now load up the admin at /admin/ and login. As you can see; Groups and Users are editable by default. But as our polls app isn't authenticated yet these are just used for the admin app itself.

Let's add our Question table to the admin interface.

Edit polls/admin.py thus:

from django.contrib import admin

# Register your models here.
from .models import Question, Choice

admin.site.register(Question)
admin.site.register(Choice)

You can now add questions and choices in the front-end. Try it out by adding an extra question with three choices.

You'll notice it's a bad user experience because it lists the choices for all questions mixed up together and there's no way to actually add a choice to a question, you have to add a choice and select which question it belongs to.

So basically we can define our own classes. Edit like this:

from django.contrib import admin

# Register your models here.
from .models import Question, Choice

class ChoiceInline(admin.TabularInline):
    model = Choice
    extra = 3

class QuestionAdmin(admin.ModelAdmin):
    fields = ['question_text', 'pub_date']
    inlines = [ChoiceInline]

admin.site.register(Question, QuestionAdmin)

Now, instead of it listing the choices seperately, the choices are listed and added as part of the Question.

Let's make a few more enhancements just to show off the available features. Add these lines to QuestionAdmin():

    # Make the list of questions include date and was_โ€‹published_โ€‹recently:
    list_display = ('question_text', 'pub_date', 'was_published_recently')

    # Add the ability to filter based on date:
    list_filter = ['pub_date']

    # Add the ability to search based on the question names:
    search_fields = ['question_text']

The Django admin interface has many features so please bookmark the Django admin site docs.

This concludes the Quickstart. But I hope you will read on because there is lots more to learn.

2 Host it on Heroku

There's no point of making a website if you don't know how to host it. Even if you don't intend to host your final site on Heroku I'd recommend following along it teaches you how to handle secrets and use the Twelve-Factor Approach.

Prerequisites:

  • make a Heroku account at https://www.heroku.com/
  • bookmark the Heroku documentation https://devcenter.heroku.com/
  • using the Heroku documentation install the Heroku CLI on your machine.
  • pick a name for your app on Heroku (it can be the same as the SITENAME if it's available).

Now let's get started.

HEROKUNAME=  #the name you chose
heroku login
heroku create $HEROKUNAME -r heroku

The "heroku create" command created a site $HEROKUNAME and a remote repository called "heroku" associated with it. See the new remote with:

git remote -v

Now, to make Django work with Heroku we need it to get certain config settings from it's environment. Heroku provides a package for this; django-heroku, but it's not very good so we do it in the more generic way, using django-environ.

Furthermore we need a production-grade HTTP-server for Heroku to send requests to. For this we use gunicorn.

pipenv install django-environ gunicorn

Please follow the "Quick Start" on the django-environ docs to customise your settings.py. You may leave out the cache part if you wish.

Note that the two lines that previously said SECURITY WARNING have now been remedied. You will also see your developemnt server will not run as it doesn't have the variables it needs.

Now unfortunately django-environ does not appear to support peer authentication so you will have to add a user and password for your local database. Here is how I did it:

sudo -u postgres psql
CREATE USER EDITME WITH PASSWORD 'EDITME';
GRANT ALL ON DATABASE EDITME TO EDITME;
\c DATABASENAME
GRANT ALL ON ALL TABLES IN SCHEMA public TO EDITME;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO EDITME;
GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO EDITME;

Now, for when we run locally, we put the variables in a file .env. This is in our .gitignore so only applies when running locally.

echo "SECRET_KEY=$(openssl rand -base64 32)" >.env
echo "DEBUG=true" >>.env
echo "DATABASE_URL=postgres://username:password@localhost/EDITME" >>.env

To set those variables for Heroku, run these commands:

heroku config:set SECRET_KEY="$(openssl rand -base64 32)"
heroku config:set DEBUG=true

๐Ÿ›ˆ heroku will automatically set the variable for the database

Heroku requires a Procfile to tell it how to run the app. We are running it with gunicorn which we installed earlier:

export SITENAME=  # set this variable as in the quickstart
echo "web: gunicorn $SITENAME.wsgi:application" > Procfile
heroku local

It should now be running locally just like the development server.

๐Ÿ›ˆ the ALLOWED_HOSTS setting may decide to kick your ass at this point. I recommend just set it to ALLOWED_HOSTS = ['*'].

๐Ÿ›ˆ henceforth you will use heroku local instead of python manage.py runserver.

Now we will configure how static files are handled. We do this using WhiteNoise.

Install WhiteNoise:

pipenv install whitenoise

Edit your settings.py again.

Add this to your MIDDLEWARE, directly under SecurityMiddleware:

'whitenoise.middleware.WhiteNoiseMiddleware',

Put this at the end:

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

One final thing to do before pushing it to the internet:

python manage.py check --deploy

Now let's push it up to the internet! Heroku will automatically detect what python version and dependencies your package needs from Pipfile and Pipfile.lock.

# everything must be in git before we push to heroku
git add .
git commit
git branch -M main

# create a database. it's URL will automatically be added to the environment variables
heroku addons:create heroku-postgresql:hobby-dev

# heroku will build our app immediately
git push heroku main:main  # localbranch:remotebranch

If that works it will give you the URL of your new site.

When you connect to the site at /polls/ you will see a 500 error. This is because, although Heroku has made a database for us, it hasn't been setup yet. You can confirm this by connecting to the database like so:

heroku pg:psql

Now list tables by typing \dt.

So having seen our database tables have not been created we need to run the migration like before.

We can run shell commands directly on the dyno. This means we can easily run our database migration.

heroku run python manage.py migrate

It should now work, although there are no Questions or Choices in the database. One way to fix this would be to use the Django shell like we did before. To do that on Heroku would just be:

heroku run python manage.py shell

Alternatively, just use the admin interface to add your Questions and Polls. (after running a createsuperuser).

You should familiarise yourself with some of the heroku commands. For example:

  • heroku config shows the environment variables heroku is exposing to Django
  • heroku addons shows add-ons the current app is using
  • heroku logs shows the logs
  • heroku help lists all the commands
  • heroku help <commandname> allows you to get help for specific commands

3 Templates & static assets

In this section you will learn several important topics:

  • learn how to overwrite an app's templates
  • learn template inheritance (and fix your broken HTML in the process)
  • use bootstrap to make your site look nice

3.1 Overwrite an app's templates

If you are using off-the-shelf apps (such as the admin app), you may want to customize how they look. Here you will learn how to do this by making modified copies of the templates.

Firstly we edit $SITENAME/settings.py. In the TEMPLATES section edit the 'DIRS' list, like this:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

That DIRS list is basically a search path for finding templates.

Create your new templates directory at the base of your project (the directory containing manage.py):

mkdir -p templates/polls

Now any template you put in there will override the one within the app. Copy polls/templates/polls/index.html to templates/polls. Now edit it by adding <h1>My poll site</h1> at the top.

๐Ÿ›ˆ if you want to customize the admin interface like this you'll have to find where the django source-code lives on your system and copy the template out of there.

3.2 template inheritance

Template inheritance allows us to have a HTML skeleton that is used by multiple pages (potentially accross multiple apps). This is useful both to keep the HTML boilerplate out of our other templates, and to apply persistent elements to all pages (such as a header or navigation).

Please bookmark the official template inheritance docs.

Edit all your templates with this at the beginning and end:

{% extends "base.html" %}
{% block content %}

...

{% endblock %}

๐Ÿ›ˆ delete the extra index.html you made in the last section, or just edit it as above

Now create your base.html in templates/ (or polls/templates/). Either way it's not namespaced which makes it available to all apps.

<!doctype html>
  
<html lang="en">
  <head>
    <meta charset="utf-8">
  
    <title>{% block title %}My poll site{% endblock %}</title>

    <meta name="description" content="What the animals on the farm eat.">
  
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  </head>
  <body>
    <header>
    </header>
    <main class="container">
        {% block content %}
        {% endblock %}
    </main>
    <footer>
    </footer>
      
    <!-- Bootstrap JS bundle -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
  </body>
</html>

We call this our skeleton. Things to note:

  • the {% block %} sections may be overwritten by the child template
  • we have added bootstrap's CSS and JS as per the bootstrap docs
  • by applying the container class to our main, bootstrap will center our page

Now load your app in the browser. You will see that the source code now contains real HTML. And bootstrap is applying fonts and margins.

๐Ÿ›ˆ bootstrap allows you to apply all sorts of formatting with little effort. See some bootstrap examples.

3.3 add static files (CSS and images)

We'll add our own stylesheet and have it apply a background image. This way you will learn not only how to insert static files into HTML but how static files can reference each-other.

The static directory needs to be namespaced just like the templates. Run this command to make it:

mkdir -p polls/static/polls/images

Create our CSS file at polls/static/polls/style.css:

body {
    background: white url("images/background.png") repeat;
}

Now put an image at polls/static/polls/images/background.png. If you like you may use this one checker background.

Open your base.html. Add this to the top of the file:

{% load static %}

And this to the <head> section

<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}">

๐Ÿ›ˆ you may need to restart your dev server for this to take effect

Now when you reload your site you should see your background.

4 Authentication

User authentication is a very common requirement in a web app. This is an enormous topic and there are lots of different ways to do things. We will set it up in a way that I would consider to be typical. However you will want to bookmark the official docs on User authentication in Django and read it at some point.

4.1 MVP

So authentication support is included with Django and enabled by default in settings.py

  • INSTALLED_APPS: django.contrib.auth & django.contrib.contenttypes
  • MIDDLEWARE: SessionMiddleware & AuthenticationMiddleware

When you ran manage.py migrate, the Django auth app created the necessary database tables, and ensures that four default permissions โ€“ add, change, delete, and view โ€“ are created for each Django model defined in one of your installed applications.

Check that those are there:

python manage.py dbshell
select * from auth_permission;

Django provides several default views that you can use for handling login, logout, and password management. There are no default templates though. The template context is documented in each view, see the docs here.

We can easily include all the default views just by adding this to the list in projectname/urls.py.

path('accounts/', include('django.contrib.auth.urls')),

This gives us a whole bunch of url patterns such as:

  • accounts/login
  • accounts/logout
  • accounts/password_โ€‹change
  • accounts/password_โ€‹reset

The default template for /accounts/login/ is registration/login.html. See the url docs for more details on that.

Install this example login template at templates/registration/login.html (the templates directory at the root of your repo that we made when we learned how to overwrite an apps templates).

{% extends "base.html" %}
{% block content %}

{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

{% if next %}
    {% if user.is_authenticated %}
    <p>Your account doesn't have access to this page. To proceed,
    please login with an account that has access.</p>
    {% else %}
    <p>Please login to see this page.</p>
    {% endif %}
{% endif %}

<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>
<tr>
    <td>{{ form.username.label_tag }}</td>
    <td>{{ form.username }}</td>
</tr>
<tr>
    <td>{{ form.password.label_tag }}</td>
    <td>{{ form.password }}</td>
</tr>
</table>

<input type="submit" value="login">
<input type="hidden" name="next" value="{{ next }}">
</form>

{# Assumes you set up the password_reset view in your URLconf #}
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>

{% endblock %}

Check you can see that by running your development server and hitting accounts/login.

Now we simply add a decorator to the views we want to be authenticated. Edit your polls/views.py, adding the import and a decorator before your detail and vote views:

from django.contrib.auth.decorators import login_required

...

@login_required
def detail(request, question_id):

...

@login_required
def vote(request, question_id):

You need to restart your development server for these changes to take effect. Now try to vote on a poll. It will redirect you to the login page \o/

๐Ÿ›ˆ if you don't get redirected to the login page, likely you are already logged in. go to /admin/ and logout.

You can make yourself a user to login as using the admin app.

4.2 More features

As I said authentication is a big topic and Django provides lots of stuff and I can't cover all of it here. But I give you some tasters of the cool stuff you can do:

Example: Use two decorators to restrict access to certain users:

from django.contrib.auth.decorators import login_required, permission_required

@login_required
@permission_required('polls.add_choice', raise_exception=True)
def my_view(request):
    ...

Example: Define a function to perform your own check:

from django.contrib.auth.decorators import user_passes_test

def email_check(user):
    return user.email.endswith('@example.com')

@user_passes_test(email_check)
def my_view(request):
    ...

Example: check permissions against the model:

user.has_perm('foo.add_bar')
user.has_perm('foo.change_bar')
user.has_perm('foo.delete_bar')
user.has_perm('foo.view_bar')

Example: by default our settings.py has django.contrib.auth.context_processors.auth, which means variables user and perms are available in templates:

{% if user.is_authenticated %}
    <p>Welcome, {{ user.username }}. Thanks for logging in.</p>
{% else %}
    <p>Welcome, new user. Please log in.</p>
{% endif %}

Or:

{% if perms.foo %}
    <p>You have permission to do something in the foo app.</p>
    {% if perms.foo.add_vote %}
        <p>You can vote!</p>
    {% endif %}
    {% if perms.foo.add_driving %}
        <p>You can drive!</p>
    {% endif %}
{% else %}
    <p>You don't have permission to do anything in the foo app.</p>
{% endif %}

5 WorkFlow

This section contains some extra bits you need to learn to support your workflow.

5.1 Adding a staging environment

In addition to local testing you will want a staging environment. A test environment in Heroku to validate your app works as expected before pushing changes to production.

This is managed by adding extra git remotes, linked to different sites.

First rename the remote we made before to have a less generic name. I use "prod" for "production". And turn of debug.

git remote rename heroku prod
heroku config:set DEBUG=false

Create the new site. It's a staging env so I call it $HEROKUNAME-stage, with a git remote called "stage".

heroku create $HEROKUNAME-stage -r stage
git remote -v
heroku config:set SECRET_KEY="$(openssl rand -base64 32)" -a $HEROKUNAME-stage
heroku config:set DEBUG=true -a $HEROKUNAME-stage
heroku addons:create heroku-postgresql:hobby-dev -a $HEROKUNAME-stage
git push stage main:main
heroku run python manage.py migrate -a $HEROKUNAME-stage
heroku run python manage.py createsuperuser -a $HEROKUNAME-stage

Cool so we now have two remotes, one called "prod" and one called "stage". Remember these as you will use these names to push all future changes like so:

git push stage main:main
git push prod main:main

Henceforth, when running heroku commands, you will need to identify which app you are refering to with -a, for example:

heroku config -a $HEROKUNAME
heroku config -a $HEROKUNAME-stage

5.2 Pulling databases down from heroku

You will want to be able to pull data back from prod to dev so you can work with realistic data.

Before you try this, make sure you have some unique data in your prod environment to pull down.

First, we get the database details of our staging environment:

heroku config:get DATABASE_URL -a $HEROKUNAME-stage

OK good now you know the deets please use them in this command:

DEETS=

# empty staging db
heroku pg:reset -a $HEROKUNAME-stage

# copy prod db to staging db
heroku pg:pull DATABASE_URL $DEETS -a $HEROKUNAME

DATABASE_URL will automatically refer to your app's live database because you only have one.

Now we will pull that down to a database on the local Postgres installation. Decide on a name for your local database and use it in the below command.

LOCALDBNAME=
heroku pg:pull DATABASE_URL $LOCALDBNAME -a $HEROKUNAME

Please bookmark the Heroku Postgres CLI docs for future reference.

5.3 Workflow for developing your app

For when you havn't worked on your app for a few weeks and forget how everything works.

Activate your environment:

cd $SITENAME
pipenv shell
heroku login

Start your local development server:

heroku local

Push it to your heroku staging environment:

git commit
git push stage localbranch:main

If you edit your models:

python manage.py migrate
python manage.py migrate -a $HEROKUNAME-stage

5.4 Allowing more people to contribute

So you want to add your friend so they can contribute too?

First setup a repository on GitHub or Gitlab as remote origin for your code and push it. Then send your friend a Heroku invite using the docs.

Now your friend needs to:

5.4.1 Get the code

Clone the code from GitHub or GitLab.

5.4.2 Setup the Python environment

pipenv can easily install all the requirements

cd $SITENAME
pipenv install
pipenv shell

๐Ÿ›ˆ if the installation of psycopg2 fails they may need to install libpq-dev

5.4.3 Heroku

Now they need to install the Heroku CLI using the Heroku documentation.

5.4.4 Setup database and .env

Next they should setup PostgreSQL locally, and create a database and a user:

sudo -u postgres psql
CREATE USER EDITME WITH PASSWORD 'EDITME';
GRANT ALL ON DATABASE EDITME TO EDITME;

Once that's done they can create the .env:

echo "SECRET_KEY=$(openssl rand -base64 32)" >.env
echo "DEBUG=true" >>.env
echo "DATABASE_URL=postgres://username:password@localhost/EDITME" >>.env

5.4.5 Lets go!

At this point they should be able to test the app using heroku local.

6 Further reading

Here are some other Django tutorials in-case you didn't like this one:

Copyright 2022 Joseph Graham (joseph@xylon.me.uk)