Django ModelForms - 'instance' not working as expected - django

I have a modelform that will either create a new model or edit an existing one - this is simple and should work, but for some reason I'm getting a new instance every time.
The scenario is this is the first step in an ecommerce order. The user must fill out some info describing the order (which is stored in the model). I create the model, save it, then redirect to the next view for the user to enter their cc info. I stick the model in the session so I don't have to do a DB lookup in the next view. There is a link in the template for the second (cc info) view that lets the user go back to the first view to edit their order.
# forms.py
class MyForm(forms.ModelForm):
class Meta:
fields = ('field1', 'field2')
model = MyModel
# views.py
def create_or_update(request):
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
m = form.save(commit=False)
# update some other fields that aren't in the form
m.field3 = 'blah'
m.field4 = 'blah'
m.save()
request.session['m'] = m
return HttpResponseRedirect(reverse('enter_cc_info'))
# invalid form, render template
...
else:
# check to see if we're coming back to edit an existing model
# this part works, I get an instance as expected
m = request.session.get('m', None)
if m:
instance = get_object_or_None(MyModel, id=m.id)
if instance:
form = MyForm(instance=instance)
else:
# can't find it in the DB, but it's in the session
form = MyForm({'field1': m.field1, 'field2': m.field2})
else:
form = MyForm()
# render the form
...
If I step through in the debugger when I go back to the view to edit an order that the form is created with the instance set to the previously created model, as expected. However, when the form is processed in the subsequent POST, it creates a new instance of the model when form.save() is called.
I believe this is because I've restricted the fields in the form, so there is nowhere in the rendered HTML to store the id (or other reference) to the existing model. However, I tried adding both a 'pk' and an 'id' field (not at the same time), but then my form doesn't render at all.
I suspect I'm making this more complicated than it needs to be, but I'm stuck at the moment and could use some feedback. Thanks in advance.

This is interesting. Here is my stab at it. Consider this line:
form = MyForm(request.POST)
Can you inspect the contents of request.POST? Specifically, check if there is any information regarding which instance of the model is being edited. You'll find that there is none. In other words, each time you save the form on POST a new instance will be created.
Why does this happen? When you create a form passing the instance=instance keyword argument you are telling the Form class to return an instance for an instance of the model. However when you render the form to the template, this information is used only to fill in the fields. That is, the information about the specific instance is lost. Naturally when you post pack there is way to connect to the old instance.
How can you prevent this? A common idiom is to use the primary key as part of the URL and look up an instance on POST. Then create the form. In your case this would mean:
def create_or_update(request, instance_id):
# ^^^^^
# URL param
if request.method == 'POST':
instance = get_object_or_None(Model, pk = instance_id)
# ^^^^^
# Look up the instance
form = MyForm(request.POST, instance = instance)
# ^^^^^^^
# pass the instance now.
if form.is_valid():
....

Related

Django - How to pass a form instance from one view to another

I have a form with multi steps in it. First part uses a forms.ModelForm, once completed, the user is re-directed to another view with a forms.Form in it.
Ideally, I would like to save the first form only if the secondary form is valid and save them both at the same time if that's the case.
I usually use self.request.session to pass data from one view to another, but a form's instance is not serializable, hence, can not append it to the session.
As an example:
FirstView contains FirstForm with fields ('firstname', 'lastname')
SecondView contains SecondForm with fields ('address', 'gender')
If FirstForm and SecondForm is valid
form1.save
form2.save
Would anyone have any suggestions?
Thank you.
Well, you can store form's cleaned_data in django session and pass it to another form as initial. For example:
def first_view(request):
form = FirstForm(request.POST)
if form.is_valid():
request.session['first_form'] = form.cleaned_data
return redirect('to_second_view')
def second_view(request):
form = SecondForm(request.POST)
if form.is_valid():
first_form_data = request.session.pop('first_form',{})
first_form_instance = FirstFormModel.objects.create(**first_form_data)
second_form_instance = form.save()
# rest of the code...

How to check whether or not a Django form is correctly bound?

Situation
Using Django 1.5, I am using forms.ModelForms to let the user edit database contents. However I can't get the form to update the database upon form.save().
Each of my models correspond to a setting form (the application is a the direct porting of a desktop software in which the user can store several settings). I needed to implement a Reset to default feature, so I thought of having a default object (imported with Django fixtures) which I would use only to reset a second one. The user would only interact with the second model.
pk=1 refers to the base object
pk=2 refers to the custom object
I have several forms on the same page (only foobar here), so basically this what I planned to do:
No POST data
Building form from either pk=1 or pk=2, depending pk=2 has been found or not
Rendering the forms to the template
AJAX request, with POST datas
Getting form content
Checking whether or not the user has permission to edit the model (checksum)
Update the model form POST datas
Returning AJAX response
Code
I have put two debug prints to illustrate the issue I am facing. The form I fetch doesn't seem to be bound to my model.
# Response codes to use in the template
RESPONSES = {
200: {'code':'0xB16B00B5', 'message':'Success'},
400: {'code':'0x8BADF00D', 'message':'Form is not valid'},
403: {'code':'0xBAADF00D', 'message':'No permission to edit the database'},
501: {'code':'0xDEADC0DE', 'message':'POST datas not found'},
}
# Those are the setting labels
TYPES = {
'foobar': {'model':FooBar, 'form':FooBarForm },
}
def index(request):
# Handling form datas
if request.method == 'POST':
response = HttpResponse(simplejson.dumps({'code':RESPONSES[501]['code']}), 'application/json')
for label in TYPES:
# Filtering the right form to handle
if label in request.POST:
model = _fetch_setting(label, mode='model')
form = _fetch_setting(label, mode='form', post=request.POST)
checksum = model.checksum # Somehow, 'form.is_valid()' is altering 'model', need to backup the checksum
if form.is_valid():
# The user has permission to edit the model
if form.cleaned_data['checksum'] == checksum:
if form.has_changed():
print form.cleaned_data['foo'] # Outputs the form data, as expected
form.save()
print model.foo # Outputs the old data
model.checksum = str(uuid4()).replace('-', '')
model.save()
response = HttpResponse(simplejson.dumps({'code':RESPONSES[200]['code']}), 'application/json')
# This one does not
else:
response = HttpResponse(simplejson.dumps({'code':RESPONSES[403]['code']}), 'application/json')
break # We are still inside the label loop
# The form is not valid
else:
response = HttpResponse(simplejson.dumps({'code':RESPONSES[400]['code']}), 'application/json')
# Form not submitted yet, building the HTML forms
else:
forms = {}
label = 'foobar'
for label in TYPES:
forms[label] = _fetch_setting(label, mode='form')
context = {'errors':RESPONSES, 'forms':forms}
response = render(request, 'home/index.html', context)
return response
# Return a setting object (model or form) corresponding to the given label
def _fetch_setting(label, mode='model', post=None):
try:
result = None
default = TYPES[label]['model'].objects.get(pk=1)
try:
model = TYPES[label]['model'].objects.get(pk=2)
except TYPES[label]['model'].DoesNotExist:
model = TYPES[label]['model'].objects.create(
checksum = default.checksum,
foo = default.foo,
bar = default.bar,
)
if mode == 'model':
result = model
if mode == 'form':
print model
result = TYPES[label]['form'](data=post, instance=model) # The 'instance' attribute doesn't seem to be applied
except KeyError:
result = None
finally:
return result
Update
07.10
It does work when I pass the instance to bound with to _fetch_setting. So I guess this issue is coming from the form validation.
def _fetch_setting(label, mode='model', post=None, instance=None):
# ...
if mode == 'form':
if instance:
model = instance
result = TYPES[label]['form'](data=post, instance=model)
# ...
As I commented in my code, form.is_valid() seems to alter the object.
Will flag as answered if no one come with a clean solution.
The issue is, you are creating a new model object with each form.save()
You need to update the same model object with commit=False
if form.cleaned_data['checksum'] == checksum:
if form.has_changed():
print form.cleaned_data['foo'] # Outputs the form data, as expected
model = form.save(commit=False)
model.checksum = str(uuid4()).replace('-', '')
model.save()
From the fabulous manual:
The first time you call is_valid() or access the errors attribute of a ModelForm triggers form validation as well as model validation. This has the side-effect of cleaning the model you pass to the ModelForm constructor. For instance, calling is_valid() on your form will convert any date fields on your model to actual date objects. If form validation fails, only some of the updates may be applied. For this reason, you’ll probably want to avoid reusing the model instance passed to the form, especially if validation fails.

Getting 'builtin_function_or_method object is not iterable' on file upload

I am unable to upload the file. I am getting
Type error builtin_function_or_method' object is not iterable
models.py
class seeker(models.Model):
user = models.OneToOneField(User)
birthday = models.DateField()
class Upload(models.Model):
user = models.ForeignKey(Seekers)
resume = models.FileField(upload_to ='resume', blank = True, null = True)
forms.py
class SeekersForm(forms.Form):
resume = forms.FileField(label = 'Select a file',help_text = 'max.3 MB')
views.py
def List(request):
# Handle file upload
if request.method == 'POST':
form = SeekersForm(request.POST, request.FILES)
if form.is_valid():
#id = User.object.get(id)
newdoc = Seekers.objects.get(user_id)
newdoc.resume =Upload(resume = request.FILES['resume'])
newdoc.save()
#seekers_edit = Seekers.objects.get(id)
#seekers_edit.resume = Seekers(resume = request.FILES['resume'])
#seekers_edit.save()
#Redirect to the document list after POST
return HttpResponseRedirect('/profile/')
else:
form = SeekersForm() # A empty, unbound form
#Load documents for the list page
seekers = Seekers.objects.all()
#Render list page with the documents and the form
return render_to_response('list.html',{'seekers':seekers,'form':form},context_instance=RequestContext(request))
It's hard to say where your problem is, but I think the following line of code is the main problem:
newdoc.resume =Upload(resume = request.FILES['resume'])
You have to save a file in a FileField explicitly before you save the entire model instance. Also, if you have a ForeignKey field in one of your models and you want to assign it an instance of another model, please save that instance first before you do the assignment. Without knowing your Seekers model, all I can do is guessing what might help you. Something like the following might get you started:
your_file = request.FILES['resume']
upload_instance = Upload()
upload_instance.resume.save(name=your_file.name, content=your_file, save=False)
upload_instance.user = ... # Here goes an instance of your Seekers model
upload_instance.save() # Here you save the whole instance of your Upload model
Also, please note the following:
Your model Seekers should rather be named Seeker using the singular, not the plural. This should generally be like that with all your models.
Python functions should always start with a lowercase letter, i.e. list instead of List. However, this name is a bad choice here anyway, because a function called list is already present in Python's standard library.
Please take a closer look at Django's documentation. It's all in there what you need to know. I recommend you to read especially these sections:
https://docs.djangoproject.com/en/1.4/ref/models/fields/#filefield
https://docs.djangoproject.com/en/1.4/ref/files/file/
Problems in your code:
Your form definition duplicates information from your model — just use forms.ModelForm (with exclude so as not to display the user field)
As currently pasted, newdoc = Seekers.objects.get(user_id) will raise a TypeError ('foo' object is not iterable); .get() accepts keyword parameter filters, not anything else.
Accessing request.FILES['resume'] manually isn't necessary or recommended
So, in short, you're almost there; just let Django forms do more of the work for you:
# forms.py
class SeekerForm(forms.ModelForm)
class Meta:
model = Seeker
# views.py
def seeker_list(request):
# Opinions are divided as to whether it's ever appropriate to
# modify the database like this on a GET request, but it seems
# to make sense here
seeker = Seekers.objects.get_or_create(user=request.user)
if request.method == 'POST':
form = SeekerForm(request.POST, request.FILES, instance=seeker)
if form.is_valid():
form.save()
return HttpResponseRedirect('/profile/')
else:
form = SeekerForm(instance=seeker)
seekers = Seekers.objects.all()
#Render list page with the documents and the form
return render_to_response('list.html', {
'seekers':seekers,
'form':form
}, context_instance=RequestContext(request))
It's not clear what the significance (if any) of the commented-out sections of your code is — I've assumed you always want to modify the current user's Seeker, but if not then adapt as appropriate.

django update only one field of a model through a form

I have this models:
class Balanta(models.Model):
data = models.DateField()
class Conturi(models.Model):
cont=models.PositiveIntegerField()
cont_debit=models.DecimalField(default=0, max_digits=30, decimal_places=2)
cont_credit=models.DecimalField(default=0, max_digits=30, decimal_places=2)
balanta = models.ForeignKey(Balanta)
And i have formsets working ok in a template and this view:
def balanta_introducere(request):
balanta=Balanta()
ConturiInlineFormSet=inlineformset_factory(Balanta, Conturi, extra=3)
if request.method=='POST':
balanta_form=BalantaForm(request.POST, instance=balanta)
if balanta_form.is_valid():
balanta, created=Balanta.objects.get_or_create(**balanta_form.cleaned_data)
#return HttpResponseRedirect('/sitfin/balantaok')
formset=ConturiInlineFormSet(request.POST, request.FILES, instance=balanta)
if formset.is_valid():
for form in formset:
data={
'cont':form.cleaned_data.get('cont'),
'cont_debit':form.cleaned_data.get('cont_debit'),
'cont_credit':form.cleaned_data.get('cont_credit'),
'balanta':form.cleaned_data.get('balanta'),
}
try:
c=Conturi.objects.get(cont=data['cont'])
except Conturi.DoesNotExist:
cont_complete,created=Conturi.objects.get_or_create(**data)
else:
cont_complete,created=Conturi.objects.get_or_create(cont=data['cont'],cont_debit=data['cont_debit'],cont_credit=data['cont_credit'],balanta=data['balanta'])
else:
balanta_form=BalantaForm()
formset=ConturiInlineFormSet(instance=balanta)
return render_to_response('sitfin/balanta_introducere.html',{'balanta_form':balanta_form,'formset':formset}, context_instance=RequestContext(request))
If i hit the first submit, all the data goes in the database (foreignkey and all)
After the second submit with the same data, the form doesn't do anything and this is ok.
If i change a value in the form (in a "cont_credit" of a "cont" for example) and hit submit again, i get another Conturi object with only the modified "cont" with the updated "cont_credit" value and this is not good!
What is the approach for updating only some fields of an existing Conturi model with the help of a form?
Something like:
If the cont it is not in the database,
create a Conturi objects with the data in the form,
If the "cont" is already in the database,
Update the cont_credit and cont_debit data with the new values entered in the form
Thank you very much.
get_or_create is trying a get with all the parameters you pass it, so if anything changes on the form, it won't find the existing object, and instead will create a new one.
If your forms are ModelForms, then you can just use form.save() to save the instance bound to the form, and formset.save() to save all the instances bound to the formset.
EDIT:
I now noticed another thing: you are using
balanta=Balanta()
and then
balanta_form=BalantaForm(request.POST, instance=balanta)
so you are forcing the form to use a new instance. try getting the specific Balanta you're editing, and pass that as the instance.

Django Forms with get_or_create

I am using Django ModelForms to create a form. I have my form set up and it is working ok.
form = MyForm(data=request.POST)
if form.is_valid():
form.save()
What I now want though is for the form to check first to see if an identical record exists. If it does I want it to get the id of that object and if not I want it to insert it into the database and then give me the id of that object. Is this possible using something like:
form.get_or_create(data=request.POST)
I know I could do
form = MyForm(instance=object)
when creating the form but this would not work as I still want to have the case where there is no instance of an object
edit:
Say my model is
class Book(models.Model):
name = models.CharField(max_length=50)
author = models.CharField(max_length=50)
price = models.CharField(max_length=50)
I want a form which someone can fill in to store books. However if there is already a book in the db which has the same name, author and price I obviously don't want this record adding again so just want to find out its id and not add it.
I know there is a function in Django; get_or_create which does this but is there something similar for forms? or would I have to do something like
if form.is_valid():
f = form.save(commit=false)
id = get_or_create(name=f.name, author=f.author, price=f.price)
Thanks
I like this approach:
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
book, created = Book.objects.get_or_create(**form.cleaned_data)
That way you get to take advantage of all the functionality of model forms (except .save()) and the get_or_create shortcut.
You just need two cases in the view before the postback has occurred, something like
if id:
form = MyForm(instance=obj)
else
form = MyForm()
then you can call form.save() in the postback and Django will take care of the rest.
What do you mean by "if an identical record exists"? If this is a simple ID check, then your view code would look something like this:
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
form.save()
else:
if get_id:
obj = MyModel.objects.get(id=get_id)
form = MyForm(instance=obj)
else:
form = MyForm()
The concept here is the check occurs on the GET request, such that on the POST to save, Django will already have determined if this is a new or existing record.
If your check for an identical record is more complex, it might require shifting the logic around a bit.
I would do this -
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
name = form.cleaned_data['name']
author = form.cleaned_data['author']
price = form.cleaned_data['prince']
if name and author and price:
book, created = Book.objects.get_or_create(name=name, \
author=author, price=price)
if created:
# fresh entry in db.
else:
# already there, maybe update?
book.save()
Based on the answers and comments, I had to create a different solution for my case, which included the use of unique_together on the base model. You may find this code useful as well, as I actually made it fairly generic.
I have custom code in the form.save() method that I want to utilize for creating a new object, so I don't want to simply not use the form.save() call. I do have to put my code check in the form.save() method, which I think is a reasonable place to put it.
I have a utility function to flatten iterables.
def flatten(l, a=list()):
"""
Flattens a list. Just do flatten(l).
Disregard the a since it is used in recursive calls.
"""
for i in l:
if isinstance(i, Iterable):
flatten_layout(i, a)
else:
a.append(i)
return a
In the ModelForm, I overwrite the validate_unique() method:
def validate_unique(self):
pass
This is about what my save method looks like:
def save(self, commit=True):
unique_fields = flatten(MyObject._meta.unique_together)
unique_cleaned_data = {k: v for k, v in self.cleaned_data.items() if k in unique_fields}
# check if the object exists in the database based on unique data
try:
my_object = MyObject.objects.get(**unique_cleaned_data)
except MyObject.DoesNotExist:
my_object = super(MyModelFormAjax, self).save(commit)
# -- insert extra code for saving a new object here ---
else:
for data, value in self.cleaned_data.items():
if data not in unique_fields:
# only update the field if it has data; otherwise, retain
# the old value; you may want to comment or remove this
# next line
if value:
setattr(my_object, data, value)
if commit:
my_object.save()
return my_object