Como faço para restringir chaves escolhas estrangeiras para objetos relacionados apenas na django

StackOverflow https://stackoverflow.com/questions/232435

  •  04-07-2019
  •  | 
  •  

Pergunta

Eu tenho uma relação externa dois sentidos semelhante à seguinte

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)

Como faço para restringir as escolhas para Parent.favoritechild a apenas as crianças cujos pais é em si? Tentei

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

mas isso faz com que a interface de administração para não listar todas as crianças.

Foi útil?

Solução

Eu deparei com ForeignKey.limit_choices_to na documentação do Django. Ainda não tenho certeza como isso funciona, mas que poderia ser apenas a coisa certa aqui.

Update: ForeignKey.limit_choices_to permite especificar uma constante, um pode ser chamado ou um objeto Q para restringir as escolhas permitidos para a chave. Uma constante, obviamente, não tem utilidade aqui, uma vez que não sabe nada sobre os objetos envolvidos.

Usando um exigível (função ou método de classe ou qualquer objeto que pode ser chamado) parece mais promissor. No entanto, o problema de como acessar as informações necessárias a partir de restos HttpRequest objeto. Usando thread local de armazenamento pode ser uma solução.

2. Update: Aqui está o que funcionou para mim:

Eu criei um middleware, conforme descrito no link acima. Ele extrai um ou mais argumentos de parte GET do pedido, como "produto = 1", e armazena essas informações nos locals de rosca.

A seguir é um método de classe no modelo que lê a variável local de segmento e retorna uma lista de ids para limitar a escolha de um campo de chave estrangeira.

@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

É importante para retornar uma consulta contendo todos os ids possíveis se nenhum foi selecionado de modo que as páginas de administração normais trabalhar ok.

O campo de chave estrangeira é então declarada como:

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

O problema é que você tem que fornecer as informações para restringir as escolhas através da solicitação. Eu não vejo uma maneira de acesso "eu" aqui.

Outras dicas

A maneira 'certa' de fazer isso é usar um formulário personalizado. A partir daí, você pode acessar self.instance, que é o objeto atual. Exemplo -

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

O novo jeito "certo" de fazer isso, pelo menos desde Django 1.1 é, substituindo o AdminModel.formfield_for_foreignkey (self, db_field, pedido, ** kwargs).

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

Para aqueles que não querem seguir o link abaixo é uma função de exemplo que está perto para os modelos de perguntas acima.

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)

Eu só estou não tenho certeza sobre como obter o objeto atual que está sendo editado. Espero que na verdade é sobre a auto algum lugar, mas eu não tenho certeza.

Isto não é como Django funciona. Você só iria criar a relação indo em uma direção.

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

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

E se você estava tentando acessar os filhos do pai que você faria parent_object.child_set.all(). Se você definir um related_name no campo myParent, então é isso que você se referem a ele como. Ex: related_name='children', então você faria parent_object.children.all()

Leia a docs http://docs.djangoproject.com/ en / dev / temas / db / modelos / # many-to-one-relações para mais.

Se você só precisa das limitações na interface de administração do Django, este trabalho poder. Baseei-lo em esta resposta de outro fórum - embora seja para relacionamentos ManyToMany, você deve ser capaz de substituir formfield_for_foreignkey para que ele funcione. Em 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)

Você quer restringir as opções disponíveis na interface de administração ao criar / editar uma instância de modelo?

Uma maneira de fazer isso é a validação do modelo. Isso permite que você gerar um erro na interface de administração se o campo estrangeiro não é a escolha certa.

É claro, a resposta de Eric está correto:. Você só precisa realmente uma chave estrangeira, do filho para o pai aqui

@Ber: Eu adicionei validação para o modelo semelhante a este

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)

que funciona exatamente como eu quero, mas seria muito bom se essa validação pode restringir opções no menu suspenso na interface de administração, em vez de validar após a escolha.

Eu estou tentando fazer algo semelhante. Parece que todo mundo dizendo 'você deve ter apenas uma chave estrangeira de uma maneira' tem talvez mal entendido que você está tentando fazer.

É uma pena o limit_choices_to = { "myParent": "auto"} que você queria fazer não trabalho ... que teria sido limpo e simples. Infelizmente, o 'self' não se avaliados e atravessa como uma cadeia simples.

Eu pensei que talvez eu poderia fazer:

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

Mas, infelizmente, que dá um erro porque a função não conseguir passar uma auto arg: (

Parece que a única maneira é colocar a lógica em todas as formas que usam este modelo (ou seja, passar um queryset para as escolhas para o seu formfield). Que é facilmente feito, mas seria mais seco para ter este no nível do modelo. Seu substituindo o método Save do modelo parece uma boa maneira de evitar escolhas inválidos passar.

Atualizar
Veja a minha resposta depois de outra maneira https://stackoverflow.com/a/3753916/202168

Uma abordagem alternativa seria não ter 'favouritechild' fk como um campo no modelo de pai.

Em vez disso você pode ter um campo booleano is_favourite sobre a criança.

Isto pode ajudar: https://github.com/anentropic/django-exclusivebooleanfield

Dessa forma, você iria contornar o problema todo de garantir Crianças só poderia ser feito o favorito do pai a que pertencem.

O código de vista seria um pouco diferente, mas a lógica de filtragem seria simples.

No Admin Você poderia até mesmo ter uma linha de modelos para crianças que expuseram a caixa is_favourite (se você só tem alguns filhos por mãe) caso contrário, o administrador teria que ser feito a partir do lado da criança.

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
    ]
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top