[Django]-In Django – Model Inheritance – Does it allow you to override a parent model's attribute?

83👍

Updated answer: as people noted in comments, the original answer wasn’t properly answering the question. Indeed, only the LongNamedRestaurant model was created in database, Place was not.

A solution is to create an abstract model representing a “Place”, eg. AbstractPlace, and inherit from it:

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)

Please also read @Mark answer, he gives a great explanation why you can’t change attributes inherited from a non-abstract class.

(Note this is only possible since Django 1.10: before Django 1.10, modifying an attribute inherited from an abstract class wasn’t possible.)

Original answer

Since Django 1.10 it’s
possible
!
You just have to do what you asked for:

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)

64👍

No, it is not:

Field name “hiding” is not permitted

In normal Python class inheritance, it is permissible for a child
class to override any attribute from the parent class. In Django, this
is not permitted for attributes that are Field instances (at least,
not at the moment). If a base class has a field called author, you
cannot create another model field called author in any class that
inherits from that base class.

👤ptone

30👍

That is not possible unless abstract, and here is why: LongNamedRestaurant is also a Place, not only as a class but also in the database. The place-table contains an entry for every pure Place and for every LongNamedRestaurant. LongNamedRestaurant just creates an extra table with the food_type and a reference to the place table.

If you do Place.objects.all(), you also get every place that is a LongNamedRestaurant, and it will be an instance of Place (without the food_type). So Place.name and LongNamedRestaurant.name share the same database column, and must therefore be of the same type.

I think this makes sense for normal models: every restaurant is a place, and should have at least everything that place has. Maybe this consistency is also why it was not possible for abstract models before 1.10, although it would not give database problems there. As @lampslave remarks, it was made possible in 1.10. I would personally recommend care: if Sub.x overrides Super.x, make sure Sub.x is a subclass of Super.x, otherwise Sub cannot be used in place of Super.

Workarounds: You can create a custom user model (AUTH_USER_MODEL) which involves quite a bit of code duplication if you only need to change the email field. Alternatively you can leave email as it is and make sure it’s required in all forms. This doesn’t guarantee database integrity if other applications use it, and doesn’t work the other way around (if you want to make username not required).

👤Mark

19👍

See 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

14👍

My solution is as simple as next monkey patching, notice how I changed max_length attribute of name field in LongNamedRestaurant model:

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

9👍

Pasted your code into a fresh app, added app to INSTALLED_APPS and ran syncdb:

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

Looks like Django does not support that.

7👍

This supercool piece of code allows you to ‘override’ fields in abstract parent classes.

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

When the fields have been removed from the abstract parent class you are free to redefine them as you need.

This is not my own work. Original code from here: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba

👤Devin

6👍

Maybe you could deal with contribute_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 works fine. I dont tried this example, in my case I just override a constraint parameter so … wait & see !

4👍

I know it’s an old question, but i had a similar problem and found a workaround:

I had the following classes:

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

    class Meta:
        abstract = True

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

But I wanted Year’s inherited image-field to be required while keeping the image field of the superclass nullable. In the end I used ModelForms to enforce the image at the validation stage:

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

It seems this is only applicable for some situations (certainly where you need to enforce stricter rules on the subclass field).

Alternatively you can use the clean_<fieldname>() method instead of clean(), e.g. if a field town would be required to be filled in:

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

1👍

You can not override Model fields, but its easily achieved by overriding/specifying clean() method. I had the issue with email field and wanted to make it unique on Model level and did it like this:

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

The error message is then captured by Form field with name “email”

Leave a comment