I have seen a few similar questions here, but none of the solutions seem to apply here(the problem is usually that it lacks a csrf token, which is not the case here).
I have a form with four fields- 3 drop down lists with SelectField and one StringField- built using flask wtforms. I tried adding an edit feature to this, which uses the same HTML template, but this it isn't getting validated(not entering the form.validate_on_submit section). This is the code for the function:
#app.route('/movements/<int:movement_id>/edit', methods=['GET', 'POST'])
def edit_movement(movement_id):
movement = Movement.query.get_or_404(movement_id)
form = MovementForm()
if form.validate_on_submit():
product = Product.query.filter_by(id=form.product.data).first()
from_location = Location.query.filter_by(id=form.from_location.data).first()
to_location = Location.query.filter_by(id=form.to_location.data).first()
if int((Balance.query.filter_by(product = product.name).filter_by(location = from_location.name).first()).balance) < int(form.quantity.data) and from_location.name != "":
flash("Invalid movement. Quantity of the product is insufficient.")
else:
movement.product_id = product.id
movement.product = product.name
movement.from_location_id = from_location.id
movement.from_location = from_location.name
movement.to_location_id = to_location.id
movement.to_location = to_location.name
movement.quantity = form.quantity.data
db.session.commit()
flash('The product movement has been edited!', 'success')
return redirect(url_for('movements'))
elif request.method == 'GET':
form.product.choices = [(product.id,product.name) for product in Product.query.all()]
form.from_location.choices = [(location.id,location.name) for location in Location.query.all()]
form.to_location.choices = [(location.id,location.name) for location in Location.query.all()]
form.quantity.data = movement.quantity
edit_button = True
return render_template('movements.html',form=form, edit_button=edit_button)
This is the code for the form:
class MovementForm(FlaskForm):
product = SelectField("Product", choices = [])
from_location = SelectField("From Location", choices = [], coerce=int)
to_location = SelectField("To Location", choices = [], coerce=int)
quantity = StringField("Quantity", validators=[DataRequired()])
add_movement = SubmitField("Add Movement")
And this is the model for the table:
class Movement(db.Model):
id = db.Column(db.Integer, primary_key=True)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'), nullable=False)
product = db.Column(db.String(50), nullable=False)
from_location_id = db.Column(db.Integer, db.ForeignKey('location.id'))
from_location = db.Column(db.String(50))
to_location_id = db.Column(db.Integer, db.ForeignKey('location.id'))
to_location = db.Column(db.String(50))
quantity = db.Column(db.Integer, nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
The HTML code for the form:
<form action="" method="POST">
{{ form.csrf_token }}
{{ form.product.label }}
{{ form.product }}
{{ form.from_location.label }}
{{ form.from_location }}
{{ form.to_location.label }}
{{ form.to_location }}
{{ form.quantity.label }}
{{ form.quantity }}
{% if edit_button %}
<input type="submit" value="Edit Movement">
{% else %}
{{ form.add_movement }}
{% endif %}
</form>
validate_on_submit is a convenient function that combines a check for whether the form was submitted (i.e., a POST, PUT, PATCH, or DELETE) with call to form.validate. If validation fails, the dictionary held byform.errors will get populated with useful information.
A useful step to debug your problem would be to log (print) the contents of form.errors if validate_on_submit returns False.
The form.errors in your code is as follows:
{'product': ['Not a valid choice'], 'from_location': ['Not a valid choice'], 'to_location': ['Not a valid choice']}
So it is reasonable for the validate_on_submit to return False.
If you comment out these fields and leave only the quantity it should work. The following changes worked for me and I managed to update the quantity in the DB. If it works also for you then you could try to uncomment each field and debug further.
Change the form to:
class MovementForm(FlaskForm):
quantity = StringField("Quantity", validators=[DataRequired()])
add_movement = SubmitField("Add Movement")
and the route to:
#app.route('/movements/<int:movement_id>/edit', methods=['GET', 'POST'])
def edit_movement(movement_id):
movement = Movement.query.get_or_404(movement_id)
form = MovementForm()
if form.validate_on_submit():
movement.quantity = form.quantity.data
db.session.commit()
flash('The product movement has been edited!', 'success')
return redirect(url_for('movements'))
elif request.method == 'GET':
form.quantity.data = movement.quantity
print(form.errors)
edit_button = True
return render_template('movements.html', form=form, edit_button=edit_button)
Change the template to:
<form action="" method="POST">
{{ form.csrf_token }}
{{ form.quantity.label }}
{{ form.quantity }}
{% if edit_button %}
<input type="submit" value="Edit Movement">
{% else %}
{{ form.add_movement }}
{% endif %}
</form>
Update:
The issue you are facing is described here.
Please try the following and if it works as expected then set similarly the remaining fields (it worked for me and I managed to update the quantity and the from_location/id in the DB):
the form:
class MovementForm(FlaskForm):
fromloc = [(location.id,location.name) for location in Location.query.all()]
from_location_id = SelectField("From Location ID", choices = fromloc, coerce=int)
quantity = StringField("Quantity", validators=[DataRequired()])
add_movement = SubmitField("Add Movement")
the route:
#app.route('/movements/<int:movement_id>/edit', methods=['GET', 'POST'])
def edit_movement(movement_id):
movement = Movement.query.get_or_404(movement_id)
form = MovementForm()
if form.validate_on_submit():
movement.from_location_id = form.from_location_id.data
movement.from_location = (Location.query.filter_by(id = form.from_location_id.data).first()).name
movement.quantity = form.quantity.data
db.session.commit()
flash('The product movement has been edited!', 'success')
return redirect(url_for('movements'))
elif request.method == 'GET':
form.from_location_id.choices = [(location.id,location.name) for location in Location.query.all()]
form.quantity.data = movement.quantity
print(form.errors)
edit_button = True
return render_template('movements.html', form=form, edit_button=edit_button)
the template:
<form action="" method="POST">
{{ form.csrf_token }}
{{ form.from_location_id.label }}
{{ form.from_location_id }}
{{ form.quantity.label }}
{{ form.quantity }}
{% if edit_button %}
<input type="submit" value="Edit Movement">
{% else %}
{{ form.add_movement }}
{% endif %}
</form>
I'm still not quite sure why my form wasn't validating, but my function started working after I replaced if form.validate_on_submit() with if request.method == 'POST'.
Related
I'm using WTForms and Flask, I am trying to create a form where I can enter information about a recipe, but the product_name SelectField is returning None every time.
The form:
class CreateRecipeForm(Form):
product_name = SelectField(choices=get_craftables_options())
product_quantity = IntegerField(default=1)
job_field = SelectField(choices=['ALC', 'GSM', 'WVR'])
line_item_list = FieldList(FormField(RecipeLineForm), min_entries=6)
save_button = SubmitField()
The view:
#bp.route('/edit/new', methods=('GET', 'POST'))
def create_recipe():
form = CreateRecipeForm()
if request.method == 'POST':
selected_product = Item.query.get(form.product_name.data)
(do stuff here)
The template
{% block content %}
<form method="post">
{{ render_field(form.product_name) }}
{{ render_field(form.product_quantity) }}
{{ render_field_no_label(form.line_item_list) }}
{{ render_field_no_label(form.save_button) }}
</form>
{% endblock %}
I believe your issue lies in declaring the product_name. Make sure the get_craftables_options() is supposed to be a function and is returning a list of items compatible with the choices argument.
product_name = SelectField(choices=get_craftables_options())
views.py
#login_required(login_url='login/')
def add_country(request):
if request.method == 'POST':
form = CountryForm(request.POST,request.FILES)
if form.is_valid():
new_form = form.save(commit=False)
new_form.edited_by = request.user
new_form.save()
return redirect('country_details')
else:
form = CountryForm()
context = {'form':form}
return render(request,'add_country.html',context)
models.py
class Countries(models.Model):
CONTINENTS = [
('Asia','Asia'),
('Europe','Europe'),
('Africa','Africa'),
('Oceania','Oceania'),
('North America','North America'),
('South America','South America'),
]
name = models.CharField(max_length=75)
continent = models.CharField(max_length=50,choices=CONTINENTS,null=True)
landmark = models.CharField(max_length=100,null=True)
food = models.CharField(max_length=100,null=True)
entertainment = models.CharField(max_length=100,null=True)
flag = models.FileField(upload_to='flags', default='default.png',null=True)
image = models.FileField(upload_to='travel', default='default.png',null=True)
edited_by = models.OneToOneField(User,on_delete=models.CASCADE,null=True)
last_updated = models.DateTimeField(auto_now_add=True,null=True)
def __str__(self):
return self.name
add_country.html
<form method="POST" action="" enctype="multipart/form-data">
{% csrf_token %}
{{ form.name.label }}<br>
{{ form.name }}<br><br>
{{ form.landmark.label }}<br>
{{ form.landmark }}<br><br>
{{ form.food.label }}<br>
{{ form.food }}<br><br>
{{ form.entertainment.label }}<br>
{{ form.entertainment }}<br><br>
{{ form.flag.label }}<br>
{{ form.flag }}<br><br>
{{ form.image.label }}<br>
{{ form.image }}<br><br>
<input type="submit" class="btn btn-primary" value="Add">
</form>
I have an issue that after I added the edited_by to assign the currently logged in user into that column then the form could not be submitted and only stayed on the same page instead of redirecting to the page that I want. I have tried different ways to make the form being submitted such as put request.method == "POST" and the page didn't work. However before I added edited_by into the models the form could be submitted accordingly and the data is being updated. May I ask what is the method to assign the user into the column edited_by after that user has added a post?
I think you have included edited_by field in your forms too.
If you are handling this field by yourself in the views then remove this field from your forms.
class CountryForm(forms.ModelForm):
class Meta:
model = Country
exclude = ['edited_by'] # or specify only required fields in form
Now your view will work fine.
Note: You can display your form's errors with {{form.errors}} in your template.
I am trying to commit fields generated from a FiedList but getting the error:
AttributeError: 'str' object has no attribute 'data'
What I'm trying to do is add a list of fields to the database which I can then retrieve and display on the page.
#App.py
#app.route('/', methods=['GET', 'POST'])
def index():
form = MainSubscriptionForm()
if form.validate_on_submit():
for x in form.subscription:
sub = Subscription(company=x.company.data, description=x.description.data)
db.session.add(sub)
db.session.commit()
elif request.method == 'GET':
list = Subscription.query.all()
return render_template('index.html', title="Home", form=form, list=list)
#forms.py
class SubscriptionForm(FlaskForm):
company = StringField(('Company'), validators=[DataRequired(), Length(min=0, max=20)])
description = StringField(('Description'), validators=[Length(min=0, max=120)])
save = SubmitField('Save')
class MainSubscriptionForm(FlaskForm):
subscription = FieldList(FormField(SubscriptionForm), min_entries=1)
#models.py
class Subscription(db.Model):
id = db.Column(db.Integer, primary_key=True)
company = db.Column(db.String(20))
description = db.Column(db.String(120))
#index.html
{% extends "base.html" %}
{% from 'bootstrap/form.html' import render_form_row %}
{% block content %}
<form method="post">
{{ form.csrf_token() }}
{% for sub in form.subscription %}
{{ render_form_row(sub) }}
{% endfor %}
</form>
{{ list }}
{% endblock %}
The issue was that a dictionary was being sent and therefore it was crashing on the below line:
sub = Subscription(company=x.company.data, description=x.description.data)
The solution was to get the key value as per below:
sub = Subscription(company=x.data['company'], description=x.data['description'])
My form has initial values in it. I use form.as_hidden to hide the values and pass those values through a POST request. However, the hidden values are not passing through. Is there a way through this?
views.py
def car_detail_view(request, id):
if request.method == "POST":
form = CarForm(request.POST)
print(form.is_valid())
if form.is_valid():
car_save = form.instance
get_car = Car.objects.get(number_plate=car_save.number_plate)
get_car.available = False
get_car.save()
return redirect('/')
else:
print(form.errors)
else:
car = Car.objects.get(id=id)
form = CarForm(initial={'brand':car.brand, 'number_plate':car.number_plate, 'price':car.price,
'available':car.available})
args = {
'car':car,
'form':form
}
return render(request, 'map/confirmation.html', args)
confirmation.html
<h1>Confirmation of Booking</h1>
{% block content %}
<p>Brand: {{ car.brand }}</p>
<p>Number Plate: {{ car.number_plate }}</p>
<p>Price: {{ car.price }}</p>
<p> Are you sure you want to book? <p>
<form class="" method="post">
{% csrf_token %}
{{ form.as_hidden }}
<input type="submit" value="Book {{ car.brand }}">
</form>
{% endblock %}
Error
<ul class="errorlist"><li>brand<ul class="errorlist"><li>This field is required.</li></ul></li><li>number_plate<ul class="errorlist"><li>This field is required.</li></ul></li><li>price<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
Django doesn't have a form.as_hidden method. Therefore {{ form.as_hidden }} will render as the empty string '' in your template.
You can use the as_hidden method for individual form fields.
{{ form.number_plate.as_hidden }}
If you use values from hidden fields, you might need to add code to prevent the user altering the field values (e.g. with their browser's developer tools). However, in your case you don't need to get the values from the form, you can fetch them from the database.
def car_detail_view(request, id):
if request.method == "POST":
car = Car.objects.get(id=id)
car.available = False
car.save()
return redirect('/')
else:
car = Car.objects.get(id=id)
args = {
'car':car,
}
return render(request, 'map/confirmation.html', args)
Once you've got this working, you might want to think about what happens if two users try to book the same car at once.
I just started learning Django and for this project I'm following the "Tango with Django" tutorial book. I have a problem with the input field of a form not showing up, while the button seems to be rendered fine.
Here's my code:
models.py
[...]
class Idea(models.Model):
keyword = models.ForeignKey(Keyword)
word = models.CharField(max_length=120)
count = models.IntegerField(default=1)
def __str__(self):
return self.word
forms.py
[...]
class Meta:
model = Keyword
fields = ('name',)
class IdeaForm(forms.ModelForm):
word = forms.CharField(max_length=120)
count = forms.IntegerField(widget=forms.HiddenInput(), initial=1)
class Meta:
model = Idea
fields = ('word',)
exclude = ('keyword',)
views.py
[...]
def keyword_detail(request, keyword_name_slug):
form = IdeaForm()
context_dict = {}
try:
keyword = Keyword.objects.get(slug=keyword_name_slug)
ideas = Idea.objects.filter(keyword=keyword)
context_dict['keyword'] = keyword
context_dict['ideas'] = ideas
except Keyword.DoesNotExist:
context_dict['keyword'] = None
context_dict['ideas'] = None
if request.method == 'POST':
form = IdeaForm(request.POST)
if form.is_valid():
idea = form.save(commit=False)
idea.keyword = keyword
idea.count = 1
idea.save()
return keyword_detail(request, keyword_name_slug)
else:
print(form.errors)
context_dict['form'] = form
return render(request, 'openminds/keyword.html', context_dict)
keyword.html
[...]
<h3>Add a new Idea</h3>
<div>
<form id="idea_form" method="post" action="">{% csrf_token %}
{% for hidden in forms.hidden_fields %}
{{ hidden }}
{% endfor %}
{% for field in forms.visible_fields %}
{{ field.errors }}
{{ field }}
{% endfor %}
<input type="submit" name="submit" value="Add Idea" />
</form>
</div>
I think you're passing in form to the template, but attempting to use forms.