1: Adding tags to articles


Now that we have a blog with articles let's start adding some headlines to the homepage.  We'll also create a tag system so we can tag articles with labels such as "featured", "wired", and "wireless".

 

Add articles to the home.html

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

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

 	{% block content %}

  	{% load static %}

	<!--CTA-->
	...

	<!--Headlines-->
	<div class="container pt-5">
	    <div class="row">
	     	<div class="col-lg-3 col-md-3 col-sm-12 pb-4">
	        	<h5 class="text-primary">New + updated</h5>
	        	<hr>  
	      	</div>
	      	<div class="col-lg-7 col-md-7 col-sm-12 pb-4">
	      	</div>
	      	<div class="col-lg-2 col-md-2 col-sm-12 pb-4">
	        	<h5 class="text-primary">Featured</h5>
	        	<hr>
	      	</div>
	    </div>
	</div>

	<!--Products-->
	...

    {% endblock %}

Create a new section with the comment Headlines between the CTA and the Products sections. Start with a container with a row and three columns nested within it. Pay attention to the column layout. On small devices, each column will take up the entire grid. 

As you may have noticed, we added "new" and "featured" sections to the homepage but we currently have any no articles tagged as featured. In order to tag articles, we will create a new model named Tag.

 

Create a tags model in models.py

env > mysite > main > models.py

...

# Create your models here.
...

class Tag(models.Model):
	tag_name = models.CharField(max_length=15)
	tag_slug = models.SlugField()
    
	def __str__(self):
		return self.tag_name

class Article(models.Model):
	article_title = models.CharField (max_length=200)
	article_published = models.DateTimeField('date published')
	article_image = models.ImageField(upload_to='images/')
	article_tags = models.ManyToManyField(Tag)   #add this
	article_content = HTMLField()
	article_slug = models.SlugField()

Create a new model named Tag and place it before Article. We are going to use tags to label articles and to create categories in our navbar. 

To connect tags to an article add a new field to the Article model: article_tags = models.ManyToManyField(Tag). This field will create a drop-down menu on the "Article" pages in the admin so we can assign tags to each article. Save the file before make making migrations.

 

Migrate the new model to the database

macOS Terminal

(env)User-Macbook:mysite user$ python3 manage.py makemigrations
...

(env)User-Macbook:mysite user$ python3 manage.py migrate
...

Windows Command Prompt

(env) C:\Users\Owner\Desktop\Code\env\mysite>py manage.py makemigrations
...

(env) C:\Users\Owner\Desktop\Code\env\mysite>py manage.py migrate
...

Run the commands for makemigrations and migrate to add Tag to the database.

 

Add Tag to admin.py

env > mysite > main > admin.py

from django.contrib import admin
from .models import Product, Article, Tag

# Register your models here.

...
admin.site.register(Tag)

Import Tag after Article at the top of the file and register Tag to the Django admin.

 

Add tags to the Django Admin

Django admin > Main > Tags > Add tag > Featured

Tag name: Featured

Tag slug: featured

 

Save and add another > Wireless

Tag name: Wireless

Tag slug: wireless

 

Save and add another > Headsets

Tag name: Gaming

Tag slug: gaming

 

Save and add another > Headphones

Tag name: Wired

Tag slugwired

Make sure your server is running and add the four tags above in the admin page, http://127.0.0.1:8000/admin/.

 

Add tags to the Blog Articles

Django admin > Main > Articles > 10 Best Wireless Work Headphones

Article tags: Featured, Wireless

 

Django admin > Main > Articles > How Beats by Dre changed the Headphones Game

Article tags: Featured, Wireless, Wired

 

Django admin > Main > Articles > 5 Professional Gamers' Headsets

Article tags: Featured, Gaming

 

Django admin > Main > Articles > Wire vs. Wireless Headphones

Article tags: Wireless, Wired

 

Django admin > Main > Articles > 8 Underrated Headphone Brands

Article tags: Wireless, Wired

Stay in the Django admin and go to the "Articles". Update each one with the tags listed above.  Hold-in CTRL to select multiple tags at once.

 

Pass the articles as context in homepage function

env > mysite > main > views.py

...

# Create your views here.

def homepage(request):	
	product = Product.objects.all()[:4]
	new_posts = Article.objects.all().order_by('-article_published')[:4]
	featured = Article.objects.filter(article_tags__tag_name='Featured')[:3]
	most_recent = new_posts.first()
	return render(request=request, template_name="main/home.html", context={'product':product, 'most_recent':most_recent, "new_posts":new_posts, "featured":featured})

Now let's add the tags to the homepage function. Create a new queryset named new_posts that equals the 4 most recent articles according to date published. Create another queryset called featured that equals all of the articles with the "featured" tag. We will also add [:3] so only the last three featured articles will appear. Name the last queryset as most_recent and get the first article from new_posts. Make sure to pass in the three new variables we created as context before saving the file. 

 

Add the new variables to the homepage

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

<!--Headlines-->
<div class="container pt-5">
  <div class="row">
    <div class="col-lg-3 col-md-3 col-sm-12 pb-4">
      <h5 class="text-primary">New + updated</h5>
      <hr>
      {% for n in new_posts %}
        <a class="text-dark" href="{{ n.article_slug }}">
          <h3>{{ n.article_title }}</h3>
          <p class="text-muted" style="font-size:12px">{{ n.article_published }}</p>  
        </a> 
        <hr>   
      {% endfor %}
    </div>
    ...

In the first column, add a for loop that iterates over new_posts.

 

    ...
    <div class="col-lg-7 col-md-7 col-sm-12 pb-4">
      <a class="text-dark" href="/{{ most_recent.article_slug }}" style="text-decoration: none">
        <img src="{{ most_recent.article_image.url }}" class="card-img-top" alt="{{most_recent.article_name }}">            
        <div class="card-body">
          <h3 class="card-title">{{ most_recent.article_title }}</h3>
          <p class="card-text text-muted" style="font-size:12px">{{ most_recent.article_published }}</p>
          <p class="card-text">{{ most_recent.article_content|safe|truncatewords:50}}</p>
          <button class="btn btn-primary btn-sm">Read more</button>
        </div> 
      </a>
    </div>
    ...

In the second column, add the most_recent article.

 

Code-it-yourself: Add featured articles to the homepage

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

    ...
    <div class="col-lg-2 col-md-2 col-sm-12 pb-4">
      <h5 class="text-primary">Featured</h5>
      <hr>
      {% for f in featured %}
        <a class="text-dark" href="/{{ f.article_slug }}">
          <img src="{{ f.article_image.url }}" class="card-img-top" alt="{{f.article_name }}">  
          <h6>{{ f.article_title }}</h6>
          <p class="text-muted" style="font-size:12px">{{ f.article_published }}</p>  
        </a> 
        <hr>   
      {% endfor %}
    </div>
  </div>
</div>

Finally in the third column, add a for loop that iterates over the featured queryset. Add an anchor element containing an href attribute with the article's slug.  Within the anchor element, nest an image element with the article's image, an h6 element with the article title, and a paragraph element with the date published.  Remember to save the home.html file.  

 

View the Headline section in the browser

Headlines section on the homepage

Open the homepage in the browser. There is now a section with headlines below the CTA.








2: Creating separate tag pages


Now that we have the tags on the articles we want users to be able to easily view all of the articles under a tag on it's own page. The pages will look and function like the blog.html file, so instead of creating four separate HTML templates for each tag we can use blog.html along with its url path and view function.

 

Update the blog path in the urls.py

env > mysite > main > urls.py

from django.urls import path
from . import views
from django.contrib.staticfiles.storage import staticfiles_storage
from django.views.generic.base import RedirectView

app_name = "main"   

urlpatterns = [
    path("", views.homepage, name = "homepage"),
    ...
    path("blog/<tag_page>", views.blog, name ="blog"),
]

Update the blog path by adding a /<tag_page> slug, similar to the <article_page> path.

 

Update the blog link in navbar.html

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

...
<li class="nav-item">
       <a class="nav-link" href="/blog/articles">Blog</a>
</li>
...

Locate the anchor element containing the blog link.  Since we changed the url path to include a variable after blog:  "blog/<tag_page>", we need to edit our blog link to include articles.  Although articles is a placeholder and not a tag, we will use it so the blog function in views.py knows when to show all articles and when to show articles with a specific tag.  When this link is clicked, all articles will be shown once we update views.py

 

Update the blog function in the views.py

env > mysite > main > views.py

...
from .models import Product, Article, Tag
...

# Create your views here.
...

def blog(request, tag_page):
	if tag_page == 'articles':
		tag =''
		blog = Article.objects.all().order_by('-article_published')
	else:
		tag = Tag.objects.get(tag_slug=tag_page)
		blog = Article.objects.filter(article_tags=tag).order_by('-article_published')
	paginator = Paginator(blog, 25)
	page_number = request.GET.get('page')
	blog_obj = paginator.get_page(page_number)
	return render(request=request, template_name="main/blog.html", context={"blog":blog_obj, "tag":tag})

...

Import Tag at the top of the page. We will start by adding the tag_page parameter to the blog function. Then we check if tag_page equals articles, meaning the user clicked on the blog link in the navbar and wants to view all articles. Next, we will add an else statement that gets the matching tag object and filters blog articles by that tag and date published.  The last thing to finish out this function is to return the context tag. Save the file.

 

Update the blog.html with tags

env > mysite > main > templates > main > blog.html

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

    {% block content %}

    <!--Blog-->
    <div class="container py-5">
        {% if tag %}
            <h1 class="font-weight-bold">{{ tag }}</h1>
        {% else %}
            <h1 class="font-weight-bold">Articles</h1>
        {% endif %}
        <hr>
        <br>
        <div class="row">
            {% for b in blog %}
                <a href="/{{ b.article_slug }}" style="text-decoration: none">
                    <div class="col-12 pb-4 text-dark">
                        <div class="row">
                            <div class="col-lg-4 col-md-6 col-sm-12 my-auto">
                                <img src="{{ b.article_image.url }}" class="img-fluid rounded" alt="{{ b.article_name }}">
                            </div>
                            <div class="col-lg-8 col-md-6 col-sm-12 my-auto">
                                <p>
                                    {% for tag in b.article_tags.all %}
                                        <span class="badge badge-primary" style ="font-size:14px; color: white">{{ tag }}</span>
                                    {% endfor %}
                                </p>
                                <h5>{{ b.article_title }}</h5>
                                <p class="card-text text-muted" style="font-size:12px">{{ b.article_published }}</p>
                                <p>{{ b.article_content|safe|truncatewords:25 }}</p>
                                <button class="btn btn-outline-dark btn-sm">View post</button>
                            </div>   
                        </div>
                    </div>
                </a>
            {% endfor %}
        </div>
        ...

    </div>

    {% endblock %}

Open the blog.html file and start by adding an if statement to the heading one element. If a tag exists than the template will display the tag, otherwise the tag is empty and "Articles" will display as the page heading. Next, scroll down to the second column and add a new paragraph element with a for loop for the tags. This for loop lists all of the tags selected for article_tags in the Article model. The last thing is to nest a span element, a generic container with no styling, with the class attribute value of a Bootstrap badge to display the tag names. Save the file. 

Blog page with tags

 

Update the article.html with tags

env > mysite > main > templates > main > article.html

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

    {% block content %}

    <!--Article-->
    <div class="article-banner text-center">
        <br><br>
        <div class="container">
            <h1 class="article-heading font-weight-bold">{{ article.article_title }}</h1>
            <p class="font-weight-bold" style="font-size:15px">{{article.article_published }}</p>
            {% for tag in article.article_tags.all %}
                <span class="badge badge-primary" style="font-size:14px; color:white">{{ tag }}</span>
            {% endfor %}
        </div>
    </div>
    <div class="container">
        <br>
        <p style="font-size:35px">{{ article.article_content|safe }}</p>
    </div>

    <style>
        .article-banner { 
            background-image:
            /* The image fade to white */
            linear-gradient(to left, rgba(0,0,0,0) 10%, #fff 85%),
            /* The image used */
            url("{{ article.article_image.url }}");
            /* Set a specific height */
            height:200px;
            /* Create the parallax scrolling effect */
            background-attachment: fixed;
            background-position: center bottom;
            background-repeat: no-repeat;
            background-size: cover;
            z-index: auto;
            position: relative;
        }
    </style>

    {% endblock %}

Let's also iterate over the tags with a for loop on the article page. The for loop will be {% for tag in article.article_tags.all %} but the content in the for loop with be the same as the for loop on blog.htmlSave the file.

Article page with tags








3: Using Django context processors


Now that we have created a path and a function to sort by tags, we need a way to access them. We could individually add each tag to the navbar, but if we ever decide to add/delete tags we'll have to keep updating the navbar over again.  The easy solution would be to pass Tags as context to navbar.html; however, navbar.html is not connected to a function in views.pyTo solve this dilemma, we will pass Tags in a new file named context_processors.py that will give all HTML templates access to the Tag model.

 

Create a new file named context_processors.py

env > mysite > main > (New File) context_processors.py

Create the file context_processors.pyGIF

Create a new file named context_processors.py in the mysite > main folder.

 

Create a new file named context_processors.py

env > mysite > main > (New File) context_processors.py

from .models import Tag

def menu(request):
    nav_tags = Tag.objects.all()[:4]
    return { 'nav_tags' : nav_tags,
    }

Import Tag from models at the top of the file and then create a new function named menu that returns a queryset of the first 4 tags.  Name the queryset nav_tags and save the file.

 

Add context processor to settings.py

env > mysite > main > settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'main.context_processors.menu',  #add this
            ],
        },
    },
]

Go to settings.py and under TEMPLATES > 'OPTIONS' > 'context_processors', add the menu function we just created in context_processors.py

 

Add variables to navbar.html

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">
    {% for tag in nav_tags %}
      <li class="nav-item">
         <a class="nav-link" href="/blog/{{ tag.tag_slug }}">{{ tag.tag_name }}</a>
      </li>
    {% endfor %}
     <li class="nav-item">
       <a class="nav-link" href="/products">Products</a>
     </li>
     <li class="nav-item">
       <a class="nav-link" href="/blog/articles">Blog</a>
     </li>
     <li class="nav-item">
       <a class="nav-link" href=" ">Contact</a>
     </li>
   </ul>
   {% if user.is_authenticated %}
    <a class="text-dark" href="/logout">Logout</a>
    <a class="btn btn-sm btn-outline-primary m-2"  href=" ">{{user.username|title}}</a>
   {% else %}
    <a class="text-dark" href="/login">Login</a>
    <a class="btn btn-sm btn-primary m-2"  href="/register">Register</a>
   {% endif %}
 </div>
</nav>

In the unordered list section of the navbar, add a for loop passing in nav_tags. Nest a list item and an anchor element in the for loop and declare the href as the blog path /blog/{{tag.tag_slug}}. Display the tag name as the text and save the file.

Reload the browser page and click on a tag link in the navbar. You should now be brought to that tag's page with all the articles listed under the tag. 

A blog tags pageGIF

 

Add a for loop for a blog section on the homepage

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

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

 	{% block content %}

  	{% load static %}

	<!--CTA-->
	...

	<!--Headlines-->
	...

	<!--Products-->
	...

	<!--Blog-->
	<div class="container py-3">
		<h2>Recent Posts</h2>
		<hr>
		<br>
		<div class="row">
			{% for n in new_posts %}

			{% endfor %}
		</div>
		<div class="container text-right">
			 <a href="/blog/articles">View more</a>
		</div>
	</div>

    {% endblock %}

After the Products section, create a new section with the comment Blog, a heading called Recent Posts and a row with a for loop that iterates over new_posts, and a new division element outside of the row for a "View more" link to the blog page.

 

Code-it-yourself: Add a blog section to the homepage

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

  <!--Blog-->
  <div class="container py-3">
    <h2>Recent Posts</h2>
    <hr>
    <br>
    <div class="row">
      {% for n in new_posts %}
        <a href="/{{ n.article_slug }}" style="text-decoration: none">
          <div class="col-12 pb-4 text-dark">
            <div class="row">
              <div class="col-lg-4 col-md-6 col-sm-12 my-auto">
                <img src="{{ n.article_image.url }}" class="card-img" alt="{{ n.article_name }}">
              </div>
              <div class="col-lg-8 col-md-6 col-sm-12 my-auto">
                <p>
                  {% for tag in n.article_tags.all %}
                    <span class="badge badge-primary" style="font-size:14px;">{{ tag }}</span>
                  {% endfor %}
                </p>
                <h5>{{ n.article_title }}</h5>
                <p class="text-muted" style="font-size:12px">{{ n.article_published }}</p>
                <p>{{ n.article_content|safe|truncatewords:45 }}</p>
                </div>
              </div>
            </div>
        </a>
      {% endfor %}
    </div>
    <div class="container text-right">
      <a href="/blog/articles">View more</a>
    </div>
  </div>

Within the for loop, add the same article layout that we used in blog.html.  The only difference is that we iterate over the queryset with n instead of b.  When you are done, reload to view the differences.

Homepage with blog section







4: Updating the navbar and CTA


Our blog and article code is now complete so let's customize the homepage further with a new navbar and CTA format. 

 

Add a logo to the navbar

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

{% load static %}

<!--Navbar-->
<nav class="navbar navbar-expand-sm navbar-light bg-light">
 <a class="navbar-brand" href="/">
    <img src="{% static "img/logo.png" %}" width="30" height="30" alt="logo">
    Encore
 </a>
...

Load static at the top of the page then nest an image in the first anchor element. Set the image source to an image named "img/logo.png" in the static folder. Save the changes and reload the browser to see a blue logo with headphones in the navbar next to the company name.

Website logo

 

Separate navbar into two navbars

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

{% load static %}

  <!--Navbar 1-->
  <nav class="navbar navbar-expand-md navbar-light bg-light">
    <div class="navbar-collapse collapse w-100 order-1 order-md-0 dual-collapse2">
      <ul class="navbar-nav mr-auto"> 
      </ul>
    </div>
    <div class="mx-auto order-0">
      <a class="navbar-brand m-0" href="/" style="font-size:30px">
        <img src="{% static "img/logo.png" %}" width="35" height="35" alt="logo"> Encore
      </a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".dual-collapse2">
        <span class="navbar-toggler-icon"></span>
      </button>
    </div>
    <div class="navbar-collapse collapse w-100 order-3 dual-collapse2">
      <ul class="navbar-nav ml-auto">
        {% if user.is_authenticated %}
          <li class="nav-item">
            <a class="nav-link" href="/logout">Logout</a>
          </li>
          <li class="nav-item">
            <a class="btn btn-outline-primary" href=" ">{{user.username|title}}</a>
          </li>
        {% else %}
          <li class="nav-item">
            <a class="nav-link" href="/login">Login</a>
          </li>
          <li class="nav-item">
             <a class="btn btn-outline-primary" href="/register">Register</a>
          </li>
        {% endif %}
      </ul>
    </div>
  </nav>

  <!--Navbar 2-->
  <div class="container-fluid sticky-top bg-dark">
    <nav class="container navbar navbar-expand-md navbar-dark">
      <div class="navbar-collapse collapse w-100 order-1 order-md-0 dual-collapse2">
        <ul class="navbar-nav mr-auto">
          {% for tag in nav_tags %}
            <li class="nav-item">
              <a class="nav-link" href="/blog/{{ tag.tag_slug }}">{{ tag.tag_name}}</a>
            </li>
          {% endfor %}
        </ul>
      </div>
      <div class="navbar-collapse collapse w-100 order-3 dual-collapse2">
        <ul class="navbar-nav ml-auto">
          <li class="nav-item">
            <a class="nav-link" href="/products">Products</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="/blog/articles">Blog</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href=" ">Contact</a>
          </li>
        </ul>
      </div>
    </nav>
  </div>

We will also replace the navbar section with two navbars to better organize our content. The first navbar will have the logo, company name, login, and username while the other will have the blog, products, and contact links. The second navbar will always be on the page given its sticky-top class attribute. Feel free to copy this code directly into navbar.html.

Reload the browser page and there are now two navbars at the top with the logo and company name centered at the top. Shrink the window and notice it's responsive. Pretty cool, right?

New navbars

 

Add a CTA banner

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

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

 	{% block content %}

  	{% load static %}

	<!--CTA-->
	<div class="cta-banner"> 
	    <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>
	    </div>
	</div>

  	...

Similar to the article banner we created for article.html, let's make our CTA image a banner instead of an image in a column.  Nest the entire CTA section in a new division element with the custom class attribute cta-banner and remove the second column that had the image before.

 

    ...

  	<style>
	    .cta-banner {
	      	background-image:
	      	/* The image fade to white */
	      	linear-gradient( to left, rgba(0,0,0,0) 10%, #fff 85%),
	      	/* The image used */
	      	url("{% static "img/cta-headphones.jpg" %}");
	      	/* Set a specific height */
	     	height:400px;
	      	/* Create the parallax scrolling effect */
	      	background-attachment: fixed;
	      	background-position: center bottom;
	      	background-repeat: no-repeat;
	      	background-size: cover;
	      	z-index: auto;
	      	position: relative;
	    }
  	</style>

    {% endblock %}

At the bottom of the home.html, add a style element with the custom class cta-banner. We will set the url to the same CTA image we used before.

Save the changes to the file and reload the page. The image that was on the right side is now a cover image with parallax. Although it is a small design change, it makes the site appear more impressive.

CTA banner






Quiz Questions


1. What type of model relationship did we use for Article and Tag?


2. Which option will register the Tag model to the Django admin?


Next lesson


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