Django: "limit_choices_to" doesn't work on ManyToManyField
I am running Django 1.1 and cannot get the "limit_choices_to" option开发者_如何学Go 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
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:
- the name of your model (in the above example it's "lock")
- the name of the ManyToManyField field in your model (in the above example it's "unlocked_photos")
- the name of the related model (in the above example it's "MemberPhoto")
- the name of the field you want to filter related objects by (in the above example it's "member")
- 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")
- And finally make sure your class names for the custom admin form and admin model all match up.
Hope this helps somebody!
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
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)
精彩评论