Как ограничить выбор внешних ключей для связанных объектов только в Django - PullRequest
48 голосов
/ 24 октября 2008

У меня двусторонняя внешняя связь, подобная следующей

class Parent(models.Model):
  name = models.CharField(max_length=255)
  favoritechild = models.ForeignKey("Child", blank=True, null=True)

class Child(models.Model):
  name = models.CharField(max_length=255)
  myparent = models.ForeignKey(Parent)

Как ограничить выбор для Parent.favoritechild только детьми, чей родитель сам по себе? Я пытался

class Parent(models.Model):
  name = models.CharField(max_length=255)
  favoritechild = models.ForeignKey("Child", blank=True, null=True, limit_choices_to = {"myparent": "self"})

но это приводит к тому, что интерфейс администратора не выводит список дочерних элементов.

Ответы [ 10 ]

31 голосов
/ 31 октября 2008

Я только что натолкнулся на ForeignKey.limit_choices_to в документации Django. Пока не уверен, как это работает, но, может быть, это правильно.

Обновление: ForeignKey.limit_choices_to позволяет указать константу, вызываемый объект или объект Q для ограничения допустимых вариантов выбора ключа. Очевидно, что константа здесь бесполезна, поскольку она ничего не знает о задействованных объектах.

Использование вызываемого (метод функции или класса или любой вызываемый объект) кажется более перспективным. Однако проблема доступа к необходимой информации из объекта HttpRequest остается. Использование локального хранилища потока может быть решением.

2. Обновление: Вот что у меня сработало:

Я создал промежуточное программное обеспечение, как описано в ссылке выше. Он извлекает один или несколько аргументов из части GET запроса, такой как «product = 1», и сохраняет эту информацию в локальных потоках.

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

@classmethod
def _product_list(cls):
    """
    return a list containing the one product_id contained in the request URL,
    or a query containing all valid product_ids if not id present in URL

    used to limit the choice of foreign key object to those related to the current product
    """
    id = threadlocals.get_current_product()
    if id is not None:
        return [id]
    else:
        return Product.objects.all().values('pk').query

Важно вернуть запрос, содержащий все возможные идентификаторы, если ни один не был выбран, чтобы нормальные страницы администратора работали нормально.

Поле внешнего ключа объявляется как:

product = models.ForeignKey(
    Product,
    limit_choices_to={
        id__in=BaseModel._product_list,
    },
)

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

27 голосов
/ 24 октября 2013

«Правильный» способ сделать это - использовать пользовательскую форму. Оттуда вы можете получить доступ к self.instance, который является текущим объектом. Пример -

from django import forms
from django.contrib import admin 
from models import *

class SupplierAdminForm(forms.ModelForm):
    class Meta:
        model = Supplier
        fields = "__all__" # for Django 1.8+


    def __init__(self, *args, **kwargs):
        super(SupplierAdminForm, self).__init__(*args, **kwargs)
        if self.instance:
            self.fields['cat'].queryset = Cat.objects.filter(supplier=self.instance)

class SupplierAdmin(admin.ModelAdmin):
    form = SupplierAdminForm
14 голосов
/ 11 января 2011

Новый «правильный» способ сделать это, по крайней мере, с Django 1.1, переопределив AdminModel.formfield_for_foreignkey (self, db_field, request, ** kwargs).

См. http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey

Для тех, кто не хочет переходить по ссылке ниже, приведен пример функции, которая близка для приведенных выше моделей вопросов.

class MyModelAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "favoritechild":
            kwargs["queryset"] = Child.objects.filter(myparent=request.object_id)
        return super(MyModelAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)

Я не уверен, как получить текущий редактируемый объект. Я ожидаю, что это где-то на самом деле, но я не уверен.

12 голосов
/ 24 октября 2008

Это не так, как работает Django. Вы бы только создали отношение, идущее в одну сторону.

class Parent(models.Model):
  name = models.CharField(max_length=255)

class Child(models.Model):
  name = models.CharField(max_length=255)
  myparent = models.ForeignKey(Parent)

И если бы вы пытались получить доступ к детям от родителей, вы бы сделали parent_object.child_set.all(). Если вы установили related_name в поле myparent, то это то, на что вы бы ссылались. Пример: related_name='children', тогда вы бы сделали parent_object.children.all()

Прочитайте документы http://docs.djangoproject.com/en/dev/topics/db/models/#many-to-one-relationships, чтобы узнать больше.

5 голосов
/ 05 апреля 2015

Если вам нужны только ограничения в интерфейсе администратора Django, это может сработать. Я основал его на этом ответе с другого форума - хотя это для отношений ManyToMany, вы должны заменить formfield_for_foreignkey, чтобы он работал. В admin.py:

class ParentAdmin(admin.ModelAdmin):
    def get_form(self, request, obj=None, **kwargs):
        self.instance = obj
        return super(ParentAdmin, self).get_form(request, obj=obj, **kwargs)

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        if db_field.name == 'favoritechild' and self.instance:       
            kwargs['queryset'] = Child.objects.filter(myparent=self.instance.pk)
        return super(ChildAdmin, self).formfield_for_foreignkey(db_field, request=request, **kwargs)
3 голосов
/ 24 октября 2008

@ Ber: Я добавил валидацию к модели, подобной этой

class Parent(models.Model):
  name = models.CharField(max_length=255)
  favoritechild = models.ForeignKey("Child", blank=True, null=True)
  def save(self, force_insert=False, force_update=False):
    if self.favoritechild is not None and self.favoritechild.myparent.id != self.id:
      raise Exception("You must select one of your own children as your favorite")
    super(Parent, self).save(force_insert, force_update)

, который работает именно так, как я хочу, но было бы очень хорошо, если бы эта проверка могла ограничивать выбор в раскрывающемся списке в интерфейсе администратора, а не проверять после выбора.

3 голосов
/ 24 октября 2008

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

Один из способов сделать это - проверка модели. Это позволяет вам вызвать ошибку в интерфейсе администратора, если чужое поле не является правильным выбором.

Конечно, ответ Эрика верен: вам действительно нужен только один внешний ключ, от ребенка к родителю.

2 голосов
/ 17 ноября 2009

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

Обидно, что limit_choices_to = {"myparent": "self"}, который вы хотели сделать, не работает ... это было бы чисто и просто. К сожалению, «я» не оценивается и проходит как простая строка.

Я думал, может быть, я мог бы сделать:

class MyModel(models.Model):
    def _get_self_pk(self):
        return self.pk
    favourite = models.ForeignKey(limit_choices_to={'myparent__pk':_get_self_pk})

Но, увы, выдает ошибку, потому что функция не получает аргумент self: (

Кажется, что единственный способ состоит в том, чтобы поместить логику во все формы, которые используют эту модель (т.е. передать набор запросов в варианты выбора для вашего поля формы). Что легко сделать, но было бы более СУХОЕ, чтобы иметь это на уровне модели. Ваш переопределение метода сохранения модели является хорошим способом предотвращения неправильного выбора.

Обновление
Смотрите мой позже ответ для другого пути https://stackoverflow.com/a/3753916/202168

1 голос
/ 20 сентября 2010

Альтернативным подходом было бы не использовать 'favouritechild' fk в качестве поля в родительской модели.

Вместо этого вы можете использовать логическое поле is_favourite для Child.

Это может помочь: https://github.com/anentropic/django-exclusivebooleanfield

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

Код представления будет немного другим, но логика фильтрации будет простой.

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

0 голосов
/ 14 февраля 2016
from django.contrib import admin
from sopin.menus.models import Restaurant, DishType

class ObjInline(admin.TabularInline):
    def __init__(self, parent_model, admin_site, obj=None):
        self.obj = obj
        super(ObjInline, self).__init__(parent_model, admin_site)

class ObjAdmin(admin.ModelAdmin):

    def get_inline_instances(self, request, obj=None):
        inline_instances = []
        for inline_class in self.inlines:
            inline = inline_class(self.model, self.admin_site, obj)
            if request:
                if not (inline.has_add_permission(request) or
                        inline.has_change_permission(request, obj) or
                        inline.has_delete_permission(request, obj)):
                    continue
                if not inline.has_add_permission(request):
                    inline.max_num = 0
            inline_instances.append(inline)

        return inline_instances



class DishTypeInline(ObjInline):
    model = DishType

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        field = super(DishTypeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
        if db_field.name == 'dishtype':
            if self.obj is not None:
                field.queryset = field.queryset.filter(restaurant__exact = self.obj)  
            else:
                field.queryset = field.queryset.none()

        return field

class RestaurantAdmin(ObjAdmin):
    inlines = [
        DishTypeInline
    ]
...