It’s time to build out our social messaging app. Much of the code here will be very similar to what we used earlier for our blog app since we’re implementing the same CRUD (Create-Read-Update-Delete) functionality. The main difference will be that our messages will have a Twitter-like 280 character limit and, in the next chapter, allow comments from other logged-in users too.

Complete source code can be found on Github.

Posts app

We’ll start off by creating a new app and defining our database models. There are no hard and fast rules around what to name your apps except that you can’t use the name of a built-in app like messages. However a general rule of thumb is to use the plural of an app name–so posts, payments, users–unless doing so is obviously wrong as in the common case of blog.

I want to emphasize Django CRUD patterns so we will call this new app the generic posts just as we did in the blog app although the model itself will differ slightly.

Start by creating our new posts app.

(msg) $ python manage.py startapp posts

Then add it to our INSTALLED_APPS and update the time zone since we’ll be timestamping our posts. You can find your time zone in this Wikipedia list. For example, I live in Boston, MA which is in the Eastern time zone of the United States. Therefore my entry is America/New_York.

# msg_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'crispy_forms',
    'users',
    'pages',
    'posts', # new
]

TIME_ZONE = 'America/New_York'

Next up we define our database model which contains only three fields: message, date, and author. Note that we’re setting a max_length of 280 characters for the message and letting Django automatically set the time and date based on our TIME_ZONE setting. For the author field we want to reference our custom user model 'users.CustomUser' which we set in the settings.py file as AUTHO_USER_MODEL. Therefore if we import settings we can refer to it as settings.AUTH_USER_MODEL.

We also implement the best practices of defining a get_absolute_url from the beginning and a __str__ method for viewing the model in our admin interface.

# posts/models.py
from django.conf import settings
from django.db import models
from django.urls import reverse


class Post(models.Model):
    message = models.CharField(max_length=280)
    date = models.DateTimeField(auto_now_add=True)
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )

    def get_absolute_url(self):
        return reverse('post_detail', args=[str(self.id)])

    def __str__(self):
        return self.message

Since we have a brand new app and a new model, it’s time to make a new migration file and then apply it to the database.

(msg) $ python manage.py makemigrations posts
(msg) $ python manage.py migrate posts

At this point I like to jump into the admin to play around with the model before building out the urls/views/templates needed to actually display the data on the website. But first we need to update admin.py so our new app is displayed.

# posts/admin.py
from django.contrib import admin

from . import models

admin.site.register(models.Post)

Now we start the server.

(msg) $ python manage.py runserver

Navigate to http://127.0.0.1:8000/admin/ and log in.

Admin page

If you click on “Posts” we can enter in some sample data.

Admin posts add page

I’ve added three new posts as you can see here. You’ll likely have two users available at this point: your superuser and testuser accounts. Use your superuser account as the author of all three posts.

Admin three posts

If you click on an individual post you’ll see that the message and author are displayed but not the date. That’s because the date was automatically added by Django for us and therefore can’t be changed. We could make the date editable–in more complex apps it’s common to have both a created_at and updated_at field but to keep things simple we’ll just use date for now. Even though date is not displayed here we will still be able to output it on our website.

Admin posts detail

URLs and Views

The next step is to configure our URLs and views. Let’s have our messages appear at posts/ so we’ll include the posts app in our project-level urls.py file.

# msg_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('pages.urls')),
    path('posts/', include('posts.urls')), # new!
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
    path('users/', include('django.contrib.auth.urls')),
]

Next we create a posts/urls.py file.

(msg) $ touch posts/urls.py

Then populate it with our routes. Let’s start with the page to list all posts at /posts/ which will use the view PostListView.

# posts/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.PostListView.as_view(), name='post_list'),
]

Now create our view using the built-in generic ListView from Django.

# posts/views.py
from django.views.generic import ListView

from . import models


class PostListView(ListView):
    model = models.Post
    template_name = 'post_list.html'

The only two fields we need to specify are the model Post and our template name which will be post_list.html.

Only one step left which is to create our template. We can make an empty file from the command line.

(msg) $ touch templates/post_list.html

Bootstrap has a built-in component called Cards that we can customize for our individual posts. Recall that ListView returns an object called object_list which we can iterate over using a for loop.

Within each post we display the author, date, and message. We can even provide links to “edit” and “delete” functionality that we haven’t built yet.

<!-- templates/post_list.html -->
{% extends 'base.html' %}

{% block title %}Posts{% endblock %}

{% block content %}
  {% for post in object_list %}
    <div class="card">
      <div class="card-header">
        <span class="font-weight-bold">{{ post.author }}</span> &middot; <span class="text-muted">{{ post.date }}</span>
      </div>
      <div class="card-body">
        {{ post.message }}
      </div>
      <div class="card-footer text-center text-muted">
        <a href="#">Edit</a> | <a href="#">Delete</a>
      </div>
    </div>
    <br />
  {% endfor %}
{% endblock content %}

Spin up the server with python manage.py runserver and check out our page at http://127.0.0.1:8000/posts/.

Posts page

Not bad eh? If we wanted to get fancy we could create a custom template filter so that the date outputted is shown in seconds, minutes, or days. This can be done with some if/else logic and Django’s date options but we won’t implement it here.

Edit/Delete

How do we add edit and delete options? We need new urls, views, and templates. Let’s start with the urls. We can make use of the fact that Django automatically adds a primary key to each database. Therefore our first post with a primary key of 1 will be at posts/1/edit/ and the delete route will be at posts/1/delete/.

# posts/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.PostListView.as_view(), name='posts'),
    path('<int:pk>/edit/',
         views.PostUpdateView.as_view(), name='post_edit'),  # new
    path('<int:pk>/',
         views.PostDetailView.as_view(), name='post_detail'),  # new
    path('<int:pk>/delete/',
         views.PostDeleteView.as_view(), name='post_delete'),  # new
]

Now write up our views which will use Django’s generic class-based views for DetailView, UpdateView and DeleteView. We specify which fields can be updated–only message–and where to redirect the user after deleting a post: post_list.

# posts/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import UpdateView, DeleteView
from django.urls import reverse_lazy

from . import models


class PostListView(ListView):
    model = models.Post
    template_name = 'post_list.html'

class PostDetailView(DetailView):
    model = models.Post
    template_name = 'post_detail.html'

class PostUpdateView(UpdateView):
    model = models.Post
    fields = ['message']
    template_name = 'post_edit.html'

class PostDeleteView(DeleteView):
    model = models.Post
    template_name = 'post_delete.html'
    success_url = reverse_lazy('posts')

Finally we need to add our new templates. Stop the server with Control+c and type the following.

(msg) $ touch templates/post_detail.html
(msg) $ touch templates/post_edit.html
(msg) $ touch templates/post_delete.html

We’ll start with the details page which will display the message, date, links to edit and delete, and also a link back to all posts. Recall that the Django templating language’s url tag wants the URL name and then any arguments passed in. The name of our edit route is post_edit and we need to pass in its primary key post.pk. The delete route name is post_delete and it also needs a primary key post.pk. Our posts page is a ListView so it does not need any additional arguments passed in.

<!-- templates/post_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="post-entry">
  <h2>{{ object.message }}</h2>
    <p>Author: {{ object.author }}</p>
    <p>{{ object.date }}</p>
  </div>

  <p><a href="{% url 'post_edit' post.pk %}">Edit</a> | <a href="{% url 'post_delete' post.pk %}">Delete</a></p>
  <p>Back to <a href="{% url 'posts' %}">All Messages</a>.</p>
{% endblock content %}

For the edit and delete pages we can use Bootstrap’s button styling to make the edit button light blue and the delete button red.

<!-- templates/post_edit.html -->
{% extends 'base.html' %}

{% block content %}
    <h1>Edit</h1>
    <form action="" method="post">{% csrf_token %}
      {{ form.as_p }}
    <button class="btn btn-info ml-2" type="submit">Update</button>
</form>
{% endblock %}
<!-- templates/post_delete.html -->
{% extends 'base.html' %}

{% block content %}
    <h1>Delete</h1>
    <form action="" method="post">{% csrf_token %}
      <p>Are you sure you want to delete "{{ post.message }}"?</p>
      <button class="btn btn-danger ml-2" type="submit">Confirm</button>
    </form>
{% endblock %}

As a final step we can add the edit and delete links to our lists page at div class for card-footer.... These will be the same as those added to the detail page.

<!-- templates/post_list.html -->
...
<div class="card-footer text-center text-muted">
  <a href="{% url 'post_edit' post.pk %}">Edit</a> |
  <a href="{% url 'post_delete' post.pk %}">Delete</a>
</div>
...

Ok, we’re ready to view our work. Start up the server with python manage.py runserver and navigate to posts page at http://127.0.0.1:8000/posts/. Click on the link for “edit” on the first entry and you’ll be redirected to http://127.0.0.1:8000/posts/1/edit/.

Edit page

If you update the “message” field and click update you’ll be redirected to the detail page which shows the new changes.

Detail page

If you click on the “Delete” link you’ll be redirected to the delete page.

Delete page

Press the scary red button for “Delete” and you’ll be redirected to the all messages page which now only has two entries.

Posts page two entries

As a sanity check click on the “edit” and “delete” links for one of the two messages still there. Everything should work.

Create page

The final step is a create page for new messages which we can do with Django’s CreateView. Our three steps are to create a view, url, and template. This flow should feel pretty familiar by now.

In our views file add CreateView to the imports and make a new class PostCreateView that specifies our model, template, and the fields available.

# posts/views.py
...
from django.views.generic.edit import CreateView, UpdateView, DeleteView

class PostCreateView(CreateView):
    model = models.Post
    template_name = 'post_new.html'
    fields = ['message', 'author']
...

Update our urls file with the new route for the view.

# posts/urls.py
...
urlpatterns = [
    ...
    path('new/', views.PostCreateView.as_view(), name='post_new'),
    ...
]

Then quit the server Control+c to create a new template named post_new.html.

(msg) $ touch templates/post_new.html

And update it with the following HTML code.

<!-- templates/post_new.html -->
{% extends 'base.html' %}

{% block content %}
    <h1>New message</h1>
    <form action="" method="post">{% csrf_token %}
      {{ form.as_p }}
      <button class="btn btn-success ml-2" type="submit">Save</button>
    </form>
{% endblock %}

As a final step we should add a link to creating new posts in our nav so it’s accessible everywhere on the site to logged-in users.

<!-- templates/base.html -->
...
<body>
  <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
    <a class="navbar-brand" href="{% url 'home' %}">Social Msg</a>
    {% if user.is_authenticated %}
    <ul class="navbar-nav mr-auto">
      <li class="nav-item"><a href="{% url 'post_new' %}">+ New</a></li>
    </ul>
    {% endif %}
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    ...

And why not use Bootstrap to improve our original homepage now too? We can update templates/home.html as follows.

<!-- templates/home.html -->
{% extends 'base.html' %}

{% block title %}Home{% endblock %}

{% block content %}
<div class="jumbotron">
  <h1 class="display-4">Social Messaging app</h1>
  <p class="lead">A Twitter clone built with Django.</p>
  <p class="lead">
    <a class="btn btn-primary btn-lg" href="{% url 'posts' %}" role="button">View All Messages</a>
  </p>
</div>
{% endblock %}

We’re all done. Let’s just confirm everything works as expected. Start up the server again python manage.py runserver and navigate to our homepage at http://127.0.0.1:8000/.

Homepage with new link in nav

Click on the link for “+ New” in the top nav and you’ll be redirected to our create page.

Create page

Note we need to specify both our message and the author. I’ve chosen the message “Does this work?” and as author my superuser account wsv. Click on the “Save” button. You will be redirected to the detail page since that’s what the get_absolute_url is as set in models.py.

Detail page

Note also that the primary key here is 4 in the URL. Even though we’re only displaying three messages right now, Django doesn’t reorder the primary keys just because we deleted one. In practice, most real-world sites don’t actually delete anything; instead they “hide” deleted fields since this makes it easier to maintain the integrity of a database and gives the option to “undelete” later on if needed. With our current approach once something is deleted it’s gone for good!

Click on the link for “All Messages” to see our new /posts page.

Updated posts page

There’s our new message on the bottom as expected.

Improved CreateView

You may have noticed that it’s less than ideal how our create form looks. We don’t want users to be able to specify a different user as the author of their message. In fact, we should configure the form so it automatically sets the author based on the current logged-in user.

This is a common need in web development and Django has us covered with a nice example in the official docs. The trick is to set the user automatically before the form is submitted. By using the form_valid method we can set author to be the current user.

# posts/views.py
class PostCreateView(CreateView):
    model = models.Post
    template_name = 'post_new.html'
    fields = ['message']

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

Now reload the browser and try clicking on the “+ New” link in the top nav again. This is the result.

New post link

Conclusion

Hopefully some of this chapter felt familiar to the steps we took in our previous blog app. CRUD functionality is seen again and again and again in web development. Django’s generic class-based views give us quick templates for common functionality that we can also extend as needed, as we did with our PostCreateView for the form.

Next up we’ll add comments so that other logged-in users can write comments on blog posts. This will introduce the concept of foreign keys and one-to-many relationships.

Continue on to Continue on to Chapter 13: Comments app.




Sign up for the Django For Beginners newsletter for updates when new chapters are available and special discounts for the print edition of the book.