Nothing Special   »   [go: up one dir, main page]

Build and Submit HTML Forms With Django – Part 4

Build and Submit HTML Forms With Django – Part 4

by Martin Breuss intermediate django web-dev

In this four-part tutorial series, you’re building a social network with Django that you can showcase in your portfolio. This project is strengthening your understanding of relationships between Django models and showing you how to use forms so that users can interact with your app and with each other. You’re also making your site look good by using the Bulma CSS framework.

In the previous part of this series, you added functionality so that users can create dweets on the back end and display them on the front end. At this point, your users can discover and follow other users and read the content of the profiles they follow. They can click a button that sends an HTTP POST request handled by Django to unfollow a profile if they want to stop reading their content.

In the fourth part of this tutorial series, you’ll learn how to:

  • Create and render Django forms from your Dweet model
  • Prevent double submissions and display helpful error messages
  • Interlink pages of your app using dynamic URLs
  • Refactor a view function
  • Use QuerySet field lookups to filter your data on the back end

Once you finish going through this final part of the tutorial, you’ll have a fully functional basic social network built with Django. It’ll allow your users to create short text-based messages, discover and follow other users, and read the content of the profiles they follow. They’ll also be able to unfollow a profile if they want to stop reading their content.

Additionally, you’ll have showcased that you can use a CSS framework to make your web app look great without too much extra effort.

You can download the code that you’ll need to start the final part of this project by clicking the link below and going to the source_code_start/ folder:

Demo

In this four-part series, you’re building a small social network that allows users to post short text-based messages. The users of your app can also follow other user profiles to see the posts of these users or unfollow them to stop seeing their text-based posts:

You’re also learning how to use the CSS framework Bulma to give your app a user-friendly appearance and make it a portfolio project you can be proud to show off.

In the fourth and final part of this tutorial series, you’ll learn how to build Django forms on top of an existing model. You’ll also set up and handle more HTTP POST request submissions so that your users can post their text-based messages.

At the end of this tutorial, you’ll have completed your basic social network built with Django. By then, your users will be able to navigate to a profile list and to individual profile pages, follow and unfollow other users, and see the dweets of the profiles they follow displayed on their dashboard. They’ll also be able to submit dweets through a form on their dashboard.

Project Overview

In this section, you’ll get an overview of what topics you’ll cover in this final part of the tutorial series. You’ll also get a chance to revisit the full project implementation steps, in case you need to skip back to a previous step that you completed in an earlier part of the series.

At this point, you should have finished working through parts one, two, and three of this tutorial series. Congratulations! You’ve made your way to the final part, which focuses on building forms and handling form submissions:

Once you’ve implemented the steps of this last part of the series, you’ve completed the basic version of your Django social network. You’ll be ready to take any next steps by yourself to make this project stand out in your web developer portfolio.

To get a high-level idea of how the final part in this series on building your Django social network fits into the context of the whole project, you can expand the collapsible section below:

You’re implementing the project in a number of steps spread out over multiple separate tutorials in this series. There’s a lot to cover, and you’re going into details along the way:

✅ Part 1: Models and Relationships

  • Step 1: Set Up the Base Project
  • Step 2: Extend the Django User Model
  • Step 3: Implement a Post-Save Hook

✅ Part 2: Templates and Front-End Styling

  • Step 4: Create a Base Template With Bulma
  • Step 5: List All User Profiles
  • Step 6: Access Individual Profile Pages

✅ Part 3: Follows and Dweets

  • Step 7: Follow and Unfollow Other Profiles
  • Step 8: Create the Back-End Logic For Dweets
  • Step 9: Display Dweets on the Front End

📍 Part 4: Forms and Submissions

  • Step 10: Submit Dweets Through a Django Form
  • Step 11: Prevent Double Submissions and Handle Errors
  • Step 12: Improve the Front-End User Experience

Each of these steps will provide links to any necessary resources. By approaching the steps one at a time, you’ll have the opportunity to pause and come back at a later point in case you want to take a break.

With the high-level structure of this tutorial series in mind, you’ve got a good idea of where you’re at and which implementation steps you might have to catch up on, if you haven’t completed them yet.

Before getting started with the next step, take a quick look at the prerequistes to skim any links to other resources that might be helpful along the way.

Prerequisites

To successfully work through this final part of your project, you need to have completed the first part on models and relationships, the second part on templates and styling, and the third part on follows and dweets. Please confirm that your project works as described there. You should also be comfortable with the following concepts:

Make sure that you’ve completed the first three parts of this series. This final part will pick up right where you left off at the end of the third part.

You can also download the code that you’ll need for starting the final part of your project by clicking the link below and going to the source_code_start/ folder:

For additional requirements and further links, check out the prerequisites mentioned in the first part of this tutorial series on building a basic social network in Django.

Step 10: Submit Dweets Using Django Forms

For the sake of this tutorial series, you decided early on to handle user creation in your Django admin. Your tiny social network is invite-only, and you’re the one who decides to create user accounts.

However, once your users get into your social network app, you’ll want to give them the opportunity to create content. They won’t have access to the Django admin interface, and your Dwitter will be barren without any chance for users to create content. You’ll need another form as an interface for your users to submit dweets.

Create a Text Input Form

If you’re familiar with HTML forms, then you might know that you could handle the text submissions by creating another HTML <form> element with specific <input> elements. It would, however, have to look a bit different from the form that you built for your buttons.

In this tutorial, you’ll learn how to create HTML forms using a Django form. You’ll write a Django form, and Django will convert it to an HTML <form> element when rendering the page.

To start with this part of the tutorial, create a new file in your Django dwitter app, and call it forms.py. This file can hold all the forms you might need for your project. You’ll only need a single form so that your users can submit their dweets:

Python
 1# dwitter/forms.py
 2
 3from django import forms
 4from .models import Dweet
 5
 6class DweetForm(forms.ModelForm):
 7    body = forms.CharField(required=True)
 8
 9    class Meta:
10        model = Dweet
11        exclude = ("user", )

In this code snippet, you create DweetForm and inherit from Django’s ModelForm. Creating forms in this way relies heavily on abstractions set up by Django, which means that in this tutorial, you need to define very little by yourself to get a working form:

  • Lines 3 to 4: You import Django’s built-in forms module and the Dweet model that you created in a previous part of this tutorial series.

  • Line 6: You create a new class, DweetForm, that inherits from forms.ModelForm.

  • Line 7: You pass the field that you want the form to render, and you define its type. In this case, you want a character field to allow for text input. body is the only field, and you make it a required field so that there won’t be any empty dweets.

  • Line 9: You create a Meta options class in DweetForm. This options class allows you to pass any information that isn’t a field to your form class.

  • Line 10: You need to define which model ModelForm should take its information from. Because you want to make a form that allows users to create dweets, Dweet is the right choice here.

  • Line 11: By adding the name of the model field that you want to exclude to the exclude tuple, you ensure that Django will omit it when creating the form. Remember to add a comma (,) after "user" so that Python creates a tuple for you!

You want to make the dweet submissions as user-friendly as possible. Users can only dweet on your social network when they’re logged in, and they can only create dweets for themselves. Therefore, you don’t need to explicitly pass which user is sending a dweet inside the form.

The setup described in this tutorial holds all the information Django needs to create HTML forms that catch all the info you need on the front end. Time to take a look from that end.

Render the Form in Your Template

After creating DweetForm in forms.py, you can import it in your code logic and send the information to your dashboard template:

Python
# dwitter/views.py

from django.shortcuts import render
from .forms import DweetForm
from .models import Profile

def dashboard(request):
    form = DweetForm()
    return render(request, "dwitter/dashboard.html", {"form": form})

With these changes to views.py, you first imported DweetForm from forms.py. Then you created a new DweetForm instance, assigned it to form, and passed it to your dashboard template in your context dictionary under the key "form". This setup allows you to access and render your form in your template:

HTML
<!-- dwitter/templates/dwitter/dashboard.html -->

{% extends 'base.html' %}

{% block content %}

<div class="column">
    {% for followed in user.profile.follows.all %}
        {% for dweet in followed.user.dweets.all %}
            <div class="box">
                {{dweet.body}}
                <span class="is-small has-text-grey-light">
                    ({{ dweet.created_at }} by {{ dweet.user.username }}
                </span>
            </div>
        {% endfor %}
    {% endfor %}
</div>

<div class="column is-one-third">
    {{ form.as_p }}
</div>

{% endblock content %}

The HTML class that you’re assigning to the <div> element uses Bulma’s CSS rules to create a new column on your dashboard page. This extra column makes the page feel less crowded and separates the feed content from the form. You then render the Django form with {{ form.as_p }}. Indeed, an input box shows up:

Dashboard showing a plain input box with a label text

This setup shows a minimal display of your Django form. It only has one field, just like you defined in DweetForm. However, it doesn’t look good, the text field seems far too small, and there’s a label reading Body next to the input field. You didn’t ask for that!

You can improve the display of your Django form by adding customizations through a widget to forms.CharField in forms.py:

Python
 1# dwitter/forms.py
 2
 3from django import forms
 4from .models import Dweet
 5
 6class DweetForm(forms.ModelForm):
 7    body = forms.CharField(
 8        required=True,
 9        widget=forms.widgets.Textarea(
10            attrs={
11                "placeholder": "Dweet something...",
12                "class": "textarea is-success is-medium",
13            }
14        ),
15        label="",
16    )
17
18    class Meta:
19        model = Dweet
20        exclude = ("user", )

By adding a Django widget to CharField, you get to control a couple of aspects of how the HTML input element will get represented:

  • Line 9: In this line, you choose the type of input element that Django should use and set it to Textarea. The Textarea widget will render as an HTML <textarea> element, which offers more space for your users to enter their dweets.

  • Lines 10 to 13: You further customize Textarea with settings defined in attrs. These settings render to HTML attributes on your <textarea> element.

  • Line 11: You add placeholder text that will show up in the input box and go away once the user clicks on the form field to enter their dweet.

  • Line 12: You add the HTML class "textarea", which relates to a textarea CSS style rule defined by Bulma and will make your input box more attractive and better matched to the rest of your page. You also add two additional classes, is-success and is-medium, that outline the input field in green and increase the text size, respectively.

  • Line 15: You set label to an empty string (""), which removes the Body text that previously showed up due to a Django default setting that renders the name of a form field as its label.

With only a few customizations in Textarea, you made your input box fit much better into the existing style of your page:

Dashboard with Dweet Textarea but without a button

The input box looks good, but it’s not a functional form yet. Did anyone ask for a Submit button?

Make Form Submissions Possible

Django forms can take the hassle out of creating and styling your form fields. However, you still need to wrap your Django form into an HTML <form> element and add a button. To create a functional form that allows POST requests, you’ll also need to define the HTTP method accordingly:

HTML
 1<!-- dwitter/templates/dwitter/dashboard.html -->
 2
 3{% extends 'base.html' %}
 4
 5{% block content %}
 6
 7<div class="column">
 8    {% for followed in user.profile.follows.all %}
 9        {% for dweet in followed.user.dweets.all %}
10            <div class="box">
11                {{dweet.body}}
12                <span class="is-small has-text-grey-light">
13                    ({{ dweet.created_at }} by {{ dweet.user.username }}
14                </span>
15            </div>
16        {% endfor %}
17    {% endfor %}
18</div>
19
20<div class="column is-one-third">
21    <form method="post">
22        {% csrf_token %}
23        {{ form.as_p }}
24        <button class="button is-success is-fullwidth is-medium mt-5"
25                type="submit">Dweet
26        </button>
27    </form>
28</div>
29
30{% endblock content %}

With another incremental update to your HTML code, you completed the front-end setup of your dweet submission form:

  • Lines 21 and 27: You wrapped the form code into an HTML <form> element with method set to "post" because you want to send the user-submitted messages via a POST request.
  • Line 22: You added a CSRF token using the same template tag you used when creating the form for following and unfollowing profiles.
  • Lines 24 to 26: You completed the form by adding a button with some Bulma styling through the class attribute, which allows your users to submit the text they entered.

The form looks nice and seems to be ready to receive your input:

Dashboard showing an input Textarea box with a submit button

What happens when you click the Dweet button? Not much, because you haven’t set up any code logic to complement your front-end code yet. Your next step is to implement the submit functionality in views.py:

Python
 1# dwitter/views.py
 2
 3def dashboard(request):
 4    if request.method == "POST":
 5        form = DweetForm(request.POST)
 6        if form.is_valid():
 7            dweet = form.save(commit=False)
 8            dweet.user = request.user
 9            dweet.save()
10    form = DweetForm()
11    return render(request, "dwitter/dashboard.html", {"form": form})

With some additions to dashboard(), you make it possible for your view to handle the submitted data and create new dweets in your database:

  • Line 4: If a user submits the form with an HTTP POST request, then you want to handle that form data. If the view function was called due to an HTTP GET request, you’ll jump right over this whole code block into line 10 and render the page with an empty form in line 11.

  • Line 5: You fill DweetForm with the data that came in through the POST request. Based on your setup in forms.py, Django will pass the data to body. created_at will be filled automatically, and you explicitly excluded user, which will therefore stay empty for now.

  • Line 6: Django form objects have a method called .is_valid(), which compares the submitted data to the expected data defined in the form and the associated model restrictions. If all is well, the method returns True. You only allow your code to continue if the submitted form is valid.

  • Line 7: If your form already included all the information it needs to create a new database entry, then you could use .save() without any arguments. However, you’re still missing the required user entry to associate the dweet with. By adding commit=False, you prevent committing the entry to the database yet.

  • Line 8: You pick the currently logged-in user object from Django’s request object and save it to dweet, which you created in the previous line. In this way, you’ve added the missing information by building the association with the current user.

  • Line 9: Finally, your dweet has all the information it needs, so you can successfully create a new entry in the associated table. You can now write the information to your database with .save().

  • Line 10 to 11: Whether or not you’ve handled a POST submission, you always pass a new empty DweetForm instance to render(). This function call re-displays the page with a new blank form that’s ready for more of your thoughts.

With that, you’ve successfully created the text input form and hooked it up to your code logic, so the submissions will be handled correctly. In this part of the tutorial series, you also got to know Django forms. You rendered a form in your template, then applied Bulma styling to it by customizing attributes in a Textarea widget.

Before you’re ready to open up your Django social network to real-life users, there is, however, one issue you need to address. If you write a dweet and submit it now, it gets added all right, but if you reload the page after submitting, the same dweet will get added again!

Step 11: Prevent Double Submissions and Handle Errors

At this point, you can create new dweets through your app’s front end and view your own dweets together with the dweets of the profiles you follow on your dashboard. At the end of this step, you’ll have prevented double dweet submissions and learned how Django displays errors with the text input.

But first, you should get an idea of what the problem is. Go to your dashboard, write an inspiring dweet, and click on Dweet to submit it. You’ll see it show up in your list of displayed dweets in your timeline, and the dweet form will show up as empty again.

Without doing anything else, reload the page with a keyboard shortcut:

  • Cmd+R on macOS
  • Ctrl+R on Windows and Linux

Your browser might prompt you with a pop-up that asks whether you want to send the form again. If this message shows up, confirm by pressing Send. Now you’ll notice that the same dweet you sent before appears a second time on your dashboard. You can keep doing this as many times as you want to:

Dashboard showing the same Dweet many times because of the double-submission bug

After posting a dweet, Django sends another POST request with the same data and creates another entry in your database if you reload the page. You’ll see the dweet pop up a second time. And a third time. And a fourth time. Django will keep making duplicate dweets as often as you keep reloading. You don’t want that!

Prevent Double Submissions

To avoid double dweet submission, you’ll have to prevent your app from keeping the request data around, so that a reload won’t have the chance to resubmit the data. You can do just that by using a Django redirect:

Python
# dwitter/views.py

from django.shortcuts import render, redirect

# ...

def dashboard(request):
    if request.method == "POST":
        form = DweetForm(request.POST)
        if form.is_valid():
            dweet = form.save(commit=False)
            dweet.user = request.user
            dweet.save()
            return redirect("dwitter:dashboard")
    form = DweetForm()
    return render(request, "dwitter/dashboard.html", {"form": form})

By importing redirect() and returning a call to it after successfully adding a newly submitted dweet to your database, you send the user back to the same page. However, this time you’re sending a GET request when redirecting, which means that any number of page reloads will only ever show the dweets that already exist instead of creating an army of cloned dweets.

You set this up by referencing the app_name variable and the name keyword argument of a path(), which you defined in your URL configuration:

  • "dwitter" is the app_name variable that describes the namespace of your app. You can find it before the colon (:) in the string argument passed to redirect().
  • "dashboard" is the value of the name keyword argument for the path() entry that points to dashboard(). You need to add it after the colon (:) in the string argument passed to redirect().

To use redirect() as shown above, you need to set up the namespacing in dwitter/urls.py accordingly, which you did in a previous part of the tutorial series:

Python
# dwitter/urls.py

# ...

app_name = "dwitter"

urlpatterns = [
    path("", dashboard, name="dashboard"),

    # ...

With urls.py set up as shown above, you can use redirect() to point your users back to their dashboard page with a GET request after successfully processing the POST request from their form submission.

After you return redirect() at the end of your conditional statement, any reloads only load the page without resubmitting the form. Your users can now safely submit short dweets without unexpected results. However, what happens when a dweet goes beyond the 140 character limit?

Try typing a long dweet that goes over the 140 character limit and submit it. What happens? Nothing! But there’s also no error message, so your users might not even know that they did something wrong.

Additionally, the text you entered is gone, a major annoyance in poorly designed user forms. So you might want to make this experience better for your users by notifying them about what they did wrong and keeping the text they entered!

Handle Submission Errors

You defined in your models that your text-based messages can have a maximum length of 140 characters, and you’re enforcing this when users submit their text. However, you’re not telling them when they exceed the character limit. When they submit a dweet that’s too long, their input is lost.

The good news is that you can use Django forms rendered with {{ form.as_p }} to display error messages that get sent along with the form object without needing to add any code. These error messages can improve the user experience significantly.

But currently, you can’t see any error messages, so why is that? Take another look at dashboard():

Python
 1# dwitter/views.py
 2
 3# ...
 4
 5def dashboard(request):
 6    if request.method == "POST":
 7        form = DweetForm(request.POST)
 8        if form.is_valid():
 9            dweet = form.save(commit=False)
10            dweet.user = request.user
11            dweet.save()
12            return redirect("dwitter:dashboard")
13    form = DweetForm()
14    return render(request, "dwitter/dashboard.html", {"form": form})

In the highlighted lines, you can see that you’re creating one of two different DweetForm objects, either a bound or an unbound form:

  1. Line 7: If your function gets called from a POST request, you instantiate DweetForm with the data that came along with the request. Django creates a bound form that has access to data and can get validated.
  2. Line 13: If your page gets called with a GET request, you’re instantiating an unbound form that doesn’t have any data associated with it.

This setup worked fine and made sense up to now. You want to display an empty form if a user accesses the page by navigating there, and you want to validate and handle the submitted data in your form if a user writes a dweet and sends it to the database.

However, the crux is in the details here. You can—and should—validate the bound form, which you do in line 8. If the validation passes, the dweet gets written to your database. However, if a user adds too many characters, then your form validation fails, and the code in your conditional statement doesn’t get executed.

Python jumps execution to line 13, where you overwrite form with an empty unbound DweetForm object. This form is what gets sent to your template and rendered. Since you overwrote the bound form that held the information about the validation error with an unbound form, Django won’t display any of the validation errors that occurred.

To send the bound form to the template if a validation error occurs, you need to change your code slightly:

Python
# dwitter/views.py

# ...

def dashboard(request):
    form = DweetForm(request.POST or None)
    if request.method == "POST":
        if form.is_valid():
            dweet = form.save(commit=False)
            dweet.user = request.user
            dweet.save()
            return redirect("dwitter:dashboard")
    return render(request, "dwitter/dashboard.html", {"form": form})

With this change, you removed the duplicate instantiation of DweetForm so that there’s only ever one form that’ll get passed to your template, whether the user submitted a valid form or not.

The syntax that you used for this change might look unfamiliar. So here’s what’s going on:

  • POST request: If you call dashboard() with a POST request that includes any data, the request.POST QueryDict will contain your form submission data. The request.POST object now has a truthy value, and Python will short-circuit the or operator to return the value of request.POST. This way, you’ll pass the form content as an argument when instantiating DweetForm, as you did previously with form = DweetForm(request.POST).

  • GET request: If you call dashboard() with a GET request, request.POST will be empty, which is a falsy value. Python will continue evaluating the or expression and return the second value, None. Therefore, Django will instantiate DweetForm as an unbound form object, like you previously did with form = DweetForm().

The advantage of this setup is that you now pass the bound form to your template even when the form validation fails, which allows Django’s {{ form.as_p }} to render a descriptive error message for your users out of the box:

Dashboard showing a form error message sent by Django when attempting to submit a Dweet that exceeds the character limit

After submitting text that exceeds the character limit that you defined in Dweet, your users will see a descriptive error message pop up right above the form input field. This message gives them feedback that their dweet hasn’t been submitted, provides information about why that happened, and even gives information about how many characters their current text has.

The best thing about this change is that you’re passing the bound form object that retains the text data that your user entered in the form. No data is lost, and they can use the helpful suggestions to edit their dweet and submit it to the database successfully.

Step 12: Improve the Front-End User Experience

At this point, you have a functional social media app that you built with the Django web framework. Your users can post text-based messages, follow and unfollow other user profiles, and see dweets on their dashboard view. At the end of this step, you’ll have improved your app’s user experience by adding additional navigation links and sorting the dweets to display the newest dweets first.

Improve the Navigation

Your social network has three different pages that your users might want to visit at different times:

  1. The empty URL path (/) points to the dashboard page.
  2. The /profile_list URL path points to the list of profiles.
  3. The /profile/<int> URL path points to a specific user’s profile page.

Your users can already access all of these pages through their respective URL slugs. However, while your users can, for example, access a profile page by clicking on the username card from the list of all profiles, there’s currently no straightforward navigation to access the profile list or the dashboard page. It’s time to add some more links so that users can conveniently move between the different pages of your web app.

Head back to your templates folder and open dashboard.html. Add two buttons above the dweet form to allow your users to navigate to different pages in your app:

  1. The profile list page
  2. Their personal profile page

You can use the dynamic URL pattern with Django’s {% url %} tags that you’ve used before:

HTML
<!-- dwitter/templates/dwitter/dashboard.html -->

<!-- ... -->

<div class="block">
    <a href="{% url 'dwitter:profile_list' %} ">
        <button class="button is-dark is-outlined is-fullwidth">
            All Profiles
        </button>
    </a>
</div>
<div class="block">
    <a href="{% url 'dwitter:profile' request.user.profile.id %} ">
        <button class="button is-success is-light is-outlined is-fullwidth">
            My Profile
        </button>
    </a>
</div>

<!-- ... -->

You can add this code as the first two elements inside <div class="column is-one-third">. You can also add a heading just above your dweet form to explain more clearly what the form is for:

HTML
<!-- dwitter/templates/dwitter/dashboard.html -->

<!-- ... -->

<div class="block">
    <div class="block">
        <h2 class="title is-2">Add a Dweet</p>
    </div>
    <div class="block">
        <form method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button class="button is-success is-fullwidth is-medium mt-5"
                    type="submit">Dweet
            </button>
        </form>
    </div>
</div>

<!-- ... -->

With these two additions, you used the "block" class to arrange the three <div> elements on top of one another, and you added sensible navigation buttons that enhance the user experience on your dashboard page:

Finished Dashboard page that shows followed dweets on the left, and a Dweet form with navigation buttons on the right.

After adding all these changes, your dashboard template will be complete. You can compare the code that you wrote to the template below:

HTML
<!-- dwitter/templates/dwitter/dashboard.html -->

{% extends 'base.html' %}

{% block content %}

<div class="column">
    {% for followed in user.profile.follows.all %}
        {% for dweet in followed.user.dweets.all %}
            <div class="box">
                {{dweet.body}}
                <span class="is-small has-text-grey-light">
                    ({{ dweet.created_at }} by {{ dweet.user.username }}
                </span>
            </div>
        {% endfor %}
    {% endfor %}
</div>

<div class="column is-one-third">

    <div class="block">
        <a href="{% url 'dwitter:profile_list' %} ">
            <button class="button is-dark is-outlined is-fullwidth">
                All Profiles
            </button>
        </a>
    </div>

    <div class="block">
        <a href="{% url 'dwitter:profile' request.user.profile.id %} ">
            <button class="button is-success is-light is-outlined is-fullwidth">
                My Profile
            </button>
        </a>
    </div>

    <div class="block">
        <div class="block">
            <h2 class="title is-2">Add a Dweet</p>
        </div>
        <div class="block">
            <form method="post">
                {% csrf_token %}
                {{ form.as_p }}
                <button class="button is-success is-fullwidth is-medium mt-5"
                        type="submit">Dweet
                </button>
            </form>
        </div>
    </div>

</div>

{% endblock content %}

Your dashboard page is functional and looks great! That’s important because it’ll likely be the page where your users will spend most of their time when they interact with your social network. Therefore, you should also give your users ample possibilities to get back to the dashboard page after they’ve navigated, for example, to their profile page.

To make this possible, you can add a link to the dashboard page right at the top of all of your pages by adding it to the app header that you wrote in base.html:

HTML
<!-- templates/base.html -->

<!-- ... -->

<a href="{% url 'dwitter:dashboard' %} ">
<section class="hero is-small is-success mb-4">
    <div class="hero-body">
        <h1 class="title is-1">Dwitter</h1>
        <p class="subtitle is-4">
            Your tiny social network built with Django
        </p>
    </div>
</section>
</a>

<!-- ... -->

By wrapping the HTML <section> element in a link element, you made the whole hero clickable and gave your users a quick way to return to their dashboard page from anywhere in the app.

With these updated links, you’ve improved the user experience of your app significantly. Finally, if your users want to stay up to date with what their network is dweeting, you’ll want to change the dweet display to show the newest dweets first, independent of who wrote the text.

Sort the Dweets

There are a couple of ways that you could sort the dweets, and a few places where you could do that, namely:

  1. In your model
  2. In your view function
  3. In your template

Up to now, you’ve built quite a bit of your code logic inside of your dashboard template. But there’s a reason for the separation of concerns. As you’ll learn below, you should handle most of your app’s code logic in the views.

If you wanted to sort the dweets to display the newest dweet first, independent of who wrote the dweet, you might scratch your head about how to do this with the nested for loop syntax that you’re currently using in your dashboard template.

Do you know why this might get difficult? Head over to dashboard.html and inspect the current setup:

HTML
{% for followed in user.profile.follows.all %}
    {% for dweet in followed.user.dweets.all %}
        <div class="box">
            {{dweet.body}}
            <span class="is-small has-text-grey-light">
                ({{ dweet.created_at }} by {{ dweet.user.username }})
            </span>
        </div>
    {% endfor %}
{% endfor %}

How would you try to approach the sorting with this setup? Where do you think you might run into difficulties, and why? Take a moment and pull out your pencil and your notebook. Liberally make use of your preferred search engine and see if you can come up with a solution or explain why this might be challenging to solve.

Instead of handling so much of your code logic in your template, it’s a better idea to do this right inside dashboard() and pass the ordered result to your template for display.

So far, you’ve used view functions that handle only the form submission and otherwise define which template to render. You didn’t write any additional logic to determine which data to fetch from the database.

In your view functions, you can use Django ORM calls with modifiers to get precisely the dweets you’re looking for.

You’ll fetch all the dweets from all the profiles that a user follows right inside your view function. Then you’ll sort them by date and time and pass a new sorted iterable named dweet to your template. You’ll use this iterable to display all these dweets in a timeline ordered from newest to oldest:

Python
 1# dwitter/views.py
 2
 3from django.shortcuts import render, redirect
 4from .forms import DweetForm
 5from .models import Dweet, Profile
 6
 7def dashboard(request):
 8    form = DweetForm(request.POST or None)
 9    if request.method == "POST":
10        if form.is_valid():
11            dweet = form.save(commit=False)
12            dweet.user = request.user
13            dweet.save()
14            return redirect("dwitter:dashboard")
15
16    followed_dweets = Dweet.objects.filter(
17        user__profile__in=request.user.profile.follows.all()
18    ).order_by("-created_at")
19
20    return render(
21        request,
22        "dwitter/dashboard.html",
23        {"form": form, "dweets": followed_dweets},
24    )

In this update to dashboard(), you make a couple of changes that deserve further attention:

  • Line 5: You add an import for the Dweet model. Until now, you didn’t need to address any dweet objects in your views because you were handling them in your template. Since you want to filter them now, you need access to your model.

  • Line 16: In this line, you use .filter() on Dweet.objects, which allows you to pick particular dweet objects from the table depending on field lookups. You save the output of this call to followed_dweets.

  • Line 17 (keyword): First, you define the queryset field lookup, which is Django ORM syntax for the main part of an SQL WHERE clause. You can follow through database relations with a double-underscore syntax (__) specific to Django ORM. You write user__profile__in to access the profile of a user and see whether that profile is in a collection that you’ll pass as the value to your field lookup keyword argument.

  • Line 17 (value): In the second part of this line, you provide the second part of the field lookup. This part needs to be a QuerySet object containing profile objects. You can fetch the relevant profiles from your database by accessing all profile objects in .follows of the currently logged-in user’s profile (request.user.profile).

  • Line 18: In this line, you chain another method call to the result of your database query and declare that Django should sort the dweets in descending order of created_at.

  • Line 23: Finally, you add a new entry to your context dictionary, where you pass followed_dweets. The followed_dweets variable contains a QuerySet object of all the dweets of all the profiles the current user follows, ordered by the newest dweet first. You’re passing it to your template under the key name dweets.

You can now update the template in dashboard.html to reflect these changes and reduce the amount of code logic that you need to write in your template, effectively getting rid of your nested for loop:

HTML
<!-- dwitter/templates/dwitter/dashboard.html -->

<!-- ... -->

{% for dweet in dweets %}
    <div class="box">
        {{dweet.body}}
        <span class="is-small has-text-grey-light">
            ({{ dweet.created_at }} by {{ dweet.user.username }}
        </span>
    </div>
{% endfor %}

<!-- ... -->

You’ve made the pre-selected and pre-sorted dweets available to your template under the name dweets. Now you can iterate over that QuerySet object with a single for loop and access the dweet attributes without needing to step through any model relationships in your template.

Go ahead and reload the page after making this change. You can now see all the dweets of all the users you follow, sorted with the newest dweets up top. If you add a new dweet while following your own account, it’ll appear right at the top of this list:

Dashboard dweets showing the newest dweets at the top

This change completes the final updates you need to make so that your Django social media app provides a user-friendly experience. You can now declare your Django social media app feature-complete and start inviting users.

Conclusion

In this tutorial, you built a small social network using Django. Your app users can follow and unfollow other user profiles, post short text-based messages, and view the messages of other profiles they follow.

In the process of building this project, you’ve learned how to:

  • Build a Django project from start to finish
  • Implement OneToOne and ForeignKey relationships between Django models
  • Extend the Django user model with a custom Profile model
  • Customize the Django admin interface
  • Integrate Bulma CSS to style your app

You’ve covered a lot of ground in this tutorial and built an app that you can share with your friends and family. You can also display it as a portfolio project for potential employers.

You can download the final code for this project by clicking the link below and going to the source_code_final/ folder:

Next Steps

If you’ve already created a portfolio site, add your project there to showcase your work. You can keep improving your Django social network to add functionality and make it even more impressive.

Here are some ideas to take your project to the next level:

What other ideas can you come up with to extend this project? Share your project links and ideas for further development in the comments below!

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Martin Breuss

Martin likes automation, goofy jokes, and snakes, all of which fit into the Python community. He enjoys learning and exploring and is up for talking about it, too. He writes and records content for Real Python and CodingNomads.

» More about Martin

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!