Обмежте вибір зовнішнього ключа в select у вбудованій формі в адміністраторі


75

Логіка моделі така:

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

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

Можливо, це занадто складно для сайту адміністратора, але це здається чимось, що було б загалом корисним ...

Відповіді:


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 рядків коду. Дякую.
Peter G

4
Завдяки мільйонів! Альтернативою є присвоєння kwargs ['queryset'] перед викликом супер згідно з документами: docs.djangoproject.com/en/dev/ref/contrib/admin/…
powlo

Цей код також заощадив ТОНИ часу. Велике спасибі за розміщення цього повідомлення
fangsterr

ЦЕ! Я шукав щось подібне для своєї проблеми. Мені знадобилися дні, щоб знайти це.
Мігель Айке

1
Але користувач все одно може неправильно вибрати Roomу спливаючому вікні. Дивіться stackoverflow.com/a/50298577/2207154 - рішення
Даниїл Машкін,

17

Прочитавши цю публікацію та багато експериментуючи, я думаю, що знайшов досить чітку відповідь на це питання. Оскільки це часто використовується шаблон дизайну, я написав 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()методі наборів запитів.


1
Дякую, чудово працює! Набагато чистіше. (І до речі, ви залишили в коді кілька виписок з журналу, які нікуди не діваються)
Дейв,

17

Існує опція limit_choices_to ForeignKey, яка дозволяє обмежити доступний вибір адміністратора для об’єкта


2
Це не допомагає, оскільки запит, який виконується в limit_choices_to, не має посилання на "батьківський клас". Тобто, якщо модель A має зовнішній ключ до B, а також до C, а C має зовнішній ключ до B, і ми хочемо забезпечити, щоб A посилався лише на C, який відноситься до того самого B, що і A , запит повинен знати про A-> B, а цього ні.
Кріс Когдон,

1
Це може бути корисно з комбінацією найкращих відповідей, див. Stackoverflow.com/a/50298577/2207154
Даниїл Машкін,

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 все ще є неправильною адресою у спливаючому вікні /?_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

Якщо Даніель, відредагувавши ваше запитання, не відповів - я не думаю, що мені багато допоможуть ... :-)

Я збираюся запропонувати, що ви намагаєтесь примусово вписати в адміністратор 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

1

Зізнаюся, я точно не дотримувався того, що ви намагаєтесь зробити, але я думаю, що це досить складно, і ви можете розглянути можливість не базувати свій сайт на адміністраторі.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.