【问题标题】:Limit foreign key choices in select in an inline form in admin在管理员的内联表单中限制外键选择
【发布时间】:2010-12-21 21:49:30
【问题描述】:

模型的逻辑是:

  • 一个Building 有很多Rooms
  • 一个Room 可能在另一个Room 内(例如,一个壁橱——'self' 上的外键)
  • Room 只能在同一建筑物中的另一个 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 表中的所有房间(包括其他建筑物中的房间)。

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

我无法想出在模型中使用limit_choices_to 的方法,也无法弄清楚如何正确覆盖管理员的内联表单集(我觉得我应该以某种方式创建一个自定义内联表单,将主表单的 building_id 传递给自定义内联,然后基于此限制字段选择的查询集——但我就是不知道该怎么做。

也许这对于管理站点来说太复杂了,但它似乎通常很有用......

【问题讨论】:

    标签: django foreign-keys inline limit admin


    【解决方案1】:

    使用请求实例作为 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)
    

    【讨论】:

    • 这里为我省去了很多麻烦。我需要过滤选择,但是通过会话变量。这个答案让我用 5 行代码来完成。谢谢。
    • 感谢一百万!另一种方法是在按照文档调用 super 之前分配 kwargs['queryset']:docs.djangoproject.com/en/dev/ref/contrib/admin/…
    • 这段代码也为我节省了大量时间。非常感谢您发布此内容
    • 这个!我正在为我的问题寻找类似的东西。我花了好几天才找到这个。
    • 但是用户仍然可以在弹出窗口中选择错误的Room。解决方案见stackoverflow.com/a/50298577/2207154
    【解决方案2】:

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

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

    为了最终让它发挥作用,我不得不更改 field.widget.rel.limit_choices_to dict

    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}
    【解决方案3】:

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

    【讨论】:

    • 这无济于事,因为在 limit_choices_to 中运行的查询没有引用“父类”。即,如果模型 A 对 B 和 C 都有一个外键,并且 C 对 B 有一个外键,并且我们希望确保 A 只引用与 A 引用相同 B 的 C ,查询需要知道 A->B,但它不知道。
    • 它可以与最佳答案组合有用,请参阅stackoverflow.com/a/50298577/2207154
    【解决方案4】:

    在 django 1.6 中:

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

    【讨论】:

    • 能否请您根据问题中存在的模型调整解决方案?
    【解决方案5】:

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

    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
    

    【讨论】:

    • 谢谢你!它是第一个适用于我的应用程序的版本,而且它也很好看。
    【解决方案6】:

    在阅读了这篇文章并进行了很多尝试之后,我想我已经找到了这个问题的一个相当明确的答案。由于这是一种常用的设计模式,我写了一个Mixin for the Django admin 来使用它。

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

    示例用法:

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

    这里,&lt;field_name&gt; 是要过滤的 FK 字段的名称,&lt;filters&gt; 是参数列表,您通常会在查询集的 filter() 方法中指定它们。

    【讨论】:

    • 谢谢,效果很好!干净多了。 (顺便说一句,您在代码中留下了一些不会去任何地方的日志记录语句)
    【解决方案7】:

    我找到了一个fairly elegant solution,它适用于内联表单。

    应用于我的模型,我正在过滤 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 选择框中的选择--之前记录被保存。但那是另一天...

    【讨论】:

      【解决方案8】:

      This question and answer is very similar, and works for a regular admin form

      在内联的内部——这就是它崩溃的地方......我只是无法获取主表单的数据来获取我需要的外键值(或获取内联的记录之一)值)。

      这是我的 admin.py。我想我正在寻找替代的魔法???? with--如果我插入一个硬编码的值(例如,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)
      

      【讨论】:

        【解决方案9】:

        我必须承认,我并没有完全按照您的意图进行操作,但我认为它已经足够复杂,您可能需要考虑不要将您的网站建立在管理员之外。

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

        【讨论】:

        • 有时我会创建广泛使用表单的网站。在这种情况下,请确保..!必须使用管理部分来操作表。除此之外,我同意你所说的。
        【解决方案10】:

        如果丹尼尔在编辑您的问题后没有回答 - 我认为我不会有太大帮助... :-)

        我将建议您尝试将一些逻辑强制融入 django 管理员,这些逻辑最好实现为您自己的一组视图、表单和模板。

        我认为不可能将这种过滤应用于 InlineModelAdmin。

        【讨论】:

          猜你喜欢
          • 2018-10-09
          • 2017-11-22
          • 1970-01-01
          • 2016-04-07
          • 2021-07-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多