Skip to content

Jonatandb/Dave-Gray-Django-Tutorials

Repository files navigation

  • Posts app with:
    • User registration
    • User authentication
    • Posts listing
    • Posts creation
    • Protected routes

Note: To follow the tutorial playlist and make a new project for each lesson, I made a script called 'clone_folder_and_increase_version.py' that creates a copy of the previous lesson folder and updates version numbers (in folders and files) accordingly (Thanks ChatGPT β™₯)


Lesson 0:

  • Project creation
  • Rendering of html templates
  • Config and use of paths and static files (css, js)

Create env (only the first time):

py -m venv venv

Activate env:

source venv/Scripts/activate

Install Django (only the first time):

pip install django

Create a new Django project:

django-admin startproject lesson0

Start app:

cd lesson0

py manage.py runserver

Visit App:

To render HTML templates, a 'view' function must be created in views.py file:

def homepage(request):
  return render(request, 'homepage.html')

To use it, a 'path' must be specified in urls.py file, inside of 'urlpatterns' list:

path('', views.homepage, name="homepage"),

By default Django will look for 'homepage.html' inside 'templates' folder in the root of the project.

Finally, to be able to use static files, the following code must be added to 'settings.py' file:

import os # Add this at the top

# Add this at the bottom of settings.py
STATICFILES_DIRS = [
  os.path.join(BASE_DIR, 'static'),
]
  • Now, static files must be placed into '/static' root folder, and can be referenced from 'homepage.html' by using '{% static %} syntax':

    <link rel="stylesheet" href="{% static 'css/styles.css' %}">

    <script src="{% static 'js/index.js' %}" defer></script>

alt text


Lesson 1:

  • App creation
  • App configuration
  • Using of namespaced templates
  • VSCode Emmet configuration for Django HTML files
  • Creation and use of "Base layout" template

Create a new Django app:

py manage.py startapp posts

Add new 'posts' app to INSTALLED_APPS on settings.py

Add new 'templates/layout.html' file with {% block title %} and {% block content %} placeholders

Update 'homepage.html' and 'about.html' to extends and use base layout by adding {% extends 'layout.html' %} and specifing blocks content.

Also add a new posts/templates/posts/posts.list.html file with {% extends 'layout.html' %}

Add posts/urls.py and update lesson1/urls.py to include the posts urls.

Start app:

cd lesson1

py manage.py runserver

Visit App:

alt text

alt text


Lesson 2:

  • Model and migrations

Create a class called Post inside posts/models.py (must inherit from models.Model)

Define fields (more info: https://docs.djangoproject.com/en/5.0/ref/models/fields/)

Apply base app migrations by running:

py manage.py migrate

Create migration for new Post model:

py manage.py makemigrations
  • A new 'posts\migrations\0001_initial.py' file will be created.

Apply migrations:

py manage.py migrate
  • This creates Post model (table) on the database.

Lesson 3:

  • ORM Intro

  • Adding '_str_' method to Post model to show post title whenever a post is shown:

    def __str__(self):
      return self.title
    

A quick way to try and play with the Django ORM is by using the shell this way:

py manage.py shell

From there the Post model can be imported to perform several operations:

from posts.models import Post

To create a Post object:

p = Post()

To set values for the attributes:

p.title = "First Post"
p.body = "First Post body text"

Now the Post object can be saved into the database:

p.save()

Finally it's possible to get all posts from the database:

Post.objects.all()
  • Expected result:

    <QuerySet [<Post: First post>]>

To exit from the shell, execute:

exit()

alt text


Lesson 4:

  • Django Admin Introduction
  • Admin panel user creation
  • Model registration to show it in admin panel
  • Django ORM Post model used to retrive ordered posts
  • Use of Python code in Jinja HTML templates to itereate and render posts

An user must be created to access to /admin panel:

  py manage.py createsuperuser
  • User created: admin

  • Password: admin123

  • Posts can be manipulated by using the admin panel, but first it's requeried to "register" the Post model:

    • Add to 'posts/admin.py', the following lines:

      from .models import Post
      admin.site.register(Post)
      
    • Next, access to http://localhost:8000/admin

      • A CMS web interface is available to manipulate (CRUD operations available) posts, users and groups.

alt text

  • To show posts list, posts must be retrieved from the database by using Django ORM, and then passing it to the render method:

    def posts_list(request):
      posts = Post.objects.all().order_by('-date')
      return render(request, 'posts/posts_list.html', { 'posts': posts })
    
    • By using '-date' param, it will retrive posts in descending order.
  • Finally, the HTML template must iterate the posts list to show the posts, it can be done by using a for loop:

    {% for post in posts %}
      <article>
        <h2>{{ post.title }}</h2>
        <p>{{ post.date }}</p>
        <p>{{ post.body }}</p>
      </article>
    {% endfor %}
    

alt text


Lesson 5: Pages, URLs & Slugs

  • URL path's names (aliases) and using URL name's on anchor tags
  • Setting app_name to app url's file
  • Using 'slug' Django path converter

Setting app_name in '\posts\urls.py' to be able to have the same url 'name' in different urls.py app files.

  • To do this, just add a variable with the app name to 'posts/urls.py' app file:

    app_name = 'posts'
    

Setting name to an url path in '\posts\urls.py':

path('', views.posts_list, name="list")
  • Now, whenever an anchor tags is created, it can use app name and url name, like this:

    <a href="{% url 'posts:list' %}">Posts</a>

Setting url path config in URL in '\posts\urls.py' for use of the post's 'slug':

path('<slug:slug>', views.post_page, name="page"),
  • First 'slug' term tells Django and slug will be used in URL, second 'slug' term specify the Post model field which will be used as slug for creation of each Post URL.
  • Before this works, all posts must have slug field complete with some value like "my-first-post", "another-post", etc.

Finally, it's possible now to visit a specific post page specifing the post slug in URL, ex:

alt text


Lesson 6: Upload & Display Images

  • Update 'settings.py' adding at the bottom:

    MEDIA_URL = 'media/'
    MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
    
  • Create a 'media' root folder.

  • Update main 'urls.py' to add:

    from django.conf.urls.static import static
    from django.conf import settings
    
    urlpatterns = [
      ... # Do not touch this part
    ]
    
    # Add this:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    
  • Install Pillow, that allows to use 'models.ImageFiled' in models:

    pip install Pillow
    
  • Update Post model in 'posts/models.py', adding a new 'banner' field:

    banner = models.ImageField(default='fallback.png', blank=True)
    
  • Make and execute migrations:

    py manage.py makemigrations
    py manage.py migrate
    
  • Create a requirements.txt file:

    pip freeze > requirements.txt
    
  • Go to admin panel and update existing posts adding the value for the banner field (by default 'fallback.png' will be used for all posts)

  • Make sure '/media' folder and 'media/fallback.png' file exists.

  • Go to 'posts/templates/post_page.html' and add an img tag to show the image of the new 'banner' field

    <img class="banner"
          src="{{ post.banner.url }}"
          alt="{{ post.title}}"
    />
    
  • Update CSS file '/static/css/styles.css' adding:

    .banner {
      display: block;
      width: 100%;
      max-width: 800px;
    }
    

alt text


Lesson 7: Adding a new 'Users' app

  • App creation:

    py manage.py startapp users
    
  • App registration in 'lesson7/lesson7/settings.py':

    INSTALLED_APPS = [
        ... # Do not touch anything here
        'posts',
        'users' # <-- Add this line
    ]
    
  • Add url path to users app in 'lesson7/lesson7/urls.py' file:

    path('posts/', include('posts.urls')), # <-- Below this line
    path('users/', include('users.urls')), # <-- Add this new line
    
  • Adding a new route path in a new 'users/urls.py' file:

    app_name = 'users'
    
    urlpatterns = [
        path('register/', views.register, name="register"),
    ]
    
  • Adding a new view for register page in 'users/views.py' file:

    def register(request):
      return render(request, 'users/register.html')
    
  • Creating the register view HTML template in 'users/templates/users/register.html' which extends the base layout:

    {% extends 'layout.html' %}
    
    {% block title %}
      Register a New User
    {% endblock %}
    
    {% block content %}
      <h2>Register a New User</h2>
    {% endblock %}
    

alt text


Lesson 8: Adding user registration form

  • Updating users register view ('/users/views.py/') to be able to create and then validate registration form, by using Django UserCreationForm functionality:

    from django.contrib.auth.forms import UserCreationForm
    
    def register(request):
        if request.method == 'POST':
            form = UserCreationForm(request.POST)
            if form.is_valid():
                form.save()
                return redirect('posts:list')
        else:
            form = UserCreationForm()
        return render(request, 'users/register.html', {'form': form})
    
  • Updating HTML template 'users\templates\users\register.html' to include the Django form created (it will also show validations errors, if any occur):

    {% block content %}
      <h1>Register a New User</h1>
      <form action="" class="form-with-validation" method="post">
        {% csrf_token %}
        {{ form }}
        <button class="form-submit">Submit</button>
      </form>
    {% endblock %}
    
    • {% csrf_token %} is required to allow Django verify that post data is coming from the same application, and be able to manage/avoid Cross site reference forgery.

alt text


Lesson 9: Login Form and User Authentication

  • Updated navigation bar to include a login link.

  • Updated 'users/urls.py' to include new 'login/' path:

    path('login/', views.login_view, name="login")
    
  • Added 'users/templates/login.html' page (similar to register.html)

  • Added a new login_view method in 'users/views.py' to manage the user login by using AuthenticationForm and login Django built-in functionalities:

    from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
    from django.contrib.auth import login
    
    def login_view(request):
        if request.method == 'POST':
            form = AuthenticationForm(data=request.POST)
            if form.is_valid():
                login(request, form.get_user())
                return redirect('posts:list')
        else:
            form = AuthenticationForm()
        return render(request, 'users/login.html', {'form': form})
    

alt text


Lesson 10: User Authorization

Added logout functionality:

  • Added logout view to 'users/views.py':

    def logout_view(request):
        if request.method == 'POST':
            logout(request)
            return redirect('posts:list')
    
  • Added logout url path to 'users/urls.py':

    path('logout/', views.logout_view, name="logout"),
    
  • Added logout navbar button to layout template 'templates/layout.html':

    <form class="logout" action="{% url 'users:logout' %}" method="post">
      {% csrf_token %}
      <button class="logout-button" aria-label="User Logout" title="User Logout">πŸ‘‹πŸ»</button>
    </form>
    

Protected route -> New Post:

  • Added 'new-post/' url path to 'posts/urls.py':

    path('new-post/', views.post_new, name="new"),
    
  • Added a new html template for the new post page: 'posts/templates/post_new.html'.

  • Added a new navbar link to the new route 'posts/new-post'.

  • Added the view for new post page with the '@login_required()' decorator that will redirect the user if he is not logged in:

    @login_required(login_url='/users/login/')
    def post_new(request):
      return render(request, 'posts/post_new.html')
    
  • URL when an unauthenticated user try to access to '/posts/new-post':

alt text

Using the 'next' query param (if it exists) to redirect the user to the page from where he was redirect to login (surely because the route was protected):

  • Updating the users login page to save the 'next' parameter, in a hidden input, if it exists:

    {% block content %}
      <h1>Login User</h1>
      <form action="" class="form-with-validation" method="post">
        {% csrf_token %}
        {{ form }}
        {% if request.GET.next %}
          <input type="hidden" name="next" value="{{ request.GET.next }}" />
        {% endif %}
        <button class="form-submit">Submit</button>
      </form>
    {% endblock %}
    
  • Updating the login_view functionality to redirect the user to the correct page depending on if the next parameter exists in the url:

    def login_view(request):
        ...
            if form.is_valid():
                login(request, form.get_user())
                if 'next' in request.POST:
                    return redirect(request.POST.get('next'))
                else:
                    return redirect('posts:list')
        else:
            ...
    
  • After a successful login:

alt text

Conditional rendering of menu options depending on if the user is logged in:

  • Updated navbar using 'if user.is_authenticated':

    <nav>
      <a href="{% url 'homepage' %}">
        <span role="img" aria-label="Home" title="Home">🏠</span>
      </a> |
      <a href="{% url 'about' %}">
        <span role="img" aria-label="About" title="About">πŸ˜ƒ</span>
      </a> |
      <a href="{% url 'posts:list' %}">
        <span role="img" aria-label="Posts" title="Posts">πŸ“°</span>
      </a> |
    
      {% if user.is_authenticated %}
        <a href="{% url 'posts:new' %}">
          <span role="img" aria-label="New Post" title="New Post">πŸ†•</span>
        </a> |
        <form class="logout" action="{% url 'users:logout' %}" method="post">
          {% csrf_token %}
          <button class="logout-button" aria-label="User Logout" title="User Logout">πŸ‘‹πŸ»</button>
        </form>
      {% else %}
        <a href="{% url 'users:register' %}">
          <span role="img" aria-label="Register" title="Register">πŸš€</span>
        </a> |
        <a href="{% url 'users:login' %}">
          <span role="img" aria-label="Login" title="Login">πŸ”</span>
        </a>
      {% endif %}
    </nav>
    
  • User not authenticated:

alt text

  • User authenticated:

alt text


Lesson 11: Django Forms

Added a new Author field to post model

  • Delete all posts since now the authenticated user will be the author each time he creates a new post.

  • Update the Post model to include a new foreign key field Author related to the user:

    from django.contrib.auth.models import User
    
    class Post(models.Model):
      ... other fileds...
      author = models.ForeignKey(User, on_delete=models.CASCADE, default=None)
    
  • Execute makemigrations and migrate to update the database table for Post model.

Added a new CreatePost class inside a new 'posts/forms.py' file

  • It extends from forms.ModelForm in django forms:

    from django import forms
    
    class CreatePost(forms.ModelForm):
      class Meta:
        model = models.Post
        fields = ['title','body','slug','banner']
    

Updated post_new view in 'posts/views.py' to instantiate and use CreatePost class and pass it to the post_new HTML template:

  @login_required(login_url='/users/login/')
  def post_new(request):
    if request.method == 'POST':
      form = CreatePost(request.POST, request.FILES)
      if form.is_valid():
        new_post = form.save(commit=False)
        new_post.author = request.user
        new_post.save()
        return redirect('posts:list')
    else:
      form = CreatePost()
    return render(request, 'posts/post_new.html', {'form': form})

Updated the post_new template to show the new post form:

    <section>
      <h2>New Post</h2>
      <form action="{% url 'posts:new' %}" method="post" class="form-with-validation" enctype="multipart/form-data">
        {% csrf_token %}
        {{ form }}
        <button class="form-submit" type="submit">Add Post</button>
      </form>
    </section>

- Very important: Add _**enctype="multipart/form-data"**_ to form meta tag, so it can be used to correctly upload the selected image.

Updated the post_page and/or the post_list HTML template to show the author of the each post:

  <p>{{ post.date }} by {{ post.author }}</p>

alt text

alt text



Lesson 12 - Optional changes for a ready-for-production app:

Update of settings.py:

  • Remove import os

  • Update DEBUG variable to False value

  • Update ALLOWED_HOSTS variable with known hostnames, ex: 'localhost', '127.0.0.1'.

  • Add a STATIC_ROOT variable, update STATICFILES_DIRS variable and then add another new MEDIA_ROOT variable as follows:

    STATIC_URL = 'static/'
    MEDIA_URL = 'media/'
    
    STATIC_ROOT = BASE_DIR / 'assets'
    MEDIA_ROOT = BASE_DIR / 'media'
    STATICFILES_DIRS = [ BASE_DIR / 'static' ]
    

    Finally, execute:

      py manage.py collectstatic
    

    This will create a new 'assets' directory containing all static files from the hole project.

Update of urls.py main project file:

  • Add a new import:

    from django.views.static import serve
    
  • Add re_path import:

    from django.urls import path, include, re_path
    
  • Add two new patterns to urlpatterns list:

    urlpatterns = [
      re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
      re_path(r'^static/(?P<path>.*)$', serve, {'document_root': settings.STATIC_ROOT}),
      ... rest of paths...
    
  • Remove or comment the last urlpatterns line:

    #urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    

Deploy steps on common online services:

Replit deploy available (just clone and then click Run):

Replit deploy available