在admin中以内联形式限制select中的外键选择


75

该模型的逻辑是:

  • 一个Building有很多Rooms
  • ARoom可能在另一个内部Room(例如壁橱-“ self”上的ForeignKey)
  • ARoom只能Room位于同一建筑物的另一个内部(这是棘手的部分)

这是我的代码:

#spaces/models.py
from django.db import models    

class Building(models.Model):
    name=models.CharField(max_length=32)
    def __unicode__(self):
        return self.name

class Room(models.Model):
    number=models.CharField(max_length=8)
    building=models.ForeignKey(Building)
    inside_room=models.ForeignKey('self',blank=True,null=True)
    def __unicode__(self):
        return self.number

和:

#spaces/admin.py
from ex.spaces.models import Building, Room
from django.contrib import admin

class RoomAdmin(admin.ModelAdmin):
    pass

class RoomInline(admin.TabularInline):
    model = Room
    extra = 2

class BuildingAdmin(admin.ModelAdmin):
    inlines=[RoomInline]

admin.site.register(Building, BuildingAdmin)
admin.site.register(Room)

内联将仅显示当前建筑物中的房间(这就是我想要的)。但是,问题在于,对于inside_room下拉菜单,它显示了“房间”表中的所有房间(包括其他建筑物中的房间)。

在的内联中rooms,我需要将inside_room选择限制rooms在当前的范围内building(当前正在由主BuildingAdmin表单更改的建筑记录)。

我既找不到limit_choices_to模型中的方法,也无法找出如何正确地正确覆盖管理员的内联表单集(我觉得我应该以某种方式创建自定义内联表单,并传递将主表单设置为自定义内联,然后根据该字段将查询集限制为该字段的选择-但我只是无法将精力集中在如何做上)。

也许这对于管理站点来说太复杂了,但是似乎总会有用...

Answers:


101

将请求实例用作obj的临时容器。重写内联方法formfield_for_foreignkey来修改查询集。这至少在Django 1.2.3上有效。

class RoomInline(admin.TabularInline):

    model = Room

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):

        field = super(RoomInline, self).formfield_for_foreignkey(db_field, request, **kwargs)

        if db_field.name == 'inside_room':
            if request._obj_ is not None:
                field.queryset = field.queryset.filter(building__exact = request._obj_)  
            else:
                field.queryset = field.queryset.none()

        return field



class BuildingAdmin(admin.ModelAdmin):

    inlines = (RoomInline,)

    def get_form(self, request, obj=None, **kwargs):
        # just save obj reference for future processing in Inline
        request._obj_ = obj
        return super(BuildingAdmin, self).get_form(request, obj, **kwargs)

1
这在这里为我省了很多麻烦。我需要过滤选择,但是要根据会话变量进行过滤。这个答案让我用5行代码来完成。谢谢。
彼得G

4
太感谢了!另一种方法是分配kwargs [“查询集”]主叫超级按照文档之前:docs.djangoproject.com/en/dev/ref/contrib/admin/...
powlo

这段代码也节省了我很多时间。非常感谢您发布此信息
fangsterr

这个!我正在寻找类似这样的东西来解决我的问题。花了我几天的时间找到这个。
Miguel Ike,

1
但是用户仍然可以Room在弹出窗口中选择错误。见stackoverflow.com/a/50298577/2207154的解决方案
达尼尔Mashkin

17

在阅读了这篇文章并进行了很多实验之后,我认为我已经找到了一个相当明确的答案。因为这是一种经常使用的设计模式,所以我为Django管理员编写了一个Mixin来使用它。

(动态地)限制ForeignKey字段的查询集现在就像子类化LimitedAdminMixin和定义一种get_filters(obj)返回相关过滤器的方法一样简单。或者,filters如果不需要动态过滤,则可以在管理员上设置属性。

用法示例:

class MyInline(LimitedAdminInlineMixin, admin.TabularInline):
    def get_filters(self, obj):
        return (('<field_name>', dict(<filters>)),)

在这里,<field_name>是要过滤的FK字段的名称,并且<filters>是参数的列表,就像您通常在filter()querysets方法中指定它们一样。


1
谢谢,效果很好!干净得多。(顺便说一句,您在代码中留下了一些日志语句,这些日志语句什么都没用)
Dave

17

limit_choices_to ForeignKey选项可以限制对象的可用管理员选择


2
这无济于事,因为在limit_choices_to中运行的查询没有对“父类”的引用。即,如果模型A对B以及C都有外键,并且C对B都有外键,并且我们要确保A仅引用与A引用相同B的C。 ,查询需要知道A-> B,而实际上不是。
克里斯·科格登

1
与最佳答案组合一起使用可能很有用,请参阅stackoverflow.com/a/50298577/2207154
Daniil Mashkin

8

您可以创建几个自定义类,然后将对父实例的引用传递给表单。

from django.forms.models import BaseInlineFormSet
from django.forms import ModelForm

class ParentInstInlineFormSet(BaseInlineFormSet):
    def _construct_forms(self):
        # instantiate all the forms and put them in self.forms
        self.forms = []
        for i in xrange(self.total_form_count()):
            self.forms.append(self._construct_form(i, parent_instance=self.instance))

    def _get_empty_form(self, **kwargs):
        return super(ParentInstInlineFormSet, self)._get_empty_form(parent_instance=self.instance)
    empty_form = property(_get_empty_form)


class ParentInlineModelForm(ModelForm):
    def __init__(self, *args, **kwargs):
        self.parent_instance = kwargs.pop('parent_instance', None)
        super(ParentInlineModelForm, self).__init__(*args, **kwargs)

在RoomInline类中,只需添加:

class RoomInline(admin.TabularInline):
      formset = ParentInstInlineFormset
      form = RoomInlineForm #(or something)

在您的表单中,您现在可以在init方法中访问self.parent_instance!parent_instance现在可以用于过滤选择和其他内容

就像是:

class RoomInlineForm(ParentInlineModelForm):
    def __init__(self, *args, **kwargs):
        super(RoomInlineForm, self).__init__(*args, **kwargs)
        building = self.parent_instance
        #Filtering and stuff

这次真是万分感谢!它是适用于我的应用程序的第一个版本,也很清晰。
贾斯汀

8

@nogus中的问题回答在弹出窗口中仍然有错误的URL /?_to_field=id&_popup=1

允许用户在弹出窗口中选择错误的项目

为了最终使它起作用,我必须更改field.widget.rel.limit_choices_to字典

class RoomInline(admin.TabularInline):
    model = Room

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):

        field = super(RoomInline, self).formfield_for_foreignkey(
            db_field, request, **kwargs)

        if db_field.name == 'inside_room':
            building = request._obj_
            if building is not None:
                field.queryset = field.queryset.filter(
                    building__exact=building)
                # widget changed to filter by building
                field.widget.rel.limit_choices_to = {'building_id': building.id}
            else:
                field.queryset = field.queryset.none()

        return field

class BuildingAdmin(admin.ModelAdmin):

    inlines = (RoomInline,)

    def get_form(self, request, obj=None, **kwargs):
        # just save obj reference for future processing in Inline
        request._obj_ = obj
        return super(BuildingAdmin, self).get_form(request, obj, **kwargs)

这在django 2.2中对我有用,无需使用field.widget.rel.limit_choices_to = {'building_id': building.id}
Twitch

4

这个问题和答案非常相似,适用于常规管理表单

在一个内联内部-那就是它的崩溃之处...我只是无法获取主表单的数据来获取我在限制范围内所需的外键值(或到内联的记录之一中以获取该值) 。

这是我的admin.py。我想我正在寻找替代魔力的魔法?使用-如果我插入一个硬编码的值(例如1),它可以正常工作并适当限制内联中的可用选择...

#spaces/admin.py
from demo.spaces.models import Building, Room
from django.contrib import admin
from django.forms import ModelForm


class RoomInlineForm(ModelForm):
  def __init__(self, *args, **kwargs):
    super(RoomInlineForm, self).__init__(*args, **kwargs)
    self.fields['inside_room'].queryset = Room.objects.filter(
                               building__exact=????)                       # <------

class RoomInline(admin.TabularInline):
  form = RoomInlineForm
  model=Room

class BuildingAdmin(admin.ModelAdmin):
  inlines=[RoomInline]

admin.site.register(Building, BuildingAdmin)
admin.site.register(Room)

4

我找到了一个相当优雅的解决方案,该解决方案非常适合内联表单。

应用于我的模型,其中我过滤了inside_room字段以仅返回位于同一建筑物中的房间:

#spaces/admin.py
class RoomInlineForm(ModelForm):
  def __init__(self, *args, **kwargs):
    super(RoomInlineForm, self).__init__(*args, **kwargs)  #On init...
  if 'instance' in kwargs:
    building = kwargs['instance'].building
  else:
    building_id = tuple(i[0] for i in self.fields['building'].widget.choices)[1]
    building = Building.objects.get(id=building_id)
  self.fields['inside_room'].queryset = Room.objects.filter(building__exact=building)

基本上,如果将'instance'关键字传递给表单,则它是内联显示的现有记录,因此我可以从实例中获取建筑物。如果不是实例,则它是内联中的空白“额外”行之一,因此它会遍历内联的隐藏表单字段,这些字段将隐式关系存储回主页,并从中获取id值。然后,它基于该building_id获取建筑物对象。最后,现在有了建筑物,我们可以将下拉菜单的查询集设置为仅显示相关项目。

比我原来的解决方案更优雅,我的解决方案以内联方式崩溃并被烧毁(但确实有效,但是,如果您不介意中途保存表单以使下拉菜单填充,则适用于单个表单):

class RoomForm(forms.ModelForm): # For the individual rooms
  class Meta:
mode = Room
  def __init__(self, *args, **kwargs):  # Limits inside_room choices to same building only
    super(RoomForm, self).__init__(*args, **kwargs)  #On init...
try:
  self.fields['inside_room'].queryset = Room.objects.filter( 
    building__exact=self.instance.building)   # rooms with the same building as this room
    except:                  #and hide this field (why can't I exclude?)
    self.fields['inside_room']=forms.CharField( #Add room throws DoesNotExist error
        widget=forms.HiddenInput,   
        required=False,
        label='Inside Room (save room first)')

对于非内联,如果房间已经存在,则可以使用。如果不是,它将抛出一个错误(DoesNotExist),因此我将捕获该错误然后隐藏该字段(因为管理员无法将其限制在正确的建筑物内,因为整个房间的记录都是新的,并且尚未设置任何建筑物!)...点击保存后,它将保存建筑物并在重新加载时可能会限制选择...

我只需要找到一种方法就可以在新记录中将外键过滤器从一个字段级联到另一个字段,即新记录,选择一栋建筑物,并在记录获得之前自动限制inside_room选择框中的选择。已保存。但这是另一天...


2

如果Daniel在编辑完您的问题后没有回答-我想我不会有太大帮助... :-)

我建议您尝试使django管理员适应一些逻辑,这些逻辑最好以您自己的视图,表单和模板组来实现。

我认为无法对InlineModelAdmin应用这种过滤。


2

在Django 1.6中:

 form = SpettacoloForm( instance = spettacolo )
 form.fields['teatro'].queryset = Teatro.objects.filter( utente = request.user ).order_by( "nome" ).all()

1
您能否将解决方案适应问题中现有的模型?
raratiru 2014年

1

我不得不承认,我并没有完全按照您的意图进行操作,但是我认为它非常复杂,您可能会考虑不将其站点置于管理员之外。

我曾经建立过一个网站,该网站最初是从简单的管理界面开始的,但最终变得如此个性化,以至于在管理员的约束下使用它变得非常困难。如果我从头开始,我会过得更好—一开始需要做更多的工作,但最后要有更多的灵活性和更少的痛苦。我的经验法则是,如果您要尝试执行的操作未记录在案(即涉及覆盖管理方法,查看管理源代码等),那么最好不要使用管理。只有我两美分。:)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.