I'm having some trouble getting this validation to work. I want the form to only submit if there's a valid .yaml, .yml, or .json at the end of the URL I'm submitting.
forms.py:
class TemplateForm(FlaskForm):
template_url = URLField( 'Template URL', validators=[InputRequired(), URL(message='error')])
submit = SubmitField('Add Template')
def validate_filetype(self, field):
form = TemplateForm
template_path = form.template_url
ext = os.path.splitext(template_path)[1]
if ext not in ('.json', '.yml', '.yaml'):
raise ValidationError('Invalid File Type')
views.py:
#apps.route('/new-template', methods=['GET', 'POST'])
#login_required
#admin_required
def new_template():
"""Add a new app."""
form = TemplateForm()
if form.validate_on_submit():
template_location = form.template_url.data
flash("added template: " + template_location)
template_path = urlparse(template_location).path
ext = os.path.splitext(template_path)[1]
flash("Extension = " + ext )
if ext == '.json':
flash('var = .json')
template_filename = wget.download(template_location, out='app/storage/templates/json')
flash(template_filename)
elif ext in ('.yml', '.yaml'):
flash('var = .yaml')
return redirect(url_for('apps.index'))
return render_template('apps/new_template.html', form=form)
new_index.html:
{% extends 'layouts/base.html' %}
{% import 'macros/form_macros.html' as f %}
{% import 'macros/check_password.html' as check %}
{% block scripts %}
{% endblock %}
{% block content %}
<div class="ui stackable centered grid container">
<div class="twelve wide column">
<a class="ui basic compact button" href="{{ url_for('apps.index') }}">
<i class="caret left icon"></i>
Back to dashboard
</a>
<h2 class="ui header">
New App Template
<div class="sub header">Add a new app template</div>
{% set flashes = {
'error': get_flashed_messages(category_filter=['form-error']),
'warning': get_flashed_messages(category_filter=['form-check-email']),
'info': get_flashed_messages(category_filter=['form-info']),
'success': get_flashed_messages(category_filter=['form-success'])
} %}
{{ f.begin_form(form, flashes) }}
{{ f.render_form_field(form.template_url, extra_classes=novalidate) }}
{{ f.render_form_field(form.submit) }}
{{ f.end_form() }}
</h2>
</div>
</div>
{% endblock %}
No matter what I do I can't get the raise ValidationError('Invalid File Type') to work. I'm trying to just use https://test.com and it won't throw the validation error.
Please let me know if there's anything else I should attach to provide more information. Everything is importing correctly so I'm pretty sure it's not a dependency issue.
This is the boiler plate I'm using: https://github.com/hack4impact/flask-base
This resolved it. Thank you #ngShravil.py I needed to use a custom validator and separate it into it's own method (outside of the class).
def validate_filetype(form, field):
template_path = field.data
ext = os.path.splitext(template_path)[1]
if ext not in ('.json', '.yml', '.yaml'):
raise ValidationError('Invalid File Type')
class TemplateForm(FlaskForm):
template_url = URLField( 'Template URL', validators=[InputRequired(), URL(message='error'), validate_filetype])
submit = SubmitField('Add Template')
Related
I am trying to implement Recaptcha 2 in a flask web form, however, my form always fails to validate when I have recaptcha enabled. To dig a little deeper I printed out the errors from recaptcha and I always get "the response parameter is missing". I have looked at the source code and it seems this error corresponds to "missing-input-response", however, I can see that the green tick appears when I click the button. Here are before and after images.
I have been searching the web for hours and can't find a reason why this is happening. Here is my flask code and my html.
flask:
import os
from flask import Flask, render_template
from flask_wtf import FlaskForm
from flask_wtf.recaptcha import RecaptchaField
from wtforms import StringField, SubmitField
from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms.validators import DataRequired
from dotenv import load_dotenv
app = Flask(__name__)
# load env variables
load_dotenv(dotenv_path='app.env')
SECRET_KEY = os.getenv('CSRF_TOKEN')
app.config['SECRET_KEY'] = SECRET_KEY
app.config['RECAPTCHA_PUBLIC_KEY'] = ''
app.config['RECAPTCHA_PRIVATE_KEY'] = ''
class InputForm(FlaskForm):
email = StringField('Email-Adresse', validators=[DataRequired()])
postcode = StringField('Postleitzahl', validators=[DataRequired()])
upload = FileField('Angebotsdatei', validators=[FileRequired(), FileAllowed(['png', 'jpg', 'pdf'], "Invalid file format")])
recaptcha = RecaptchaField()
submit = SubmitField('Starten')
#app.route('/', methods=['GET', 'POST'])
def index_2():
email = None
postcode = None
upload = None
form = InputForm()
# validate form
if form.validate_on_submit():
email = form.email.data
postcode = form.postcode.data
upload = form.upload.data
form.email.data = ''
form.postcode.data = ''
form.upload = ''
return render_template(
"index_2.html",
email = email,
postcode = postcode,
upload = upload,
form = form
)
if __name__ == "__main__":
app.run(debug=True, host='127.0.0.1', port=5000)
html:
{% extends 'base.html' %}
{% block content %}
<div class="container containerform pt-3">
<div class="container">
{% if email %}
<p class="text-center">Thanks for submitting to submitting the form!</p>
{% else %}
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ form.email.label(
class="form-label"
) }}
{{ form.email(
class="form-control"
) }}
{{ form.postcode.label(
class="form-label"
) }}
{{ form.postcode(
class="form-control"
) }}
{{ form.upload.label(
class="form-label"
) }}
{{ form.upload(
class="form-control"
) }}
</div>
<br>
<div class="recapture-box">
{% for error in form.recaptcha.errors %}
{{ error }}
{% endfor %}
{{ form.recaptcha }}
</div>
<br>
<div class="submit-button-form pb-4">
{{ form.submit(
class="btn btn-primary btn-form"
) }}
</div>
</form>
{% endif %}
</div>
{% endblock %}
I would appreciate any pointers here.
UPDATE
I still haven't managed to fix the problem, however, the issue lies in validation. While I can click and run recaptcha, I am not able to store the validation from google, and hence my validation_on_submit() returns False.
I recommend you to review the explanation on this web page.
You can use this instead of the if form.validate_on_submit():.
captcha_response = request.form['g-recaptcha-response']
if len(captcha_response) > 0:
I used the modelformset_factory method for multiple photo uploads. But when I want to update the post, I don't know how to do this, I tried something, but this only serves to re-upload photos and upload the uploaded photos again. How can I update or delete a previously uploaded photo? Also, I cannot access the photos in formset as {{formset.image}}. How can I access this?
Views.py
def post_update(request, slug):
post = get_object_or_404(Post, slug=slug)
if not request.user.is_authenticated and not request.user == post.seller or not request.user.is_admin:
raise Http404
ImageFormSet = modelformset_factory(PostImage, form=PostImageForm, extra=5, can_delete = True)
form = PostForm(data=request.POST or None, instance = post, files = request.FILES or None)
formset = ImageFormSet(data=request.POST or None, files= request.FILES or None, queryset=PostImage.objects.filter(post__slug=slug))
if form.is_valid():
if formset.is_valid():
for forms in formset.cleaned_data:
if forms:
image = forms['image']
print(forms['image'])
photo = PostImage(post=post, image=image)
photo.save()
form.save(commit=True)
messages.success(request,"Success")
return HttpResponseRedirect(reverse('gayrimenkul:detail',kwargs={'slug':form.instance.slug}))
return render(request,'post_update.html',{'form':form,'formset':formset,'slug':slug})
post_update.html
{% extends "main_page.html" %}
{% load static %}
{% block icerik %}
{% load crispy_forms_tags %}
<div class="row">
<div class="container">
<h2 class="page_header">İlan Ver</h2>
<hr>
{% if form.errors %}
{{form.errors}}
{% endif %}
<div class="col-md-12">
<form enctype="multipart/form-data" method="POST">
{% csrf_token %}
{{form.media}}
{{form|crispy}}
<div class="row">
<div class="col-md-6">
{{formset}}
</div>
<div class="col-md-6">
<button type="submit" id="inp" class="btn btn-outline-primary" style="float:right;">Kaydet</button>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
Creating posts works successfully.
def post_create(request):
if not request.user.is_authenticated:
raise Http404
ImageFormSet = modelformset_factory(PostImage, form=PostImageForm, extra=5)
if request.method == 'POST':
post_form = PostForm(request.POST, request.FILES)
formset = ImageFormSet(request.POST, request.FILES, queryset=PostImage.objects.none())
if post_form.is_valid():
created_post = post_form.save(commit=False)
created_post.seller = request.user
created_post.save()
for form in formset.cleaned_data:
if form:
image = form['image']
photo = PostImage(post=created_post, image=image)
photo.save()
messages.success(request,'Success')
return HttpResponseRedirect(reverse('gayrimenkul:detail',kwargs={'slug':created_post.get_slug()}))
else:
post_form = PostForm()
formset = ImageFormSet(queryset = PostImage.objects.none())
return render(request,'post_create.html',{'form':post_form,'formset':formset})
Before checking if the form is valid just filter the model of the formset
data = PostImage.objects.filter(post=post)
Give index of the item for a formset item starting form 0 and (f)the item itself and if the item of the id is not changed than save previous item itself else change the database image with the new image you have updated.
if formset.is_valid():
for index, f in enumerate(formset):
if f.cleaned_data:
if f.cleaned_data['id'] is None:
pic = PostImage(post=post, image=f.cleaned_data.get('image'))
pic.save()
elif f.cleaned.data['image'] is False:
pic = PostImage.objects.get(id = request.POST.get('form-' + str(index) + '-id'))
pic.delete()
else:
pic = PostImage(post=post, image=f.cleaned_data.get('image'))
d = PostImage.objects.get(id=data[index].id) #get slide id which was uploaded
d.image = pic.image # changing the database title with new title
d.save()
This might work for you or get you some idea.
So I am working on a form which has two inputs and a check condition. If the check condition is not satisfied raise error. Else take the value in the db.
Given below is my forms.py script.
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, validators
# Define QuoteForm below
class QuoteForm(FlaskForm):
# qauthor = StringField("Quote Author",[validators.DataRequired(message="This field is required"),validators.Length(min =3, max =100,message = "Field must be between 3 and 100 characters long.")])
qauthor = StringField("Quote Author",validators=[validators.DataRequired(message="This field is required"),validators.Length(min =3, max =100,message = "Field must be between 3 and 200 characters long.")])
qstring = StringField("Quote",validators=[validators.DataRequired(message="This field is required"),validators.Length(min =3, max =200,message = "Field must be between 3 and 200 characters long.")])
submit = SubmitField(" Add Quote")
As you can see the min length should be greater than 3 for both fileds.
I am also capturing the error in the HTML page addquote.html
<body>
<h2>QuoteForm</h2>
<form action="", method="post">
<p>
{{form.qauthor.label}} : {{form.qauthor}}<br>
{%for error in form.qauthor.errors%}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</p>
<p>
{{form.qstring.label}} : {{form.qstring}}<br>
{%for error in form.qstring.errors%}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</p>
<p>{{form.submit}}</p>
</form>
</body>
I am calling the form in my flask function. Given below.
#app.route('/addquote/', methods=['GET', 'POST'])
def add_quote():
form = QuoteForm()
if form.is_submitted():
breakpoint()
result = request.form
qqauthor = result['qauthor']
qqstring = result['qstring']
add_tab = Quotes(quoteauthor=qqauthor,quotestring=qqstring)
db.session.add(add_tab)
db.session.commit()
return render_template("addquote_confirmation.html")
return render_template("addquote.html",form=form)
Now the inputs I passed are Quote Author to be: "AT" and Quote to be: "This is a beautiful world"
Quote Author to be: *"AT" and Quote to be: "RT"
I am not getting an error for both these cases where I have mentioned the condition in my forms.py minimum length to be 3.
Why the error is not coming or validation is not happening?
Dependency:
flask_wtf=='0.14.3'
flask=='1.0.2'
You need to call the form's validate() method. This is more conveniently done calling validate_on_submit(), which is a shortcut for checking the active request is a submission (POST, PUT, PATCH, or DELETE) and the form's data is valid (see docs).
#app.route('/addquote/', methods=['GET', 'POST'])
def add_quote():
form = QuoteForm()
if form.validate_on_submit():
breakpoint()
add_tab = Quotes(quoteauthor=form.qqauthor.data, quotestring=form.qqstring.data)
db.session.add(add_tab)
db.session.commit()
return render_template("addquote_confirmation.html")
return render_template("addquote.html",form=form)
Simple full example code below, no database just print out form's data.
app.py
from flask import Flask, render_template, url_for
from flask_wtf import FlaskForm
from markupsafe import Markup
from wtforms import StringField, SubmitField, validators
class QuoteForm(FlaskForm):
author = StringField(
"Quote Author",
validators=[
validators.DataRequired(message="This field is required"),
validators.Length(min=3, max=100, message="Field must be between 3 and 200 characters long.")
]
)
quote = StringField(
"Quote",
validators=[
validators.DataRequired(message="This field is required"),
validators.Length(min=3, max=200, message="Field must be between 3 and 200 characters long.")
]
)
submit = SubmitField(" Add Quote")
app = Flask(__name__)
app.config['SECRET_KEY'] = 'MY SECRET KEY'
#app.route('/', methods=['GET'])
def index():
_quote_url = url_for('add_quote')
return Markup(f'<a href="{_quote_url}">Add Quote</a')
#app.route('/add-quote/', methods=['GET', 'POST'])
def add_quote():
form = QuoteForm()
if form.validate_on_submit():
print(f'Author: {form.author.data}')
print(f'Quote: {form.quote.data}')
return "Quote form is validated"
return render_template("add-quote.html", form=form)
if __name__ == '__main__':
app.run()
add-quote.html
<body>
<h2>QuoteForm</h2>
<form action="" method="post">
{{ form.csrf_token }}
<p>
{% for error in form.errors %}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</p>
<p>
{{ form.author.label }} : {{ form.author }}<br>
{% for error in form.author.errors %}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</p>
<p>
{{ form.quote.label }} : {{ form.quote }}<br>
{% for error in form.quote.errors %}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</p>
<p>{{ form.submit }}</p>
</form>
</body>
When i log in the flash message is not diplayed.
How can i fix it?
#app.route('/Login', methods=['GET','POST'])
def show_login():
if request.method == 'POST':
resp = make_response('your email is ' + request.form['email'])
resp.set_cookie('email_user', request.form['email'])
session['username'] = request.form['email']
flash("login is success", "success")
return redirect(url_for('show_profile', username= session['username']))
if 'username' in session:
username = session['username']
return redirect(url_for('show_profile', username=username))
return render_template('inihtml.html')
Can't see your template file? Have you added below code in your template file? flash is just saving the message in session (behind the scene) and you can access that by function get_flashed_messages(). As you are using category as well (Success in your case), so you have to give with_categories = true.
Below code should work for you if you haven't got already in your .html file.
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class=flashes>
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
I want to be able to display the rows of a database model on my web page as an updatable prepopulated form! I can display the rows on the page easily and I can add a link to each to open a seperate page to edit each row, I can also display each row with the update form, but I cant display the form populated. here is my code:
#bp.route('/stock', methods=['GET', 'POST'])
#bp.route('/stock/<int:id>', methods=['GET', 'POST'])
#login_required
def stock(id=None):
if id is not None:
obj = Stock.query.get_or_404(id)
form = AddStockForm(request.form, obj=obj)
if form.validate_on_submit():
form.populate_obj(obj)
db.session.commit()
return redirect(url_for('stock.stock'))
else:
form = AddStockForm()
page = request.args.get('page', 1, type=int)
stock = Stock.query.order_by(Stock.id.desc()).paginate(
page, current_app.config['ITEMS_PER_PAGE'], False)
next_url = url_for('stock.stock', page=stock.next_num) \
if stock.has_next else None
prev_url = url_for('stock.stock', page=stock.prev_num) \
if stock.has_prev else None
return render_template('stock/stock.html',form=form, title=Stock, stock=stock.items,
next_url=next_url, prev_url=prev_url)
and the stock.html code:
{% extends "base.html" %}
{% block content %}
<div class="main">
<h2>Stock</h2>
<div class="books">Upload Stock csv
</div>
<div class="books">Add a new Item</div>
{% for s in stock %}
{% include 'stock/_stock.html' %}
{% endfor %}
</div>
_stock.html code:
<div class="stock">
<div class="c0"><img src="{{ s.image_url }}" alt="{{ s.image_filename }}"></div>
<form action="{{ url_for('stock._stock', id=s.id) }}" method="post",
enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div>SKU<BR> {{ s.id }} </div>
<div>Date of Purchase<br>{{ form.date(class="input") }}</div>
<div>Description<br>{{ form.description(class="input") }}</div>
<div>Market<br>{{ form.event(class="input") }}</div>
<div>Purchase Price<br>{{ form.achat(class="input") }}</div>
<div>Sale Price<br>{{ form.vente(class="input") }}</div>
<div>Sold<br>{{ form.sold(class="input") }}</div>
<div>{{ form.submit(class="submit") }}</div>
</form>
</div>
on google developer tools each form shows stock/4 or 3 but this does not seem to then populate the form. there are no error messages.
thanks in advance for any help.
regards Paul
EDIT
hi #Greg Cowell, Thank you for your help, I have addapted your code but I Get this error
File "/var/www/accounts/bdf/templates/base.html", line 30, in root
</body>
File "/var/www/accounts/env/lib/python3.7/site-packages/jinja2/runtime.py", line 262, in call
return __obj(*args, **kwargs)
File "/var/www/accounts/env/lib/python3.7/site-packages/flask/helpers.py", line 370, in url_for
return appctx.app.handle_url_build_error(error, endpoint, values)
File "/var/www/accounts/env/lib/python3.7/site-packages/flask/app.py", line 2215, in handle_url_build_error
reraise(exc_type, exc_value, tb)
File "/var/www/accounts/env/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/var/www/accounts/env/lib/python3.7/site-packages/flask/helpers.py", line 358, in url_for
endpoint, values, method=method, force_external=external
File "/var/www/accounts/env/lib/python3.7/site-packages/werkzeug/routing.py", line 2020, in build
raise BuildError(endpoint, values, method, self)
werkzeug.routing.BuildError: Could not build url for endpoint 'stock.stock'. Did you forget to specify values ['id']?
[pid: 29588|app: 0|req: 1/1] 192.168.0.13 () {48 vars in 1095 bytes} [Sun Nov 17 09:26:58 2019] GET /index =>
here is the adapted code:
#bp.route('/stock/stock/<int:id>/', methods=['GET', 'POST'])
#login_required
def stock(id=None):
if id is not None:
group = current_user.group()
try:
stock = Stock.query.filter_by(group=group, id=id).one()
except NoResultFound:
flash('Invalid account.')
return redirect(url_for('stock.add_stock'))
accounts = current_user.group().stock
form = AddStockForm()
form.stock_id.default = stock.id
if form.validate_on_submit():
if form.modify.data:
for s in stock:
if (
item.id == form.stock_id.data and
item.id != form.stock_id.default
):
flash('Another account already has this name.')
return redirect(url_for('stock.add_stock', id=id))
stock.id = form.stock_id.data
db.session.add(stock)
db.session.commit()
form.process() # Do this after validate_on_submit or breaks CSRF token
else:
form = AddStockForm()
page = request.args.get('page', 1, type=int)
stock = Stock.query.order_by(Stock.id.desc()).paginate(
page, current_app.config['ITEMS_PER_PAGE'], False)
next_url = url_for('stock.stock', page=stock.next_num) \
if stock.has_next else None
prev_url = url_for('stock.stock', page=stock.prev_num) \
if stock.has_prev else None
return render_template('stock/stock.html',form=form, title=Stock, stock=stock.items,
next_url=next_url, prev_url=prev_url)
SECOND EDIT
I added this to the top of the view function:
#bp.route('/stock', methods=['GET', 'POST'])
which got rid of the above error, I then got a similar error for the "stock.html code"
{% for s in stock %}
{% include 'stock/_stock.html' %}
{% endfor %}
which I changed to:
{% include {{ url_for('stock._stock', id=['s.id']) }} %}
Which gives me the following error:
jinja2.exceptions.TemplateSyntaxError: expected token ':', got '}'
I use a different method for prepopulating forms.
In summary, I set the default for each form field and process the form before rendering. In the example below I set the value for the account_name field default using form.account_name.default = account.accname and then call form.process() before rendering the form.
#web.route('/accounts/modify/<int:accno>/', methods=['GET', 'POST'])
#login_required
def modify_account(accno):
"""
Modify or delete accounts.
Return a form for modifying accounts or process submitted
form and redirect to Accounts HTML page.
"""
group = current_user.group()
try:
account = Account.query.filter_by(group=group, accno=accno).one()
except NoResultFound:
flash('Invalid account.')
return redirect(url_for('.accounts_page'))
accounts = current_user.group().accounts
form = ModifyAccountForm()
form.account_name.default = account.accname
if form.validate_on_submit():
if form.modify.data:
for item in accounts:
if (
item.accname == form.account_name.data and
item.accname != form.account_name.default
):
flash('Another account already has this name.')
return redirect(url_for('.modify_account', accno=accno))
account.accname = form.account_name.data
db.session.add(account)
db.session.commit()
elif form.delete.data:
for transaction in current_user.group().transactions:
if transaction.account == account:
unknown_account = Account.query.filter_by(
group=current_user.group(), accname='Unknown').one()
transaction.account = unknown_account
db.session.add(transaction)
db.session.commit()
db.session.delete(account)
db.session.commit()
elif form.cancel.data:
pass
return redirect(url_for('.accounts_page'))
form.process() # Do this after validate_on_submit or breaks CSRF token
return render_template(
'modify_account.html', form=form, accno=accno, menu="accounts")
To make an HTML page which has a form for each record and an individual submit button for each record, you need to create multiple forms in your view/route function and then loop through them all in your HTML template.
View/route:
#web.route('/stocks', methods=['GET', 'POST'])
def stocks():
"""Return Stocks HTML page."""
stocks = Stock.query.all()
forms = []
for stock in stocks:
form = ModifyStockForm()
form.stock_id.default = stock.stock_id
form.stock_name.default = stock.stock_name
forms.append(form)
for form in forms:
if form.validate_on_submit():
if form.modify.data:
stock = Stock.query.filter_by(stock_id=form.stock_id.data).one()
stock.stock_name = form.stock_name.data
db.session.add(stock)
db.session.commit()
elif form.delete.data:
stock = Stock.query.filter_by(stock_id=form.stock_id.data).one()
db.session.delete(stock)
db.session.commit()
return redirect(url_for('.stocks'))
form.process() # Do this after validate_on_submit or breaks CSRF token
return render_template('stocks.html', forms=forms, stocks=stocks)
Template:
<h2>Stocks:</h2>
{% for form in forms %}
<form method="post" role="form" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ form.stock_id }}
{{ form.stock_name }}
{{ form.modify }}
{{ form.delete }}
</form>
{% endfor %}
I've put a complete example in this repo: Example of multiple forms on a single page