Matthew Scott
Matthew Scott

Reputation: 111

Creating a models.UniqueConstraint in abstract model

I am creating an abstract model for my django app, SrdObject. One of the characteristics of my model is that it has a pair of fields that, taken together, must be unique: 'name' and the foreign key 'module'.

Here is an example of what I had

class SrdObject(models.Model):
    name = models.CharField(max_length=50)
    slug_name = models.SlugField(max_length=75, unique=True)
    active = models.BooleanField(default=True)
    module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name='%(class)s', blank=False, null=False, default='default')

    class Meta:
        unique_together = ['name', 'module']
        ordering = ['name']
        abstract = True

This seemed to be working ok, but the unique_together attribute has been marked as deprecated by django (See here), so I changed it to this

class Meta:
    constraints = [
        models.UniqueConstraint(fields=['name', 'module'], name='unique-in-module')
    ]
    ordering = ['name']
    abstract = True

This doesn't work because the name field must be unique, and since this is an abstract class, the constraint is repeated over several models.

I also tried

models.UniqueConstraint(fields=['name', 'module'], name='{}-unique-in-module'.format(model_name))

But obviously this falls into scoping problems, so I tried a decorator method

def add_unique_in_module_constraint(cls):
    cls._meta.constraints = [
        models.UniqueConstraint(fields=['name', 'module'], name='unique-in-module')
    ]
    return cls

@add_unique_in_module_constraint
class SrdObject(models.Model):
    class Meta:
        ordering = ['name']
        abstract = True

But this didn't seem to do anything.

So how do I create a models.UniqueConstraint in abstract model if every constraint needs a unique name attribute?

Upvotes: 11

Views: 4091

Answers (2)

stefanitsky
stefanitsky

Reputation: 483

LATEST UPDATE

Since 3rd version you finally can do that by specifying interpolation:

Changed in Django 3.0:
Interpolation of '%(app_label)s' and '%(class)s' was added.

Example:

UniqueConstraint(fields=['room', 'date'], name='%(app_label)s_unique_booking')

OLD (Django < 3.0)

You can't do that, same problem for me, so sad...

Source: django docs

Constraints in abstract base classes

You must always specify a unique name for the constraint. As such, you cannot normally specify a constraint on an abstract base class, since the Meta.constraints option is inherited by subclasses, with exactly the same values for the attributes (including name) each time. Instead, specify the constraints option on subclasses directly, providing a unique name for each constraint.

Upvotes: 12

Shane
Shane

Reputation: 653

I took this model setup:

class Module(models.Model):
    pass


class SrdObject(models.Model):
    name = models.CharField(max_length=50)
    slug_name = models.SlugField(max_length=75, unique=True)
    active = models.BooleanField(default=True)
    module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name='%(class)s', blank=False, null=False, default='default')

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['name', 'module'], name='unique-in-module')
        ]
        ordering = ['name']
        abstract = True


class SrdObjectA(SrdObject):
    pass


class SrdObjectB(SrdObject):
    pass

And then ran these tests:

class TestSrdObject(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.module = Module.objects.create()
        SrdObjectA.objects.create(name='A', module=cls.module)

    def test_unique_applies_to_same_model(self):
        with self.assertRaises(IntegrityError):
            SrdObjectA.objects.create(name='A', module=self.module)

    def test_unique_does_not_apply_to_different_model(self):
        self.assertTrue(SrdObjectB.objects.create(name='A', module=self.module))

And they pass. Perhaps I'm still missing the problem you're running into?

Upvotes: 0

Related Questions