In this chapter we’ll continue working on our blog application from Chapter 5 by adding forms so a user can create, edit, or delete any of their blog entries.

Complete code can be found on Github.

Forms

Forms are very complicated in practice. Any time you are accepting user input there are security concerns (XSS Attacks), proper error handling is required, and there are UI considerations around how to alert the user to problems with the form as well as redirects on success.

Fortunately for us Django Forms abstract away much of the difficulty and provide a rich set of tools to handle common use cases working with forms.

To start, update our base template to display a link to a page for entering new blog posts. It will take the form <a href="{% url 'post-new' %}"></a> where post-new is the name for our URL.

Your updated file will look as follows:

<!-- blog_app/templates/blog_app/base.html -->
{% load staticfiles %}
<html>
  <head>
    <title>Django blog</title>
    <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400" rel="stylesheet">
    <link rel="stylesheet" href="{% static 'css/base.css' %}">
  </head>
  <body>
    <div class="container">
      <header>
        <div class="nav-left">
          <h1><a href="/">Django blog</a></h1>
        </div>
        <div class="nav-right">
          <a href="{% url 'post-new' %}">+ New Blog Post</a>
        </div>
      </header>
      {% block content %}
      {% endblock content %}
    </div>
  </body>
</html>

Let’s add a new URLConf for post-new now:

# blog_app/urls.py
from django.conf.urls import url
from . views import BlogListView, BlogDetailView, BlogCreateView

urlpatterns = [
    url(r'^$',
      BlogListView.as_view(), name='post-list'),
    url(r'^post/(?P<pk>\d+)/$',
      BlogDetailView.as_view(), name='post-detail'),
    url(r'^post/new/$',
        BlogCreateView.as_view(), name='post-new'),
]

At the top we import a new view called BlogCreateView which we will write next. Then we create a dedicated url pattern for it, specifying urls will start with post/new/, and be named post-new. Simple, right?

Now let’s create our view by importing a new generic class called CreateView and then subclass it to create a new view called BlogCreateView.

# blog_app/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView
from . models import Post


class BlogListView(ListView):
    model = Post
    template = 'post_list.html'


class BlogDetailView(DetailView):
    model = Post
    template = 'post_detail.html'


class BlogCreateView(CreateView):
    model = Post
    template_name = 'blog_app/post_new.html'
    fields = '__all__'

Within BlogCreateView we specify our database model Post, the name of our template blog_app/post_new.html, and all fields with '__all__' since we only have two: title and author.

The last step is to create our template, which we will call post_new.html.

(blog) $ touch blog_app/templates/blog_app/post_new.html

And then add the following code:

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

{% block content %}
    <h1>New post</h1>
    <form action="" method="post">{% csrf_token %}
      {{ form.as_p }}
      <input type="submit" value="Save" />
    </form>
{% endblock %}

Let’s breakdown what we’ve done:

  • On the top line we inherit from our base template.
  • Use HTML <form> tags with the method POST since we’re sending data. If we were receiving data from a form, for example in a search box, we would use GET.
  • Add a {% csrf_token %} which Django provides to protect our form from cross-site scripting attacks. You should use it for all your Django forms.
  • Then to output our form data we use {{ form.as_p }} which renders it within paragraph <p> tags.
  • Finally specify an input type of submit and assign it the value “Save”

To view our work, start the server with ./manage.py runserver and go to the homepage at http://127.0.0.1:8000/.

Homepage with New button

Click on our link for “+ New Blog Post” which will redirect you to http://127.0.0.1:8000/post/new/.

Blog new page

Go ahead and try to create a new blog post and submit it.

Blog new page

Oops! What happened?

Blog new page

Django’s error message is quite helpful. It’s complaining that we did not specify where to send the user after successfully submitting the form. Let’s just send a user back to the homepage after success since that will show the list of all their blogs.

We can follow Django’s suggestion and add an get_absolute_url to our model. Open the models.py file. Add a new import at the top for reverse and a new method get_absolute_url.

# blog_app/models.py
from django.urls import reverse
from django.db import models


class Post(models.Model):
    author = models.ForeignKey('auth.User')
    title = models.CharField(max_length=200)
    text = models.TextField()

    def get_absolute_url(self):
        return reverse('post-list')

    def __str__(self):
        return self.title

Reverse is a very handy utility function Django provides us while get_absolute_url is a method on our database model that provides a canonical URL for an object. Basically if Django is unsure where to send a user using our Post model it sends them to the named url we provide, which is post-list, our homepage as specified in blog_app/urls.py.

Try to create a new blog post again at http://127.0.0.1:8000/post/new/ and you’ll find you are redirected to the homepage where the post appears.

Blog new page with input

You’ll also notice that our earlier blog post is also there. It was successfully sent to the database, but Django didn’t know how to redirect us after that.

Blog homepage with four posts

While we could go into the Django admin to delete unwanted posts, it’s better if we add forms so a user can update and delete existing forms directly from the site.

Update Form

The process for creating an update form so users can edit blog posts should feel familiar. We’ll again use a built-in Django class-based generic view, UpdateView, and create the requisite template, url, and view.

To start, let’s add a new link to post_detail.html so that the option to edit a blog post appears on an individual blog page.

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

{% block content %}
  <div class="post-entry">
    <h2>{{ object.title }}</h2>
    <p>{{ object.text }}</p>
  </div>

  <a href="{% url 'post-edit' post.pk %}">+ Edit Blog Post</a>
{% endblock content %}

We’ve added a link using <a href>...</a> and the Django template engine’s {% url ... %} tag. Within it we’ve specified the target name of our url, which will be called post-edit and also passed the parameter needed, which is the primary key of the post post.pk.

Next we create the template for our edit page called post_edit.html.

(blog) $ touch blog_app/templates/blog_app/post_edit.html

And add the following code:

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

{% block content %}
    <h1>Edit post</h1>
    <form action="" method="post">{% csrf_token %}
      {{ form.as_p }}
    <input type="submit" value="Update" />
</form>
{% endblock %}

We again use HTML <form></form> tags, Django’s csrf_token for security, form.as_p to display our form fields with paragraph tags, and finally give it the value “Update” on the submit button.

Now to our view. We need to import UpdateView on the second-from-the-top line and then subclass it in our new view BlogUpdateView.

# blog_app/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView
from . models import Post


class BlogListView(ListView):
    model = Post
    template = 'post_list.html'


class BlogDetailView(DetailView):
    model = Post
    template = 'post_detail.html'


class BlogCreateView(CreateView):
    model = Post
    template_name = 'blog_app/post_new.html'
    fields = '__all__'


class BlogUpdateView(UpdateView):
    model = Post
    fields = ['title', 'text']
    template_name = 'blog_app/post_edit.html'

Within BlogUpdateView we specify our model, individually list the fields we want as opposed to selecting all of them as we did for CreateView though since we only have two fields the result is the same. And finally we specify the name of our template associated with the view.

The final step is to update our urls.py file as follows:

# blog_app/urls.py
from django.conf.urls import url
from . views import BlogListView, BlogDetailView, BlogCreateView, BlogUpdateView


urlpatterns = [
    url(r'^$',
        BlogListView.as_view(), name='post-list'),

    url(r'^post/(?P<pk>\d+)/$',
        BlogDetailView.as_view(), name='post-detail'),

    url(r'^post/new/$',
        BlogCreateView.as_view(), name='post-new'),

    url(r'^post/(?P<pk>\d+)/edit/$',
        BlogUpdateView.as_view(), name='post-edit'),
]

At the top we add our view BlogUpdateView to the list of imported views, then created a new url pattern for /post/pk/edit and given it the name post-edit.

Now if you click on a blog entry you’ll see our new Edit button.

Blog page with edit button

If you click on “+ Edit Blog Post” you’ll be redirected to http://127.0.0.1:8000/post/1/edit/ if it’s your first blog post.

Blog edit page

Note that the form is prefilled with our database’s existing data for the post. Let’s make a change…

Blog edit page

And after clicking the “Update” button we are redirected to the homepage and can see the change!

Blog homepage with edited post

Delete View

The process for creating a form to delete blog posts is very similar to that for updating a post. We’ll use a generic class-based view, DeleteView, to help and need to create a view, url, and template for the functionality.

Let’s start by adding a link to delete blog posts on our individual blog page, post_detail.html.

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

{% block content %}
  <div class="post-entry">
    <h2>{{ object.title }}</h2>
    <p>{{ object.text }}</p>
  </div>

  <p><a href="{% url 'post-edit' post.pk %}">+ Edit Blog Post</a></p>
  <p><a href="{% url 'post-delete' post.pk %}">+ Delete Blog Post</a></p>
{% endblock content %}

Then create a new file for our delete page template. First quit the local server Control-c and then type the following command:

(blog) $ touch blog_app/templates/blog_app/post_delete.html

And fill it with this code:

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

{% block content %}
    <h1>Delete post</h1>
    <form action="" method="post">{% csrf_token %}
      <p>Are you sure you want to delete "{{ post.title }}"?</p>
      <input type="submit" value="Confirm" />
    </form>
{% endblock %}

Note we are using post.title here to display the title of our blog post. We could also just use object as it too is provided by DetailView.

Now update our views.py file, by importing DeleteView and reverse_lazy at the top, then create a new view that subclasses DeleteView.

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

from . models import Post


class BlogListView(ListView):
    model = Post
    template = 'post_list.html'


class BlogDetailView(DetailView):
    model = Post
    template = 'post_detail.html'


class BlogCreateView(CreateView):
    model = Post
    template_name = 'blog_app/post_new.html'
    fields = '__all__'


class BlogUpdateView(UpdateView):
    model = Post
    fields = ['title', 'text']
    template_name = 'blog_app/post_edit.html'


class BlogDeleteView(DeleteView):
    model = Post
    template_name = 'blog_app/post_delete.html'
    success_url = reverse_lazy('post-list')

We use reverse_lazy as opposed to just reverse so that it won’t execute the URL redirect until our view has finished deleting the blog post.

Finally add a url by importing our view BlogDeleteView and adding a new pattern:

# blog_app/urls.py
from django.conf.urls import url
from . views import BlogListView, BlogDetailView, BlogCreateView, BlogUpdateView, BlogDeleteView

urlpatterns = [
    url(r'^$',
        BlogListView.as_view(), name='post-list'),

    url(r'^post/(?P<pk>\d+)/$',
        BlogDetailView.as_view(), name='post-detail'),

    url(r'^post/new/$',
        BlogCreateView.as_view(), name='post-new'),

    url(r'^post/(?P<pk>\d+)/edit/$',
        BlogUpdateView.as_view(), name='post-edit'),

    url(r'^author/(?P<pk>\d+)/delete/$',
        BlogDeleteView.as_view(), name='post-delete'),
]

If you start the server again ./manage.py runserver and refresh the individual post page you’ll see our “Delete Blog Post” link.

Blog delete post

Clicking on the link takes us to the delete page for the blog post, which displays the name of the blog post.

Blog delete post page

If you click on the “Confirm” button, it redirects you to the homepage where the blog post has been deleted!

Homepage with post deleted

So it works!

Conclusion

In a small amount of code we’ve built a blog application that allows for creating, reading, updating, and deleting blog posts. This core functionality is know by the acronym CRUD. While there are multiple ways to achieve this same functionality–we could use function-based views or write our own class-based views–we’ve demonstrated how little code it takes in Django to make this happen.

In the next chapter we’ll add user accounts. In addition to sign up and log in pages, we’ll restrict access to content so that only the author of a blog post can see his or her’s content.

Continue on to Chapter 7: Blog app with user accounts.




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.