Django: "limit_choices_to" doesn't work on ManyToManyField
Asked Answered
H

4

7

I am running Django 1.1 and cannot get the "limit_choices_to" option for my ManytoManyField to work.

I have two models:

class MemberPhoto(ImageModel):
    title       = models.CharField(_('title'), max_length=255, blank=True, null=True)
    caption     = models.CharField(_('caption'), max_length=255, blank=True, null=True)
    date_added  = models.DateTimeField(_('date added'), default=datetime.now, editable=False)
    member      = models.ForeignKey(User)

    def __unicode__(self):
        return u'%s (%s)' % (self.member.username, self.id)

and

class lock(models.Model):
    user = models.ForeignKey(User, related_name="owner")
    to_user = models.ForeignKey(User, related_name="to_user")
    unlocked_photos = models.ManyToManyField(MemberPhoto, blank=True, null=True, limit_choices_to = {'member':'user'})
    objects = locking_manager()

in the second model, i want to make sure in Django's admin that the only "unlocked_photos" ("MemberPhoto" objects) presented in the multiple select field are those who have a "member" value (a User object) the same as the "lock" object's "user" (also a User object).

I thought I had followed the Django docs on this, but it doesn't work. I get the following error:

TemplateSyntaxError

Caught an exception while rendering: invalid input syntax for integer: "user"

I've tried changing the "limit_choices_to" to things like:

limit_choices_to = {'member': user} --- Doesnt work

limit_choices_to = {'member__username':'kyle'} --- this DOES work but it's useless, i'm just manually specifying a username

How can I instead get the user from the current "lock" object and filter the MemberPhoto "member" property by that?

Thanks to anybody who can help.

Kyle

Heterocyclic answered 20/8, 2011 at 17:36 Comment(0)
H
15

I found an answer that achieves exactly what I wanted at this link: Django MTMField: limit_choices_to = other_ForeignKeyField_on_same_model?, and I'm posting my working code here for anybody having the same problem. It seems from looking around that "limit_choices_to" may simply not be able to achieve what I wanted, and that customizing the form used by the admin is the way to go:

from django.contrib import admin
from django import forms
from gayhop.apps.locking.models import lock
from gayhop.apps.photos.models import MemberPhoto

class LockAdminForm(forms.ModelForm):
  class Meta:
    model = lock

  def __init__(self, *args, **kwargs):
    super(LockAdminForm, self).__init__(*args, **kwargs)
    self.fields['unlocked_photos'].queryset = MemberPhoto.objects.filter(member=self.instance.user)


class LockAdmin(admin.ModelAdmin):
  form = LockAdminForm
  filter_horizontal = ('unlocked_photos',)

django.contrib.admin.site.register(lock, LockAdmin)

All you have to change is:

  1. the name of your model (in the above example it's "lock")
  2. the name of the ManyToManyField field in your model (in the above example it's "unlocked_photos")
  3. the name of the related model (in the above example it's "MemberPhoto")
  4. the name of the field you want to filter related objects by (in the above example it's "member")
  5. the value for the field you want to use to filter related objects by (it will start with "self.instance." and then be the name of the field, in the above example it's "user")
  6. And finally make sure your class names for the custom admin form and admin model all match up.

Hope this helps somebody!

Heterocyclic answered 21/8, 2011 at 8:2 Comment(2)
You could have done it using the form_for_foreignkey method of the admin models as well. ;)Overweary
You may need to enclose the assignment in a try block to cater for the create use case where the filter values do not exist yet.Trula
P
5

To add to Kyle's answer,

Creating a custom form is the only way for to customize a many to many field in that way. But, like I discovered, that method only works if you are changing an instance of that model. (at least in Django 1.5)

This is because: self.instance will return Model object. (I know crazy concept) But if you are creating an instance of that model, since that model hasn't been created yet, the self.instance will return a DoesNotExist exception.

A way around this issue is create two forms:

class MyModelChangeForm(forms.ModelForm):
    class Meta:
        model = MyModel

    def __init__(self, *args, **kwargs):
        super(MyModelChangeForm, self).__init__(*args, **kwargs)
        my_model = self.instance
        self.fields['fields'].queryset = OtherRelatedModel.objects.filter(other_id=my_model.other)


class MyModelCreationForm(forms.ModelForm):
    class Meta:
        model = MyModel

    def save(self, commit=True):
        my_model = super(MyModelCreationForm, self).save(commit=False)
        *** Do other things with the my_model if you want ***
        my_model.save()
        return my_model

Then inside admin.py we would create a separate fieldset for our creation form:

class MyModelAdmin(admin.ModelAdmin):
    filter_horizontal = ('other')
    list_display = ('field1', 'field2' 'field3')

    fieldsets = (
        ("Model info:", {'fields': ("field1", "field2", "field3")}),
        ("More Model info:", {'fields': ("other",)}),
    )
    add_fieldsets = (
        ("Initial info:", {'fields': ("field1", "field2", "field3")}),
    )

    form = MyModelChangeForm
    add_form = MyModelCreationForm

    def get_fieldsets(self, request, obj=None):
        if not obj:
            return self.add_fieldsets
        return super(MyModelAdmin, self).get_fieldsets(request, obj)

    def get_form(self, request, obj=None, **kwargs):
        """
        Use special form during MyModel creation
        """
        defaults = {}
        if obj is None:
            defaults.update({
                'form': self.add_form,
                'fields': admin.util.flatten_fieldsets(self.add_fieldsets),
            })
        defaults.update(kwargs)
        return super(MyModelAdmin, self).get_form(request, obj, **defaults)

Note that we had to override the get_form and get_fieldsets that if the obj is None (or in other words if the request is to add an instance of the model) it uses the MyModelCreationForm. This is the same method that the django developers use in django.contrib.auth.admin to get their custom UserCreation form and fieldsets. (Look in side the source code there for that example)

Finally the models would look something like this in model.py:

class MyModel(models.Model):
    *** field definitions ***
    other = models.ManytoManyField(OtherRelatedModel, null=True, blank=True)

class OtherRelatedModel(models.Model):
    other_id = model.AutoField(primary_key=True)
    *** more field definitions ***

The only reason I included the models is so you could see the many to many field definition in class MyModel. It doesn't have to have null and blank set to True. Just remember if you don't you will have to either assign them a default value in the definition or set them in the save() function in the MyModelCreationForm.

Hope this helps! (If it's utterly wrong please correct me! I need to learn too.)

-Thanks

Pinsk answered 31/7, 2013 at 22:14 Comment(1)
Could you show us an example? I have a similar question here: #77705207Beitch
P
1

I spent few hours trying to get this to work for Django 3. Pasting it here for fellow Django people. The piece that needed updating was the "def init" part.

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.fields['unlocked_photos'].queryset = MemberPhoto.objects.filter(member=self.instance.user)
Prehensible answered 20/5, 2021 at 6:2 Comment(0)
S
0

Until save self.instance will not return anything. Try to use django-smart-selects

Stead answered 9/1 at 10:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.