Adding a contact form to a multilanguage page with content from a database

28 September 2019 Updated 11 October 2019 by Peter

When the page content comes from a database you will want to add a contact form using a tag.

post main image
unsplash.com/@nickmorrison

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