python-3.xflaskflask-wtformsflask-mongoengine

Removing csrf_token before inserting into MongoDB


In my application I use flask_mongoengine to connect to a MongoDB. I get the error

The fields "{'csrf_token'}" do not exist on the document "Study"

when inserting a document.

I manage to overcome the error by manually deleting the csrf_token before saving (see code below). Is there a more elegant way to overcome this error? This would be especially helpful for models containing EmbeddedDocument since all the embedded documents have a csrf_token.

app.py

from flask import Flask, render_template, request
from flask_bootstrap import Bootstrap
from flask_mongoengine import MongoEngine
from flask_wtf.csrf import CSRFProtect
from flask_mongoengine.wtf import model_form
from wtforms.meta import DefaultMeta


db = MongoEngine()
bootstrap = Bootstrap()
csrf = CSRFProtect()

app = Flask(__name__)


app.secret_key = 'My secret key'

app.config['MONGODB_HOST'] = 'host'
app.config['MONGODB_PORT'] = 27017
app.config['MONGODB_DB'] = 'database'
app.config['MONGODB_USERNAME'] = 'user'
app.config['MONGODB_PASSWORD'] = 'user'

db.init_app(app)
bootstrap.init_app(app)
csrf.init_app(app)

class Study(db.Document):
    name = db.StringField()

@app.route('/', methods=['GET', 'POST'])
def hello_world():

    StudyForm = model_form(Study, field_args={})

    study_form = StudyForm()

    if request.method == 'POST':
        study_form = StudyForm(request.form)

        if study_form.validate():
            del study_form._fields['csrf_token']
            study_form.save()
            return "Success"

    return render_template('index.html', form=study_form)

if __name__ == '__main__':
    app.run(debug=True,host='0.0.0.0')

index.html

{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block content %}

<div class="container">
    <form action="" method="POST">
        {{ form.hidden_tag() }}
        {{ form.name.label() }}
        {{ form.name() }}
         <input type=submit class='btn btn-primary '>
    </form>    
</div>    

{% endblock %

Solution

  • This is a quirk/bug that can be solved as follows.

    Option 1

    Pass an instance of your model (Study) to the form constructor. You can just pass an empty instance which will get filled with the form values when you save it.

    study_form = StudyForm(request.form, instance=Study())
    study_form.save()
    

    Option 2

    Explicitly create, fill and save the model instance yourself. To fill the form, use Form.populate_obj(). Note that you must then call save() on the model instance, not the form.

    study = Study()
    study_form.populate_obj(study)
    study.save()
    

    Root cause

    When Flask-MongoEngine's ModelForm.save() needs to create the model instance, it fills it with values from Form.data which contains all form fields, including the csrf_token. By contrast, Form.populate_obj() discriminates between field types, skipping CSRFTokenField instances as desired.

    class ModelForm(FlaskForm):
        """A WTForms mongoengine model form"""
        ...
    
        def save(self, commit=True, **kwargs):
            if self.instance:
                self.populate_obj(self.instance)
            else:
                self.instance = self.model_class(**self.data)
    
            if commit:
                self.instance.save(**kwargs)
            return self.instance