We have the admin site in place, but this is to be viewed by the admin. Now, let’s create something every user can see. In this part of the series, we’ll create the views and templates all the pages of our application. In particular, we’ll have the following pages:
– the home (or index) page – it will display information about all the projects,
– the projects_in_category page – it will display the projects that belong to a specific category,
– the projects_with_technology page – it will display the projects that implement a specific technology,
– the detail page – it will display the details of a single project.
You’ll find the code related to this part of the series on Github.
For each page to work, we’ll implement a URL mapping, a view and a template.
Table of Contents
URL Mapping
In order to navigate to the different pages, we have to add some URLs. To keep it simple, let’s define them like so:
catalog/
– the home page,
catalog/category /<category>
– the projects_in_category page, for example catalog/category/Games will navigate to the page that displays only the projects in the Games category,
catalog/technology/<technology>
– the projects_with_technology page, for example catalog/technology/Python will navigate to the page that displays only the projects built with Python,
catalog/project/<pk>
– the detail page, for example catalog/project/5 will navigate to the page that displays the details of the project with id 5. By the way, pk stands for primary key.
When we created the app, we added the following piece of code in the portfolio/urls.py file:
urlpatterns = [
path('admin/', admin.site.urls),
path('catalog/', include('catalog.urls')),
path('', RedirectView.as_view(url='catalog/')),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
What we’re interested in here is the line:
path('catalog/', include('catalog.urls'))
Here, we include the catalog.urls module.
In the /catalog/urls.py file we defined an empty urlpatterns
list:
urlpatterns = [
]
This is a placeholder for our actual paths. This list will be than included in the portfolio/urls.py file. So, let’s add the first path to the placeholder. We’ll need it for the home page:
urlpatterns = [
path('', views.ProjectsListView.as_view(), name='index')
]
We’ll add the other paths later on.
The first argument that we pass to the path function is the pattern in the URL that we have to follow, remembering that this is the part that will follow the catalog/
part, which is already included in each of the above paths. For example, with the home page, the pattern is an empty string, so we will navigate to that page with the following URL:
catalog/
With the other pages, we’ll have to add some more information, like the name of the category or technology we’re interested in (these are strings) or the project’s id (which is an integer). But we’ll see to it later.
The second parameter is the view class (yet to be defined) that will be used for the URL pattern. With class views, we also call the as_view
method to access an appropriate view function. This method is responsible for creating the instance of the class and ensuring that HTTP requests are handled properly.
The last argument is name. This is a unique identifier for the URL mapping. We use it, for example, in reversed URL mapping.
Views
Views in Django can be function-based or class-based. Function-based views are more flexible, you can use them any way you want.
A function-based view is a function that processes an HTTP request, fetches the data from the database, uses a template to render the HTML page and returns the page in the HTTP response. The page is then displayed in the browser.
Class-based views are a cleaner and faster solution if your view is of a specific type, like for example if it should display a list or some details. In our project, we’ll be using the ListView
and DetailView
generic display views defined by Django.
The home page will display a list of all the categories and under each category, a list of projects that belong to that category. It will also display the number of projects in each category. So, we’ll implement it as a class view that inherits from ListView
.
In order to render a page, we need data and a template. For example, in our home page we’ll need data from the database related to the categories and projects. We’ll also need a template to display the data in a readable way to the user.
We’ll define our views in the catalog/views.py file. At this moment all the file contains is the import of the render function that is used to render the HTML pages when we use function-based views:
from django.shortcuts import render
As we’re going to use class-based views, we don’t need it. Instead, we have to import the generic module. Here’s the view for the home page:
from django.views import generic
from .models import Project, Category, Technology
class ProjectsListView(generic.ListView):
model = Project
template_name = "catalog/project_list.html"
context_object_name = "projects"
def get_queryset(self):
"""
Optimized query to fetch projects along with their categories
using select_related (ForeignKey) and technologies using
prefetch_related (ManyToMany).
"""
return Project.objects.select_related('category').prefetch_related('technologies')
def get_context_data(self, **kwargs):
"""
Add categories, technologies, and project filtering data to the context.
"""
context = super().get_context_data(**kwargs)
# Get projects
projects = context['projects']
# Get categories and technologies
categories = Category.objects.filter(project__isnull=False).distinct()
technologies = Technology.objects.filter(project__isnull=False).distinct()
# Map categories to their projects
category_projects = {category: [] for category in categories}
for project in projects:
if project.category:
category_projects[project.category].append(project)
# Map technologies to their projects
technology_projects = {technology: [] for technology in technologies}
for project in projects:
for technology in project.technologies.all():
technology_projects[technology].append(project)
# Count projects per category
category_counts = [(category, len(projects))
for category, projects
in category_projects.items()]
# Add to context
context['categories'] = categories
context['technologies'] = technologies
context['category_projects'] = category_projects # pojects by category
context['technology_projects'] = technology_projects # projects by technology
context['category_counts'] = category_counts # project counts per category
return context
Here, we specify a couple things. First, we specify the model to be used for the view. The model is Project
, so the view will query the database to get all the projects. But we don’t need just the projects, we also need the categories and technologies that are related to them, so we use the get_queryset
method to do that.
Inside this method, we call the select_related
method to get all the related categories. This method is used with ForeignKey
relationships. It optimizes the database query by performing a SQL JOIN, so there’s no need for a separate query for each project’s category.
To get all the technologies, we use the prefetch_related
method. It also reduces the number of queries to the database by prefetching the related many-to-many fields.
Next, there’s the get_context_data
method. We use it to add the categories and technologies to the context for the template. This way, in the template, we’ll be able to not only reference the projects themselves, but also the categories and technologies, and this is something we’re definitely going to need.
So, in the template we’ll use projects
to reference the projects. We’ll use categories
and technologies
to respectively reference all the categories and technologies. One thing worth mentioning here is how we create the set of categories and technologies. Our strategy is to only gather the ones that are set for at least one project. So, if we added a category in the admin site, for example, but there is no project in this category, this category will not be taken into account and won’t be displayed in the list of categories in the home page. It works the same way for the technologies. If you prefer to solve it differently, feel free to do so.
Next, we map categories and technologies to their projects. This way we’ll know which projects belong where and where to display them.
We also create a list of project counts per category so that we can display the number of projects in each category.
Finally, we add all the pieces we need to the context. We’ll be able to use them in the template then.
Now, outside the get_context_data
method, we also set context_object_name
in the class, which is our own name for the list as a template variable.
And there’s one more thing. We specify the template that will be used for our view:
template_name = 'catalog/project_list.html'
This path may look weird at first glance. We’ll discuss it in the section on templates. Speaking of which…
Templates
Templates define the layout of the page. We’ll create a base template that will contain everything that should be displayed on every page, and then extend it for the particular pages to contain also the content that is needed only on those pages.
The base template will contain all the regular HTML markup for the head and body, the other templates will only contain the HTML tags that are needed there.
In templates we use special syntax to include particular sections. We delimit them with the block
and endblock
template tags. These blocks may be empty in the base template, just to be replaced by the other templates, or they may contain some default content that will be shared among pages. We’ll have a closer look at the syntax used in templates as we proceed.
For styling, we’ll be using Bootstrap in our app, but also our own styles. Just like with any regular HTML file, styles are linked in the head section.
We’ll save all the templates in a templates folder inside the catalog folder. This is how we commonly do it, so make sure to add this folder there. Besides, we’ll create a static folder for our static elements like images and styles. Inside the images folder we’ll add some images (available on Github) that will be used as logos in the base template, and for the styles we’ll create a css folder and in it, we’ll add the styles.css file that we are going to use to style some elements in our app. The file hierarchy should now look like this:

Base Template
As you can see, inside the templates folder, we added the base template. We named it base_template.html. Here’s the code that goes in there:
<!doctype html>
<html lang="en">
<head>
{% block title %}
<title>Portfolio</title>
{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- custom styles -->
{% load static %}
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
</head>
<body>
<div class="d-flex">
<button class="menu-button" onclick="toggleSidebar()">☰ Menu</button>
<div class="sidebar">
<a class="sidebar-top" href="#">My Portfolio</a>
<h5 class="list-title">Categories</h5>
{% if categories is None %}
<p><em>Loading...</em></p>
{% else %}
<div class="scrollable-list">
<ul class="list-unstyled">
{% for category in categories %}
<a href="#">
<div class="list-item">
<i class="{{ category.icon }}"></i>
<span class="list-name">{{ category.name }}</span>
</div>
</a>
{% endfor %}
</ul>
</div>
{% endif %}
<h5 class="list-title">Technologies</h5>
{% if technologies is None %}
<p><em>Loading...</em></p>
{% else %}
<div class="scrollable-list">
<ul class="list-unstyled">
{% for technology in technologies %}
<a href="#">
<div class="list-item">
<i class="{{ technology.icon }}"></i>
<span class="list-name">{{ technology.name }}</span>
</div>
</a>
{% endfor %}
</ul>
</div>
{% endif %}
<h5 class="social list-title">My Blogs</h5>
<div class="sidebar-social px-3 d-flex">
<a href="https://prosperocoder.com/" target="_blank">
<img class="img-fluid" src="{% static 'images/Logo Coder.png' %}">
</a>
<a href="https://prosperoenglish.com/" target="_blank">
<img class="img-fluid" src="{% static 'images/Logo English.png' %}">
</a>
</div>
<h5 class="social list-title">My YouTube Channels</h5>
<div class="sidebar-social px-3 d-flex">
<a href="https://www.youtube.com/c/ProsperoCoder/" target="_blank">
<img class="img-fluid" src="{% static 'images/Prospero Coder YT Banner.png' %}">
</a>
<a href="https://www.youtube.com/c/ProsperoBlender" target="_blank">
<img class="img-fluid" src="{% static 'images/Prospero Blender YT Banner.png' %}">
</a>
<a href="https://www.youtube.com/c/ProsperoEnglish" target="_blank">
<img class="img-fluid" src="{% static 'images/Prospero English YT Banner.png' %}">
</a>
</div>
<h5 class="social list-title"><i class="contact bi bi-envelope-at"></i>Contact</h5>
<div class="sidebar-social px-3 d-flex">
<p>prosperocoder@gmail.com</p>
</div>
</div>
<main>
<div class="header">
<div class="personal-info">
<div class="personal-info-main">
<div class="personal-info-text">
<h2>Kamil Pakula - About me</h2>
<p>I'm looking for a job as a Python developer. I have a couple years of experience with this awesome programming language, mostly working on my own projects, keeping my Prospero Coder blog and publishing ebooks. My main areas of interest are Data Science, Machine Learning and everything related to AI. For web development I use Django and Flask, for desktop and mobile development I use Kivy. I love learning new technologies and libraries and trying them out in my personal projects. I love team work, but I can also work individually.</p>
<p>Here are some of my projects...</p>
</div>
<div class="personal-info-image">
<img class='face-image' src="{% static 'images/ID Photo.png' %}" alt="face photo">
</div>
</div>
<div class="personal-info-links">
<a href="https://github.com/prospero-apps" target="blank">
<i class="devicon-github-original github-icon"></i> Github
</a>
</div>
</div>
</div>
<div class="content px-4">
{% block content %}{% endblock %}
</div>
</main>
</div>
<script>
function toggleSidebar() {
document.querySelector(".sidebar").classList.toggle("show");
}
</Script>
</body>
</html>
This is the entire file. Let’s discuss it one piece at a time.
The head Section
At the very top we have the head section of the document, just like in any typical HTML file:
<head>
{% block title %}
<title>Portfolio</title>
{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- custom styles -->
{% load static %}
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
</head>
Here, we link Bootstrap and some icon libraries (devicons, bootstrap-icons, font-awesome). The names of the particular icons that we set in the admin site, are actually the names used in these libraries. We also link our custom styles.css file that we put in the css folder inside the static folder. If we want to use stuff from the static folder, we have to add this line of code to load it:
{% load static %}
There’s one more thing. As mentioned before, we use blocks to separate parts of the code. Here, we have the block to set the title of the page:
{% block title %}
<title>Portfolio</title>
{% endblock %}
The title is what you see on the tab in the browser. If you want a different title later for other pages, you can easily overwrite it.
The body Section
Next, we have the body section. Here’s the general structure of the body:
<body>
<div class="d-flex">
<button class="menu-button" onclick="toggleSidebar()">☰ Menu</button>
<div class="sidebar">
...
</div>
<main>
...
</main>
</div>
<script>
...
</Script>
</body>
These are the parts we’re going to see. After we define the styles that we use in the HTML file, launch the app and navigate to the home page, we’ll see the following:

We don’t see the menu button, because it will be only visible on mobile devices (or smaller screens in general). But we can see the sidebar on the left and the main (bigger) part on the right.
The main Part
Let’s have a look at the main part first. Its structure looks like this:
<main>
<div class="header">
...
</div>
<div class="content px-4">
{% block content %}{% endblock %}
</div>
</main>
It consists of two parts, the header and the content section.
The header contains some text, a link with an icon and an image. Besides, it’s animated, which you should see in the browser. Let’s see how the icon and image were added.
For the icon, we use the <i>
tag and set the class of the tag to the name of the icon:
<i class="devicon-github-original github-icon"></i> Github
It renders as a link because it’s inside an <a>
tag:

The image is stored in the static folder, so we must reference it like so:
<img class='face-image' src="{% static 'images/ID Photo.png' %}" alt="face photo">
Generally, in templates, we use the curly braces with percent signs for template tags. We’re going to see many more examples like this in a moment.
There’s also the content section below. Here we just insert an empty content block:
{% block content %}{% endblock %}
This is why we don’t see anything in the page below the header. The other pages will then extend the base template and overwrite the content block. Whatever we define in their content blocks, will replace the base template’s content block.
The sidebar Part
The sidebar on the left seems more interesting than the half-empty main section. Let’s have a look at its structure:
<div class="sidebar">
<a class="sidebar-top" href="/">My Portfolio</a>
<h5 class="list-title">Categories</h5>
...
<h5 class="list-title">Technologies</h5>
...
<h5 class="social list-title">My Blogs</h5>
...
<h5 class="social list-title">My YouTube Channels</h5>
...
<h5 class="social list-title"><i class="contact bi bi-envelope-at"></i>Contact</h5>
...
</div>
At the top we have the My Portfolio logo that, when clicked, will take us to the home page:

Below are the scrollable lists of categories and technologies:

These lists will be used to filter the projects.
At the bottom, we have some links and contact information:

If you click on the links, you’ll navigate to my blogs and YouTube channels.
Now, let’s have a closer look at the particular parts. This will also give us an opportunity to examine some template syntax in more detail.
Template Syntax
Sometimes we need to access the value of a variable in the template. To this end, we put the variable inside double curly braces. For example, we access the icon
and name
fields on the category
object like this:
<i class="{{ category.icon }}"></i>
<span class="list-name">{{ category.name }}</span>
For conditionals and for loops, we use special template tags. They are put inside {% %}
and each opening tag has its corresponding closing tag with the same name, but preceded by ‘end
’. Let’s have a look at this for
loop in the technologies section:
{% for technology in technologies %}
<a href="#">
<div class="list-item">
<i class="{{ technology.icon }}"></i>
<span class="list-name">{{ technology.name }}</span>
</div>
</a>
{% endfor %}
Inside the template tags, we use the variables directly, just like above. In the view file, we added technologies
to the context. This is why it’s available in the template. The for
loop is enclosed between the opening and closing tags. The closing tag is named endfor
, following the rule I explained above.
Inside a for
loop, we can do anything we like. Here, we’re displaying each technology’s icon and name. A similar loop is implemented for the categories.
We can also use conditionals in templates. Have a look:
{% if categories is None %}
<p><em>Loading...</em></p>
{% else %}
<div class="scrollable-list">
<ul class="list-unstyled">
{% for category in categories %}
<a href="#">
<div class="list-item">
<i class="{{ category.icon }}"></i>
<span class="list-name">{{ category.name }}</span>
</div>
</a>
{% endfor %}
</ul>
</div>
{% endif %}
We use the if
, elif
(not used here), else
and endif
template tags with conditionals. Which elements are actually displayed is based on these conditions. If categories
is not null, the icons and names of each category are displayed.
We know what the base template looks like. The particular elements are styled using Bootstrap and our own styles.
Custom Styles
We defined our custom styles in the styles.css file. You’ll find the file on Github. We’re not going to discuss them in detail, because it’s not a CSS-related article, but we already saw what effect they produce.
One thing worth mentioning is that I also added some @media
rules to the CSS file so that user experience on mobile devices, or devices with smaller screens in general, is good. Let’s compare the base template on a regular laptop screen, on a smaller screen and on a narrow screen, like a cell phone’s:
REGULAR LAPTOP SCREEN:

SMALLER SCREEN

MOBILE SCREEN

As you can see, the font size in the header is smaller on smaller screens for the entire text to fit in.
Also, on smaller screens, we don’t see the sidebar at all. Instead, we can see the menu button that was invisible before:
<button class="menu-button" onclick="toggleSidebar()">☰ Menu</button>
In mobile, the button becomes visible and the sidebar becomes invisible. The sidebar becomes visible if you toggle the menu button, though:

It now takes up the entire width of the screen and the lower part has a slightly simplified layout to prevent cluttering. If you want to see the header or the content section, you have to scroll or toggle the menu button off. There’s also a simple JavaScript script in the template file near the ending </body>
tag that handles this toggling on and off.
We are now ready to implement the home page and the other pages.
Home Page
Now that we have our base template, we can create the other templates that will be, so to speak, injected into the content block of the base template.
If you recall, we specified the template for our home page in the ProjectsListView
class like so:
template_name = 'catalog/project_list.html'
Weird as this path may seem, it’s actually where generic views look for their templates. To be more precise, inside the catalog/templates folder there must be a catalog/project_list.html file. Here catalog is the name of the application and should be replaced by the actual application name in other projects. Anyway, the full path to the template is:
catalog/templates/catalog/project_list.html
This means, we have to create this hierarchy in our project:

Each of our pages will extend the base template, which means it will inherit everything from it and only overwrite the content section. Let’s deliver a very basic implementation for the home page to demonstrate it:
{% extends "base_template.html" %}
{% block content %}
<h1>Home Page</h1>
{% endblock %}
In the first line of code we extend the base template, so the home page will inherit everything from it. Below, we redefine the content block. Whatever comes here, will overwrite the content block inherited from the base template. Now we can see the new content:

We can implement the home page any way we want. And what do we want?
Well, as stated before, we want all the categories along with their related projects and project counts to be displayed.
The good news is we can still use all the variables defined in the view like projects
, categories
, category_projects
, and so on, because they are inherited from the base view.
We could just list the projects in the home page, but this would be rather boring. Instead, let’s create a card for each project. The card should contain the project’s title, the image associated with the project, the icons of the technologies used in the project, the description of the project, or, if it’s too long, just the initial part of it, and the icons for the links. If we click on a project card, we’ll navigate to the project’s detail page.
But how to do it? Actually, we could do something very similar to components in React or other JavaScript frameworks. We can create a separate template to serve as the component and then use it inside the home page. This is the approach we’re going to take, but first, let’s implement the remaining part of the home page.
Here’s the code for the home page (the project_list.html template):
{% extends "base_template.html" %}
{% block content %}
<h1 class="my-projects-text">My Projects</h1>
{% if projects is None %}
<p><em>Loading...</em></p>
{% else %}
{% for category, projects in category_projects.items %}
<h3 class="category-name">
{{ category.name }}
{% for cat, count in category_counts %}
{% if cat == category %}
({{ count }})
{% endif %}
{% endfor %}
</h3>
<ul>
<div class="container-fluid">
<div class="row">
{% for project in projects %}
<div class="col-md-6 col-lg-4 mb-2">
{% include "catalog/project_card.html" with project=project %}
</div>
{% empty %}
<li>No projects in this category.</li>
{% endfor %}
</div>
</div>
</ul>
{% endfor %}
{% endif %}
{% endblock %}
As you can see, most of the code sits inside the content block, so it will overwrite the empty content block of the base template.
Provided there are some projects, they are displayed category after category. This is what this loop is for:
{% for category, projects in category_projects.items %}
So, the name of the category is displayed, followed by the number of projects in that category. Now, look how the projects are displayed:
<ul>
<div class="container-fluid">
<div class="row">
{% for project in projects %}
<div class="col-md-6 col-lg-4 mb-2">
{% include "catalog/project_card.html" with project=project %}
</div>
{% empty %}
<li>No projects in this category.</li>
{% endfor %}
</div>
</div>
</ul>
So, all projects within a category are inside an unordered list (the <ul>
tag). We can see a new tag here, empty, which is used in case there are no projects in a given category, which, in our app should never happen because we chose not to display categories with no projects at all. But if you chose differently, this option might come in handy.
And here comes the interesting part:
{% include "catalog/project_card.html" with project=project %}
We use the include
tag to include our project template, so the component, so to speak, that we are yet to create. Here we assume that the template will be named project_card.html and will be stored in the catalog folder. We also assume the template will be passed data from the current project
in the for
loop so that we can display it there.
So, all we need is the project card. Let’s create it next.
Project Card
Let’s add a new project_card.html file to templates/catalog and implement it like so:
<div class="card">
<a href="#">
<img src="{{ project.image.url }}" alt="{{ project.name }}">
<div class="card-body">
<h5 class="card-title mb-3">{{ project.name }}</h5>
<div class="tech-icons">
{% for technology in project.technologies.all %}
<div class="single-icon">
<i class="{{ technology.icon }}"></i>
</div>
{% endfor %}
</div>
{% if project.description|length > 110 %}
<p>{{ project.description|slice:":110" }}...</p>
{% else %}
<p>{{ project.description }}</p>
{% endif %}
<div class="link-icons">
{% for link in project.links.all %}
<div class="single-icon">
<i class="{{ link.icon }}"></i>
</div>
{% endfor %}
</div>
<p class="more-info">Click to view more...</p>
</div>
</a>
</div>
There are a couple things here we have to take care of. If we want to be able to display the project images, we have to use the following syntax:
<img src="{{ project.image.url }}" alt="{{ project.name }}">
The field we defined in the Project
model is called image
, but it’s used to store the image itself. What we need here is the image’s URL.
We can iterate over the technologies of a single project like this:
<div class="tech-icons">
{% for technology in project.technologies.all %}
<div class="single-icon">
<i class="{{ technology.icon }}"></i>
</div>
{% endfor %}
</div>
What’s more, we can even iterate over the links associated with a project due to their many-to-many relationship:
<div class="link-icons">
{% for link in project.links.all %}
<div class="single-icon">
<i class="{{ link.icon }}"></i>
</div>
{% endfor %}
</div>
If the description of the project is short, let’s say up to 110 characters, we can display it in its entirety, otherwise, we’ll just display the first 110 characters followed by an ellipsis:
{% if project.description|length > 110 %}
<p>{{ project.description|slice:":110" }}...</p>
{% else %}
<p>{{ project.description }}</p>
{% endif %}
We’re using the project
variable here in this template. This is the variable that was fed with data from the containing template. This means, each project card will display the data of its underlying project.
Before you run your app to test it, check out some changes in the styles.css file. I put the header and sidebar in fixed position and added some margin to the content block so that everything is displayed correctly. Here’s the effect:

Naturally, you have to scroll down to see more projects.
On mobile devices it looks like so:

The project cards are displayed in a single column and you have to scroll down to see more of them.
At this moment, we can see all the projects. But the categories and technologies in the sidebar are there to serve a purpose. Let’s use them to filter the projects.
Projects in Category
As mentioned before, we’re going to create separate templates for projects belonging to a specific category and for projects implementing a specific technology. Let’s start with the former.
It all starts in the URLs, one could say. Open the catalog/urls.py file and add the required URL:
urlpatterns = [
path('', views.ProjectsListView.as_view(), name='index'),
path('category/<str:category_name>', views.CategoryListView.as_view(), name='category_view'
]
Here we can see the path we described before, and a new view that we have to implement in the views.py file. Let’s do it then:
from django.views import generic
from .models import Project, Category, Technology
from django.shortcuts import get_object_or_404
class ProjectsListView(generic.ListView):
...
class CategoryListView(generic.ListView):
model = Project
template_name = "catalog/project_in_category_list.html"
context_object_name = "projects_in_category"
def get_queryset(self):
"""
Optimized query to fetch projects belonging to a category along with
their technologies using prefetch_related (ManyToMany).
"""
category_name = self.kwargs.get('category_name')
category = get_object_or_404(Category, name=category_name)
return Project.objects.filter(category=category).prefetch_related('technologies')
def get_context_data(self, **kwargs):
"""
Add selected category, categories and technologies to the context.
"""
context = super().get_context_data(**kwargs)
categories = Category.objects.filter(project__isnull=False).distinct()
technologies = Technology.objects.filter(project__isnull=False).distinct()
# Add to context
context['categories'] = categories
context['technologies'] = technologies
context['selected_category'] = self.kwargs.get('category_name', None)
return context
We’re using the same model here. We also specify the template to be project_in_category_list.html. In the get_queryset
method, we read the name of the category from the arguments:
category_name = self.kwargs.get('category_name')
and use it to access the actual category:
category = get_object_or_404(Category, name=category_name)
We use the category to filter the projects:
Project.objects.filter(category=category)
In the get_context_data
method, we add the selected category to the context, as well as all the categories and technologies.
Next, we have to create the template. Add the project_in_category_list.html file to templates/catalog and implement it like so:
{% extends "base_template.html" %}
{% block content %}
<h1 class="my-projects-text">Category: {{ selected_category }}</h1>
{% if projects_in_category is None %}
<p><em>Loading...</em></p>
{% else %}
<div class="container-fluid">
<div class="row">
{% for project in projects_in_category %}
<div class="col-md-6 col-lg-4 mb-2">
{% include "catalog/project_card.html" with project=project %}
</div>
{% empty %}
<li>No projects in this category.</li>
{% endfor %}
</div>
</div>
{% endif %}
{% endblock %}
It’s pretty much the same as the home page, but this time, only projects that belong to a specific category will be displayed. For this to work, though, we must set the URL in the base template:
<h5 class="list-title">Categories</h5>
...
{% for category in categories %}
<a href="{% url 'category_view' category.name %}">
...
</a>
...
If you now run the app and select a category in the sidebar, like Games, you will only see the projects in this category:

Next, let’s do the same with the technologies.
Projects with Technology
So, open the catalog/urls.py file and add the technology URL:
urlpatterns = [
path('', views.ProjectsListView.as_view(), name='index'),
path('category/<str:category_name>',
views.CategoryListView.as_view(),
name='category_view'),
path('technology/<str:technology_name>',
views.TechnologyListView.as_view(),
name='technology_view')
]
Let’s add the view:
from django.views import generic
from .models import Project, Category, Technology
from django.shortcuts import get_object_or_404
class ProjectsListView(generic.ListView):
...
class CategoryListView(generic.ListView):
...
class TechnologyListView(generic.ListView):
model = Project
template_name = "catalog/project_with_technology_list.html"
context_object_name = "projects_with_technology"
def get_queryset(self):
"""
Optimized query to fetch projects implementing a technology along with
all their technologies using prefetch_related (ManyToMany).
"""
technology_name = self.kwargs.get('technology_name')
technology = get_object_or_404(Technology, name=technology_name)
return Project.objects.filter(technologies__in=[technology]).prefetch_related('technologies')
def get_context_data(self, **kwargs):
"""
Add selected technology, categories and technologies to the context.
"""
context = super().get_context_data(**kwargs)
categories = Category.objects.filter(project__isnull=False).distinct()
technologies = Technology.objects.filter(project__isnull=False).distinct()
# Add to context
context['categories'] = categories
context['technologies'] = technologies
context['selected_technology'] = self.kwargs.get('technology_name', None)
return context
The only thing that is different here as compared with the CategoryListView
class is how the projects are filtered. We’re not comparing to a single value this time, but rather checking if the technology is in a list of technologies:
Project.objects.filter(technologies__in=[technology])
With that in place, let’s create the template. Add the project_with_technology_list.html file to templates/catalog and implement it like so:
{% extends "base_template.html" %}
{% block content %}
<h1 class="my-projects-text">Technology: {{ selected_technology }}</h1>
{% if projects_with_technology is None %}
<p><em>Loading...</em></p>
{% else %}
<div class="container-fluid">
<div class="row">
{% for project in projects_with_technology %}
<div class="col-md-6 col-lg-4 mb-2">
{% include "catalog/project_card.html" with project=project %}
</div>
{% empty %}
<li>No projects with this technology.</li>
{% endfor %}
</div>
</div>
{% endif %}
{% endblock %}
Naturally, we also have to set the URL in the base template:
<h5 class="list-title">Technologies</h5>
...
{% for technology in technologies %}
<a href="{% url 'technology_view' technology.name %}">
...
</a>
...
If you now select a technology in the sidebar, like Python, you will only see the projects that implement this technology:

Styling Selected Items
If we select a category or technology in the sidebar, the projects are filtered correctly, and we navigate to the appropriate page. But we don’t see in the sidebar which category or technology is currently being selected, which would be a nice feature to have.
To keep things simple, let’s just display the selected item in bold. Let’s add the following style to the styles.css file:
.selected {
font-weight: bold;
}
Now, we can add conditional styling in the base template, using the selected_category
and selected_technology
variables from the context:
<a href="{% url 'category_view' category.name %}"
class="{% if selected_category == category.name %}selected{% endif %}">
<a href="{% url 'technology_view' technology.name %}"
class="{% if selected_technology == technology.name %}selected{% endif %}">
When you now select a category or technology, it will stand out in the list:

Finally, let’s add the project detail view so that we can read more about a particular project.
Project Detail Page
When we click on a project card, we should navigate to the corresponding project’s detail page. On the detail page, we should see the image of the project, its full description, the technologies used in it, and the links to other websites related to the project.
Let’s add a URL mapping first:
from django.urls import path
from . import views
urlpatterns = [
path('', views.ProjectsListView.as_view(), name='index'),
path('category/<str:category_name>',
...
path('technology/<str:technology_name>',
...
path('project/<int:pk>',
views.ProjectDetailView.as_view(),
name='project-detail')
]
We have to create the ProjectDetailView
in the views.py file. This time, we’ll inherit from another generic class, DetailView
:
...
class ProjectsListView(generic.ListView):
...
class CategoryListView(generic.ListView):
...
class TechnologyListView(generic.ListView):
...
class ProjectDetailView(generic.DetailView):
model = Project
def get_context_data(self, **kwargs):
"""
Add categories and technologies to the context.
"""
context = super().get_context_data(**kwargs)
categories = Category.objects.filter(project__isnull=False).distinct()
technologies = Technology.objects.filter(project__isnull=False).distinct()
# Add to context
context['categories'] = categories
context['technologies'] = technologies
return context
If we don’t specify the template name, by default it will be project_detail.html. As we didn’t define the context_object_name
, the default of project
will be used, after the model.
So, add the template to the same folder as the other templates and implement it like so:
{% extends "base_template.html" %}
{% block content %}
<h1 class="mb-5">{{ project.name }}</h1>
<div class="row">
<div class="col-lg-6 mb-4">
<img class="img-fluid" src="{{ project.image.url }}" alt="project image">
</div>
<div class="col-lg-6">
<p>{{ project.description }}</p>
<h5 class="mb-4">Technologies used in this project:</h5>
<div class="techs">
{% for technology in project.technologies.all %}
<div class="tech">
<i class="{{ technology.icon }}"></i>
{{ technology.name }}
</div>
{% endfor %}
</div>
<h5 class="mb-4">Useful links related to this project:</h5>
<div class="links">
{% for link in project.links.all %}
<div class="link">
<a href="{{ link.address }}" target="_blank">
<span><i class="{{ link.icon }}"></i>{{ link.name }}</span>
</a>
</div>
{% endfor %}
</div>
<a href="{{ request.META.HTTP_REFERER|default:'/' }}">Go Back</a>
</div>
</div>
{% endblock %}
The only thing that is new here is the HTTP referer:
<a href="{{ request.META.HTTP_REFERER|default:'/' }}">Go Back</a>
We use it to create a link to the previous page.
Make sure to check out the styles.css file on Github. There are some new styles added for the detail page. The last thing we have to do is add the link in the project card template:
<div class="card">
<a href="{{ project.get_absolute_url }}">
...
If we now click on a project, we’ll navigate to its detail page:

If you click one of the links on the right, a new tab will open and you’ll navigate to the appropriate address.
This is it. Our little application is finished. Let’s deploy it in the next and final part of the series.