En Django - Herencia del modelo - ¿Le permite anular el atributo de un modelo principal?

99

Estoy buscando hacer esto:

class Place(models.Model):
   name = models.CharField(max_length=20)
   rating = models.DecimalField()

class LongNamedRestaurant(Place):  # Subclassing `Place`.
   name = models.CharField(max_length=255)  # Notice, I'm overriding `Place.name` to give it a longer length.
   food_type = models.CharField(max_length=25)

Esta es la versión que me gustaría usar (aunque estoy abierto a cualquier sugerencia): http://docs.djangoproject.com/en/dev/topics/db/models/#id7

¿Es esto compatible con Django? Si no es así, ¿hay alguna forma de lograr resultados similares?

Johnny 5
fuente
¿Puede aceptar la respuesta a continuación, desde django 1.10 es posible :)
holms
¡@holms solo si la clase base es abstracta!
Micah Walter

Respuestas:

64

Respuesta actualizada: como señalaron las personas en los comentarios, la respuesta original no respondía correctamente a la pregunta. De hecho, solo el LongNamedRestaurantmodelo se creó en la base de datos, Placeno lo fue.

Una solución es crear un modelo abstracto que represente un "lugar", por ejemplo. AbstractPlacey heredar de él:

class AbstractPlace(models.Model):
    name = models.CharField(max_length=20)
    rating = models.DecimalField()

    class Meta:
        abstract = True

class Place(AbstractPlace):
    pass

class LongNamedRestaurant(AbstractPlace):
    name = models.CharField(max_length=255)
    food_type = models.CharField(max_length=25)

Lea también la respuesta de @Mark , él da una gran explicación de por qué no puede cambiar los atributos heredados de una clase no abstracta.

(Tenga en cuenta que esto solo es posible desde Django 1.10: antes de Django 1.10, no era posible modificar un atributo heredado de una clase abstracta).

Respuesta original

¡Desde Django 1.10 es posible ! Solo tienes que hacer lo que pediste:

class Place(models.Model):
    name = models.CharField(max_length=20)
    rating = models.DecimalField()

    class Meta:
        abstract = True

class LongNamedRestaurant(Place):  # Subclassing `Place`.
    name = models.CharField(max_length=255)  # Notice, I'm overriding `Place.name` to give it a longer length.
    food_type = models.CharField(max_length=25)
qmarlats
fuente
8
El lugar debe ser abstracto, ¿no?
DylanYoung
4
No creo haber respondido una pregunta diferente, ya que solo digo que el código publicado en la pregunta ahora funciona desde Django 1.10. Tenga en cuenta que, de acuerdo con el enlace que publicó sobre lo que quería usar, se olvidó de hacer un resumen de la clase Place.
qmarlats
2
No estoy seguro de por qué esta es la respuesta aceptada ... OP está usando herencia de múltiples tablas. Esta respuesta solo es válida para clases base abstractas.
MrName
1
las clases abstractas estaban disponibles mucho antes de Django 1.10
rbennell
1
@NoamG En mi respuesta original, Placeera abstracto, por lo tanto, no se creó en la base de datos. Pero OP quería que ambos Placey LongNamedRestaurantse crearan en la base de datos. Por lo tanto, actualicé mi respuesta para agregar el AbstractPlacemodelo, que es el modelo "base" (es decir, abstracto) tanto Placecomo LongNamedRestaurantheredado. Ahora ambos Placey LongNamedRestaurantse crean en la base de datos, como lo solicitó OP.
qmarlats
61

No, no lo es :

No se permite "ocultar" el nombre de campo

En la herencia normal de clases de Python, está permitido que una clase secundaria anule cualquier atributo de la clase principal. En Django, esto no está permitido para atributos que son Fieldinstancias (al menos, no por el momento). Si una clase base tiene un campo llamado author, no puede crear otro campo modelo llamado authoren ninguna clase que herede de esa clase base.

ptone
fuente
11
Vea mi respuesta de por qué es imposible. A la gente le gusta esto porque tiene sentido, pero no es inmediatamente obvio.
Marcos
4
@ leo-the-man creo que User._meta.get_field('email').required = Truepodría funcionar, no estoy seguro.
Jens Timmerman
@ leo-the-manic, @JensTimmerman, @utapyngo Establecer el valor de propiedad de su clase no tendrá efecto en los campos heredados. _metaMyParentClass._meta.get_field('email').blank = Falseemail
Debe
1
Vaya, lo siento, el código de @ utapyngo anterior es correcto, ¡pero debe colocarse fuera del cuerpo de la clase, después! Establecer el campo de la clase principal como sugerí puede tener efectos secundarios no deseados.
Peterino
Quiero que un campo en cada una de las subclases sea de un tipo diferente a un campo con el mismo nombre en la clase principal abstracta para garantizar que todas las subclases tengan un campo con un nombre determinado. El código de utapyngo no satisface esta necesidad.
Daniel
28

Eso no es posible a menos que sea abstracto, y aquí está la razón: LongNamedRestauranttambién es a Place, no solo como una clase sino también en la base de datos. La mesa de lugar contiene una entrada para cada puro Placey para cada LongNamedRestaurant. LongNamedRestaurantsimplemente crea una tabla adicional con food_typey una referencia a la tabla de lugar.

Si lo hace Place.objects.all(), también obtendrá cada lugar que sea un LongNamedRestaurant, y será una instancia de Place(sin food_type). Entonces Place.namey LongNamedRestaurant.namecomparten la misma columna de base de datos y, por lo tanto, deben ser del mismo tipo.

Creo que esto tiene sentido para los modelos normales: cada restaurante es un lugar y debería tener al menos todo lo que tiene ese lugar. Quizás esta consistencia sea también la razón por la que no era posible para modelos abstractos antes de 1.10, aunque no daría problemas de base de datos allí. Como señala @lampslave, fue posible en 1.10. Personalmente, recomendaría cuidado: si Sub.x anula a Super.x, asegúrese de que Sub.x sea una subclase de Super.x; de lo contrario, Sub no se puede utilizar en lugar de Super.

Soluciones alternativas : puede crear un modelo de usuario personalizado ( AUTH_USER_MODEL) que implica bastante duplicación de código si solo necesita cambiar el campo de correo electrónico. Alternativamente, puede dejar el correo electrónico como está y asegurarse de que sea obligatorio en todos los formularios. Esto no garantiza la integridad de la base de datos si otras aplicaciones la usan y no funciona al revés (si desea que el nombre de usuario no sea obligatorio).

marca
fuente
Supongo que se debe a los cambios en 1.10: "Campos de modelo de invalidación permitidos heredados de clases base abstractas". docs.djangoproject.com/en/2.0/releases/1.10/#models
lamplave
Lo dudo ya que aún no estaba disponible en ese momento, pero es bueno agregar eso, ¡gracias!
Mark
19

Consulte https://stackoverflow.com/a/6379556/15690 :

class BaseMessage(models.Model):
    is_public = models.BooleanField(default=False)
    # some more fields...

    class Meta:
        abstract = True

class Message(BaseMessage):
    # some fields...
Message._meta.get_field('is_public').default = True
azulado
fuente
2
AttributeError: no se puede establecer el atributo ((((pero estoy intentando establecer opciones
Alexey
Esto no funciona en Django 1.11 (solía funcionar en versiones anteriores) ... la respuesta aceptada funciona
acaruci
9

Pegó su código en una aplicación nueva, agregó la aplicación a INSTALLED_APPS y ejecutó syncdb:

django.core.exceptions.FieldError: Local field 'name' in class 'LongNamedRestaurant' clashes with field of similar name from base class 'Place'

Parece que Django no lo admite.

Brian Luft
fuente
7

Esta pieza de código súper genial le permite 'anular' campos en clases padre abstractas.

def AbstractClassWithoutFieldsNamed(cls, *excl):
    """
    Removes unwanted fields from abstract base classes.

    Usage::
    >>> from oscar.apps.address.abstract_models import AbstractBillingAddress

    >>> from koe.meta import AbstractClassWithoutFieldsNamed as without
    >>> class BillingAddress(without(AbstractBillingAddress, 'phone_number')):
    ...     pass
    """
    if cls._meta.abstract:
        remove_fields = [f for f in cls._meta.local_fields if f.name in excl]
        for f in remove_fields:
            cls._meta.local_fields.remove(f)
        return cls
    else:
        raise Exception("Not an abstract model")

Cuando los campos se han eliminado de la clase principal abstracta, puede redefinirlos según lo necesite.

Este no es mi propio trabajo. Código original desde aquí: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba

Devin
fuente
6

Quizás podrías lidiar con contrib_to_class:

class LongNamedRestaurant(Place):

    food_type = models.CharField(max_length=25)

    def __init__(self, *args, **kwargs):
        super(LongNamedRestaurant, self).__init__(*args, **kwargs)
        name = models.CharField(max_length=255)
        name.contribute_to_class(self, 'name')

Syncdb funciona bien. No probé este ejemplo, en mi caso simplemente anulo un parámetro de restricción, así que ... ¡espera y verás!

JF Simon
fuente
1
también los argumentos para contrib_to_class parecen extraños (¿también incorrectos?) Parece que escribiste esto de memoria. ¿Podría proporcionar el código real que probó? Si lograste que esto funcionara, me encantaría saber exactamente cómo lo hiciste.
Michael Bylstra
Esto no me funciona. También estaría interesado en un ejemplo práctico.
garromark
por favor vea blog.jupo.org/2011/11/10/django-model-field-injection debería ser contrib_to_class (<ModelClass>, <fieldToReplace>)
goh
3
Place._meta.get_field('name').max_length = 255en el cuerpo de la clase debería hacer el truco, sin anular __init__(). También sería más conciso.
Peterino
4

Sé que es una pregunta antigua, pero tuve un problema similar y encontré una solución:

Tuve las siguientes clases:

class CommonInfo(models.Model):
    image = models.ImageField(blank=True, null=True, default="")

    class Meta:
        abstract = True

class Year(CommonInfo):
    year = models.IntegerField() 

Pero quería que se requiriera el campo de imagen heredado de Year mientras se mantenía el campo de imagen de la superclase anulable. Al final utilicé ModelForms para hacer cumplir la imagen en la etapa de validación:

class YearForm(ModelForm):
    class Meta:
        model = Year

    def clean(self):
        if not self.cleaned_data['image'] or len(self.cleaned_data['image'])==0:
            raise ValidationError("Please provide an image.")

        return self.cleaned_data

admin.py:

class YearAdmin(admin.ModelAdmin):
    form = YearForm

Parece que esto solo es aplicable para algunas situaciones (ciertamente, donde necesita hacer cumplir reglas más estrictas en el campo de subclase).

Alternativamente, puede usar el clean_<fieldname>()método en lugar de clean(), por ejemplo, si townse requiere completar un campo :

def clean_town(self):
    town = self.cleaned_data["town"]
    if not town or len(town) == 0:
        raise forms.ValidationError("Please enter a town")
    return town
pholz
fuente
1

No puede anular los campos del modelo, pero se logra fácilmente anulando / especificando el método clean (). Tuve el problema con el campo de correo electrónico y quería hacerlo único en el nivel de modelo y lo hice así:

def clean(self):
    """
    Make sure that email field is unique
    """
    if MyUser.objects.filter(email=self.email):
        raise ValidationError({'email': _('This email is already in use')})

El mensaje de error es capturado por el campo de formulario con el nombre "correo electrónico"

Phoenix49
fuente
La pregunta es sobre la extensión max_length de un campo char. Si la base de datos aplica esto, entonces esta "solución" no ayuda. Una solución alternativa sería especificar la longitud máxima más larga en el modelo base y usar el método clean () para aplicar la longitud más corta allí.
DylanYoung
0

Mi solución es tan simple como la siguiente monkey patching, observe cómo cambié el max_lengthatributo fo namecampo en el LongNamedRestaurantmodelo:

class Place(models.Model):
   name = models.CharField(max_length=20)

class LongNamedRestaurant(Place):
    food_type = models.CharField(max_length=25)
    Place._meta.get_field('name').max_length = 255
NoamG
fuente