DEV Community

Otavio Monteagudo
Otavio Monteagudo

Posted on

The Concise Django Tutorial

Intro

These are the notes I made while studying the official Django Tutorial.
It is very similar to the tutorial itself. However, it is generally more condensed, but I changed some stuff (for instance, all materials on the Admin interface are in its own section instead of going back and forth to it) and added some more (like the section on having different settings per environment).
I hope you enjoy it and it is useful.

Command Reference

# Creates a project
django-admin startproject <project_name>
# Runs project in localhost
python manage.py runserver
# Creates an app inside the project
python manage.py startapp <app_name>
# Creates migrations based on existing model files
python manage.py makemigrations <app_name_OPTIONAL> # will work without app name param
# Applies migrations to alter current database
python manage.py migrate
# Outputs DDL SQL that will be generated and applied for a specific migration. Doesn't alter the db.
# ex: params would be `this_is_an_app` & `0001` for '/this_is_an_app/migrations/0001_initial.py'
python manage.py sqlmigrate <app_name> <numeric_mig_file_prefix> 
# Runs app as a console in a python interpreter environment; similar to rails console
python manage.py shell
# Runs SQL shell with project's Database
python manage.py dbshell
# Creates an user for the Admin Interface
python manage.py createsuperuser
# Checks project for common problems. 
# These checks are context-specific and a different settings module can be applied.
# Ex: append --deploy --settings=production_settings --fail-level=DEBUG to 
# use extra deploy checks, with production settings and return non-0 up to DEBUG statements (ERROR level is the default).
python manage.py check
# Output the location of Django source files
python -c "import django; print(django.__path__)"
Enter fullscreen mode Exit fullscreen mode

Initialize Project

# Optional but highly recommended: set-up pyenv version. See: https://dev.to/otamm/python-version-management-with-pyenv-3fig
pyenv local 3.12
# Optional but highly recommended: setup virtual env
python -m venv .venv && source .venv/bin/activate
# installs django to venv context
python -m pip install django~=5.1
# writes libraries and versions to a reusable file
pip freeze > requirements.txt 
# Creates project. DO NOT USE RESERVED NAMES such as python, test, django, etc
django-admin startproject polltutorial
# Optional: setup direnv. See: https://dev.to/otamm/one-environment-per-project-manage-directory-scoped-envs-with-direnv-in-posix-systems-4n3c
touch .envrc && echo 'export VIRTUAL_ENV=."venv" && layout python' >> .envrc && direnv allow
Enter fullscreen mode Exit fullscreen mode

0.1 Project File Structure

polltutorial/ # Project name; can be edited to whatever you want
    manage.py # Collection of command-line utilities to perform common project tasks
    polltutorial/ # Namespaces project
        __init__.py # Declares package: https://docs.python.org/3/tutorial/modules.html#tut-packages
        settings.py # Project-wide settings: https://docs.djangoproject.com/en/5.1/topics/settings/
        urls.py # URL declarations: https://docs.djangoproject.com/en/5.1/topics/http/urls/
        asgi.py # Asynchronous Server Gateway Interface; connects app with your async web server.
        wsgi.py # Web Server Gateway Interface; "legacy", easier to config, adequate for most cases
Enter fullscreen mode Exit fullscreen mode

0.2 Dev Server

Initialize with

python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

It has code auto-reload. Will road without migrations, for now. Runs on port 8000 as default.

1.0 Initialize Apps

Code below will create a Django app; these can be located anywhere in your PYTHON_PATH.

# Initialize 'polls' app
python manage.py startapp polls
Enter fullscreen mode Exit fullscreen mode

File structure resulting is (note that urls.py will have to be created):

polltutorial/ # Project files
    # ...
    polls/ # Root directory for the 'polls' Django app
        __init__.py # Python package indicator, allows importing from this directory
        admin.py # Configuration for the Django admin interface
        apps.py # App configuration file
        migrations/ # Directory for database migrations
            __init__.py # Makes migrations a Python package
        models.py # Defines database models (tables) for the app
        tests.py # Contains test cases for the app
        views.py # Handles the app's views (request/response logic)
Enter fullscreen mode Exit fullscreen mode

Remember to add the app to the project's installed apps setting; we'll revisit the settings soon. Your INSTALLED_APPS setting should look like this:

polltutorial/settings.py

INSTALLED_APPS = [
    "polls.apps.PollsConfig",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]
Enter fullscreen mode Exit fullscreen mode

1.1 First View

polls/views.py

from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")
Enter fullscreen mode Exit fullscreen mode

Most basic view possible, but still must be mapped to an URL:

polls/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
]
Enter fullscreen mode Exit fullscreen mode

It's best to import app URLs all at once at the website's URLs:

polltutorial/urls.py

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

urlpatterns = [
    path("polls/", include("polls.urls")),
    path("admin/", admin.site.urls),
]
Enter fullscreen mode Exit fullscreen mode

1.1.1 include() and path()

include()

include() allows referencing other URLconfs. When Django finds include(), it chops off whatever part of the URL matched up to that point and sends the remaining string to the included URLconf for further processing.

This is to make easy to plug-and-play URLs. You can rename the path, such as /fun_polls or /content/polls and it would still work.

Only exception is the admin.site.urls, as you can see just above.

path()

path() has four arguments (first two are mandatory):

  • route: has URL pattern. When a request is processed, Django starts at the first pattern in urlpatterns and goes down until one is matched. Patterns to not search for GET and POST URL parameters or the domain name.
  • view: when a pattern is matched, Django calls the specified view function with an HttpRequest object as the first argument and any other params as keyword arguments.
  • kwargs: arbitrary keyword args can be passed as a dict to the target view.
  • name: allows for unambiguous name to refer to the URL from other parts of the project.

2.0 First look at Settings & Database

polltutorial/settingspy contains a Python module with module-level vars representing configuration settings.
By default, DATABASES uses sqlite, which comes with Python.
It's a good idea to set TIME_ZONE to your time zone as soon as the app is created.
Also note the INSTALLED_APPS setting. It holds names of all Django apps active in this Django project. Apps should be modular and you can package and distribute them for use by others.
These the 6 default ones:

1: django.contrib.admin – The admin site

  • Provides an automatic, customizable interface for managing your site's content
  • Offers CRUD (Create, Read, Update, Delete) operations for your models
  • Includes user authentication and permissions
  • Allows customization of the admin interface layout and functionality
  • Generates forms automatically based on your model definitions
  • Provides a search functionality and filtering options for your data

2: django.contrib.auth – An authentication system

  • Handles user accounts, groups, permissions, and cookie-based user sessions
  • Provides views for logging in, logging out, and password management
  • Includes decorators for controlling access to views based on user permissions
  • Offers a user model that can be extended for custom user information
  • Provides password hashing and validation
  • Includes a system for password reset via email

3: django.contrib.contenttypes – A framework for content types

  • Enables generic relationships between models
  • Allows associating content with any model in your Django project
  • Provides a ContentType model to track all models installed in your project
  • Enables the creation of reusable components that can work with any model
  • Supports polymorphic relationships in the database

4: django.contrib.sessions – A session framework

  • Manages user sessions across requests
  • Stores and retrieves arbitrary data on a per-site-visitor basis
  • Supports different session backends (database, file, cache)
  • Provides middleware to manage session data
  • Allows customization of session behavior and storage

5: django.contrib.messages – A messaging framework

  • Provides a way to store messages in one request and retrieve them in a subsequent request
  • Supports different storage backends (cookies, database, cache)
  • Includes message levels for different types of messages (debug, info, success, warning, error)
  • Offers template tags for easy rendering of messages in templates
  • Allows customization of message behavior and appearance

6: django.contrib.staticfiles – A framework for managing static files

  • Collects static files from each of your applications into a single location
  • Provides a management command for collecting static files
  • Offers a static file serving view for development (not for production use)
  • Supports different storage backends for serving static files
  • Includes template tags for easy inclusion of static files in templates
  • Allows customization of static file handling and storage

2.1 Postgres Setup

TODO

2.2 Migrating

Run to alter database state. If you have no other models, migrations for standard apps will be applied. These apps can be commented out or deleted and their migrations will be skipped.

python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

2.3 Django Models

Models in Django are the single source of truth about data. It contains essential fields and behaviors of data. Data is defined in the data model and information is derived from it, including migrations. After running a command to create migrations, Django will detect changes and create migration files to update the database in order to match current models.

In this app, we'll create two models: Question and Choice. A Question has a question text and a publication date. A Choice has text of the choice and a vote tally. Each Choice is associated with a Question.

polls/models.py

from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField("date published")


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
Enter fullscreen mode Exit fullscreen mode

Each of these classes subclasses django.db.models.Model. These fields above are all declared as class variables, each representing a field in the model.

Each field is a Field instance (like CharField for character fields and DateTimeField for datetimes).

The name of each Field instance is the field's name in machine-friendly format; it'll be used in Python code and as the database field in the model. They also accept an option, human readable name that would be used in Django introspectively and as doc. The machine-readable will be used otherwise.

Some Field classes have required arguments; CharField for instance requires a max_length. This is used both to reflect in the database schema as in Django-level object validation.

A Field can also have many optional arguments; in this case, we set the default value of the IntegerField named votes as 0.

The ForeignKey field defines the relationship; it tells Django each Choice is related to a question. Django supports many-to-one, one-to-one and many-to-many relationships.

These models give Django a lot of information. From it Django is able to create a database schema and Python db-access API call to access Question and Choice objects.

2.4 Django Migrations

With the app installed in the settings and the models declared, we can now generate the migrations:

python manage.py makemigrations polls
Enter fullscreen mode Exit fullscreen mode

You can also inspect the SQL that will be applied for the specific migration file. In this case,

python manage.py sqlmigrate polls 0001
Enter fullscreen mode Exit fullscreen mode

Would output

BEGIN;
--
-- Create model Question
--
CREATE TABLE "polls_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_text" varchar(200) NOT NULL, "pub_date" datetime NOT NULL);
--
-- Create model Choice
--
CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL, "question_id" bigint NOT NULL REFERENCES "polls_question" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");
COMMIT;
Enter fullscreen mode Exit fullscreen mode

Run the migrations with this command to have them applied to your database.

python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

NOTE: SQL generated varies depending on DB used. This was generated for SQLite.

2.4.1 Properties of Migrations

  • Table names are generated by combining app & model names (ex: polls_choice).
  • Primary keys (IDs) are added automatically.
  • Foreign key name is foreign_model_name + _ids (ex: question_id). Note that this was generated for a one-to-many relationship and that it uses the model name, not the table's(polls_question).

NOTE: Many of these properties are overridable.

2.5 The Django App Console

First of all, let's define better representation for our models. This is done through the __str__() magic method in each model.

You can see I'm returning a 'main' field + the ID. It is pretty subjective to choose what will best represent the object, but you'll have to return a string.

Let's also add a helper method to return any questions created in the last day.

polls/models.py

class Question(models.Model):
    # ...
    def __str__(self):
        return f"{self.question_text}  ID: {self.id}"

    # returns true if published in the last 24h or false otherwise
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)


class Choice(models.Model):
    # ...
    def __str__(self):
        return f"{self.choice_text}  ID: {self.id}"
Enter fullscreen mode Exit fullscreen mode

In there you'll choose the field(s) that better represent the object.

2.5.1 Console Interaction

Run it to go to the shell. Below are some examples of what you can do; it is particularly good to create, edit and inspect objects. This will load the python interpreter with the DJANGO_SETTINGS_MODULE set, giving it access to the import paths under polltutorial/settings.py.

python manage.py shell
Enter fullscreen mode Exit fullscreen mode

This is a list of field lookup suffixes you can use to look up objects.

get() will return a single object or an error if none or more than one match the condition.
filter() will return a QuerySet object composed of zero or more objects that match the condition.

# Import the model classes we just wrote.
from polls.models import Choice, Question  
# No questions are in the system yet.
Question.objects.all() # <QuerySet []>
# Create a new Question.
# Support for time zones is enabled in the default settings file, so
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
from django.utils import timezone
q = Question(question_text="What's new?", pub_date=timezone.now())
# Save the object into the database. You have to call save() explicitly.
q.save()
# It gets an ID once it is persisted in the DB.
q.id # 1
# Access model field values via Python attributes.
q.question_text # "What's new?"
q.pub_date # datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=datetime.timezone.utc)
# Change values by changing the attributes, then calling save().
q.question_text = "What's new?"
q.save()
# objects.all() displays all the questions in the database.
Question.objects.all() # <QuerySet [<Question: What's new? ID: 1>]>
# import timezone module to use time operations
from django.utils import timezone
current_year = timezone.now().year
# looks Questions created in the current year
Question.objects.get(pub_date__year=current_year) # <Question: What's new? ID: 1>
# Uses helper method on Question just created
Question.objects.get(id=1).was_published_recently() # True
# Display any choices from the related object set -- none so far.
q.choice_set.all() # <QuerySet []>
# The create call constructs a new `Choice` object, does the INSERT statement & adds the choice
# to the set of available choices and returns the new Choice object. Django creates
# a set (automatically defined as "choice_set") to hold the "other side" of a ForeignKey
# relation (e.g. a question's choice) which can be accessed via the API.
q.choice_set.create(choice_text="Not much", votes=0) # <Choice: Not much ID: 1>
q.choice_set.create(choice_text="The sky", votes=0) # <Choice: The sky ID: 2>
q.choice_set.create(choice_text="Just hustling again", votes=0) # <Choice: Just hustling again ID: 3>
# Now let's access the choices again:
q.choice_set.all() 
# Note how you can query choices by properties of the parent object:
Choice.objects.filter(question__pub_date__year=current_year) # <QuerySet [<Choice: Not much ID: 1>, <Choice: The sky ID: 3>, <Choice: Just hustling again ID: 3>]>
# It is of course also possible to query them by their own properties.
c = q.choice_set.filter(choice_text__startswith="Just hustling") # <Choice: Just hustling again ID: 3>
c.delete() # removes Choice # 3
Enter fullscreen mode Exit fullscreen mode

2.5.2 Hot Reloading the Console

You can use IPython to have interactive python on steroids.

Install it with pip and then initiate a new shell session:

pip install ipython
python manage.py shell -i IPython # should use IPython automatically. `-i` arg is optional 
Enter fullscreen mode Exit fullscreen mode

To have hot-reloading, you have to activate the extension inside the interactive interpreter session:

# Explicitly activate autoreload in IPython's interpreter
In [1]: %load_ext autoreload
In [2]: %autoreload 2 # see list of %autoreload options below
# How it can be used now:
In [3]: from polls.models import Question
In [4]: q = Question.objects.get(id=1)
In [5]: print(q) #=> What's new?  ID: 1
# Change Question's __str__() to `return f"{self.question_text} HEllow  ID: {self.id}"`
In [6]: q = Question.objects.get(id=1) # reloads object in memory
# Executes the redefined method
In [5]: print(q) #=> What's new? HEllow ID: 1
Enter fullscreen mode Exit fullscreen mode
  • %autoreload

    • Reload all modules (except those excluded by %aimport) automatically now.
  • %autoreload 0

    • Disable automatic reloading.
  • %autoreload 1

    • Reload all modules imported with %aimport every time before executing the Python code typed.
  • %autoreload 2

    • Reload all modules (except those excluded by %aimport) every time before executing the Python code typed.
  • %aimport

    • List modules which are to be automatically imported or not to be imported.
  • %aimport foo

    • Import module 'foo' and mark it to be autoreloaded for %autoreload 1
  • %aimport -foo

    • Mark module 'foo' to not be autoreloaded.

3.0 Django Admin

The Django admin is basically a built-in CMS.

To create an admin user, run:

python manage.py createsuperuser # will then prompt for username, password etc
Enter fullscreen mode Exit fullscreen mode

Create your admin and access it in localhost:8000/admin. You can set the default language under LANGUAGE_CODE in polltutorial/settings.py.

It is also necessary to register every model you'll want to perform CRUD on through the admin panel. To do so:

polls/admin.py

from django.contrib import admin

from .models import Question

admin.site.register(Question)
Enter fullscreen mode Exit fullscreen mode

Reload localhost:8000/admin and you'll see the editable Question. Each field type will have a corresponding HTML input widget, sometimes with extra functionalities like the JS calendar popup to input dates for DateTimeFields.

3.1 Customizing the Django Admin

3.1.1 Customizing Display Order of Fields

The Question model is pretty simple, but what if it had dozens of fields? You can customize their order? You can register then specifically in order; let's swap the order of pub_date and question_text and display pub_date first:

You'll have to create a QuestionAdmin class and register it as the second parameter to admin.site.register():

polls/admin.py

# ...
class QuestionAdmin(admin.ModelAdmin):
    fields = ["pub_date", "question_text"]

# ...
admin.site.register(Question, QuestionAdmin)
Enter fullscreen mode Exit fullscreen mode

3.1.2 Creating Fieldsets

You can also create fieldsets to aggregate a model's fields according to your own criteria. Let's split question text and date edition in their own sections, adding no title to the question_text section and adding a Date Information header to the pub_date section:

polls/admin.py

# ...
class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None, {"fields": ["question_text"]}),
        ("Date Information", {"fields": ["pub_date"]}),
    ]

# ...
admin.site.register(Question, QuestionAdmin)
Enter fullscreen mode Exit fullscreen mode

3.1.3 Adding Related Objects

As you might have noticed, Question does not display options on Choices. We can register Choice just as we did with Question, and since the foreign key to Question is registered in the Choice model we will be able to create them and associate them with a Question.

We can also add the option of creating Choice objects directly in the Question, by adding it as an Inline model:

polls/admin.py

from django.contrib import admin

from .models import Choice, Question


class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3 # will add 3 empty Choice forms when editing a choiceless (or sub-3 choice) Question


class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None, {"fields": ["question_text"]}),
        ("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}),
    ]
    inlines = [ChoiceInline]


admin.site.register(Question, QuestionAdmin)
Enter fullscreen mode Exit fullscreen mode

If you find that this takes way too much space (every Choice field will have its own line), you can switch the StackedInline class with the TabularInline. This will show each choice in a table-like more compact format spreading the fields accross a single line instead of one line per Choice field.

polls/admin.py

# ...
class ChoiceInline(admin.TabularInline):
    model = Choice
    extra = 3
# ...
Enter fullscreen mode Exit fullscreen mode

3.1.4 Customizing the Change List (Question Display in the Admin Panel)

By default, Django displays the __str()__ implementation of each object. What if we wanted to display it as a table, showing each field?

We can use the list_display class variable admin option, which sets a list of field names to display as columns.

This supports not only model fields (that map to instance variables) but also instance methods, such as our previously defined was_published_recently().

polls/admin.py

# ...
class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ["question_text", "pub_date", "was_published_recently"]
# ...
Enter fullscreen mode Exit fullscreen mode

You can click on each field header to sort the table by its value (note that sorting by method output is not supported out of the box).

You can also add decorators to your instance method to refine its display in the admin panel. You can see a more thorough discussion on these possibilities here.

polls/models.py

# ...
from django.contrib import admin
# ...

class Question(models.Model):
    # ...
    @admin.display(
        boolean=True,
        ordering="pub_date",
        description="Was the question published recently?",
    )
    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now
# ...
Enter fullscreen mode Exit fullscreen mode

3.1.4.1 Filtering the Change List

You can add a list_filter class variable to the QuestionAdmin to add a filterable field to the admin panel:

polls/admin.py

# ...
class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_filter = ["pub_date"]
# ...
Enter fullscreen mode Exit fullscreen mode

Since this is a date, Django has pre-defined options for a date field, such as filter by date published, the ones published today, in the last 7 days, etc.

You can also declare fields that will be searchable. This will add a search bar to the admin panel for questions, and you can type something in this bar to get results for a lookup of this or these fields.

polls/admin.py

# ...
class QuestionAdmin(admin.ModelAdmin):
    # ...
    search_fields = ["question_text"]
# ...
Enter fullscreen mode Exit fullscreen mode

This is the final admin file with all these options applied:

polls/admin.py

from django.contrib import admin

from .models import Question, Choice

class ChoiceInline(admin.TabularInline):
    model = Choice
    extra = 3 # will add 3 empty Choice forms when editing a choiceless (or sub-3 choice) Question


class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None, {"fields": ["question_text"]}),
        ("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}),
    ]
    inlines = [ChoiceInline]
    list_display = ["question_text", "pub_date", "was_published_recently"]
    list_filter = ["pub_date"]
    search_fields = ["question_text"]

admin.site.register(Question, QuestionAdmin)
Enter fullscreen mode Exit fullscreen mode

3.2 Customizing the Admin Panel Look

Currently the admin panel has the Django administration title. You can change it, of course.

Create a templates directory under your project directory (polltutorial, the same one containing manage.py).

We'll then need to declare our templates path so Django to look up for our new templates in the file system.

Go to the project settings and find the "DIRS" option under TEMPLATES, and declare the BASE_DIR / "templates" path inside the array.

Comments below further explain the options.

polltutorial/settings.py

# ...
TEMPLATES = [
    {
        # Specifies the template engine to be used
        # DjangoTemplates is the built-in template engine for Django
        "BACKEND": "django.template.backends.django.DjangoTemplates",

        # Defines directories where Django should look for template files
        # BASE_DIR / "templates" points to a 'templates' folder in your project root
        "DIRS": [BASE_DIR / "templates"],

        # If True, Django will also look for templates inside installed apps
        "APP_DIRS": True,

        # Additional options for the template engine
        "OPTIONS": {
            # Context processors add variables to the template context
            # These are functions that return a dictionary of data
            "context_processors": [
                # Adds debugging variables to the context
                "django.template.context_processors.debug",

                # Adds request object to the context
                "django.template.context_processors.request",

                # Adds auth-related variables (like current user) to the context
                "django.contrib.auth.context_processors.auth",

                # Adds message-related variables to the context
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
# ...
Enter fullscreen mode Exit fullscreen mode

We can then copy the admin templates from the django source code to our newly created templates directory inside the project directory and customize our admin looks directly.

NOTE: the last command below uses relative paths. You should only run it from the base of your project directory (the one with manage.py). This also assumes your project name is polltutorial.

# captures the django installation path in the django_path variable,
# adds the directory where admin views are located as a suffix to the django path,
# copies admin files from django to our project directory to better customize them
django_path=$(python -c "import django; print(django.__path__[0])") && \
source_path="${django_path}/contrib/admin/templates/admin" && \
cp -rf "${django_path}/contrib/admin/templates/admin" ./polltutorial/templates/admin
Enter fullscreen mode Exit fullscreen mode

NOTE: double check that templates is at the project root, so at the level of manage.py and not of settings.py & wsgi.py.

As an example, I'll change the site title (the one visible in the browser tab text) and the header title. You can find these in polltutorial/templates/admin/base_site.html.

Here's the original template:

polltutorial/templates/admin/base_site.html

{% extends "admin/base.html" %}

{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}

{% block branding %}
<div id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></div>
{% if user.is_anonymous %}
  {% include "admin/color_theme_toggle.html" %}
{% endif %}
{% endblock %}

{% block nav-global %}{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Here's the edited version:

polltutorial/templates/admin/base_site.html

{% extends "admin/base.html" %}

{% block title %}CUSTOM TITLE YAHOO!{% endblock %}

{% block branding %}
<div id="site-name"><a href="{% url 'admin:index' %}">CUSTOM HEADER YIPPIE</a></div>
{% if user.is_anonymous %}
  {% include "admin/color_theme_toggle.html" %}
{% endif %}
{% endblock %}

{% block nav-global %}{% endblock %}

Enter fullscreen mode Exit fullscreen mode

4.0 Django Views

A view generally serves a specific function within a specific template. In a blog you can have views such as a homepage displaying latest entries, detail page displaying more info on an entry, a comment action to post user comments, etc.

In the poll project we'll have the following views:

  • Question index: Display latest questions
  • Question detail: displays question text, with no results but with a form to cast a vote
  • Question results: displays results for a question
  • Vote action: handles voting for a choice

these will all delivered by views. Each view is a function or method (in case of class-based views). Django will select a view by examining the URL requested.

The documentation page for the URL dispatcher structure is here.

Let's add a few more views for the polls app:

polls/views.py

from .models import Question # add the models import

def index(request): # replace the current useless index view with this
    # `[:5]` looks like pure python but is equivalent to a 'LIMIT 5' SQL clause
    latest_question_list = Question.objects.order_by("-pub_date")[:5] # displays last 5 questions in the system
    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)
Enter fullscreen mode Exit fullscreen mode

And then declare the URLs:

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"),
]
Enter fullscreen mode Exit fullscreen mode

You can access these URLs in the browser and they'll render. However, the presentation aspect is unexistent. How to change them?

4.1 Django Templates

First of all, let's create a base template:

polltutorial/templates/base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Django Polls App{% endblock %}</title>
    {% block extra_head %}{% endblock %}
</head>
<body>
    <header>
        <!-- You could add some header content here -->
    </header>

    <main>
        {% block content %}
        {% endblock %}
    </main>

    <footer>
        <!-- You could add some footer content here -->
    </footer>

    {% block extra_body %}{% endblock %}
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Create a templates directory under polls; you also have to create another polls directory inside templates. Final path will be polltutorial/polls/templates/polls/<template files>; Django will look for templates there.

Then, add a template with some logic embedded in the HTML. Note that it is extending from base.html:

polls/templates/polls/index.html

{% 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 %}
Enter fullscreen mode Exit fullscreen mode

It is also required to explicitly add the template loader module into the view file that will serve views to templates. So up to now the imports in polls/views.py should be these:

polls/views.py

from django.http import HttpResponse
from django.template import loader # this one should be added now

from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by("-pub_date")[:5]
    template = loader.get_template("polls/index.html")
    context = {
        "latest_question_list": latest_question_list,
    }
    return HttpResponse(template.render(context, request))

# ...
Enter fullscreen mode Exit fullscreen mode

Here is the syntatic-sugar version of the code above. They should be functionally equivalent, but this one uses render():

polls/views.py

from django.http import HttpResponse
from django.shortcuts import render

from .models import Question


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)

# ...
Enter fullscreen mode Exit fullscreen mode

4.1.1 404 Views

Sometimes the requested content won't exist. Let's add this to the detail view in case an unexisting ID for a poll is sent in the URL. Notice that Http404 has to be requested:

polls/views.py

from django.http import Http404 # add this to requests
# ...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, "polls/detail.html", {"question": question})
# ...
Enter fullscreen mode Exit fullscreen mode

And now to polls/detail.html:

polls/templates/polls/detail.html

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
Enter fullscreen mode Exit fullscreen mode

There is another shortcutty, syntatic-sugary option to quickly serve 404 as well:

polls/views.py:

from django.shortcuts import get_object_or_404, render # notice the new import

from .models import Question


# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/detail.html", {"question": question})
# ...
Enter fullscreen mode Exit fullscreen mode

4.1.2 URL Care

In order to ensure our URL will be correctly mapped, we must sometimes have to make sure Django knows which one is which.
For instance, the same name could be used for different URLs in different apps, an URL path could be changed and etc.

4.1.2.1 Adding Views and Names as path() Params

We can specify the view directly in the path() function, and also the name.
This will make sure that the URL is mapped to a specific view, and the name will make it possible to reference this same view

polls/urls.py

# ...
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"),
]
Enter fullscreen mode Exit fullscreen mode

4.1.2.2 Namespacing URLs

We can namespace a collection of URLs; it is usually a good idea to namespace them with the app's name. So polls/urls.py should now look like this:

# ...
app_name = "polls" # notice this variable assignment
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"),
]
Enter fullscreen mode Exit fullscreen mode

We can now update our index, from this semi-hardcoded version:

<!-- ... --->
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
<!-- ... --->
Enter fullscreen mode Exit fullscreen mode

To this much more dynamic one:

<!-- ... --->
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
<!-- ... --->
Enter fullscreen mode Exit fullscreen mode

You could now even change the path structure without breaking your templates!

5.0 Django Forms

Let's update our detail view to add the ability to cast a vote to it:

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>
Enter fullscreen mode Exit fullscreen mode

This will:

  • Display a rdio button for each choice; value of each is the question choice ID. The ID will be sent on form submission.
  • Form action is set to {% url 'polls:vote' question.id %} with method="post" (the one used when server-side data is meant to be altered).
  • Note the CSRF tag just inside the form. This is important to insert in all POST forms targeting internal URLs to prevent Cross Site Request Forgery attacks.

5.1 Casting a Vote

Now let's create the view for this URL here:

polls/urls.py

path("<int:question_id>/vote/", views.vote, name="vote"),
Enter fullscreen mode Exit fullscreen mode

Note that you'll have to import F from django.db.models, HttpResponseRedirect from django.http and reverse from django.urls, and also Choice from your own models.

Notes

  • request.POST is a dictionary-like object; its values are always strings. Note that django also provides request.GET and etc, we're explicitly using POST to ensure that the data will only be altered through a POST call.
  • F("votes) + 1 instructs the db to increse vote count by 1.
  • Note that in a successful vote submission, user will be redirected to the results page; this avoids form resubmission if the user presses back to repeat the last request.
  • Lastly, note the reverse() function in the HttpResponseRedirect() constructor. It is used to dynamically generate URLs based on URL patterns; more details on this after the code.

polls/views.py

from django.db.models import F
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, 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 = F("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,)))
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

5.1.1 The reverse() function

NOTE: there is also reverse_lazy(); this should be used when the reverse() params aren't known beforehand, such as in abstract classes and decorator functions.

The basic syntax of reverse() is as follows:

from django.urls import reverse
# signature
reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None)
# named URL
url = reverse('news-archive')
# with arguments
url = reverse('arch-summary', args=[1945])
# with keyword arguments
url = reverse('admin:app_list', kwargs={'app_label': 'auth'})
Enter fullscreen mode Exit fullscreen mode
  • viewname: This can be a URL pattern name or the callable view object 2.
  • urlconf: Optional. Specifies the URLconf module to use for reversing. By default, it uses the root URLconf for the current thread 2.
  • args: Optional. A list of positional arguments to be passed to the URL pattern 2.
  • kwargs: Optional. A dictionary of keyword arguments to be passed to the URL pattern 2.
  • current_app: Optional. Provides a hint about the application to which the currently executing view belongs 2.

5.2 Generic Views

Our results and detail views are the same, and the index view is pretty similar while rendering various objects instead of one.
Django has generic helpers to keep a common structure. Let's remake most views (except for the vote one) using pre-defined generic Django classes.
We also need to redefine URLs; note that the question_id param was switched to pk (shortcut for Primary Key) in order to be correctly identified by the Django configuration so these changes can work.

polls/views.py

from django.db.models import F
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


# Each generic view needs to know what model it will be acting upon. This is provided using either the model attribute 
# (in this example, model = Question for DetailView and ResultsView) or by defining the get_queryset() method (as shown in IndexView)
class IndexView(generic.ListView):
    template_name = "polls/index.html"
    context_object_name = "latest_question_list"

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by("-pub_date")[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = "polls/detail.html"


class ResultsView(generic.DetailView):
    model = Question
    template_name = "polls/results.html"



def vote(request, question_id):
    # keep it the same
    # ...
Enter fullscreen mode Exit fullscreen mode

polls/urls.py

urlpatterns = [
    # ex: /polls/
    path("", views.IndexView.as_view(), name="index"),
    # ex: /polls/5/
    path("<int:pk>/", views.DetailView.as_view(), name="detail"),
    # ex: /polls/5/results/
    path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
    # ex: /polls/5/vote/
    path("<int:question_id>/vote/", views.vote, name="vote"),
]
Enter fullscreen mode Exit fullscreen mode

By default, the DetailView generic view uses a template called /_detail.html. In our case, it would use the template "polls/question_detail.html". The template_name attribute is used to tell Django to use a specific template name instead of the autogenerated default template name. We also specify the template_name for the results list view – this ensures that the results view and the detail view have a different appearance when rendered, even though they’re both a DetailView behind the scenes.

Similarly, the ListView generic view uses a default template called /_list.html; we use template_name to tell ListView to use our existing "polls/index.html" template.

In previous parts of the tutorial, the templates have been provided with a context that contains the question and latest_question_list context variables. For DetailView the question variable is provided automatically – since we’re using a Django model (Question), Django is able to determine an appropriate name for the context variable. However, for ListView, the automatically generated context variable is question_list. To override this we provide the context_object_name attribute, specifying that we want to use latest_question_list instead. As an alternative approach, you could change your templates to match the new default context variables – but it’s a lot easier to tell Django to use the variable you want.

Full documentation on generic views is available here.

6.0 Django Tests

Not to extend too much on an indoctrination of why tests are important, but they:

  • Ensure consistency in the app before and after changes;
  • Guide development with clear objectives;

Remember, about tests:

  • Don't test against the framework: create tests for your functionality, not if the framework is working as it should.
  • Write tests for public interfaces as they will change way less than implementations; you don't want to have headaches every time you refactor something;
  • One test class for one class (view, model, etc) is a good rule of thumb;
  • Test method names that are descriptive;

Note: you might notice that some of the tests below might contradict this advice. They are technical examples on how to do this type of test, and not examples of implementation of testing principles on why, which is a lot more context-specific and up to you to determine when and how to follow.

6.0.1 Test Driven Development

TDD is a discipline in which you start a feature development by writing tests. This is good as it uses the test itself to guide the development. Personally in very new apps I often skip it at the beginning, then start implementing it when the app reaches a certain draft stage and then keep further development with TDD, but that's purely subjective.

6.1 Testing a Model

There is a bug in the polls app: Question.was_published_recently() returns True if the question was published within the last day (as it should) but if, say, the publishing date is set for tomorrow, this would also return True, meaning that it checks for the lower bound of time but not the upper:

# create a Question instance with pub_date 30 days in the future
future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
# was it published recently?
future_question.was_published_recently() # True
Enter fullscreen mode Exit fullscreen mode

Add the tests for the correctly working version (recent is a question createn in a time bigger than or equal to 1 day ago and smaller than or equal to this instant):

polls/tests.py

import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question


class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

    def test_was_published_recently_with_old_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is older than 1 day.
        """
        time = timezone.now() - datetime.timedelta(days=1, seconds=1)
        old_question = Question(pub_date=time)
        self.assertIs(old_question.was_published_recently(), False)


    def test_was_published_recently_with_recent_question(self):
        """
        was_published_recently() returns True for questions whose pub_date
        is within the last day.
        """
        time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
        recent_question = Question(pub_date=time)
        self.assertIs(recent_question.was_published_recently(), True)
Enter fullscreen mode Exit fullscreen mode

Run the test:

python manage.py test
Enter fullscreen mode Exit fullscreen mode

...and it will fail, of course.

Now, let's refactor the model functionality based on the

polls/models.py

# ...
def was_published_recently(self):
    # How it was:
    # return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
    # How it is:
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date <= now
# ...
Enter fullscreen mode Exit fullscreen mode

And now, the tests will pass.

6.2 Testing a View

It is possible to set up publish dates for polls to the future, so they are available at the time when they should. As of now, they are available as the '5 most recent', which means that this will prioritize questions scheduled for the future.

We should show the 5 most recent ones based on the present time, not the future.

6.2.1 The Django Test Client

Django provides a client to simulate a user interecting with the project through a browser.
It is not as comprehensive as tools such as Selenium and Cypress, though.
This is an example of its use:

from django.test.utils import setup_test_environment
setup_test_environment() # sets up a template renderer; double check TIME_ZONE in settings.py as it might affect it
from django.test import Client
client = Client() # instantiates the client
# gets response from '/'
response = client.get("/") # Not Found: /
# we should expect a 404 from that address; if you instead see an
# "Invalid HTTP_HOST header" error and a 400 response, you probably
# omitted the setup_test_environment() call described earlier.
response.status_code # 404
# on the other hand we should expect to find something at '/polls/'
# we'll use 'reverse()' rather than a hardcoded URL
from django.urls import reverse
response = client.get(reverse("polls:index"))
response.status_code # 200
response.content # b'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#x27;s up?</a></li>\n    \n    </ul>\n\n'
response.context["latest_question_list"] # <QuerySet [<Question: What's up?>]>
Enter fullscreen mode Exit fullscreen mode

6.2.2 Implementing the Index View Tests

Let's add some more tests. Every test method should have a descriptive name and probably a further description on what it does.

polls/tests.py

# ...
from django.urls import reverse # dynamically creates URLs

# helper to easily create questions
def create_question(question_text, days):
    """
    Create a question with the given `question_text` and published the
    given number of `days` offset to now (negative for questions published
    in the past, positive for questions that have yet to be published).
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)


class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        """
        If no questions exist, an appropriate message is displayed.
        """
        response = self.client.get(reverse("polls:index"))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")
        self.assertQuerySetEqual(response.context["latest_question_list"], [])

    def test_past_question(self):
        """
        Questions with a pub_date in the past are displayed on the
        index page.
        """
        question = create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"],
            [question],
        )

    def test_future_question(self):
        """
        Questions with a pub_date in the future aren't displayed on
        the index page.
        """
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse("polls:index"))
        self.assertContains(response, "No polls are available.")
        self.assertQuerySetEqual(response.context["latest_question_list"], [])

    def test_future_question_and_past_question(self):
        """
        Even if both past and future questions exist, only past questions
        are displayed.
        """
        question = create_question(question_text="Past question.", days=-30)
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"],
            [question],
        )

    def test_two_past_questions(self):
        """
        The questions index page may display multiple questions.
        """
        question1 = create_question(question_text="Past question 1.", days=-30)
        question2 = create_question(question_text="Past question 2.", days=-5)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"],
            [question2, question1],
        )
Enter fullscreen mode Exit fullscreen mode

6.2.3 Implementing the Fix

Here's a reminder of how our get_queryset() method is implemented:

polls/views.py

# ...
class IndexView(generic.ListView):
    template_name = "polls/index.html"
    context_object_name = "latest_question_list"

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by("-pub_date")[:5]
# ...
Enter fullscreen mode Exit fullscreen mode

We'll fix this by:

  • Finding the current time based on the app's timezone;
  • Filtering the query with an upper-bound (ordering by publish time with the current instant as the upper-bound)

This is the new implementation:

polls/views.py

# ...
from django.utils import timezone
# ...
def get_queryset(self):
    """
    Return the last five published questions (not including those set to be
    published in the future).
    """
    # __lte suffix = Less Than or Equal to
    return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[
        :5
    ]
Enter fullscreen mode Exit fullscreen mode

6.2.4 Implementing Detail View Tests

The index now prevents the output of future quests. The detail view, however, does not.

We should add tests to the detail view to check that a question with a pub_date in the past can be displayed, one with the pub_date in the future cannot.

polls/tests.py

# ...
class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        The detail view of a question with a pub_date in the future
        returns a 404 not found.
        """
        future_question = create_question(question_text="Future question.", days=5)
        url = reverse("polls:detail", args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        The detail view of a question with a pub_date in the past
        displays the question's text.
        """
        past_question = create_question(question_text="Past Question.", days=-5)
        url = reverse("polls:detail", args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)
Enter fullscreen mode Exit fullscreen mode

Then we need to add the same constraint to the detail view:

polls/views.py

# ...
class DetailView(generic.DetailView):
    # ...
    def get_queryset(self):
        """
        Excludes any questions that aren't published yet.
        """
        return Question.objects.filter(pub_date__lte=timezone.now())
Enter fullscreen mode Exit fullscreen mode

7.0 Static Assets

You might want to serve static files such as css and images.

Create a new static directory, similar to the templates one:

mkdir polls/static
mkdir polls/static/polls
mkdir polls/static/polls/images
Enter fullscreen mode Exit fullscreen mode

And add some color to links under lists. You can also add a background image; note that you'll have to add your own background.png to polls/static/polls/images/background.png.

polls/static/polls/style.css

li a {
    color: green;
}

body {
    background: white url("images/background.png") no-repeat;
}
Enter fullscreen mode Exit fullscreen mode

Finally, you'll have to add the static template tag to your templates. It'll load your static folder.

polls/templates/polls/index.html

{% load static %}

<link rel="stylesheet" href="{% static 'polls/style.css' %}">
<!-- ... --->
Enter fullscreen mode Exit fullscreen mode

The {% static %} template tag generates the absolute URL of static files.

7.1 Final Poll Templates

polls/templates/polls/index.html

{% extends 'base.html' %}
{% load static %}
{% block extra_head %}
    {{ block.super }}
    <link rel="stylesheet" href="{% static 'polls/style.css' %}">
{% endblock %}
{% block content %}
    {% 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 %}
{% endblock %}

Enter fullscreen mode Exit fullscreen mode

polls/templates/polls/detail.html

{% extends 'base.html' %}
{% load static %}

{% block extra_head %}
    {{ block.super }}
    <link rel="stylesheet" href="{% static 'polls/style.css' %}">
{% endblock %}

{% block content %}
    <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>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

polls/templates/polls/results.html

{% extends 'base.html' %}
{% load static %}

{% block extra_head %}
    {{ block.super }}
    <link rel="stylesheet" href="{% static 'polls/style.css' %}">
{% endblock %}

{% block content %}
    <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>
{% endblock %}

Enter fullscreen mode Exit fullscreen mode

8.0 Organizing and Debugging a Django Project

8.1 The Django Debug Toolbar

This is a 3rd party app (one of the most downloaded), not a native part of the Django ecosystem, but it exemplifies very well the modular and pluggable nature of Django apps.

It shows a toolbar while inspecting the app in the browser that shows stuff like request time, SQL queries executed and etc. If you want an API-mode equivalent, check out Django Silk made by the same developers of the django debug toolbar.

Install it in the app:

python -m pip install django-debug-toolbar
pip freeze > requirements.txt 
Enter fullscreen mode Exit fullscreen mode

Further installation instructions are here in case these below don't work.

Ensure settings.py have these:

settings.py

INSTALLED_APPS = [
    # ...
    "django.contrib.staticfiles",
    # ...
]
# ...
STATIC_URL = "static/"
# ...
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "APP_DIRS": True,
        # ...
    }
]
# ...
Enter fullscreen mode Exit fullscreen mode

Add 127.0.0.1 (the localhost address) to INTERNAL_IPS.

It is also a good idea to conditionally declare the debug toolbar's middleware and add the debug_toolbar, so the toolbar does not interfere with testing.

polltutorial/polltutorial/settings.py

INTERNAL_IPS = [
    # ...
    "127.0.0.1",
    # ...
]
# ...
from sys import argv

TESTING = "test" in sys.argv
if not TESTING:
    INSTALLED_APPS = [
        *INSTALLED_APPS,
        "debug_toolbar",
    ]
    MIDDLEWARE = [
        "debug_toolbar.middleware.DebugToolbarMiddleware",
        *MIDDLEWARE,
    ]
# ...
Enter fullscreen mode Exit fullscreen mode

Finally, add the URLs to the project URLs, also conditionally to the project running outside the test environment:

polltutorial/polltutorial/urls.py

# ...
from django.conf import settings
from debug_toolbar.toolbar import debug_toolbar_urls

if not settings.TESTING:
    urlpatterns = [
        *urlpatterns,
    ] + debug_toolbar_urls()
Enter fullscreen mode Exit fullscreen mode

8.2 Separating Configs per Environment

It is smart to have an app that prioritizes ease of inspection in development and performance in production.

Let's create a new directory, polltutorial/settings, then add 4 files into it:

mkdir polltutorial/settings && \
touch polltutorial/settings/__init__.py && \ # makes directory a python module
touch polltutorial/settings/base.py && \ # base settings
touch polltutorial/settings/development.py && \ # development-specific settings
touch polltutorial/settings/production.py && \ # production-specific settings
Enter fullscreen mode Exit fullscreen mode

Then, copy all contents of polltutorial/settings.py into polltutorial/settings/base.py.

Finally, add code both in polltutorial/settings/development.py and polltutorial/settings/production.py to import the base settings:

polltutorial/settings/development.py & polltutorial/settings/production.py

from .base import *
Enter fullscreen mode Exit fullscreen mode

Finally, you have to register in manage.py that the default settings module is now polltutorial/settings/development.py instead of polltutorial/settings.py.

Let's add some specific configuration to the development environment.

Find this line:

manage.py

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'polltutorial.settings')
Enter fullscreen mode Exit fullscreen mode

And replace it with this:

manage.py

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'polltutorial.settings.development')
Enter fullscreen mode Exit fullscreen mode

You will also have to redefine the BASE_DIR since our new configuration is one directory deeper from what it was.

So, in settings/development.py, find this line:

settings/development.py

BASE_DIR = Path(__file__).resolve().parent.parent
Enter fullscreen mode Exit fullscreen mode

And replace it with this:

settings/development.py

BASE_DIR = Path(__file__).resolve().parent.parent.parent
Enter fullscreen mode Exit fullscreen mode

Restart your server and if everything went smoothly, the project should work exactly as it was already working, but now with a much better settings organization.

Adding More Detailed Logging to Console

Let's add more detailed logging to the server so we can see stuff such as SQL queries performed and etc in the console when a new view is loaded:

polltutorial/polltutorial/settings/development.py

LOGGING = {
    # Specifies the version of the logging configuration schema
    'version': 1,

    # Keeps any existing loggers from being disabled
    'disable_existing_loggers': False,

    # Defines the handlers for the logging system
    'handlers': {
        # Configures a console handler
        'console': {
            # Uses Python's built-in StreamHandler to output logs to the console
            'class': 'logging.StreamHandler',
        },
    },

    # Configures specific loggers
    'loggers': {
        # Logger for database-related logs
        'django.db.backends': {
            # Specifies that logs from this logger should be sent to the console
            'handlers': ['console'],
            # Sets the minimum log level to DEBUG (will capture all levels of logs)
            'level': 'DEBUG',
        },
        # Logger for request-related logs
        'django.request': {
            # Sends logs to the console
            'handlers': ['console'],
            # Captures all logs of DEBUG level and above
            'level': 'DEBUG',
        },
        # Logger for server-related logs
        'django.server': {
            # Outputs logs to the console
            'handlers': ['console'],
            # Logs all messages of DEBUG level and higher
            'level': 'DEBUG',
        },
    },
}
Enter fullscreen mode Exit fullscreen mode

Run the server:

python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

Now, while navigating through your app in the browser, check the output of the console tab that is running your Django server process; it should be a lot more detailed.

Running App with Production Settings

Edit production.py to add some minimal production-like settings:

production.py

from .base import *

ALLOWED_HOSTS = ['localhost', '127.0.0.1'] # when not in debug mode, hosts must be explicitly authorized

DEBUG = False
Enter fullscreen mode Exit fullscreen mode

And run the app in production mode

python manage.py runserver --settings=polltutorial.settings.production
Enter fullscreen mode Exit fullscreen mode

If you don't see as many logs as before, it means the app is correctly running in production mode.

Congratulations! You have completed the tutorial. Have fun using Django.

Top comments (0)