File not uploading with Flask-wtforms in cookiecutter-flask app
Asked Answered
G

4

13

I am having a problem getting a file upload to work in a cookiecutter-flask app (v. 0.10.1). Right now, it is not saving the file uploaded.

Cookiecutter-Flask by default installs WTForms and Flask-WTForms. I have tried adding Flask-Uploads to this but I'm not convinced that module adds anything at this point so I have uninstalled it. This is the Flask-WTF file upload documentation: http://flask-wtf.readthedocs.io/en/latest/form.html#module-flask_wtf.file

The main difference between the documentation and my app is that I seem to have information across more files, in keeping with the conventions of the cookiecutter.

In app_name/spreadsheet/forms.py:

from flask_wtf import Form
from wtforms.validators import DataRequired
from flask_wtf.file import FileField, FileAllowed, FileRequired

class UploadForm(Form):
    """Upload form."""

    csv = FileField('Your CSV', validators=[FileRequired(),FileAllowed(['csv', 'CSVs only!'])])

    def __init__(self, *args, **kwargs):
        """Create instance."""
        super(UploadForm, self).__init__(*args, **kwargs)
        self.user = None

    def validate(self):
        """Validate the form."""
        initial_validation = super(UploadForm, self).validate()
        if not initial_validation:
            return False

In app_name/spreadsheet/views.py:

from flask import Blueprint, render_template
from flask_login import login_required
from werkzeug.utils import secure_filename
from app_name.spreadsheet.forms import UploadForm
from app_name.spreadsheet.models import Spreadsheet
from app_name.utils import flash, flash_errors

blueprint = Blueprint('spreadsheet', __name__, url_prefix='/spreadsheets', static_folder='../static')

@blueprint.route('/upload', methods=['GET', 'POST']) #TODO test without GET since it won't work anyway
@login_required
def upload():
    uploadform = UploadForm()
    if uploadform.validate_on_submit():
        filename = secure_filename(form.csv.data.filename)
        uploadform.csv.data.save('uploads/csvs/' + filename)
        flash("CSV saved.")
        return redirect(url_for('list'))
    else:
        filename = None
    return render_template('spreadsheets/upload.html', uploadform=uploadform)

This is the command line output showing no errors when I upload a file:

 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [04/Sep/2016 10:29:10] "GET /spreadsheets/upload HTTP/1.1" 200 -
127.0.0.1 - - [04/Sep/2016 10:29:10] "GET /_debug_toolbar/static/css/toolbar.css?0.3058158586562558 HTTP/1.1" 200 -
127.0.0.1 - - [04/Sep/2016 10:29:14] "POST /spreadsheets/upload HTTP/1.1" 200 -
127.0.0.1 - - [04/Sep/2016 10:29:14] "GET /_debug_toolbar/static/css/toolbar.css?0.3790246965220061 HTTP/1.1" 200 -

For the uploads/csvs directory I have tried absolute and relative paths and the directory is permissioned 766.

The template file is:

{% extends "layout.html" %}
{% block content %}
    <h1>Welcome {{ session.username }}</h1>

    {% with uploadform=uploadform  %}
        {% if current_user and current_user.is_authenticated and uploadform %}
            <form id="uploadForm" method="POST" class="" action="{{ url_for('spreadsheet.upload') }}" enctype="multipart/form-data">
              <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
              <div class="form-group">
                {{ uploadform.csv(class_="form-control") }}
              </div>
              <button type="submit" class="btn btn-default">Upload</button>
            </form>
        {% endif %}
    {% endwith %}

{% endblock %}

Which generates this HTML:

        <form id="uploadForm" method="POST" class="" action="/spreadsheets/upload" enctype="multipart/form-data">
          <input type="hidden" name="csrf_token" value="LONG_RANDOM_VALUE"/>
          <div class="form-group">
            <input class="form-control" id="csv" name="csv" type="file">
          </div>
          <button type="submit" class="btn btn-default">Upload</button>
        </form>
Gynecoid answered 4/9, 2016 at 15:31 Comment(0)
P
1

Looking through the documentation, the link you provided indicates that the data field of csv is an instance of werkzeug.datastructures.FileStorage. The documentation for FileStorage.save() suggests that:

If the destination is a file object you have to close it yourself after the call.

Could it be that because you aren't closing the file, it isn't being written to disk?

Palmerpalmerston answered 16/9, 2016 at 11:15 Comment(0)
K
1

Try this:

from flask import request

if uploadform.validate_on_submit():
    if 'csv' in request.files:
        csv = request.files['csv']
        csv.save('uploads/csvs/' + csv.filename)
Kimbrakimbrell answered 16/9, 2016 at 12:36 Comment(0)
I
1

The main reason of your problem lands here:

def validate(self):
    """Validate the form."""
    initial_validation = super(UploadForm, self).validate()
    if not initial_validation:
        return False

so in validate method of UploadForm class.

Let's quick investigate what is happening here.

In views.py in line:

if uploadform.validate_on_submit():

flask_wtf package calls validate method. So take a look again on your overwritten method:

def validate(self):
    """Validate the form."""
    initial_validation = super(UploadForm, self).validate()
    if not initial_validation:
        return False

what is wrong here? In case initial_validation would be True, your validate method will return None. So what should happen? Only html rendering:

def upload():
    uploadform = UploadForm()
    if uploadform.validate_on_submit(): # <--- here it's None
        filename = secure_filename(form.csv.data.filename)
        uploadform.csv.data.save('uploads/csvs/' + filename)
        flash("CSV saved.")
        return redirect(url_for('list'))
    else:                               # <--- so this block runs
        filename = None
    # And your app will only render the same view as when using HTTP GET on that method
    return render_template('spreadsheets/upload.html', uploadform=uploadform)

So if overwriting validate method is not necessary, then just remove it, and if is, then adjust it to return True:

def validate(self):
    """Validate the form."""
    initial_validation = super(UploadForm, self).validate()
    if not initial_validation:
        return False
    return True # <-- this part is missing

Of course you can use shortened and I think more appropriate version:

def validate(self):
    """Validate the form."""
    initial_validation = super(UploadForm, self).validate()
    return not initial_validation
Immovable answered 20/9, 2016 at 19:42 Comment(0)
E
1

There is a simpler way to upload files in my opinion. This is something I implemented, hope it can be of help to you. Cause your current requirement looks similar to mine yet, your solution looks a little complex.

So I wanted to make a pdf uploader page, this is what I did.

  1. go to the config.py file or where you define the sql database link
UPLOAD_FOLDER = r'C:\location\app\upload'
ALLOWED_EXTENSIONS = {'pdf'}
  1. go to your views or routes and write this, it checks if the file uploaded matchs the extension requirement.
def allowed_file(filename):
   return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
  1. Then, what i did here is i made a method to store a filename in a table in database. When i call a function,it looks in the folder for that particular filename and retrieves and shows it to me.
@app.route("/#route details here", methods=['GET', 'POST'])
def xyz():

    if request.method == 'POST': 
        if 'file' not in request.files:
            flash(f'No file part', 'danger')
            return redirect(request.url)

        file = request.files['file']

        if file.filename == '':
            flash(f'No selected file', 'danger')
            return redirect(request.url)

        if file and allowed_file(file.filename): #allowed file is the definition i created in point 2. 
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) #save file in a target folder.

            new_report = Report(report_name=filename, report_welder_wps_association_id=report_id) #create a database entry with exact filename

            db.session.add(new_report)
            db.session.commit()

            return redirect(url_for(#redirection on success condition))

    return render_template(#render template requirements go here)
  1. And finally a view to obtain the file whenever i request it. I just query my database, get the filename and redirect it to this view with the filename as parameter, and it spits out the file from the target folder.
@app.route('/upload/<filename>')
def uploaded_file(filename) -> object:
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

And this is the only form i need to define :

class XYZ(db.Model):
    __tablename__ = 'xyz'

    uploaded_file_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    uploaded_file_name = db.Column(db.String(300), nullable=False)
Etrem answered 30/4, 2019 at 5:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.