First steps with Django - Part 3

First steps with Django - Part 3

Learn Django through an example: create a blog

First of all

Welcome on the third part of the tutorial about discovering Django with an example.

If you didn't read the second part:

The source code of the second part is here:

Be sure that you understood all the concepts of the first and second parts. If not, feel free to go back on it until you're comfortable.

Now you should understand quite a lot of Django's concepts, that's why now I will not explain the things added if we already saw them in the previous parts.

Authentication

Permissions

Django provide a very powerful authentication and rights management system. Remember in the first part we did our first migrations and we so Django's migrations as well. Part of them are used for the authentication system. Django divided that in few models:

  • User This model will represent a user with few fields like the name or the password
  • Permission This model is used to store the different permissions. Django create automatically four permissions for every model:
    • Write
    • Read
    • Delete
    • Update
  • Group this model is used to store the user groups. All the users of a group will inherit the permissions of the group.

It's important to understand that by default these permissions are just checked in the built-in admin of Django. If we want to use it in our code we will have to code it. So we can give permissions to users or groups and, in our application, check if the logged in user has the permission we need.

Custom User model

The Django's user model is not editable. Which means you can not add or remove fields. That can be annoying. You can of course create a second profile linked one-to-one to the model but it's not really sexy. Thankfully we can ask Django to use another model for the user, and we can make this model inherit from the built-in User model. We will do that. We will create a new application which will have to deal with all the user system. Let's call it authentication:

python3 manage.py startapp authentication

Remember to add this application in the installed apps of your settings.py. And now in the models of this app we can add:

from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
    pass

Quite straightforward, we create a new class User, based on the Django's abstract user class. And we do nothing in this class for now. The problem we have is Django already did his migrations for the authentication system and will not appreciate that you change it. Usually we do this custom class before running the first migration.

What we will have to do is to revert the Django's migrations to be able to apply ours:

python3 manage.py migrate admin zero
python3 manage.py migrate auth zero
python3 manage.py migrate contenttypes zero
python3 manage.py migrate sessions zero

Now we have to precise to Django that this class has to be used as the default User model. To do that add in your settings:

AUTH_USER_MODEL = "authentication.User"

And don't forget to add this application in the INSTALLED_APPS of your settings. Now we can make the migrations and run them:

python3 manage.py makemigrations
python3 manage.py migrate

Now we are all good, you can use this model to add or edit fields for your User.

Create a superuser

A superuser is a normal user with his superuser field at True. A superuser is not impacted by the permissions, it's kind of if he has all the permissions we did and will do. To create a superuser we will use the terminal:

python3 manage.py createsuperuser

Django will ask you for a username, an e-mail and a password. That's it your superuser is ready to go. But to use it we will have to be able to login, and of course, Django makes it easy to do.

Create a login page

In fact we will not have to create this view: Django will do it, we will just tell Django when to use it, and this will happen in our urls. So we will create a urls.py in our authentication package and include it in the main urls.py file (check how to do it in the first part if you don't remember how to do it) and we will create this url in it:

from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

app_name = 'authentication'

urlpatterns = [
    path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'),
]

So we decided that the url login will point to our login view. As it's integrated in Django we just ask it from the package auth_views. The as_view is because this view is a class so we have to precise to use it as a view. Finally we give the name of the template to use to display the form.

We will do a very simple template login.html, that we will store in a templates folder in your authentication application:

{% extends 'main.html' %}
{% load crispy_forms_tags %}
{% block top_menu %}
{% endblock %}
{% block content %}
    <h2 class="text-center">Log in</h2>
    <div class="text-center mb-100">
        <form action="{% url 'authentication:login' %}" method="post">
            {% csrf_token %}
            {{ form|crispy }}
            <button type="submit" class="btn btn-pastel-teal">Connect</button>
        </form>
    </div>
{% endblock %}

Wait, since when do we have an url authentication:login? Can we not put directly login? That is due to the fact that we have multiple applications now. Adding the name of the app first will make Django generate the good url. Now if you go to http://127.0.0.1:8000/auth/login/ you should see your form (in my example, in the main urls.py I specified that the app authentication will have an url auth/). You can now try to login, if the wrong credentials are provided you will have an error message, If not you will have ... an error. In fact by default, Django will redirect to accounts/profile/ after login. We will ask him to redirect to the homepage in the settings:

LOGIN_REDIRECT_URL = 'blog:list_posts'

Now it should ... not work. You should have an error saying 'blog' is not a registered namespace. In fact when we added a new application, Django automatically created a namespace for it. But not for the one we created before. If you compare the 2 urls.py in both application you will see the difference: app_name is defined in authentication but not in our blog app. Let's add it:

app_name = 'blog'

there is a mess everywhere I have error message all time long Yes you have and that is normal. Now each of our application has a space name which means that your links are not working properly. We will modify them in the homepage.html of our blog app:

{% extends 'main.html' %}
{% load crispy_forms_tags %}
{% block content %}
    <div class="row mt-3">
        {% if posts %}
            {% for post in posts %}
                <div class="col-md-6">
                    <div class="card flex-md-row mb-4 box-shadow h-md-250">
                        <div class="card-body d-flex flex-column align-items-start">
                            <h3 class="mb-0">
                                <a class="text-dark" href="{% url 'blog:display_post' post_id=post.id %}">
                                    {{ post.title }}
                                </a>
                            </h3>
                            <p class="card-text mb-auto">
                                {{ post.content|truncatewords:15 }}
                            </p>
                            <a href="{% url 'blog:display_post' post_id=post.id %}">Continue reading</a>
                        </div>
                    </div>
                </div>
            {% endfor %}
        {% else %}
            <h4 class="text-warning"> There is nothing to see here </h4>
        {% endif %}
    </div>
    <div class="row mt-3">
        <div class="col-md-6 offset-md-3">
            <form class="form-control" action="{% url 'blog:list_posts' %}" method="post">
                {% csrf_token %}
                {{ form|crispy }}
                <input type="submit" class="btn btn-info" value="Add my post"/>
            </form>
        </div>
    </div>
{% endblock %}

There is 3 urls so you may change 3 lines to add blog: before the name of the view. And now everything is working well.

Logout

The logout is probably the easiest to do. We just need an url in our authentication app:

    path('logout/', auth_views.LogoutView.as_view(), name='logout'),

And that's it if you visit the page you'll be disconnected.

Access the logged user

When you are logged in, an object user is created and added to the request which goes in your views and template. Let's use it in the templates to display our username when we are connected, as well, a link to logout and finally a login link if we are not connected. For that we will edit our main template because we want it to be on every page:

[...]
<header class="blog-header py-3">
    <div class="row flex-nowrap justify-content-between align-items-center">
        <div class="col-4 text-center offset-4">
            <a class="blog-header-logo text-dark" href="#">My awesome blog</a>
        </div>
        <div class="col-4 d-flex justify-content-end align-items-center">
            {% if user.is_authenticated %}
                                    Hello {{ user.username }} - <a href="{% url 'authentication:logout' %}">Log out</a>
            {% else %}
                <a class="btn btn-sm btn-outline-secondary" href="{% url 'authentication:login' %}">Login</a>
            {% endif %}
        </div>
    </div>
</header>
[...]

As you can see, because the user is now register in the request, we can access it directly in the template. is_authenticated is a Django shortcut in your user model which return a boolean depending if a user is authenticated or not.

Create custom permissions

As we said before, Django will create four permissions per model. But maybe you want your own permission. Or maybe you don't want to create these permissions. We can manage that very easily in the models. You can create or delete permissions directly. Let say that for our model Post we want to create a permission Can post, I know we already have it automatically but let's just do that for the example. We will add in our model:

from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    publication_date = models.DateTimeField(auto_now_add=True)
    edition_date = models.DateTimeField(auto_now=True)

    class Meta:
        permissions = [
            ("can_post", "Can add a post to the blog"),
        ]

Now , make the migrations and migrate and this new permission will be stored in the database. Of course for now our permission will not do anything but we will use it soon. If you want to delete the default permissions of a model, in the meta class just add default_permissions = () and it's done. I fully discourage you to remove the default permissions of a model. You can ignore them but try to not remove them, it's not a problem to have them for nothing, but it can be a big problem to not have them when needed.

Add a permission to a user

For now we don't have an interface to add the permissions to a user, so we will use the terminal to do it. Here is the plan:

  • Create a normal user named norights with password norights
  • Create a normal user named rights with password rights
  • Give the right can_post to the user rights
  • See how we use these permissions

Remember we don't use our current user because it's a superuser which means that the permissions have no effects on it.

So to do that in your terminal type python3 manage.py shell This command will open a python shell where we will do all of this:

from django.contrib.auth import get_user_model #get_user_model will pick the model specified in the settings.
from django.contrib.auth.models import Permission

u = get_user_model().objects.create(username='norights', email='norights@norights.com')
u.set_password('norights')
u.save()
u = get_user_model().objects.create(username='rights', email='rights@rights.com')
u.set_password('rights')
u.save()

perm = Permission.objects.get(codename='can_post')
u.user_permissions.add(perm)

Now we do have our two users including one having the rights. Let see how to use these permission in the template. Right now everybody can see the form to create a post. Now we want only a user authenticated and with the right can_post to be able to see the form.

Check the permissions in the template

For that we will modify our homepage.html, and explain that we want the form if and only if the user is authenticated and have the rights:

[...]
{% if user.is_authenticated and perms.blog.can_post %}
    <div class="row mt-3">
        <div class="col-md-6 offset-md-3">
            <form class="form-control" action="{% url 'blog:list_posts' %}" method="post">                  
                {% csrf_token %}
                {{ form|crispy }}
                <input type="submit" class="btn btn-info" value="Add my post"/>
            </form>
        </div>
    </div>
{% endif %}
[...]

When a user has a permission or is part of a group having a permission, this permission is accessible in the template in perms.application_name.permission_codname. As we created this permission in the application blog, this line will return True or False depending on the user permissions. You can now login with rights and norights to see the difference.

Of course checking the permission in the template will not be enough to secure our app. It can be great to check it as well in the view.

Check the permissions in the view

To check the permission and the authentication in the view there is two ways to do it, depending on if we want the restriction to apply to the all view or only a part of it. In our case there is just a part of the view we want to restrict, is the treatment of the form. So we will edit our view like this:

def list_posts(request):
    posts = Post.objects.all()
    if request.user.is_authenticated and request.user.has_perm('blog.can_post'):
        form = PostForm(request.POST if request.method == 'POST' else None)
        if request.method == 'POST':
            if form.is_valid():
                form.save()
                form = PostForm()
    return render(request, 'homepage.html', locals())

It's quite easy here, it works like in the template. We will get the form in the locals and treat this form only if the user is authenticated and has the rights. You can check, removing the condition in the template and refresh, the form will not be here.

The second method for checking the permissions is easier (yes it's possible). but to do that we will change our page architecture. What we will do is create a view specific to create a post. In the home page we will have a link pointing to this page if we are logged and have the permissions. You already know how to do it, create the view, the template and the url. Here is how I did it:

homepage.html:

{% extends 'main.html' %}
{% block content %}
    <div class="row mt-3">
        {% if posts %}
            {% for post in posts %}
                <div class="col-md-6">
                    <div class="card flex-md-row mb-4 box-shadow h-md-250">
                        <div class="card-body d-flex flex-column align-items-start">
                            <h3 class="mb-0">
                                <a class="text-dark" href="{% url 'blog:display_post' post_id=post.id %}">
                                    {{ post.title }}
                                </a>
                            </h3>
                            <p class="card-text mb-auto">
                                {{ post.content|truncatewords:15 }}
                            </p>
                            <a href="{% url 'blog:display_post' post_id=post.id %}">Continue reading</a>
                        </div>
                    </div>
                </div>
            {% endfor %}
        {% else %}
            <h4 class="text-warning"> There is nothing to see here </h4>
        {% endif %}
    </div>
    {% if user.is_authenticated and perms.blog.can_post %}
        <a href="{% url 'blog:add_post' %}">Click here to add a post</a>
    {% endif %}
{% endblock %}

views.py:

from django.shortcuts import render

from blog.forms import PostForm
from blog.models import Post


def list_posts(request):
    posts = Post.objects.all()
    return render(request, 'homepage.html', locals())


def add_post(request):
    form = PostForm(request.POST if request.method == 'POST' else None)
    if request.method == 'POST':
        if form.is_valid():
            form.save()
            form = PostForm()
    return render(request, 'addpost.html', locals())


def display_post(request, post_id):
    post = Post.objects.get(id=post_id)
    return render(request, 'post.html', locals())

urls.py:

from blog import views
from django.urls import path

app_name = 'blog'

urlpatterns = [
    path('', views.list_posts, name='list_posts'),
    path('post/<int:post_id>', views.display_post, name='display_post'),
    path('addpost/', views.add_post, name='add_post'),
]

Nothing complicated here we already did that few times before.

Now let's add the rights check. For that we will use the decorators provided by Django. In your views.py:

from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render
[...]
@login_required
@permission_required('blog.can_post')
def add_post(request):
    form = PostForm(request.POST if request.method == 'POST' else None)
    if request.method == 'POST':
        if form.is_valid():
            form.save()
            form = PostForm()
    return render(request, 'addpost.html', locals())
[...]

So as you can see, we use two different decorators here the first one @login_required is checking that the user is authenticated. The second one @permission_required('blog.can_post') check if the user has the permission mentioned as first argument. Now if you try to go to the page with norights or without login in, you will have ... a weird error, it seems that Django redirected you on an unknown page. That's because by default, if you don't have the permissions or you're not logged in, Django will redirect you to the login url. But we didn't define it. let's do that in settings.py:

LOGIN_URL = 'authentication:login'

Now it will works well.

The groups

Now you understood the permissions, groups will be easy. A group provide its own permissions to all the users in the group. To understand better we will create a group named bloggers, and give the permission can_post to it. Then, we will add our user 'norightsto this group. To do that we will do it inpython3 manage.py shell`:

from django.contrib.auth.models import Group, Permission
from django.contrib.auth import get_user_model

newgroup = Group.objects.create(name='bloggers')
perm = Permission.objects.get(codename='can_post')
newgroup.permissions.add(perm)
user = get_user_model().objects.get(username='norights')
user.groups.add(newgroup)

Now logout and login with norights user (you have to logout and login again because the permissions are not updated if you don't. As you can see, now you can add a new post. But we didn't give the rights to the user. But the user is part of a group which have this permission. You can add as many permissions to a group, as many groups to a user, and as many users to the group you want.

Edit and delete a post

Our blog start to be very good right? But if what if we do a mistake in our posts? Impossible to delete them or edit them without touching the database. We don't want anyone touching the database so we will do it by ourselves.

Edit a post

To edit a post we will not have to create a view. In fact the creation view will be way enough to do it, we will just have to edit it a bit. You remember, in the part 2, we talked about binded and unbinded forms. That can be great if we can bind an instance of post to our form before displaying it right? That is exactly what we will do. First, we need our view to get the id of the post we want to edit. We will give a parameter to the view for that:

@login_required
@permission_required('blog.can_post')
def add_post(request, post_id=None):
    form = PostForm(request.POST if request.method == 'POST' else None)
    if request.method == 'POST':
        if form.is_valid():
            form.save()
            form = PostForm()
    return render(request, 'addpost.html', locals())

We added the parameter, and giving a default value at None because this view can be called without any parameter (if we want to create a new post). This behaviour will impact as well our urls, because our url currently doesn't accept any parameter. But if we add a parameter in the url, we will not be able to access it without parameters. So how to do it? Simple we will do both:

urlpatterns = [
    path('', views.list_posts, name='list_posts'),
    path('post/<int:post_id>', views.display_post, name='display_post'),
    path('addpost/', views.add_post, name='add_post'),
    path('addpost/<int:post_id>', views.add_post, name='add_post'),
]

Now in our view we will bind our form if the id is not None:

@login_required
@permission_required('blog.can_post')
def add_post(request, post_id=None):
    post = Post.objects.get(pk=post_id) if post_id else None
    form = PostForm(request.POST or None, instance=post)
    if request.POST and form.is_valid():
        form.save()
        return redirect('blog:list_posts')
    return render(request, 'addpost.html', locals())

So what we did here is get the instance of the post if an id is given and pass it with to the form as instance parameter. That way our form will be binded with the instance of the post or unbinded if there is no instance to bind. But to make it work we will have to do a change in our template as well. We need to give the id while we submit the form. At this point, we want to submit our form to the exact same page and for that HTML5 make it very easy: You just have to remove the action parameter of the form. That way the form will be submitted on the exact same url:

<form class="form-control" method="post">

Try it out you should be able to edit any of your posts accessing the good url. Of course typing the url is not really convenient. Let's add a link in the post, if we have the rights add_post, so in post.html:

{% extends 'main.html' %}
{% block content %}
    <div class="row mt-3">
        <div class="col-md-8 offset-2">
            <h1>{{ post.title }}</h1>
        </div>
    </div>
    {% if user.is_authenticated and perms.blog.can_post %}
        <div class="row mt-3">
            <div class="col-md-6 offset-3">
                <a href="{% url 'blog:add_post' post_id=post.id %}">Edit this post</a>
            </div>
        </div>
    {% endif %}
    <div class="row mt-3">
        <div class="col-md-8 offset-2">
            {{ post.content }}
        </div>
    </div>
{% endblock %}

Delete a post

The last thing we will do today, is to create the opportunity to delete a specific post. Compare to what we did until now it's quite easy first we will create our url and edit our templates:

blog/urls.py

from blog import views
from django.urls import path

app_name = 'blog'

urlpatterns = [
    path('', views.list_posts, name='list_posts'),
    path('post/<int:post_id>', views.display_post, name='display_post'),
    path('addpost/', views.add_post, name='add_post'),
    path('addpost/<int:post_id>', views.add_post, name='add_post'),
    path('delpost/<int:post_id>', views.del_post, name='del_post'),
]

blog/templates/post.html

[...]
{% if user.is_authenticated and perms.blog.can_post %}
    <div class="row mt-3">
        <div class="col-md-6 offset-3">
            <a href="{% url 'blog:add_post' post_id=post.id %}">Edit this post</a>
            <a href="{% url 'blog:del_post' post_id=post.id %}">Delete this post</a>
        </div>
    </div>
{% endif %}
[...]

We don't need a specific template on this one, we will redirect to the post list. Now let's do our del_post view:

from django.shortcuts import render, get_object_or_404, redirect

[...]

@login_required
@permission_required('blog.can_post')
def del_post(request, post_id=None):
    post = get_object_or_404(Post, pk=post_id)
    post.delete()
    return redirect('blog:list_posts')

[...]

Nothing really complicated, we get the instance with the id and we delete it then we redirect to the list_posts page. One new thing is get_object_or_404() This is a shortcut Django, if the post is not found, an error 404 will be raised.

Enough for today

Now we are able to login, logout, edit and delete our posts, put some permissions system. That's a big thing and you have all the keys to create a signup system for your blog and to play around. There is a lot of things to discover in Django and the documentation will give you a lot of informations about it. There is still some things we will see in the next part: The category and comment system which will show us how to join models together, and edit the user to be able to custom it, and put an avatar which will show us how to work with files in Django.

The code of this part is available here: