Django Votr Example
This writeup is meant to be a reference for the completed Django Votr app we built in class. The app itself was adapted from the Django tutorial, which you can view here. It covers up until part 4 of the tutorial. This may be helpful in starting your own Djano project in the future.
Project Setup
Let’s create a simple web application for running polls using Django! Our finished application will allow users to create polls and vote on them. First, let’s generate the boilerplate code for our Django project.
# create a folder for our project
mkdir django_example
# generate django boilerplate for project `votr` in the newly created folder
django-admin startproject votr django_example
# run the server
cd django_example
python manage.py runserver
There’s quite a bit of generated code! Let’s go over it briefly. First, there’s the manage.py
file at the root of our folder. You won’t need to touch this, it simply provides commands that Django includes to help you develop and manage you webapp (e.g. run it locally or manage database migrations). Django also created a votr
folder for us. This is basically where configurations for your application live. You can ignore asgi.py
and wsgi.py
, they’re files to help you actually run your webapp in production. The important files are settings.py
and urls.py
. The settings file contains configs for your application. It tells Django things like how to hook up to your database, what sort of user authentication you want, and what code to include in your webapp. We won’t really need to touch anything except for the INSTALLED_APPS
variable for this tutorial, as we’ll see in a bit. You can add custom configurations here if you wish too. The urls.py
file is where the top level routes for your server are defined, i.e. polls/123
maybe should be the URL for the poll with id 123.
Now, let’s generate some more code to help start our webapp.
python manage.py startapp polls
This generates code for an app within Django. You’ll now see there’s a polls
directory in our project. Each Django project can have multiple apps. The idea is that you might have all your code in one place, but want to run a web server with just a subset of the apps you have. This is handy for much larger projects and deployments, but we won’t really need this distinction for our use case, so all of our logic will be within this polls
folder. There isn’t much in each of these files, but Django generates them since it’s good practice to organize your code this way. We’ll end up modifying each of these files in our tutorial, except for tests.py
, but it should be self-explanatory what that file should contain.
Models
To start, we define the models that represent the structure of our data, also called a database schema. Specifically, we need something to represent a poll, a choice for a poll, and a vote. These are defined in the models.py
file of the provided code. Note that Choice
has a foreign key to Poll
. This represents ownership, that is, a choice is owned by a poll. We also used the built in User
model from Django to keep track of users. Note the unique constraint on Vote
: a user can only vote for each choice once.
Migrations
Once we have defined our models, we need to update our database schema to match our models. A schema is like a blueprint, it tells the database what objects look like. Updating the schema is typically called a migration. Django provides some very nice tools to help us generate code to run migrations and to actually run them. Specifically, to generate migrations, we can run
python manage.py makemigrations
This results in generated files in the migrations
folder of polls/
. They’re numbered starting from 0001. At a high level, what Django is doing is looking at the current schema of your database and comparing it to the models you wrote. It generates the necessary actions to run on the database to match your model code. To actually apply the migrations, simply run
python manage.py migrate
So to recap, if you make any changes to your models, you will need to make migrations and then run the migrations. However, if you’re just developing from scratch, it may be easier to simply nuke the entire database and only ever deal with a single migration file. To do this, simply delete the db.sqlite3
file and all files inside your migrations
folder. Then, make migrations and migrate once again.
Using the Django ORM
Django provides a very nice way to query and write to our database without having to write a database specific language like SQL. More details and examples can be found in the Django documentation, but essentially we can write to the database by simply creating an instance of the model in Python code and then call save()
on that model. To query the database, we can use the objects
attribute to filter and select the rows of the database we wanted.
from polls.models import Choice, Question
Question.objects.all()
# create a question
from django.utils import timezone
from datetime import timedelta
open_time = timezone.now()
close_time = open_time + timedelta(days=30)
q = Question(question_text="Python or Javascript?", open_date=open_time, close_date=close_time)
# This is not saved in our database yet! We need to call save()
q.save()
# Once the object is saved to the database, it is assigned an id
q.id
# We can create some choices linked to the question like so
c1 = Choice(question=q, choice_text='Python')
c1.save()
# We can create choices like this as well, note we don't need to specify the question it's owned by since it's implied
q.choice_set.create(choice_text='Javascript')
# We can grab all choices on the question with the provided query set that
# is automatically generated by Django. It uses the foreign key name
q.choice_set.all()
Django Admin
The Django admin site is another built in functionality that is helpful in creating test data when developing. First, create a superuser
python manage.py createsuperuser
Then, we navigate to localhost:8000/admin
and log in with the user you just created. There’s a lot of nice functionality out of the box that allows us to manage users and groups. We can also register our Question
and Choice
models to be included in the admin site, which will allow us to modify the database with the nice admin GUI:
# polls/admin.py
from django.contrib import admin
from .models import Question, Choice
admin.site.register(Question)
admin.site.register(Choice)
Views
Let’s get started defining actual views (endpoints) that our users can interact with. We would like a view for the homepage (index), a detail view for a poll, a results view for a poll, and a vote view for a poll. First, let’s set up the routes so that we have a basic skeleton to work with.
Route Skeleton
First, in order to have Django realize we want to use the polls
app, we need to add it to INSTALLED_APPS
in settings first.
Add the line polls.apps.AppConfig
to the INSTALLED_APPS
list under votr/settings.py
. You can just put polls
, too, but the
Django documentation prefers the explicit app config.
Next, let’s define some endpoints that do a simple “Hello, world” response in polls/views.py
:
from django.shortcuts import render
from django.http import HttpResponse
def index(request):
return HttpResponse("this is the homepage")
def question_detail(request, question_id):
return HttpResponse(f"this is question detail for id: {question_id}")
def question_results(request, question_id)
return HttpResponse(f"this is question results for id: {question_id}")
def vote(request, question_id)
return HttpResponse(f"this is vote for id: {question_id}")
def login_view(request):
return HttpResponse(f"login")
def logout_view(request):
return HttpResponse(f"logout")
def signup(request):
return HttpResponse(f"signup")
Now, we can add route definitions and the app name to polls/urls.py
:
from django.urls import path
from . import views
# this enables URL reversal, e.g. polls:question_detail 123 -> /polls/123
app_name = 'polls'
urlpatterns = [
path("", views.index, name="homepage"),
path("<int:question_id>", views.question_detail, name="question_detail"),
path("<int:question_id>/results", views.question_results, name="question_results"),
path("<int:question_id>/vote", views.vote, name="vote"),
path("signup", views.signup, name="signup"),
path("login", views.login_view, name="login"),
path("logout", views.logout_view, name="logout"),
]
Finally, we add a top-level route to votr/urls.py
that all of the above routes will derive from. In this case,
all routes defined in polls/urls.py
will begin with polls/
.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('polls/', include('polls.urls')),
path('admin/', admin.site.urls),
]
Templates
We can manage our frontend by using templates, which are HTML files we can add some code to. For example, we can use the following for the index view and place it in polls/templates/index.html
, which will display if a user is logged in and a list of recent questions.
<h1>Votr</h1>
{% if user.is_authenticated %}
Currently logged in as {{ user.username }}.</br>
{% endif %}
There are {{ questions|length }} active questions:
<ul>
{% for question in questions %}
<li><a href="/polls/{{ question.id }}">{{ question.text }}</a></li>
{% endfor %}
</ul>
Code operations are inside the {% %}
blocks and getting values from variables are inside {{ }}
blocks. You can call methods on variables as normal, just without parentheses, e.g. var.method
. You can read more about what you can do with templates in the Django documentation. The variables are populated by providing context to the template, which we do inside of the view method for index. The variable user
is included already by Django.
def index(request):
questions = Question.objects.order_by("-start_time")[:5]
return render(request, "index.html", context={
"questions": questions,
})
User Authentication
We can implement login/signup/logout views by leveraging built-in Django methods. First, take a look at signup.html
:
<h1>Sign Up</h1>
{% if message %}
<p style="color: tomato;"><strong>{{ message }}</strong></p>
{% endif %}
<form action="{% url 'polls:signup' %}" method="post">
{% csrf_token %}
<fieldset>
<legend><h3>Sign Up Information</h3></legend>
<label for="fname">First name:</label>
<input type="text" id="fname" name="fname"><br><br>
<label for="lname">Last name:</label>
<input type="text" id="lname" name="lname"><br><br>
<label for="username">Username:</label>
<input type="text" id="username" name="username"><br><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password"><br><br>
<input type="submit" value="Register">
</fieldset>
This is an HTML form that makes a POST request to the signup endpoint. It expects a first name, last name, username, and password. Note the use of if message
to display an error message if we need to. Now, we define the backend logic for signup:
def signup(request):
if request.method == "GET":
return render(request, "signup.html")
try:
fname = request.POST['fname']
lname = request.POST['lname']
username = request.POST['username']
password = request.POST['password']
User.objects.create_user(
username=username,
password=password,
first_name=fname,
last_name=lname,
)
return render(request, "signup.html", context={
"message": "successfully created user!"
})
except Exception as e:
return render(request, "signup.html", context={
"message": f"something went wrong: {e}"
})
This function actually handles two endpoints: the GET and POST for signup. The GET simply renders the template, while the POST does the creation of a user. We’ve thrown everything into a simple try-except for simplicity, but you can propogate more specific errors to the user, e.g. missing field, username taken, password too short, etc.
The logic for login uses the built-in Django methods authenticate
and login
from django.contrib.auth
. authenticate
checks if credentials are valid, and login
persists a user’s session. Importantly, if a user is logged in, on any request, we can grab the user object with request.user
.
def login_view(request):
if request.method == "GET":
return render(request, "login.html")
try:
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return render(request, "login.html", context={
"message": "successfully logged in!",
})
finally:
return render(request, "login.html", context={
"message": "invalid credentials!"
})
Finally, the logout view is very simple: we logout the user and redirect to the homepage.
def logout_view(request):
logout(request)
return HttpResponseRedirect(reverse("polls:homepage"))
Votr Logic
Next, let’s create the question detail detail.html
. This should include some information about the question along with a vote. The template we used was taken from the Django tutorial:
<h1>Question Detail</h1>
{% if message %}<p style="color: tomato;"><strong>{{ message }}</strong></p>{% endif %}
<h3>{{ question.text }}</h3>
{% if user.is_authenticated %}
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
<legend><h1>Choices</h1></legend>
{% 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 }}</label><br>
{% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>
{% endif %}
This template is essentially a form with radio buttons to pick a choice. Note the use of {% url 'polls:vote' question.id %}
. This renders a url for the vote endpoint inside the polls app. Let’s have our question detail view render this template:
def question_detail(request, question_id):
question = get_object_or_404(Question, id=question_id)
return render(request, "detail.html", context={
"question": question,
})
Now, we need to define the logic for the vote view. Since we’re representing votes as (choice, user) tuples, we just need to create a new Vote object. The database will deal with determining if a duplicate exists and error if so.
def vote(request, question_id):
question = get_object_or_404(Question, id=question_id)
choice_id = request.POST['choice']
choice = get_object_or_404(Choice, id=choice_id)
try:
Vote.objects.create(
choice=choice,
user=request.user,
)
return render(request, "detail.html", context={
"question": question,
"message": "voted successfully!",
})
except Exception as e:
print(e)
return render(request, "detail.html", context={
"question": question,
"message": "vote failed!",
})
Finally, let’s implement the results page. For a given question, we need to count the number of votes for each of its choices. Let’s add the following method on the Question
model itself:
# Count is imported from django.db.models
class Question(models.Model):
...
def get_choices_with_votes(self):
return self.choice_set.annotate(vote_count=Count('vote'))
Let’s break down what’s happening here, there’s a lot of Django magic going on. Since Choice
has a foreign key to Question
, we can think of a Question
having many Choice
s, or a one-to-many relationship. As a result, Django automatically creates an attribute named <model>_set
on the parent model that references all of the children. In this case, every Question
has a choice_set
that refers to the set of all of its choices. Next, we call annotate
on the set of choices, which simply adds a field on to each choice that is the result of a computation. This field is called vote_count
and is defined as Count('vote')
. That is, for each Choice
in a Question
’s set of choices, we are annotating it with a new field vote_count
that is the number of vote
s for that choice. The field vote
comes from the many-to-many relationship Choice
and User
have defined by the Vote
model. You might expect it to be vote_set
, but the Count()
method is a type of aggregation, in which Django expects simply the lowercase name of the model. So, ultimately, we get a query set of choices that have a newly annotated field with the count of votes for each one.
If this was a bit confusing, that’s OK. Database queries can be very complicated, and this one is fairly involved. Additionally, the drawback of using an ORM like Django’s is that complex database queries become even more complex due to assumed naming conventions and “magic” (e.g. where does the name choice_set
come from?). Typically, queries become more complex the more optimal you try to make them. Consider the following approach for the same problem:
results = defaultdict(int)
choices = question.choice_set.all()
for c in choices:
results[c.choice] = Vote.objects.filter(choice_id=c.id).count()
Overall, there’s nothing logically wrong with the code (ignoring race conditions). However, it makes a database query per choice, which can ultimately result in heavy slowdowns under large loads. For this class, though, you won’t need to worry about any of this and it’s perfectly fine to use either approach!
We need a simple template for the question results page:
<h1>Results for "{{ question.text }}"</h1>
{% for choice in results %}
<p>{{ choice.choice }}: {{ choice.vote_count }}</p>
{% endfor %}
and we render it like so:
def question_results(request, question_id):
question = get_object_or_404(Question, id=question_id)
return render(request, "results.html", context={
"question": question,
})
Generic Views
The most basic way to define the logic for a route is to use a view function.
As the name suggests, these are just methods that expect an HTTP request as input (with maybe some parameters) and return an HTTP response.
These are how we initially defined the routes above. Note how parameters are passed: the path for question detail has a
variable called question_id
, which is expected to exist as a method argument on the view function question_detail
.
Django provides generic views for common types of endpoints, e.g. detail or list views. Overall, we can save a bit of code when using generic views. We can use a list view for the homepage and a detail view for the poll detail endpoint.
class IndexView(generic.ListView):
template_name = "index.html"
context_object_name = "questions"
def get_queryset(self):
return Question.objects.order_by("-created_at")[:5]
class DetailView(generic.DetailView):
model = Question
template_name = "detail.html"
class ResultsView(generic.DetailView):
model = Question
template_name = "results.html"
What’s happening here? The list view populates a variable specified by context_object_name
for the template to use. The get_queryset()
method is defined by use to return the list of elements Django should populate that variable with. For the detail view, we simply provided the model name it’s for and the template. By default, Django uses the lowercase singular of the model name as the context variable. Then, to actually use these in our urls, we need to update our polls/urls.py
:
from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
path("", views.IndexView.as_view(), name="homepage"),
# note that we need to change question_id to pk as this is the
# name generic.DetailView expects
path("<int:pk>", views.DetailView.as_view(), name="question_detail"),
path("<int:question_id>/results", views.question_results, name="question_results"),
path("<int:question_id>/vote", views.vote, name="vote"),
path("signup", views.signup, name="signup"),
path("login", views.login_view, name="login"),
path("logout", views.logout_view, name="logout"),
]
There’s an incredible number of things you can do with views, and Django allows you to customize pretty much every aspect of them. If you’re interested, you can take a look at the documentation to get an idea of the use cases.
Bootstrap
Finally, the last step once we have finished the functionality of our website is to make it look nice! There’s a myriad of libraries that exist, but Bootstrap is a well-known package that has intuitive paradigms. This guide won’t cover how to use it in depth, but it’s important to at least understand how the grid layout Bootstrap uses works. Bootstrap first encourages you to have containers
, in which you can put rows
of content. Each of these rows can have cols
that contain pieces of content. Bootstrap will then automatically space and divide the columns within each row. All of these are simply HTML <div>
s with a class, e.g. <div class='container'>
. There’s a bunch more you can do with Bootstrap. If you’re interested, the Bootstrap website has nice examples and further documentation.