Adding a contact form to a multilanguage page with content from a database
When the page content comes from a database you will want to add a contact form using a tag.
Update 11 October 2019: I changed the addon-tag from '{% addon: ... %}' to '[[ addon: ... ]]'. The reason is that I wanted to be able to render the page text coming from the database, using render_template_string, and '{% ... %}' conflicts with Jinja2 tags. And yes, I do not want to implement a Jinja2 custom tag.
What is difficult about implementing a contact page with a contact form with Flask and WTForms? You can find solutions on how to implement a contact page in Flask but every time the page is a single language page and uses a Jinja2 template file. So why write a post about this?
The reason is that this is not trivial when the page content can be multilanguage and comes from a database. I have the page content I can edit using the administrator, and I want the contact form to be placed somewhere on the page by using a tag. Why a tag? Because we must be able to put the contact form at any location in the content. Once the tag is replaced by the contact form it also must be processed when submitted. Easy? Maybe for you, but not for me.
Introducing add-ons
Looking at other solutions, I thought it would be useful to implement the contact form as an add-on. Why? Because an add-on is something that you should be able to add in a very easy way to your content. It should also be possible to add the contact form to multiple pages. There is more to an add-on, for example the contact form add-on also adds a contact form function to the admin where we can look at the contact forms that have been submitted.
Implementing the add-on
The first thing I did was defining a tag that would identify the contact form add-on:
{% addon:contact_form,id=87 %}
This is the tag we can add to the content of our multilanguage page that comes from the database. Other components of the contact form add-on are:
- ContactForm, the model (table)
- Admin part, where we can see the submitted forms
And then we need a general mechanism that processes the add-on when we display a page. As you may remember from a previous post there is only one function that generates a page. As the content does not change, it is cached:
@pages_blueprint.route('/<slug>', methods=['GET', 'POST'])
def page_view(slug):
...
# get content_item
...
# render content_item of get from cache
hit, rendered_content_item = current_app.app_cache.load(cache_item_id)
if not hit:
rendered_content_item = render_template(
...
content_item=content_item,
content_item_translation=content_item_translation,
)
# cache it
current_app.app_cache.dump(cache_item_id, rendered_content_item)
...
return render_template(
...
rendered_content_item=rendered_content_item,
)
This function must be modified and extended so that it is capable of handling add-ons.
Converting MVC to a class
In Flask using WTForms the contact form implementation is very straightforward, for example:
@pages_blueprint.route('/contact-form', methods=['GET', 'POST'])
def contact_form():
form = ContactFormForm()
if form.validate_on_submit():
contact_form = ContactForm()
form.populate_obj(contact_form)
db.add(contact_form)
db.commit()
flash( _('Contact form submitted.'), 'info')
return redirect(url_for('pages.thank_you'))
return render_template(
'pages/contact_form.html',
form=form)
And the ContactFormForm is:
class ContactFormForm(FlaskForm):
name = StringField(_l('Name'), validators=[
Length(min=2, max=60),
InputRequired()])
email = StringField(_l('Your email'), validators=[
InputRequired(),
Email()])
message = TextAreaField(_l('Your message'), validators=[
Length(min=6, max=500),
InputRequired()])
submit = SubmitField(_l('Send'))
We cannot use this here so we rewrite this as a class. I decided to return success or error for the GET and POST methods and have a separate method for getting the rendered contact form.
class AddonContactForm:
def __init(self)__:
...
self.errors = False
self.rendered_contact_form = ''
def get_contact_form(self):
self.errors = False
form = ContactFormForm()
self.rendered_contact_form = render_template(
'addons/contact_form.html',
form=form)
return self.errors
def get_rendered_contact_form(self):
return self.rendered_contact_form
def post_contact_form(self):
self.errors = False
form = ContactFormForm()
if form.validate_on_submit():
contact_form = ContactForm()
form.populate_obj(contact_form)
db.add(contact_form)
db.commit()
flash( _('Contact form submitted.'), 'info')
return redirect(url_for('pages.thank_you'))
self.errors = True
self.rendered_contact_form = render_template(
'addons/contact_form.html',
form=form)
return self.errors
The ContactFormForm is extended with a hidden parameter identifying the add-on:
addon_id = HiddenField('Addon id')
Using this we now can change the page_view function:
@pages_blueprint.route('/<slug>', methods=['GET', 'POST'])
def page_view(slug):
...
if request.method == 'POST':
addon_id = None
if 'addon_id' in request.form:
addon_id = request.form['addon_id']
if addon_id is not None:
if addon_id == 'contact_form':
addon_contact_form = AddonContactForm()
if addon_contact_form.process_contact_form():
addon_redirect_url = addon_contact_form.get_redirect_url()
return redirect(addon_redirect_url)
# error(s) found during processing
rendered_contact_form = addon_contact_form.get_rendered_contact_form()
addon_error = True
addon_error_message = addon_contact_form.get_error_message()
....
# addon: processing if '{% addon' found
if '{% addon:' in rendered_content_item:
m = re.findall('\{\%\s*(addon)\s*\:\s*([a-z_]+)\s*.*?\%\}', rendered_content_item)
addon_name = None
if m:
addon_name = m[0][1]
if addon_name == 'contact_form':
if request.method == 'GET':
addon_contact_form = AddonContactForm()
if addon_contact_form.get_contact_form():
rendered_contact_form = addon_contact_form.get_rendered_contact_form()
else:
rendered_contact_form = ''
error = True
error_message = addon_contact_form.get_error_message()
rendered_content_item = re.sub('\{\%\s*(addon)\s*\:\s*([a-z_]+)\s*.*?\%\}', rendered_contact_form, rendered_content_item)
elif request.method == 'POST':
# here we just paste the result from the addon
# typically we only come here when an error was detected in the form
rendered_content_item = re.sub('\{\%\s*(addon)\s*\:\s*([a-z_]+)\s*.*?\%\}', rendered_contact_form, rendered_content_item)
return render_template(
...
)
Summary
The above is merely a summary, there is more but I just wanted to give you the basics. I also implemented a FAQ add-on, where we only have to deal with a GET. You can check out the Contact and FAQ pages on this website. This was just a first attempt to implement add-ons, and no, it is not final. I should now define a clear interface of all the methods and attributes an add-on can use or must use. Another time ...
Links / credits
Exact difference between add-ons, plugins and extensions
https://stackoverflow.com/questions/33462500/exact-difference-between-add-ons-plugins-and-extensions
Intro to Flask: Adding a Contact Page
https://code.tutsplus.com/tutorials/intro-to-flask-adding-a-contact-page--net-28982
Read more
Flask Jinja2 Multilanguage WTForms
Recent
- Hiding database UUID primary keys of your web application
- Don't Repeat Yourself (DRY) with Jinja2
- SQLAlchemy, PostgreSQL, maximum number of rows per user
- Show the values in SQLAlchemy dynamic filters
- Secure data transfer with Public Key encryption and pyNaCl
- rqlite: a high-availability and distributed SQLite alternative
Most viewed
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- Connect to a service on a Docker host from a Docker container
- Using PyInstaller and Cython to create a Python executable
- SQLAlchemy: Using Cascade Deletes to delete related objects
- Flask RESTful API request parameter validation with Marshmallow schemas