In this chapter, we will build the URL patterns that route to different views, processing a request sent to the server. One of the jobs of a view is to send processed information in the form of context to a template that will be used to render static or dynamically changing content. By the end of this chapter, we will have created several URL patterns for the user to visit and view data. Some examples will trigger errors or not-found exceptions on purpose to help demonstrate the concepts provided in this chapter.
Django is based on what is called the Model-Template-View (MTV) architectural design pattern, which is similar to the well-known Model-View-Controller (MVC) design pattern used for a variety of popular web-based software systems today. The view in both of these architectural design patterns is what sometimes confuses people who are starting to learn Django and come from an MVC background. In both patterns, the model is the same, and both correspond to the tables within a database. In Django, the view is best compared to the controller used in the MVC design pattern, while the template in Django's MTV pattern is best compared to the view in an MVC design pattern.
We will begin this chapter by discussing URL patterns that let us tell Django what paths we want available on a website, within our project. A path is considered anything found after the suffix of a web address, where a suffix is the .com, .org, or .edu part of a URL. The path in www.example.com/my-url-pattern/ would be /my-url-pattern/. We can tell Django to map different URL patterns to different views and we can point different URL patterns to the same view. Views are what process a request and return a response. Usually, a response is returned in the form of an HTML template, but a response can also be in the form of JSON, XML, or any other data type. Templates take context provided by a view and/or a context processor and then use that context data to render dynamic HTML in a client's browser. Context is actually a dictionary of dynamic variables that change as conditions and states change within your app. Data that lives in the database is also provided to the template through that same context. Views perform queries and/or communicate with caching systems and APIs to fetch data from a data storage device, used when rendering templates.
In this chapter, we will cover the following:
To work with the code in this chapter, the following tools will need to be installed on your local machine:
We will continue to work with the solution created in Chapter 2, Project Configuration. However, it is not necessary to use the Visual Studio IDE. The main project itself can be run using another IDE or run independently using a terminal or command-line window from within the project root folder. This is where the manage.py file resides. Whatever editor or IDE you are using, a virtual environment will also be needed to work with the Django project. Instructions for how to create a project and virtual environment can be found in Chapter 2, Project Configuration. You will need a database to store the data contained in your project. PostgreSQL was chosen for the examples in the previous chapter; however, any database type that you choose for your project can be used to work with the examples in this chapter.
We will also be using data that is in the form of a Django fixture, provided previously in Chapter 3, Models, Relations, and Inheritance, in the subsection titled Loading the Chapter_3 data fixture. Make sure the chapter_3 fixture is loaded into your database. If this has already been done, then you may skip the next command. If you have already created the tables found in Chapter 3, Models, Relations, and Inheritance, and have not loaded that fixture yet, then run the following command, after activating your virtual environment:
(virtual_env) PS > python manage.py loaddata chapter_3
All of the code created in this chapter can be found in the GitHub repository for this book: https://github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer. The bulk of the code used in this chapter can be found in the /becoming_a_django_entdev/becoming_a_django_entdev/chapter_4/ directory.
Check out the following video to see the Code in Action: https://bit.ly/3A6AxNU.
Start by creating a new app in your project called chapter_4 by following the steps discussed in Chapter 2, Project Configuration, in the subsection titled Creating a Django app. As discussed in that section, don't forget to change the value of your name = variable for your app class found in the /becoming_a_django_entdev/becoming_a_django_entdev/chapter_4/apps.py file to now point to the path where you installed your app. Be sure to also include this app in your INSTALLED_APPS variable found in the settings.py file as well.
Django controls and processes URL patterns in what it calls a URL dispatcher. Django starts with the urls.py file, which is specified as the ROOT_URLCONF variable, found in the settings.py file. Visual Studio automatically created the ROOT_URLCONF variable for us when we created a project and it should have also done so when executing the Django startproject command.
If your project did not create this variable, add the following setting to your settings.py file:
# /becoming_a_django_entdev/settings.py
...
ROOT_URLCONF = 'becoming_a_django_entdev.urls'
The urls.py file defined in the ROOT_URLCONF variable is what Django considers the root URLconf of any project, short for URL configuration. Other url.py files can be linked together by importing them using an import() function. Django looks for only one thing in these urls.py files, a single variable named urlpatterns, which contains a set of URL patterns that have been defined for a project or reusable app. This file can contain many methods, classes, and other utilities that help you formulate those patterns.
Django provides us with a variety of path functions to build URL patterns. These functions create and return elements that will be included within any urlpatterns variable. The path() and re_path() functions can accept up to four positional arguments in the following order: route, view, kwargs, and name. The first two of these arguments are required and must be defined. The first argument, route, expects a string; this can be a simple string or a fairly complex string when combining path converters and using regular expressions. If you are using a method to perform logic of some kind for this argument, it just needs to return a string. The route argument is the path that Django is listening for and then mapping to the second argument, view. The view argument is used to tell Django how to process the GET request of a URL pattern. view can perform any kind of logic. The third argument is the Keyword Arguments (kwargs), and this is a dictionary of additional keywords to pass into view. The last argument, name, is a way to map URL patterns when using other functions, such as a reverse lookup.
Let's go over some examples of using basic functions before we dive into more complicated URL patterns, using path converters.
The static() function is provided by Django to help serve up static files when running a project locally and with debug mode turned on. These are files such as images, CSS, and JavaScript files that are placed in the static folder of a Django app. This function will enable access to those static folders, allowing you to run your project and add, delete, and edit those files all without having to run the python manage.py collectstatic command to reflect those changes in your browser. Of course, in the browser, you still have to hit refresh unless you have other tools/plugins installed in your browser to update a page when it detects changes to files that it is using.
To activate static files in your local environment, in your main urls.py file, add the following import statements and append the following function to the urlpatterns variable:
# /becoming_a_django_entdev/urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [...] + static(
settings.STATIC_URL,
document_root = settings.STATIC_ROOT
)
In the preceding example, we imported the settings.py file to gain access to the values of the STATIC_URL and STATIC_ROOT variables. Since we installed the pip whiteNoise package, to work with Heroku as our host, we actually do not need to write the static() function depicted previously. This means we can skip writing the preceding code if we want to, but adding it will not hurt either and will allow your project to work on other hosts.
This can also be written using a conditional that checks whether DEBUG is enabled.
The alternative would be written as follows:
# /becoming_a_django_entdev/urls.py
...
urlpatterns = [...]
if settings.DEBUG:
urlpatterns += static(
settings.STATIC_URL,
document_root = settings.STATIC_ROOT
)
Please use only one of the examples depicted in this subsection and not both of them at the same time. You can comment out the unused one.
Let's configure media files next.
Even with the whitenoise package, we still need to use the static() function to serve up media files. Media files are just like static files except they are considered what a user would upload through FileField, ImageField, or several other methods of uploading files to a media storage device. These files are also known as User-Generated Content (UGC) and they can range from anything such as an image to a PDF document, Excel document, Word document, audio file, or even movie file. The file that gets uploaded is placed in the media folder that we created and configured in Chapter 2, Project Configuration.
To access these images when running a project locally, follow these steps:
# /becoming_a_django_entdev/urls.py
...
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [...] + static(
settings.STATIC_URL,
document_root = settings.STATIC_ROOT
) + static(
settings.MEDIA_URL,
document_root = settings.MEDIA_ROOT
)
(virtual_env) PS > mkdir media
Let's take these functions a step further and build our first path next.
A path() function takes in the route, view, kwargs, and name attributes and returns a single element to be included in the urlpatterns list. A path() function can be thought of as handling static paths as well as dynamic paths using path converters. If you want to use regular expressions to register a dynamic path converter, you will want to use the re_path() function instead.
Follow these steps to work with the path() function in your project:
When we activated the static and media URL patterns, we caused this error message to happen. This is the reason we are not seeing the famous Django success rocket ship that we are used to seeing. It's nothing to be alarmed about; it just means that we haven't created a URL pattern to handle the home page yet. This error message can be thought of as a reminder to create that home page, which we will do next.
Using the path() function, we will define a single static URL pattern that will listen for the home page URL. Before we do that, let's create the HTML file that it will serve up. When we used Visual Studio to create the chapter_4 app, a file named index.html was automatically created for us in the /becoming_a_django_entdev/chapter_4/templates/chapter_4/ directory.
The index.html file will be used as a custom home page and we will focus on just the URL pattern at this time; we will have dived into templates in more depth by the end of this chapter.
Writing a URL pattern to listen for the home page is pretty easy compared to how complex other URL patterns can be to write. Django will try to match a URL to a pattern by starting from first to last in the order that they are placed within the urlpatterns list. It's usually best to include static URL patterns at the top and then place your dynamic patterns below them. If a static pattern is similar to a dynamic pattern, the static URL pattern will be matched first, which is likely what you want to happen.
# /becoming_a_django_entdev/urls.py
...
from django.urls
import path
from django.views.generic
import TemplateView
urlpatterns = [
path(
'',
TemplateView.as_view(
template_name = 'chapter_4/index.html'
)
),
]
The preceding path() function is used to listen for a route/path defined as nothing (''), and then we are using the built-in TemplateView class, provided by the django.views.generic library, to serve up the home page in template form. Since this is a static page and a static URL, meaning no content on the page will change and the URL itself will not change either, we do not need to write a view class to handle how the context of the page will change. Instead, we can skip creating the view in this example by using the TemplateView class. With a TemplateView class, we could still pass in keyword arguments and define the name argument if we wanted to. If we did want to pass in kwargs, that would be done using the following step.
# /becoming_a_django_entdev/urls.py
...
urlpatterns = [
path(
'',
TemplateView.as_view(
template_name = 'chapter_4/index.html'
),
kwargs = {
'sub_title': 'I am the sub title.'
}
),
]
# /becoming_a_django_entdev/chapter_4/templates/chapter_4/index.html
{% load static %}
<html>
<head><title></title></head>
<body style="text-align:center">
<img src="{% static 'chapter_4/home_page.jpg' %}" role="img" alt="Home Page Image" width="400" style="margin: 0 auto" />
{% if sub_title %}
<p>{{ sub_title }}</p>
{% endif %}
</body>
</html>
We will explain more about building templates before the end of this chapter, in the Working with templates section.
One of the reasons we configured our project to handle static files in Chapter 2, Project Configuration, and in this chapter, under the subsection titled Static files, was to access those files within a template, as is done in the example shown previously. The {% load static %} tag statement allows us to begin using the static template tag, such as {% static 'chapter_4/home_page.jpg' %}. The {% static %} tag returns a working URL, pointing to the image file at http://localhost:8000/chapter_4/home_page.jpg.
(virtual_env) PS > mkdir becoming_a_django_entdev/chapter_4/static/chapter_4
Django automatically searches the static folder found in each app of a project. It is common practice to override static files, such as images, CSS, and JavaScript files, of packages that are installed in your virtual environment by including the same path and filename in the static folder of any app in your project. The same principle also applies when working with template files.
In step 5, the highlighted {{ sub_title }} variable tag is the keyword argument that was passed into that URL pattern in step 4. A custom function/callable can also be used instead of hardcoding a value here. Any context variable can be recalled in a template using the bracket syntax, {{ }}. Objects such as a dictionary, list, set, and query set can all be accessed using a period for each key and subkey, as in {{ context_variable.key.subkey }}.
Next, let's work with the include() function to import URL patterns from other apps/packages.
The include() function is used to import additional urls.py files that contain their own urlpatterns variable. This is how we can write URL patterns for reusable apps, and then include them for use in a project within ROOT_URLCONF of a site.
Let's use this to better organize our chapter-specific URL patterns by following these steps:
# /becoming_a_django_entdev/chapter_4/urls.py
from django.urls
import path
from django.views.generic
import TemplateView
urlpatterns = [
path(
'chapter-4/',
TemplateView.as_view(
template_name='chapter_4/chapter_4.html'
)
),
]
# /becoming_a_django_entdev/urls.py
...
from django.urls
import include, path
urlpatterns = [
path(
'',
include(
'becoming_a_django_entdev.chapter_4.urls'
)
),
]
Now that we have the include() example working, we will put all new URL patterns in the /chapter_4/urls.py file and organize all future chapters in a similar manner.
Now, let's practice redirecting URLs.
Instead of using the TemplateView class as we have been, we can write URL patterns to handle redirects from within the project, without having to configure them directly in a web server. This is convenient because in traditional web development, redirects are handled by the web server and it is much easier to manage in a project than it is in a web server. Redirects can be handled using the RedirectView class provided by Django.
We are going to specify a redirect rule on the http://localhost:8000/my_path/my_unwanted_url/ path to take us to http://localhost:8000/my_wanted_url/ instead. Follow these steps to configure your redirect:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import include, path
from django.views.generic
import (
TemplateView,
RedirectView
)
urlpatterns = [
...,
path(
'my_path/my_unwanted_url/',
RedirectView.as_view(
url = 'http://localhost:8000/my_wanted_url/'
)
),
]
# /becoming_a_django_entdev/chapter_4/urls.py
...
urlpatterns = [
...,
path(
'my_path/my_unwanted_url/',
RedirectView.as_view(
url = 'http://localhost:8000/my_wanted_url/',
permanent = True
)
),
]
Django also allows us to define pattern_name and query_string as additional arguments of the RedirectView class.
Note
The preceding path has a hardcoded value of http://localhost:8000/, which can become a problem in a remote environment that is not your local machine. To overcome this, you will need to adopt the concept of global context variables discussed later in this chapter in the subsection titled Creating a context processor.
Next, let's discuss using path converters to listen for dynamic path routes.
A path converter in Django is a URL pattern that is designed to listen to a dynamic path, where that path can change. There are five standard path converters built into Django and available to use: str, int, slug, uuid, and path. These are preformatted converters that allow a variety of choices and permit strings and integers within a pattern. For example, the path converter called path is used in the following code to search for any variety of characters, numbers, and certain symbols that a URL can possess.
To practice using path converters, follow these steps:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import include, path
from django.views.generic
import ..., TemplateView
urlpatterns = [
...,
path(
'my_path/<path:my_pattern>/',
TemplateView.as_view(
template_name = 'chapter_4/index.html'
)
),
]
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import include, path
from django.views.generic
import ..., TemplateView
urlpatterns = [
...,
path(
'my_year_path/<int:my_year>/',
TemplateView.as_view(
template_name = 'chapter_4/index.html'
)
)
]
When we wrote int:my_year, the my_year in this argument can be named anything we want. The same applies to the my_pattern in the path:my_pattern argument and in any other converter type. The second parameter is what is used to access that key word argument in a view class or method.
Let's write a custom path converter next.
A custom path converter is a way for us to write a class that uses a regular expression to define the path that Django listens for. The converter class is structured in a way to return the data type that is intended to be used in the view, such as an int data type used in the example of the previous subsection. This class also returns another string representation of the data type sent to the view that is intended to be used in the URL. For example, if we do not want http://localhost:8000/my_year_path/2/ to be a valid URL and we only want to allow four-digit numbers, a custom path converter can be used to accomplish this.
Follow these steps to create your custom path converter:
# /becoming_a_django_entdev/chapter_4/converters.py
class YearConverter:
regex = '[0-9]{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return '%04d' % value
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import path, register_converter
from .converters
import YearConverter
register_converter(YearConverter, 'year')
urlpatterns = [
...,
path(
'my_year_path/<year:year>/',
TemplateView.as_view(
template_name = 'chapter_4/index.html'
)
),
]
We can get even more in-depth by writing a method-based or class-based view and in that view compare whether a year is greater than, say, the year 1900, and if it is not, return a 404 response. We will discuss doing that soon in the section titled Working with conditional responses of this chapter.
Next, let's practice working with regular expression paths.
A re_path() function, better known as a regular expression path function, is similar to a path() function but allows us to pass in a formatted regular expression string as the route parameter without the need to create a custom path converter.
For example, we could write the same year example as previously without the converter class. In your /chapter_4/urls.py file, add the path shown as follows, and comment out the previous my_year_path:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import path, re_path
...
urlpatterns = [
...,
re_path(
'my_year_path/(?P<year>[0-9]{4})/$',
TemplateView.as_view(
template_name = 'chapter_4/index.html'
)
),
]
There is actually a difference between using a re_path() function and writing your own converter class. The difference is in the data type of the value of the pattern recognized when we use that value within a view class or method. With the re_path() function, the data type of this value when used in a view will always be a string, whereas the data type of the value when using a converter class will always be the data type defined by the def to_python() method of that class, meaning you can transform the data type to anything you want, if you need to.
Before we illustrate the difference in data types between using a converter class and using the re_path() function, let's map a URL pattern to a simple view.
Writing custom views is a way for us to perform all of the tasks and services needed to render a page that includes all of the content that we want. Within a view, we can validate against business logic rules to determine how to handle a request.
In this exercise, we will use the year pattern that we wrote earlier in this chapter, to only allow a year greater than 1900. Anything less than that, we will tell Django to serve up a 404 response.
A simple view is also known as a method-based view, which is a callable function in Python.
Follow these steps to map to a simple view in your project:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import ..., register_converter
from .converters
import YearConverter
from .views
import practice_view
register_converter(YearConverter, 'year')
urlpatterns = [
...,
path(
'my_year_path/<year:year>/',
practice_view
),
]
The thing we did differently from before is that we replaced the TemplateView class with a custom simple view class, called practice_view.
# /becoming_a_django_entdev/chapter_4/views.py
from django.template.response
import (
TemplateResponse
)
def practice_view(request, year):
return TemplateResponse(
request,
'chapter_4/my_practice_page.html',
{
'year': year
}
)
We are almost there. The success message we see here is to be expected. What we actually want is to return a 404 response instead of a valid path, in order to comply with the business logic discussed earlier, to only allow a year greater than or equal to 1900. Therefore, we need to use keyword arguments and conditional statements to perform custom validation when a request is processed, which we will do next.
To access keyword arguments inside of a view method, we need to pass it in as a positional argument of that method. In the example, def practice_view(request, year):, year would be the positional keyword argument. Since we defined a path converter in the urls.py file with the name of year, we are required to include year as a positional argument when accessing a view with that same name. Without this argument, Django would give us an error during runtime.
Follow these steps to configure your view method:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import ..., register_converter
from .converters
import YearConverter
from .views
import ..., practice_year_view
register_converter(YearConverter, 'year')
urlpatterns = [
...,
path(
'my_year_path/<year:year>/',
practice_year_view
),
]
# /becoming_a_django_entdev/chapter_4/views.py
from django.template.response
import (
TemplateResponse
)
...
def practice_year_view(request, year):
print(type(year))
print(year)
return TemplateResponse(
request,
'chapter_4/my_year.html',
{'year': year}
)
# /becoming_a_django_entdev/chapter_4/urls.py
...
from .views
import ..., practice_year_view
#register_converter(YearConverter, 'year')
urlpatterns = [
...,
re_path(
'my_year_path/(?P<year>[0-9]{4})/$',
practice_year_view
),
]
You can comment out the register_converter that we previously used.
Now, we can actually see the differences between writing a pattern using re_path() and the alternative, going through the steps to create a custom converter class. With the re_path() function, we now have to take additional steps in a view to convert a keyword argument into an integer before we can even check whether the year value is greater than a certain year. If we do not do that conversion, we would wind up with an error telling us '>=' not supported between instances of 'str' and 'int'. If the same regular expression pattern is used over and over again, it would mean converting a string into an integer many times, one time for each view that is being used by that pattern. This is what is known as the Write Everything Twice (WET) design principle and is usually frowned upon. Writing a converter class will solve that problem and allow you to write it just once according to the Don't Repeat Yourself (DRY) design principle.
Let's work with conditional responses next.
Instead of returning a valid TemplateResponse() like we have been doing in previous exercises, we will finally check if the value of the year kwarg is greater than or equal to 1900. If the year value is less than 1900, we are going to raise a Http404() response. Using the URL pattern that uses the custom path converter YearConverter class that we wrote earlier, we will serve up an integer instead of a string as the data type of the keyword argument year, allowing us to perform mathematical operations using that value.
Follow these steps to configure your conditional statements:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import ..., register_converter
from .converters
import YearConverter
from .views
import ..., practice_year_view
register_converter(YearConverter, 'year')
urlpatterns = [
...,
path(
'my_year_path/<year:year>/',
practice_year_view
),
]
# /becoming_a_django_entdev/chapter_4/views.py
from django.http
import Http404
from django.template.response
import (
TemplateResponse
)
def practice_year_view(request, year):
if year >= 1900:
return TemplateResponse(
request,
'chapter_4/my_year.html',
{'year': year}
)
else:
raise Http404(f'Year Not Found: {year}')
Let's link models to our views and templates next.
Using the same models that we created in Chapter 3, Models, Relations, and Inheritance, we can provide information about those objects within a template. We will write a URL pattern that will point to a new simple view method and display information about a vehicle.
Follow these steps to display model information in your templates:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from .views
import ..., vehicle_view
urlpatterns = [
...,
path(
'vehicle/<int:id>/',
vehicle_view,
name = 'vehicle-detail'
),
]
Our new view will listen for the primary key, also known as the ID, that is passed in to us as a keyword argument of that path converter. The ID is used to look up that object in the database, and if not found, it will serve up a 404 response instead. Instead of writing <int:id>, we could target the path converter to listen for a string, such as the VIN using <str:vin>. Then, in the view where we perform the database query, search for a record matching the VIN instead of the ID of a vehicle. You are welcome to practice both options.
# /becoming_a_django_entdev/chapter_4/views.py
...
from django.http
import Http404
from ..chapter_3.models
import Vehicle
def vehicle_view(request, id):
try:
vehicle = Vehicle.objects.get(id=id)
except Vehicle.DoesNotExist:
raise Http404(f'Vehicle ID Not Found: {id}')
return TemplateResponse(
request,
'chapter_4/my_vehicle.html',
{'vehicle': vehicle}
)
The preceding import statement uses two periods (..), which is Python path syntax, to navigate up one directory level and enter the sibling chapter_3 folder, to access the models that are written in the chapter_3 app. When you are working with many different apps in your project, this is common practice. The try/except block shown previously checks to see whether the requested object exists and if it does exist, a 404 response is raised.
We can access any field in a model object from within the template by using the name of the context variable that we passed into TemplateResponse. For example, when used in a template file, the vehicle context variable would be written as {{ vehicle.vin }}. This is already done in the template file you just copied into your project.
If you change the ID in your URL, the vehicle will change. If you activated the VIN as the path converter, then you would navigate to http://localhost:8000/vehicle/aa456789012345678/ in order to see the same results, using the data provided in the chapter_3 data fixture.
Now that we have views to work with, we can practice getting the reverse URL when providing only the kwarg of a path converter and the name attribute value.
Resolving a URL is the process of taking a relative path or object and obtaining the URL that relates to a unique field such as a primary key. Django's reverse resolution of URL patterns is a method of generating a URL structure using argument values that we provide instead of hardcoding URL paths in places, which can break over time. We can use template tags and statements throughout the project to use the name argument of a URL pattern. This is encouraged as best practice and follows a DRY design principle, which is less prone to breakage as your project evolves.
Let's discuss how to use the name attribute to get a reverse resolution pattern.
Using the same custom YearConverter class and the same my_year_path URL pattern that we created earlier in this chapter, do the following to configure your URL pattern.
In your /chapter_4/urls.py file, you should have the path shown in the following code block, using the highlighted name attribute:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import ..., register_converter
from .converters
import YearConverter
from .views
import ..., practice_year_view
register_converter(YearConverter, 'year')
urlpatterns = [
...,
path(
'my_year_path/<year:year>/',
practice_year_view,
name = 'year_url'
),
]
Now we can use a reverse() function, which we will do next.
The reverse() function provides us with the relative URL of an object, providing the name attribute value. In our view, we will write several print statements to tell us the relative path of objects when provided with different input arguments.
Follow these steps to configure your view method:
# /becoming_a_django_entdev/chapter_4/views.py
...
from django.urls import reverse
# /becoming_a_django_entdev/chapter_4/views.py
...
def practice_year_view(request, year):
...
print(reverse('year_url', args=(2023,)))
print(reverse('year_url', args=(2024,)))
...( Repeat as desired )...
The reverse() method is how we can look up a URL, using the arguments that are passed into that function. The reverse() method can be imported and used anywhere within a project, not just within a view class or method. This method takes in two positional arguments, the first being the name of a URL pattern, such as year_url highlighted in the preceding example, and is required. The second positional argument is the keyword arguments that get passed into the reverse() method, which is sometimes required. If there is more than one path converter defined for a URL pattern, they would be included in the reverse() method in the order in which they were created for that pattern and separated by a comma. Remember that the position of the keyword arguments pertaining to each path converter is important and follows the order in which keyword arguments were created for that URL pattern.
The {% url arg1 arg2 %} template tag works just like the reverse() method, except used directly in a template. This tag also takes in two positional arguments, just like the reverse() method does. The first argument listens for the name of the URL pattern and the second is the arguments list. These arguments are separated with a space when using this template tag. Additional arguments are provided in the order the path converters were created for that URL pattern. When using the {% url %} tag, it is acceptable to include arguments written with and without keyword syntax. For example, both of the following tags and how they are used are valid:
# Dummy Code
{% url 'year_url' 2023 5 25 %}
{% url 'year_url' year=2023 month=5 day=25 %}
The second example in the preceding code block would be used if we had actually created three path converters for the URL pattern, being year, month, and day.
They can also be replaced by using context variables, if we created the three context variables called year, month, and day to be used in a template, as shown in the following code block:
# Dummy Code
{% url 'year_url' year month day %}
The code shown previously was for illustrative purposes only and will break if you try to use it without building the related URL patterns and views.
Follow these steps to configure your project for this exercise:
# /becoming_a_django_entdev/chapter_4/templates/chapter_4/my_year.html
...
<html>
...
<body style="text-align:center">
...
<br /> <br />
<a href="{% url 'year_url' 2023 %}">2023</a>
<a href="{% url 'year_url' 2024 %}">2024</a>
...( Repeat as desired )...
</body>
</html>
Each hyperlink that is rendered points to the correlating relative path at href="/my_year_path/####/". We can continue modifying these two examples to format absolute URLs instead of relative URLs. This means we will include the www.example.com part of the URL. We will discuss that in the section titled Resolving absolute URLs later in this chapter. Let's process trailing slashes next.
In Django, we can use the re_path() function in combination with the custom YearConverter class to write one URL pattern accepting a path with and without a trailing slash, /. What this means is that we can write a URL to listen for www.example.com/my_path/ and will also allow www.example.com/my_path to render a success, essentially combining two paths into one statement.
To process your trailing slashes, in your /chapter_4/urls.py file, add the following path and comment out all other my_year_path examples:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls import (
...,
re_path,
register_converter
)
from .converters
import YearConverter
from .views
import ..., practice_view
register_converter(YearConverter, 'year')
urlpatterns = [
...,
re_path(
r'^my_year_path/(?P<year>[0-9]+)/?$',
practice_view
),
]
route is defined in the re_path() function as r'^my_year_path/(?P<year>[0-9]+)/?$', which constructs the path in a way that will listen for an optional forward slash. year is also written using just the tag name. If we were to write the statement using <year:year> as we did in previous exercises, then we would receive the following error message in the terminal or command-line window:
django.core.exceptions.ImproperlyConfigured: "^my_year_path/(?P<year:year>[0-9]+)/?$" is not a valid regular expression: bad character in group name 'year:year' at position 18
Since we are listening for a trailing slash via the use of regular expression operations, there is no need to modify values in the settings.py file, such as APPEND_SLASH. In order to actually use the APPEND_SLASH variable, Django requires the common middleware to be installed. You can learn more about using this approach instead of the regular expression approach here: https://docs.djangoproject.com/en/4.0/ref/settings/#append-slash. Using the regular expression basic structure shown previously, we don't need to worry about middleware.
Now that we have resolved relative URLs, let's resolve absolute URLs next.
An absolute URL includes the scheme, host, and port of a URL, as in the following format, scheme://host:port/path?query. This is an example of an absolute URL: https://www.example.com:8000/my_path?query=my_query_value.
Next, we will resolve an absolute URL while introducing the practice of using custom context processors.
Context processors are useful in many ways: they provide context that is shared globally among all templates and views within a project. Alternatively, the context being created in a view can only be used by the template that the view is using and no other templates. In the next example, we will create and then activate a custom global context processor where we will add the base URL of the site. We will call the context variable base_url, referring to scheme://host:port of the URL found throughout this project's site.
Follow these steps to create your context processor:
# /becoming_a_django_entdev/context_processors.py
def global_context(request):
return {
'base_url': request.build_absolute_uri(
'/'
)[:-1].strip('/'),
}
Context is returned as a dictionary of key-value pairs, where we can pack as many keys as we would like to.
# /becoming_a_django_entdev/settings.py
TEMPLATES = [
{
...
'OPTIONS':
{
'context_processors': [
...,
'becoming_a_django_entdev.context_processors.global_context',
],
},
},]
Place your custom context processor below any existing context_processors in the preceding list.
Context processors can be broken down into individual apps within a project as well. Include each additional context processor that you create inside the preceding list and in the order desired. Additional global context processor variables have been included with the code of this book for extra practice as well.
Let's use our newly created base_url context in a template next.
Using a {% url %} template tag, we can modify hyperlinks to use the context that we just made available in the previous example as a context processor called global_context().
Follow these steps to configure your template:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import ..., register_converter
from .converters
import YearConverter
from .views
import ..., practice_year_view
register_converter(YearConverter, 'year')
urlpatterns = [
...,
path(
'my_year_path/<year:year>/',
practice_year_view,
name = 'year_url'
),
]
# /becoming_a_django_entdev/chapter_4/templates/chapter_4/my_year.html
...
<html>
...
<body style="text-align:center">
...
<br /> <br />
<a href="{{ base_url }}{% url 'year_url' 2023 %}">2023</a>
<a href="{{ base_url }}{% url 'year_url' 2024 %}">2024</a>
...( Repeat as desired )...
</body>
</html>
When we added the {{ base_url }} template variable, we referenced the dictionary key of the context that was provided.
In this exercise, we will resolve the absolute URL using a request object. Follow these steps to do that in your project:
# /becoming_a_django_entdev/chapter_4/views.py
...
from django.urls
import reverse
def practice_year_view(request, year):
...
print(
request.build_absolute_uri(
reverse('year_url', args=(2023,))
)
)
print(
request.build_absolute_uri(
reverse('year_url', args=(2024,))
)
)
...( Repeat as desired )...
The preceding print statements also utilize the reverse() method found in the django.urls library.
Note
The relative path of the current page can be retrieved from the request object using print(request.path). On this page, it would return /my_year_path/2022/. Using print(request.build_absolute_uri()) without the reverse() lookup function will return the absolute path of that particular request.
Let's practice looking up absolute URLs from within a model class next.
We will be expanding on the same vehicle_view() method for this next example, to get a formatted URL from an existing object. We are going to work in the same /chapter_3/models.py file that we worked on in Chapter 3, Models, Relations, and Inheritance.
Follow these steps to configure your model class:
# /becoming_a_django_entdev/chapter_3/models.py
class Vehicle(models.Model):
...
def get_url(self):
from django.urls import reverse
return reverse(
'vehicle-detail',
kwargs = {'id': self.pk}
)
def get_absolute_url(self, request):
from django.urls import reverse
base_url = request.build_absolute_uri(
'/'
)[:-1].strip('/')
return base_url + reverse(
'vehicle-detail',
kwargs = {'id': self.pk}
)
These methods import the reverse() function that was introduced earlier in this section to get the URL of the object in reference. The import statements are added to the methods themselves instead of at the top of this document to allow for better performance handling when using these model class methods. The first method, get_url(), is used to return a relative URL path to that object, while the other method, get_absolute_url(), is intended to return the absolute path to that object.
# /becoming_a_django_entdev/chapter_4/views.py
...
from django.http
import Http404
from ..chapter_3.models
import Vehicle
...
def vehicle_view(request, id):
try:
vehicle = Vehicle.objects.get(id=id)
except Vehicle.DoesNotExist:
raise Http404(f'Vehicle ID Not Found: {id}')
else:
print(vehicle.get_url())
print(vehicle.get_absolute_url(request))
...
The else catch means the Vehicle object that it was searching for was found without errors. Remember to leave the same return statement that we previously wrote at the end of this vehicle_view() method.
We have been practicing with simple views, otherwise known as method-based views. Many projects need views to provide a bit more power and usability, which can be achieved with class-based views, which we will create next.
A view method will suffice for a lot of different situations. For more robust and large-scale projects, we can apply a few tricks to make these views more adaptable in complicated use cases. Class-based views are used when writing adaptable and reusable applications.
With class-based views, we can write code that can be reused and extended easily. Just like when we extended models in Chapter 3, Models, Relations, and Inheritance, we can extend view classes in the exact same way, whereas function-based view methods cannot provide this ability. Two templates have been provided with the source code of this book to be used in the next exercise. These two files are the exact same file as the my_vehicle.html file, except that the title of the <h1> tag in each has been changed to VehicleView Class 1 and VehicleView Class 2 so that when we run the following examples, we can see the differences between them.
Follow these steps to configure your class-based views:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from .views
import ..., VehicleView
urlpatterns = [
...,
path(
'vehicle/<int:id>/',
VehicleView.as_view(),
name = 'vehicle-detail'
),
]
Don't forget to comment out the old /vehicle/ URL patterns that were written before experimenting with this one.
# /becoming_a_django_entdev/chapter_4/views.py
...
from django.http
import Http404
from django.template.response
import (
TemplateResponse
)
from django.views.generic
import View
from ..chapter_3.models
import Vehicle
...
class VehicleView(View):
template_name = 'chapter_4/my_vehicle_class_1.html'
# /becoming_a_django_entdev/chapter_4/views.py
...
class VehicleView(View):
...
def get(self, request, id, *args, **kwargs):
try:
vehicle = Vehicle.objects.get(id=id)
except Vehicle.DoesNotExist:
raise Http404(
f'Vehicle ID Not Found: {id}'
)
return TemplateResponse(
request,
self.template_name,
{'vehicle': vehicle}
)
# /becoming_a_django_entdev/chapter_4/views.py
...
from django.http
import ..., HttpResponseRedirect
...
class VehicleView(View):
...
def post(self, request, *args, **kwargs):
return HttpResponseRedirect(
'/success/'
)
# /becoming_a_django_entdev/chapter_4/urls.py
...
from .views
import ..., VehicleView
urlpatterns = [
...,
path(
'vehicle/<int:id>/',
VehicleView.as_view(
template_name = 'chapter_4/my_vehicle_class_2.html'
),
name = 'vehicle-detail'
),
]
The def get() submethod depicted in step 4 is where all of the code in the method-based view is moved to. It's also the only required method. Other optional methods, such as def post(), are used when working with form objects, when a postback response is executed. It can also be used to redirect the user to a success page, which is illustrated in the code of step 5, but you will never get Django to trigger this redirect with how we are using this class now, which is to be expected. We will discuss this in more depth later in Chapter 5, Django Forms. When we are working with positional keyword arguments of a URL, they are passed into the view class, where the id attribute is written in the preceding get() method. If you have more than one keyword argument, they would be added after id in the order that they exist in that URL pattern.
We performed step 7 and step 8 just to check that this is working and to see how we can still override default settings just like we did earlier in this chapter. Let's extend our class-based views next, also known as inheritance.
Extending class-based views, also known as inheritance, is done in the exact same way as when we extended model classes in Chapter 3, Models, Relations, and Inheritance. We can display the same title on the page by extending the first class into a second class, eliminating the need to define template_name in the URL pattern itself, among many other benefits.
Follow these steps to extend your class:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from .views
import ..., VehicleView2
urlpatterns = [
...,
path(
'vehicle/<int:id>/',
VehicleView2.as_view(),
name = 'vehicle-detail'
),
]
# /becoming_a_django_entdev/chapter_4/views.py
...
class VehicleView2(VehicleView):
template_name = 'chapter_4/my_vehicle_class_2.html'
The preceding example is just a very simple extension of the existing VehicleView class that demonstrates how to extend a view class. The only thing we are changing/overriding in this exercise is the template_name variable in order to demonstrate this concept.
Next, let's learn what asynchronous views are used for.
Django also offers support of asynchronous views, a feature first introduced in Django 3.1. Asynchronous views are views that can be processed in individual processing threads and run together at the same time. These are used to build better multithreaded apps. Traditional Django projects use the Web Server Gateway Interface (WSGI) by default. To actually use asynchronous function- and class-based views, we need to configure a project and the server to use the Asynchronous Server Gateway Interface (ASGI) instead of WSGI. Since this requires quite a bit more work to configure the server and potentially the hosting provider, we will skip providing any examples for this section, but if this is something you want or need in your project, you can get started here: https://docs.djangoproject.com/en/4.0/topics/async/.
Up to now, we have been using templates that have been pre-built and provided with the code of this book in order to demonstrate core programming concepts. Next, let's explore what it takes to actually build those templates on our own.
The Django template language provides us with a set of template tags and template filters that are used to perform simple actions directly within a template. It makes it easy to perform simple logic operations, such as Python operations. Tags and filters are actually two different things that closely resemble each other. The Django template language can be closely compared to Shopify's Liquid syntax and is similar to the Razor syntax used in ASP.NET frameworks, but the Django template language is a bit easier to use and read. Django also allows us to create custom tags and filters for use within a project. Custom filters are most commonly used to transform a single context variable. Custom tags provide for more robust and complex use cases. For a complete breakdown of all of the template tags and template filters that exist, read the official Django documentation about them here: https://docs.djangoproject.com/en/4.0/ref/templates/builtins/.
Next, we will touch briefly on the features and capabilities of the most commonly used template tags and filters that are available.
We can structure a template to feel more like an app by breaking it down into smaller components. Those components can then be used interchangeably within other templates. For example, we can write a base template that contains the <head> and <body> elements of a page and then break apart subtemplates that structure the body content of each of those templates. Areas can be created in the <head> and <body> of a document where we can pass dynamic text and HTML into them, such as the <title> tag.
For the next example, let's use the {% block %}, {% extend %}, and {% include %} template tags to create two template files, demonstrating how to break templates down into manageable pieces.
Follow these steps to configure your template tags:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from .views
import ..., TestPageView
urlpatterns = [
...,
path(
'test_page_1/',
TestPageView.as_view(),
name = 'test-page'
),
]
# /becoming_a_django_entdev/chapter_4/views.py
...
from django.template.response
import (
TemplateResponse
)
from django.views.generic
import View
...
class TestPageView(View):
template_name = 'chapter_4/pages/test_page_1.html'
def get(self, request, *args, **kwargs):
return TemplateResponse(
request,
self.template_name,
{
'title': 'My Test Page 1',
'page_id': 'test-id-1',
'page_class': 'test-page-1',
'h1_tag': 'This is Test Page 1'
}
)
In the TestPageView class, we are defining a default template_name as 'chapter_4/pages/test_page_1.html'. In the get() method, we pass in hardcoded context variables to be used in this demonstration. In a real-world scenario, this information would be generated after performing logic that is written to generate those values.
# /becoming_a_django_entdev/chapter_4/templates/chapter_4/pages/test_page_1.html
{% extends 'chapter_4/base/base_template_1.html' %}
{% load static %}
{% block page_title %}{{ title }}{% endblock %}
{% block head_stylesheets %}{% endblock %}
{% block js_scripts %}{% endblock %}
{% block page_id %}{{ page_id }}{% endblock %}
{% block page_class %}{{ block.super }} {{ page_class }}{% endblock %}
{% block body_content %}
{% if h1_tag %}
<h1>{{ h1_tag }}</h1>
{% else %}
<h1>Title Not Found</h1>
{% endif %}
{% endblock %}
This template starts with the {% extends %} template tag, which states that we want to actually start with the /chapter_4/base/base_template_1.html file, even though we specified the test_page_1.html file in our view class. Then, everywhere there is a {% block %} tag found in this file, we override or append to that same {% block %} found in the base_template_1.html file that we are extending. We are passing the value of {{ title }}, which was defined in the view, into the {% block page_title %} tag of the /chapter_4/pages/test_page_1.html file. The {{ block.super }} tag can be used to keep what is found in that same block of the base_template_1.html file. Without this tag, all code inside the parent block will be overwritten. HTML can be written inside of any block; the {% block body_content %} block, shown in step 5 that follows, is where the bulk of the page content will be found.
# /becoming_a_django_entdev/chapter_4/templates/chapter_4/base/base_template_1.html
{% load static %}
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>{% block page_title %}My Page Title{% endblock %}</title>
<link rel="stylesheet" href="{{ base_url }}{% static 'chapter_8/css/site.css' %}">
{% block head_stylesheets %}{% endblock %}
<script defer type="text/javascript" src="{{ base_url }}{% static 'chapter_8/js/site-js.js' %}"></script>
</head>
</html>
# /becoming_a_django_entdev/chapter_4/templates/chapter_4/base/base_template_1.html
...
</head>
<body id="{% block page_id %}{% endblock %}" class="{% block page_class %}base-template-class{% endblock %}" style="text-align: center;">
{% block header %}
{% include 'chapter_4/headers/header_1.html' %}
{% endblock %}
{% block site_container %}
<div class="site-container">
<div class="body-content">
{% block body_content %}
{% endblock %}
</div>
{% block footer %}
{% include 'chapter_4/footers/footer_1.html' with message='Footer of Document' %}
{% endblock %}
</div>
{% endblock %}
{% block js_scripts %}{% endblock %}
</body>
</html>
In the preceding steps, the main HTML of the page is broken down into a header, body content, and footer format. The {% include %} tags used previously demonstrate different ways of working with those files. Adding a with attribute to any {% include %} tag is how we can pass context into that file from the parent template. That is what is done to the preceding footer file. This means making context available without the need for a context processor or by writing code twice. The preceding HTML is structured in a way that allows us to get fancy by modifying everything within the {% block site_container %} tag, if we wanted or needed to. In order to do that, we would write the {% block site_container %} block again in the file that extends this template file and write the modified code there. That is essentially what step 3 did for us, with the {% block body_content %} tag.
Let's work with template filters next.
Template filters are a way to transform the value of a context variable. They can do things such as make a string upper or lowercase using the {{ context_variable|upper }} or {{ context_variable|lower }} filters. They can be used to find the number of items in a list using the {{ my_list|length }} filter or even format time with a {{ my_time|time:" n/j/Y" }} filter. When using a time filter, it is not necessary to specify the :" n/j/Y" argument of that filter. Even without these specifications, Django will default to the setting specified in your settings.py file as the TIME_FORMAT variable. To learn about all of the filters that are available, visit the official Django documentation found here: https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#built-in-filter-reference.
Let's check out custom tags and filters next.
Earlier, in Figure 4.10, we saw that the value of the make of the vehicle was displayed as the number 3. This is a perfect example of how we can write a custom filter that takes in a numeric value and returns the string representation of that value.
Follow these steps to create your custom filter:
# /becoming_a_django_entdev/chapter_4/templatetags/chapter_4.py
from django.template
import Library
register = Library()
@register.filter(name = 'vehicle_make')
def vehicle_make(value):
from ...chapter_3.models import MAKE_CHOICES
for i, choice in enumerate(MAKE_CHOICES):
if i == value:
try:
return choice[1]
except ValueError:
pass
return ''
Here, we are writing a very simple method called vehicle_make() that takes in the numeric value of 3 and returns to us the string representation of Chevrolet, when used in a template. In this method, we are using Python path syntax to import the MAKE_CHOICES variable, which we created in Chapter 3, Models, Relations, and Inheritance, in the subsection titled Mutable versus immutable objects.
# /becoming_a_django_entdev/chapter_4/urls.py
...
from .views import ..., vehicle_view
urlpatterns = [
...,
path(
'vehicle/<int:id>/',
vehicle_view,
name = 'vehicle-detail'
),
]
# /becoming_a_django_entdev/chapter_4/templates/chapter_4/my_vehicle.html
{% load static chapter_4 %}
...
{% if vehicle %}
...
<p>{{ vehicle.make }}</p>
<p>{{ vehicle.make|vehicle_make }}</p>
...
{% endif %}
...
In order to use the template filter that we registered, we import it into the HTML file using the {% load chapter_4 %} tag, where the name of the template tag set that we are loading is the name of the Python file that we created in any templatetags folder of an app.
Creating custom template tags instead of custom filters can be done by changing @register.filter(name = 'my_filter') to @register.tag(name = 'my_tag'). In this scenario, the tag can be used in a template similar to {% my_tag %}. To learn more about the complexities of writing your own template tags and how they can be useful in your project, visit the official documentation on that subject found here: https://docs.djangoproject.com/en/4.0/howto/custom-template-tags/#writing-custom-template-tags.
Next, let's add some custom error pages.
Django provides a very easy way to create your own custom error page templates for errors such as 400, 403, 404, and 500. Other errors, such as a 502 Bad Gateway error, will not work since the error implies there is a problem with your web server and not with Django itself. For the four error types that we can manipulate, we can create four files in any of the templates directories, 400.html, 403.html, 404.html, and 500.html, as long as they are not placed in a subfolder. These four template files have been provided with the code of this book and follow the same design pattern as depicted in the subsection titled Template tags of this chapter. In order to see a custom debug template, we must turn off DEBUG in the settings.py file.
Follow these steps to configure your error pages:
# /becoming_a_django_entdev/settings.py
...
DEBUG = False
(virtual_env) PS > python manage.py collectstatic
By now, we have constructed what might feel like an entire project, but in reality, an application will consist of so much more than what was covered in this chapter. What we do have is a way to route URL paths to views and render different contexts in each template used. We learned how we can query the database in a view to get the data that we want to render in a template. We even covered the different ways we can handle and process an error page or simply redirect a URL to another path. We even used class-based views to write reusable class structures, making a project more adaptable to change in the long run.
In the next chapter, we will discuss how we can use form objects in combination with the function-based and class-based views and templates we learned how to create in this chapter.
18.225.95.216