Package: django-extended-choices
django-extended-choicesA little application to improve Django choicesdjango-extended-choicesaims to provide a better and more readable
way of usingchoicesinDjango.InstallationYou can install directly via pip (since version0.3):$ pip install django-extended-choicesOr from theGithubrepository (masterbranch by default):$ git clone git://github.com/twidi/django-extended-choices.git
$ cd django-extended-choices
$ sudo python setup.py installUsageThe aim is to replace this:STATE_ONLINE=1STATE_DRAFT=2STATE_OFFLINE=3STATE_CHOICES=((STATE_ONLINE,'Online'),(STATE_DRAFT,'Draft'),(STATE_OFFLINE,'Offline'),)STATE_DICT=dict(STATE_CHOICES)classContent(models.Model):title=models.CharField(max_length=255)content=models.TextField()state=models.PositiveSmallIntegerField(choices=STATE_CHOICES,default=STATE_DRAFT)def__unicode__(self):returnu'Content "%s" (state=%s)'%(self.title,STATE_DICT[self.state])print(Content.objects.filter(state=STATE_ONLINE))by this:fromextended_choicesimportChoicesSTATES=Choices(('ONLINE',1,'Online'),('DRAFT',2,'Draft'),('OFFLINE',3,'Offline'),)classContent(models.Model):title=models.CharField(max_length=255)content=models.TextField()state=models.PositiveSmallIntegerField(choices=STATES,default=STATES.DRAFT)def__unicode__(self):returnu'Content "%s" (state=%s)'%(self.title,STATES.for_value(self.state).display)print(Content.objects.filter(state=STATES.ONLINE))As you can see there is only one declaration for all states with, for each state, in order:the pseudo-constant name which can be used (STATES.ONLINEreplaces the previousSTATE_ONLINE)the value to use in the database - which could equally be a stringthe name to be displayed - and you can wrap the text inugettext_lazy()if you need i18nAnd then, you can use:STATES, orSTATES.choices, to use withchoices=in fields declarationsSTATES.for_constant(constant), to get the choice entry from the constant nameSTATES.for_value(constant), to get the choice entry from the key used in databaseSTATES.for_display(constant), to get the choice entry from the displayable value (can be useful in some case)Each choice entry obtained byfor_constant,for_valueandfor_displayreturn a tuple as
given to theChoicesconstructor, but with additional attributes:>>>entry=STATES.for_constant('ONLINE')>>>entry==('ONLINE',1,'Online')True>>>entry.constant'ONLINE'>>>entry.value1>>>entry.display'Online'These attributes are chainable (with a weird example to see chainability):>>>entry.constant.value1>>>entry.constant.value.value.display.constant.display'Online'To allow this, we had to remove support forNonevalues. Use empty strings instead.Note that constants can be accessed via a dict key (STATES['ONLINE']for example) if
you want to fight your IDE that may warn you about undefined attributes.You can check whether a value is in aChoicesobject directly:>>>1inSTATESTrue>>>42inSTATESFalseYou can even iterate on aChoicesobjects to get choices as seen by Django:>>>forchoiceinSTATES:...print(choice)(1,'Online')(2,'Draft')(3,'Offline')To get all choice entries as given to theChoicesobject, you can use theentriesattribute:>>>forchoice_entryinSTATES.entries:...print(choice_entry)('ONLINE',1,'Online'),('DRAFT',2,'Draft'),('OFFLINE',3,'Offline'),Or the following dicts, using constants, values or display names, as keys, and the matching
choice entry as values:STATES.constantsSTATES.valuesSTATES.displays>>>STATES.constants['ONLINE']isSTATES.for_constant('ONLINE')True>>>STATES.values[2]isSTATES.for_value(2)True>>>STATES.displays['Offline']isSTATES.for_display('Offline')TrueIf you want these dicts to be ordered, you can pass the dict class to use to theChoicesconstructor:fromcollectionsimportOrderedDictSTATES=Choices(('ONLINE',1,'Online'),('DRAFT',2,'Draft'),('OFFLINE',3,'Offline'),dict_class=OrderedDict)Since version1.1, the newOrderedChoicesclass is provided, that is exactly that:
aChoicesusingOrderedDictby default fordict_class. You can directly import
it fromextended_choices.You can check if a constant, value, or display name exists:>>>STATES.has_constant('ONLINE')True>>>STATES.has_value(1)True>>>STATES.has_display('Online')TrueYou can create subsets of choices within the sameChoicesinstance:>>>STATES.add_subset('NOT_ONLINE',('DRAFT','OFFLINE',))>>>STATES.NOT_ONLINE(2,'Draft')(3,'Offline')Now,STATES.NOT_ONLINEis a realChoicesinstance, with a subset of the mainSTATESconstants.You can use it to generate choices for when you only want a subset of choices available:offline_state=models.PositiveSmallIntegerField(choices=STATES.NOT_ONLINE,default=STATES.DRAFT)As the subset is a realChoicesinstance, you have the same attributes and methods:>>>STATES.NOT_ONLINE.for_constant('OFFLINE').value3>>>STATES.NOT_ONLINE.for_value(1).constantTraceback(mostrecentcalllast):...KeyError:3>>>list(STATES.NOT_ONLINE.constants.keys())['DRAFT','OFFLINE']>>>STATES.NOT_ONLINE.has_display('Online')FalseYou can create as many subsets as you want, reusing the same constants if needed:STATES.add_subset('NOT_OFFLINE',('ONLINE','DRAFT'))If you want to check membership in a subset you could do:defis_online(self):# it's an example, we could have just tested with STATES.ONLINEreturnself.statenotinSTATES.NOT_ONLINEIf you want to filter a queryset on values from a subset, you can usevalues, but asvaluesis a dict,keys()must be user:Content.objects.filter(state__in=STATES.NOT_ONLINE.values.keys())You can add choice entries in many steps usingadd_choices, possibly creating subsets at
the same time.To construct the sameChoicesas before, we could have done:STATES=Choices()STATES.add_choices(('ONLINE',1,'Online'))STATES.add_choices(('DRAFT',2,'Draft'),('OFFLINE',3,'Offline'),name='NOT_ONLINE')You can also pass theargumentto theChoicesconstructor to create a subset with all
the choices entries added at the same time (it will calladd_choiceswith the name and the
entries)The list of existing subset names is in thesubsetsattributes of the parentChoicesobject.If you want a subset of the choices but not save it in the originalChoicesobject, you can
useextract_subsetinstead ofadd_subset>>>subset=STATES.extract_subset('DRAFT','OFFLINE')>>>subset(2,'Draft')(3,'Offline')As for a subset created byadd_subset, you have a realChoicesobject, but not accessible
from the originalChoicesobject.Note that inextract_subset, you pass the strings directly, not in a list/tuple as for the
second argument ofadd_subset.Additional attributesEach tuple must contain three elements. But you can pass a dict as a fourth one and each entry of this dict will be saved as an attribute
of the choice entry>>>PLANETS=Choices(...('EARTH','earth','Earth',{'color':'blue'}),...('MARS','mars','Mars',{'color':'red'}),...)>>>PLANETS.EARTH.color'blue'Auto display/valueWe provide two classes to eases the writing of your choices, attended you don’t need translation on the display value.AutoChoicesIt’s the simpler and faster version: you just past constants and:the value saved in database will be constant lower casedthe display value will be the constant with_replaced by spaces, and the first letter capitalized>>>fromextended_choicesimportAutoChoices>>>PLANETS=AutoChoices('EARTH','MARS')>>>PLANETS.EARTH.value'earth'>>>PLANETS.MARS.display'Mars'If you want to pass additional attributes, pass a tuple with the dict as a last element:>>>PLANETS=AutoChoices(...('EARTH',{'color':'blue'}),...('MARS',{'color':'red'}),...)>>>PLANETS.EARTH.value'earth'>>>PLANETS.EARTH.color'blue'You can change the transform function used to convert the constant to the value to be saved and the display value, by passingvalue_transformanddisplay_transformfunctions to the constructor.>>>PLANETS=AutoChoices(...'EARTH','MARS',...value_transform=lambdaconst:'planet_'+const.lower()....display_transform=lambdaconst:'Planet: '+const.lower()....)>>>PLANETS.EARTH.value'planet_earth'>>>PLANETS.MARS.display'Planet: mars'If you find yourself repeting these transform functions you can have a base class that defines these function, as class attributes:>>>classMyAutoChoices(AutoChoices):...value_transform=staticmethod(lambdaconst:const.upper())...display_transform=staticmethod(lambdaconst:const.lower())>>>PLANETS=MyAutoChoices('EARTH','MARS')>>>PLANETS.EARTH.value'EARTH'>>>PLANETS.MARS.dispay'mars'Of course you can still override the functions by passing them to the constructor.If you want, for an entry, force a specific value, you can do it by simply passing it as a second argument:>>>PLANETS=AutoChoices(...'EARTH',...('MARS','red-planet'),...)>>>PLANETS.MARS.value'red-planet'And then if you want to set the display, pass a third one:>>>PLANETS=AutoChoices(...'EARTH',...('MARS','red-planet','Red planet'),...)>>>PLANETS.MARS.value'red-planet'>>>PLANETS.MARS.display'Red planet'To force a display value but let the db value to be automatically computed, useNonefor the second argument:>>>PLANETS=AutoChoices(...'EARTH',...('MARS',None,'Red planet'),...)>>>PLANETS.MARS.value'mars'>>>PLANETS.MARS.display'Red planet'AutoDisplayChoicesIn this version, you have to define the value to save in database. The display value will be composed like inAutoChoices>>>fromextended_choicesimportAutoDisplayChoices>>>PLANETS=AutoDisplayChoices(...('EARTH',1),...('MARS',2),...)>>>PLANETS.EARTH.value1>>>PLANETS.MARS.display'Mars'If you want to pass additional attributes, pass a tuple with the dict as a last element:>>>PLANETS=AutoDisplayChoices(...('EARTH','earth',{'color':'blue'}),...('MARS','mars',{'color':'red'}),...)>>>PLANETS.EARTH.value1>>>PLANETS.EARTH.display'Earth'>>>PLANETS.EARTH.color'blue'As inAutoChoices, you can change the transform function for the value to display by passingdisplay_transformto the
constructor.If you want, for an entry, force a specific display, you can do it by simply passing it as a third argument:>>>PLANETS=AutoChoices(...('EARTH',1),...('MARS',2,'Red planet'),...)>>>PLANETS.MARS.display'Red planet'NotesYou also have a very basic field (NamedExtendedChoiceFormField`) inextended_choices.fieldswhich accept constant names instead of valuesFeel free to read the source to learn more about this little Django app.You can declare your choices where you want. My usage is in themodels.pyfile, just before the class declaration.CompatibilityThe version1.0provided a totally new API, and compatibility with the previous one
(0.4.1) was removed in1.1. The last version with the compatibility was1.0.7.If you need this compatibility, you can use a specific version by pinning it in your requirements.LicenseAvailable under theBSDLicense. See theLICENSEfile includedPython/Django versions supportDjango versionPython versions1.8, 1.9, 1.102.7, 3.4, 3.51.112.7, 3.4, 3.5, 3.62.03.4, 3.5, 3.6, 3.72.1, 2.23.5, 3.6, 3.7TestsTo run tests from the code source, create a virtualenv or activate one, install Django, then:python -m extended_choices.testsWe also provides some quick doctests in the code documentation. To execute them:python -m extended_choicesNote: the doctests will work only in python version not displayuprefix for strings.Source codeThe source code is available onGithub.DevelopingIf you want to participate in the development of this library, you’ll needDjangoinstalled in your virtualenv. If you don’t have it, simply run:pip install -r requirements-dev.txtDon’t forget to run the tests ;)Feel free to propose a pull request onGithub!A few minutes after your pull request, tests will be executed onTravisCifor all the versions
of python and Django we support.DocumentationYou can find the documentation onReadTheDocTo update the documentation, you’ll need some tools:pip install -r requirements-makedoc.txtThen go to thedocsdirectory, and run:make htmlAuthorWritten by Stephane “Twidi” Angel <s.angel@twidi.com> (http://twidi.com), originally forhttp://www.liberation.fr