Happy Solstice!

Review

Yesterday I finished hooking in my first model and ran the migration. Then I opened the shell, imported the model classes and used the API to interact with my db, and created an object. Next I added a couple of methods to my classes. The docs telling me to use the string override to display the relevant information about an object is a great reminder about how OOP languages work after spending a couple years mostly in Golang. Djangos built in ORM follows relations all the way through automatically when building queries, so that is pretty spiffy too. Although as I sit here and think that through, I am wondering if there are some gotchas around that.

Next up was the Admin module, which so far seems like such a great thing to package automatically with a web framework. I can see where Djangos’s roots in the publishing world have shaped its development. Having just built a custom CMS at my last company, I’m so acutely aware of how little understanding there is around content management. To have that there for free means that a lot of companies actually never to feel the pain cuz a solution was there from the start and hopefully it should eventually change the narrative as the idea becomes more normalized.

Views were the next item on the list in the grand tour of Django. At a very basic level, the only thing that needs to exist for Django to work is a urlConf that maps a url/route to a view, and that the view returns a valid HttpResponse or throws an exception. What the view puts into that HttpResponse is wide open. There are some nice conveniences in Django, such as get_object_or_404 and get_list_or_404 in the view, and the ability to use render to take the data, template name and context and do the rest of the work of loading the template and formatting the response. It’s also important to remember that templates for an app need to be given an app namespace (dir) within the templates folder itself so that they remain unique to the app.

Templates Continued

In order to do a link, a dev might write something like this:

<li><a href="/polls//"></a></li>

But this is tedious and a bit fragile if suddenly the url needs to change (hence redirects to soften the flow), but if we did have to really change a route, finding all the places in the code would be tedious. So django gives us a way to use a template tag instead and inside it we put url 'detail' question.id

In templates, Django uses double brackets for variable assignment and {\% \%} template tags for expressions (NOTE: due to writing this post in markdown, have to escape template tags). And it uses the name given to the declared path in the urls to look up which route should be used. Now the routes declared in the urls files are the only ones that need to be maintained, everything else can just reference it by name.

# the 'name' value as called by the template tag
path("<int:question_id>/", views.detail, name="detail"),

There is one last change to make to get the polls urls and templates set up correctly. This project has only a single app called “polls”. If we added another app like “cats”, it too would have a detail path declared. Like templates, the url names declared with each path() need to be unique across the entire project. Namespacing to the rescue again. This time, we simplay have to declare the app_name in the urls.py.

app_name = "polls"
urlpatterns = [
    path("", views.index, name="index"),
    ...
]

And we change the template reference ever so slightly to polls:detail:

<li><a href="{\% url 'polls:detail' question.id \\%}"></a></li>

Forms

Here is a sample form from the django docs:

<form action="{\% url 'polls:vote' question.id \\%}" method="post">
{\% csrf_token \%}
<fieldset>
    <legend><h1></h1></legend>
    {\% if error_message \%}<p><strong></strong></p>{\% endif \%}
    {\% for choice in question.choice_set.all \%}
        <input type="radio" name="choice" id="choice" value="">
        <label for="choice"></label><br>
    {\% endfor \%}
</fieldset>
<input type="submit" value="Vote">
</form>

There’s a lot going on in there! At the top we see again how to call a namespaced path in the forms action, as well as the reminder that a form must be POST, which yay for me, is second nature already. Next up is a csrf token which django takes care of for me automatically. There is a property called forloop.counter which knows which iteration its on. That’s rad.

In order to collect that form data, the “vote” method needs a bit of an update:

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 += 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,)))

At the top we see a familiar get_object_or_404 in action, and from that question’s choice_set we try to get the value given to us from the POST request with request.POST['choice']. GET requests also have data that can be accessed in this way. The exception handler will redisplay the question along with an error if a non-valid choice is submitted. And there is also the reverse function in the view which takes a path name plus any arguments the path needs, looks up the view and template, and fulfills the request. This is a great helper so that urls don’t have to get hardcoded into view methods either.

TIL - after POSTing a form submission, best practice is to use a redirect to get the user to the next page, that way if they try to hit the back button they can’t submit data twice.

Generic Views

Django goes a step further and provides a way to reduce repetitive code within view methods. Since a detail page will pretty much always get the object by ID, 404 if not found, and pass the data into a template, Django provides generic methods where all that needs to be specified is which model and which template.

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