Tumblelog by Soup.io
Newer posts are loading.
You are at the newest post.
Click here to check if anything new just came in.

January 13 2020

January 11 2020

How to have default/initial values in a Django form that is bound and rendered

Django's Form framework is excellent. It's intuitive and versatile and, best of all, easy to use. However, one little thing that is not so intuitive is how do you render a bound form with default/initial values when the form is never rendered unbound.

If you do this in Django:

class MyForm(forms.Form):
    name = forms.CharField(required=False)

def view(request):
    form = MyForm(initial={'name': 'Peter'})
    return render(request, 'page.html', form=form)

# Imagine, in 'page.html' that it does this:
#  <label>Name:</label>
#  {{ form.name }}

...it will render out this:

<label>Name:</label>
<input type="text" name="name" value="Peter">

The whole initial trick is something you can set on the whole form or individual fields. But it's only used in UN-bound forms when rendered.

If you change your view function to this:

def view(request):
    form = MyForm(request.GET, initial={'name': 'Peter'}) # data passed!
    if form.is_valid():  # makes it bound!
        print(form.cleaned_data['name'])
    return render(request, 'page.html', form=form)

Now, the form is bound and the initial stuff is essentially ignored.
Because name is not present in request.GET. And if it was present, but an empty string, it wouldn't be able to benefit for the default value.

My solution

I tried many suggestions and tricks (based on rapid Stackoverflow searching) and nothing worked.

I knew one thing: Only the view should know the actual initial values.

Here's what works:

import copy


class MyForm(forms.Form):
    name = forms.CharField(required=False)

    def __init__(self, data, **kwargs):
        data = copy.copy(data)
        for key, value in kwargs.get("initial", {}).items():
            data[key] = data.get(key, value)
        super().__init__(data, **kwargs)

Now, suppose you don't have ?name=something in request.GET the line print(form.cleaned_data['name']) will print Peter and the rendered form will look like this:

<label>Name:</label>
<input type="text" name="name" value="Peter">

And, as expected, if you have ?name=Ashley in request.GET it will print Ashley and produce this rendered HTML too:

<label>Name:</label>
<input type="text" name="name" value="Ashley">

January 10 2020

Github basic auth deprecation and jenkins

I have been getting periodic deprecation notice emails from github for the last few months:

Hi @nenskins,

You recently used a password to access an endpoint through the GitHub API using okhttp/2.7.5. We will deprecate basic authentication using password to this endpoint soon:

https://api.github.com/

We recommend using a personal access token (PAT) with the appropriate scope to access this endpoint instead. Visit https://github.com/settings/tokens for more information.

Thanks, The GitHub Team

Hm, that @nenskins user, that is our old jenkins instance talking to github somehow. Apparently through basic auth. Only... where? Most of the github traffic seemed to use just an access token. Jenkins calls that the secret text type. Basic auth is type username with password in jenkins.

What it turned out to be was the github branch source plugin. This periodically looks at our github organisation to see if there are new projects or new branches that it missed. Normally github tells our jenkins when there's a new project or pull request or so.

Ok, on to the jenkins settings for my organisation. The confusing thing here is that the "credentials" setting says this:

Note that only "username with password" credentials are
supported. Existing credentials of other kinds will be filtered out. This
is because jenkins exercises GitHub API, and this last one does not
support other ways of authentication.

Huh? Github is refusing user/password basic auth, which is what this plugin only supports? I updated every plugin, but the problem still persisted.

I only got it after reading this bug report and especially this comment:

Isn't that message saying that you can continue to use basic auth so long as instead of using your actual password you use a personal access token. Generate a personal access token from the GitHub "Settings" page and store that personal access token in the Jenkins username / password credential as the password. Place your username as the username. Check that it works. It has been working that way for me.

Ah! So "github is refusing user/password basic auth" really means "github is refusing user/password basic auth". Using an access token instead of your password is actually fine.

The info in jenkins on those credentials actually mention that somewhat:

If your organization contains private repositories, then you need to
specify a credential from an user who have access to those
repositories. This is done by creating a "username with password"
credential where the password is GitHub personal access tokens. The
necessary scope is "repo".

So I visited https://github.com/settings/tokens and generated a new token with full "repo" rights (this is actually quite restricted in scope, despite the name).

In Jenkins I added a new global username/password credential with the github username + the access token and hurray, everything worked again.

January 09 2020

Django News - Issue 5 - Jan 9th 2020

News

Django 3.0.2 bugfix release

It's always a good idea to be on the latest version of Django. The 3.0.2 bug fix was just released. Make sure to upgrade with the help of this guide: https://docs.djangoproject.com/en/3.0/howto/upgrade-version/

djangoproject.com

7.0.0 — Pillow (PIL Fork) 7.0.0 documentation

If you manage images then you probably use Pillow. Pillow 7 is out and drops support for Python 2.7.

readthedocs.io

Articles

Typechecking Django and DRF

Nikita Sobolev guides us through the process of adding typechecking to our Django and DRF applications using a few third-party libraries.

sobolevn.me

Getting a Django Application to 100% Coverage

Adam Johnson shows us how to install and use coverage to achieve 100% test coverage in our Django Application. A few plugins were new to us, including template coverage.

adamj.eu

From Browser to Django

A solid overview of how browsers and Django work together.

mattlayman.com

I'm not feeling the async pressure by Armin Ronacher

From the creator of Flask, some deep thoughts on async.

pocoo.org

Podcasts

Django Chat - Authentication with José Padilla

José has made major contributions to the Django ecosystem, especially around Django REST Framework and authentication. He is now an engineer at Auth0.

djangochat.com

Projects

djangorestframework-stubs

Adds static type checking to Django REST Framework.

github.com

sloria/environs: simplified environment variable parsing

If you are looking for a good library to use to which supports The Twelve-Factor App methodology, then give environs a try. It works similar to envparse and django-environ, but adds a few nice features.

github.com

Events

DjangoCon Europe 2020 Announcement

Last week we let you know that DjangoCon Europe 2020 was going to be in Porto, Portugal. This week wanted you to see their official announcement and let you know that they are looking for volunteers.

djangoproject.com

This RSS feed is published on https://django-news.com/. You can also subscribe via email.

Get access to world-class Yahoo Customer Service with us

Our Yahoo Customer Service can provide an answer to every type of your yahoo question. You are free to call on our Yahoo number irrespective of your time or location. Yahoo experts are round the clock busy offering reliable solutions for yahoo issues. Some technical errors in between can irritate ...

Manage categories in yahoo inbox, dial Yahoo phone number

The categories tab in the yahoo inbox is really very useful. This is something that can help you prevent being annoyed. There is nothing that you cannot do in yahoo mail, including the addition of any new category in the box. You can get help from the techies at Yahoo ...

Fix yahoo login errors at Yahoo phone number

Login error is not an uncommon thing for yahoo. But there is nothing to worry about. You can easily get all that you need and can get everything that you want at this place. The techies at Yahoo phone number place will get you everything that you want and will ...

January 08 2020

January 07 2020

Feature Checking versus Version Checking

Measure ye these two approaches

Bruno Oliveira, known for his work on the Pytest project, tweeted this thread last July:

I always prefer explicitly checking versions instead of checking for presence of features:

# feature checking
try:
    from importlib import metadata
except ImportError:
    import importlib_metadata as metadata

# explicit checking
if sys.version_info >= (3, 8):
    from importlib import metadata
except ImportError:
    import importlib_metadata as metadata

Reasons:

1) This makes it explicit which versions supports the desired functionality, which serves as documentation and makes it easy to drop the legacy code later when that version is no longer supported: search for sys.version_info in the codebase and remove the old code.

2) In the specific case of ImportErrors, broken environments might cause an otherwise valid import to raise an error, which would then hide the real reason why the import failed. Had seen this myself a few times in "frozen" applications (cx_freeze, pyinstaller) where build problems caused ImportErrors which should not happen on that version.

I have also moved towards doing this myself, for the same reasons.

1. It’s More Explicit

Being more explicit, is the primary appeal to me. Feature-checking is normally accompanied with an explanatory comment about version numbers anyway. For example here’s some code I had in an old version of Django-MySQL:

try:
    # Django 1.11+
    from django.utils.text import format_lazy

    def lazy_string_concat(*strings):
        return format_lazy('{}' * len(strings), *strings)

except ImportError:
    from django.utils.translation import string_concat as lazy_string_concat

That # Django 1.11 comment contains useful information outside of code. It could also, like any comment, be a lie.

Additionally, unless you are very disciplined in how you format such comments, it can be hard to find them all during upgrades.

With version checking, the comment becomes code:

if django.VERSION >= (1, 11):
    from django.utils.text import format_lazy

    def lazy_string_concat(*strings):
        return format_lazy('{}' * len(strings), *strings)

else:
    from django.utils.translation import string_concat as lazy_string_concat

2. It Reveals Broken Sub-Imports

The second reason, unexpected sub-import failures, is a case of bug silencing.

I’ve not worked with frozen environments as Bruno has, but it can happen in other situations. For example, importing a Python module can fail if it tries to import a missing C library. Such ImportErrors are not related to the feature check, but the other code path would be mistakenly run as well.

This 2011 post from Armin Ronacher covers the problem in depth. It’s a good read and has a workaround “cautious import” function, which avoids catching sub-module ImportErrors.

Fin

I hope this helps you build better Python projects that support multiple language or library versions.

—Adam

January 06 2020

Counting Queries: Basic Performance Testing in Django

It's very common to read about testing techniques such as TDD and how to test application business logic. But testing the performance of an application is whole different issue. There are many ways you can do it, but a common approach is to setup an environment where you can DDoS your application and watch how it behaves. This is a very interesting

A Python Packaging Carol

I have endeavoured in this Ghostly little book, to raise the Ghost of an Idea, which shall not put my readers out of humour with themselves, with each other, with the season, or with me. May it haunt their houses pleasantly, and no one wish to lay it.

Every year around Christmas, I make a point of re-reading Charles Dickens’ A Christmas Carol. If you’ve only ever been exposed to the story through adaptations into …

Read full entry

MyCLI: A MySQL CLI Based on Python with Auto-completion and Syntax Highlighting

If you prefer to work with MySQL via its command-line interface, you'll like mycli which is a CLI tool with auto-completion and syntax highlighting built on top of Python and prompt_toolkit for building interactive command line applications with Python.

It is cross-platform and it is tested on Linux, MacOS and Windows.

According to the official website:

mycli is a command line interface for MySQL, MariaDB, and Percona with auto-completion and syntax highlighting.

MyCLI

Prerequisites

  • Python 2.7 or Python 3.4+.

How to Install MyCLI?

Supposed you have Python and pip installed, open a new terminal and run the following command:

$ pip install mycli

Check out the official website for instructions on how to install MyCLI on the other platforms.

You can check the source code of this tool on GitHub.

January 05 2020

Our Transition to Multiple Publishing Imprints Under the Roy Greenfeld Umbrella

Starting today, we're going to be using different imprints for different types of books. 

We'll continue to publish our Two Scoops of Django books under the Two Scoops Press imprint. We may publish other technical books under this imprint in the future as well.

We're moving our fantasy books to a new imprint. We just came up with it a few hours ago. Impossible Hero Books will be the imprint we use to publish all fantasy books from now on. We'll be republishing our past fantasy books under this imprint. That effort will include revising those books and producing new covers. We're excited about this new brand.

We're also going to be publishing children's books, apps, and physical products under the Fuzzy Rainbow brand. Years ago, Audrey created this imprint and published a children's book with it. We're going to be republishing that book and its corresponding mobile app. We have other children's books and products in the works too.

We've decided to call our main umbrella brand Roy Greenfeld. (Honestly, we didn't know what else to call it. :)

Roy Greenfeld will be our main company, with Two Scoops Press, Fuzzy Rainbow, and Impossible Hero Books as imprints that we use for different book genres.

Our online store is moving from twoscoopspress.com to roygreenfeld.com. This will make it possible for us to offer books from all of our imprints and genres in one unified store.

Our Transition to Multiple Publishing Imprints Under the Roy Greenfeld Umbrella

Starting today, we're going to be using different imprints for different types of books. 

We'll continue to publish our Two Scoops of Django books under the Two Scoops Press imprint. We may publish other technical books under this imprint in the future as well.

We're moving our fantasy books to a new imprint. We just came up with it a few hours ago. Impossible Hero Books will be the imprint we use to publish all fantasy books from now on. We'll be republishing our past fantasy books under this imprint. That effort will include revising those books and producing new covers. We're excited about this new brand.

We're also going to be publishing children's books, apps, and physical products under the Fuzzy Rainbow brand. Years ago, Audrey created this imprint and published a children's book with it. We're going to be republishing that book and its corresponding mobile app. We have other children's books and products in the works too.

We've decided to call our main umbrella brand Roy Greenfeld. (Honestly, we didn't know what else to call it. :)

Roy Greenfeld will be our main company, with Two Scoops Press, Fuzzy Rainbow, and Impossible Hero Books as imprints that we use for different book genres.

Our online store is moving from twoscoopspress.com to roygreenfeld.com. This will make it possible for us to offer books from all of our imprints and genres in one unified store.

January 04 2020

Mental Models for Class Based Views

Django’s class-based views are powerful and customizable, but unless you have a solid mental model of how they work, they’re also confusing and tricky to master. It’s easy to use class-based views without a deep understanding of their inner workings, which leads to Googling for which method or attribute to modify rather than confident usage of your tools.

In this post, we’ll walk through the base classes Django uses its class-based views. You’ll gain a useful picture of how class-based views work under the hood and how they differ from function-based views.

Hopefully, this will make you more comfortable working with class-based views and less dependent on StackOverflow in the future.

The entirety of this post focuses on a single Django file, /django/django/views/generic/base.py, which contains the classes that live at the top of the view classes’ inheritance trees.

If you’re looking for something specific, you can jump to that section below.

View

Right away, we can see that there is a base view class. RedirectView builds on the functionality from View. TemplateView also builds on View, and includes functionality from the ContextMixin and the TemplateResponseMixin.

Here’s the code from View.

class View:
    """
    Intentionally simple parent class for all views. Only implements
    dispatch-by-method and simple sanity checking.
    """

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

    def __init__(self, **kwargs):
        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in kwargs.items():
            setattr(self, key, value)

    @classonlymethod
    def as_view(cls, **initkwargs):
        """Main entry point for a request-response process."""
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            self.setup(request, *args, **kwargs)
            if not hasattr(self, 'request'):
                raise AttributeError(
                    "%s instance has no 'request' attribute. Did you override "
                    "setup() and forget to call super()?" % cls.__name__
                )
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())
        return view

    def setup(self, request, *args, **kwargs):
        """Initialize attributes shared by all view methods."""
        if hasattr(self, 'get') and not hasattr(self, 'head'):
            self.head = self.get
        self.request = request
        self.args = args
        self.kwargs = kwargs

    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

    def http_method_not_allowed(self, request, *args, **kwargs):
        logger.warning(
            'Method Not Allowed (%s): %s', request.method, request.path,
            extra={'status_code': 405, 'request': request}
        )
        return HttpResponseNotAllowed(self._allowed_methods())

    def options(self, request, *args, **kwargs):
        """Handle responding to requests for the OPTIONS HTTP verb."""
        response = HttpResponse()
        response['Allow'] = ', '.join(self._allowed_methods())
        response['Content-Length'] = '0'
        return response

    def _allowed_methods(self):
        return [m.upper() for m in self.http_method_names if hasattr(self, m)]

We could read this class from top to bottom, but it’s probably more helpful to follow the data. I notice that View.as_view() is something I see quite frequently in within urls.py files.

For example, in a Slack app I’m building, I have this line of code.

urlpatterns = [
    path("events/", Events.as_view()),
]

Events is the name of a class-based view, which means that .as_view() is referencing or overridingView.as_view().

Since we know that urls.py files call View.as_view() as an entry point, this seems like a good entry point for us as well.

@classonlymethod
def as_view(cls, **initkwargs):

The method signature has a few interesting details worth noting. First, it uses the @classonlymethod decorator from django.utils.decorators. As the name suggests, this makes sure that the method is only called on the class directly, not on an instance of the class. If called on an instance, @classonlymethod throws an exception. This makes sense if we consider the urls.py code from above. .as_view() is called directly on the Event view, not on an object instance.

Note that the first parameter of as_view is cls, rather than self, which makes sense for a method that can only be called directly on a class.

Finally, **initkwargs is a dictionary of keyword arguments.

Moving on.

        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

Errors are often a useful way of understanding how a piece of code is used and misused. This for loop iterates over the initkwargs and checks the keys to make sure they’re valid.

It references cls.http_method_names, which is included in the code above:

http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

So this first check is making sure that none of the keyword arguments in a urls.py file use an HTTP verb for a name. This behavior makes sense, as those http_method_names have existing purposes that developers shouldn’t override via keyword args.

The next check ensures that all keys match class-level attributes, even though we don’t see any class-level attributes defined on View, except for cls.http_method_names. Presumably, classes that inherit from View can define their own class-level attributes, which can, in turn, be overridden with initkwargs.

What follows is the most interesting piece of code in base.py.

      def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            self.setup(request, *args, **kwargs)
            if not hasattr(self, 'request'):
                raise AttributeError(
                    "%s instance has no 'request' attribute. Did you override "
                    "setup() and forget to call super()?" % cls.__name__
                )
            return self.dispatch(request, *args, **kwargs)
      view.view_class = cls
      view.view_initkwargs = initkwargs

        # take name and docstring from class
      update_wrapper(view, cls, updated=())

      # and possible attributes set by decorators
      # like csrf_exempt from dispatch
      update_wrapper(view, cls.dispatch, assigned=())
      return view

The method view() is defined within .as_view(). As you can see from the bottom of this snippet, the view() function will get returned from .as_view() to its caller, which in the case of urls.py means that view() will be passed as an argument to the path() method.

It’s not clear how path() makes use of view(), but we do know that view() isn’t executed inside of .as_view(). That’s important to keep in mind because it means the code from the snippet above won’t all be executed at the same time.

If we look inside of view(), we’ll see the first instantiation of the View class that we’ve come across.

self = cls(**initkwargs)

This is sort of tricky. Normally, when you see self in Python, it’s the name of a method argument that refers to the object itself. That’s not what’s going on here. In this case, self is just the name of a variable that is local to the scope of the view() method. self is used to store a new instance of the View class.

Just to reiterate in case this is confusing (as it was confusing to me, at first):

  1. .as_view() is called at the class level, meaning it’s not tied to an object instance.
  2. When .as_view() gets called, it returns the entire view() function.
  3. When view() gets called, later on, it creates a new instance of the View class and assigns that value to self.

Closures

You might wonder how the view() function still has access to initkwargs, given that it isn’t passed in as an argument to view(), and the .as_view() method has already returned by the time the code in view()executes.

This is something known as a closure. The basic idea is that view() has access to the variables that were available in its local scope when it was first defined. This allows for some interesting software design opportunities.

Closures make it possible to effectively make information private. When path() gets passed the view() method, path() doesn’t have access to initkwargs, because that variable is out of scope for path(). This lets view() access variables that are hidden from path().

On a related note, this ensures that values can be passed from .as_view() to .view() without telling the client code to do so. This lets path() worry only about the public API for view() while ensuring that the necessary values are available.

A full dive into closures is outside the scope of this article, so if you find this idea interesting (or infuriating), I recommend reading up on the subject, because it’s common in many programming languages.

Function-Based vs. Class-Based Views

Before diving back into the details of view(), let’s talk about function-based views vs. class-based views. I’ve always wondered how class-based views were treated by Django in comparison to function-based views.

Conceptually, function-based views are simple. Here’s a simple function-based view that returns an HTML snipped displaying the current time. The view itself receives a Request object and returns an HttpResponse.

def index_view(request):
    now = datetime.datetime.now()
    html = "<html><body>It is now %s.</body></html>" % now
    return HttpResponse(html)

The corresponding URL pattern matches a path (home/) to the view method.

urlpatterns = [
    path("home/", index_view),
]

It’s easy to picture Django with a key-value store that matches home/ to the index_view() function. When a request enters Django at the home/ path, it looks up index_view() and calls it with the Request object passed in.

Until now, I’ve never been quite clear how this is handled with class-based views. If we return to the initial urls.py file, we can see that it isn’t obvious what gets passed into path(), since the actual argument is whatever value gets returned from .as_view().

urlpatterns = [
    path("events/", Events.as_view()),
]

With the view() function, that question has been answered. When a request enters Django at events/, Django calls view() and passes it the Request object.

Now back to the nested view() function to see what happens when that request gets received.

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            self.setup(request, *args, **kwargs)
            if not hasattr(self, 'request'):
                raise AttributeError(
                    "%s instance has no 'request' attribute. Did you override "
                    "setup() and forget to call super()?" % cls.__name__
                )
            return self.dispatch(request, *args, **kwargs)

That first line of code, self = cls(**initkwargs), makes a call to View.__init__().

    def __init__(self, **kwargs):
        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in kwargs.items():
            setattr(self, key, value)

This behavior is straightforward. It receives the values that were first passed to .as_view() as initkwargs and stores them as instance attributes.

That description, “Go through keyword arguments, and either save their values to our instance, or raise an error.”, appears to be slightly incorrect, as other methods handle raising errors if the keyword arguments are invalid.

The rest of view() calls .setup(), makes sure the instance has a .request attribute, and finally calls .dispatch(), returning the result.

Before looking into .setup() and .dispatch(), let’s take a look at the last few lines of .as_view().

view.view_class = cls
view.view_initkwargs = initkwargs

# take name and docstring from class
update_wrapper(view, cls, updated=())

# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
return view

This snippet starts by setting the .view_class and .view_initwkargs arguments on the view method. This seems strange because it’s not clear to me if these values are actually used in Django. A quick search in GitHub doesn’t point to any usages outside of tests and documentation. If you know the purpose of the .view_class and .view_initkwargs attributes, I’d love to hear.

It’s especially curious since view() doesn’t make use of .view_initkwargs, instead using the initkwargs dictionary passed into .as_view().

The calls to update_wrapper() are responsible for overwriting the metadata for the view() function. This is common when a function is wrapped by another function, as is frequent when using decorator functions. To avoid returning the metadata of the wrapper function, the metadata of the wrapped function is copied over.

View.setup()

The view() function called the .setup() method, so this seems like a solid place to look next.

def setup(self, request, *args, **kwargs):
    """Initialize attributes shared by all view methods."""
    if hasattr(self, 'get') and not hasattr(self, 'head'):
        self.head = self.get
    self.request = request
    self.args = args
    self.kwargs = kwargs

Another fairly simple method. First, it checks whether there is a .get attribute but no .head attribute, and if so, assigns self.head to self.get. This makes sense, as an HTTP HEAD request returns the headers that would get returned from a full GET request. Presumably, this is just a convenience for programmers who override the .get() functionality, but forget to add .head() functionality. It’s a way for Django to guarantee that all endpoints that support GET also support HEAD.

The final three lines just take the arguments passed into the view() function and assign them to attributes of the View instance.

View.dispatch()

.dispatch() is responsible for handling the actual HTTP request and sending it to the proper method for that HTTP verb.

    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

Remember http_method_names from the top of View?

http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

.dispatch() gets a request object, and checks whether the request.method attribute matches one of those method names. If so, it looks for a method of View (or more likely, one of its children) with the same name. So when a GET request is received, .dispatch() will look for a .get() method.

If there isn’t a matching method name, then .http_method_not_allowed() is used instead. Looking below, we can see that this method just logs a warning about an invalid response, and returns an HttpResponseNotAllowed object with a list of allowed methods.

    def http_method_not_allowed(self, request, *args, **kwargs):
        logger.warning(
            'Method Not Allowed (%s): %s', request.method, request.path,
            extra={'status_code': 405, 'request': request}
        )
        return HttpResponseNotAllowed(self._allowed_methods())

The .get() method, or whichever is appropriate for the particular request, is the closest comparison to the function in a function-based view. While function-based views are called directly when a request is received, for class-based views, the view() method gets called, which in turn calls .get() or another method.

Let’s look at the final two methods in View.

    def options(self, request, *args, **kwargs):
        """Handle responding to requests for the OPTIONS HTTP verb."""
        response = HttpResponse()
        response['Allow'] = ', '.join(self._allowed_methods())
        response['Content-Length'] = '0'
        return response

    def _allowed_methods(self):
        return [m.upper() for m in self.http_method_names if hasattr(self, m)]

.options() is the only explicit declaration of a method handler on View. Developers can add as many HTTP-specific methods as they want, and Django will helpfully gather those method names for use in the .options() method.

Finally, ._allowed_methods() is a helper method used in .options() and .http_method_not_allowed(). It returns a list of available method names formatted in uppercase to match standard formatting for HTTP verbs.

Parting thoughts on View

By far, the most interesting thing I learned from reading this class is that View depends on methods whose names match the corresponding HTTP verbs to handle requests. I’m glad to have that piece of information included in my mental model of Django.

I’m also pleased to see that .as_view() functions by providing a callback. It makes the code a little less readable, but from the client’s standpoint, it makes views much easier to work with. View is an elegant abstraction that includes everything necessary to create a class-based view while giving Django the same interface as function-based views.

RedirectView

For our next class, let’s look at RedirectView. It’s a useful example because View is its only ancestor, and it provides a clear picture of how Django’s views are built on top of one another. It also provides little new functionality, as its sole purpose is to redirect requests to a different endpoint.

Feel free to skim the source code in its entirety.

class RedirectView(View):
    """Provide a redirect on any GET request."""
    permanent = False
    url = None
    pattern_name = None
    query_string = False

    def get_redirect_url(self, *args, **kwargs):
        """
        Return the URL redirect to. Keyword arguments from the URL pattern
        match generating the redirect request are provided as kwargs to this
        method.
        """
        if self.url:
            url = self.url % kwargs
        elif self.pattern_name:
            url = reverse(self.pattern_name, args=args, kwargs=kwargs)
        else:
            return None

        args = self.request.META.get('QUERY_STRING', '')
        if args and self.query_string:
            url = "%s?%s" % (url, args)
        return url

    def get(self, request, *args, **kwargs):
        url = self.get_redirect_url(*args, **kwargs)
        if url:
            if self.permanent:
                return HttpResponsePermanentRedirect(url)
            else:
                return HttpResponseRedirect(url)
        else:
            logger.warning(
                'Gone: %s', request.path,
                extra={'status_code': 410, 'request': request}
            )
            return HttpResponseGone()

    def head(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def options(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

The first couple of lines define class-level attributes. If you recall from View.as_view(), there was a test to ensure that all **initkwargs passed in from urls.py match predefined class-level attributes. There weren’t any examples of this in View, but now we’re seeing the first case of a view adding additional attributes that can be overridden via **initkwargs. It would be possible to call RedirectView.as_view(permanent=True) from urls.py or any other client.

class RedirectView(View):
    """Provide a redirect on any GET request."""
    permanent = False
    url = None
    pattern_name = None
    query_string = False

While you can probably infer the purpose of each attribute from its name, there isn’t much else to examine until we see them in action.

The next method is .get_redirect_view(), but that appears to be used only internally, so let’s look at .get(), which will call .get_redirect_view(). Remember, as we saw in View, .get() is the method that will be dispatched to handle any request that comes in via GET request.

    def get(self, request, *args, **kwargs):
        url = self.get_redirect_url(*args, **kwargs)
        if url:
            if self.permanent:
                return HttpResponsePermanentRedirect(url)
            else:
                return HttpResponseRedirect(url)
        else:
            logger.warning(
                'Gone: %s', request.path,
                extra={'status_code': 410, 'request': request}
            )
            return HttpResponseGone()

Right away we see that .get() calls .get_redirect_url(), and passes it’s own *args and **kwargs arguments through. After getting the new URL, Django makes use of one of the predefined class-level attributes, self.permanent, and depending on that value, it returns either HttpResponsePermanentRedirect or HttpResponseRedirect. Note that .get() is only handling the mechanics of choosing where to redirect; the actual redirection logic is abstracted behind various response objects.

In the event that no redirect URL is found, Django logs a warning and return HttpResponseGone.

If we look at the rest of the verb-based methods, we’ll see a consistent pattern of calling .get().

    def head(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def options(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

This is a great example of why I love reading framework code. It’s easy to think of frameworks as magical things that are impossible to understand, but right under the surface, they’re just normal code written by skilled developers. In this case, all of the redirect behavior is identical for each HTTP verb, so all of the other methods call .get() and return the response.

Finally, let’s look at the code for .get_redirect_url.

    def get_redirect_url(self, *args, **kwargs):
        """
        Return the URL redirect to. Keyword arguments from the URL pattern
        match generating the redirect request are provided as kwargs to this
        method.
        """
        if self.url:
            url = self.url % kwargs
        elif self.pattern_name:
            url = reverse(self.pattern_name, args=args, kwargs=kwargs)
        else:
            return None

        args = self.request.META.get('QUERY_STRING', '')
        if args and self.query_string:
            url = "%s?%s" % (url, args)
        return url

Django lets you define a redirect target by either defining a URL explicitly or giving a pattern name that Django will perform a reverse-lookup upon to find the matching URL. Either way, it will pull the *args and **kwargs from the original URL and attempt to insert them into the new URL.

It then does the same thing with any query parameters from the original URL. This functionality is especially helpful if you’ve simply changed the pattern for how you create URLs. For example, if your API endpoints were initially prefixed with api/, but then you add versioning so they now start with api/v1/, Django can redirect the request while maintaining all of the request data. It’s trivial to redirect example.com/api/blogs/3?order=date&reverse=true to example.com/api/v1/blogs/3?order=date&reverse=true.

TemplateView

Thus far, we’ve looked at two fully-fledged classes, but this wouldn’t be Django if it didn’t make considerable use of mixins. I often find that mixins make code harder to read, but thankfully, in this case, we can step through each piece without needing to keep other pieces in mind.

The TemplateView renders and returns templates. As you can see below, it does this via a .get() method.

class TemplateView(TemplateResponseMixin, ContextMixin, View):
    """
    Render a template. Pass keyword arguments from the URLconf to the context.
    """
    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

All of the functionality is handled by mixins. TemplateView calls .get_context_data() from ContextMixinto create the appropriate context data based on the **kwargs from the request. The context is passed to .render_to_response(), which is provided by TemplateResponseMixin.

Let’s now take a look at ContextMixin so we can see how context data is gathered before sending it to the template.

ContextMixin

class ContextMixin:
    """
    A default context mixin that passes the keyword arguments received by
    get_context_data() as the template context.
    """
    extra_context = None

    def get_context_data(self, **kwargs):
        kwargs.setdefault('view', self)
        if self.extra_context is not None:
            kwargs.update(self.extra_context)
        return kwargs

.get_context_data() is itself quite simple. It receives **kwargs, adds a few extra keys, returns `** kwargs’ to the caller.

kwargs.setdefault('view', self) is a dictionary method that doesn’t change anything if view is already in kwargs; otherwise, it adds view as a key with self (the instance of TemplateView) as the value.

We can infer that extra_context is a way for classes that use TemplateView to add additional information to the context dictionary before it is passed to the template.

Finally, let’s take a look at TemplateResponseMixin, which we assume is responsible for receiving a template name, populating that template with the provided context information, and somehow formatting this as a response.

TemplateResponseMixin

Give the class a quick skim and we’ll look at the different pieces.

class TemplateResponseMixin:
    """A mixin that can be used to render a template."""
    template_name = None
    template_engine = None
    response_class = TemplateResponse
    content_type = None

    def render_to_response(self, context, **response_kwargs):
        """
        Return a response, using the `response_class` for this view, with a
        template rendered with the given context.

        Pass response_kwargs to the constructor of the response class.
        """
        response_kwargs.setdefault('content_type', self.content_type)
        return self.response_class(
            request=self.request,
            template=self.get_template_names(),
            context=context,
            using=self.template_engine,
            **response_kwargs
        )

    def get_template_names(self):
        """
        Return a list of template names to be used for the request. Must return
        a list. May not be called if render_to_response() is overridden.
        """
        if self.template_name is None:
            raise ImproperlyConfigured(
                "TemplateResponseMixin requires either a definition of "
                "'template_name' or an implementation of 'get_template_names()'")
        else:
            return [self.template_name]

Just like we did before, let’s start by looking at the class attributes.

class TemplateResponseMixin:
    """A mixin that can be used to render a template."""
    template_name = None
    template_engine = None
    response_class = TemplateResponse
    content_type = None

The attribute names are fairly descriptive. Note that TemplateResponse is the default response class. I’ve never overridden template_engine, but it’s interesting to see that Django supports multiple levels of customization.

Now I’m curious what actually happens inside of .render_to_response().

def render_to_response(self, context, **response_kwargs):
    """
    Return a response, using the `response_class` for this view, with a
    template rendered with the given context.

    Pass response_kwargs to the constructor of the response class.
    """
    response_kwargs.setdefault('content_type', self.content_type)
    return self.response_class(
        request=self.request,
        template=self.get_template_names(),
        context=context,
        using=self.template_engine,
        **response_kwargs
    )

The first thing I notice is that the method signature mentions **response_kwargs, which TemplateView doesn’t explicitly use. **response_kwargs is merged with content_type information (which by default is None), and passed into the .__init__() method of the response class as keyword arguments.

This is somewhat curious behavior. It’s not clear to me why content_type is added to **response_kwargs, if **response_kwargs is passed into the response class (which by default is TemplateResponse) as keywords.

Otherwise, most of the arguments are straightforward. It looks like .render_to_response() follows a similar design to RedirectView.get(). It is only concerned with marshaling the proper data, and it depends on the response class to handle the underlying mechanics.

.render_to_response() calls .get_template_names(), which is our final method. Let’s take a look.

def get_template_names(self):
    """
    Return a list of template names to be used for the request. Must return
    a list. May not be called if render_to_response() is overridden.
    """
    if self.template_name is None:
        raise ImproperlyConfigured(
            "TemplateResponseMixin requires either a definition of "
            "'template_name' or an implementation of 'get_template_names()'")
    else:
        return [self.template_name]

The default behavior involves returning the template name inside of a list; otherwise, it raises an exception if no template name has been configured. In future posts, I’ll dive into other generic views that Django provides, and I’m curious to see how other classes make use of this method. This method appears to support multiple template names, which makes sense for nested templates.

Parting Thoughts

Why does this matter? It’s easy to let your eyes glaze over as you’re looking at functions that seem to either modify a dictionary or raise an exception without doing anything more exciting.

But now that we know all of the attributes used in TemplateView, we have the information necessary to use TemplateView in our own code and understand exactly what’s happening.

Let’s override a few of these attributes for a pretend views.py file.

class HomeView(TemplateView):
    extra_context = {'currency': '$'}
    template_name = 'home.html'

While this doesn’t look like much, we have enough understanding to create a useful view, add additional context, and send that data to the specified template. It’s important to keep in mind how the code we’re reading fits into Django projects.

Hopefully, you have a better understanding of how class-based views fit together, and can picture how other classes build on top of this base-level of functionality. In future posts will look into this exact subject and explore the rest of Django’s generic class-based views.

unsplash-logo Alain Pham

Older posts are this way If this message doesn't go away, click anywhere on the page to continue loading posts.
Could not load more posts
Maybe Soup is currently being updated? I'll try again automatically in a few seconds...
Just a second, loading more posts...
You've reached the end.

Don't be the product, buy the product!

Schweinderl