In Django when using the choices parameter on a form field the format passed in must be as follows:
# within your models.Model class... STUDENT_TYPE_CHOICES = ( ('0', 'freshman'), ('1', 'sophomore'), ('2', 'junior'), ('3', 'senior'), ) student_type = models.CharField(max_length=1, choices=STUDENT_TYPE_CHOICES)
This means elsewhere in your code if you want to specify a choice field value, you’d have to enter the first slot of the tuple’s value, e.g.:
junior_students = Student.objects.filter(student_type='2')
This is pretty terrible since it’s hardcoded in our source, possibly over many files.
How to fix this mess:
In my project I added common/utils.py containing the following:
from enum import Enum class ChoiceEnum(Enum): @classmethod def choices(cls): return tuple((i.name, i.value) for i in cls)
That’s the hard work over.
Now when you create your field with choices:
from common.utils import ChoiceEnum class StudentTypes(ChoiceEnum): freshman = 0 sophomore = 1 junior = 2 senior = 3 # within your models.Model class... class Student(models.Model): student_type = models.CharField(max_length=1, choices=StudentTypes.choices())
Now if we need to access StudentTypes from elsewhere in our source code, we can simply:
junior_students = Student.objects.filter(student_type=StudentTypes.junior.value)
That’s it. If anyone knows of a nicer way feel free to comment below.
In the choices method you can just do “for member in cls” or “for member in cls.__members__”.
That did not work for me with Python 2.6. I had to use “for member in cls._member_names_”.
Yes – there exists a library which provides a much nicer syntax: https://github.com/bigjason/django-choices
It lets you use MyModel.MyModelOptions.my_choice to set a choice, or set it in the field as choices=MyModelOptions.choices iirc.
I think you’re solving a problem that doesn’t exist and and adding unnecessary complexity.
You’re not limited to using integers as keys for the choices. You’re storing the key in a charfield, so a string will work fine (increase the max_length, obviously).
[re-posted with code block]
Here’s what we used for the same issue:
@classmethod
def choices(cls):
return [(x.value, x.name) for x in cls]
Beautiful! Thanks
Interesting idea, thanks for sharing it but with this method is not possible to use _(…)
choices = tuple([(str(p[1].value), p[0]) for p in props])
There is not need in creating the intermediary list:
choices = tuple((str(p[1].value), p[0]) for p in props)
Hi and thanks for the tip !
Enumerations provided by enum34 support iteration, so your snippet can be simplified to a one liner :
return [(choice.name, choice.value) for choice in cls]
fyi, there are at least two packages on pypi that handle this already: django-enumfield and django-enumfields.
This solution cannot be recommended. Use the django-enumfield instead
https://pypi.python.org/pypi/django-enumfield/
The *.value syntax will give you headaches in the long run especially in equality tests on your models:
At some point you will test your model fields for equality:
model.student_type == StudentTypes.freshman
vs
model.student_type == StudentTypes.freshman.value
Have been there, do not want to go there again.
That’s easily solvable by just defining your own Enum class with __str__, __repr__ and __eq__ special methods. You can define them at one place and use it everywhere. By this way you have to use StudentTypes.freshman.value only at the Field definition in the model(i.e. only when you provide a default field value). At all other places StudentTypes.freshman will work. i think that is much better than using a third party library.
The ChoiceEnum.choices function was not returning the choices in the order I declared them so I reworked that class and ended up with a more compact implementation:
class ChoiceEnum(Enum):
@classmethod
def choices(cls):
# format into django choice list
return [(cls[m].value,m.replace('_',' ').title()) for m in reversed(cls._member_names_)]
This also massages the user visible names for multi-word enum members.
Why not something like:
class Choices(object):
def __init__(self, **choices):
self.__dict__.update(choices)
self._choices = choices
self._rev_choices = {v:k for k,v in choices.items()}
@property
def choices(self):
return self._choices
def choice_name(self, value):
return self._rev_choices.get(value)
tuple((value.value, key) for key, value in cls.__dict__.items() if key[0:1] != ‘_’)
Just in case… Whether you want to show your “Human-readable” name in Django Admin just change the order from your choices class method like:
return tuple((i.value, i.name) for i in cls)