Django Super Quick Essential Start Guide

I recently started learning the Django web framework. The quick start documentation is terrific, but I could use a condensed version. That's this page. Intent here is to have a single-page version of all of the key info needed to get a Django app up and running. This guide focuses on basic, essential information – future guides will contain info on more advanced topics.

This guide assumes that Python 3, Django, and python-dotenv are already installed on the system.

New app, file system configuration

Go to directory where you'd like to store your code, then:

django-admin startproject mysite

where “mysite” is the name of your project.

To create an app within this project, go into the “mysite” folder, then:

python manage.py startapp appname

where “appname” is the name of the app within this project.

If, at any point, you want to run the test server, you can do so with:

python manage.py runserver

Unfortunately, this configuration has some security problems by default, and you should fix them before pushing to Github.

First, create the file “mysite/.gitignore”. Use the toptal configuration at this link as a start.

In “mysite/settings.py”, you'll find

SECRET_KEY=<long random string>

Copy this entry into a file called “.env” in your “mysite” directory. Then, in “mysite/settings.py”, change the entry to the following:

SECRET_KEY = os.environ['SECRET_KEY']

Setting up views

The server uses the following chain of calls for views: 1. “mysite/urls.py” –> 2. “appname/urls.py” –> 3. “appname/views.py”.

In “mysite/urls.py”, you'll need to configure the following to get started:

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

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

In “appname/urls.py”, you'll start with the following:

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

Then, in “appname/views.py”, this will be tuned more specifically to your individual app needs. For a basic “Hello World!” configuration, you can use:

from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the appname index.")

(Very important to note that the “ttp” is lowercase. “HTTPResponse” will fetch you nothing but heartache.)

More commonly, paths will likely involve some data reference in the URL. In “appname/urls.py”, the paths are checked sequentially through the urlpatterns list until a match is made. For instance, the following config might be used in a polling application:

app_name = 'polls'
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'),
]

“app_name” creates the “polls” namespace for these calls, so that all calls here must be prefaced by “https://mysite.com/polls". This helps separate this page from other, potentially similarly-named pages in other apps of the project.

So, if you visit “https://mysite.com/polls/5/", it would get processed by the second entry down. The number “5” would get processed as a keyword argument for the function called (in this case, views.detail), with “question_id” being the variable name associated with the value, and “int” being the type of acceptable patterns being matched. (If someone tried to visit “https://mysite.com/polls/hello!/", they'd get a 404 “No page found” since it doesn't fit an integer pattern.)

With the “path” command: 1. The first argument is the URL pattern to be matched after stripping away the domain and app name (in this case, “https://mysite.com/polls") 2. The second argument is the function to call in “appname/views.py” (in the format “views.“ 3. The third argument contains other variables to be sent to that function. Most commonly, this is used for providing a helpful separation between the page function called and the specific URL formatting used to get there. This is most helpful in templates (see that section below for more details).

Models and Migration

Models go in “appname/models.py”. Here is an example for that file:

from django.db import models

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

    def __str__(self):
        return self.question_text

(It's good practice to include the str function for each model type so that it is easy for model objects to describe themselves.)

The call chain for migration calculations goes: 1. “mysite/settings.py” (looking at INSTALLED_APPS) –> 2. “appname/apps.py” –> 3. “appname/models.py”.

In “mysite/settings.py”, update the section under “INSTALLED_APPS” to feature your app. It should look similar to the following (note the first entry in the list):

INSTALLED_APPS = [
    'appname.apps.AppnameConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

In the “appname/apps.py” file, there is already likely a reference to “AppnameConfig”, so you're probably good there.

To create the migration, make sure you're in the “mysite” directory, then use the command:

python manage.py makemigrations appname

To then execute the migration, use:

python manage.py migrate

More on Views

Above, the basic pattern used for views utilized HttpResponse to send something back to the requester. Here are few more sophisticated options.

For contextless pages that just push data to the user (like an index page showing the top 5 blog entries, but don't use specific input from the user), you can use the “render” function instead of HttpResponse.

For example, in “appname/views.py”, you could use:

def index(request):
    latest_post_list = Post.objects.order_by('-pub_date')[:5]
    context = {'latest_post_list': latest_post_list}
    return render(request, 'appname/index.html', context)

Here, “latestpostlist” is a variable with data pulled from the database, “context” repackages latest_post_list to be sent to the template rendering engine. In the “render” function: 1. The first entry (“request”) is the context received from the requester 2. The second entry (“appname/index.html”) is the template to pull for rendering 3. The third entry (“context”) is the data to be sent to the template rendering engine.

For pages that are guided by user input (say, selecting a specific blog post), you can use “get_object_or_404()” to return a page if the object is found or a 404 if it isn't.

For example, in “appname/views.py”, you could use:

def detail(request, post_id):
    post = get_object_or_404(Post, pk=post_id)
    return render(request, 'appname/detail.html', {'post': post})

If there's no object, page goes straight to 404 without reaching “return render”. If there is an object, it processes normally through the “return render” line, following the same convention as above. If you're grabbing a list instead of an object, you can instead use “get_list_or_404()“.

Templates

Templates use the Jinja2 conventions for integrating variable data into HTML pages. By convention, templates are stored in your file system at “mysite/appname/templates/appname/pagename.html”, where “pagename.html” is the name of the template for that page. In the “appname/views.py” file, a template render would reference “appname/pagename.html” for that particular template.

The following example shows common markup used in templates:

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
{% endfor %}
</ul>

Double brackets are used for variables shown directly. Properties on an object are accessed using dot notation (“question.question_text”, where “question_text” is a property of the object “question”).

{% %} brackets are used for control elements. Note the “for” and “endfor” blocks.

For creating links to other parts of the site, the format “{% url 'detail' question.id %}” is used. Here, 'polls:detail' references the “name” variable in the path function from “appname/urls.py” in the polls app (see section above). Rather than use a hardcoded path for this link, it looks up whatever path has the name “details” in the polls app, then uses the link format to create the link, using “question.id” as the input to the link format. So, if question.id = 34, it would create the link “https://mysite.com/appname/polls/34/" and insert into this part of the template.

Forms

Within the template for the form page, the code will be structured something like the following:

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}

[Form tags and code]

</form>

Some key elements here: – In the opening tag, the “action” element points to the entry in the “appname/urls.py” page that will process the form. – The “method” in that tag is “post”, as it will be for every form. – “{% csrf_token %} is a data tag which adds security to your form by helping prevent cross-site request forgeries. This should be included in every form as well.

The view for this form will also need to be updated to accommodate the various states of the form entry sequence (new form, incorrectly filled form, completed form). An example is shown below:

def vote(request, question_id):
    #retrieve question object (based on question_id) from database,
    #either return object if object exists or a 404 page if it doesn't
    question = get_object_or_404(Question, pk=question_id)

    #attempt to assign the selected choice based on the completed form
    try: 
        selected_choice = question.choice_set.get(pk=request.POST['choice'])

    #if form was completed incorrectly
    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.",
        })

    #form was completed correctly, update database
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

Some notes on the above: – request.POST is a dictionary-like object that allows you to look up the returned data by key. The data returned are always strings. – On successfully processing the data, the view returns an “HttpResponseRedirect” which takes one argument: the URL to which the user will be redirected after successful processing. – In the “HttpResponseRedirect”, the example uses “reverse('polls:results', args=(question.id,))” to perform a look-up in the “appname/urls.py” file in a similar fashion to the templates.

Admin Site

Before you can create an admin site, you need a superuser for the site. Do this with:

python manage.py createsuperuser

Then follow the prompts to create the user.

To add a model to the administrator site, update the “appname/admin.py” file in a manner similar to the one below:

from django.contrib import admin

from .models import Question

admin.site.register(Question)

Automated Testing

Tests for the app live in “appname/tests.py”. An example of a test to verify a function only returns True if the question dates are recent but not in the future may look like the following:

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)

Some notes on the above: – For the “Question” model, we created a subclass of the TestCase class specifically to examine this model and functions relating to it. The various test methods for this model are then built under this subclass. – The test methods must all begin with the prefix “test_” (the Django testing suite specifically looks for this) – The test validation is done on the final line – in this case, after the function is executed, it must return “False” in order to have a passing test.

The tests can then be run with the command:

python manage.py test appname

which then returns the test results to the command line.

The process executed with this command is: 1. “manage.py test appname” looks for “appname/tests.py” 2. It identifies a subclass of “django.test.TestCase” 3. It creates a special database for testing 4. In the example above, it creates an instance of that model with the specified pub_date 5. It checks the accuracy of the function for the model instance using the “assertIs()” method.

Some other validation functions that are especially helpful for testing templates:

#Use this command to check the information sent back in the page's "post" response
self.assertQuerysetEqual(response.context['latest_question_list'], [])

#Use this approach to check the type of page response received on a request
self.assertEqual(response.status_code, 404)

#Use this approach to perform a text-match within the response
self.assertContains(response, past_question.question_text)

Written by Dulany Weaver. Copyright 2022-2024. All rights reserved.