Django Votr Example


django_example.zip

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.

Code Generation

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. It will have just two pages: a homepage and a detail view for a poll. The homepage will display the newest active polls and results of the most recent finished polls. The detail view for a poll will display information about that poll where users can submit their vote for a poll.

First, let’s generate the starter code for our Django project.

mkdir django_example
django-admin startproject votr 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.

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. 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. You’ll now see there’s a polls directory in our project. 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 defined the models that will be stored in our database. Specifically, we wanted something to represent a poll and a choice for a poll. 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.

Migrations

Once we defined our models, we needed to actually 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 ran

python manage.py makemigrations polls

This resulted in generated files in the migrations folder of our app. 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 run the migrations, we simply ran

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

Once we defined our models, we also saw that Django provided 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 could write to the database by simply creating an instance of the model in python code and then calling save() on that model. To query the database, we could 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="Pirates or Ninajs?", 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='Pirates')
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='Ninajs')

# 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()

Views

After we defined our models, we started to define actual views (endpoints) that our users could interact with. We created 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. We went over two ways to create views: using methods or generic views provided by Django.

Method Views

At first, we defined some simple methods like so in polls/views.py:

def index(request):
    latest_question_list = Question.objects.order_by("-pub_date")[:5]
    output = ", ".join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

These methods responded with some plain text. Note that all of these methods included a request argument, which is necessary for Django. Some methods included extra arguments, which were parsed by Django from the url itself. We told Django how to use these views by defining the following in polls/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    # ex: /polls/
    path("", views.index, name="index"),
    # ex: /polls/5/
    path("<int:question_id>/", views.detail, name="detail"),
    # ex: /polls/5/results/
    path("<int:question_id>/results/", views.results, name="results"),
    # ex: /polls/5/vote/
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

We made things a bit prettier by using templates, which are HTML files we could add some code to. For example, we used the following for the index view

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

Code operations are inside the {% %} blocks and getting values from variables are inside {{ }} blocks. The variables were populated by providing context to the template, which we did inside of the view method for index. We then returned the rendered view as the response.

def index(request):
    latest_questions = Question.objects.order_by("-created_at")[:5]

    template = loader.get_template("index.html")
    context = {
        "questions": latest_questions,
    }

    return HttpResponse(template.render(context, request))

Finally, we added the vote form. The template we used was taken from the Django tutorial:

<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>

First, note the use of {% url 'polls:vote' question.id %}. This renders a url for the vote endpoint inside the polls app. However, we needed to tell Django where to find the polls app by adding the following to polls/urls.py:

app_name = 'polls'

The template was essentially a simple form with radio buttons to pick a choice. We then modified the vote method to handle the vote choice and save it to the database:

def vote(request, question_id):
    try:
        q = get_object_or_404(Question, id=question_id)
        choice_id = request.POST['choice'][0]
        c = q.choice_set.get(id=choice_id)
        c.votes = F("votes") + 1
        c.save()
    except KeyError:
        return render(
            request,
            "vote_detail.html",
            {
                "question": q,
                "error_message": "Please make a choice!",
            },
        )
    except Exception:
        # Redisplay the question voting form.
        return render(
            request,
            "vote_detail.html",
            {
                "question": q,
                "error_message": "Something bad happened!",
            },
        )

    # Redirect back to detail, you should always do this to ensure someone cannot double
    # submit a form. 
    return HttpResponseRedirect(reverse("polls:detail", args=(q.id,)))

One important thing to note was the use of the F() method in Django. This is done to prevent race conditions. At a high level, the value stored on votes could be stale by the time we write to the database. This is because the value of votes only reflects what was in the database at the time of the query. Instead, the more correct thing to do is to have the database itself increment the votes field. This is essentially what using the F() method does. You can read more about it in the Django documentation.

Generic Views

Django provides generic views for common types of endpoints, e.g. detail or list views. We used the detail and list view in class to demonstrate how it could save a bit of code. We used a list view for the index endpoint 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"

Somethings to note: 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 updated polls/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.IndexView.as_view(), name="index"),
    # note that we needed to change question_id to pk
    path('<int:pk>/', views.DetailView.as_view(), name="detail"),
    path('<int:question_id>/results', views.results, name="results"),
    path('<int:question_id>/vote', views.vote, name="vote"),
]

Django Admin

We also showed some of the built-in Django admin functionality. Specifically, we first created a superuser

python manage.py createsuperuser

Then, we navigated to localhost:8000/admin and logged in. There was a lot of nice functionality out of the box that allowed us to manage users and groups. We then registered our Question and Choice models to be included in the admin site:

# polls/admin.py
from django.contrib import admin
from .models import Question, Choice

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

Which gave the functionality to modify and create objects in our database through the Django adminsite!

Bootstrap

Finally, we briefly touched on how to make things look a bit nicer using Bootstrap. To add Bootstrap to an HTML page, we added the relavent info in the <head> of the HTML page, and added a <script> to the end of our body tag. We saw how adding classes to our HTML elements gave us the Bootstrap formatting out of the box. Additionally, we touched a bit on the grid layout of Bootstrap. That is, 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. There’s a bunch more you can do with Bootstrap. If you’re interested, the Bootstrap website has nice examples and further documentation.