Passing data through Django forms - python

I feel I'm missing the obvious but I can't work it out!
I have written a custom form (for use outside of django admin), which I want to use to create / update instances of a number of model instances as well as hold conditional fields. However I seem to be losing my conditional data.
In my view I instansiate an instance of my form and pass it into the request context:-
view.py
form = MyForm(my_bool=True, pid=7)
render(request 'my_page.html', {'form': form})
forms.py
class MyForm(forms.Form):
def __init__(self, *args, **kwargs):
my_bool = kwargs.pop('my_bool', False)
self.pid = kwargs.pop('pid', None)
super(MyForm, self).__init__(*args, **kwargs)
if my_bool:
self.fields['textbox'] = forms.CharField(max_length=256)
That all works fine and the form renders as expected. Now when I submit the form it hits the below
view.py
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
# Do Stuff
My problem is that the form object in the above code does not contain my 'textbox' or 'pid' fields even though the form I submitted did. I'm certain whatever I'm doing wrong is extremely obvious but from a whole lot of googling I cant work out a simple way to instantiate a form, pass it some data about what fields I want to show as well as say the id of the model I eventually want it to update and then have access to the info I passed in the post part of the code.
The # Do Stuff part of my code is supposed to take the 'pid' I passed to the form and use that to fetch a product e.g. Product.objects.get(pk=pid), however without storing the pid in the session I cant work out how to access it from the postback.
If a validation error occurs in my form i.e. form.is_valid() returns false I render the request again passing the form I already have, that way I see the validation errors however any conditional fields I passed initially are missing and their values ignored.
Maybe I'm just going about it completely wrong and I should be doing this a different way. What I am trying to achieve is a content entry form whose fields change depending on the type of product it is passed, once filled in the form saves the data to that instance of the product.
Any help would be greatly appreciated, I've found Django to be extremely accommodating to anything I've thrown at it so far and this feels like such a common use-case that I must just be doing it wrong!
I'm on Django v1.11 & Python v3.6.

you are passing two arguments to create the form, namely my_bool and pid. But you are not passing those arguments in the POST view. In particular my_bool is None so the textbox field never gets generated. Changing
form = MyForm(request.POST)
to
form = MyForm(request.POST, my_bool=True, pid=7)
should do the trick.

Related

How can I avoid form.is_valid() flagging an empty field as an error in Django?

I have a template that is used for updating a table. However, the user can choose to update one field or all of them. The chances are that he won't be updating all the fields, so I want to be able to run form.is_valid(), but I don't want to have it return false if there are empty fields in the form which there will almost certainly be.
I don't want to use required=False in the form, because there is another template that uses the same form where the user must fill in almost all the fields.
The simplest thing to do would probably be to subclass your form and set required=False in the subclass, either repeating the field definition or doing it in __init__. Otherwise, you could set required=False in your view, eg:
def some_view(request):
if request.METHOD == 'POST':
form = SomeFormClass(request.POST)
form.fields['fieldname'].required = False
if form.is_valid():
# etc

django custom form validation

In Django/Python, when you make a custom form, does it need to have a clean() method, or will calling .is_valid() perform a default validation?
if request.method == 'POST':
filter = FilterForm(request.POST)
if filter.is_valid():
print 'Month is ' + filter.cleaned_data['month']
print 'Type is ' + filter.cleaned_data['type']
print 'Number is ' +filter.cleaned_data['number']
else:
print 'Invalid form'
print filter.errors
"Invalid Form" gets printed but no errors get printed.
class FilterForm(forms.Form):
months = [('January','January'),
('February','February'),
('March','March'),
('April','April'),
('May','May'),
('June','June'),
('July','July'),
('August','August'),
('September','September'),
('October','October'),
('November','November'),
('December','December'),]
types = [('text','Text'),
('call','Call'),]
month = forms.ChoiceField(months)
type = forms.ChoiceField(choices=types,widget=forms.CheckboxSelectMultiple)
def __init__(self,numbers,*args, **kwargs):
super(FilterForm,self).__init__(*args, **kwargs)
self.fields['number'] = forms.ChoiceField(choices=numbers)
def clean(self):
return self.cleaned_data
I've tried it with and without the clean method.
does it need to have a clean() method
No. Completely optional.
There's a big list of things that Django does in a specific order when it validates forms. You can learn about the process here:
http://docs.djangoproject.com/en/dev/ref/forms/validation/
As for finding your problem, if you stick a {{form.errors}} on your template, you'll see which field is blowing up. I have a feeling it could be that your choices is defined in a place that something can't get a handle on when it needs to (Move them out of the class).
Edit: Almost missed this. Look at this line:
def __init__(self,numbers,*args, **kwargs)
And then look at this line:
filter = FilterForm(request.POST)
You need to pass the numbers argument in this time too. It's a completely new instance. it can't validate because it doesn't know what numbers is.
If you have no specific clean method, Django will still validate all the fields in your form to ensure that all required fields are present, that they have the correct type where necessary, and that fields with choices have a value corresponding to one of the choices.
There are a couple of issues with this form that could be causing your problem. Firstly, you have overridden __init__ so that the first parameter after self is numbers. However, when you instantiate the form you don't pass this parameter - you just pass request.POST.
Secondly, it's a bad idea to add fields dynamically to self.fields as you do in __init__. Instead, declare your number field with an empty choices list and just set that in __init__:
self.fields['number'].choices = numbers

Django 1.1 FormWizard, Dynamically extend form

I am trying to create a multipage form, where the number of field elements on the second page is defined by the answers given on the first.
I have a formWizard set up and my understanding is that I need to use process_step() to alter the setup for the next page. I can either extend an existing form definition to add more elements, or merge 2 or more form definitions together to produce the correct number of form elements, but i have no idea how to do this.
Eg
Page 1 - Select interested subjects:
Page 2 - for each subject: ask relevant questions. Questions are defined as seperate forms in application, but need to be shown on one page, or merged into a single form.
Any help much appreiciated.
Spender
Spender,
At least at the moment I don't know a way of merging multiple forms onto one page in a FormWizard. In django 1.2 you will be able to include FormSets as steps in FormWizards (as per this ticket) but those only deal with multiple copies of identical forms, not compilations of many forms. But there is a way to do what you ask:
from django.contrib.formtools.wizard import FormWizard
from django import forms
class SubjectForm(forms.Form):
subjects = forms.MultipleChoiceField(choices = (('language', 'language'),
('sport','sport')))
class RelatedQForm(forms.Form):
"""Overload the __init__ operator to take a list of forms as the first input and generate the
fields that way."""
def __init__(self, interested_subjects, *args, **kwargs):
super(RelatedQForm, self).__init__(*args, **kwargs)
for sub in interested_subjects:
self.field[sub] = forms.CharField(label = "What do you think about %s" % subject)
class SubjectWizard(FormWizard):
def done(self, request, form_list):
process_form_list(form_list)
def process_step(self, request, form, step):
if step == 1:
chosen_subs = form.cleaned_data['subjects']
self.form_list[1] = RelatedQForm(chosen_subs)
With this code you instantiate your FormWizard as you normally would in the view and then let the wizard class take care of everything behind the scenes.
The general idea is to overload the init class of a "RelatedQForm" to dynamically alter the fields. This code snippet was taken from here. You can make the processing within the init operator as complex as you'd like, read "include the fields from your forms as if-elif blocks inside the for-loop" ... you could probably even figure out a way to strip the fields from your current forms programatically, I'd have to see them to figure it out though.
Your "process_form_list" function will need to loop over the fields using something like:
for field, val in form.cleaned_data.items():
do_stuff
Hope this gets you on your way :)
I don't think the
self.form_list[1] = RelatedQForm(chosen_subs)
part works. I always get the error message:
object is not callable
It seems to be like form_list only accepts RelatedQForm (the name of the form), not an instance of it.

How do I build a custom "list-type" entry to request.POST

Basically I have a model with a ManyToMany field, and then a modelform derived from that model where that field is rendered as a "multiple choice" selectbox. In my template I'm having that field omitted, electing instead to prepare the values for that field in the view, then pass those prepared values into request.POST (actually a copy of request.POST because request.POST is immutable), then feeding request.POST to the form and then carry on as normal. I can't figure out how to do this, because request.POST isn't just a simple python dictionary, but instead a QueryDict, which behaves a little differently.
The field I need to populate is called "not_bases". When I create the widget using the form, it works perfectly well internally, but just not to my liking UI-wise. When I inspect the django-form submitted POST value via django's handy debug error window, the working QueryDict looks like this:
<QueryDict: {u'not_bases': [u'005', u'00AR', u'00F', u'00FD'], [...] }>
It appears the value for "not_bases" is a list, but it's not simply a list. I can't just .append() to it because it won't work. I dug around the documentation and found .update(), which appears to work, but doesn't. Here is my code:
newPOST = request.POST.copy()
for base in bases:
newPOST.update({"not_bases": base.identifier})
and here is the output:
<QueryDict: {u'not_bases': [u'KMER', u'KYIP'], u'reference': [u''], [...] }>
But when I feed that QueryDict to the form, I get an form validation error that says "not_bases: Enter a list of values.". Its obvious that the list-looking things coming from the str() representation of the QueryDict are not the same in the two cases above, even though they look exactly the same
So how do I do this?
It's really not clear what you're trying to do here, but I doubt that hacking the QueryDict is the right way to achieve it.
If you are trying to customise the display of the not_bases field, you can simply override the definition in your modelform declaration:
class MyModelForm(forms.ModelForm):
not_bases = forms.ChoiceField(choices=[(base, base) for base in bases])
class Meta:
model = MyModel
Or, if you simply want to avoid showing it on the form, you can exclude it from the form and set the value after validation.
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
exclude = ['not_bases']
....
if request.POST:
if form.is_valid():
instance = form.save(commit=False)
instance.not_bases = bases
instance.save()
Does either of these do what you want?

Django Form Preview - How to work with 'cleaned_data'

Thanks to Insin for answering a previous question related to this one.
His answer worked and works well, however, I'm perplexed at the provision of 'cleaned_data', or more precisely, how to use it?
class RegistrationFormPreview(FormPreview):
preview_template = 'workshops/workshop_register_preview.html'
form_template = 'workshops/workshop_register_form.html'
def done(self, request, cleaned_data):
# Do something with the cleaned_data, then redirect
# to a "success" page.
registration = Registration(cleaned_data)
registration.user = request.user
registration.save()
# an attempt to work with cleaned_data throws the error: TypeError
# int() argument must be a string or a number, not 'dict'
# obviously the fk are python objects(?) and not fk_id
# but how to proceed here in an easy way?
# the following works fine, however, it seems to be double handling the POST data
# which had already been processed in the django.formtools.preview.post_post
# method, and passed through to this 'done' method, which is designed to
# be overidden.
'''
form = self.form(request.POST) # instansiate the form with POST data
registration = form.save(commit=False) # save before adding the user
registration.user = request.user # add the user
registration.save() # and save.
'''
return HttpResponseRedirect('/register/success')
For quick reference, here's the contents of the post_post method:
def post_post(self, request):
"Validates the POST data. If valid, calls done(). Else, redisplays form."
f = self.form(request.POST, auto_id=AUTO_ID)
if f.is_valid():
if self.security_hash(request, f) != request.POST.get(self.unused_name('hash')):
return self.failed_hash(request) # Security hash failed.
return self.done(request, f.cleaned_data)
else:
return render_to_response(self.form_template,
{'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state},
context_instance=RequestContext(request))
I've never tried what you're doing here with a ModelForm before, but you might be able to use the ** operator to expand your cleaned_data dictionary into the keyword arguments expected for your Registration constructor:
registration = Registration (**cleaned_data)
The constructor to your model classes take keyword arguments that Django's Model meta class converts to instance-level attributes on the resulting object. The ** operator is a calling convention that tells Python to expand your dictionary into those keyword arguments.
In other words...
What you're doing currently is tantamount to this:
registration = Registration ({'key':'value', ...})
Which is not what you want because the constructor expects keyword arguments as opposed to a dictionary that contains your keyword arguments.
What you want to be doing is this
registration = Registration (key='value', ...)
Which is analogous to this:
registration = Registration (**{'key':'value', ...})
Again, I've never tried it, but it seems like it would work as long as you aren't doing anything fancy with your form, such as adding new attributes to it that aren't expected by your Registration constructor. In that case you'd likely have to modify the items in the cleaned_data dictionary prior to doing this.
It does seem like you're losing out on some of the functionality inherent in ModelForms by going through the form preview utility, though. Perhaps you should take your use case to the Django mailing list and see if there's a potential enhancement to this API that could make it work better with ModelForms.
Edit
Short of what I've described above, you can always just extract the fields from your cleaned_data dictionary "by hand" and pass those into your Registration constructor too, but with the caveat that you have to remember to update this code as you add new fields to your model.
registration = Registration (
x=cleaned_data['x'],
y=cleaned_data['y'],
z=cleaned_data['z'],
...
)

Categories