Skip to main content

Convenient constants fields for Django

Project description

Django Konst

CircleCI Codecov Code style: black

django-konst

django-konst is a utility for Django that makes the definition, use and storage of both integer and string based constants easy and readable. It avoids passing of constants into template contexts and makes evaluation concise and readable.

It also makes exposure of these constants via forms and DRF serializers simple.

Definition

Constants can be defined with friendly names, backed by either integers or text.

Constants

from konst import Constant

# states backed by integers
states = Constants(
    Constant(pending=0),
    Constant(active=1),
    Constant(inactive=2)
)

# and another set of constants backed by strings
colours = Constants(
    Constant(red="FF0000"),
    Constant(green="00FF00"),
    Constant(yellow="FFFF80"),
    Constant(white="FFFFFF")
)

Constant groups

At times, it will be necessary to group constants and test membership within that group. To achieve this, django-konst provides a ConstantGroup class.

from konst import Constant, ConstantGroup

# states backed by integers
states = Constants(
    Constant(active=0),
    Constant(cancelled_ontime=1),
    Constant(cancelled_late=2),
    ConstantGroup(
        "cancelled",
        ("cancelled_ontime", "cancelled_late")
    )
)

Within a model

While not strictly necessary, it is advisable to effectively namespace your constants by defining them in the scope of a model definition. This means you have your constants wherever you have the model class, as well as any model instance.

from django.db import models
from django.utils.translation import ugettext_lazy as _

from konst import Constant, ConstantGroup, Constants
from konst.models.fields import (
    ConstantChoiceCharField,
    ConstantChoiceField
)

class Apple(models.Model):

    purposes = Constants(
        Constant(cooking=0, label=_("Cook me!")),
        Constant(eating=1, label=_("Eat me!")),
        Constant(juicing=2, label=_("Juice me!")),
        Constant(ornamental=3, label=_("Just look how pretty I am!")),
        ConstantGroup(
            "culinary", ("cooking", "eating", "juicing")
        )
    )
    colours = Constants(
        Constant(red="FF0000", label=_("red")),
        Constant(green="00FF00", label=_("green")),
        Constant(yellow="FFFF80", label=_("yellow")),
        Constant(white="FFFFFF", label=_("white")),
    )

    name = models.CharField(max_length=30)
    purpose = ConstantChoiceField(constants=purposes)
    colour = ConstantChoiceCharField(constants=colours, max_length=30)

Use

The entire point of this library is to make the use of constants defined in this way easy and concise.

In code

apple = Apple.objects.get(name='Granny Smith')
apple.purpose.cooking
True
apple.colour.red
True
apple.colour.green
False

# we don't care about the specific purpose, just whether it is as food
# or not, so use the ConstantGroup!
apple.purpose.culinary
True

In templates

{% if apple.purpose.eating %}
    You should bite this {{ apple.name }}!
{% endif %}

With Django's ORM

red_apples = Apple.objects.filter(colour=Apple.colours.red)
culinary_apples = Apple.objects.filter(
    purpose__in=Apple.purposes.culinary
)

With Django Rest Framework

Using the konst.extras.drf.fields.ConstantChoiceField serializer field with the Django Rest Framework it is possible to both output and receive constant values.

from konst.extras.drf.fields import ConstantChoiceField

from rest_framework import serializers


class AppleSerializer(serializers.ModelSerializer):

    purpose = ConstantChoiceField(Apple.purposes)
    colour = ConstantChoiceField(Apple.colours)

    class Meta:
        model = Apple
        fields = (
            "name", "purpose", "colour"
        )


# let's see how it handles bad values
serializer = AppleSerializer(
    data={
        "name": "Fuji",
        "colour": "blue",
        "purpose": "dicing"
    }
)
serializer.is_valid()
False
serializer.errors
{
    'colour': [u'"blue" is not a valid choice.'],
    'purpose': [u'"dicing" is not a valid choice.']
}


# and now how it handles some good values
serializer = AppleSerializer(
    data={
        "name": "Fuji",
        "colour": "red",
        "purpose": "eating"
    }
)
serializer.is_valid()
True


# let's create a database entry!
instance = serializer.save()


# and now our instance can be interacted with neatly
instance.colour.red
True


# finally, let's see how this looks when rendering JSON
AppleSerializer(instance=instance).data
{
    "name": "Fuji",
    "colour": "red",
    "purpose": "eating"
}

Gotchas

Setting a field on a model to the value of a constant rather than the Constant object

When the Django ORM instantiates a model instance from the database, it will ensure that any ConstantChoiceField or ConstantChoiceCharField fields have values set to Constant instances.

If you create or modify a model instance using a raw, hard-coded constant value, you're likely to hit errors along the lines of "AttributeError: 'int' object has no attribute 'attribute-name'".

Take the example below where the caller sets the colour of a model instance to the underlying value for the Constant:

apple = Apple.objects.get(name='Granny Smith')
apple.purpose = 1  # constant for `eating` .. I know, who on earth would eat a granny smith straight up?!
apple.save()

Django will happily persist the data correctly, but let's say you have a post_save signal (or any other code, really) that does something further with the instance:

from django.db.models.signals import post_save
from django.dispatch import receiver

from .models import Apple

@receiver(post_save, sender=Apple)
def apple_updated_receiver(sender, instance, created, *args, **kwargs):
    if not created and instance.purpose.eating:
        print(f"Oh my, {instance.name} is now for eating!")

This code will sadly raise AttributeError: 'int' object has no attribute 'purpose', as instance.purpose is just the integer we set it to before saving.

The good news is that you can avoid this by always setting django-konst fields on instances to Constant instances such that downstream code can happily handle the instance as if it'd come straight out of the database:

apple = Apple.objects.get(name='Granny Smith')
apple.purpose = Apple.purposes.eating
apple.save()

Regardless of whether you're using django-konst for your constants, it's good practice to not hard-code constant values in order to avoid subtle mistakes and to ease changes in the future.

Contribute

django-konst supports a variety of Python and Django versions. It's best if you test each one of these before committing. Our Circle CI Integration will test these when you push but knowing before you commit prevents from having to do a lot of extra commits to get the build to pass.

Environment Setup

In order to easily test on all these Pythons and run the exact same thing that CI will execute you'll want to setup pyenv and install the Python versions outlined in tox.ini.

If you are on Mac OS X, it's recommended you use brew. After installing brew run:

brew install pyenv pyenv-virtualenv pyenv-virtualenvwrapper

Next, install the various python versions we want to test against and create a virtualenv specifically for django-konst:

pyenv install 3.6.10
pyenv install 3.7.6
pyenv install 3.8.1
pyenv virtualenv 3.8.1 konst
pyenv activate konst
pip install detox
pyenv shell konst 3.6.10 3.7.6

Now ensure the konst virtualenv is activated, make the other python versions also on our path, and run the tests!

pyenv shell konst 3.6.10 3.7.6
detox

This will execute the test environments in parallel as defined in the tox.ini.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django-konst-2.0.0.tar.gz (10.6 kB view details)

Uploaded Source

Built Distribution

django_konst-2.0.0-py3-none-any.whl (13.4 kB view details)

Uploaded Python 3

File details

Details for the file django-konst-2.0.0.tar.gz.

File metadata

  • Download URL: django-konst-2.0.0.tar.gz
  • Upload date:
  • Size: 10.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/45.2.0 requests-toolbelt/0.9.1 tqdm/4.43.0 CPython/3.7.6

File hashes

Hashes for django-konst-2.0.0.tar.gz
Algorithm Hash digest
SHA256 789301f4e1fd4492f5e305ad4c26cda807104def025d83ebb6be592d1807058f
MD5 2ccf0c6791cda003dba23c7f520f52ea
BLAKE2b-256 866983a4fad274fd20f38a046f1e3d803d8b04eed00fdd71f2cec8dc655f00ec

See more details on using hashes here.

File details

Details for the file django_konst-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: django_konst-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 13.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/45.2.0 requests-toolbelt/0.9.1 tqdm/4.43.0 CPython/3.7.6

File hashes

Hashes for django_konst-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ecf6111e194658af714abf682182ddc4c24f4c0506ad9b202033ed16d09d32f6
MD5 0badc9ce6e5ac880c0a7fd84ebdb9fa6
BLAKE2b-256 b5a0b71559906a9428294b38564c583314fe4829ceeb2ab0bafe3fca182801c7

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page