current_user.is_authenticated always False (TACACS and flask_login) - python-3.x

Summary
I'm trying to implement a TACACS+ based flask_login system and I'm stuck because the current_user.is_authenticated is always false. I am not using any local databases - the username and password that is submitted via my login form are directly passed to TACACS.
My custom User class implements the 3 attributes and 1 method described in flask_login's documentation here:
Below, you'll find the relevant code for each file so you can set this up in your own environment.
There is a lot of information below, so I wanted to share my actual question as clearly as possible:
How can I connect my custom User class to the current_user proxy?
The underlying issue with my implementation is that when I go to any flask routes with the #login_required decorator, the app thinks the user is not logged in and redirects them to the login page. I've determined it to be because the current_user.is_authenticated attribute is never True.
app.py
from flask import Flask, request, render_template, url_for, flash, redirect, session, Markup
from forms import LoginForm
from flask_login import LoginManager, login_user, logout_user, login_required, current_user, UserMixin
import user_auth as auth
# TODO: Save the user_dict to a pickle file so that users persist between service restarts
user_dict = {}
class User():
def __init__(self, username, password):
self.username = username
self.password = password
# Send TACACS authentication and authorization requests
priv = auth.login(self.username, self.password)
if priv:
self.is_authenticated = True
self.is_active = True
self.is_anonymous = False
if priv == 'admin':
self.priv_lvl = 15
elif priv == 'user':
self.priv_lvl = 10
elif priv == 'employee':
self.priv_lvl = 5
else:
self.is_authenticated = False
self.is_anonymous = True
self.priv_lvl = -1
def get_id(self):
return self.username
app = Flask(__name__)
app.secret_key = 'mysecretkey!'
app.static_url_path = 'static/'
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'index'
#login_manager.user_loader
def load_user(user):
global user_dict
if user in user_dict.keys():
return user_dict[user]
#app.route('/', methods=['GET', 'POST'])
def index():
form = LoginForm()
try:
if user.is_authenticated:
return render_template('test.html')
except UnboundLocalError:
user = None
pass
if form.validate_on_submit():
username = form.username.data
password = form.password.data
user = User(username, password)
username = password = None
print(f"User {user.username} logged in. User authenticated: {user.is_authenticated}")
print(f"Is current_user authenticated? {current_user.is_authenticated}")
if user.priv_lvl >= 0:
# SOLUTION -> The following was missing
login_user(user, remember=form.remember.data)
user_dict.update({
user.username : user
})
# END SOLUTION
print("User is authorized to view test.html.")
return render_template('test.html', current_user=current_user, user=user)
else:
flash(f'Invalid login', 'error')
return render_template('index.html', title='Login Required', form=form, user=user)
return render_template('index.html', title='Login Required', form=form, user=user)
#app.route("/home", methods=['GET'])
#login_required
def home():
return render_template('test.html')
user_auth.py
from tacacs_plus.client import TACACSClient
from tacacs_plus.flags import TAC_PLUS_ACCT_FLAG_START, TAC_PLUS_ACCT_FLAG_WATCHDOG, TAC_PLUS_ACCT_FLAG_STOP
import socket
ISE = 'my.ip.add.rr'
auth_key = 'password'
def login(username, password):
cli = TACACSClient(ISE, 49, auth_key, timeout=10, family=socket.AF_INET)
authen = cli.authenticate(username, password)
if authen.valid:
author = cli.authorize(username, arguments=[b"service=", b"protocol="])
if author.valid:
role = author.arguments[0].decode('utf-8')
if 'user' in role.lower():
priv = 'user'
elif 'admin' in role.lower():
priv = 'admin'
else:
print("User has authenticated successfully, but failed authorization.")
priv = 'employee'
else:
print("User failed authentication.")
priv = None
return priv
forms.py
from flask_wtf import FlaskForm
from wtforms import Form, StringField, SubmitField, BooleanField
from wtforms.fields import PasswordField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
remember = BooleanField("Remember Me")
submit = SubmitField("Login")
index.html
<div id='login-container' class='container'>
<form method="POST" action="">
{{ form.csrf_token }}
{{ form.hidden_tag() }}
<fieldset>
<legend class="border-bottom mb-4">Login</legend>
<p>Use your TACACS credentials.</p>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alerts">
{% for message in messages %}
<div class="alert alert-warning" role="alert">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class='form-group'>
{{ form.username.label(class="form-control-label") }}
{% if form.username.errors %}
{{ form.username(class="form-control form-control-lg is-invalid") }}
<div class='custom-invalid-feedback'>
{% for error in form.username.errors %}
<span>
{{ error }}
</span>
{% endfor %}
</div>
{% else %}
{{ form.username(class="form-control form-control-lg") }}
{% endif %}
</div>
<div class='form-group'>
{{ form.password.label(class="form-control-label") }}
{% if form.password.errors %}
{{ form.password(class="form-control form-control-lg is-invalid") }}
<div class='custom-invalid-feedback'>
{% for error in form.password.errors %}
<span>
{{ error }}
</span>
{% endfor %}
</div>
{% else %}
{{ form.password(class="form-control form-control-lg") }}
{% endif %}
</div>
{{form.remember.label}}
{{form.remember}}
<div class="form-group">
{{ form.submit(class="btn btn-outline-info") }}
</div>
</fieldset>
</form>
</div>
test.html
{% if user.is_authenticated %}
<h2>My custom user class is authenticated</h2>
{% else %}
<h2> My custom user class is NOT authenticated</h2>
{% endif %}
{% if current_user.is_authenticated %}
<h2> current_user is authenticated</h2>
{% else %}
<h2>current_user is NOT authenticated</h2>
{% endif %}
requirements.txt (in case you want to test in your own environment)
Jinja2==2.10
dominate==2.5.1
Flask==1.1.1
flask_login==0.5.0
flask_wtf==0.14.3
plotly==4.5.3
tacacs_plus==2.6
WTForms==2.2.1
Note: If you want to test in your own application and don't want to bother with TACACS, manually set priv equal to admin or user
When I login with valid credentials, here is what I see from my console:
User LetMeIn logged in. User authenticated: True
Is current_user authenticated? False
User is authorized to view test.html.
... And on the test.html page in my browser, I see

I figured it out after dissecting my code for the quadrillionth time.
Simply put, I was missing the login_user() function call. This is what associates the custom User class to the flask_login current_user association.
I also needed to create a simple local username storage method so that the required load_user function could work properly. I did this by adding a globally accessible dictionary that stores my custom User object as a value associated to a key that holds the username.
I will update the code snippets in my original post with these changes in hopes that my efforts will be useful for someone in the future. I couldn't find much online about integrating TACACS with flask_login.

Related

Flask-wtf Recaptcha always returns "the response parameter is missing"

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:

ModelForm Fields are not pre-populated with existing data during updating in Django

I want to update the User and Lab model. I am able to see the form but it is not pre-populated with existing database information even after setting the instance parameter. If I submit a blank form then all fields are reset to blank values in the database. I have tried several solutions available online but nothing works.
My queries -
How do I pre-populate my form with existing data?
If the user doesnt fill out a particular field, I want the previous information to be stored as it is and not as a blank value. How do I achieve this?
I have the following models.py
class Lab(models.Model):
uid = models.OneToOneField(User, on_delete=models.CASCADE)
company=models.CharField(max_length=200,blank=True)
#receiver(post_save, sender=User)
def create_lab_profile(sender, instance, created, **kwargs):
if created:
Lab.objects.create(uid=instance)
#receiver(post_save, sender=User)
def save_lab_profile(sender, instance, **kwargs):
instance.lab.save()
Forms.py
class UserForm(forms.ModelForm):
email=forms.EmailField(max_length=300)
class Meta:
model = User
fields = ('first_name', 'last_name', 'email',)
class LabForm(forms.ModelForm):
class Meta:
model = Lab
fields = ('company',)
views.py
#login_required
def update_user_details(request,pk):
if request.method == 'POST':
user_form = UserForm(request.POST,instance=request.user)
lab_form = LabForm(request.POST,instance=request.user.lab)
if user_form.is_valid() and lab_form.is_valid():
user_form.save()
lab_form.save()
messages.success(request,'Your profile was successfully updated!')
return redirect('user_details')
else:
messages.error(request,('Please correct the error below.'))
else:
user_form = UserForm(instance=request.user)
lab_form = LabForm(instance=request.user.lab)
return render(request, 'update_user_details.html', {'user_form': user_form,'lab_form': lab_form})
template -
{% extends 'base.html' %}
{% block content %}
{% csrf_token %}
<H3> Update Personal information - </H3>
<br>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ user_form.as_p }}
{{ lab_form.as_p }}
<button type="submit" class="btn btn-primary">Save changes</button>
</form>
{% endblock %}
Any help/suggestions will be appreciated!

Why the wtforms not generating error when the Validators condtion is not met?

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>

If statement in method in forms.py not processing

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')

pass a parameter to a view function (form) in a for loop to populate form flask wtforms sqlalchemy

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

Resources