Django formset cleaned_data empty when submitted form unchanged - python

I've been experiencing a weird problem, regarding Django 1.4 and formsets: when the submitted data is unchanged, the cleaned_data field of the formset is empty, even if the formset itself passes the validation.
Here is an example:
forms.py:
class NameForm(forms.Form):
name = forms.CharField(required=False, initial='Foo')
views.py:
def welcome(request):
Formset = formset_factory(NameForm, extra=1)
if request.method == 'POST':
formset = Formset(request.POST)
print '1.Formset is valid?', formset.is_valid()
print '2.Formset', formset
print '3.Formset cleaned_data', formset.cleaned_data
else:
formset = Formset()
return render_to_response('template.html', locals())
Although formset is valid, and it actually contains data, line 3 prints a list of an empty dictionary, unless I've actually changed the initial value in the field.
This seems weird to me, but I'm probably doing something wrong. Any help?

A formset renders bunch of forms as well as several hidden input fields holding info such as max number of forms. Thus a correctly POSTed formset always contains data.
And, the initial 'Foo' inside CharField name is the reason that formset got empty dictionary. When empty_permitted of a form is set to True, and all items of the form equals to its initial value, Django will treat the form as empty and set its cleaned_data to be empty dict. The empty_permitted defaults to False, formset will set it to True for extra forms. Thus after you clicked submit w/o editing the value 'Foo', the form instance was treated as empty, hence the formset wrapping the form got an empty cleaned_data.

This happened to me. #okm is correct but it's not clear from his answer how to fix it. See this answer for a solution:
class MyModelFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
super(MyModelFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.empty_permitted = False

Related

How to validate a formset in dajngo

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.

Django: form validation errors on form creation

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

Setting the form value when the form is initiated

I am trying to set a field value when a form is initiated.
The value of this field is retrieved when we enter the view - the view being the timesheet. Then for each Time set in the view, I want to relate it back to the timesheet.
#login_required
#requires_csrf_token
def timesheet(request, timesheet_id):
timesheet = TimeSheet.objects.get(pk=timesheet_id)
NewTimeFormSet = modelformset_factory(Time, form=TimeForm, formset=RequiredFormSet)
if request.method == 'POST':
newtime_formset = NewTimeFormSet(request.POST, request.FILES)
for form in newtime_formset:
if form.is_valid():
form.save()
#then render template etc
So, to make sure the form validates I want to set this field when the form is initiated. When I try to set this field after POST in the view, I haven't been able to get the field to set or form to validate.
My code gets the timesheet_id when the model instance is initiated on entering the view
def __init__(self, *args, **kwargs):
# this allows it to get the timesheet_id
print "initiating a timesheet"
super(TimeSheet, self).__init__(*args, **kwargs)
And then the form is generated and I run the form init. So this is what I've tried
class TimeForm(forms.ModelForm):
class Meta:
model = Time
fields = ['project_id', 'date_worked', 'hours', 'description', 'timesheet_id',]
# some labels and widgets, the timesheet_id has a hidden input
def __init__(self, *args, **kwargs):
print "initiating form"
super(TimeForm, self).__init__(*args, **kwargs)
timesheet = TimeSheet.objects.get(id=timesheet_id)
self.fields['timesheet_id'] = timesheet
This raises the error
NameError: global name 'timesheet_id' is not defined
I don't know how to do this...
I've also attempted setting the field in the form clean() method, but it populates (shown by a print) and then still doesn't validate and I raise a formset error 'This field is required'.
Help!
You don't actually accept a timesheet_id parameter in the form init method, so that value is not defined hence the error.
However, this is the wrong approach. There is no point passing a value to a form, outputting it as a hidden field, then getting it back, when you had it all along. The way to do this is to exclude the value from the form's fields, and set it on save.
class TimeForm(forms.ModelForm):
class Meta:
model = Time
fields = ['project_id', 'date_worked', 'hours', 'description',]
...
if request.method == 'POST':
newtime_formset = NewTimeFormSet(request.POST, request.FILES)
if newtime_formset.is_valid():
for form in newtime_formset:
new_time = form.save(commit=False)
new_time.timesheet_id = 1 # or whatever
new_time.save()
Note, again, you should check the validity of the whole formset before iterating through to save; otherwise you might end up saving some of them before encountering an invalid form.

Django Model Formset: only track changes to those items that have been updated/saved in the set?

So, I'm using Django's Model Formset to produce sets of forms for different data. It is working great, but I want to add a feature where when a user displays the formset and, say, updates 2 out of the 10 items, I can track just the 2 updated, and output a message like "You have updated 2 items" kind-of-thing.
Do Django Model Formsets have a built in API for this? I can't seem to find it on the Django Docs.
I've tried various approaches but keep getting this when using the code offered by Peter below:
'Attendance' object has no attribute 'has_changed.'
If I switch form.has_changed to formset.has_changed(), I get
'list' object has no attribute 'has_changed'
My View and Post method
class AttendanceView(TemplateView):
template_name = 'example.html'
def changed_forms(self, formset):
return sum(1 for form in formset if form.has_changed())
def post(self, request, *args, **kwargs):
formset = AttendanceFormSet(request.POST)
if formset.is_valid():
formset = formset.save()
forms_changed = self.changed_forms(formset)
context = self.get_context_data(**kwargs)
context['total_changed_forms'] = forms_changed
return self.render_to_response(context)
else:
return HttpResponse("POST failed")
So I figured it out, just change:
formset = formset.save()
to
formset.save()
Formsets have a has_changed method which will report whether or not any of its forms have been changed. That's not exactly what you're looking for, but if you look at its implementation it should show you how to do it. That method is:
def has_changed(self):
"""
Returns true if data in any form differs from initial.
"""
return any(form.has_changed() for form in self)
So you can count changed forms with:
def changed_forms(formset):
return sum(1 for form in formset if form.has_changed())
Or if you're comfortable using the integer meanings of boolean values:
return sum(form.has_changed() for form in formset)
I personally find that unappealing compared to the more explicit mapping from true to 1, but opinions differ there.

Override POST initial value in Django form field

I have a page with a list of the same forms. Each form has a different value for a hidden field to distinguish the submissions. If I submit one of the forms, the page is refreshed and all the same forms are shown. However, each form now has it's fields populated with the data submitted on the last request. I want each form's fields to be empty no matter what. I tried supplying initial='' to the field in the form object; this didn't work.
Ideas?
Thanks
If you want to use a list of same forms, you should use formsets.
Overriding POST initial values:
form = SomeForm()
if request.method == 'POST':
form = SomeForm(request.POST)
if form.is_valid():
#do something and redirect somewhere
#next line will be reached only if form is invalid
#and you want to override POST data
form = SomeForm()
return render(.....)

Categories

Resources