django Formset won't save - django

I have the below view for a Formset, but when I save the form it doesn't save changes to the database?
def schedule(request, year, month, day):
EntriesFormset = modelformset_factory(Entry, extra = 1, exclude=("creator", "date"),can_delete=True)
if request.method == 'POST':
formset = EntriesFormset(request.POST)
if formset.is_valid():
# add current user and date to each entry & save
entries = formset.save(commit=False)
for entry in entries:
entry.creator = request.user
entry.date = date(int(year), int(month), int(day))
entry.save()
return HttpResponseRedirect(reverse("Pipettes.views.month", args=(year, month)))
else:
# display formset for existing enties and one extra form
formset = EntriesFormset(queryset=Entry.objects.filter(date__year=year,date__month=month, creator=request.user))
return render_to_response("Scheduler.html", add_csrf(request, entries=formset, year=year,
month=month, day=day))

I suspect that the formset is invalid but instead of displaying of the formset with errors you returning the redirect. You should move the redirecting to one level right, into the if statement:
if formset.is_valid():
...
return HttpResponseRedirect(reverse("Pipettes.views.month", args=(year, month)))
UPDATE: If your formset is not validated but you don't see any errors on the page then your formset rendering may be invalid. For testing purposes try to use the simplest possible template:
<table>
{{ formset }}
</table>
Also note that with formset.save(commit=False) deleted objects are not deleted automatically. See the side note in this chapter of the docs.

Related

Inline formset returns empty list on save?

When I try to save my inline formset it just returns an empty list and no changes are reflected in the database. I have tried doing it with no option and commit=False but they both have the same result. I know there is data because I printed the formset as a table, and I know it is valid because the property is_valid() method returns true. Here is the code:
def edit(request):
if request.method == 'POST':
print(request.POST)
form = TombstoneForm(request.POST)
print(form.is_valid())
t = form.save(commit=False)
t.edit_date = datetime.now()
t.user_editor = request.user
t.save()
print(t)
formset_construct = inlineformset_factory(Tombstone, Tank, form=TombstoneForm)
formset = formset_construct(request.POST)
print("Passed the inline formset")
print(formset.as_table())
print(formset.is_valid())
l = formset.save()
print(l)
return render(request, 'main.html')
So I believe I have found the source of my problem and a workaround. The problem was occuring in the BaseModelFormSet class in this method:
def save_existing_objects(self, commit=True):
self.changed_objects = []
self.deleted_objects = []
if not self.initial_forms:
return []
saved_instances = []
forms_to_delete = self.deleted_forms
for form in self.initial_forms:
obj = form.instance
# If the pk is None, it means either:
# 1. The object is an unexpected empty model, created by invalid
# POST data such as an object outside the formset's queryset.
# 2. The object was already deleted from the database.
if obj.pk is None:
continue
if form in forms_to_delete:
self.deleted_objects.append(obj)
self.delete_existing(obj, commit=commit)
elif form.has_changed():
self.changed_objects.append((obj, form.changed_data))
saved_instances.append(self.save_existing(form, obj, commit=commit))
if not commit:
self.saved_forms.append(form)
return saved_instances
The problem was occuring in:
if obj.pk is None:
continue
Where it was always hitting continue, so my solution has just been to save the individual form data instead of the formset data:
for form in formset:
val = form.save(commit=False)
val.Vessel = t
val.save()
To help out others, where they are having similar problems with formset save returning empty lists, and theres is not much else on SO:
According to the Django docs:
After calling save(), your model formset will have three new attributes containing the formset’s changes:
models.BaseModelFormSet.changed_objects
models.BaseModelFormSet.deleted_objects
models.BaseModelFormSet.new_objects
new_objects is simply list of new objs, i.e.
[obj1, obj2]
changed_objects is a list of tuples, i.e.
[
(obj1, [list of field names changed]),
(obj2, [list of field names changed]),
...
]
I was having a problem where deleted_objects was always [].
First ensure you have the required hidden field in your form:
{% for form in formset %}
{{ form.id }} # <- have you got this?
{{ form.DELETE }}
...
{{ form.foo }}
{% endfor %}
And secondly, I had the bright idea of disabling all fields when the user the check the delete checkbox. Disabled items are not posted, so then the items to be deleted would not a PK, as OP mentions above if obj.pk is blank, then it would not be added to the (.deleted_objects in this case) list . The solution is to rather use readonly, in JavaSript use .readOnly = true and in css :read-only.

How to update formset with new pk id?

I have a formset that I am rendering with the default {{ formset }}. The page does a post back to itself when the form is submitted.
The issue is when new instances are created, the fields containing the ids are still blank like <input id="id_form-0-id" name="form-0-id" type="hidden">. So if I resubmit the form after it comes back, it ends up creating new instances instead of updating the newly-created instances. If I refresh the page then the fields contain the ids <input id="id_form-0-id" name="form-0-id" type="hidden" value="18">.
This is my controller function:
def main_categories (request):
dict = {}
FormSet = modelformset_factory (MainCategory, formset = MainCategoryFormSet, fields = ['name'], extra = 1, can_delete = True)
if request.method == 'POST':
formset = FormSet (request.POST)
if formset.is_valid ():
with transaction.atomic ():
formset.save ()
else:
formset = FormSet ()
dict ['formset'] = formset
return render (request, 'equipment/admin/main_categories.html', dict)
If you want to edit previously submitted queryset, you must provide queryset into FormSet class. See documentation

Insert or update data with Django formsets

it's not clear to me how to manage formsets in Django. This is my views.py:
def newAd(request):
newAdFormSet = modelformset_factory(Ad)
if request.method == 'POST':
formset = newAdFormSet(request.POST, request.FILES)
if formset.is_valid():
formset.save()
return render_to_response('conf.html',
{'state':'Your ad has been successfull created.'},
context_instance = RequestContext(request),)
else:
formset = newAdFormSet()
return render_to_response('ad_form.html',
{'form':formset},
context_instance=RequestContext(request),)
It works but it always returns one prefilled form for each existing tuple plus, at the end, a blank form.
Now, i can't get how to say where it must return a blank form (to perform a new insert), and where it must instead return a single prefilled form (possibly passing the Ad's id) to perform an update.
modelformset_factory and formset helps to solve a lot, take your code for example
def newAd(request):
newAdFormSet = modelformset_factory(Ad, extra=1)
if request.method == 'POST':
formset = newAdFormSet(request.POST, request.FILES)
if formset.is_valid():
formset.save()
return render_to_response('conf.html',
{'state':'Your ad has been successfull created.'},
context_instance = RequestContext(request),)
else:
formset = newAdFormSet(queryset=Ad.objects.all())
return render_to_response('ad_form.html',
{'form':formset},
context_instance=RequestContext(request),)
Note the extra=1 in modelformset_factory line, it ensures there is only one extra blank form. And queryset=Ad.objects.all() in the second newAdFormSet inside else statement, it pre-fills forms for Ad objects from DB and correctly set PK in, mostly hidden, field for backend code to recognize submitted objects.
update
if you want to set Ad().codU to point to an User() instance, request.user for example, you could simply just set it by
instances = formset.save(commit=False)
for obj in instances:
obj.codU = request.user
obj.save()
I'm still not 100% clear what your question is, but it sounds like you don't want a formset at all. If you're only interested in adding or updating a single record at a time, you want a simple ModelForm, not a formset. So:
class AdForm(forms.ModelForm):
class Meta:
model = Ad
def add_update_ad(request, pk=None):
if pk is not None:
instance = Ad.objects.get(pk=pk)
else:
instance = Ad()
if request.POST:
form = AdForm(request.POST, instance=instance)
if form.is_valid():
new_instance = form.save()
return HttpResponseRedirect('my_confirmation_view')
else:
form = AdForm(instance=instance)
return render(request, 'ad_form.html', {'form': form})

ModelForm with ImageFields, not clearing properly if validation error

First the code.
The ModelForm (im1 and im2 are models.ImageField):
class TestForm(forms.ModelForm):
checkme = forms.BooleanField(required=True)
class Meta:
model = UserProfile
fields = ('im1', 'im2')
The view:
def test(request):
profile = request.user.get_profile()
form = TestForm(instance=profile)
if request.method == "POST":
form = TestForm(request.POST, request.FILES, instance=profile)
if form.is_valid():
form.save()
return render(request, 'test.html', {'form':form})
The template:
<html>
<head>
<title>Test</title>
</head>
<body>
<form method="post" enctype="multipart/form-data">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="submit" />
</form>
</body>
</html>
The problems:
If im1 contains a valid image, and I check the clear checkbox next to it but don't check checkme and submit, the form comes back with an error saying that checkme is required. Although the form returns with the error, it appears as if im1 has been cleared. In reality it has not because if I reload the form im1 shows back up with its file and clear checkbox.
My question is how can I fix this? Is it something I am doing or is this something to do with django?
Django is acting exactly as it should.
If the request is a POST request, then your form is bound to the data from request.POST and request.FILES. instance=profile is simply telling the form what particular object to save to if all validation passes. Even if your form isn't valid, it's still bound to the data with the cleared image, and that's what you're passing to render().
Firstly, you shouldn't be creating the first bound form if the request method is POST:
def test(request):
profile = request.user.get_profile()
if request.method == "POST":
form = TestForm(request.POST, request.FILES, instance=profile)
if form.is_valid():
form.save()
else:
form = TestForm(instance=profile)
return render(request, 'test.html', {'form':form})
Secondly, why do you want your user to do the same exact action twice if they did indeed want to delete an image but simply missed another checkbox?
If you really need Django to act this way, I would do one of two things. Either create a bound form from an instance of UserProfile and pass both the non-valid form and the newly created form to the template and use the non-valid form for displaying the errors and the other one for displaying the rest of the form:
def test(request):
profile = request.user.get_profile()
if request.method == "POST":
errors_form = TestForm(request.POST, request.FILES, instance=profile)
if errors_form.is_valid():
errors_form.save()
form = errors_form
else:
form = TestForm(instance=profile)
return render(request, 'test.html', {'form':form, 'errors_form': errors_form})
else:
form = TestForm(instance=profile)
return render(request, 'test.html', {'form':form})
OR I'd do the same thing but save the errors from the non-valid form to the newly created form so you don't end up with renders() all over the place:
def test(request):
profile = request.user.get_profile()
if request.method == "POST":
errors_form = TestForm(request.POST, request.FILES, instance=profile)
if errors_form.is_valid():
errors_form.save()
form = errors_form
else:
form = TestForm(instance=profile)
#this is left up to you to implement, but you'd do something like
#form.errors = errors_form.errors
#and also iterate through every form attribute.errors and assign it to
#errors_form attribute.errors etc...
else:
form = TestForm(instance=profile)
return render(request, 'test.html', {'form':form})
Both aren't very elegant solutions and I'm not positive the second one will even work as expected without some more hacks since I'm not completely familiar with the Django Forms implementation.
I don't see that doing this is worth it. As I stated before, you're just creating more work for your user...

Django - Adding initial value to a formset

I have a many-to-many relationship between two classes (Lesson and Student), with an intermediary class (Evaluation).
I am trying to set up a form which will allow me to add a lesson with students and the related evaluation data. I can get all of the fields I want to display correctly, however I also need to set an initial value behind the scenes (the current user), as it does not make sense to have it in the form.
I have tried following the docs but I think I have a syntax error in the way I am passing the data to the formset.
The error I receive is as follows:
__init__() got an unexpected keyword argument 'initial'
My actual view (with my attempt at adding the initial data removed) looks like this:
def addlesson(request):
LessonFormset = inlineformset_factory(Lesson, Evaluation, exclude=('user',), max_num=5)
if request.method == 'POST':
lesson = Lesson(user=request.user)
form = LessonForm(request.POST, instance=lesson, user = request.user)
formset = LessonFormset(request.POST, instance = lesson)
if form.is_valid() and formset.is_valid():
form.save()
formset.save()
return HttpResponseRedirect("/")
else:
form = LessonForm(user = request.user)
formset = LessonFormset()
return render_to_response("addlesson.html", {
'form': form,
'formset' : formset,
}, context_instance=RequestContext(request))
Could anyone show me to correct syntax to use to set the current user in the formset?
This is what I had before but it was giving me the error at the start of my post:
initial={'user': request.user},
Any advice appreciated
Thanks
It's not clear to me why you are using a formset when it looks like you only want to add one row. A regular form would have been how I would do it if there was only one row. But, here's how I set the default value in a formset.
I exclude the field, just like you already have in your code. Then:
if form.is_valid() and formset.is_valid():
form.save()
models = formset.save(commit=False)
for i in models:
i.user = request.user
i.save()
return HttpResponseRedirect("/")
I tried Umang's answer and it didn't work good for when you want to change a value with a specific index. When you save the formset it will change the values that was changed.
But if you change models = formset.save(commit=False) to models = formset
and then you also need to change i.user = request.user to i.instance.user = request.user
if form.is_valid() and formset.is_valid():
form.save()
# changed to formset instead of formset.save(commit=False)
models = formset
for model in models:
# changed to i.instance.user instead of i.user, except renamed i to model.
model.instance.user = request.user
model.save()
# save the formset
formset.save()
return HttpResponseRedirect("/")
Now when you want to change an index it will include all the forms, not only the ones that was changed when you save.
Example:
views.py
if form.is_valid() and formset.is_valid():
form.save()
models = formset
index = 0
# This will only change the first form in the formset.
models[index].instance.user = request.user
models.save()
formset.save()
return HttpResponseRedirect("/")