Opened 8 years ago

Closed 7 years ago

Last modified 7 years ago

#6407 closed (worksforme)

ModelChoiceField with widget=HiddenInput doesn't behave as expected

Reported by: Andy McCurdy <sedrik@…> Owned by: nobody
Component: Forms Version: master
Severity: Keywords: ModelChoiceField HiddenInput
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: UI/UX:

Description

I'm attempting to render a ModelChoiceField as a HiddenInput widget. I generate the form instance within a template tag, allowing template engineers to drop in the form wherever they want. They pass in a string parameter to the tag representing a User model instance, the tag's Node resolves it, creates a new form instance, passing initial=resolved_user_var to the form's __init__ method. I'd expect the HTML that gets generated to have an <input type="hidden" name="user" value="<id of user instance>"/>. However, the value attribute of the input tag is getting set to __unicode__ instead.

Here are the details:

I have a model with a ForeignKey to contrib.auth.models.User. The model looks like this:

class MyModel(models.Models):
    body = models.TextField()
    user = models.ForeignKey(User)

I then have a ModelForm for MyModel, called MyForm.

class MyForm(forms.ModelForm):
    body = forms.CharField(widget=forms.Textarea) 
    user = forms.ModelChoiceField(queryset=User.objects.none(), widget=forms.HiddenInput)
    
    class Meta:
        model = MyModel

When creating the form, which I'm currently doing using a template tag, I know the exact user instance I care about, so I pass that into as the initial argument of MyForm's init:

    # Normally a resolved template tag instance, but this will illustrate the point
    u = User.objects.get(id=1)
    f = MyForm(initial={'user':u})

However, when rendering the widget from my form, the value attribute of the <input> tag seems to be the result of __unicode__ rather than the ID of the user I supplied:

    f = MyForm(initial={'user':u})
    f.as_p()

>>> u'<p><label for="id_body">Body:</label> <textarea id="id_body" rows="10" cols="40" name="body"></textarea><input type="hidden" name="user" value="andy" id="id_user" /></p>'

I also tried supplying the ID to user_id, rather than the model instance, which outputted no value attribute at all:

    f = MyForm(initial={'user_id':u.id})
    f.as_p()

>>> u'<p><label for="id_body">Body:</label> <textarea id="id_body" rows="10" cols="40" name="body"></textarea><input type="hidden" name="user" id="id_user" /></p>'

The user attribute in question is not the currently logged in user, but another user, hence why I can't just use request.user and need to pass the ID through the POST.

I'm attempting to work on a patch, but wanted to ensure the behavior I'm anticipating is what is expected.

Change History (8)

comment:1 Changed 8 years ago by SmileyChris

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset
  • Resolution set to worksforme
  • Status changed from new to closed

I'm pretty sure that you should just be using:

f = MyForm(initial={'user': u.id})

comment:2 follow-up: Changed 7 years ago by josho

  • Resolution worksforme deleted
  • Status changed from closed to reopened

I'm reopening this ticket becaues there is still a problem, though not exactly as described above. I'm doing essentially the same thing as Andy, and my code is basically identical. When I pass initial={'user': user.id} the id appears properly as the value of the field, however the form fails to validate. Instead I get this error: (Hidden field user) Select a valid choice. 63 is not one of the available choices.

comment:3 in reply to: ↑ 2 ; follow-up: Changed 7 years ago by josho

Appologies, I did have a typo, I was making the field a ChoiceField rather than a ModelChoiceField. However having corrected that it still is failing, though for a different reason. The form validates just fine now, however I instead get the error that the field does not accept null values.

comment:4 in reply to: ↑ 3 Changed 7 years ago by arien

  • Resolution set to worksforme
  • Status changed from reopened to closed

I'm unable to reproduce this.

I don't see how MyForm would ever validate: for the value of user to be a valid choice it has to be in the EmptyQuerySet produced by User.objects.none(), which by definition is empty.

Changing MyForm so that the user field gets passed e.g. queryset=User.objects.all(), the form validates just fine:

>>> from django.contrib.auth.models import User
>>> from t6407.forms import MyForm
>>> 
>>> u = User.objects.get(pk=1)
>>> f = MyForm(initial={'user': u.pk})
>>> print f
<tr><th><label for="id_body">Body:</label></th><td><textarea id="id_body" rows="
10" cols="40" name="body"></textarea><input type="hidden" name="user" value="1" 
id="id_user" /></td></tr>
>>> 
>>> f = MyForm({'body': '...', 'user': '1'})
>>> f.is_valid()
True
>>> f.save()
<MyModel: MyModel object>

comment:5 follow-up: Changed 7 years ago by oceantara

  • Resolution worksforme deleted
  • Status changed from closed to reopened

But there's no reason to have to use User.objects.all(). You know the exact object you want. Plus, having a User.objects.all() is dangerous - you're selecting all user objects and exposing to a selected user. Its just too easy to imagine a simple mistake where all users or other type of foreign key objects are exposed to the end user. Isn't there a way to restrict the queryset based on Model's id?

comment:6 Changed 7 years ago by oceantara

Well I found a solution that worked very well. Now I don't have objects.all() throughout my forms.py. This would be great to have in django.

http://www.davidcramer.net/code/109/modelchoicefields-as-charfields-in-django.html

Btw - django rocks

comment:7 in reply to: ↑ 5 ; follow-up: Changed 7 years ago by arien

  • Resolution set to worksforme
  • Status changed from reopened to closed

Replying to oceantara:

But there's no reason to have to use User.objects.all().

If you use User.objects.none() it is impossible for the form to be valid: for the form to be valid, the user has to be in the queryset of the ModelChoiceField and User.objects.none() is empty by definition. You don't have to use User.objects.all(); it was just an example of a queryset you could use if you want to allow for the possibility of a form that actually validates.

You know the exact object you want.

Great, but it still has to be in the queryset of the ModelChoiceField for the form to validate, so using User.objects.none() is not going to work.

And besides, how do you know which object you want? Because the submitted form tells you that's the one you want? If so, how do you know the user hasn't changed the value you sent in the hidden field? If not, then why are you passing around data you already know?

Plus, having a User.objects.all() is dangerous - you're selecting all user objects and exposing to a selected user. Its just too easy to imagine a simple mistake where all users or other type of foreign key objects are exposed to the end user.

No, you're only saying that the form cannot be valid unless the user is one of those in User.objects.all(). Again, User.objects.all() is just an example; use whatever non-empty queryset you like.

Isn't there a way to restrict the queryset based on Model's id?

Please use the mailing list for usage questions.

Replying to oceantara:

Well I found a solution that worked very well. Now I don't have objects.all() throughout my forms.py. This would be great to have in django.

http://www.davidcramer.net/code/109/modelchoicefields-as-charfields-in-django.html

So what are you using as the queryset for this InlineModelChoiceField?

Anyway, that code is broken as-is. (You would have found out if you'd actually tried to validate an instance of MyForm that uses this field.) You'll at least want to use this in the try block:

        try:
            key = self.to_field_name or 'pk'
            return self.queryset.filter(**{key: value}).get()
         except:
            self.queryset.model.DoesNotExist:

There is no Django bug here, closing again.

comment:8 in reply to: ↑ 7 Changed 7 years ago by arien

No idea what happened there; that section of code should read:

        try:
            key = self.to_field_name or 'pk'
            return self.queryset.filter(**{key: value}).get()
         except self.queryset.model.DoesNotExist:
Note: See TracTickets for help on using tickets.
Back to Top