Set Django IntegerField by choices=... name - python

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?

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

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

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;-).

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]

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 _

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()

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)

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.

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:
The first item is the database value
The second item is a code-readable value
The third item is a human-readable value
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:
You can use your human-readable value to actually choose the value of your field (in my case, it's useful because i'm scraping wild content and storing it in a normalized way)
A clean value is stored in your database
You have nothing non-DRY to do ;)
Enjoy :)

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)

Related

Python: Construct class (and variable names) through a function?

I recently started to work with Python's classes, since I need to work with it through the use of OTree, a Python framework used for online experiment.
In one file, I define the pages that I want to be created, using classes. So essentially, in the OTree system, each class corresponds to a new page. The thing is, all pages (so classes) are basically the same, at the exception to some two parameters, as shown in the following code:
class Task1(Page):
form_model = 'player'
form_fields = ['Envie_WordsList_Toy']
def is_displayed(self):
return self.round_number == self.participant.vars['task_rounds'][1]
def vars_for_template(player):
WordsList_Toy= Constants.WordsList_Toy.copy()
random.shuffle(WordsList_Toy)
return dict(
WordsList_Toy=WordsList_Toy
)
#staticmethod
def live_method(player, data):
player.WTP_WordsList_Toy = int(data)
def before_next_page(self):
self.participant.vars['Envie_WordsList_Toy'] = self.player.Envie_WordsList_Toy
self.participant.vars['WTP_WordsList_Toy'] = self.player.WTP_WordsList_Toy
So here, the only thing that would change would be the name of the class, as well as the suffix of the variable WordsList_ used throughout this code, which is Toy.
Naively, what I tried to do is to define a function that would take those two parameters, such as this:
def page_creation(Task_Number,name_type):
class Task+str(Task_Number)(Page):
form_model = 'player'
form_fields = ['Envie_WordsList_'+str(name_type)]
def is_displayed(self):
return self.round_number == self.participant.vars['task_rounds'][1]
def vars_for_template(player):
WordsList_+str(name_type) = Constants.WordsList+str(name_type).copy()
random.shuffle(WordsList_+str(name_type))
return dict(
WordsList_+str(name_type)=WordsList_+str(name_type)
)
#staticmethod
def live_method(player, data):
player.WTP_WordsList_+str(name_type) = int(data)
def before_next_page(self):
self.participant.vars['Envie_WordsList_+str(name_type)'] = self.player.Envie_WordsList_+str(name_type)
self.participant.vars['WTP_WordsList_+str(name_type)'] = self.player.WTP_WordsList_+str(name_type)
Obviously, it does not work since I have the feeling that it is not possible to construct variables (or classes identifier) this way. I just started to really work on Python some weeks ago, so some of its aspects might escape me still. Could you help me on this issue? Thank you.
You can generate dynamic classes using the type constructor:
MyClass = type("MyClass", (BaseClass1, BaseClass2), {"attr1": "value1", ...})
Thus, according to your case, that would be:
cls = type(f"Task{TaskNumber}", (Page, ), {"form_fields": [f"Envive_WordList_{name_type}"], ...})
Note that you still have to construct your common methods like __init__, is_displayed and so on, as inner functions of the class factory:
def class_factory(*args, **kwargs):
...
def is_displayed(self):
return self.round_number == self.participant.vars['task_rounds']
def vars_for_template(player):
...
# Classmethod wrapping is done below
def live_method(player, data):
...
cls = type(..., {
"is_displayed": is_displayed,
"vars_for_template": vars_for_template,
"live_method": classmethod(live_method),
...,
}
#classmethod could be used as a function - {"live_method": classmethod(my_method)}

Python: referencing class object list of lists

I am fairly new to python. I have tried to define a class, I then want to create an instance from a file, then refer to specific pieces of it, but cannot seem to. This is Python 3.3.0
Here's the class....
class Teams():
def __init__(self, ID = None, Team = None, R = None, W = None, L = None):
self._items = [ [] for i in range(5) ]
self.Count = 0
def addTeam(self, ID, Team, R=None, W = 0, L = 0):
self._items[0].append(ID)
self._items[1].append(Team)
self._items[2].append(R)
self._items[3].append(W)
self._items[4].append(L)
self.Count += 1
def addTeamsFromFile(self, filename):
inputFile = open(filename, 'r')
for line in inputFile:
words = line.split(',')
self.addTeam(words[0], words[1], words[2], words[3], words[4])
def __len__(self):
return self.Count
Here's the code in Main
startFileName = 'file_test.txt'
filename = startFileName
###########
myTestData = Teams()
myTestData.addTeamsFromFile(startFileName)
sample data in file
100,AAAA,106,5,0
200,BBBB,88,3,2
300,CCCC,45,1,4
400,DDDD,67,3,2
500,EEEE,90,4,1
I think I am good to here (not 100% sure), but now how do I reference this data to see... am i not creating the class correctly? How do I see if one instance is larger than another...
ie, myTestData[2][2] > myTestData[3][2] <----- this is where I get confused, as this doesn't work
Why don't you create a Team class like this :
class Team():
def __init__(self, ID, Team, R=None, W = 0, L = 0)
# set up fields here
Then in Teams
class Teams():
def __init__(self):
self._teams = []
def addTeam (self, ID, Team, R=None, W = 0, L = 0)
team = Team (ID, Team, R=None, W = 0, L = 0)
self._teams.append (team)
Now If i got it right you want to overwrite the > operator's behaviour.
To do that overload __gt__(self, other) [link]
So it will be
class Team ():
# init code from above for Team
def __gt__ (self, otherTeam):
return self.ID > otherTeam.ID # for example
Also be sure to convert those strings to numbers because you compare strings not numbers. Use int function for that.
The immediate problem you're running into is that your code to access the team data doesn't account for your myTestData value being an object rather than a list. You can fix it by doing:
myTestData._items[2][2] > myTestData._items[3][2]
Though, if you plan on doing that much, I'd suggest renaming _items to something that's obviously supposed to be public. You might also want to make the addTeamsFromFile method convert some of the values it reads to integers (rather than leaving them as strings) before passing them to the addTeam method.
An alternative would be to make your Teams class support direct member access. You can do that by creating a method named __getitem__ (and __setitem__ if you want to be able to assign values directly). Something like:
def __getitem__(self, index):
return self._items[index]
#Aleksandar's answer about making a class for the team data items is also a good one. In fact, it might be more useful to have a class for the individual teams than it is to have a class containing several. You could replace the Teams class with a list of Team instances. It depends on what you're going to be doing with it I guess.

Django model class decorator

I have a need to track changes on Django model instances. I'm aware of solutions like django-reversion but they are overkill for my cause.
I had the idea to create a parameterized class decorator to fit this purpose. The arguments are the field names and a callback function. Here is the code I have at this time:
def audit_fields(fields, callback_fx):
def __init__(self, *args, **kwargs):
self.__old_init(*args, **kwargs)
self.__old_state = self.__get_state_helper()
def save(self, *args, **kwargs):
new_state = self.__get_state_helper()
for k,v in new_state.items():
if (self.__old_state[k] != v):
callback_fx(self, k, self.__old_state[k], v)
val = self.__old_save(*args, **kwargs)
self.__old_state = self.__get_state_helper()
return val
def __get_state_helper(self):
# make a list of field/values.
state_dict = dict()
for k,v in [(field.name, field.value_to_string(self)) for field in self._meta.fields if field.name in fields]:
state_dict[k] = v
return state_dict
def fx(clazz):
# Stash originals
clazz.__old_init = clazz.__init__
clazz.__old_save = clazz.save
# Override (and add helper)
clazz.__init__ = __init__
clazz.__get_state_helper = __get_state_helper
clazz.save = save
return clazz
return fx
And use it as follows (only relevant part):
#audit_fields(["status"], fx)
class Order(models.Model):
BASKET = "BASKET"
OPEN = "OPEN"
PAID = "PAID"
SHIPPED = "SHIPPED"
CANCELED = "CANCELED"
ORDER_STATES = ( (BASKET, 'BASKET'),
(OPEN, 'OPEN'),
(PAID, 'PAID'),
(SHIPPED, 'SHIPPED'),
(CANCELED, 'CANCELED') )
status = models.CharField(max_length=16, choices=ORDER_STATES, default=BASKET)
And test on the Django shell with:
from store.models import Order
o=Order()
o.status=Order.OPEN
o.save()
The error I receive then is:
TypeError: int() argument must be a string or a number, not 'Order'
The full stacktrace is here: https://gist.github.com/4020212
Thanks in advance and let me know if you would need more info!
EDIT: Question answered by randomhuman, code edited and usable as shown!
You do not need to explicitly pass a reference to self on this line:
val = self.__old_save(self, *args, **kwargs)
It is a method being called on an object reference. Passing it explicitly in this way is causing it to be seen as one of the other parameters of the save method, one which is expected to be a string or a number.

Combine several filters into one filter() with django-filters

I'm using django-filter app. There is however one problem I do not know how to solve. It's almost exactly the same thing as is described in django documentation:
https://docs.djangoproject.com/en/1.2/topics/db/queries/#spanning-multi-valued-relationships
I want to make a query where I select all Blogs that has an entry with both "Lennon" in headline and was published in 2008, eg.:
Blog.objects.filter(entry__headline__contains='Lennon',
entry__pub_date__year=2008)
Not to select Blogs that has an entry with "Lennon" in headline and another entry (possibly the same) that was published in 2008:
Blog.objects.filter(entry__headline__contains='Lennon').filter(
entry__pub_date__year=2008)
However, if I set up Filter such that there are two fields (nevermind __contains x __exact, just an example):
class BlogFilter(django_filters.FilterSet):
entry__headline = django_filters.CharFilter()
entry__pub_date = django_filters.CharFilter()
class Meta:
model = Blog
fields = ['entry__headline', 'entry__pub_date', ]
django-filter will generete the latter:
Blog.objects.filter(entry__headline__exact='Lennon').filter(
entry__pub_date__exact=2008)
Is there a way to combine both filters into a single filter field?
Well, I came with a solution. It is not possible to do using the regular django-filters, so I extended it a bit. Could've been improved, this is quick-n-dirty solution.
1st added a custom "grouped" field to django_filters.Filter and a filter_grouped method (almost copy of filter method)
class Filter(object):
def __init__(self, name=None, label=None, widget=None, action=None,
lookup_type='exact', required=False, grouped=False, **kwargs):
(...)
self.grouped = grouped
def filter_grouped(self, qs, value):
if isinstance(value, (list, tuple)):
lookup = str(value[1])
if not lookup:
lookup = 'exact' # we fallback to exact if no choice for lookup is provided
value = value[0]
else:
lookup = self.lookup_type
if value:
return {'%s__%s' % (self.name, lookup): value}
return {}
the only difference is that instead of creating a filter on query set, it returns a dictionary.
2nd updated BaseFilterSet qs method/property:
class BaseFilterSet(object):
(...)
#property
def qs(self):
if not hasattr(self, '_qs'):
qs = self.queryset.all()
grouped_dict = {}
for name, filter_ in self.filters.iteritems():
try:
if self.is_bound:
data = self.form[name].data
else:
data = self.form.initial.get(name, self.form[name].field.initial)
val = self.form.fields[name].clean(data)
if filter_.grouped:
grouped_dict.update(filter_.filter_grouped(qs, val))
else:
qs = filter_.filter(qs, val)
except forms.ValidationError:
pass
if grouped_dict:
qs = qs.filter(**grouped_dict)
(...)
return self._qs
The trick is to store all "grouped" filters in a dictionary and then use them all as a single filter.
The filter will look something like this then:
class BlogFilter(django_filters.FilterSet):
entry__headline = django_filters.CharFilter(grouped=True)
entry__pub_date = django_filters.CharFilter(grouped=True)
class Meta:
model = Blog
fields = ['entry__headline', 'entry__pub_date', ]

How should I represent a bit flags int field in django admin?

I have a data model with a bitfield defined something like this:
alter table MemberFlags add column title varchar(50) not null default '';
alter table MemberFlags add column value integer( 3) not null default 0;
insert into MemberFlags (title, value) values
("Blacklisted", 1),
("Special Guest", 2),
("Attend Ad-hoc Sessions", 4),
("Attend VIP Sessions", 8),
("Access Facility A", 16),
("Access Facility B", 32)
And used like this:
alter table Membership add column title varchar(50) not null default '';
alter table Membership add column flags integer( 3) not null default 0;
insert into Membership (title, flags) values
("Guest Pass", 4+2 ),
("Silver Plan", 16+ 4 ),
("Gold Plan", 32+16+ 4+2 ),
("VIP Pass", 32+16+8+4+2 )
My questions are:
A) What's the easiest way to represent the different bitflags as separate items in the admin site? Should I override the template, or do something with forms?
B) How about the search list? I could create functions in the model to represent each bit, but how would searching and sorting be done?
I'm new to Django.
A great tested solution, even if it doesn't fit your model right away, would be using django-bitfield
I think the best solution here would be for you to create a new field type by subclassing models.Field. You could make use of the choices parameter to assign the valid bit flags and their meanings. This would help keep your model declaration clean and readable, with a final result along the lines of:
class BitFlagField(models.Field):
...
class MyModel(models.Model):
...
FLAG_CHOICES = (
(1, 'Blacklisted'),
(2, 'Special Guest'),
(4, 'Attend Ad-hoc Sessions'),
(8, 'Attend VIP Sessions'),
(16, 'Access Facility A'),
(32, 'Access Facility B'),
)
flags = BitFlagField(choices=FLAG_CHOICES)
...
The Django documentation has a great in-depth article on how to go about subclassing models.Field:
Writing Custom Model Fields
It seems to cover everything you need to do, including:
Specifying Form Field (Tying a form to the field so that django-admin knows how to display it.)
Preparing Values for Lookups (Which will allow you to use the field for searches and filtering.)
If you're looking for an example of a subclassed field, this snippet might be of use. Its goal is similar (multiple choices as a model field), but its manner of storing them in the database differs (it's using a CSV text field instead of bit flags).
Working off the snippet in Andrew's answer, here are the changes you'd need to make:
from django.db import models
from django import forms
class BitFlagFormField(forms.MultipleChoiceField):
widget = forms.CheckboxSelectMultiple
def __init__(self, *args, **kwargs):
super(BitFlagFormField, self).__init__(*args, **kwargs)
class BitFlagField(models.Field):
__metaclass__ = models.SubfieldBase
def get_internal_type(self):
return "Integer"
def get_choices_default(self):
return self.get_choices(include_blank=False)
def _get_FIELD_display(self, field):
value = getattr(self, field.attname)
choicedict = dict(field.choices)
def formfield(self, **kwargs):
# do not call super, as that overrides default widget if it has choices
defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name),
'help_text': self.help_text, 'choices':self.choices}
if self.has_default():
defaults['initial'] = self.get_default()
defaults.update(kwargs)
return BitFlagFormField(**defaults)
def get_db_prep_value(self, value):
if isinstance(value, int):
return value
elif isinstance(value, list):
return sum(value)
def to_python(self, value):
result = []
n = 1
while value > 0:
if (value % 2) > 0:
result.append(n)
n *= 2
value /= 2
return sorted(result)
def contribute_to_class(self, cls, name):
super(BitFlagField, self).contribute_to_class(cls, name)
if self.choices:
func = lambda self, fieldname = name, choicedict = dict(self.choices):" and ".join([choicedict.get(value,value) for value in getattr(self,fieldname)])
setattr(cls, 'get_%s_display' % self.name, func)
This is how I would use the flags with my User class:
FLAGS = {
1:"Blacklisted",
2:"SpecialGuest",
4:"AttendAd-hocSessions",
8:"AttendVIPSessions",
16:"AccessFacilityA",
32:"AccessFacilityB",
}
class User(object):
def __init__(self, name="John Doe", groups=0):
self.name = name
self.groups = groups
def memberof(self):
''' Display string representation of the groups. '''
for flag in sorted(FLAGS):
if (flag & self.groups) == flag:
print FLAGS[flag]
Of course instead of printing the flags, you can create a comma-separated string to display in the admin view, or whatever you desire.
For the admin, just use a boolean for each of the group values.

Categories