Have a Minimal Project ready if you want to follow this tutorial.
Blog App Code In GitHub

Create An App

Create an app called blog:

1
python3 manage.py startapp blog

Add blog to INSTALLED_APPS:

1
vim mysite/settings.py
1
2
3
4
5
INSTALLED_APPS = [
    ...
    'django.contrib.staticfiles',
    'blog', # < this
]

Create A Model

Edit blog/models.py and add a Blog class:

1
vim blog/models.py
1
2
class Blog(models.Model):
    title = models.CharField(max_length=255, default='', blank=True)

max_length is the only required argument for CharField. default=’‘ value is used for the field if none is provided when creating the object. This value will also show in the form for the title field when creating a new Blog post. blank=True means that the form doesn’t require an input for the field so we can leave it empty. It is not recommended to use null=True for string-based fields like CharField. Read more: Field options - djangoproject.com

Create new migration files based on the model and apply them:

1
2
python3 manage.py makemigrations
python3 manage.py migrate

Makemigrations packages model changes into migration files (similar to version control commits) and migrate applies those changes. Read more: Migrations - djangoproject.com

Admin Area

Create a superuser.

1
python3 manage.py createsuperuser

Now if you go to the Admin area (/admin/), you can’t see the Blog app yet.

Edit blog/admin.py and add these lines:

1
2
3
from .models import Blog

admin.site.register(Blog)

This allows us edit Blog items from the Admin interface.

Save and open the Admin area again:

Add some blog items.

Object string representations

Now the blog list looks something like this:

Let’s show item titles by defining the __str__ - djangoproject.com method:

1
vim blog/models.py
1
2
3
4
5
class Blog(models.Model):
    title = models.CharField(max_length=255, blank=True, null=True)

    def __str__(self):
        return '%s' % self.title

The admin display is using str() function on the blog objects. This will call the __str__() method which we can use to return a human readable representation of the object.

Modulo (%) is used here as string formatting operator. ‘%s’ denotes the conversion type (string). Read more: String formatting - python.org.

Refresh the admin page and you should see list of titles.

ModelForm

ModelForm class allows you to create forms from models.

Create forms.py file:

1
vim blog/forms.py
1
2
3
4
5
6
7
from django.forms import ModelForm
from .models import Blog

class BlogForm(ModelForm):
    class Meta:
        model = Blog
        fields = ['title']

The documentation recommends that you specify explicitly the fields like we did above. You can also safely use fields = ‘__all__‘ to use all fields from the model or exclude = [‘description’,’date’] to use all fields but the ones excluded.

Read more: ModelForm - djangoproject.com

Urls

We need a path where to access the form. Create a urls.py file:

1
vim blog/urls.py
1
2
3
4
5
6
from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^add/blog/$', views.add_blog, name='add_blog'),
]

url function is used to instantiate patterns. Urlpatterns should be a list of those instances.

r’^add/blog/$ A string that should contain a regular expression re - python.org
r Raw string syntax allows using backslash sequences like \d in the string without having to escape them with another backslash.
^ Caret matches the start of the string.
$ Dollar matches the end of the string. If we don’t add it then this pattern would match everything starting with add/blog/ like add/blog/random/10
views.add_blog The function that handles requests to this url (yet to be implemented).
name=’add_blog’ Name of the pattern. This can be used elsewhere like in templates to avoid hard coding urls.

Open the main urls.py file and reference the blog app urls from there:

1
vim mysite/urls.py

Import include and add url(r’^’, include(‘blog.urls’)) line:

1
2
3
4
5
6
7
from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('blog.urls')),
]
include This function includes the url patterns from the Blog app urls.py. include - djangoproject.com

Note that we don’t add the $ sign to match the end of the string. If you want to define a base path then add a trailing slash (/): url(r’^moderators/’, include(‘blog.urls’)). In this case our form could be accessed in here: mysite.com/moderators/add/blog/.

View

Let’s create the add_blog function to handle requests to the add/blog path:

1
vim blog/views.py
1
2
3
4
5
6
7
8
9
10
11
12
from django.shortcuts import render
from .forms import BlogForm

def add_blog(request):
    if request.method == "POST":
        form = BlogForm(request.POST)
        if form.is_valid():
            blog_item = form.save(commit=False)
            blog_item.save()
    else:
        form = BlogForm()
    return render(request, 'blog/blog_form.html', {'form': form})
django.shortcuts provides general helper functions and classes like render function.
request When you request a page, an HttpRequest object is created and passed to the view. This contains metadata about the request like the method used and the form data.
POST POST method is used for more sensitive data like passwords where the data is sent to the server encoded. GET method would be used in something more insensitive like search queries: mysite/?search=django. This information would be visible in the url and saved in the browser history.
form = BlogForm(request.POST) This creates the form instance and bounds form data to it. Request.POST contains the form data.
form.is_valid() is_valid method runs validation against the fields and returns either False or True.
form.save(commit=False) ModelForm has a save method that creates and saves an object bound to the form. commit=False means that we return an object that has not been stored in the database yet. That’s why have to run save() against the object to actually save it to the database.
form = BlogForm() This creates an unbound form instance. Because we don’t have POST data available, we have no data to bound to the form. This will result in a empty form when it is rendered with the render function.
render This function returns a HttpResponse object. This will contain the content for the page. We are declaring the template file and the form variable to be used in that template.

So when we go to the address add/blog we will see the unbound form with no data. If we input data and post the form, the request.method == “POST” will be TRUE and we will validate and possibly create the item.

Django’s Form class represents a form and determines how it works. Our BlogForm class is a ModelForm subclass which helps you create that Form class from Models.

Template

Let’s create the form template file:

1
2
mkdir -p templates/blog
vim templates/blog/blog_form.html

p creates the parent folder templates also.

1
2
3
4
5
<form method="POST" action="">
  {% csrf_token %}
  {{ form }}
  <input type="submit" value="Save">
</form>
action=”“ Empty action means that we are sending the form POST data to the current url (add/blog/).  
{% csrf_token %} This token provides protection for Cross Site Request Forgery CSRF - djangoproject.com
{{ form }} This renders the form fields with their labels and inputs. There are some other output options like {{ form.as_p }} that wraps the label / input pairs in p tags. Forms - djangoproject.com

Use the CSRF token inside form elements that uses POST method and targets internal paths.

Now you should be able to see the form in /add/blog/.

This is the markup that will be generated:

1
2
3
4
5
<form method="POST" enctype="multipart/form-data" action="">
  <input type="hidden" name="csrfmiddlewaretoken" value="FtrR243JfXvGf1SVebKHV5syAsOGqT9C8IdZD0Yoxus6thadT2tJxbMUDc3NJRWM">
  <label for="id_title">Title:</label><input type="text" name="title" id="id_title" maxlength="255">
  <input type="submit" value="Save">
</form>

Edit form

We can use the same form template for editing content. Let’s add another pattern in the blog app urls.py file:

1
vim blog/urls.py
1
2
	url(r'^add/blog/$', views.add_blog, name='add_blog'),
	url(r'^edit/blog/(?P<id>\d+)/$', views.edit_blog, name='edit_blog'),

(?P<id>\d+)/$ is a named regular-expression group - djangoproject.com.

Syntax for it is this: (?P<name>pattern).

?P<id> This means that we are sending a variable named id to the view.  
\d+ \d matches digit characters and + matches 1 or more of those like 1 or 102. regular-expression-syntax - python.org

Add the edit_blog function in the blog app views.py:

1
vim blog/views.py

Make sure to Import get_object_or_404 and Blog:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from django.shortcuts import render, get_object_or_404
from .forms import BlogForm
from .models import Blog

def add_blog(request):
    if request.method == "POST":
        form = BlogForm(request.POST)
        if form.is_valid():
            blog_item = form.save(commit=False)
            blog_item.save()
    else:
        form = BlogForm()
    return render(request, 'blog/blog_form.html', {'form': form})


def edit_blog(request, id=None):
    item = get_object_or_404(Blog, id=id)
    form = BlogForm(request.POST or None, instance=item)
    if form.is_valid():
        form.save()
    return render(request, 'blog/blog_form.html', {'form': form})
id=None None is the default value for the id argument.
get_object_or_404 This returns an object with the given id or raise a Http404 error if the object doesn’t exist.
request.POST or None If there is POST data available, we use that data (eg. if we change data and hit save).
instance=item If existing item is provided through the instance argument, then save method will update that instance instead of creating a new one. Also the form is initially populated with the data from the existing object.

So, now you can add blog posts in /add/blog and edit them in /edit/blog/<id>.

Add fields manually

You can also add the fields manually. Let’s add a description field to the model:

1
vim blog/models.py
1
2
3
class Blog(models.Model):
    title = models.CharField(max_length=255, default='', blank=True)
    description = models.TextField(default='', blank=True) # -- here --

Read more: TextField - djangoproject.com

1
vim blog/forms.py
1
2
3
4
class BlogForm(ModelForm):
    class Meta:
        model = Blog
        fields = ['title', 'description'] # -- here --
1
2
python3 manage.py makemigrations
python3 manage.py migrate
1
vim blog/templates/blog/blog_form.html

Let’s change the order of the fields:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<form method="POST" enctype="multipart/form-data" action="">
  {% csrf_token %}
  {{ form.non_field_errors }}
  <div class="field_wrapper">
    {{ form.description.errors }}
    <label for="{{ form.description.id_for_label }}">Description:</label>
    {{ form.description }}
  </div>
  <div class="field_wrapper">
    {{ form.title.errors }}
    <label for="{{ form.title.id_for_label }}">Title:</label>
    {{ form.title }}
  </div>
  <input type="submit" value="Save">
</form>

form.non_field_errors Prints out errors that are not related to any specific field. form.non_field_errors
form.description.errors Prints out field specific errors.  
form.description.id_for_labels Prints out id of the field for the label for attribute.  
form.description Prints out the field.  

Redirect and blog page

Let’s create a page for blog posts and redirect the user to that page after adding / editing an object:

Redirect

1
vim blog/views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.shortcuts import render, get_object_or_404, redirect # -- here --
from .forms import BlogForm
from .models import Blog

def add_blog(request):
    if request.method == "POST":
        form = BlogForm(request.POST)
        if form.is_valid():
            blog_item = form.save(commit=False)
            blog_item.save()
            return redirect('/blog/' + str(blog_item.id) + '/') # -- here --
    else:
        form = BlogForm()
    return render(request, 'blog/blog_form.html', {'form': form})
	
def edit_blog(request, id=None):
    item = get_object_or_404(Blog, id=id)
    form = BlogForm(request.POST or None, instance=item)
    if form.is_valid():
        form.save()
        return redirect('/blog/' + str(item.id) + '/') # -- here --
    return render(request, 'blog/blog_form.html', {'form': form})

Redirect function does the redirecting for us.

Templates

Create base.html and blog.html templates:

1
vim blog/templates/blog/base.html

base.html:

1
2
3
4
5
6
7
8
9
10
11
12
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ModelForm tutorial | WDTutorials.com</title>
</head>
<body>

{% block content %} {% endblock %}
  
</body>
</html>

This is the basic layout skeleton for the site.

{% block content %} {% endblock %} Defines a block that child templates can override.
1
vim blog/templates/blog/blog.html

blog.html:

1
2
3
4
5
6
7
{% extends 'blog/base.html' %}

{% block content %}
<h1>{{ blog.title }}</h1>
<p>{{ blog.description }}</p>
{% endblock %}

{% extends ‘blog/base.html’ %} Defines the parent template.
{% block content %} {% endblock %} Content inside this block will dynamically replace the content block in the parent template.
blog.title You can access individual fields with a dot (.)

So, when the blog template is evaluated, system locates the parent template (base.html) and replace the block contents with the contents of the child template.

Url

1
vim blog/urls.py
1
2
3
4
5
6
7
8
from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^add/blog/$', views.add_blog, name='add_blog'),
    url(r'^edit/blog/(?P<id>\d+)/$', views.edit_blog, name='edit_blog'),
    url(r'^blog/(?P<id>\d+)/$', views.blog, name='blog'),  # -- here --
]

Added url pattern takes id as an argument like in the edit/blog pattern.

View

1
vim blog/views.py

Add the blog function:

1
2
3
def blog(request, id=id):
    blog = Blog.objects.get(id=id)
    return render(request, 'blog/blog.html', {'blog': blog })

In here we do a database query to fetch a Blog object based on the primary key and pass it in the template as a variable called blog.

So now you will be redirected to the blog page in /blog/<id>/ when you add/edit blog.

Conclusion

This was an example on how to create forms from Models with ModelForm class. Make sure to check out these links to learn more about forms: Forms - djangoproject.com Forms API - djangoproject.com