Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django restrict data that can be given to model field

I have the following model in django:

class Cast(TimeStampedModel):
    user = models.ForeignKey(User, unique=True)
    count = models.PositiveIntegerField(default=1)
    kind = models.CharField(max_length = 7)

    def __str__(self):
        return(f"{self.kind} || {self.count} || {self.modified.strftime('%x')}")

But I want the 'kind' field to only take one of the following values: up, down, strange, charm, top, or bottom. How can I enforce this in the database or can this only be enforced in the views when taking in data?

like image 453
dot64dot Avatar asked Dec 31 '17 04:12

dot64dot


1 Answers

I think choices should do?

class Cast(TimeStampedModel):
    user = models.ForeignKey(User, unique=True)
    count = models.PositiveIntegerField(default=1)
    kind = models.CharField(
        max_length=7,
        choices=(
            ("up", "Up"),
            ("down", "Down"),
            ("strange", "Strange"),
            ("charm", "Charm"),
            ("top", "Top"),
            ("bottom", "Bottom")
        )
    )

Although in many occasions I've seen it used as a SmallInteger to save space in the database: In the DB you store a number, and in your Admin area you'll see a drop down with the "human friendly" choices.

kind = models.PositiveSmallIntegerField(
    choices=(
        (1, "Up"),
        (2, "Down"),
        (3, "Strange"),
        (4, "Charm"),
        (5, "Top"),
        (6, "Bottom")
    )
)

See:

Choices in action

This is not enforced at the DB level (see this ticket and this SO question) which means you still can do:

>>> c = Cast.objects.first()
>>> c.kind = 70
>>> c.save()

but it is enforced in the Admin. If you need it to be enforced in a lower level, I suggest you go with Noah Lc's answer.

As far as I understand, that is (still) not 100% enforced: You can still do bulk updates that don't go through the .save() method of the model; meaning: doing Cast.objects.all().update(kind=70) would still set an invalid value (70) in the kind field, but his solution is, indeed, one step "lower" than the Admin choices. You won't be able to do model updates that go through the .save() method of the instance. Meaning, you won't be allowed to do this:

>>> c=Cast.objects.first()
>>> c.kind=70
>>> c.save()

If you do need REAL database enforcement, you will need to actually check your databases's possibilities and add a constraint on the cast.kind column.

For instance, for Postgres (and probably for most of other SQL flavors) you could create a new migration that did this:

from django.db import migrations


def add_kind_constraint(apps, schema_editor):
    table = apps.get_model('stackoverflow', 'Cast')._meta.db_table
    schema_editor.execute("ALTER TABLE %s ADD CONSTRAINT check_cast_kind"
                          " CHECK (kind IN (1, 2, 3, 4, 5, 6) )" % table)


def remove_kind_constraint(apps, schema_editor):
    table = apps.get_model('stackoverflow', 'Cast')._meta.db_table
    schema_editor.execute("ALTER TABLE %s DROP CONSTRAINT check_cast_kind" % table)


class Migration(migrations.Migration):

    dependencies = [
        ('stackoverflow', '0003_auto_20171231_0526'),
    ]

    operations = [
        migrations.RunPython(add_kind_constraint, reverse_code=remove_kind_constraint)
    ]

And then yeah... You'd be 100% secured (the check doesn't depend on Django: now is in the hands of your database engine):

>>> c = Cast.objects.all().update(kind=70)
django.db.utils.IntegrityError: new row for relation "stackoverflow_cast" violates check constraint "check_cast_kind"
DETAIL:  Failing row contains (2, 1, 70, 1).
like image 107
BorrajaX Avatar answered Sep 22 '22 03:09

BorrajaX