Let's say I have a Django form with ChoiceField:
class MyForm(forms.Form):
name = forms.CharField(label="Name", required=True)
some_object = forms.ChoiceField(label="Object", choices=[(0, '-----')] + [(x.id, x.name) for x in Obj.objects.all()])
Choisefield is being initialized with list of objects headed by 'empty choice'. There is no object with pk=0.
In that form I have a clean() method:
def clean(self):
if not Obj.objects.filter(self.cleaned.data['some_object'].exists():
self.errors.update({'some_object': ['Invalid choice']})
It works well when I'm sending a form to a server, if data in it doesn't match conditions, field.is_valid returns False and I render form with error messages. But, when I create an empty form like this:
if request.method == 'GET':
data = {'name': 'Untitled',
'some_object': 0}
form = MyForm(data)
return render(request, 'app\template.html', {'form': form})
Django renders form with error message ('Invalid choice') even though form was just created and 'Object' select was intended to be set in empty position. Is it possible to disable form clean() method in specific cases? Or maybe I'm doing this all wrong? What is the best practice to create an empty form?
The problem is that a form with any data dictionary passed to it counts as a "bound" form, against which Django will perform data validation. See here for details: https://docs.djangoproject.com/en/2.1/ref/forms/api/#bound-and-unbound-forms
You want an "unbound" form - to have default initial values in here, just set the initial property on your form fields. See full details here: https://docs.djangoproject.com/en/2.1/ref/forms/fields/#initial
Related
I am using formset to input my data into the database but for some reason it just doesn't validate, whenever I test in the terminal and call the .is_valid() It just returns false no matter what I try. Here's the code in my views.py and forms.py . Any help will be much appreciated!
# Advanced Subjects (Advanced Biology)
def form_5_entry_biology_view(self, request):
current_teacher = User.objects.get(email=request.user.email)
logged_school = current_teacher.school_number
students_involved = User.objects.get(school_number=logged_school).teacher.all()
data = {"student_name": students_involved}
formset_data = AdvancedStudents.objects.filter(class_studying="Form V", combination="PCB")
student_formset = formset_factory(AdvancedBiologyForm, extra=0)
initial = []
for element in formset_data:
initial.append({"student_name": element})
formset = student_formset(request.POST or None, initial=initial)
print(formset.is_valid())
context = {
"students": students_involved,
"formset": formset,
"class_of_students": "Form V",
"subject_name": "Advanced Biology",
}
return render(request, "analyzer/marks_entry/marks_entry_page.html", context)
And here is my forms.py
class AdvancedBiologyForm(forms.ModelForm):
student_name = forms.CharField()
class Meta:
model = ResultsALevel
fields = ('student_name', 'advanced_biology_1', 'advanced_biology_2',
'advanced_biology_3',)
Before using request.POST and is_valid() you probably want to check if there actually is a post request or if the page is just viewed:
def form_5_entry_biology_view(self, request):
current_teacher = User.objects.get(email=request.user.email)
logged_school = current_teacher.school_number
students_involved = User.objects.get(school_number=logged_school).teacher.all()
data = {"student_name": students_involved}
formset_data = AdvancedStudents.objects.filter(class_studying="Form V", combination="PCB")
# Here you are creating the formset using the model
student_formset = formset_factory(AdvancedBiologyForm, extra=0)
# Here you are generating your initial data
initial = []
for element in formset_data:
initial.append({"student_name": element})
# Here you are using the initial data to create pre-populated
# forms with it using the formset.
# These forms will be displayed when the page loads.
formset = student_formset(initial=initial)
context = {
"students": students_involved,
"formset": formset,
"class_of_students": "Form V",
"subject_name": "Advanced Biology",
}
# But if the user hits the "submit"-Button...
if request.method == 'POST':
# ... you don't want to have the formset with your
# initial data. Instead you want the entries by the user
# which are transmitted in request.POST to populate the
# formset forms.
formset = student_formset(request.POST or None)
# Now you can validate the formset with the fields which
# got input the the user; not the "initial" data like in
# your original code
if formset.is_valid():
# This runs formset validation.
# You can define your own formset validations like
# you would for models/forms.
for form in formset:
# And / Alternatively:
# you could in theory also add another "if form.is_valid():" in here
# This would trigger any validation on the
# model/form; not the validators on the formset.
form.save()
return HttpResponseRedirect(...
return render(request, "analyzer/marks_entry/marks_entry_page.html", context)
Otherwise you might call is_valid() on an unbound form.
From Django docs:
If the form is submitted using a POST request, the view will once again create a form instance and populate it with data from the request: form = NameForm(request.POST) This is called “binding data to the form” (it is now a bound form).
Basically if a form is empty it's unbound, and if it's populated with data it gets bound after POST. When you open a page and immediately try "is_valid()" it will basically always be false as you are checking if an empty form is valid; which it likely never is.
To point out the error:
formset = student_formset(request.POST or None, initial=initial)
print(formset.is_valid())
This is not valid. Because initial values are not equal to populating a form field with "real" values. So what happens is that it tries to populate the fields in the form with request.POST or None.
But you don't have a if request.method == 'POST': condition. So your code will just run through before hitting the last line of code which is the return statement displaying the page.
This means that your code validates request.POST or None before the user even saw the page. So there is no way a user could have already entered data and hit submit. Which means there is no POST-request, so it always turns None. So you are basically calling is_valid() on a form that has no field values in it which leads to the validation failing.
EDIT 1: I just noticed in your forms.py you have written:
fields = ('student_name', 'advanced_biology_1', 'advanced_biology_2',
'advanced_biology_3',)
This should be a list instead:
fields = ['student_name', 'advanced_biology_1', 'advanced_biology_2',
'advanced_biology_3',]
EDIT 2: fixed wrong variable name
EDIT 3: added extensive comments to clarify what's happening in the code
EDIT 4: pointed out cause of problem more distinctly.
If you dynamically add a field to a WTForms form, the newly-added fields are not validated (even if you add validators).
For instance, in Flask:
#app.route('/add', methods=['GET', 'POST'])
def add():
if 'state' not in session:
session['state'] = 1
form = MyForm()
if request.method == 'POST' and form.validate():
if session['state'] == 1:
setattr(
MyForm,
'asd',
StringField(
'asdfield',
validators = [
DataRequired(),
Length(min=1)
]
)
)
form = MyForm()
session['state'] = 2
return render_template(
'add.html',
form=form
)
print(len(form.asd.data)) # can equal 0
session['state'] = 1
return redirect('/add')
return render_template(
'add.html',
form=form
)
I believe this is due to the fact that form = MyForm() is run every time you go to /add, so even if session['state'] == 2 you run form.validate() on a default form which does not have the dynamically-added field. Therefore, this field cannot be part of the form validation process.
How can one properly address this behaviour ? If it's not possible, then how can one dynamically add fields to an existing form in such a way that all fields get properly validated upon submission ?
Since you call validate() before adding the field, naturally, you can't validate a field which doesn't exist yet. That said, you don't want to add a field to an instance instead of a class. This is because since WTForms processes its input data at construction, adding fields to the instance is mostly a meaningless thing.
If your field name is static, you can use the del trick detailed here
If it's dynamic, you could instead follow the dynamic form composition pattern from the docs.
Since I've gone over this in detail, I'll link my previous example here:
Wtfforms dynamic generation
Just to be clear, I'm asking about accessing the fields in views.py
I want to add extra data into the form before it is validated (because it's a required field), and another answer on stackexchange seems to imply I have to create a new form to do so.
Right now my code look something like this:
if request.method == 'POST':
# create a form instance and populate it with data from the request:
form = TestForm(request.POST)
data = {}
for ---:
---add to data---
comp = Component.objects.get(name = path)
data['component'] = comp.id
form = TestForm(data)
if form.is_valid():
test = form.save(commit = 'false')
test.save()
return submitTest(request, var)
How could I fill in the parts with dashes?
This is the wrong thing to do. There is no reason to add in a required field programmatically; if you know the value of the field already, there is no reason to include it on the form at all.
I don't know what you mean about having to create another form; instead you should explicitly exclude that field, in the form's Meta class, and set the value on the test object before calling test.save().
Edit after comment I still don't really understand why you have data coming from two separate places, but maybe you should combine them before passing to the form:
data = request.POST.copy()
data['myvalue'] = 'myvalue'
form = MyForm(data)
I figured out what I was doing wrong. In my TestForm modelform I didn't include the 'component' field because I didn't want it to show up on the form. As a result, the 'component' data was being cleaned out during form validation even if I inserted it into the form correctly. So to solve this I just added 'component' into the fields to display, and to hide it on the form I added this line
widgets = {'component': HiddenInput()}
to the TestForm class in forms.py.
I'm trying to run a simple update form that should update all object values in DB submitted from the form.
This is my update view that does nothing other than redirecting to the "/". No errors but no update either.
def update(request, business_id):
if request.method == 'POST':
form = BusinessForm(request.POST)
if form.is_valid():
t = Business.objects.get(id=business_id)
t.save()
return HttpResponseRedirect("/")
else:
...
You are not updating any fields, use form.cleaned_data to get the form field values:
Once is_valid() returns True, the successfully validated form data
will be in the form.cleaned_data dictionary. This data will have been
converted nicely into Python types for you.
if form.is_valid():
t = Business.objects.get(id=business_id)
t.my_field = form.cleaned_data['my_field']
t.save()
Also, consider using an UpdateView class-based generic view instead of a function-based:
A view that displays a form for editing an existing object,
redisplaying the form with validation errors (if there are any) and
saving changes to the object. This uses a form automatically generated
from the object’s model class (unless a form class is manually
specified).
How can I clean the data in a form and have the cleaned data redisplayed instead of the submitted data?
There are several fields in my form, and every time the user submits it, it should be redisplayed with the values the user entered. However, some of the fields I would like to clean and update for the user. More specifically, I have a field FriendlyIntegerField(forms.CharField) in which I override to_python to not only call int(str(value)), but also set any negative number to 0 etc. I do not want to redisplay the form with the invalid data and have the user fix it himself (which is how Django wants me to do it).
I don't have a problem cleaning the data and use it for the rest of my view-function, but how can I update the actual form with this data?
By the way, the form does not reflect a structure in my data model, and so inherits from Form, not ModelForm.
Edit:
My Field (in a stripped down version) looks like this:
class FriendlyIntegerField(forms.CharField):
def to_python(self, value):
try:
return str(int(str(value).replace(' ','')))
except:
raise forms.ValidationError('some error msg')
My Form (in a stripped down version) looks like this:
class SearchForm(forms.Form):
price_from = FriendlyIntegerField()
price_to = FriendlyIntegerField()
And my view:
def search(request, key):
if request.method == 'POST':
form = SearchForm(request.REQUEST)
if not form.is_valid():
print "Form not valid"
else:
form = SearchForm()
return render_to_response('path_to_template', {'form' : form}
If, after you've cleaned your form with is_valid(), you render that cleaned form with your view, rather than redirect to a new page, you'll see the cleaned data in your page.
(If you wanted the user to see this cleaned data and then properly submit it, you could use a hidden field to track whether the form data has already been cleaned, but this isn't without complications...)