Python Flask forms not capturing form input - python

I am running the latest Flask, Python 3.10. The app is installed on a Windows IIS Server. I have three forms on a single page. There is one submit button. Each form has a unique ID.
When I fill out one or all of the forms and hit the Submit button there is a view Class handler for the POST request. It is being called but the form data is missing.
One of the form elements looks like the following; the other two are similar but without the render_submit_field call:
<form id="add_session_form" action="{{ url_for('session_api') }}" method="POST" novalidate
role="form">
{{ session_form.hidden_tag() }}
{% for field in session_form %}
{% if not field.flags.hidden %}
{% if field.type == 'BooleanField' %}
{{ render_checkbox_field(field, tabindex=loop.index * 300) }}
{% elif field.type == 'SelectField' or field.type == 'SelectMultipleField' %}
{{ render_select_field(field, tabindex=loop.index * 300) }}
{% elif field.type == 'SubmitField' %}
{{ render_submit_field(field, tabindex=loop.index * 300) }}
{% else %}
{{ render_field(field, tabindex=loop.index * 300) }}
{% endif %}
{% endif %}
{% endfor %}
</form>
The POST method of the view Class looks like:
#staticmethod
def post():
try:
db_session = db()
school_form = SchoolForm(obj=current_user)
class_form = ClassForm(obj=current_user)
session_form = SessionForm(obj=current_user)
school_list = SchoolAPI.get_all_schools()
if school_form.validate_on_submit():
# Copy form fields to user_profile fields
school = School()
school_form.populate_obj(school)
db_session.add(school)
db_session.commit()
school_form = SchoolForm()
if class_form.validate_on_submit():
class_ = Class()
class_form.populate_obj(class_)
db_session.add(class_)
db_session.commit()
classes_form = ClassForm()
if session_form.validate_on_submit():
session = Session()
session_form.populate_obj(session)
db_session.add(session)
db_session.commit()
session_form = ClassForm()
return render_template(
'main/user_page.html',
current_user=current_user,
school_form=school_form,
class_form=class_form,
session_form=session_form,
school_list=school_list
)
except Exception as e:
return Response(repr(e), status=HTTPStatus.BAD_REQUEST)
The curious part is that this was working at the beginning of 2022. I was pulled onto another project and have recently been put back on this one. I updated all dependencies and am not seeing any errors.
I am not sure what to check at this point.
Ideas?

Related

Flask : Storing , Get and Update List in session using flask_session

I have been trying for a couple of days, but I have had no success using a session to store the list( e.g., list of notes).
Below is the code that I have written. It store 2 variables successfully and when I try adding the third variable to the list , it overrides the second variable instead of appending to the list
from flask import Flask, render_template, request, session
from flask_session import Session
app = Flask(__name__)
app.config['SECRET_KEY'] = "some_random"
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_PERMANENT']= False
Session(app)
#app.route("/notes", methods=["GET","POST"])
def addNotes():
if 'notes' not in session:
session['notes'] = []
if request.method == "POST":
note=request.form.get("note")
notes_list = session['notes']
notes_list.append(note)
session['notes'] = notes_list
return render_template("notes.html", notes=session['notes'])
notes.html :
{% extends "layout.html" %}
{% block heading %}
Sticky Notes
{% endblock %}
{% block body %}
<ul>
{% for note in notes %}
<li>{{ note }}</li>
{% endfor %}
</ul>
<form action="{{ url_for('addNotes') }}" method="POST">
<input type="text" name="note" placeholder="Enter a note here">
<button>Add Note</button>
</form>
{% endblock %}
Please suggest if there is any way to store, get and update the list variable stored in the session object.
I have tried using session.modification=True also, which was based on some suggestions on stackoverflow.
#app.route("/notes", methods=["GET","POST"])
def addNotes():
if request.method == "POST":
note = request.form.get("note")
if 'notes' in session:
session['notes'].extend([note])
else:
session['notes'] = [note]
This works for me. But I mostly use sessions to store list of dicts.
Since you are storing your notes in the session, you don't have to pass it in the render template. You can directly access the session object in Jinja
<ul>
{% if session['notes'] %}
{% for note in session['notes'] %}
<li>{{ note }}</li>
{% endfor %}
{% endif %}
</ul>

Flask edit form submit truble

Objects
class MyObj:
def __init__(self, data_dict):
self.id = ''
self.name = ''
self.minDescription = ''
self.descriptions = ''
class MyObjForm(Form):
name = StringField('name')
minDescription = StringField('minDescription')
descriptions = TextAreaField('descriptions')
Routings
This good work in "POST" and "GET" mode. Submit button have a good reactions when click.
#app.route("/create", methods=["GET", "POST"])
#login_required
def create():
if request.method == 'POST':
form = MyObjForm(request.form)
if form.validate():
new_obj = MyObj(request.form)
return redirect(url_for("/"))
else:
return render_template('create.html', form=form)
else:
return render_template('create.html', form=MyObjForm())
When i route in "GET" mode my code is good work and view have old object data but click on submit button is not have any reactions.
#app.route("/edit/<id>", methods=["GET", "POST"])
#login_required
def edit(id):
if request.method == 'GET':
old_obj = d.get_by_id(id)
form = MyObjForm(obj=old_obj)
return render_template('create.html', form=form, id=id)
else:
#never entry
pass
HTML 'create.html'
<form method="post" role="form">
{{ form.csrf }}
{{ macros.render_field(form.name) }}
{{ macros.render_field(form.minDescription) }}
{{ macros.render_field(form.descriptions) }}
<input class="btn special" type="submit" value="Save"/>
</form>
I try this:
<form method="post" role="form" action="{{ url_for('edit', id=id) }}">
and this:
class MyObjForm(Form):
id = StringField()
any not have a progress :(
Wath wrong?
I identified the problem:
If not use args ('obj=new_obj') when MyObjForm create the save button call my route in post mode as well
When i published the question too simplified the description of the problem. Realy MyObjForm include BooleanField:
class MyObjForm(Form):
name = StringField('name')
minDescription = StringField('minDescription')
descriptions = TextAreaField('descriptions')
isArchive = BooleanField('Is not public')
I didn't notice the checkbox stopped showing up on the form for reasons unknown to me. The standard rendering method was used for output:
{{ macros.render_field2(form.isArchive) }}
{% macro render_field2(field) %}
{{ field.label }}
{{ field(**kwargs)|safe }}
{% if field.errors %}
{% for error in field.errors %}
{{ error }}
{% endfor %}
{% endif %}
{% endmacro %}
To solve the issue with the visualization applied the following:
<input type="checkbox" id="isArchive" name="isArchive">
<label for="isArchive">{{ form.isArchive.label }}</label>
{% for error in form.isArchive.errors %}
<li><font color="red">{{ error }}</font></li>
{% endfor %}
One last thing. Did not notice how, but the form also stopped working when you add an object, no longer pass validation. And the reason was {{ form.csrf }}} which had to be replaced with {{ form.csrf_token() }}. Still had to remove the validation parameters for BooleanField.
Now the problem is partially solved.I associate these issues with the installation of the flask-security component. Unfortunately, I was not able to make a normal display of the checkbox, but at least I can move on.
If anyone has any idea how these things can be related to each other or what I'm wrong please let me know. I do not want to leave gaps in knowledge.

Adding parameter to render call when extending flask admin template

I am trying to add content to a Flask-admin list view.
I want to add my own content on top of the list view.
What I have done so far is to extend the default list view and added my own content like so :
{% extends 'admin/model/list.html' %}
{% block body %}
<h3>Submit report</h3>
{% if report_form.errors %}
<ul class="errors">
{% for field_name, field_errors in report_form.errors|dictsort if field_errors %}
{% for error in field_errors %}
<li>{{ form[field_name].label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
<form action="{{ url_for('report.index') }}" method="post" enctype="multipart/form-data">
{{ report_form.file }}
{{ report_form.csrf_token }}
<button type="submit">Send</button>
</form>
{{ super() }}
{% endblock %}
And my custom model view for this template is :
class ReportAdmin(ModelView):
#expose('/', methods=('GET', 'POST'))
def index(self):
report_form = ReportFileForm()
if form.validate_on_submit():
file = form.file.data
#Check for report duplicate
if Report.query.filter(Report.filename == file.filename).all():
flash('Could not add report because a report with filename {} already exists.'.format(file.filename), 'error')
else:
try:
report = parser_factory(file)
flash('Report was submitted succesfully')
return redirect(url_for('report.index_view'))
except ValueError as e:
form.file.errors.append(e)
return self.render('report/index.html', report_form=report_form)
Now my problem is that the list view expects a certain number of parameters to be set (to handle displaying the list).
Those parameters are defined inside base.py
return self.render(
self.list_template,
data=data,
form=form,
delete_form=delete_form,
# List
list_columns=self._list_columns,
sortable_columns=self._sortable_columns,
editable_columns=self.column_editable_list,
# Pagination
count=count,
pager_url=pager_url,
num_pages=num_pages,
page=view_args.page,
# Sorting
sort_column=view_args.sort,
sort_desc=view_args.sort_desc,
sort_url=sort_url,
# Search
search_supported=self._search_supported,
clear_search_url=clear_search_url,
search=view_args.search,
# Filters
filters=self._filters,
filter_groups=self._filter_groups,
active_filters=view_args.filters,
# Actions
actions=actions,
actions_confirmation=actions_confirmation,
# Misc
enumerate=enumerate,
get_pk_value=self.get_pk_value,
get_value=self.get_list_value,
return_url=self._get_list_url(view_args),
)
So of course when trying to display the page, I get an error :
jinja2.exceptions.UndefinedError: 'num_pages' is undefined
My question is : how do I include my parameter into the render call of the parent view?
Thanks!
Try overriding the view's render method where you will be able to inject your variables into the kwargs argument.
Example (untested)
class ReportAdmin(ModelView):
list_template = 'report/index.html'
# Override to allow POSTS
#expose('/', methods=('GET', 'POST'))
def index_view(self):
return super(ReportAdmin, self).index_view(self)
def render(self, template, **kwargs):
# we are only interested in our custom list page
if template == 'report/index.html':
report_form = ReportFileForm()
if report_form.validate_on_submit():
file = report_form.file.data
#Check for report duplicate
if Report.query.filter(Report.filename == file.filename).all():
flash('Could not add report because a report with filename {} already exists.'.format(file.filename), 'error')
else:
try:
report = parser_factory(file)
flash('Report was submitted succesfully')
except ValueError as e:
report_form.file.errors.append(e)
kwargs['report_form'] = report_form
return super(ReportAdmin, self).render(template, **kwargs)

Dynamically change WTForms field type between SelectField and HiddenField

I have a WTForms field (value_currency) that I want to sometimes be a SelectField and sometimes a HiddenField. I use the same view and template for a page that both creates new items and edits existing items. If I load the page to create a new item, I want this field to be a SelectField, and if I load the page to edit an existing item, I want this field to be a HiddenField because it's a non-editable field.
Here is what I have so far:
FORM
class PromoForm(Form):
value = StringField('value')
currencies = Currency.query.order_by(Currency.id).all()
currency_choices = []
for currency in currencies:
currency_choice = (currency.id, currency.name)
currency_choices.append(currency_choice)
value_currency = SelectField('value_currency', choices=currency_choices)
VIEW
#app.route('/promo/<id>', methods=['GET', 'POST'])
#login_required
def promo(id):
form = PromoForm()
# Existing promo will pass in its id
# id = 0 if a new promo is to be created
if id != str(0):
# Load existing promo
promo = Promo.query.get(id)
# display value in decimal format
form.value.default = "{0}.{1:0>2}".format(
promo.value_cents//100, promo.value_cents%100)
form.process()
return render_template('promo.html', promo=promo, form=form)
else:
# New promo
audit_log('GET', client, session=session)
return render_template('promo.html', form=form)
TEMPLATE
{% extends "base.html" %}
{% block content %}
{% if promo is defined %}
<form action="{{ url_for('.promo', id=promo.id) }}" method="post">
{% else %}
<form action="{{ url_for('.promo', id=0) }}" method="post">
{% endif %}
{{ form.hidden_tag() }}
<div>
<label for="value">Promo Value</label>
{% if promo is defined %}
{{ form.value() }}
{% else %}
{{ form.value() }}
{% endif %}
{% for error in form.value.errors %}
<span class="error">[{{ error }}]</span>
{% endfor %}
{% if promo is defined %}
# ----> Promo.value_currency should be a hidden field here (Doesn't work)
{{ promo.value_currency }}
{% else %}
# ----> Promo.value_currency is a select field here (Currently works)
{{ form.value_currency() }}
{% endif %}
</div>
<div class="submit_btn">
{% if promo is defined %}
<input type="submit" value="Update Promo">
{% else %}
<input type="submit" value="Create Promo">
{% endif %}
</div>
{% endblock %}
I know I can just simply hardcode the hidden input element and drop in the value with Jinja, but I prefer to do it with WTForms and not do any form element hard coding. Is that possible?
See (duplicated) question: Flask, WTForms: Is there a way to make a StringField in the form _temporarily_ hidden?.
You cannot just omit the field, and you cannot change its object type (from SelectField to HiddenField) either.
However, you can change its widget object dynamically.
Replace this with a HiddenInput.
from wtforms.widgets import HiddenInput
class PromoForm(Form):
value = StringField('value')
currencies = Currency.query.order_by(Currency.id).all()
currency_choices = []
for currency in currencies:
currency_choice = (currency.id, currency.name)
currency_choices.append(currency_choice)
value_currency = SelectField('value_currency', choices=currency_choices)
def hide_value_currency(self, value):
"""
Hide the value_currency field by morping it into a
HiddenInput.
"""
self.value_currency.widget = HiddenInput()
# wtforms chokes if the data attribute is not present
self.value_currency.data = value
# wtforms chokes on SelectField with HiddenInput widget
# if there is no _data() callable
self.value_currency._value = lambda: value
Call form.hide_value_currency(pre_set_value) in your view when required.
No logic in your template necessary.

reacting to users posts

Regardless of my specific problem below, whats an effective way to respond multiple times to a users posts on the same page. So that on each progressive post on the page can capture new requests and display new information? Forgive me if i'm not describing the problem correctly.
I'm trying to build a page that reacts the the users posts. But i'm running into a :
Bad Request
The browser (or proxy) sent a request that this server could not
understand.
I'm guessing because in my current solution:
#app.route('/survey', methods = ['GET', 'POST'])$
#contributer_permission.require(403)$
def survey():
organization_id = None
survey_header_id = None
survey_section_id = None
organization_selected = None
survey_header_selected = None
survey_section_selected = None
if request.method== 'POST':
if not organization_id:
organization_id = request.form['organization_id']
organization_selected = Organization.query.get(organization_id)
elif not survey_header_id:
survey_header_id = request.form['survey_header_id']
survey_header_selected = SurveyHeader.query.get(survey_header_id)
elif not survey_section_id:
pass
else:
pass
return render_template('survey.html',
organization_class = Organization,
organization_selected = organization_selected,
organization_id = organization_id,
survey_header_id = survey_header_id,
survey_header_selected = survey_header_selected,
survey_section_id = survey_section_id,
survey_section_selected = survey_section_selected)
once i receive the post carrying the survey_header_id. It reloops and
organization_id becomes none
Here is the the accompanying html/json
{% extends "base.html" %}
{% block content %}
<div class ="entries"> <!-- should be a DIV in your style! -->
<form action="{{url_for('survey') }}" method="post" class="add-entry"/>
<dl>
{% if not organization_id %}
{% for organization in organization_class.query.all() %}
<dt><input type="radio", name="organization_id",
value="{{ organization.id }}"/>{{ organization.name }}</dt>
{% endfor %}
<dt><input type ="submit", name="submit_organization"/>
{% elif not survey_header_id %}
<h1>{{ organization_selected.name }}</h1>
{% for survey_header in organization_selected.survey_headers.all() %}
<dt><input type="radio", name="survey_header_id"
value="{{ survey_header.id }}"/>{{ survey_header.name }}
{% endfor %}
<dt><input type ="submit", name="submit_survey_header"/>
{% elif not survey_section_id %}
<p>hi</p>
{% else %}
{% endif %}
<dl>
</form>
</div>
{% endblock %}
What should i be doing?
Bad Request is generally the result of accessing a parameter that doesn't exist, such as request.form['organization_id'] when there is no element in the form with that name.
Your survey route will always attempt to retrieve organization_id from the form because you set it to None, and there's nothing to change that before it's tested. On the second Post, your template does not create an organization_id element at all because the value was passed from the previous Post, so you see an error when survey() still attempts to retrieve it.
You need some way to pass your submitted values from one step to the next. For example, you could write them to hidden or disabled fields within the form so you can grab them and send them back out to the template after each Post, or store your state somewhere else like the session.

Categories