Ограничьте выбор внешнего ключа в select в встроенной форме в admin - PullRequest
66 голосов
/ 01 декабря 2009

Логика модели такова:

  • A Building имеет много Rooms
  • A Room может находиться внутри другого Room (шкаф, например, ForeignKey для «self»)
  • A 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 мне нужно ограничить выбор inside_room только rooms, которые находятся в текущем building (запись здания в настоящее время изменяется основной формой BuildingAdmin).

Я не могу найти способ сделать это с помощью limit_choices_to в модели, и при этом я не могу понять, как точно переопределить встроенный набор форм администратора должным образом (я чувствую, что я должен каким-то образом создать собственный встроенный набор формы, передайте building_id главной формы в пользовательский inline, а затем ограничьте набор запросов для выбора поля, основываясь на этом - но я просто не могу обдумать, как это сделать).

Может быть, это слишком сложно для администратора сайта, но кажется, что это было бы вообще полезно ...

Ответы [ 10 ]

96 голосов
/ 21 ноября 2010

Использовал экземпляр запроса как временный контейнер для объекта. Переопределенный встроенный метод 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)
15 голосов
/ 15 февраля 2011

Прочитав этот пост и много экспериментировав, я думаю, что нашел довольно определенный ответ на этот вопрос. Поскольку это часто используемый шаблон проектирования, я написал Mixin для администратора Django , чтобы использовать его.

(Динамически) ограничение набора запросов для полей 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() наборов запросов.

14 голосов
/ 12 июля 2012

Существует limit_choices_to ForeignKey опция, которая позволяет ограничить доступные административные опции для объекта

8 голосов
/ 12 апреля 2012

Вы можете создать несколько пользовательских классов, которые затем передадут ссылку на родительский экземпляр в форму.

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
4 голосов
/ 09 декабря 2009

Я нашел довольно элегантное решение , которое хорошо работает для встроенных форм.

Применяется к моей модели, где я фильтрую поле 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 - перед запись будет сохранена. Но это на другой день ...

4 голосов
/ 02 декабря 2009

Этот вопрос и ответ очень похожи, и работает для обычной формы администратора

Внутри встроенного - и вот где он разваливается ... Я просто не могу получить данные основной формы, чтобы получить значение внешнего ключа, которое мне нужно в моем лимите (или одну из записей встроенного ключа, чтобы получить значение).

Вот мой 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)
3 голосов
/ 11 мая 2018

Проблема в ответе @nogus: во всплывающем окне все еще неверный URL /?_to_field=id&_popup=1

, которые позволяют пользователю выбрать неправильный элемент во всплывающем окне

Чтобы наконец это заработало, мне пришлось изменить 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)
2 голосов
/ 18 июля 2014

В Джанго 1.6:

 form = SpettacoloForm( instance = spettacolo )
 form.fields['teatro'].queryset = Teatro.objects.filter( utente = request.user ).order_by( "nome" ).all()
2 голосов
/ 01 декабря 2009

Если Даниэль, после редактирования вашего вопроса, не ответил - не думаю, что мне сильно поможет ...: -)

Я собираюсь предположить, что вы пытаетесь втиснуть в администратор django некоторую логику, которая будет лучше реализована в виде вашей собственной группы представлений, форм и шаблонов.

Я не думаю, что такой тип фильтрации можно применить к InlineModelAdmin.

1 голос
/ 01 декабря 2009

Должен признать, я не совсем понял, что вы пытаетесь сделать, но я думаю, что это достаточно сложно, чтобы вы могли подумать о том, чтобы не основывать свой сайт вне администратора.

Однажды я создал сайт, который начинался с простого интерфейса администратора, но в конечном итоге стал настолько настроенным, что стало очень трудно работать в рамках ограничений администратора. Мне было бы лучше, если бы я только начал с нуля - больше работы в начале, но гораздо больше гибкости и меньше боли в конце. Мое эмпирическое правило: если то, что вы пытаетесь сделать, не задокументировано (т.е. включает переопределение методов администратора, вглядывание в исходный код администратора и т. Д.), То вам, вероятно, лучше не использовать администратора. Только у меня два цента. :)

...