views:

5023

answers:

4

Another Django Form question.

My form :

class PlanForm(forms.ModelForm):    
    owner = forms.ModelChoiceField(label="",
                                  queryset=Profile.objects.all(),
                                  widget=forms.HiddenInput())
    etc...

    class Meta:
        model = Plan

Owner, in the model, is a ForeignKey to a Profile.

When I set this form, I set the value of "owner" to be a Profile object.

But when this comes out on the form, it seems to contain the name of the Profile like this :

<input type="hidden" name="owner" value="phil" id="id_owner" />

When the form is submitted and gets back to my views.py I try to handle it like this :

    form = PlanForm(request.POST)
    ...
    if form.is_valid():                
        plan = form.save()
        return HttpResponseRedirect('/plans/%s'%plan.id) # Redirect after POST

However, what I get is a type-conversion error as it fails to turn the string "phil" (the user's name that was saved into the "owner" field) into an Int to turn it into the ForeignKey.

So what is going on here. Should a ModelForm represent a foreign key as a number and transparently handle it? Or do I need to extract the id myself into the owner field of the form? And if so, how and when do I map it back BEFORE I try to validate the form?

+1  A: 

There's usually no need to put related object into form field. There's a better way and this is specifying parent id in form URL.

Let's assume you need to render a form for new Plan object and then create one when form is bubmitted. Here's how your urlconf would look like:

(r"/profile/(?P<profile_id>\d+)/plan/new", view.new_plan), # uses profile_id to define proper form action
(r"/profile/(?P<profile_id>\d+)/plan/create", view.create_plan) # uses profile_id as a Plan field

And if you're changing existing object, all you need is plan_id, you can deduce any related record from it.

Alex Lebedev
that's an interesting approach. It isn't right for me in this case because Plans are not conceptually "below" owner Profiles (and I may have a lot of other related objects) But I can see it working in some cases.
interstar
Seconded. This is the approach I prefer to use myself, too.
ayaz
Along the same lines, the URLConf could be(r'^plan/create', view.plan.create),and pass the profile and/or other plan info as a GET. Your view checks for GET params and pre-populates the form with them. Plan creation is still idempotent, since the GET request doesn't actually create a new Plan, just seeds the form.
Chris Lawlor
+2  A: 

When you assign a Profile object to the form, Django stringifies it and uses the output as the value in the form. What you would except though, is for Django to use the ID of the object instead.

Luckily, the workaround is simple: Just give the form primary key values of the Profile objects instead:

form = PlanForm(initial={'profile': profile.pk})

On the other end, when you're working with bound forms, however, they work much more sensibly:

form = PlanForm(request.POST)
if form.is_valid():
    print form.cleaned_data['profile']  # the appropriate Profile object
Guðmundur H
OK ... that helps. Weird you have to explicitly take the .pk when creating the form but not re-reference when reading
interstar
+4  A: 

I suspect that the __unicode__ method for the Profile model instance, or the repr thereof is set to return a value other than self.id. For example, I just set this up:

# models.py
class Profile(models.Model):
    name = models.CharField('profile name', max_length=10)

    def __unicode__(self):
        return u'%d' % self.id

class Plan(models.Model):
    name = models.CharField('plan name', max_length=10)
    profile = models.ForeignKey(Profile, related_name='profiles')

    def __unicode__(self):
        return self.name


# forms.py
class PlanForm(forms.ModelForm):
    profile = forms.ModelChoiceField(queryset=Profile.objects.all(),
            widget=forms.HiddenInput())

    class Meta:
        model = Plan

# views.py
def add_plan(request):

    if request.method == 'POST':
        return HttpResponse(request.POST['profile'])


    profile = Profile.objects.all()[0]
    form = PlanForm(initial={'profile':profile})
    return render_to_response('add_plan.html',
            {
                'form':form,
            },
            context_instance=RequestContext(request))

With that, I see PlanForm.profile rendered thus in the template:

<input type="hidden" name="profile" value="1" id="id_profile" />
ayaz
you're quite right. That explains the mystery.
interstar
+2  A: 

Hmm...

This might actually be a security hole.

Suppose a malicious attacker crafted a POST (say, by using XmlHttpRequest from FireBug) and set the profile term to some wacky value, like, your profile ID. Probably not what you wanted?

If possible, you may want to get the profile from the request object itself, rather than what's being submitted from the POST values.

form = PlanForm(request.POST)
if form.is_valid():
    plan = form.save(commit=False)
    plan.owner = request.user.get_profile()
    plan.save()
    form.save_m2m() # if neccesary
TokenMacGuy
+1. POST data can easily be tempered. But why reference the profile when we can reference the user?
muhuk
The `commit=False` trick saved my day, thanks.
NiKo