3

I want to set null and blank to true on all the fields inherited from an abstract model.

My current attempt follows similar SO questions, e.g. overriding the 'default' attribute on ABC and overriding parent model's attribute, which say it is possible. I get the required runtime behaviour, when initialising objects from the python console, but it is not reflected in the migrations file or database.

Context:

I have a System model where I want to be able to create client specific overrides on certain data. I have the following models:

  • abstract BaseSystem - defining the overridable fields
  • concrete SystemOverride - containing the partially overridden records
  • concrete System - containing the 'full' System records.

It is important to make all the fields in SystemOverride null/blank = True so that only the fields that are initialised (by the client) will override the related System object.

Code:

class BaseSystem(models.Model):

    class Meta:
        abstract = True

    def __init__(self, *args, **kwargs):
        super(BaseSystem, self).__init__(args, kwargs)

        # Mark all fields with 'override' attribute
        for field in self._meta.get_fields():
            field.override = True

    name = models.CharField(max_length=128)


class System(BaseSystem):
    pass


class SystemOverride(BaseSystem):

    def __init__(self, *args, **kwargs):
        super(SystemOverride, self).__init__(args, kwargs)

        # Set all overridable fields to null/blank = True. 
        for field in self._meta.get_fields():
            if(hasattr(field, 'override') and field.override):
                field.null = True
                field.blank = True

    # Override-specific fields
    system = models.ForeignKey(System)

The result of makemigrations:

class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='System',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=128)),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='SystemOverride',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=128)),
                ('system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='overide.System')),
            ],
            options={
                'abstract': False,
            },
        ),
    ]

null=True and blank=True have not been added to the name field in SystemOveride.

Greg Brown
  • 1,251
  • 1
  • 15
  • 32

3 Answers3

0

This can't be done in the init of the class. The makemigrations will never see it. You need to do it at the metaclass level.

  • 2
    Thanks for the answer but can you elaborate on/show some code for how to achieve that? – Greg Brown Jun 07 '17 at 08:24
  • Your init does not run when you do makemigrations. it runs when you create an instance of the class. Check modelbase if you want to modift the attribute of the class before the class creation. https://github.com/django/django/blob/master/django/db/models/base.py – William R. Marchand Jun 07 '17 at 19:13
  • 2
    Thanks for pointing that out. I'm still struggling to figure out how to implement this. Do I need to override the `__new__(cls, name, bases, attrs)` method on my SystemOverride class, to modify the attributes before they go to `ModelBase.__new__`? Any suggestions will be most appreciated. Thanks – Greg Brown Jun 12 '17 at 14:28
0

You can create an abstract CharField class like this:

class AbstractCharField(models.CharField):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.null = True
        self.blank = True

    class Meta:
        abstract = True


# usage
name = AbstractCharField(max_length=128)
Anatol
  • 3,720
  • 2
  • 20
  • 40
0

I was actually looking for a similar thing recently, but couldn't find a solution. I did find this post however. This is the solution I eventually came up with:

from django.db.models.base import ModelBase


class NullableInheritanceMeta(ModelBase):

    def __new__(mcs, name, bases, attrs, **kwargs):
        cls = super().__new__(mcs, name, bases, attrs, **kwargs)
        null_fields_from = cls.null_parent_fields_classes

        for parent in null_fields_from:
            for field in parent._meta.fields:
                cls._meta.get_field(field.name).null = True

        return cls

I used a metaclass, because I really wanted it to be DRY and re-usable. The way you can use this is:

class AbstractPermissions(models.Model):

    permission1 = models.BooleanField()
    permission2 = models.BooleanField()
    permission3 = models.BooleanField()

    class Meta:
        abstract = True


class Plan(AbstractPermissions):
    pass


class PlanOverride(AbstractPermissions, metaclass=NullableInheritanceMeta):
    null_parent_fields_classes = [PermissionMixin]

Thanks to the null_parent_fields_classes attribute, you should be able to implement from many base classes, but choose which one you should clear the fields from.

samu
  • 2,870
  • 16
  • 28