views:

7567

answers:

4

Say I have the following in my models.py:

class Company(models.Model):
   name = ...

class Rate(models.Model):
   company = models.ForeignKey(Company)
   name = ...

class Client(models.Model):
   name = ...
   company = models.ForeignKey(Company)
   base_rate = models.ForeignKey(Rate)

I.e. there are multiple Companies, each having a range of Rates and Clients. Each Client should have a base Rate that is chosen from it's parent Company's Rates, not another Company's Rates.

When creating a form for adding a Client, I would like to remove the Company choices (as that has already been selected via an "Add Client" button on the Company page) and limit the Rate choices to that Company as well.

How do I go about this in Django 1.0?

My current forms.py file is just boilerplate at the moment:

from models import *
from django.forms import ModelForm

class ClientForm(ModelForm):
    class Meta:
        model = Client

And the views.py is also basic:

from django.shortcuts import render_to_response, get_object_or_404
from models import *
from forms import *

def addclient(request, company_id):
    the_company = get_object_or_404(Company, id=company_id)

    if request.POST:
     form = ClientForm(request.POST)
     if form.is_valid():
      form.save()
      return HttpResponseRedirect(the_company.get_clients_url())
    else:
     form = ClientForm()

    return render_to_response('addclient.html', {'form': form, 'the_company':the_company})

In Django 0.96 I was able to hack this in by doing something like the following before rendering the template:

manipulator.fields[0].choices = [(r.id,r.name) for r in Rate.objects.filter(company_id=the_company.id)]

ForeignKey.limit_choices_to seems promising but I don't know how to pass in the_company.id and I'm not clear if that will work outside the Admin interface anyway.

Thanks. (This seems like a pretty basic request but if I should redesign something I'm open to suggestions.)

+25  A: 

ForeignKey is represented by django.forms.ModelChoiceField, which is a ChoiceField whose choices are a model QuerySet. See the reference for ModelChoiceField.

So, provide a QuerySet to the field's queryset attribute. Depends on how your form is built. If you build an explicit form, you'll have fields named directly.

form.rate.queryset = Rate.objects.filter(company_id=the_company.id)

If you take the default ModelForm object, form.fields["rate"].queryset = ...

This is done explicitly in the view. No hacking around.

S.Lott
Ok, that sounds promising. How do I access the relevant Field object? form.company.QuerySet = Rate.objects.filter(company_id=the_company.id) ? or via a dictionary?
Tom
Ok, thanks for expanding the example, but I seem to have to use form.fields["rate"].queryset to avoid "'ClientForm' object has no attribute 'rate'", am I missing something? (and your example should be form.rate.queryset to be consistent too.)
Tom
Excellent, thanks for clarifying. For future reference, it might be worth noting when you edit your answer via a comment because edits don't show up in the responses tab of my user page.
Tom
Wouldn't it be better to set the fields' queryset, in the form's `__init__` method?
Lakshman Prasad
@becomingGuru. You can't. A Form's `__init__` method populates the Form's data for validation purposes, usually from `request.POST`
S.Lott
@SLott the last comment is not correct (or my site shouldn't be working :). You can populate the validation data by making using the super(...).__init__ call in your overridden method. If you are making several of these queryset changes its a lot more elegant to package them by overriding the __init__ method.
michael
@michael: Since becomingGuru provided no code, it was not possible to deduce what they were talking about. You're approach may (or may not) be what becomingGuru is talking about. I hesitate to guess.
S.Lott
@Slott cheers, I've added an answer as it'd take more than 600 chars to explain . Even if this question is old it's getting a high google score.
michael
Thanks so much, this answered it for me. I've been searching all over - I wish this were in the docs! (If it is and I just missed it then I'd be delighted to be corrected).
Wayne Koorts
I guess _init_ call is more DRY than doing this in every view functions. But this method is much less hacky. I gotta change one of my form class based on this method. Right now I am using a hack to avoid the validation error, which is caused the AJAX filter of foreignkey choices.
Dingle
A: 

Many thanks! I have had a very similar problem. After hours of searching the web I saw it was that simple. Nice post.

Ralf
+13  A: 

In addition to S.Lotts answer and as becomingGuru mentioned in comments, its possible to add the queryset filters by overriding the ModelForm.__init__ function. (This could easily apply to regular forms) it can help with reuse and keeps the view function tidy.

class ClientForm(forms.ModelForm):
    def __init__(self,company,*args,**kwargs):
        super (ClientForm,self ).__init__(*args,**kwargs) # populates the post
        self.fields['rate'].queryset = Rate.objects.filter(company=company)
        self.fields['client'].queryset = Client.objects.filter(company=company)

    class Meta:
        model = Client

def addclient(request, company_id):
        the_company = get_object_or_404(Company, id=company_id)

        if request.POST:
            form = ClientForm(the_company,request.POST)  #<-- Note the extra arg
            if form.is_valid():
                    form.save()
                    return HttpResponseRedirect(the_company.get_clients_url())
        else:
            form = ClientForm(the_company)

        return render_to_response('addclient.html', {'form': form, 'the_company':the_company})

This can be useful for reuse say if you have common filters needed on many models (normally I declare an abstract Form class). Eg.

class UberClientForm(ClientForm):
    class Meta:
        model = UberClient

 def view(request):
    ...
    form = UberClientForm(company)
    ...

 #or even extend the existing custom init
 class PITAClient(ClientForm):
      def __init__(company, *args, **args):
          super (PITAClient,self ).__init__(company,*args,**kwargs)
          self.fields['support_staff'].queryset = User.objects.exclude(user='michael')

Other than that I'm just restating django blog material of which there are many good ones out there.

michael
There's a typo in your first code snippet, you're defining args twice in __init__() instead of args and kwargs.
tpk
cheers, thats updated
michael
A: 

So, I've really tried to understand this, but it seems that Django still doesn't make this very straightforward. I'm not all that dumb, but I just can't see any (somewhat) simple solution.

I find it generally pretty ugly to have to override the Admin views for this sort of thing, and every example I find never fully applies to the Admin views.

This is such a common circumstance with the models I make that I find it appalling that there's no obvious solution to this...

I've got these classes:

# models.py
class Company(models.Model):
    # ...
class Contract(models.Model):
    company = models.ForeignKey(Company)
    locations = models.ManyToManyField('Location')
class Location(models.Model):
    company = models.ForeignKey(Company)

This creates a problem when setting up the Admin for Company, because it has inlines for both Contract and Location, and Contract's m2m options for Location are not properly filtered according to the Company that you're currently editing.

In short, I would need some admin option to do something like this:

# admin.py
class LocationInline(admin.TabularInline):
    model = Location
class ContractInline(admin.TabularInline):
    model = Contract
class CompanyAdmin(admin.ModelAdmin):
    inlines = (ContractInline, LocationInline)
    inline_filter = dict(Location__company='self')

Ultimately I wouldn't care if the filtering process was placed on the base CompanyAdmin, or if it was placed on the ContractInline. (Placing it on the inline makes more sense, but it makes it hard to reference the base Contract as 'self'.)

Is there anyone out there who knows of something as straightforward as this badly needed shortcut? Back when I made PHP admins for this sort of thing, this was considered basic functionality! In fact, it was always automatic, and had to be disabled if you really didn't want it!

Tim