4

I am trying to make a form to upload a file, but the file data is not being sent with the request. I'm manually navigating to my file and hitting submit. My FileRequired validator fails. (And if I don't include it the data field on form.scan_file is empty.)

Here's my form:

from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed, FileRequired

class ScanForm(FlaskForm):
    scan_file = FileField(validators=[FileAllowed(['nii', 'nii.gz', 'zip']), FileRequired()])

Here's my views.py:

from flask import Blueprint, render_template, request, flash, redirect, url_for, session
from .models import Scan
from .forms import ScanForm
from .service import ScanService
from cookiecutter_mbam.utils import flash_errors

blueprint = Blueprint('scan', __name__, url_prefix='/scans', static_folder='../static')

@blueprint.route('/add', methods=['GET', 'POST'])
def add():
    """Add a scan."""
    form = ScanForm(request.form)
    if form.validate_on_submit():
        f = form.scan_file.data
        service = ScanService()
        xnat_uri = service.upload(session['user_id'], session['curr_experiment'], f)
        Scan.create(xnat_uri=xnat_uri)
        flash('You successfully added a new scan.', 'success')
        return redirect(url_for('experiment.experiments'))
    else:
        flash_errors(form)
    return render_template('scans/upload.html',scan_form=form)

Here's my upload.html:

{% extends "layout.html" %}

{% block content %}


<form method="POST" action="{{ url_for('scan.add') }}" enctype="multipart/form-data">
    {{ scan_form.csrf_token }}

    <input type="file" name="file">
    <input class="btn btn-primary" type="submit" value="Submit">

</form>

{% endblock %}

It doesn't look like I'm making the same mistake as this person. What am I doing wrong?

EDIT: Since posting, I have found this question, but on working through the offered solutions, none seem relevant to my situation.

EDIT 2: At one point, I printed request.files in the Werkzeug debugger and it was an empty dict. I can't reconstruct exactly what I did to get that result. Since then, I've inserted some print statements and in fact, request.files has my file object. So I have a way to retrieve my file. But I am supposed to be able to retrieve my file object at form.scan_file.data (see here). Right now this evaluates to None. More specifically, form.scan_file.has_file() evaluates to False. form.data evaluates to {'scan_file': None, 'csrf_token': <long-random-string> }

Even if I have another way of retrieving my file object, a consequence of this problem is that validation isn't working. My form doesn't pass the FileRequired() validation.

EDIT 3: With my new understanding of my problem, I see that it's similar to this question. However, it's at least apparently not duplicative because none of form = ScanForm(request.form), form = ScanForm(), or form = ScanForm(CombinedMultiDict((request.files, request.form))) make any difference to the behavior outlined in Edit 2.

Katie
  • 808
  • 1
  • 11
  • 28
  • 1
    `request.files['file']` – Swift Oct 20 '18 at 00:11
  • 2
    `type="file"` `name="file"` `enctype="multipart/form-data"` – Attack68 Oct 20 '18 at 11:39
  • 1
    @Swift `request.files` is empty. @Attack68 I tried changing the input element as you suggested and it didn't make a difference. – Katie Oct 20 '18 at 12:19
  • If I remember correctly, I don't think that you're doing this right. I will come back with a link to the forms tutorial. If you aren't getting the file in the request, then the client isn't sending the file our you aren't accessing it correctly on the server in your route. I can't remember exactly but if you use flask-wtform then you need to instantiate the class object in the route. – Swift Oct 20 '18 at 15:55
  • @Katie https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-iii-web-forms – Swift Oct 20 '18 at 15:56
  • 1
    @Swift I'm sure I'm not doing it right since it's not working. I've seen that tutorial (and others) and it's still not clear to me how. I instantiate the class object (if you mean the form object) in the route. It was helpful that you outlined the two possibilities, and Dinko below also makes this point: I think the client isn't sending the file. Not sure the best way to check, but in the Network pane on dev tools, the size of the add request is the same whether I refresh the page or try to add a file. So what makes the client not send it? I need to work through Dinko's solution now. – Katie Oct 20 '18 at 16:22
  • revision: I'm less likely to believe the client isn't sending the file, and I may be homing in on a solution. Sorry for the misdirection. If anyone has a good "interpreting network traffic for beginning web developers" tutorial to recommend, I'd take it. – Katie Oct 20 '18 at 17:04
  • 1
    Take a look at this https://developers.google.com/web/tools/chrome-devtools/network-performance/reference – Swift Oct 20 '18 at 17:06
  • Does a dictionary exist at `request.files` please can you print that and show me what it displays? – Swift Oct 20 '18 at 17:12
  • @Swift see edits to the post. – Katie Oct 20 '18 at 18:04
  • Glad to see you solved this in the end – Swift Oct 22 '18 at 18:03
  • Thanks for your help! – Katie Oct 23 '18 at 12:00

2 Answers2

6

First of all, check if your data gets posted on that route. Second, I think you don't need to pass request.form to ScanForm, you just need to instantiate it like:

def add():
    """Add a scan."""
    form = ScanForm()
    ...

To check what gets posted with form, instead of

if form.validate_on_submit():

you can use, and print form.scan_file.data:

if form.is_submitted():
    print(form.scan_file.data)

Lastly, you can render input file with {{scan_form.scan_file }} or <input type="file" name="scan_file"> (name attribute of input element should be equal to "scan_file")

Here is my example:

Form:

class ArticleForm(FlaskForm):
    article_image = FileField('Article_image', validators=[FileRequired()])

Form in template:

<form action="" method="post" enctype="multipart/form-data">
    {{ article_form.csrf_token }}
    {{ article_form.article_image }}
    <input type="submit" value="submit"/>
</form>

Controller (saving file):

article_form = ArticleForm()

        if article_form.validate_on_submit():

            f = article_form.article_image.data
            name = current_user.username + "__" + f.filename
            name = secure_filename(name)
            f.save(os.path.join("./static/article_images/", name))
Dinko Pehar
  • 5,454
  • 4
  • 23
  • 57
  • 1
    Hi Dinko. You're totally right and I should have figured that out 3 hours ago. I didn't mean to be ignoring your answer all this time. I actually tried to work through it and for some reason `{{scan_form.scan_file }} ` the first time I tried it was just causing the file field not to render at all. But now I can't reproduce that, and it's working fine, along with not including anything in the arguments to ScanForm(). I'll upvote and accept your answer. Thanks! – Katie Oct 20 '18 at 19:02
  • 1
    Finally can comment xd. Maybe template didn't render properly. Use debug mode in flask or just put TEMPLATES_AUTO_RELOAD = True into app.config . Also, sometimes static files are used from cache even if change is made. For that, use [hard refresh](https://refreshyourcache.com/en/cache/) – Dinko Pehar Oct 20 '18 at 20:46
  • 1
    Great suggestion @DinkoPehar - `if form.csvfile.data != None:` worked like a charm – Edward Jul 22 '20 at 23:30
  • I wrote this 2 years ago, so I had to take time to remember what I answered here. I'm glad I helped you :D – Dinko Pehar Jul 23 '20 at 07:22
0

I was stupid enough to forget to add enctype="multipart/form-data" to the form tag which caused the form.file.data to contain only the name of the file and not the whole life.