1: Installing the TinyMCE package


At this point, you have completed many of the basics of web development.  From adding Bootstrap components to an HTML template to utilizing the Django template language, you are developing a solid foundation for creating web apps.  Now, in order to further test your learning and add more functionality to the site, let's add a blog.

A blog is great way to provide valuable and informative content to users.  It's also an important process to get more traffic to your site.  SEO or search engine optimization is the process of posting relevant content to a site that will appear in search engine results.  While obtaining high SEO rankings on Google or Yahoo takes time to accomplish, creating a blog is a practical way to start to get visitors.

 

Install TinyMCE

macOS Terminal

(env)User-Macbook:mysite user$ pip install django-tinymce4-lite
...

Windows Command Prompt

(env) C:\Users\Owner\Desktop\Code\env\mysite>pip install django-tinymce4-lite
...

Quit the server and install TinyMCE, an online text editor that will help you create blog articles from the Django admin. This HTML editor is designed to make adding content to your website easier by providing a basic word processor.

 

Add TinyMCE to the settings.py

env > mysite > mysite > settings.py

...
INSTALLED_APPS = [
    'main.apps.MainConfig',
    ...
    'tinymce', #add this
]

TINYMCE_DEFAULT_CONFIG = {
    'height': 400,
    'width': 1120,
    'cleanup_on_startup': True,
    'custom_undo_redo_levels': 20,
    'selector': 'textarea',
    'browser_spellcheck': 'true',
    'theme': 'modern',
    'plugins': '''
            textcolor save link image media preview codesample contextmenu
            table code lists fullscreen  insertdatetime  nonbreaking
            contextmenu directionality searchreplace wordcount visualblocks
            visualchars code fullscreen autolink lists  charmap print  hr
            anchor pagebreak
            ''',
    'toolbar1': '''
            fullscreen preview bold italic underline | fontselect,
            fontsizeselect  | forecolor backcolor | alignleft alignright |
            aligncenter alignjustify | indent outdent | bullist numlist table |
            | link image media | codesample
            ''',
    'toolbar2': '''
            visualblocks visualchars |
            charmap hr pagebreak nonbreaking anchor |  code |
            ''',
    'contextmenu': 'formats | link image',
    'menubar': True,
    'statusbar': True,
    }

Open the settings.py file and add tinymce to the installed apps list and copy and paste TINYMCE_DEFAULT_CONFIG settings below the installed apps. The default settings include a spellchecker and the ability to change the font size and style from the editor.

 

Add tinymce path to mysite > urls.py

env > mysite > mysite > urls.py

"""mysite URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include ('main.urls')),
    path('tinymce/', include('tinymce.urls')), #add this

]

if settings.DEBUG: #add this
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Go to the urls.py in the mysite > mysite folder and add the TinyMCE path. 








2: Adding an article model


Create an Article model

env > mysite > main > models.py

from django.db import models
from tinymce.models import HTMLField  #add

# Create your models here.
...

class Article(models.Model):
	article_published = models.DateTimeField('date published')
	article_content = HTMLField()

Go to the models.py file and import HTMLField from tinymce.models at the top of the page. Then create a new class called Article. Within this model add fields article_published and article_content.  HTMLField will give us TinyMCE's text editor that comes with a handful of style and formatting features.

 

Code-it-yourself: Add fields to Article model

env > mysite > main > models.py

from django.db import models
from tinymce.models import HTMLField  #add

# Create your models here.
...

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_content = HTMLField()
	article_slug = models.SlugField()

	def __str__(self):
		return self.article_title

Add three more fields: article_title as a CharField with a max length of 200, article_image as an ImageField that uploads to the 'images/' directory, and article_slug as a SlugField.  A SlugField is similar to a CharField except only letters, numbers, underscores and hyphens are allowed since they are used in URLs.  Also add a function that returns the article_title as the display name in the Django admin like we did with Product.

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 update the database with the new information added to models.py.

 

Code-it-yourself: Add the Article model to admin.py

env > mysite > main > admin.py

from django.contrib import admin
from .models import Product, Article  #add Article

# Register your models here.

admin.site.register(Product)
admin.site.register(Article)

Import Article at the top of admin.py and register the Article model to the admin site so it will appear on the Django admin web page.

 

Add an article to the Django admin

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

Add blog articlesGIF

Article title: 10 Best Wireless Work Headphones

Date published: 2020-03-08 11:22:38

Article image: work-headphones.jpg

Article content: Headphones have become an important part of the work environment as a means of tuning out distractions and getting into the work flow. From hip-hop to folk music, whichever music genre gets you through the work-day cannot blare in a quite workplace, so check out the top 10 best wireless work headphones to help you jam in the office or library. Before we jump in, let's get in to why wireless headphones are the ideal headphone for work places.

Article slug: 10-best-wireless-work-headphones

Run the server again. Go back to the Django admin page and login with the admin superuser, the very first user you created. Then click on the new model "Articles" and add an article. Just like the product images, click on the image link above and save the image in your Downloads folder so you can easily upload them to the admin.

 

Add the rest of the example articles

Save and add another > How Beats by Dre changed the Headphones Game

Article title: How Beats by Dre changed the Headphones Game

Date published: 2020-03-11 12:50:49

Article image: beats-by-dre.jpg

Article content: With their iconic logo and strong celebrity presence, Beats has become the best known headphones brand within the last 10 years. Athletes, rappers, and popular celebrities all sport the headphones in advertisements and everyday life, but how did Beats by Dre become so popular?

Article slug: how-beats-by-dre-changed-the-headphones-game

 

Save and add another > 5 Professional Gamers' Headsets

Article title: 5 Professional Gamers' Headsets

Date published: 2020-03-18 17:39:06

Article image: gaming-headset.jpg

Article content: Professional gaming is becoming a lucrative profession among younger generations.  With large followings and good statics, professional gamers turn what was once a hobby into a profession. Although every gamer may not reach mass stardom, anyone can feel like a professional gamer when using the right equipment. Here is a breakdown of 5 professional gamers and their gaming headsets.

Article slug: 5-professional-gamers-headsets

 

Save and add another > Wire vs. Wireless Headphones

Article title: Wire vs. Wireless Headphones

Date published: 2020-03-19 18:00:16

Article image: wire-headphones.jpg

Article content: As technology has advanced, cords and wires have become a thing of the past.  Bluetooth has made cords unnecessary for most music lovers and big headphone brands have jumped on this trend.  But wires are still the best way to listen to music given wired headphones tend to support a higher number of kilobits per second (kbps).

Article slug: wire-vs-wireless-headphones

 

Save and add another > 8 Underrated Headphone Brands

Article title: 8 Underrated Headphone Brands

Date published: 2020-03-21 11:14:46

Article image: underrated-headphones.jpg

Article content: Brands like Beats by Dre and Apple are arguably the most well recognized headphones on the market. But what about the other brands that have been providing quality sound way before the Apple Airpods dropped?  Let's take a look at 8 underrated headphone brands that may get you to switch your brand loyalty.

Article slug: 8-underrated-headphone-brands

Add the remaining 4 articles. We have provided the example articles above to copy and paste so you can view the blog pages with articles. Feel free to edit the appearance of the article content in the TinyMCE editor.








3: Creating a blog page


Now that we have our article model and created some actual articles, we need an HTML template to render the information. First let's work on the blog page that will list all articles.

 

Create a blog.html file

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

Create a blog.htmlGIF

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

 

Code-it-yourself: Edit blog.html

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

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

    {% block content %}

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

    {% endblock %}

Let's set up the basic structure of the document. Extend the header.html template and add the block content tags. Add a division element with the class attributes container py-5. Nest a heading one with the class attribute font-weight-bold within it. Write in "Articles" as the heading text. Then add a thematic break and a regular break before the closing tag of the division element. Save the file.

 

Code-it-yourself: Add 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("blog", views.blog, name ="blog"),

]

Add a path for blog to urls.py.

 

Add a blog function to views.py

env > mysite > main > views.py

...
from .models import Product, Article #import Article from models
...

# Create your views here.
...

def blog(request):
	blog = Article.objects.all().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})

Import Article at the top of the page.  For the article function, we are going to pass the Article objects as context just like in the products function.  However, we also add order_by('-article_published') so the articles are always listed from newest to oldest publication date.

Let's also use Django pagination. Add the three paginator variables and pass blog_obj as context then save the file.

 

Add for loop to blog.html

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

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

    {% block content %}

    <!--Blog-->
    <div class="container py-5">
        <h1 class="font-weight-bold">Articles</h1>
        <hr>
        <br>
        <div class="row">
            {% for b in blog %}
                <div class="col-12 pb-4 text-dark">
                    
                </div>
            {% endfor %}
        </div>
    </div>

    {% endblock %}

It's time to render the queryset of articles on the blog.html page. Let's start by creating a row and adding the Django template language so we can iterate over the queryset we passed. Now add a column within the for loop that has dark text, padding at the bottom, and always takes up the entire row with col-12.

 

Add the model to the blog.html

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

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

    {% block content %}

    <!--Blog-->
    <div class="container py-5"> 
        <h1 class="font-weight-bold">Articles</h1>
        <hr>
        <br>
        <div class="row">
            {% for b in blog %}
                <div class="col-12 pb-4 text-dark">
                    <div class="row">
                        <div class="col-lg-4 col-md-6 col-sm-12 my-auto">
                        </div>
                        <div class="col-lg-8 col-md-6 col-sm-12 my-auto">
                        </div>   
                    </div>
                </div>
            {% endfor %}
        </div>
    </div> 

    {% endblock %}

Within the column, nest a new row that will have two columns, one column for the article image and the other for the article title and text. Add my-auto to both columns so they are vertically centered with one another.

 

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

    {% block content %}

    <!--Blog-->
    <div class="container py-5"> 
        <h1 class="font-weight-bold">Articles</h1>
        <hr>
        <br>
        <div class="row">
            {% for b in blog %}
                <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">
                            <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>
            {% endfor %}
        </div>
     </div>

     {% endblock %}

In the first column nest the article image url and in the second column add the article title, publication date, and content. Add |safe to the article_content so TinyMCE will render the text we added to the Django admin then add |truncatewords:25 immediately after to only allow the first 25 words to appear when the article's content is called. The last thing is to add a "View post" button to the right column.

 

Add blog 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">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>

Add the blog slug to the href attribute of the blog anchor element in the navbar and save the file. Refresh the browser displaying your project and click on the blog navigation link. You will be directed to the blog page displaying all of the models we added. Now try and click on one. Nothing will happen; that's because we never added the link to the articles or created an article.html to display the content.

Blog page with no article linkGIF

 

Add links to articles

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

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

    {% block content %}

    <!--Blog-->
    <div class="container py-5"> 
        <h1 class="font-weight-bold">Articles</h1>
        <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">
                            <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 %}

Add an anchor tag with the href attribute /{{b.article_slug}} immediately after the start of the for loop. Close the anchor tag before the end of the for loop. Then add the style attribute text-decoration:none to the opening anchor tag so the information within the anchor tag is not underlined when a user hovers over the content. 

 

Add pagination to article.html

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

    <!--Blog-->
    <div class="container py-5">
        <h1 class="font-weight-bold">Articles</h1>
        <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">
                                <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>
        <!--Pagination-->
        <div class="container"> 
            <div class="pagination justify-content-center">
                <span class="step-links">
                    {% if blog.has_previous %}
                        <a href="?page=1">&laquo; first</a>
                        <a href="?page={{ blog.previous_page_number }}">previous</a>
                    {% endif %}
                    <span class="current">
                        Page {{ blog.number }} of {{ blog.paginator.num_pages }}
                    </span>
                    {% if blog.has_next %}
                        <a href="?page={{ blog.next_page_number }}">next</a>
                        <a href="?page={{ blog.paginator.num_pages }}">last &raquo;</a>
                    {% endif %}
                </span>
            </div>
        </div>
        <!--end of Pagination-->
    </div>


{% endblock %}

After the for loop and closing division element of the row, add a container containing the pagination variables similar to the products page.

Blog page with pagination








4: Creating an article page


We added the article links to the blog.html file but if you click on one of the links, you are brought to a Django debug page that gives a 404 error, "Page not found". That's because we still do not have a template, url, or view for our articles.

 

Create an article.html file

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

Create an article.html fileGIF

Create a new file named article.html in templates > main.

 

Edit the article.html file

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

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

    {% block content %}

    <!--Article HTML-->

    {% endblock %}

Extend the header and add block content to the file. Save the changes.

 

Add an article 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("<article_page>", views.article, name = "article"),
]

Add a path for an article page to urls.py.  You'll notice article_page is surrounded by <>.  This will allow articles with different slugs to be directed to the same view.

 

Add an article function to views.py

env > mysite > main > views.py

...

# Create your views here.
...

def article(request, article_page):
    article = Article.objects.get(article_slug=article_page)
    return render(request=request, template_name='main/article.html', context={"article": article})

Create a new function called article with the two parameters request and article_page. Then create a new variable within the function called article that gets the specific Article object that matches the article_page being requested.  Finally return the HTML template article.html and the context article.

We are able to pass article_page as a parameter in the function since we surrounded the variable in <>'s in the previous step.  

 

Update article.html with article variables

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

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

    {% block content %}

    <!--Article-->
    <div class="container">
        <h1 class="font-weight-bold">{{ article.article_title }}</h1>
        <p class="font-weight-bold" style="font-size:15px">{{ article.article_published }}</p>
    </div>
    <div class="container">
        <br>
        <p>{{ article.article_content|safe }}</p>
    </div>

    {% endblock %}

Create one container for the title and the publication date and another container with the content. As you can see, we do not need a for loop to render an article because we only pass one article as context instead of multiple articles as a queryset. As always, make sure to save the file before moving on.

Reload the browser page that is displaying blog.html. Click on one of the articles and you will be brought to an article page.  The page is not very interesting so let's up the appeal with a cover image.

View article page in browserGIF

 

Add a cover image to the article.html file

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="font-weight-bold">{{ article.article_title }}</h1>
            <p class="font-weight-bold" style="font-size:15px">{{ article.article_published }}</p>
            
        </div>
    </div>
    <div class="container">
        <br>
        <p>{{ article.article_content|safe }}</p>
    </div>


    {% endblock %}

Go to the article.html template and nest the first container, article title, and date of publication in a new division with the custom class attribute article-banner and the Bootstrap utility text-center. Add a few break elements within the new division to separate it from the original container. 

 

...

    <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 %}

Now add a style element to the bottom of the file that calls on the custom class article-banner. It may look complicated at first, but essentially these style attributes declare an image as a background, add a white gradient over the image to increase text visibility, and specify a height. The bottom six lines of code then create a parallax scrolling effect so the background image will stay in place while the user scrolls.

Save the file and refresh the article page in the browser. There is now a cover image behind the article title.

Updated article page with cover image






Quiz Questions


1. Which queryset will return all Article objects?


2. What is the difference between SlugField and CharField?


Next lesson


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