I have a Flask App that passes user input validation when it should fail. I have similar code in another part of the app that works just fine. It seems like the FileAllowed() method is not being called. Or if it is, it's returning true.
This code uploads a user file to s3.
The MultipleFileField() method has a validation check for only image file extensions. However, any file passes this check. The InputRequired() method works just fine.
I've tried multiple variations of this and nothing has worked. It's not a CRSF issue because other routes with similar code work without it.
flask_wtf Form:
class AddImgForm(FlaskForm): # should use InputRequired() not DataRequired()
images= MultipleFileField('Upload Images', validators=[InputRequired(),FileAllowed(['jpg', 'png', 'jpeg', 'tif'])])
submitBTN2 = SubmitField('Upload')
Route:
#users.route("/account", methods=['GET', 'POST'])
#login_required
def account():
form = UpdateAccountForm()
if form.validate_on_submit():
if form.picture.data: # if a picture is provided save picture
picture_file= save_picture(form.picture.data, 'p') # saves picture and returns dict with ['filepath'] and ['filename']
BUCKET= os.environ['BUCKET'] # should send to 'bucket-publicaccess/uploads' bucket in production
s3= boto3.resource("s3",
region_name = "us-east-2", # had to add "us-east-2" as incorrect region was generated
config= boto3.session.Config(signature_version='s3v4'), # must add this to address newer security
aws_access_key_id = os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key = os.environ["AWS_SECRET_ACCESS_KEY"]) # AWS Generated key pairs
s3.Bucket(BUCKET).upload_file(picture_file['filepath'], 'uploads/'+ picture_file['filename']) #upload to s3
current_user.image_file= 'uploads/'+picture_file['filename']
print(current_user.image_file)
os.remove(picture_file['filepath']) # remove file from tmp directory
current_user.username = form.username.data
current_user.email = form.email.data
db.session.commit() # commit changes
flash('Your account has been updated!', 'success')
return redirect(url_for('users.account'))
elif request.method == 'GET':
form.username.data = current_user.username
form.email.data = current_user.email
image_file = current_user.image_file
return render_template('account.html', title='Account',
image_file=image_file, form=form)
HTML:
<form method="POST" action="" enctype="multipart/form-data" id="addImgForm">
{{ addImgForm.hidden_tag() }}
<fieldset class="form-group">
<div class="form-group">
{{ addImgForm.images.label() }}
{{ addImgForm.images(class="form-control-file") }}
{% if addImgForm.images.errors %}
{% for error in addImgForm.images.errors %}
<span class="text-danger">{{ error }}</span></br>
{% endfor %}
{% endif %}
</div>
<div class="form-group">
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
{{ addImgForm.submitBTN2(class="btn btn-outline-info") }}
</div>
</fieldset>
</form>
Any help would be appreciated as most questions are about this failing while this code always passes.
The problem is with the FileAllowed validator, it is expecting a single instance of FileStorage when validating, whereas MultipleFileField passes a list of FileStorage instances to the validator. You can overcome this by writing your own validator, for example:
class MultiFileAllowed(object):
def __init__(self, upload_set, message=None):
self.upload_set = upload_set
self.message = message
def __call__(self, form, field):
# FileAllowed only expects a single instance of FileStorage
# if not (isinstance(field.data, FileStorage) and field.data):
# return
# Check that all the items in field.data are FileStorage items
if not (all(isinstance(item, FileStorage) for item in field.data) and field.data):
return
for data in field.data:
filename = data.filename.lower()
if isinstance(self.upload_set, Iterable):
if any(filename.endswith('.' + x) for x in self.upload_set):
return
raise StopValidation(self.message or field.gettext(
'File does not have an approved extension: {extensions}'
).format(extensions=', '.join(self.upload_set)))
if not self.upload_set.file_allowed(field.data, filename):
raise StopValidation(self.message or field.gettext(
'File does not have an approved extension.'
))
A simple single file example using Flask, Flask-WTF and Flask-Boostrap:
from collections import Iterable
from flask_bootstrap import Bootstrap
from flask import Flask, redirect, url_for, render_template_string
from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed
from markupsafe import Markup
from werkzeug.datastructures import FileStorage
from wtforms.fields import MultipleFileField, SubmitField
from wtforms.validators import InputRequired, StopValidation
app = Flask(__name__)
app.config['SECRET_KEY'] = '123456790'
Bootstrap(app)
class MultiFileAllowed(object):
def __init__(self, upload_set, message=None):
self.upload_set = upload_set
self.message = message
def __call__(self, form, field):
if not (all(isinstance(item, FileStorage) for item in field.data) and field.data):
return
for data in field.data:
filename = data.filename.lower()
if isinstance(self.upload_set, Iterable):
if any(filename.endswith('.' + x) for x in self.upload_set):
return
raise StopValidation(self.message or field.gettext(
'File does not have an approved extension: {extensions}'
).format(extensions=', '.join(self.upload_set)))
if not self.upload_set.file_allowed(field.data, filename):
raise StopValidation(self.message or field.gettext(
'File does not have an approved extension.'
))
class ImagesForm(FlaskForm):
images = MultipleFileField(
'Upload Images',
validators=[
InputRequired(),
MultiFileAllowed(['jpg', 'png', 'jpeg', 'tif'])
]
)
submit = SubmitField('Upload')
upload_template = '''
{% import "bootstrap/wtf.html" as wtf %}
<form method="POST" enctype="multipart/form-data">
{{ wtf.quick_form(form) }}
</form>
'''
#app.route('/')
def index():
return Markup("<a href='uploads'>Go to the uploads<a>")
#app.route('/uploads', methods=['GET', 'POST'])
def upload():
form = ImagesForm()
if form.validate_on_submit():
if form.images:
for image in form.images.data:
print 'Uploaded File: {}'.format(image.filename)
return redirect(url_for('index'))
else:
print form.errors
return render_template_string(upload_template, form=form)
if __name__ == '__main__':
app.run()
Thanks for the above! I had to change to
class MultiFileAllowed(object):
def __init__(self, upload_set, message=None):
self.upload_set = upload_set
self.message = message
def __call__(self, form, field):
if not (all(isinstance(item, FileStorage) for item in field.data) and field.data):
return
for data in field.data:
filename = data.filename.lower()
if isinstance(self.upload_set, Iterable):
print(filename, flush=True)
print(any(filename.endswith("." + x) for x in self.upload_set), flush=True)
if not any(filename.endswith("." + x) for x in self.upload_set):
raise StopValidation(
self.message
or field.gettext("File does not have an approved extension: {extensions}").format(
extensions=", ".join(self.upload_set)
)
)
Related
Is there a way to do this?
Below is an example of what I'm trying to do:
class TestForm(FlaskForm):
email = EmailField('Email', validators=[InputRequired('Email is required.')])
start = SubmitField()
Then in a route:
del form.email.validators
# I also tried:
form.email.validators.remove
Basically I want to use stored data to determine if the field should be required or not for a predefined form.
Dynamically create an internal subclasses of the form within your view. Remove any validators from the fields of the internal subclass and then instance a form from the internal subclass. In code, something like:
Define a form, the first_name field has two validators.
class TestForm(FlaskForm):
first_name = StringField(validators=[InputRequired(), Length(8)])
submit = SubmitField()
In your view:
# Dynamic subclass
class F(TestForm):
pass
# Remove the length validator.
# Validators are in a dict called kwargs
validators = F.first_name.kwargs.get('validators')
for validator in validators:
if isinstance(validator, Length):
validators.remove(validator)
# instance a form
_form = F()
# use the form ....
Single file example app.py:
from flask import Flask, render_template_string
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import InputRequired, Length
app = Flask(__name__)
app.config['SECRET_KEY'] = '13332311ecd738748f27a992b6189d3f5f30852345a1d5261e3e9d5a96722fb9'
class TestForm(FlaskForm):
first_name = StringField(validators=[InputRequired(), Length(8)])
submit = SubmitField()
html_template = '''
{% if form.first_name.errors %}
<ul class="errors">
{% for error in form.first_name.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<form role="form" method="post" action="" name="Form1">
{{ form.hidden_tag() }}
{{ form.first_name }}
{{ form.submit() }}
</form>
'''
success_template = '''
<h1>Success</h1>
'''
#app.route('/', methods=['GET', 'POST'])
def index():
# Dynamic subclass
class F(TestForm):
pass
# Remove the length validator.
# Validators are in a dict called kwargs
validators = F.first_name.kwargs.get('validators')
for validator in validators:
if isinstance(validator, Length):
validators.remove(validator)
# instance a form
_form = F()
# print the validators
for field in _form:
print(f"Field: {field.name} has {len(field.validators)} validator(s)")
for validator in field.validators:
print(validator)
if _form.validate_on_submit():
print('Validation passed')
return render_template_string(success_template)
return render_template_string(html_template, form=_form)
if __name__ == '__main__':
app.run()
You can try:
form.email.validators = ()
This worked well for me.
You can also add back the validators with:
form.email.validators = [InputRequired('Email is required')]
I am confused when I try to insert record from a "GET" request
I will try to explain what I want to do.
I am creating an application to take inventory of assets.
I have 3 tables in my database.
I have a main table called
fixed asset("ActFijo") where all the assets of my company are registered.
Another call Inventory ("Inventario"), which stores the name of each inventory
and another call Inventory_detail ("Inventario_detalle"), where the details or assets in which they are being counted are stored to verify that the equipament or furniture is not being stolen in that location.
From the main table ("ActFijo") I have to search for the furniture or asset and store it in the detail table ("Inventario_detalle")
I'm confused I don't know how to work on a GET request and then do a POST all in one request
Do I have to write my code in parts in a GET request and then POST?
Or can I do everything in the GET request?
This is the code I have so far
I don't know if it's ok, please I need guidance
For example my code does not pass the validation of the form.
if form.is_valid():
I am trying to print, But I don't see any validation error, it doesn't print anything
print(form.errors)
Views.py
from django.shortcuts import redirect, render
from .form import InventarioDetalle_Form, InventarioForm
from .models import ActFijo, Inventario, Inventario_detalle
# Create your views here.
def inventario_home_view(request):
if request.method == "GET":
inv = Inventario.objects.all()
context = {"inventarios": inv}
return render(request, "inventario/index.html", context)
def inventario_crear_view(request):
if request.method == "POST":
form = InventarioForm(request.POST)
if form.is_valid():
form.save()
return redirect("inventario-home")
else:
form = InventarioForm()
inv = Inventario.objects.all()
context = {"formulario": form, "inventarios": inv}
return render(request, 'inventario/crear.html', context)
def inventario_detalle_view(request, inventario):
if request.method == "GET":
# Obtener el valor del input "Buscar"
codigo_activo = request.GET.get("buscar")
print("[CODIGO ACTIVO]:", codigo_activo)
# Buscar el activo en la bd por el campo codigo
try:
activo = ActFijo.objects.get(codigo=codigo_activo)
# print(activo)
except ActFijo.DoesNotExist:
activo = None
if activo:
form = InventarioDetalle_Form(instance=activo)
# print(form)
print(form.errors)
if form.is_valid():
instance = form.save(commit=False)
instance.inventario_id = inventario
instance.save()
else:
print(
"This request does not pass the validation")
else:
print(
"The element does not exist")
context = {"item": Inventario_detalle.objects.all()}
return render(request, "inventario/detalle.html", context)
form.py:
from django import forms
from .models import Inventario, Inventario_detalle
class InventarioForm(forms.ModelForm):
class Meta:
model = Inventario
fields = '__all__'
class InventarioDetalle_Form(forms.ModelForm):
class Meta:
model = Inventario_detalle
fields = '__all__'
url.py
from django.urls import path
from django import views
from . import views
urlpatterns = [
path("", views.inventario_home_view, name="inventario-home"),
path("create/", views.inventario_crear_view,
name="inventario-create"),
path('detail/<int:inventario>',
views.inventario_detalle_view, name="inventario-detail"),
]
detail.html
{% extends "core/base.html" %} {% block content%}
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="titulo mt-5">
<h1>Inventario Detalle</h1>
</div>
<form method="get">
<input type="text" class="form-control" placeholder="Buscar Activo" name="buscar" />
</form>
<div style="overflow-x: auto">
<table>
<thead>
<tr>
<th>Codigo</th>
<th>Descripcion</th>
<th>Accion</th>
</tr>
</thead>
<tbody>
{% for i in item %}
<tr>
<td>{{i.codigo}}</td>
<td>{{i.descripcion}}</td>
<td><button type="button" class="btn btn-danger">Eliminar</button></td>
</tr>
</tbody>
{% endfor %}
</tbody>
</table>
</div>
<div>{{request.GET}}</div>
</div>
</div>
</div>
{% endblock %}
The problem is here
def inventario_detalle_view(request, inventario):
if request.method == "GET":
codigo_activo = request.GET.get("buscar")
print("[CODIGO ACTIVO]:", codigo_activo)
try:
activo = ActFijo.objects.get(codigo=codigo_activo)
print(activo)
except ActFijo.DoesNotExist:
activo = None
if activo:
form = InventarioDetalle_Form(instance=activo)
print(form.errors)
if form.is_valid():
instance = form.save(commit=False)
instance.inventario_id = inventario
instance.save()
else:
print(
"This request does not pass the validation")
else:
print(
"The element does not exist")
context = {"item": Inventario_detalle.objects.all()}
return render(request, "inventario/detalle.html", context)
I believe you don't see any validation errors on the form because for the GET request, you are not passing in anything. The only thing you're passing into the form is the model instance and you're running form.is_valid on it which does not make sense. You dont need to use the form at all. Use this instead.
def inventario_detalle_view(request, inventario):
if request.method == "GET":
codigo_activo = request.GET.get("buscar")
print("[CODIGO ACTIVO]:", codigo_activo)
try:
activo = ActFijo.objects.get(codigo=codigo_activo) # get activo object
activo.inventario_id = inventario # update object
activo.save() # save changes
print(activo)
except ActFijo.DoesNotExist:
# you can do anthing here
# maybe redirect with a message..
pass
context = {"item": Inventario_detalle.objects.all()}
return render(request, "inventario/detalle.html", context)
there are tons of relevant posts:
https://docs.djangoproject.com/en/3.2/topics/http/file-uploads/
Uploading A file in django with ModelForms
https://github.com/axelpale/minimal-django-file-upload-example
I am creating a simple app that allow user to upload css file:
First, validate field field to make sure the file is css and the max size does not exceed 5MB.
fields.py
from django.db import models
from django import forms
from django.template.defaultfilters import filesizeformat
def mb(n):
return n * 1048576
class FileField(models.FileField):
def __init__(self, *args, **kwargs):
self.content_types = kwargs.pop('content_types', [])
self.max_upload_size = kwargs.pop('max_upload_size', [])
super().__init__(*args, **kwargs)
def clean(self, *args, **kwargs):
data = super().clean(*args, **kwargs)
file = data.file
try:
content_type = file.content_type
if content_type in self.content_types:
if file.size > self.max_upload_size:
raise forms.ValidationError('Please keep filesize under {}. Current filesize {}'
.format(filesizeformat(self.max_upload_size), filesizeformat(file.size)))
else:
raise forms.ValidationError('File type rejected')
except AttributeError:
pass
return data
second, implement validation into model
models.py
# Create your models here.
class Css(models.Model):
file = FileField(
upload_to='css',
content_types=['text/css'],
max_upload_size=mb(5),
)
third, create form for model creation
forms.py
class CssCreateForm(forms.ModelForm):
class Meta:
model = Css
fields = ['file']
last, write a callable view
views.py
# Create your views here.
def cssUploadView(request):
if request.method == 'POST':
print(request.POST['file'])
print(type(request.POST['file']))
print(request.FILES)
form = forms.CssCreateForm(request.POST, request.FILES)
if form.is_valid():
print('---------')
print(form.cleaned_data['file'])
else:
print('not valid')
else:
form = forms.CssCreateForm()
return render(request, 'css/css_create.html', {
'form': form,
})
template
<form method="POST">
{% csrf_token %}
<div class="form-title">Upload a new CSS file</div>
{{ form.as_p }}
<div class="button-area">
<button id="go" type="submit">Go</button>
</div>
</form>
The expected result is that
when uploading html file, or css file larger than 5MB, ValidationError will be raised
However, in view
Both request.FILES and form.cleaned_data['file'] fails to retrieve uploaded file.
form.is_valid always returns True
for example, when I upload clean.py:
clean.py
<class 'str'>
<MultiValueDict: {}>
---------
None
I wonder why file fails to upload and validation does not work. Any suggestion will be appreciated.
You have to specify the data encoding method in your html form.
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-title">Upload a new CSS file</div>
{{ form.as_p }}
<div class="button-area">
<button id="go" type="submit">Go</button>
</div>
</form>
I am beginner in Django and recently studied form-validation. I implemented the code but was unable to raise ValidationError for some constraints.
Here are my subsequent file content.
forms.py
from django import forms
from django.core import validators
class formClass(forms.Form):
name = forms.CharField(max_length=128)
email = forms.EmailField(max_length=256)
text = forms.CharField(widget=forms.Textarea)
catchBot = forms.CharField(required=False, widget=forms.HiddenInput,
validators=[validators.MaxLengthValidator(0)])
def clean(self):
cleaned_data = super(formClass, self).clean()
t = self.cleaned_data.get('name')
if t[0].lower() != 'd':
raise forms.ValidationError('Name must start with d.')
return cleaned_data
views.py
from django.shortcuts import render
from formApp import forms
from django.http import HttpResponseRedirect
def formNameView(request):
formObj = forms.formClass()
formDict = {'form': formObj}
if request.method == 'POST':
formObj = forms.formClass(request.POST)
if formObj.is_valid():
# SOME CODE
print("NAME: " + formObj.cleaned_data['name'])
print("EMAIL: " + formObj.cleaned_data['email'])
return HttpResponseRedirect('/users')
return render(request, 'formApp/forms.html', context=formDict)
My valid input works great, but it doesn't happen with my invalid input.
for example: if name = 'Alex', it should raise an error. But it doesn't.
Could someone please help me in it?
EDIT:
[Added forms.html and validators callable.]
Previously, I used validators callable to raise ValidationError instead of clean() method. But the results were same.
Here is my code:
def checkForD(value):
if value[0].lower() != 'd':
raise forms.ValidationError('Name must start with d.')
.
.
.
# in my formClass()
name = forms.CharField(max_length=128, validators[checkForD])
...
Forms.html
<body>
<div class='container'>
<div class='jumbotron'>
<h3>Welcome to the form page.</h3>
<h2>Please insert the form.</h2>
</div>
<form method="post">
{{form.as_p}}
{% csrf_token %}
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</div>
</body>
In your POST block you've redefined formObj to be the bound form, but you haven't replaced the instance in the context dict - so what is passed to the template is the empty unbound form, and no errors will be shown on that template.
The easiest fix would be to move the definition of the dict to the end of the function:
formDict = {'form': formObj}
return render(request, 'formApp/forms.html', context=formDict)
Now the correct instance will be used and the errors will show.
You can try this: don't define the formDict in post, directly define it in return render
return render(request, 'formApp/forms.html', {'form': formObj})
I'm trying to test a form that contains an ImageField and I'm getting an error. I've tried stepping through the Django code but I keep getting lost and I can't see the problem. I've researched how this is supposed to be done and I thought I was doing everything correctly.
Here's the error which occurs at the assertTrue statement in my test:
TypeError: is_valid() takes exactly 2 arguments (1 given)
Here are my files:
# forms.py
class UploadMainPhotoForm(forms.Form):
photo = forms.ImageField(error_messages={'required': err_msg__did_not_choose_photo})
def is_valid(self, request):
# Run parent validation first
valid = super(UploadMainPhotoForm, self).is_valid()
if not valid:
return valid
if request.FILES:
photo_file = request.FILES['photo']
file_name, file_ext = os.path.splitext(photo_file.name)
else:
self._errors['photo'] = 'You forgot to select a photo.'
return False
# Return an error if the photo file has no extension
if not file_ext:
self._errors['photo'] = err_msg__photo_file_must_be_jpeg
return False
return True
# upload_photo.html (template)
<form method="post" enctype="multipart/form-data" action=".">{% csrf_token %}
<input type="file" name="photo" />
<input type="submit" name="skip_photo" value="Skip Photo" />
<input type="submit" name="upload_photo" value="Upload Photo">
</form>
# form_tests.py
class TestUploadMainPhotoForm(TestCase):
def initial_test(self):
post_inputs = {'upload_photo': '[Upload Photo]'}
test_photo = open('photo.jpg', 'rb')
file_data = {'file': SimpleUploadedFile(test_photo.name, test_photo.read())}
self.assertTrue(UploadMainPhotoForm(post_inputs, file_data).is_valid())
Here:
def is_valid(self, request):
You have defined is_valid method with a parameter request and while calling it,
self.assertTrue(UploadMainPhotoForm(post_inputs, file_data).is_valid())
You have not specified the parameter. Hence the error.
You can make use of the RequestFactory to get the request object in the unittest.
from django.utils import unittest
from django.test.client import RequestFactory
class TestUploadMainPhotoForm(TestCase):
def setUp(self):
self.request = RequestFactory()
def initial_test(self):
post_inputs = {'upload_photo': '[Upload Photo]'}
test_photo = open('photo.jpg', 'rb')
file_data = {'file': SimpleUploadedFile(test_photo.name, test_photo.read())}
self.assertTrue(UploadMainPhotoForm(post_inputs, file_data).is_valid(self.request))