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 14 | from wtforms import Form, StringField, TextAreaField, validators
from wtforms import IntegerField
from wtforms.widgets import HiddenInput
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 = IntegerField(widget=HiddenInput())
|
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.
18 19 20 21 22 23 24 25 26 27 | @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.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 | @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():
del form.id # SECURITY: prevent overwriting of primary key
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.
Plase notice the line:
39 | del form.id # SECURITY: prevent overwriting of primary key
|
Note
IMPORTANT SECURITY INFORMATION
For the sake of tutorial, we showcase how to create inheritable form schemas where creation view might be have different fields than update/moderation view, in this case this is id field.
One important thing to remember is that populate_obj will overwrite ALL fields that are present in the Form object, so you need to remember to remove the read-only fields or better yet implement your own population method on your model. As of today WTForms does not provide any built-in field exclusion mechanisms.
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 42 43 | 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():
del form.id # SECURITY: prevent overwriting of primary key
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.