Any web application is here to provide and receive information from clients. A client sends a request to the server, and it answers with some response. Usually, the communication between these two sides is processed by the HTTP protocol using GET, POST methods and also some other types. While HTTP protocol defines multiple different methods, only GET and POST are used by web browsers. Other methods are most commonly used in REST APIs; you can read more about them in the REST API tutorial.
This topic will focus on processing GET requests in Django as they are most frequently used. As the name suggests, they are used to GET some data from the server.
Processing GET requests
My friend Willy Wonka heard about Django and wanted to use it for advertising the candy his factory makes. To help him reach the goal, we will create a simple response with candies.
We assume that the name of the Django project is factory and the application name is candies.
Don't forget to add "candies" to INSTALLED_APPS in the settings.py file
Let's look at the project layout:
candies = {
"Fudge": {
"color": "beige",
"price": "priceless",
"available": 100,
},
"Chocolate shock": {
"color": "brown",
"price": "precious",
"available": 50,
},
"Marshmallow" : {
"color": "pink",
"price": "all the money in the world",
"available": 200,
},
}Add this dictionary right to the top of the candies/views.py file.
On the main page, Willy wants to put only the list of what he has and nothing else. So when you go to the site, you send a GET request to his service. GET is a method to receive data from the server.
Every request to a current address of the Django application is processed with view functions. These are methods that take a request object (HttpRequest class instance) as an argument and then return a response to the client. The request object contains all the essential data about the request itself: request type, for example, and many more that we'll explore later. The response object is also an inheritor of the Django built-in class HttpResponse.
Let's add this piece of code to the candies/views.py module:
from django.http import HttpResponse
def main_page_view(request):
if request.method == "GET":
html = "\n".join(f"<div>{candy}</div>" for candy in candies)
return HttpResponse(html)View functions are just like usual Python methods. As described above, our brand new main_page_view function takes request as an argument and returns an HttpResponse object (import it from django.http).
Every view function processes a GET request by default, but to clarify the example, we're explicitly checking if the request method was GET. If the same view function is created to process multiple request methods, we can easily add more if-clauses and directions on what to do when each particular method is triggered. For example, if the request.method is POST, you may want to process some incoming data as well.
So, back to our sweet matters: passing HTML as a string to the HttpResponse class, we return a simple HTML page with a list of candies. The Django application does the rest of the work to send the data to the client. It's that simple to make a response.
URL Routing
We create request handlers, but how does Django choose the appropriate one? In Django applications, we store all the addresses in urls.py files. Usually, there is one main urls.py located in your project directory, and each application has a separate urls.py module located as <app_name>/urls.py. Technically, you can store all your addresses inside the main urls.py file, but that's only acceptable when working on a small project. As the project grows, you'll have to store more and more URLs, and it's better to separate them instead of keeping thousands of paths in one file. This approach will also make it easier to re-use the application in other projects, as app-specific routing is defined within that app rather than on the project level.
In this topic, we'll start with this concept right away. Let's create an urls.py file in your candies app and put some code there:
from django.urls import path
from .views import main_page_view
urlpatterns = [
path("candies/", main_page_view),
]Then add the following lines to your project's urls.py:
from django.urls import include, path
urlpatterns = [
path('', include('candies.urls')),
]This means that all URLs defined in candies/urls.py are accessible at the root address of your Django app. So now, for example, if Willy's site has the hostname www.willywonka.com, then the assortment page will be available at the address www.willywonka.com/candies/.
To bind a link with an appropriate handler, we called the path() function and added the result to the urlpatterns list. The first argument of path() receives a string describing a link pattern that comes after the hostname. The second argument is a handler that will process a request (main_page_view in the example above) or a list of other URL patterns that have paths with addresses and handlers (include('candies.urls')). Note that if routing is defined with include() function, the part of the URL that was matched is removed before proceeding to candies.urls, so we could get the same result of a call to www.willywonka.com/candies/ if routing was defined as below
candies/urls.py:
urlpatterns = [
path("", main_page_view),
]urls.py:
urlpatterns = [
path('candies/', include('candies.urls')),
]The link pattern may be a simple string and a mapping in case we need to get output depending on the link input. Let's see that as an example.
Passing parameters to URLs
What if we want to display detailed information for each candy on a separate page? Of course, we could add the three new URLs to the candies/urls.py: /candies/Fudge/, /candies/Chocolate_Shock/, and /candies/Marshmallow/. All good, but what if we have a hundred different types of candies on our website? We don't want to type this many addresses by hand, do we? Especially when they look so similar.
That's when the URL patterns come in handy. Let's add one single line to candies/urls.py (to the urlpatterns list):
path("candies/<name>/", candy_info_view)This pattern will process any request sent to /candies/any_value/. Don't forget to import candy_info_view from .views, we'll create it in a moment.
Note that Django checks all the URLs individually in the order they are placed inside the urlpatterns list until it finds a function suitable for this address. This means if at some point you'll want to create the /candies/discounts/ path, make sure to put it before the /candies/<name>/ path. Otherwise, the discounts view will never be reached.
Now, let's create a view to display detailed info on each candy. Add the following method to candies/views.py:
from django.http import HttpResponse
def candy_info_view(request, name):
title = f"<h1>{name}</h1>"
html = "\n".join(f"<div>{attr}: {value}</div>" for attr, value in candies[name].items())
return HttpResponse(title+html)As you see, the name argument received from the URL pattern is passed as an argument to the view function. After that, we can process the value and send a response depending on the received input argument value.
Class-based views
So, we have created an app with only one type of view and only one type of request. Sometimes apps can be more complicated, with many and many different pages. In this case, it can be impractical to use separate functions for every possible request, primarily when some should be associated with others. For such cases, Django has Class-based views (CBV). It's an alternative way to implement views as Python objects instead of functions. With class-based views, we can create different methods for the same view instead of conditional branching and use inheritance instead of using the same code repeatedly. They do not replace function-based views, rather they are mainly used to keep the codes DRY (don't repeat yourself, a principle of software development aimed at reducing repetition of software patterns) and make the codes extendable. Generally, class-based views inherit and override the generic view classes or the base classes that Django has provided. These generic and base classes are used as boilerplates. Using class-based views, we can better structure our codes. However, class-based views are harder to implement and read and require a good understanding of inheritance and other concepts of OOP.
So, now we can change our view this way:
from django.http import HttpResponse
from django.views import View
class MainPageView(View):
def get(self, request, *args, **kwargs):
html = "\n".join(f"<div>{candy}</div>" for candy in candies)
return HttpResponse(html)
class CandyView(View):
def get(self, request, name, *args, **kwargs):
title = f"<h1>{name}</h1>"
html = "\n".join(f"<div>{attr}: {value}</div>" for attr, value in candies[name].items())
return HttpResponse(title + html)We make a new class and inherit them from view. To provide a method to handle the GET request, we define a method with the name get. This is a general rule in Django, so if you want to make a POST handler, you define a method with the name post.
Change the urls.py module, and don't forget to import our new classes:
from django.urls import path
from candies.views import MainPageView, CandyView
urlpatterns = [
path("candies/", MainPageView.as_view()),
path("candies/<str:name>/", CandyView.as_view()),
]For Django class-based views, we access an appropriate view function by calling the class method as_view(). This does all the work of creating an instance of the class and ensuring that the correct handler methods are called for incoming HTTP requests.
Not Found Pages
For each customer wishing to know more about a particular candy, make another page and class to process that request. And if the customer asks us about a nonexistent candy, we've got to report that we couldn't find it. The corresponding HTTP response status code is 404 Not Found, but we don't see any codes in the previous example.
Django does a lot of work under the hood. The HttpResponse sets the status code 200 OK in the answer for you, which means that the communication was OK. We can either change this code in the HttpResponse class (HttpResponse(status=404)), or use the exception class Http404, which will signal to a user that they're trying to GET a page that doesn't exist.
HTTP defines multiple response status codes to handle communication easily. Still, the general rule is that codes starting with 2 indicate success, ones that begin with 4 mean client-side errors (wrong URL in cases of 404), and ones that begin with 5 mean server-side errors.
Let's make a handler for a custom candy page in the same module:
from django.http import HttpResponse, Http404
from django.views import View
class CandyView(View):
def get(self, request, name, *args, **kwargs):
if name not in candies:
raise Http404 # or return HttpResponse(status=404)
candy_info = "".join(
f"<tr><td>{key}:</td><td>{value}</td></tr>"
for key, value in candies[name].items()
)
return HttpResponse(f"<table><tbody>{candy_info}</tbody></table>")As you see, the third parameter in our GET method is the cryptic candy_name. If we have this candy, we'll display all the information about it. But if we don't have one, we raise a Http404 exception because we can't find it in our stock.
We return an instance of the HttpResponse, but we raise Http404 since Http404 is the exception class
Conclusion
To process a request in the Django application, you should define a handler and implement all methods that it can process. You can create a view function that matches the desired HTTP address or use a class-based view. If you inherit this class from View, you should check the preferred HTTP method with the same name as the method in your class. Specifically, if you want to process a GET request, define the GET method and return an instance of the Django HttpResponse class.
You can then bind this handler to the link it belongs to. Just add it to the list of urlpatterns in the urls.py module.