1: Connecting HTML files with DTL


As of right now we have constructed our web app similarly to a regular website.  Let's improve our project by adding additional Django-specific features. We have already used the Django template language (DTL) to render a model in an HTML template, but now let's use it to save ourselves development time as we create new HTML files for different web pages.

 

Create a new HTML document called header.html

env > mysite > main > templates > main > (New File) header.html

Create a header.htmlGIF

Let's start off by re-organizing home.html into two separate files. First, we'll create a new file named header.html. Right click on the templates > main folder and click New File to create a file named header.html.

 

Edit the header.html

env > mysite > main > templates > main >  header.html

<!DOCTYPE html>
<html>
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Encore</title>
    <!--Bootstrap CSS-->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
  </head>
  <body>
    

    <!-- Optional Javascript -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

We'll use this file to separate our CDN links and meta tags from the visible HTML elements we display. Copy the contents of home.html and paste into header.html so the files are identical.  In the header.html file, delete all code inside the body tag like the example above.

 

Load in block content

env > mysite > main > templates > main > header.html

<!DOCTYPE html>
<html>
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Encore</title>
    <!--Bootstrap CSS-->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
  </head>
  <body>

    {% block content %}

    {% endblock %} 
      
    <!-- Optional Javascript -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

Next we will connect the two documents together using the Django template language (DTL). We previously used DTL for creating multiple cards from information provided from our models but it can also be applied to load in HTML code from another document. Add block tags inside the body element.  This will automatically link HTML files that we specify in the next step.

 

Configure the home.html

env > mysite > main > templates > main > home.html

{% extends 'main/header.html' %}

  {% block content %}

  {% load static %}


  <!--Navbar-->
  <nav class="navbar navbar-expand-sm navbar-light bg-light">
    <a class="navbar-brand" href="/">Encore</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarText">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item">
          <a class="nav-link" href=" ">Products</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href=" ">Blog</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href=" ">Contact</a>
        </li>
      </ul>
    </div>
  </nav>


  <!--CTA-->
  <div class="container py-5">
    <div class="row">
      <div class="col-sm-12 col-md-12 col-lg-6 pb-4">
        <h1 class="display-4 font-weight-bold">Elevate your listening</h1>
        <h5>Everyday headphones that make your favorite artists sound like their performing a never-ending encore.</h5>
        <a class="btn btn-primary mt-2" href="/products">FIND YOUR SET</a>
      </div>
      <div class="col-sm-12 col-md-12 col-lg-6">
        <img class="img-fluid rounded" src="{% static 'img/cta-headphones.jpg' %}" alt="Headphones">
      </div>
    </div>
  </div>


  <!--Products-->
  <div class="container py-3">
  	<h2>Products</h2>
  	<hr>
  	<br>
    <div class="row">
      {% for p in product %}
        <div class="col-sm-12 col-md-6 col-lg-3 pb-4">
          <div class="card h-100" style="border:none">
            <img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name}}" style="width: auto; height: 250px; object-fit: scale-down;">
            <div class="card-body">
              <h5 class="card-title">{{ p.product_name }}</h5>
              <p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
              <p class="card-text">{{ p.product_description }}</p>
              <a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>
            </div>
          </div>
        </div>
      {% endfor %}
    </div>
  </div>

  {% endblock %}

Go to home.html and delete everything outside of the body element and the optional Javascript code. In other words, only keep the code specific to the homepage. Then add the following tags to the top of the document: {% extends 'main/header.html' %}  {% block content %} and the following to the bottom {% endblock %} of the file.  Make sure home.html looks identical to the example above and save.

Reload the http://127.0.0.1:8000/ page to view the results but the page should actually look the same. We simply reorganized our documents so that we can use the same CDNs on future HTML templates.  By "extending" the header.html template at the top of the file, home.html has access to its contents. 

 

Inspect the web page

Inspect homeapgeGIF

Right click on the browser window displaying the homepage and click "Inspect". You can see the two HTML documents render as one page.







2: Creating a separate navbar template


Create a navbar.html file

env > mysite > main > templates > main > (New Folder) includes > (New File) navbar.html

Django includes folder and navbarGIF

Just like the CDNs and head element, we do not want to add a navbar in every new HTML file we create. First, create a new folder inside the templates main folder called includes. Inside the includes folder create a new HTML file called navbar.html.

 

Place the navbar in the navbar.html file

env > mysite > main > templates > main > includes > navbar.html

<!--Navbar-->
<nav class="navbar navbar-expand-sm navbar-light bg-light">
 <a class="navbar-brand" href="/">
    Encore
 </a>
 <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
   <span class="navbar-toggler-icon"></span>
 </button>
 <div class="collapse navbar-collapse" id="navbarText">
   <ul class="navbar-nav mr-auto">
     <li class="nav-item">
       <a class="nav-link" href=" ">Products</a>
     </li>
     <li class="nav-item">
       <a class="nav-link" href=" ">Blog</a>
     </li>
     <li class="nav-item">
       <a class="nav-link" href=" ">Contact</a>
     </li>
   </ul>
 </div>
</nav>

Cut and paste all of the navbar code that was in home.html into navbar.html. Be sure that the navbar from home.html has been removed and both files are saved.

 

Connect navbar.html to header.html

env > mysite > main > templates > main > header.html

<!DOCTYPE html>
<html>
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Encore</title>
    <!--Bootstrap CSS-->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
  </head>
  <body>
    
    {% include 'main/includes/navbar.html' %}

    {% block content %}

    {% endblock %} 

    <!-- Optional Javascript -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

Next, let's connect the navbar.html to header.html. We want the navbar to be accessible on all future pages so we will add it to header.html with {% include 'main/includes/navbar.html' %} instead of using the block content tags.

While we haven't changed the overall appearance of anything, we included some important web app functionality that will save us time as we add new documents.  Now the navbar will always appear at the top of our pages and new HTML files will not need a <head> </head> element.








3: Creating a products page


Now let's create a new template that displays all of the products while extending the header and navbar elements. 

 

Code-it-yourself: Create a products.html file

env > mysite > main > templates > main > (New File) products.html

Create the products.htmlGIF

Create a new file named products.html in the templates > main folder. 

 

Edit the products.html file

env > mysite > main > templates > main > products.html

{% extends "main/header.html" %}

    {% block content %}

    <!--Products-->
    <div class="container py-5">
        <h1 class="font-weight-bold">Products</h1>
        <hr>
        <br>
        <div class="row">
            
        </div>
    </div>

    {% endblock %}

Open the products.html file and add the code above. 

 

Add a products path to urls.py

env > mysite > main > urls.py

from django.urls import path
from . import views

app_name = "main"   


urlpatterns = [
    path("", views.homepage, name = "homepage"),
    path("products", views.products, name = "products"),
]

Open the main > urls.py file and add a new path for products. Save the file before we move on to the views.py.

 

Code-it-yourself: Add a products function to views.py

env > mysite > main > views.py

from django.shortcuts import render
from .models import Product

# Create your views here.
def homepage(request):
	...


def products(request):
	products = Product.objects.all()
	return render(request = request, template_name="main/products.html", context = {"products":products})

We have already imported the Product model at the top of the page so we only need to create a simple function that returns a render of products.html with a queryset of all Product objects passed as context.

 

Update the products.html file

env > mysite > main > templates > main > products.html

{% extends "main/header.html" %}

    {% block content %}
    <!--Products-->
    <div class="container py-5">
        <h1 class="font-weight-bold">Products</h1>
        <hr>
        <br>
        <div class="row">
            {% for p in products %}          
                <div class="col-sm-12 col-md-6 col-lg-4 pb-4">
                    <div class="card h-100" style="border:none">
                        <img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name }}" style="width: auto; height: 200px; object-fit: scale-down;">
                        <div class="card-body">
                            <h5 class="card-title">{{ p.product_name }}</h5>
                            <p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
                            <p class="card-text">{{ p.product_description }}</p>
                            <a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>
                        </div> 
                    </div> 
                </div>
            {% endfor %}
        </div>
    </div>
    {% endblock %}

Open the products.html file again and create a for loop within the row. Add a division element with col-sm-12 col-md-6 col-lg-4 and pb-4 so three products will display per row. Nest a division with a card class attribute in the column and then pass in the product image, name, type, description and url below. We will also add a style attribute object-fit to these product images and a border:none to the card.

 

Add the products link to the navbar.html

env > mysite > main > templates > main > includes > navbar.html

<!--Navbar-->
<nav class="navbar navbar-expand-sm navbar-light bg-light shadow">
 <a class="navbar-brand" href="/">
    Encore
 </a>
 <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
   <span class="navbar-toggler-icon"></span>
 </button>
 <div class="collapse navbar-collapse" id="navbarText">
   <ul class="navbar-nav mr-auto">
     <li class="nav-item">
       <a class="nav-link" href="/products">Products</a>
     </li>
     <li class="nav-item">
       <a class="nav-link" href=" ">Blog</a>
     </li>
     <li class="nav-item">
       <a class="nav-link" href=" ">Contact</a>
     </li>
   </ul>
 </div>
</nav>

Add the /products slug to the correct anchor element in navbar.html. Save the file and then refresh the browser window. Click on the navigation link "Products", or the CTA button, and the products.html page will appear with all of the products listed.

Products pageGIF

 

Add the products link to the home.html

env > mysite > main > templates > main > home.html

 <!--Products-->
  <div class="container py-3">
    <h2>Products</h2>
    <hr>
    <br>
    <div class="row">
      {% for p in product %}
        <div class="col-sm-12 col-md-6 col-lg-3 pb-4">
          <div class="card h-100" style="border:none">
            <img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name}}" style="width: auto; height: 250px; object-fit: scale-down;">
            <div class="card-body">
              <h5 class="card-title">{{ p.product_name }}</h5>
              <p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
              <p class="card-text">{{ p.product_description }}</p>
              <a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>
            </div>
          </div>
        </div>
      {% endfor %}
    </div>
    <div class="container text-right">
      <a href="/products">View more</a>
    </div>
  </div>

Let's also add a link to the products page below the Products section on the homepage. Add a new division element with the class attributes container and text-right after the closing tag of the row. Within the new division element, nest an anchor element linking to the products page. Save the changes to the home.html file.

View more button below Products section








4: Using Django pagination


We currently only have four products to display but what happens when we add more? Eventually the page will become lengthy and require a lot of scrolling. So let's add the built-in Django pagination that will automatically create "previous" and "next" links for additional products. 

 

Add pagination to the products views.py function

env > mysite > main > views.py

...
from django.core.paginator import Paginator #import Paginator

# Create your views here.
...

def products(request):
	products = Product.objects.all()
	paginator = Paginator(products, 18)
	page_number = request.GET.get('page')
	page_obj = paginator.get_page(page_number)
	return render(request = request, template_name="main/products.html", context = {"page_obj":page_obj})

Import the Paginator object at the top of the page then add three new variables to the products function.  Assign the Paginator object to a paginator variable.  The object will have the products queryset and the number of products per page, 18, as arguments.  Next create a page_number variable that looks at the request to get the current page number.  Finally, combine all this data into one variable, page_obj.  This is the only variable we need to pass as context.

 

Add pagination to the products.html

env > mysite > main > templates > main > products.html

{% extends "main/header.html" %}

    {% block content %}
    <!--Products-->
    <div class="container py-5">
        <h1 class="font-weight-bold">Products</h1>
        <hr>
        <br>
        <div class="row">
            {% for p in page_obj %}          
              ...
            { % endfor %}
        </div>

        <!--Pagination-->
        <div class="container">
            <div class="pagination justify-content-center">
                <span class="step-links">
                    {% if page_obj.has_previous %}
                        <a href="?page=1">&laquo; first</a>
                        <a href="?page={{ page_obj.previous_page_number }}">previous</a>
                    {% endif %}

                    <span class="current">
                        Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
                    </span>

                    {% if page_obj.has_next %}
                        <a href="?page={{ page_obj.next_page_number }}">next</a>
                        <a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
                    {% endif %}
                </span>
            </div>
        </div>
        <!--end of Pagination-->

    </div>
    {% endblock %}

Go to the products.html file and replace {% for p in products %} with {% for p in page_obj %}. Then add a new comment called Pagination and the above code after it. Save the file.

Reload the products page in the browser and it should look exactly the same but with the text Page 1 of 1 at the bottom. Because we set the number of products to 18, "next" and a "previous" links will not appear until we pass 18 products.

If you want to see the paginator in action, go back to the views.py file and set the variable paginator to equal  Paginator(products, 2). Refresh the page and there with now only be 2 products and a next/last link at the bottom of the page. Click either of the links and the pagination will take you to the next page of products. Change the paginator back to 18 when you are done.

Products page pagination






Quiz Questions


1. What is the purpose of {% extends 'main/header.html' %} in HTML files such as home.html?


2.  What two arguments does the Paginator() object take?


Next lesson


Check out the comments and debug buttons if you get stuck.