DRY Meta.constraints

I’m somewhat new to Django and would like to take advantage of the new model validation of Meta.constraints in Django 4.1. I have an Enabled/Disabled status field that will be in most of my tables and would like to implement constraints on that field in a DRY manner. I have the following model constraints that work nicely with my status field.

class Color(models.Model):
    status = models.CharField("status", choices=Status.choices, default=Status.ENABLED)
    name = models.CharField("name")

    class Meta:
        constraints = [
            models.CheckConstraint(
                check=Q(status__exact=Status.ENABLED)
                | Q(status__exact=Status.DISABLED),
                name="status__exact",
            ),
        ]

Instead of copying and pasting the code in Meta for almost every table in my app, how can I create something that I just call to take the place of all this code? I’m picturing something like constraints += status_contstraints(status). The status_constraints(status) bit would live in my core app, where I keep things most apps would use. I could probably override models.Model, but I don’t think I will need the status field in every table, so that feels a bit wasteful. Additionally, I generally like Adam Johnson’s ideas about using partials instead of overriding base classes when possible. I’m uncertain what would be the best approach considering all of this and would be interested in some help. Thanks for reading all of this and any advice folks can provide!

Indeed, you can make a function that returns the desired check constraint. Use the % formatting in the name to name each constraint differently per table. Use __in rather than OR’ing two __exact lookups. A partial would be a bit unclear here imo.

def status_constraint():
    return models.CheckConstraint(
        name="%(app_label)s_%(class)s_valid_status",
        check=Q(status__in=(Status.ENABLED, Status.DISABLED)),
    )

used like

class Color(...):
    ...
    class Meta:
        constraints = [
            status_constraint(),
            ...
        ]

You hinted at needing an argument to change which statuses are allowed(?), I hope you can see how to extend the function to take that argument and use it in the constraint.

2 Likes

Thank you for all the help! I’ve plugged in the code you suggested, and I’m not getting any errors. I’ve run out of time tonight, but I’ll play with it some more tomorrow to make certain it’s working properly, and I understand how to use/modify it. It’s so neat having Adam Johnson answer my question. So cool, thank you!

Actually, the status argument was kind of pseudocode meant to pass the status field to the function somehow. I didn’t realize that the status field would be in scope when calling a function while in Meta. At least, I assume that’s what’s going on. Nor did I know there was a class variable that apparently returns the name of the current class. Maybe I’m misunderstanding how this works. I will test out all of these assumptions tomorrow night when I have a bit more time!

To set the valid values available for the status field, I used models.TextChoices (see below) that I learned from your terrific article Moving to Django 3.0’s Field.choices Enumeration Types.

class Status(models.TextChoices):
    ENABLED = "enabled"
    DISABLED = "disabled"

That said, I can see how one could pass in valid values for status via an argument as you mentioned.

Glad to help!

It’s not really “in scope”. Instead, we’re constructing a CheckConstraint object that could refer to any field names whatsoever. It’s after Django sees the constraint object in Meta.constraints that it determines which fields are allowed.

1 Like