Dans Django - Héritage de modèle - Vous permet-il de remplacer l'attribut d'un modèle parent?

99

Je cherche à faire ceci:

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)

C'est la version que j'aimerais utiliser (bien que je sois ouvert à toute suggestion): http://docs.djangoproject.com/en/dev/topics/db/models/#id7

Est-ce pris en charge dans Django? Sinon, existe-t-il un moyen d'obtenir des résultats similaires?

Johnny 5
la source
pouvez-vous s'il vous plaît accepter la réponse ci-dessous, de django 1.10 c'est possible :)
holms
@holms uniquement si la classe de base est abstraite!
Micah Walter

Réponses:

64

Réponse mise à jour: comme les gens l'ont noté dans les commentaires, la réponse originale ne répondait pas correctement à la question. En effet, seul le LongNamedRestaurantmodèle a été créé en base de données, Placene l'était pas.

Une solution consiste à créer un modèle abstrait représentant un «lieu», par exemple. AbstractPlace, et en hériter:

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)

Veuillez également lire la réponse @Mark , il explique pourquoi vous ne pouvez pas modifier les attributs hérités d'une classe non abstraite.

(Notez que cela n'est possible que depuis Django 1.10: avant Django 1.10, modifier un attribut hérité d'une classe abstraite n'était pas possible.)

Réponse originale

Depuis Django 1.10 c'est possible ! Il vous suffit de faire ce que vous avez demandé:

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
la source
8
Le lieu doit être abstrait, non?
DylanYoung
4
Je ne pense pas avoir répondu à une question différente puisque je dis simplement que le code affiché dans la question fonctionne maintenant depuis Django 1.10. Notez que d'après le lien qu'il a posté sur ce qu'il voulait utiliser, il a oublié de rendre abstraite la classe Place.
qmarlats
2
Je ne sais pas pourquoi c'est la réponse acceptée ... OP utilise l'héritage multi-table. Cette réponse n'est valable que pour les classes de base abstraites.
MrName
1
les classes abstraites étaient disponibles bien avant Django 1.10
rbennell
1
@NoamG Dans ma réponse originale, Placeétait abstrait, donc il n'a pas été créé dans la base de données. Mais OP voulait les deux Placeet LongNamedRestaurantêtre créé dans une base de données. Par conséquent, j'ai mis à jour ma réponse pour ajouter le AbstractPlacemodèle, qui est le modèle «de base» (c'est-à-dire abstrait) Placeet dont il LongNamedRestauranthérite. Maintenant, les deux Placeet LongNamedRestaurantsont créés dans la base de données, comme OP l'a demandé.
qmarlats
61

Non, ce n'est pas :

Le nom de champ "masquer" n'est pas autorisé

Dans l'héritage de classe Python normal, il est permis à une classe enfant de remplacer tout attribut de la classe parent. Dans Django, cela n'est pas autorisé pour les attributs qui sont des Fieldinstances (du moins pas pour le moment). Si une classe de base a un champ appelé author, vous ne pouvez pas créer un autre champ de modèle appelé authordans une classe qui hérite de cette classe de base.

tonalité
la source
11
Voir ma réponse pour savoir pourquoi c'est impossible. Les gens aiment ça parce que cela a du sens, ce n'est tout simplement pas évident.
Mark
4
@ leo-the-manic, je pense, User._meta.get_field('email').required = Truepourrait fonctionner, je ne suis pas sûr de penser.
Jens Timmerman
@ leo-the-manic, @JensTimmerman, @utapyngo La définition de la valeur de propriété de votre classe n'aura aucun effet sur les champs hérités. Vous devez agir sur le _metade la classe parent, par exemple MyParentClass._meta.get_field('email').blank = False(pour rendre un emailchamp hérité obligatoire dans l'Admin)
Peterino
1
Oups, désolé, le code de @ utapyngo ci-dessus est correct, mais il doit être placé en dehors du corps de la classe, après! Définir le champ de la classe parent comme je l'ai suggéré peut avoir des effets secondaires indésirables.
Peterino
Je veux qu'un champ dans chacune des sous-classes soit d'un type différent d'un champ avec le même nom dans la classe parent abstraite afin de garantir que toutes les sous-classes ont toutes un champ avec un certain nom. Le code d'utapyngo ne répond pas à ce besoin.
Daniel
28

Cela n'est possible que si abstrait, et voici pourquoi: LongNamedRestaurantest également a Place, non seulement en tant que classe mais aussi dans la base de données. La table de place contient une entrée pour chaque pur Placeet pour chaque LongNamedRestaurant. LongNamedRestaurantcrée simplement une table supplémentaire avec food_typeet une référence à la table d'espace.

Si vous le faites Place.objects.all(), vous obtenez également chaque place qui est a LongNamedRestaurant, et ce sera une instance de Place(sans le food_type). Donc Place.nameet LongNamedRestaurant.namepartager la même colonne de base de données, et doit donc être du même type.

Je pense que cela a du sens pour les modèles normaux: chaque restaurant est un endroit et devrait au moins avoir tout ce que cet endroit a. Peut-être que cette cohérence est aussi la raison pour laquelle ce n'était pas possible pour les modèles abstraits avant la 1.10, même si cela ne poserait pas de problèmes de base de données. Comme le remarque @lampslave, cela a été rendu possible en 1.10. Je recommanderais personnellement de faire attention: si Sub.x remplace Super.x, assurez-vous que Sub.x est une sous-classe de Super.x, sinon Sub ne peut pas être utilisé à la place de Super.

Solutions de contournement : vous pouvez créer un modèle utilisateur personnalisé ( AUTH_USER_MODEL) qui implique un peu de duplication de code si vous devez uniquement modifier le champ de messagerie. Vous pouvez également laisser le courrier électronique tel quel et vous assurer qu'il est requis dans tous les formulaires. Cela ne garantit pas l'intégrité de la base de données si d'autres applications l'utilisent et ne fonctionne pas dans l'autre sens (si vous souhaitez rendre le nom d'utilisateur non requis).

marque
la source
Je suppose que c'est à cause des changements dans 1.10: "Champs de modèle de remplacement autorisés hérités des classes de base abstraites." docs.djangoproject.com/en/2.0/releases/1.10/#models
lamplave
J'en doute car il n'était pas encore sorti à l'époque, mais c'est une bonne chose à ajouter, merci!
Mark
19

Voir 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
bleui
la source
2
AttributeError: impossible de définir l'attribut ((((mais j'essaye de définir des choix
Alexey
Cela ne fonctionne pas sur Django 1.11 (il fonctionnait sur les versions précédentes) ... la réponse acceptée fonctionne
acaruci
9

Vous avez collé votre code dans une nouvelle application, ajouté une application à INSTALLED_APPS et exécuté syncdb:

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

On dirait que Django ne prend pas en charge cela.

Brian Luft
la source
7

Ce morceau de code super cool vous permet de «remplacer» les champs dans les classes parents abstraites.

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")

Lorsque les champs ont été supprimés de la classe parent abstraite, vous êtes libre de les redéfinir selon vos besoins.

Ce n'est pas mon propre travail. Code d'origine d'ici: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba

Devin
la source
6

Vous pourriez peut-être vous occuper de 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 fonctionne très bien. Je n'ai pas essayé cet exemple, dans mon cas, je remplace simplement un paramètre de contrainte alors ... attendez et voyez!

JF Simon
la source
1
aussi les arguments de contrib_to_class semblent étranges (également faux?) On dirait que vous avez tapé ceci de mémoire. Pourriez-vous s'il vous plaît fournir le code que vous avez testé? Si cela fonctionne, j'aimerais savoir exactement comment vous l'avez fait.
Michael Bylstra
Cela ne fonctionne pas pour moi. Serait également intéressé par un exemple de travail.
garromark
s'il vous plaît voir blog.jupo.org/2011/11/10/django-model-field-injection il devrait être contrib_to_class (<ModelClass>, <fieldToReplace>)
goh
3
Place._meta.get_field('name').max_length = 255dans le corps de la classe devrait faire l'affaire, sans surcharger __init__(). Serait aussi plus concis.
Peterino
4

Je sais que c'est une vieille question, mais j'ai eu un problème similaire et j'ai trouvé une solution de contournement:

J'ai eu les cours suivants:

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

    class Meta:
        abstract = True

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

Mais je voulais que le champ image hérité de Year soit requis tout en gardant le champ image de la superclasse nullable. En fin de compte, j'ai utilisé ModelForms pour appliquer l'image au stade de la validation:

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

Il semble que cela ne s'applique que dans certaines situations (certainement où vous devez appliquer des règles plus strictes sur le champ de sous-classe).

Vous pouvez également utiliser la clean_<fieldname>()méthode à la place de clean(), par exemple, si un champ towndoit être rempli:

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
la source
1

Vous ne pouvez pas remplacer les champs de modèle, mais c'est facilement réalisé en remplaçant / en spécifiant la méthode clean (). J'ai eu le problème avec le champ de courrier électronique et je voulais le rendre unique au niveau du modèle et je l'ai fait comme ceci:

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')})

Le message d'erreur est ensuite capturé par le champ Formulaire avec le nom "email"

Phoenix49
la source
La question est d'étendre max_length d'un champ char. Si cela est imposé par la base de données, alors cette «solution» n'aide pas. Une solution de contournement serait de spécifier la longueur max_length la plus longue dans le modèle de base et d'utiliser la méthode clean () pour y appliquer la longueur la plus courte.
DylanYoung
0

Ma solution est aussi simple que la suivante monkey patching, remarquez comment j'ai changé le champ max_lengthattribut fo namedans le LongNamedRestaurantmodèle:

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
la source