I have a web report that uses a Django form (new forms) for fields that control the query used to generate the report (start date, end date, ...). The issue I'm having is that the page should work using the form's initial values (unbound), but I can't access the cleaned_data field unless I call is_valid(). But is_valid() always fails on unbound forms.
It seems like Django's forms were designed with the use case of editing data such that an unbound form isn't really useful for anything other than displaying HTML.
For example, if I have:
if request.method == 'GET':
form = MyForm()
else:
form = MyForm(request.method.POST)
if form.is_valid():
do_query(form.cleaned_data['start_date'], form.cleaned_data['end_date'])
is_valid() will fail if this is a GET (since it's unbound), and if I do:
if request.method == 'GET':
form = MyForm()
do_query(form.cleaned_data['start_date'], form.cleaned_data['end_date'])
else:
form = MyForm(request.method.POST)
if form.is_valid():
do_query(form.cleaned_data['start_date'], form.cleaned_data['end_date'])
the first call to do_query triggers exceptions on form.cleaned_data, which is not a valid field because is_valid() has not been called. It seems like I have to do something like:
if request.method == 'GET':
form = MyForm()
do_query(form['start_date'].field.initial, form['end_date'].field.initial)
else:
form = MyForm(request.method.POST)
if form.is_valid():
do_query(form.cleaned_data['start_date'], form.cleaned_data['end_date'])
that is, there isn't a common interface for retrieving the form's values between a bound form and an unbound one.
Does anyone see a cleaner way to do this?
If you add this method to your form class:
def get_cleaned_or_initial(self, fieldname):
if hasattr(self, 'cleaned_data'):
return self.cleaned_data.get(fieldname)
else:
return self[fieldname].field.initial
you could then re-write your code as:
if request.method == 'GET':
form = MyForm()
else:
form = MyForm(request.method.POST)
form.is_valid()
do_query(form.get_cleaned_or_initial('start_date'), form.get_cleaned_or_initial('end_date'))
Unbound means there is no data associated with form (either initial or provided later), so the validation may fail. As mentioned in other answers (and in your own conclusion), you have to provide initial values and check for both bound data and initial values.
The use case for forms is form processing and validation, so you must have some data to validate before you accessing cleaned_data.
You can pass a dictionary of initial values to your form:
if request.method == "GET":
# calculate my_start_date and my_end_date here...
form = MyForm( { 'start_date': my_start_date, 'end_date': my_end_date} )
...
See the official forms API documentation, where they demonstrate this.
edit: Based on answers from other users, maybe this is the cleanest solution:
if request.method == "GET":
form = MyForm()
form['start_date'] = form['start_date'].field.initial
form['end_date'] = form['end_date'].field.initial
else:
form = MyForm(request.method.POST)
if form.is_valid():
do_query(form.cleaned_data['start_date'], form.cleaned_data['end_date'])
I haven't tried this though; can someone confirm that this works? I think this is better than creating a new method, because this approach doesn't require other code (possibly not written by you) to know about your new 'magic' accessor.
Related
I want to create an object with transmitting some data from other model. And it works good, but instead of creation one object of model, I got two objects.
I create one object and try modify it, but it saves two objects, created and modified. I want to save only one object, which was modified. I am using the approach that was suggested to me: Django instance in model form
Views
topic = Topic.objects.get(id=pk)
room = Room.objects.create(topic=topic)
form = RoomForm(request.POST, instance=room)
if request.method == 'POST':
if form.is_valid():
room = form.save(commit=False)
room.host=request.user
room.save()
return redirect('home')
Don't create an object yourself; let the form do this. With your approach, you create one in the GET request, and one in the POST request:
def my_view(request, pk):
topic = Topic.objects.get(id=pk)
# no create
if request.method == 'POST':
form = RoomForm(request.POST)
if form.is_valid():
form.instance.topic_id = pk
form.instance.host = request.user
form.save()
return redirect('home')
else:
form = RoomForm()
# …
if you want to modify an object which created before just do this.
search from your model and update the field you want
Topic.objects.filter(id=pk).update(fields = something)
that it bro.
remember use if when you want to sure that the object you choose is the right one
I'm trying to get form data using a POST request and save the form data to my database which was created using a django model, which is InfoModel. I'm getting the data from the POST request, but I don't know how to save all of it at once so that it all saves to the same row in the db. The way I'm doing it now, each object from the form saves to a different row of the database which is obviously not useful at all. I expect the answer is simple, but I haven't seen this in the docs.
views.py:
def home(request):
if request.method == 'POST':
# if POST request, validate the data
form = InfoForm(request.POST)
if form.is_valid():
# if the form is valid, collect the data, submit to db, and thank the user
valid = True
form_data = request.POST
f = InfoModel(fname=form_data['fname'])
f.save()
l = InfoModel(lname=form_data['lname'])
l.save()
e = InfoModel(email=form_data['email'])
e.save()
p = InfoModel(phone=form_data['phone'])
p.save()
return render(request, 'form_db/home.html', {'form': form, 'valid': valid})
else:
# if the form is invalid, populate the form with the entered data and show error message
valid = False
form = InfoForm(request.POST)
return render(request, 'form_db/home.html', {'form': form, 'valid': valid})
else:
# if GET request, return blank form as normal
form = InfoForm()
return render(request, 'form_db/home.html', {'form': form})
You can simply, give all fields' names of your InfoModel at once in the following way:
if form.is_valid():
valid=True
fName=form.cleaned_data['fname']
lname=form.cleaned_data['lname']
email=form.cleaned_data['email']
phone=form.cleaned_data['phone']
instance=InfoModel(fname=fName,lname=lname,email=email,phone=phone)
instance.save()
return render(request,"form_db/home.html",{'form': form,'valid':valid})
Note: Models in django doesn't require model to be the suffix, so it will be better if you only give model name Info rather than InfoModel.
Every time you call f = InfoModel() you are instantiating a new instance, and then saving it using f.save(), which is why you are getting so many rows. All this is unnecessary since a form has it's own save() method, which will save all the fields at once into ONE row.
The best way to handle forms is to use the classic Post/Redirect/Get method where if the form data comes in as Post, then you process it and redirect, usually back to the same view, but it can be another view as well. If it is a Get, then you render the blank form.
def home(request):
if request.method == 'POST':
form = InfoForm(request.POST or None)
if form.is_valid():
form.save()
return redirect('home')
return render(request, 'form_db/home.html', {'form':form})
Note the form = InfoForm(request.POST or None), which is handy since it will create a blank form with the None if it is not a Post request, but if it is will fill the form with the data request.POST if it's a Post request.
I made a model form in Django that enables user to create an instance of that model. All is well save one thing. Built-in error lists of Django is annoying. I want those errors to show itself when user actually made that error. When I open my page it looks like this
Please help me how do I remove these errorlist temporarily so that when user left input fields empty. It pop ups itself?
I don't know if you need these codes to solve this problem, but here it is:
views.py file here
#login_required(login_url='/accounts/login/')
def article_create(request):
article_form = ArticleForm(request.POST or None, request.FILES)
if article_form.is_valid():
instance = article_form.save(commit=False)
instance.author = request.user
article_form.save()
return render(request, 'blog/article-create.html', {'form': article_form})
Thank you
You use the request.POST or None idiom - which I dislike, but never mind - for the data parameter, but not for the files parameter. Since you pass an (empty) dict for the files, Django takes your form as bound, and therefore tries to validate it.
If you insist on using this idiom, you need to do it for both:
article_form = ArticleForm(request.POST or None, request.FILES or None)
But really, it's much more Pythonic to be explicit:
if request.method == 'POST':
article_form = ArticleForm(request.POST, request.FILES)
if article_form.is_valid():
instance = article_form.save(commit=False)
instance.author = request.user
article_form.save()
return redirect('/')
else:
article_form = ArticleForm()
return render(request, 'blog/article-create.html', {'form': article_form})
Note, you always need to redirect after a successful POST, as I have done above.
I have a form, "results", where one of the fields, "subjectID", is many-to-many because there's more than one result for each subject. I want one of the submit buttons to let me save what I've entered, then redirect to the same form, now unbound except that the many-to-many "subjectID" field stays the same so I can enter more results for that subject.
Edit: I should have made it clear that I wanted the instance that I had selected in the subjectID field to stay the same. I posted the code below that actually seems to be working for me
from models.py
class ResultsForm(forms.Modelform):
class Meta:
model = models.Results
fields = ['subjectID', # this is the field want
# to populate the form with when I "save and add another"
'slideNum', # IntegerField
'resultType' ] # ForeignKey
from views.py
def addResults(request):
if request.method == 'POST'
form = ResultsForm(request.POST)
if form.is_valid():
form.save()
if 'Save_and_add_another' in request.POST:
subjectID = form.fields['subjectID']
prepop = {'subjectID' : subjectID}
form = ResultsForm(initial=prepop)
return render(request, 'slideAdmin/addResults.html', {'form': form})
elif 'Save_and_return' in request.POST:
return HttpResponseRedirect('/home/')
else:
form = ResultsForm()
return render(request, 'slideAdmin/addResults.html', {'form': form})
Right now when I click on "save and add another" from my addResults form, I get this error:
TypeError at /slidebox/addResults
'ModelMultipleChoiceField' object is not iterable
which happens when rendering {{ form.as_p }} in the template.
Edit: Changes I made to views.py
if 'Save_and_add_another' in request.POST:
subjectID = form.cleaned_data.get('subjectID')
form = ResultsForm(initial={'subjectID': subjectID})
return render(request, 'slideAdmin/addResults.html', {'form': form})
As far as I can tell, this change works. Thanks again
You should always use form.cleaned_data.get('subjectID') versus pulling the field directly from the post data. You need to pass in a list of the pk's for the M2M field.
Your view can also use a touch of cleanup:
from django.core.urlresolvers import reverse
def addResults(request):
form = ResultsForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
form.save()
if 'Save_and_add_another' in request.POST:
subjectID = form.cleaned_data.get('subjectID', [])
if subjectID:
subjectID = subjectIDs.split(',')
form = ResultsForm(initial={'subjectID': subjectID})
elif 'Save_and_return' in request.POST:
return HttpResponseRedirect(reverse('home')) # don't hard code
return render(request, 'slideAdmin/addResults.html', {'form': form})
I'm not sure if you will be able to keep the form unbound when initialized.
Your form.fields is an ordered dict of django.forms.fields objects. You just want the ids, and not all the other info that comes across it.
Get the data straight from the POST dictionary.
subjectID = request.POST.get('subjectID', '')
If this is a true many to many model. You need to make sure the data is setup correctly for the initialization.
# We have to special-case M2Ms as a list of comma-separated PKs.
if isinstance(f, models.ManyToManyField):
initial[k] = initial[k].split(",")
Here is the initialization method from the django source code for Admin (or as I call it my super detailed and complicated Django cheat sheet, I am pedantic)
def get_changeform_initial_data(self, request):
"""
Get the initial form data.
Unless overridden, this populates from the GET params.
"""
initial = dict(request.GET.items())
for k in initial:
try:
f = self.model._meta.get_field(k)
except FieldDoesNotExist:
continue
# We have to special-case M2Ms as a list of comma-separated PKs.
if isinstance(f, models.ManyToManyField):
initial[k] = initial[k].split(",")
return initial
Some PEP8 nonsense as well
classes are camel case ex: class MyAwesomeClass(object):
everything else is lower with underscores. ex: awesome_id = awesome1245
Good Luck!!
I have simple view in django app, which I want to show only when one of the forms is valid. I have something like:
#login_required
#require_role('admin')
def new_package(request):
invoicing_data_form = InvoicingDataForm(instance=request.user.account.company.invoicingdata)
if invoicing_data_form.is_valid():
# all here
return HttpResponse('Form valid')
else:
logger.info("Form invalid")
return HttpResponse(json.dumps(invoicing_data_form.errors)
I always get log info message that form is invalid, however, I get nothing in
invoicing_data_form.errors
It is very strange, because I am validating this form in other view using user input data and it works just fine. Any idea?
EDIT:
Just for clarification.
I am not requesting any data from user in this form.
I am using this form to validate some model instance (this form is subclassing from ModelForm).
That's because you're not "feeding" your form.
Do this:
invoicing_data_form = InvoicingDataForm(instance=invoice, data=request.POST or None)
You have an unbound form.
https://docs.djangoproject.com/en/1.7/ref/forms/api/#bound-and-unbound-forms
A Form instance is either bound to a set of data, or unbound.
If it’s bound to a set of data, it’s capable of validating that data and rendering the form as HTML with the data displayed in the HTML.
If it’s unbound, it cannot do validation (because there’s no data to validate!), but it can still render the blank form as HTML.
To bind data to a form, pass the data as a dictionary as the first parameter to your Form class constructor:
invoicing_data_form = InvoicingDataForm(request.POST or None, instance=invoice)
If you're already giving request.POST to your form using request.POST or None, but it's still invalid without errors, check that there isn't any redirect going on. A redirect loses your POST data and your form will be invalid with no errors because it's unbound.
I got this for AuthenticationForm which needs AuthenticationForm(None, request.POST) see Using AuthenticationForm in Django
I want to expand on the answer by #yuji-tomita-tomita
I typically use a CBV approach in Django, and how I'm handling forms:
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
# do things
Reading the source code I noticed that self.get_form() using get_form_kwargs(self) to populate the form with request.POST, thus getting bound to data. So if you're overloading it like I did:
def get_form_kwargs(self):
company = self.get_company()
return {"company": company}
Make sure to call the super(), and it will finally work:
def get_form_kwargs(self):
company = self.get_company()
kwargs = super().get_form_kwargs()
kwargs.update({"company": company})
return kwargs