Package: django-konst
Django Konstdjango-konstdjango-konstis 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.DefinitionConstants can be defined with friendly names, backed by either integers or text.ConstantsfromkonstimportConstant# states backed by integersstates=Constants(Constant(pending=0),Constant(active=1),Constant(inactive=2))# and another set of constants backed by stringscolours=Constants(Constant(red="FF0000"),Constant(green="00FF00"),Constant(yellow="FFFF80"),Constant(white="FFFFFF"))Constant groupsAt times, it will be necessary to group constants and test membership within that group.
To achieve this,django-konstprovides aConstantGroupclass.fromkonstimportConstant,ConstantGroup# states backed by integersstates=Constants(Constant(active=0),Constant(cancelled_ontime=1),Constant(cancelled_late=2),ConstantGroup("cancelled",("cancelled_ontime","cancelled_late")))Within a modelWhile 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.fromdjango.dbimportmodelsfromdjango.utils.translationimportugettext_lazyas_fromkonstimportConstant,ConstantGroup,Constantsfromkonst.models.fieldsimport(ConstantChoiceCharField,ConstantChoiceField)classApple(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)UseThe entire point of this library is to make the use of constants defined in this way
easy and concise.In codeapple=Apple.objects.get(name='Granny Smith')apple.purpose.cookingTrueapple.colour.redTrueapple.colour.greenFalse# we don't care about the specific purpose, just whether it is as food# or not, so use the ConstantGroup!apple.purpose.culinaryTrueIn templates{% if apple.purpose.eating %}
    You should bite this {{ apple.name }}!
{% endif %}With Django's ORMred_apples=Apple.objects.filter(colour=Apple.colours.red)culinary_apples=Apple.objects.filter(purpose__in=Apple.purposes.culinary)With Django Rest FrameworkUsing thekonst.extras.drf.fields.ConstantChoiceFieldserializer field with the
Django Rest Framework it is possible to both output and receive constant values.fromkonst.extras.drf.fieldsimportConstantChoiceFieldfromrest_frameworkimportserializersclassAppleSerializer(serializers.ModelSerializer):purpose=ConstantChoiceField(Apple.purposes)colour=ConstantChoiceField(Apple.colours)classMeta:model=Applefields=("name","purpose","colour")# let's see how it handles bad valuesserializer=AppleSerializer(data={"name":"Fuji","colour":"blue","purpose":"dicing"})serializer.is_valid()Falseserializer.errors{'colour':[u'"blue" is not a valid choice.'],'purpose':[u'"dicing" is not a valid choice.']}# and now how it handles some good valuesserializer=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 neatlyinstance.colour.redTrue# finally, let's see how this looks when rendering JSONAppleSerializer(instance=instance).data{"name":"Fuji","colour":"red","purpose":"eating"}GotchasSetting a field on a model to the value of a constant rather than the Constant objectWhen the Django ORM instantiates a model instance from the database, it will ensure that anyConstantChoiceFieldorConstantChoiceCharFieldfields have values set toConstantinstances.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 theConstant: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 apost_savesignal (or any other code, really) that does something further with the instance:fromdjango.db.models.signalsimportpost_savefromdjango.dispatchimportreceiverfrom.modelsimportApple@receiver(post_save,sender=Apple)defapple_updated_receiver(sender,instance,created,*args,**kwargs):ifnotcreatedandinstance.purpose.eating:print(f"Oh my,{instance.name}is now for eating!")This code will sadly raiseAttributeError: 'int' object has no attribute 'purpose', asinstance.purposeis just the integer we set it to before saving.The good news is that you can avoid this byalwayssettingdjango-konstfields on instances toConstantinstances 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.eatingapple.save()Regardless of whether you're usingdjango-konstfor 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.Contributedjango-konstsupports a variety of Python and Django versions. It's best if you test each one of these before committing. OurCircle CI Integrationwill 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 SetupIn order to easily test on all these Pythons and run the exact same thing that CI will execute you'll want to setuppyenvand install the Python versions outlined intox.ini.If you are on Mac OS X, it's recommended you usebrew. After installingbrewrun:brewinstallpyenvpyenv-virtualenvpyenv-virtualenvwrapperNext, install the various python versions we want to test against and create a virtualenv specifically fordjango-konst:pyenvinstall3.6.10
pyenvinstall3.7.6
pyenvinstall3.8.1
pyenvvirtualenv3.8.1konst
pyenvactivatekonst
pipinstalldetox
pyenvshellkonst3.6.103.7.6Now ensure thekonstvirtualenv is activated, make the other python versions also on our path, and run the tests!pyenvshellkonst3.6.103.7.6
detoxThis will execute the test environments in parallel as defined in thetox.ini.