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'],
...
)
Related
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.
I've been trying to alter the value of a form field the Django REST Framework's admin panel and for some reason the change never takes place. I have the serializer below
class SomeView(ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
# I Want to override this and change the POST data
def perform_create(self, serializer):
user = self.request.user.id
# this was a form field where I manually entered the user ID
# I now want it to default to the logged in user
serializer.data['user'] = user
# This returns the original user that was entered into the form field
print serializer.data
I checked out serializer.data with dir() and it's just a python dictionary so I can't figure out why I can't modify the value. As a test, I tried to add extra values but that doesn't work either
# this doesnt work
serializer.data['some_new_field'] = 'test'
EDIT
On another note, I can copy the data and edit it
fake_data = serializer.data.copy()
fake_data['old_value'] = 'new value'
However, it always fails to validate
serializer = MyModelSerializer(data=fake_data)
serializer.is_valid() # returns false
EDIT EDIT:
Ok, so the validation error was caused by Django returning a SimpleLazyObject. Everything works now when I perform a copy of the data, but I'm really curious as to why I can't edit serializer.data directly without copying it. The problem is solved now, but if anyone can provide insight on the issue just for curiosity, that would be awesome.
I checked out serializer.data with dir() and it's just a python dictionary so I can't figure out why I can't modify the value.
While the value returned from Serializer.data is indeed a dictionary, Serializer.data is not a simple instance variable.
If you look at rest_framework/serializers.py:
class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
# [...]
#property
def data(self):
ret = super().data
return ReturnDict(ret, serializer=self)
ReturnDict inherits from OrderedDict, but you still get a new dictionary every time you access Serializer.data.
The real data is in _data, however as noted by the underscore you might not want to modify that either as it is not intended to be public. The values are filled by Serializer.to_representation() which you could override on the viewset.
As for the second part: ModelViewSet defines get_serializer() that is called with the request POST data to create the serializer you want to modify. I'd suggest try to change the input data before the serializer is created, instead.
I have subclassed a text-field form field in Django to create my own custom widget for a field. I was wondering if it's possible to check if all other fields of the form are valid (I want its server side behavior to vary based on the validation of other fields)
See comment
Something like:
class CustomField(TextInput):
def __init__(self, *args, **kwargs):
...
super(CustomField, self).__init__(*args, **kwargs)
input_type = 'hidden'
def value_from_datadict(self, data, files, name):
aws_file_key = data.get(name, None)
_media_bucket = boto.connect_s3(settings.AWS_ACCESS_KEY_ID,
settings.AWS_SECRET_ACCESS_KEY)\
.lookup(settings.AWS_MEDIA_STORAGE_BUCKET_NAME)
try:
key = _media_bucket.get_key(aws_file_key)
except:
print 'Failed to get key.'
key = None
if key and aws_file_key:
fh = tempfile.TemporaryFile()
key.get_contents_to_file(fh)
fh.seek(0)
files = SimpleUploadedFile(key.name, fh.read())
### IF FORM IS VALID DELETE KEY, OTHERWISE, KEEP IT.
if code_to_check_if_valid:
_media_bucket.delete_key(key)
fh.close()
return files
...... etc.
If you want to validate a certain field depending on the values of other fields, you need to to it at the form level and overwrite the field's clean method. Here's the docs on the subject - they are very good.
class CustomForm(forms.Form):
custom_field = CustomField()
def clean(self):
cleaned_data = super(CustomForm, self).clean()
custom_field = cleaned_data.get("custom_field")
...
If you look at the flow of how forms are validated, you will see that the clean method is run if all the other fields validate independently, so at this stage, the form can be considered valid:
These methods are run in the order given above, one field at a time. That is, for each field in the form (in the order they are declared in the form definition), the Field.clean() method (or its override) is run, then clean_<fieldname>(). Finally, once those two methods are run for every field, the Form.clean() method, or its override, is executed.
The final clean method is actually run regardless of if there's an error so you have to iterate through the cleaned_data to make sure there are no errors
The clean() method for the Form class or subclass is always run. If that method raises a ValidationError, cleaned_data will be an empty dictionary.
The previous paragraph means that if you are overriding Form.clean(), you should iterate through self.cleaned_data.items(), possibly considering the _errors dictionary attribute on the form as well. In this way, you will already know which fields have passed their individual validation requirements.
The clean methods for individual fields are called in the same order as the form declaration order or explicitly specified order. [django source code]
Although I wouldn't recommend this approach over using the clean method for multi-field validation, if your custom field is the last field in the order, you can expect self._errors to indicate whether all other fields have passed validation or not. However at this stage, non-field errors won't be available.
I have a very complicated form and I choose to not use ModelForm since I needed flexibility and control over the fields. Since I am not using ModelForm, I can't simply do something like instance=order, where order = Order.objects.get(pk=1).
Currently I am pre-populating every field with initial in the forms.py as oppose to the views.py like this
self.fields['work_type'] = forms.ChoiceField(choices=Order.WORK_TYPE_CHOICES, initial=order.work_type)
But I was wondering if I could pass the entire order object to a form or do I have to declare initial to every field?
Is there a way to do something like
order_form = OrderEditForm(data=request.POST, initial=order)
in views.py?
I have a very complicated form and I choose to not use ModelForm since
I needed flexibility and control over the fields
Everything that you can do using a Form, you can do in a ModelForm such as adding new fields or over-riding attributes on the fields etc.
But I was wondering if I could pass the entire order object to a form
or do I have to declare initial to every field?
You can pass the order object into the form but you will still have to populate each field individually either in the forms or in the view function.
So in your view you would do something like this:
intial = {'order_number': order.number, 'order_id': order.id}
form = OrderForm(initial=initial)
The easiest way to prepopulate data to a form is passing a dictionary as first argument to de form constructor.
order_form = OrderEditForm(order.__dict__())
where __dict__() is a method that passes "order" object attributes to a dictionary with each attribute's name as a key and their content as value.
An example of how to "invent" such a method could be something like:
order_initial = Order.objects.filter(pk=order.pk).values()[0]
and then construct the form with:
order_form = OrderEditForm(order_initial)
Look at this example (how they populate values at "post" time):
For future reference to other people:
I have since found out after reading SO's comments and answers that it's better to use ModelForm even if you end up explicitly defining every field manually (using something like self.fields['foo'] = forms.CharField()).
In any case, if you are trying to pass a dictionary of current values in a form then the best (built-in) way to convert a model to a dictionary is actually using model_to_dict:
from django.forms.models import model_to_dict
order = Order.objects.get(pk=1)
dictionary = model_to_dict(order)
form = OrderEditForm(dictionary)
I got the solution from this blog. I hope this will be helpful for someone.
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