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:
- set it up for production with the Twelve-Factor Approach
- template inheritance and using bootstrap
- user authentication
- some extra bits about workflow
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 installlibpq-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 Djangoheroku addons
shows add-ons the current app is usingheroku logs
shows the logsheroku help
lists all the commandsheroku 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 ourmain
, 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.contenttypesMIDDLEWARE
: 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: