Reputation: 6362
When you have a model field with a choices option you tend to have some magic values associated with human readable names. Is there in Django a convenient way to set these fields by the human readable name instead of the value?
Consider this model:
class Thing(models.Model):
PRIORITIES = (
(0, 'Low'),
(1, 'Normal'),
(2, 'High'),
)
priority = models.IntegerField(default=0, choices=PRIORITIES)
At some point we have a Thing instance and we want to set its priority. Obviously you could do,
thing.priority = 1
But that forces you to memorize the Value-Name mapping of PRIORITIES. This doesn't work:
thing.priority = 'Normal' # Throws ValueError on .save()
Currently I have this silly workaround:
thing.priority = dict((key,value) for (value,key) in Thing.PRIORITIES)['Normal']
but that's clunky. Given how common this scenario could be I was wondering if anyone had a better solution. Is there some field method for setting fields by choice name which I totally overlooked?
Upvotes: 146
Views: 128172
Reputation: 1461
Model's choices option accepts a sequence consisting itself of iterables of exactly two items (e.g. [(A, B), (A, B) ...]) to use as choices for this field.
In addition, Django provides enumeration types that you can subclass to define choices in a concise way:
from django.utils.translation import gettext_lazy as _
class ThingPriority(models.IntegerChoices):
LOW = 0, _('Low')
NORMAL = 1, _('Normal')
HIGH = 2, _('High')
class Thing(models.Model):
priority = models.IntegerField(default=ThingPriority.NORMAL, choices=ThingPriority.choices)
Django supports adding an extra string value to the end of this tuple to be used as the human-readable name, or label. The label can be a lazy translatable string.
# in your code
thing = get_thing() # instance of Thing
thing.priority = ThingPriority.LOW
Note: you can use that using ThingPriority.HIGH
, ThingPriority.['HIGH']
, or ThingPriority(0)
to access or lookup enum members.
You need to import from django.utils.translation import gettext_lazy as _
Upvotes: 9
Reputation: 811
As of Django 3.0, you can use:
class ThingPriority(models.IntegerChoices):
LOW = 0, 'Low'
NORMAL = 1, 'Normal'
HIGH = 2, 'High'
class Thing(models.Model):
priority = models.IntegerField(default=ThingPriority.LOW, choices=ThingPriority.choices)
# then in your code
thing = get_my_thing()
thing.priority = ThingPriority.HIGH
Upvotes: 47
Reputation: 5079
Originally I used a modified version of @Allan's answer:
from enum import IntEnum, EnumMeta
class IntegerChoiceField(models.IntegerField):
def __init__(self, choices, **kwargs):
if hasattr(choices, '__iter__') and isinstance(choices, EnumMeta):
choices = list(zip(range(1, len(choices) + 1), [member.name for member in list(choices)]))
kwargs['choices'] = choices
super(models.IntegerField, self).__init__(**kwargs)
def to_python(self, value):
return self.choices(value)
def get_db_prep_value(self, choice):
return self.choices[choice]
models.IntegerChoiceField = IntegerChoiceField
GEAR = IntEnum('GEAR', 'HEAD BODY FEET HANDS SHIELD NECK UNKNOWN')
class Gear(Item, models.Model):
# Safe to assume last element is largest value member of an enum?
#type = models.IntegerChoiceField(GEAR, default=list(GEAR)[-1].name)
largest_member = GEAR(max([member.value for member in list(GEAR)]))
type = models.IntegerChoiceField(GEAR, default=largest_member)
def __init__(self, *args, **kwargs):
super(Gear, self).__init__(*args, **kwargs)
for member in GEAR:
setattr(self, member.name, member.value)
print(Gear().HEAD, (Gear().HEAD == GEAR.HEAD.value))
Simplified with the django-enumfields
package package which I now use:
from enumfields import EnumIntegerField, IntEnum
GEAR = IntEnum('GEAR', 'HEAD BODY FEET HANDS SHIELD NECK UNKNOWN')
class Gear(Item, models.Model):
# Safe to assume last element is largest value member of an enum?
type = EnumIntegerField(GEAR, default=list(GEAR)[-1])
#largest_member = GEAR(max([member.value for member in list(GEAR)]))
#type = EnumIntegerField(GEAR, default=largest_member)
def __init__(self, *args, **kwargs):
super(Gear, self).__init__(*args, **kwargs)
for member in GEAR:
setattr(self, member.name, member.value)
Upvotes: 0
Reputation: 251
My answer is very late and might seem obvious to nowadays-Django experts, but to whoever lands here, i recently discovered a very elegant solution brought by django-model-utils: https://django-model-utils.readthedocs.io/en/latest/utilities.html#choices
This package allows you to define Choices with three-tuples where:
So here's what you can do:
from model_utils import Choices
class Thing(models.Model):
PRIORITIES = Choices(
(0, 'low', 'Low'),
(1, 'normal', 'Normal'),
(2, 'high', 'High'),
)
priority = models.IntegerField(default=PRIORITIES.normal, choices=PRIORITIES)
thing.priority = getattr(Thing.PRIORITIES.Normal)
This way:
Enjoy :)
Upvotes: 2
Reputation: 4807
I appreciate the constant defining way but I believe Enum type is far best for this task. They can represent integer and a string for an item in the same time, while keeping your code more readable.
Enums were introduced to Python in version 3.4. If you are using any lower (such as v2.x) you can still have it by installing the backported package: pip install enum34
.
# myapp/fields.py
from enum import Enum
class ChoiceEnum(Enum):
@classmethod
def choices(cls):
choices = list()
# Loop thru defined enums
for item in cls:
choices.append((item.value, item.name))
# return as tuple
return tuple(choices)
def __str__(self):
return self.name
def __int__(self):
return self.value
class Language(ChoiceEnum):
Python = 1
Ruby = 2
Java = 3
PHP = 4
Cpp = 5
# Uh oh
Language.Cpp._name_ = 'C++'
This is pretty much all. You can inherit the ChoiceEnum
to create your own definitions and use them in a model definition like:
from django.db import models
from myapp.fields import Language
class MyModel(models.Model):
language = models.IntegerField(choices=Language.choices(), default=int(Language.Python))
# ...
Querying is icing on the cake as you may guess:
MyModel.objects.filter(language=int(Language.Ruby))
# or if you don't prefer `__int__` method..
MyModel.objects.filter(language=Language.Ruby.value)
Representing them in string is also made easy:
# Get the enum item
lang = Language(some_instance.language)
print(str(lang))
# or if you don't prefer `__str__` method..
print(lang.name)
# Same as get_FOO_display
lang.name == some_instance.get_language_display()
Upvotes: 8
Reputation: 283345
class Sequence(object):
def __init__(self, func, *opts):
keys = func(len(opts))
self.attrs = dict(zip([t[0] for t in opts], keys))
self.choices = zip(keys, [t[1] for t in opts])
self.labels = dict(self.choices)
def __getattr__(self, a):
return self.attrs[a]
def __getitem__(self, k):
return self.labels[k]
def __len__(self):
return len(self.choices)
def __iter__(self):
return iter(self.choices)
def __deepcopy__(self, memo):
return self
class Enum(Sequence):
def __init__(self, *opts):
return super(Enum, self).__init__(range, *opts)
class Flags(Sequence):
def __init__(self, *opts):
return super(Flags, self).__init__(lambda l: [1<<i for i in xrange(l)], *opts)
Use it like this:
Priorities = Enum(
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High')
)
priority = models.IntegerField(default=Priorities.LOW, choices=Priorities)
Upvotes: 5
Reputation:
Here's a field type I wrote a few minutes ago that I think does what you want. Its constructor requires an argument 'choices', which may be either a tuple of 2-tuples in the same format as the choices option to IntegerField, or instead a simple list of names (ie ChoiceField(('Low', 'Normal', 'High'), default='Low') ). The class takes care of the mapping from string to int for you, you never see the int.
class ChoiceField(models.IntegerField):
def __init__(self, choices, **kwargs):
if not hasattr(choices[0],'__iter__'):
choices = zip(range(len(choices)), choices)
self.val2choice = dict(choices)
self.choice2val = dict((v,k) for k,v in choices)
kwargs['choices'] = choices
super(models.IntegerField, self).__init__(**kwargs)
def to_python(self, value):
return self.val2choice[value]
def get_db_prep_value(self, choice):
return self.choice2val[choice]
Upvotes: 7
Reputation:
Do as seen here. Then you can use a word that represents the proper integer.
Like so:
LOW = 0
NORMAL = 1
HIGH = 2
STATUS_CHOICES = (
(LOW, 'Low'),
(NORMAL, 'Normal'),
(HIGH, 'High'),
)
Then they are still integers in the DB.
Usage would be thing.priority = Thing.NORMAL
Upvotes: 193
Reputation: 882751
I'd probably set up the reverse-lookup dict once and for all, but if I hadn't I'd just use:
thing.priority = next(value for value, name in Thing.PRIORITIES
if name=='Normal')
which seems simpler than building the dict on the fly just to toss it away again;-).
Upvotes: 7
Reputation: 11529
Simply replace your numbers with the human readable values you would like. As such:
PRIORITIES = (
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High'),
)
This makes it human readable, however, you'd have to define your own ordering.
Upvotes: 2