我正在尝试创建一个自定义模型字段,该字段将namedtuples作为选项(将名称存储在CharField中的db中,并从to_python
返回namedtuple对象 - followup to this question)。在我的模型测试中,我得到了表单验证错误Select a valid choice. ReminderFrequency(db_value="monthly", display_value="every month", get_next_reminder_time=1) is not one of the available choices.
也就是说,验证所看到的选择值是实际的namedtuple对象(namedtuple的__repr__
返回那个很好的长描述性字符串),而不是db_value属性。我在这里做错了什么?
完整示例:
from django.core.exceptions import ValidationError
from django.db import models
from django.forms.fields import TypedChoiceField
from django.utils import six
class NTChoiceFormField(TypedChoiceField):
def to_python(self, value):
if value in self.empty_values:
return ''
#do i need to return the namedtuple here too? how? i don't have
#access to the nt_choices_dict...
return value
class NamedTupleChoiceField(models.CharField):
"""
A field to handle using namedtuple as choices.
Accepts all CharField options, except it substitutes
`nt_choices` for the `choices` keyword argument.
`nt_choices` should be a list or tuple of namedtuples of the same type.
"""
__metaclass__ = models.SubfieldBase
description = unicode("A choice field that stores namedtuple names in the db"
" and returns the namedtuple itself in python code.")
def __init__(self, *args, **kwargs):
nt_choices = kwargs.pop("nt_choices")
assert not kwargs.has_key("choices"), "Specify nt_choices as list or tuple of named tuples instead of standard Django choices tuple."
assert hasattr(nt_choices, "__iter__")
# make sure all nt_choices are of same type
self.choice_base_class = type(nt_choices[0])
for choice in nt_choices[1:]:
assert isinstance(choice, self.choice_base_class)
# store choices in a dict so we can get the namedtuples out by key from db
self.nt_choice_dict = {}
for choice in nt_choices:
self.nt_choice_dict.update({choice.db_value: choice})
# build the choices spec as tuple of tuples that django wants
choices_tuple = [(choice.db_value, choice.display_value) for choice in nt_choices]
super(NamedTupleChoiceField, self).__init__(choices=choices_tuple, *args, **kwargs)
def get_prep_value(self, value):
if isinstance(value, six.string_types):
return value
elif value is None:
return u''
return value.db_value
def get_default(self):
# Error checking to avoid specifying default as instance of namedtuple
assert not isinstance(self.default, self.choice_base_class), "Specify default=<SomeChoice>.db_value instead of SomeChoice instance."
default = super(NamedTupleChoiceField, self).get_default()
return default
def formfield(self, **kwargs):
form_field = super(NamedTupleChoiceField, self).formfield(
choices_form_class=NTChoiceFormField,
**kwargs)
return form_field
def to_python(self, value):
if value in (None, u"") or isinstance(value, self.choice_base_class):
return value
return self.nt_choice_dict[value]
这是我的失败测试:
ReminderFrequency = namedtuple('ReminderFrequency',
['db_value',
'display_value',
"get_next_reminder_time"])
Weekly = ReminderFrequency(db_value="weekly",
display_value="every week",
get_next_reminder_time=1)
Monthly = ReminderFrequency(db_value="monthly",
display_value="every month",
get_next_reminder_time=1)
class NamedTupleChoiceModel(models.Model):
nt_choice = NamedTupleChoiceField(nt_choices=[Weekly, Monthly],
max_length=50,
default=Monthly.db_value)
class NamedTupleChoiceFieldTests(TestCase):
def test_with_modelform(self):
data = {'nt_choice': Monthly.db_value}
form = modelform_factory(model=NamedTupleChoiceModel)(data)
success = form.is_valid() # need to assign if we want to print errors
self.assert_(success, msg=form.errors)
form.save()
self.assertEqual(NamedTupleChoiceModel.objects.count(), 1)