6. Adding and editing blog entries

Form handling with WTForms library

For form validation and creation, we will use a very friendly and easy to use form library called WTForms. First we need to define our form schemas that will be used to generate form HTML and validate values of form fields.

In the root of our application, let's create the file forms.py with following content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from wtforms import Form, StringField, TextAreaField, validators
from wtforms import HiddenField

strip_filter = lambda x: x.strip() if x else None

class BlogCreateForm(Form):
    title = StringField('Title', [validators.Length(min=1, max=255)],
                        filters=[strip_filter])
    body = TextAreaField('Contents', [validators.Length(min=1)],
                         filters=[strip_filter])

class BlogUpdateForm(BlogCreateForm):
    id = HiddenField()

We create a simple filter that will be used to remove all the whitespace from the beginning and end of our input.

Then we create a BlogCreateForm class that defines two fields:

  • title has a label of "Title" and a single validator that will check the length of our trimmed data. The title length needs to be in the range of 1-255 characters.
  • body has a label of "Contents" and a validator that requires its length to be at least 1 character.

Next is the BlogUpdateForm class that inherits all the fields from BlogCreateForm, and adds a new hidden field called id. id will be used to determine which entry we want to update.

Create blog entry view

Now that our simple form definition is ready, we can actually write our view code.

Lets start by importing our freshly created form schemas to views/blog.py.

4
5
from ..services.blog_record import BlogRecordService
from ..forms import BlogCreateForm, BlogUpdateForm

Add the emphasized line as indicated.

Next we implement a view callable that will handle new entries for us.

17
18
19
20
21
22
23
24
25
26
@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_create(request):
    entry = BlogRecord()
    form = BlogCreateForm(request.POST)
    if request.method == 'POST' and form.validate():
        form.populate_obj(entry)
        request.dbsession.add(entry)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form, 'action': request.matchdict.get('action')}

Only the emphasized lines need to be added or edited.

The view callable does the following:

  • Create a new fresh entry row and form object from BlogCreateForm.
  • The form will be populated via POST, if present.
  • If the request method is POST, the form gets validated.
  • If the form is valid, our form sets its values to the model instance, and adds it to the database session.
  • Redirect to the index page.

If the form doesn't validate correctly, the view result is returned, and a standard HTML response is returned instead. The form markup will have error messages included.

Create update entry view

The following view will handle updates to existing blog entries.

29
30
31
32
33
34
35
36
37
38
39
40
41
@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_update(request):
    blog_id = int(request.params.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    form = BlogUpdateForm(request.POST, entry)
    if request.method == 'POST' and form.validate():
        form.populate_obj(entry)
        return HTTPFound(
            location=request.route_url('blog', id=entry.id,slug=entry.slug))
    return {'form': form, 'action': request.matchdict.get('action')}

Only the emphasized lines need to be added or edited.

Here's what the view does:

  • Fetch the blog entry from the database based in the id query parameter.
  • Show a 404 Not Found page if the requested record is not present.
  • Create the form object, populating it from the POST parameters or from the actual blog entry, if we haven't POSTed any values yet.

Note

This approach ensures our form is always populated with the latest data from the database, or if the submission is not valid then the values we POSTed in our last request will populate the form fields.

  • If the form is valid, our form sets its values to the model instance.
  • Redirect to the blog page.

For convenience, here is the complete views/blog.py thusfar, with added and edited lines emphasized.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPNotFound, HTTPFound
from ..models.blog_record import BlogRecord
from ..services.blog_record import BlogRecordService
from ..forms import BlogCreateForm, BlogUpdateForm

@view_config(route_name='blog',
             renderer='pyramid_blogr:templates/view_blog.jinja2')
def blog_view(request):
    blog_id = int(request.matchdict.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    return {'entry': entry}


@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_create(request):
    entry = BlogRecord()
    form = BlogCreateForm(request.POST)
    if request.method == 'POST' and form.validate():
        form.populate_obj(entry)
        request.dbsession.add(entry)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form, 'action': request.matchdict.get('action')}


@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_update(request):
    blog_id = int(request.params.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    form = BlogUpdateForm(request.POST, entry)
    if request.method == 'POST' and form.validate():
        form.populate_obj(entry)
        return HTTPFound(
            location=request.route_url('blog', id=entry.id,slug=entry.slug))
    return {'form': form, 'action': request.matchdict.get('action')}

Create a template for creating and editing blog entries

The final step is to add a template that will present users with the form to create and edit entries. Let's call it templates/edit_blog.jinja2 and place the following code as its content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{% extends "pyramid_blogr:templates/layout.jinja2" %}

{% block content %}
    <form action="{{request.route_url('blog_action',action=action)}}" method="post" class="form">
        {% if action =='edit' %}
            {{ form.id() }}
        {% endif %}

        {% for error in form.title.errors %}
            <div class="error">{{ error }}</div>
        {% endfor %}

        <div class="form-group">
            <label for="title">{{ form.title.label }}</label>
            {{ form.title(class_='form-control') }}
        </div>

        {% for error in form.body.errors %}
            <div class="error">{{ error }}</div>
        {% endfor %}

        <div class="form-group">
            <label for="body">{{ form.body.label }}</label>
            {{ form.body(class_='form-control') }}
        </div>
        <div class="form-group">
            <label></label>
            <button type="submit" class="btn btn-default">Submit</button>
        </div>


    </form>
    <p><a href="{{ request.route_url('home') }}">Go Back</a></p>
{% endblock %}

Our template knows if we are creating a new row or updating an existing one based on the action variable value. If we are editing an existing row, the template will add a hidden field named id that holds the id of the entry that is being updated.

If the form doesn't validate, then the field errors properties will contain lists of errors for us to present to the user.

Now launch the app, and visit http://localhost:6543/ and you will notice that you can now create and edit blog entries.

Note

Because WTForms form instances are iterable, you can easily write a template function that will iterate over its fields and auto generate dynamic HTML for each of them.

Now it is time to work towards securing them.

Next: 7. Authorization.