Building a Social Media App With Django and Python Part 14: Direct Messages Pt. 1

LegionScript
6 min readApr 26, 2021

--

Video Tutorial

In this tutorial we are going to start to add direct messages between users. In this part we will get it working with it’s basic functionality and then we will come back and add some extra features to it next. This will not be a real time chat, to do that would be more complicated and would require something like Django channels. If there is any interest in that, I could come back later and update it to include that. However for now, we won’t do that. We will need to make quite a few changes to get this working, let’s start with out models.py.

Updating the Models.py

We will need two new models for this, we will need a ThreadModel which will hold all messages between two users. Then we will have a MessageModel that will hold the data for the individual message. On the thread model, we will need to hold the user, the receiving user, and a boolean for if there are any unread messages. For the message model, we will hold the thread its attached to, the sending user, the receiving user, the body, the image, the date it was sent, and a boolean for if its read.

class ThreadModel(models.Model):  user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+')  receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+')  has_unread = models.BooleanField(default=False)
class MessageModel(models.Model):
thread = models.ForeignKey('ThreadModel', related_name='+', on_delete=models.CASCADE, blank=True, null=True) sender_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+') receiver_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+') body = models.CharField(max_length=1000) image = models.ImageField(upload_to='', blank=True, null=True) date = models.DateTimeField(default=timezone.now) is_read = models.BooleanField(default=False)

Next we need to create a couple forms to handle creating a new thread or message.

Updating the Forms.py

We will need two forms, one will create a thread, where we need to input a username of the person we want to talk to. We also need a message thread that will have a text box for the message. Let’s add both of these two our social/forms.py file.

class ThreadForm(forms.Form):  username = forms.CharField(label='', max_length=100)class MessageForm(forms.Form):  message = forms.CharField(label='', max_length=1000)

Next we need to create a couple views that use these forms and hold the logic for the functionality of our dm system.

Updating the Views.py

We are going to need a couple views, a CreateThread, a ListThreads (our inbox), a CreateMessage, and a ThreadView. These will handle all of the basic functionality for the direct messages. Let’s go through them one by one.

The CreateThread view will have two methods on it, one will be a get method that will display the form to enter a user name. There will also be a post method to handle creating the thread. To create a thread we want to get what was entered in the form and see if there is a user that matches that username. If there is not we will stop here. If there is a user, we will check to see if there is a thread between these users already, if there is we won’t create one and we will just open that thread up. And finally, if there is no thread we will create one and redirect to that new thread.

class CreateThread(View):  def get(self, request, *args, **kwargs):    form = ThreadForm()    context = {      'form': form    }    return render(request, 'social/create_thread.html', context)  def post(self, request, *args, **kwargs):    form = ThreadForm(request.POST)    username = request.POST.get('username')    try:      receiver = User.objects.get(username=username)      if ThreadModel.objects.filter(user=request.user, receiver=receiver).exists():        thread = ThreadModel.objects.filter(user=request.user, receiver=receiver)[0]        return redirect('thread', pk=thread.pk)

if form.is_valid():
sender_thread = ThreadModel( user=request.user, receiver=receiver ) sender_thread.save() thread_pk = sender_thread.pk return redirect('thread', pk=thread_pk) except: return redirect('create-thread')

Next, we need a ListThreads view. This will act as an inbox, where we can see all of our conversations. All we need here is a get method that will get all threads where the logged in user is either the sending user or receiving user.

class ListThreads(View):  def get(self, request, *args, **kwargs):  threads = ThreadModel.objects.filter(Q(user=request.user) | Q(receiver=request.user))  context = {    'threads': threads  }  return render(request, 'social/inbox.html', context)

Next we need a CreateMessage view. This will need one method, a post method to create a message. We don’t need a get method because our form will be displayed in our ThreadView which will show the conversation. We just need to send a post request to this view to create the message and then redirect back to the ThreadView.

class CreateMessage(View):  def post(self, request, pk, *args, **kwargs):    thread = ThreadModel.objects.get(pk=pk)    if thread.receiver == request.user:      receiver = thread.user    else:      receiver = thread.receiver      message = MessageModel(        thread=thread,        sender_user=request.user,        receiver_user=receiver,        body=request.POST.get('message'),      )      message.save()      return redirect('thread', pk=pk)

Finally, we need a ThreadView that will show all messages in a thread and it will display a form at the bottom to send a new message.

class ThreadView(View):  def get(self, request, pk, *args, **kwargs):    form = MessageForm()    thread = ThreadModel.objects.get(pk=pk)    message_list = MessageModel.objects.filter(thread__pk__contains=pk)    context = {      'thread': thread,      'form': form,      'message_list': message_list    }    return render(request, 'social/thread.html', context)

Now that we have the views created, we need to create our html templates for each of them.

Creating the HTML Templates

First we need the create thread with the form.

{% extends 'landing/base.html' %}{% load crispy_forms_tags %}{% block content %}<div class="container">  <div class="row mt-5">    <div class="col-md-3 col-sm-6">      <a href="#" class="btn btn-light">Back to Your Inbox</a>    </div>  </div>  <div class="row justify-content-center mt-5">    <div class="col-md-5 col-sm-12">      <h5>Start a Conversation!</h5>    </div>  </div>  <div class="row justify-content-center mt-3 mb-5">    <div class="col-md-5 col-sm-12">      <form method="POST">        {% csrf_token %}        <p>Enter the username for the person you would like to talk to.</p>        {{ form | crispy }}        <div class="d-grid gap-2">        <button type="submit" class="btn btn-success mt-3">Continue</button>        </div>      </form>    </div>  </div></div>{% endblock content %}

Next we need the ListThreads template which we will call inbox.html

{% extends 'landing/base.html' %}{% load crispy_forms_tags %}{% block content %}<div class="container">  <div class="row">    <div class="col-md-12 p-5">      <h3>Your Conversations</h3>    </div>  </div>  <div class="row">    <div class="col-md-12 p-5">      <form method="GET" action="{% url 'create-thread' %}">        <button class="btn btn-light p-3" type="submit">Start a Conversation</button>      </form>    </div>  </div>  {% for thread in threads.all %}  <div class="row mb-3">    <div class="card col-md-12 p-5 shadow-sm">      <h5>{{ thread.receiver }}</h5>      <a class="stretched-link" href="{% url 'thread' thread.pk %}"></a>    </div>  </div>  {% endfor %}</div>{% endblock content %}

We don’t need a template for the CreateMessage view so the last view we need to create a template for is the ThreadView. This one will be a little more complicated. We need to put the messages on the left or right of the screen based on who sent them, if the logged in user is the user who sent the message, that message will show up on the left, otherwise it will show up on the right. Then at the bottom we will show our create message form.

{% extends 'landing/base.html' %}{% load crispy_forms_tags %}{% block content %}  <div class="container">    <div class="row">      <div class="card col-md-12 mt-5 p-3 shadow-sm">        <h5>@{{ thread.receiver }}</h5>      </div>    </div>    {% if message_list.all.count == 0 %}    <div class="row my-5">      <div class="col-md-12">        <p class="empty-text">No Messages</p>      </div>    </div>    {% endif %}    {% for message in message_list %}    <div class="row">      <div class="col-md-12 my-1">        {% if message.sender_user == request.user %}      <div class="sent-message my-3">        <p>{{ message.body }}</p>      </div>    {% elif message.receiver_user == request.user%}    <div class="received-message my-3">      <p>{{ message.body }}</p>    </div>    {% endif %}    </div>  </div>  {% endfor %}  <div class="row">    <div class="card col-md-12 p-3 shadow-sm">      <form method="POST" action="{% url 'create-message' thread.pk %}" enctype="multipart/form-data">        {% csrf_token %}        {{ form | crispy }}        <div class="d-grid gap-2 mt-3">          <button class="btn btn-light" type="submit">Send Message</button>        </div>      </form>    </div>  </div></div>{% endblock content %}

Next we need to update our urls.py

Updating Urls.py

Nothing new or special here, we just need url paths for each of our views.

path('inbox/', ListThreads.as_view(), name='inbox'),path('inbox/create-thread', CreateThread.as_view(), name='create-thread'),path('inbox/<int:pk>/', ThreadView.as_view(), name='thread'),path('inbox/<int:pk>/create-message/', CreateMessage.as_view(), name='create-message')

Adding the Inbox Link to the Navbar

Let’s add a link to the navbar to get to our inbox.

<div class="nav-item inbox-icon-container">  <a href="{% url 'inbox' %}" class="inbox-icon"><i class="far fa-paper-plane"></i></a></div>

Adding Some CSS Styles

The final step is to add some CSS styles to make everything look better, we already added them to our templates so let’s just add them to our style.css now.

.inbox-icon-container {  margin-right: 15px;}.inbox-icon {  color: #333;  transition: 0.5s;}.inbox-icon:hover {  transition: 0.5s;}.sent-message {  background-color: #f3f3f3;  border-radius: 30px;  padding: 10px 25px;  width: 25%}.received-message {  background-color: rgb(1, 196, 255);  color: #fff;  border-radius: 30px;  padding: 10px 25px;  width: 25%;  float: right;}.empty-text {  color: #777;  font-size: 1.5rem;  text-align: center;}

This is where we will stop for today, next we will come back and add some extra features like notifications, image uploads, and unread alerts.

--

--

No responses yet