Reputation: 1746
I'm trying to write a simple test case to test a Django form that permits the assignment of an Orange
object to an Apple
object.
forms.py
class AppleOrangeAssignmentForm(forms.ModelForm):
orange = forms.ModelChoiceField(required=True, \
queryset=Orange.objects.filter(apple=None))
class Meta:
model = Apple
fields = ('orange')
The queryset
on orange
is there to ensure that the values in the dropdown are only Orange
s that aren't already assigned to other Apple
s. This code works correctly and consistently in the view that calls it.
In the test case below, I am create a brand new Orange
to ensure that I have one that it is not assigned anywhere else.
test.py
def test_apple_orange_assignment(self):
apple = Apple.objects.get(pk=1)
self.assertEquals(apple.orange, None)
orange = Orange.objects.create(name='clementime')
form_data = { 'orange': orange }
form = AppleOrangeAssignmentForm(data=form_data, instance=apple)
self.assertTrue(form.is_valid()) # <=== Fails here
Weirdly, the form does not validate in the test case! form.errors
says: {'orange': ['Select a valid choice. That choice is not one of the available choices.']}
. When I dig in further, I can see that the orange
I am trying to assign does appear in form.fields['orange'].queryset
, though.
I have tried everything to try to get this to validate. I have tried changing the queryset
in the form field to Orange.objects.all()
. I have tried changing the creation of the orange
variable in the test case to form.fields['orange'].queryset[0]
to ensure I'm picking an orange that is in its choices. But nothing works.
As I said, this all works perfectly in the view. What am I doing wrong here in the test case?
Upvotes: 0
Views: 1716
Reputation: 2222
From Django's official docs on the topic of Django's role in forms:
receiving and processing submitted forms and data from the client
The form is expecting data coming from the client not programmatically. In your test you attempt to pass an instance (orange
) of a python class (Orange
), but since django forms are built for accepting data from the client, it does not make sense to allow objects, but rather only values that can be inputted by a client e.g. integers, strings, floats, etc.
Because of that, Django converts ForeignKey fields to ModelChoiceField and expects the input to be the id of the instance not the instance itself, so when you pass an instance, the form attempts to validate with the passed instance as an id, and thus fails.
From the docs
class ModelChoiceField(**kwargs)
Default widget: Select
Empty value: None
Normalizes to: A model instance.
Validates that the given id exists in the queryset.
Error message keys: required, invalid_choice
Example solution:
form_data = {'orange': orange.id}
Upvotes: 2
Reputation: 4934
Firstly there's no need to put a \
after required=True \
because the statement will end with a bracket.
You need to specify the object id rather than the whole orange object when instantiating the form class.
def test_apple_orange_assignment(self):
# More code here
form_data = { 'orange': orange.id }
form = AppleOrangeAssignmentForm(data=form_data, instance=apple)
assert form.is_valid(), form.errors
That's it!
Tip: You can use assert False, form.as_p()
to make the test fail and trace the form html, and there you'll find that it isn't looking for the object but the object_id.
Upvotes: 5