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:
Get Source Code: Click here to get the source code you’ll use to build and submit HTML forms with Django.
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:
Step 10 | Submit Dweets Through a Django Form |
Step 11 | Prevent Double Submissions and Handle Errors |
Step 12 | Improve the Front-End User Experience |
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
- 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:
- Using object-oriented programming in Python
- Setting up a basic Django project
- Managing routing and redirects, view functions, templates, models, and migrations in Django
- Using and customizing the Django Admin interface
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.
Note: You won’t be able to follow this part of the tutorial series if you don’t have the working project from the previous parts ready to go.
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:
Get Source Code: Click here to get the source code you’ll use to build and submit HTML forms with Django.
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.
Note: Feel free to expand on this with Django’s user management system and build the necessary templates by following the linked tutorial.
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:
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 theDweet
model that you created in a previous part of this tutorial series. -
Line 6: You create a new class,
DweetForm
, that inherits fromforms.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 inDweetForm
. 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.
Note: Associating a dweet to a user is necessary, but you’ll handle that in the back end instead.
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:
# 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:
<!-- 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:
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
:
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
. TheTextarea
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 inattrs
. 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
andis-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:
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:
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 withmethod
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:
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
:
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 informs.py
, Django will pass the data tobody
.created_at
will be filled automatically, and you explicitly excludeduser
, 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 returnsTrue
. 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 requireduser
entry to associate the dweet with. By addingcommit=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 todweet
, 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 torender()
. 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:
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:
# 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 theapp_name
variable that describes the namespace of your app. You can find it before the colon (:
) in the string argument passed toredirect()
."dashboard"
is the value of thename
keyword argument for thepath()
entry that points todashboard()
. You need to add it after the colon (:
) in the string argument passed toredirect()
.
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:
# 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()
:
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:
- 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. - 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:
# 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.
Note: Python’s Boolean or
operator is a short-circuit operator. This means that it only evaluates the second argument if the first one is False
, or falsy.
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, therequest.POST
QueryDict
will contain your form submission data. Therequest.POST
object now has a truthy value, and Python will short-circuit theor
operator to return the value ofrequest.POST
. This way, you’ll pass the form content as an argument when instantiatingDweetForm
, as you did previously withform = 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 theor
expression and return the second value,None
. Therefore, Django will instantiateDweetForm
as an unbound form object, like you previously did withform = 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:
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.
Note: You didn’t have to add any HTML to your template to make this change. Django knows how to render form submission errors when they get sent along in a bound form object inside the {{ form.is_p }} tag.
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:
- The empty URL path (
/
) points to the dashboard page. - The
/profile_list
URL path points to the list of profiles. - 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:
- The profile list page
- Their personal profile page
You can use the dynamic URL pattern with Django’s {% url %} tags that you’ve used before:
<!-- 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:
<!-- 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:
After adding all these changes, your dashboard template will be complete. You can compare the code that you wrote to the template below:
<!-- 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
:
<!-- 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:
- In your model
- In your view function
- 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:
{% 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:
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()
onDweet.objects
, which allows you to pick particular dweet objects from the table depending on field lookups. You save the output of this call tofollowed_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 writeuser__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
. Thefollowed_dweets
variable contains aQuerySet
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 namedweets
.
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:
<!-- 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:
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
andForeignKey
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:
Get Source Code: Click here to get the source code you’ll use to build and submit HTML forms with Django.
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:
- Implement User Authentication: Allow new users to sign up through the front end of your web app by following the steps outlined in Get Started With Django Part 2: Django User Management.
- Deploy Your Dwitter Project: Put your web app online for the whole world to see by hosting your Django project on Heroku.
- Get Social: Invite your friends to join your Django social network, and start dweeting your thoughts to one another.
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!