Newbie to all this! i'm working on displaying phone field displayed as (xxx)xxx-xxxx on front end.below is my code. My question is 1. all fields are mandatory, for some reason,phone is not behaving as expected.Even if it is left blank its not complaining and 2.how can i test this widget's functionality
class USPhoneNumberWidget(forms.MultiWidget):
def __init__(self,attrs=None):
widgets = (forms.TextInput(attrs={'size':'3','maxlength':'3'}),forms.TextInput(attrs={'size':'3','maxlength':'3'}),forms.TextInput(attrs={'size':'3','maxlength':'4'}))
super(USPhoneNumberWidget,self).__init__(widgets,attrs=attrs)
def decompress(self, value):
if value:
val = value.split('-')
return [val[0],val[1],val[2]]
return [None,None,None]
def compress(self, data_list):
if data_list[0] and data_list[1] and data_list[2]:
ph1 = self.check_value(data_list[0])
ph2 = self.check_value(data_list[1])
ph3 = self.check_value(data_list[2])
return '%s''%s''%s' %(ph1,ph2,ph3)
else:
return None
def check_value(self,val):
try:
if val.isdigit():
return val
except:
raise forms.ValidationError('This Field has to be a number!')
def clean(self, value):
try:
value = re.sub('(\(|\)|\s+)','',smart_unicode(value))
m = phone_digits_re.search(value)
if m:
return u'%s%s%s' % (m.group(1),m.group(2),m.group(3))
except:
raise ValidationError('Phone Number is required.')
def value_from_datadict(self,data,files,name):
val_list = [widget.value_from_datadict(data,files,name+'_%s' %i) for i,widget in enumerate(self.widgets)]
try:
return val_list
except ValueError:
return ''
def format_output(self,rendered_widgets):
return '('+rendered_widgets[0]+')'+rendered_widgets[1]+'-'+rendered_widgets[2]
class CustomerForm(ModelForm):
phone = forms.CharField(required=True,widget=USPhoneNumberWidget())
class Meta:
model = Customer
fields = ('fname','lname','address1','address2','city','state','zipcode','phone')
In models blank and null are not true.
Any input it highly appreciated.Thanks
Here is the phone field:
phone = forms.CharField(label = 'Phone',widget=USPhoneNumberWidget()
class USPhoneNumberWidget(forms.MultiWidget):
"""
A widget that splits phone number into areacode/next3/last4 with textinput.
"""
def __init__(self,attrs=None):
widgets = (forms.TextInput(attrs={'size':'3','maxlength':'3'}),forms.TextInput(attrs={'size':'3','maxlength':'3'}),forms.TextInput(attrs={'size':'4','maxlength':'4'}))
super(USPhoneNumberWidget,self).__init__(widgets,attrs=attrs)
def decompress(self, value):
if value:
val = value
return val[:3],val[3:6],val[6:]
return None,None,None
def compress(self, data_list):
if data_list[0] and data_list[1] and data_list[2]:
return '%s''%s''%s' %(data_list[0],data_list[1],data_list[2])
else:
return None
def value_from_datadict(self,data,files,name):
val_list = [widget.value_from_datadict(data,files,name+'_%s' %i) for i,widget in enumerate(self.widgets)]
if val_list:
return '%s''%s''%s' %(val_list[0],val_list[1],val_list[2])
def format_output(self,rendered_widgets):
return '( '+rendered_widgets[0]+' )'+rendered_widgets[1]+' - '+rendered_widgets[2]
But depending on how you store the phone# in db 'return' line is to be changed. here I'm accepting it as (xxx)-xxx-xxxx format.In compress it receives ph_0(areacode),ph_1(next 3),ph_2(last4) in that order.but I'm storing it as xxxxxxxxxx.
Firebug helped me understand better about what return values should be. I'll update the answer when i come to know how testing could be done.
Related
I have created a bespoke form widget which saves an address as a list.
class AddressWidget(MultiWidget):
def __init__(self, base_widget, attrs=None):
widgets = (
forms.TextInput(attrs={'placeholder': 'Address', 'class': 'form-control'}),
forms.TextInput(attrs={'placeholder': 'Address Line 2', 'class': 'form-control'}),
forms.TextInput(attrs={'placeholder': 'City', 'class': 'form-control'}),
forms.TextInput(attrs={'placeholder': 'State', 'class': 'form-control'}),
forms.TextInput(attrs={'placeholder': 'Postcode', 'class': 'form-control'}),
)
super().__init__(widgets, attrs)
def decompress(self, value):
if value:
return (value.address1, value.address2, value.city, value.state, value.postcode)
return (None, None, None, None, None)
Saving the form works as I want, but when re-entering the form in order to change values, it doesn't get prepopulated, although all other regular fields do.
How do I get it to populate the field?
It is used in the form as:
class ResponseForm(forms.ModelForm)
address = AddressField()
...
class Meta:
model = SomeModel
fields = ('address',)
class AddressField(MultiValueField):
"""
Custom field to take user inputs of Address
"""
widget = AddressWidget(base_widget=TextInput)
def __init__(self, *, attrs=None, **kwargs):
fields = (
CharField(label=_('Address Line 1'), max_length=25),
CharField(label=_('Address Line 2'), max_length=25),
CharField(label=_('City'), max_length=25),
CharField(label=_('State'), max_length=25),
CharField(label=_('Country'), max_length=25)
)
super().__init__(fields, required=False)
def clean(self, value, initial=None):
value = super().clean(value)
return value
def compress(self, value_list):
if value_list:
return value_list
return [[],[],[]]
Within the model it is defined as:
class SomeModel(models.Model):
address = models.TextField()
...
A typical value entered might be:
123 Some Street
Example Area
This Town
MYP 0ST
It is saved to the database table like this in a text field.
EDIT
I think my problem is the ResponseForm. I am using an app and response form is initialising the widgets.
class ResponseForm(forms.ModelForm):
FIELDS = {
Question.TINY_TEXT: TinyCharField,
Question.EXTRA_SHORT_TEXT: ExtraShortCharField,
Question.SHORT_TEXT: ShortCharField,
Question.TEXT: forms.CharField,
Question.EXTRA_LONG_TEXT: forms.CharField,
Question.SELECT_MULTIPLE: forms.MultipleChoiceField,
Question.INTEGER: forms.IntegerField,
Question.FLOAT: forms.FloatField,
Question.DATE: forms.DateField,
Question.TIME: forms.TimeField,
Question.CHECK_BOXES: forms.MultipleChoiceField,
Question.ADDRESS: forms.CharField, #AddressField,
Question.DISCLAIMER: forms.BooleanField,
Question.HEIGHT: HeightFormField,
Question.WEIGHT: WeightFormField,
Question.RANGE: IntegerRangeField,
Question.VOLUME: VolumeFormField,
}
WIDGETS = {
Question.TINY_TEXT: forms.TextInput(),
Question.EXTRA_SHORT_TEXT: forms.TextInput(),
Question.SHORT_TEXT: forms.TextInput(),
Question.TEXT: forms.Textarea(attrs={'maxlength':250}),
Question.ADDRESS: forms.Textarea(attrs={'maxlength':250}),
Question.EXTRA_LONG_TEXT: forms.Textarea(attrs={'maxlength':750}),
Question.RADIO: forms.RadioSelect,
Question.SELECT: forms.Select,
Question.SELECT_IMAGE: ImageSelectWidget,
Question.SELECT_MULTIPLE: forms.SelectMultiple,
Question.CHECK_BOXES: forms.CheckboxSelectMultiple,
Question.DATE: DatePickerInput,
Question.DISCLAIMER: forms.CheckboxInput(attrs={'required': True}),
}
class Meta:
model = Response
fields = ()
def __init__(self, *args, **kwargs):
"""Expects a survey object to be passed in initially"""
self.survey = kwargs.pop("survey")
self.user = kwargs.pop("user")
try:
self.step = int(kwargs.pop("step"))
except KeyError:
self.step = None
super().__init__(*args, **kwargs)
self.uuid = uuid.uuid4().hex
self.categories = self.survey.non_empty_categories()
self.qs_with_no_cat = self.survey.questions.filter(category__isnull=True).order_by("order", "id")
if self.survey.display_method == Survey.BY_CATEGORY:
self.steps_count = len(self.categories) + (1 if self.qs_with_no_cat else 0)
else:
self.steps_count = len(self.survey.questions.all())
# will contain prefetched data to avoid multiple db calls
self.response = False
self.answers = False
self.add_questions(kwargs.get("data"))
self._get_preexisting_response()
if not self.survey.editable_answers and self.response is not None:
for name in self.fields.keys():
self.fields[name].widget.attrs["disabled"] = True
def add_questions(self, data):
'''
add a field for each survey question, corresponding to the question
type as appropriate.
'''
if self.survey.display_method == Survey.BY_CATEGORY and self.step is not None:
if self.step == len(self.categories):
qs_for_step = self.survey.questions.filter(category__isnull=True).order_by("order", "id")
else:
qs_for_step = self.survey.questions.filter(category=self.categories[self.step])
for question in qs_for_step:
self.add_question(question, data)
else:
for i, question in enumerate(self.survey.questions.all()):
not_to_keep = i != self.step and self.step is not None
if self.survey.display_method == Survey.BY_QUESTION and not_to_keep:
continue
self.add_question(question, data)
def current_categories(self):
if self.survey.display_method == Survey.BY_CATEGORY:
if self.step is not None and self.step < len(self.categories):
return [self.categories[self.step]]
return [Category(name="No category", description="No cat desc")]
else:
extras = []
if self.qs_with_no_cat:
extras = [Category(name="No category", description="No cat desc")]
return self.categories + extras
def _get_preexisting_response(self):
"""Recover a pre-existing response in database.
The user must be logged. Will store the response retrieved in an attribute
to avoid multiple db calls.
:rtype: Response or None"""
if self.response:
return self.response
if not self.user.is_authenticated:
self.response = None
else:
try:
self.response = Response.objects.prefetch_related("user", "survey").get(
user=self.user, survey=self.survey
)
except Response.DoesNotExist:
LOGGER.debug("No saved response for '%s' for user %s", self.survey, self.user)
self.response = None
return self.response
def _get_preexisting_answers(self):
"""Recover pre-existing answers in database.
The user must be logged. A Response containing the Answer must exists.
Will create an attribute containing the answers retrieved to avoid multiple
db calls.
:rtype: dict of Answer or None"""
if self.answers:
return self.answers
response = self._get_preexisting_response()
if response is None:
self.answers = None
try:
answers = Answer.objects.filter(response=response).prefetch_related("question")
self.answers = {answer.question.id: answer for answer in answers.all()}
except Answer.DoesNotExist:
self.answers = None
return self.answers
def _get_preexisting_answer(self, question):
"""Recover a pre-existing answer in database.
The user must be logged. A Response containing the Answer must exists.
:param Question question: The question we want to recover in the
response.
:rtype: Answer or None"""
answers = self._get_preexisting_answers()
return answers.get(question.id, None)
def get_question_initial(self, question, data):
"""Get the initial value that we should use in the Form
:param Question question: The question
:param dict data: Value from a POST request.
:rtype: String or None"""
initial = None
answer = self._get_preexisting_answer(question)
if answer:
# Initialize the field with values from the database if any
if question.type in [Question.SELECT_MULTIPLE]:
initial = []
if answer.body == "[]":
pass
elif "[" in answer.body and "]" in answer.body:
initial = []
unformated_choices = answer.body[1:-1].strip()
for unformated_choice in unformated_choices.split(settings.CHOICES_SEPARATOR):
choice = unformated_choice.split("'")[1]
initial.append(slugify(choice))
else:
# Only one element
initial.append(slugify(answer.body))
elif question.type == Question.DATE:
initial = datetime.datetime.strptime(answer.body, "%Y-%m-%d").date()
else:
initial = answer.body
if data:
# Initialize the field field from a POST request, if any.
# Replace values from the database
initial = data.get("question_%d" % question.pk)
return initial
def get_question_widget(self, question):
"""Return the widget we should use for a question.
:param Question question: The question
:rtype: django.forms.widget or None"""
try:
return self.WIDGETS[question.type]
except KeyError:
return None
#staticmethod
def get_question_choices(question):
"""Return the choices we should use for a question.
:param Question question: The question
:rtype: List of String or None"""
qchoices = None
if question.type not in [Question.TEXT, Question.SHORT_TEXT, Question.INTEGER, Question.FLOAT, Question.DATE]:
qchoices = question.get_choices()
# add an empty option at the top so that the user has to explicitly
# select one of the options
if question.type in [Question.SELECT, Question.SELECT_IMAGE]:
qchoices = tuple([("", "-------------")]) + qchoices
return qchoices
def get_question_field(self, question, **kwargs):
"""Return the field we should use in our form.
:param Question question: The question
:param **kwargs: A dict of parameter properly initialized in
add_question.
:rtype: django.forms.fields"""
# logging.debug("Args passed to field %s", kwargs)
try:
return self.FIELDS[question.type](**kwargs)
except KeyError:
return forms.ChoiceField(**kwargs)
def add_question(self, question, data):
"""Add a question to the form.
:param Question question: The question to add.
:param dict data: The pre-existing values from a post request."""
kwargs = {"label": question.text, "required": question.required}
initial = self.get_question_initial(question, data)
if initial:
kwargs["initial"] = initial
choices = self.get_question_choices(question)
if choices:
kwargs["choices"] = choices
widget = self.get_question_widget(question)
if widget:
kwargs["widget"] = widget
field = self.get_question_field(question, **kwargs)
field.widget.attrs["category"] = question.category.name if question.category else ""
if question.type == Question.DATE:
field.widget.attrs["class"] = "date"
# logging.debug("Field for %s : %s", question, field.__dict__)
self.fields["question_%d" % question.pk] = field
def has_next_step(self):
if not self.survey.is_all_in_one_page():
if self.step < self.steps_count - 1:
return True
return False
def next_step_url(self):
if self.has_next_step():
context = {"id": self.survey.id, "step": self.step + 1}
return reverse("survey:survey-detail-step", kwargs=context)
def current_step_url(self):
return reverse("survey-detail-step", kwargs={"id": self.survey.id, "step": self.step})
def save(self, commit=True):
"""Save the response object"""
# Recover an existing response from the database if any
# There is only one response by logged user.
response = self._get_preexisting_response()
if not self.survey.editable_answers and response is not None:
return None
if response is None:
response = super().save(commit=False)
response.survey = self.survey
response.interview_uuid = self.uuid
if self.user.is_authenticated:
response.user = self.user
response.save()
# response "raw" data as dict (for signal)
data = {"survey_id": response.survey.id, "interview_uuid": response.interview_uuid, "responses": []}
# create an answer object for each question and associate it with this
# response.
for field_name, field_value in list(self.cleaned_data.items()):
if field_name.startswith("question_"):
# warning: this way of extracting the id is very fragile and
# entirely dependent on the way the question_id is encoded in
# the field name in the __init__ method of this form class.
q_id = int(field_name.split("_")[1])
question = Question.objects.get(pk=q_id)
answer = self._get_preexisting_answer(question)
if answer is None:
answer = Answer(question=question)
if question.type == Question.SELECT_IMAGE:
value, img_src = field_value.split(":", 1)
# TODO Handling of SELECT IMAGE
LOGGER.debug("Question.SELECT_IMAGE not implemented, please use : %s and %s", value, img_src)
answer.body = field_value
data["responses"].append((answer.question.id, answer.body))
LOGGER.debug("Creating answer for question %d of type %s : %s", q_id, answer.question.type, field_value)
answer.response = response
answer.save()
survey_completed.send(sender=Response, instance=response, data=data)
return response
Since address is stored as models.TextField() and retrieved as Python str in the model:
In MultiWidget decompress, load from str:
class AddressWidget(MultiWidget):
...
def decompress(self, value):
if value:
return json.loads(value)
return (None, None, None, None, None)
In MultiValueField compress, dump to str:
class AddressField(MultiValueField):
...
def compress(self, value_list):
if value_list:
return json.dumps(value_list)
return ''
I want to override the render for RadioSelect in django. (I have done a similar thing for checkboxes and I want both to look the same). The general workflow for this would be to write a custom renderer, and then change the render in the ChoiceInput, BUT when I copy the existing code and run it the html output is not safe and the escaped html string is shown. This is not making any sense as I didn't do any changes to the class yet, other than change the name:
In my widgets.py:
class ButtonRadioFieldRenderer(ChoiceFieldRenderer):
choice_input_class = OtherRadioChoiceInput
class OtherRadioChoiceInput(OtherChoiceInput):
input_type = 'radio'
def __init__(self, *args, **kwargs):
super(OtherRadioChoiceInput, self).__init__(*args, **kwargs)
self.value = force_text(self.value)
#html_safe
#python_2_unicode_compatible
class OtherChoiceInput(SubWidget):
"""
An object used by ChoiceFieldRenderer that represents a single
<input type='$input_type'>.
"""
input_type = None # Subclasses must define this
def __init__(self, name, value, attrs, choice, index):
self.name = name
self.value = value
self.attrs = attrs
self.choice_value = force_text(choice[0])
self.choice_label = force_text(choice[1])
self.index = index
if 'id' in self.attrs:
self.attrs['id'] += "_%d" % self.index
def __str__(self):
return self.render()
def render(self, name=None, value=None, attrs=None, choices=()):
if self.id_for_label:
label_for = format_html(' for="{}"', self.id_for_label)
else:
label_for = ''
attrs = dict(self.attrs, **attrs) if attrs else self.attrs
return format_html(
'<label{}>{} {}</label>', label_for, self.tag(attrs), self.choice_label
)
def is_checked(self):
return self.value == self.choice_value
def tag(self, attrs=None):
attrs = attrs or self.attrs
final_attrs = dict(attrs, type=self.input_type, name=self.name, value=self.choice_value)
if self.is_checked():
final_attrs['checked'] = 'checked'
return format_html('<input{} />', flatatt(final_attrs))
#property
def id_for_label(self):
return self.attrs.get('id', '')
In my forms.py:
DELIMITER_CHOICES = [
('space', ugettext_lazy("space")),
('underscore', "_"),
]
class SingleDelimiterForm(forms.Form):
delimiter = forms.ChoiceField(initial=0, widget=forms.RadioSelect(renderer=ButtonRadioFieldRenderer), choices=DELIMITER_CHOICES)
The only changes I did was to put "Other" and "Button" in front of already existing classes, and the code doesn't run anymore. If I change OtherChoiceInput to ChoiceInput the code is working. (in the end I only want to add a class to the label...)
I needed unicode strings for the code to work, so from __future__ import unicode_literals fixed the issue. I still found this error very confusing.
This is how my model.py looks like. The third field (label) should contain more than one value (as a news post can have more than one label) like a list. How to achieve that?
from django.db import
class News(models.Model)
heading=models.CharField(max_length=50)
post_date=models.DateTimeField('Date Posted')
labels=models.???
Django not provide default list field, so that you have to create your own list field from inherit TextField, I am posting one of the example code have a look.
class ListField(models.TextField, metaclass=models.SubfieldBase):
description = "ListField store List of element"
SPLIT_CHAR= ';'
def __init__(self, *args, list_choices=None, **kwargs):
super(ListField, self).__init__(*args, **kwargs)
if list_choices is None:
list_choices = []
self.list_choices = list_choices
def to_python(self, value):
res = []
if value is None:
res = []
if isinstance(value, list):
res = value
if isinstance(value, builtins.str):
res = value.split(self.SPLIT_CHAR)
try:
if "[" in value and "]" in value:
res = eval(value)
except:
pass
return res
def get_prep_value(self, value):
if value is None or value == "":
return None
res = self.SPLIT_CHAR.join(value)
return res
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_prep_value(value)
def formfield(self, **kwargs):
return forms.MultipleChoiceField(choices=self.list_choices)
Then create List field like this one.
from django.db import
class News(models.Model)
heading=models.CharField(max_length=50)
post_date=models.DateTimeField('Date Posted')
labels=ListField()
I have the following class which to be used for a custom model field:
class PaymentGateway(object):
def fullname(self):
return self.__module__ + "." + self.__class__.__name__
def authorize(self):
raise NotImplemented()
def pay(self):
raise NotImplemented()
def __unicode__(self):
return self.fullname()
class DPS(PaymentGateway):
def authorize(self):
pass
def pay(self):
pass
This is how I am writing the custom model field:
from django.db import models
from django.utils.six import with_metaclass
from django.utils.module_loading import import_by_path
class PaymentGatewayField(with_metaclass(models.SubfieldBase, models.CharField)):
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 255
super(PaymentGatewayField, self).__init__(*args, **kwargs)
def to_python(self, value):
if value and isinstance(value, basestring):
kls = import_by_path(value)
return kls()
return value
def get_prep_value(self, value):
if value and not isinstance(value, basestring):
return value.fullname()
return value
def value_from_object(self, obj):
return self.get_prep_value(getattr(obj, self.attname))
def formfield(self, **kwargs):
defaults = {'form_class': PaymentGatewayFormField}
defaults.update(kwargs)
return super(PaymentGatewayField, self).formfield(**defaults)
class PaymentGatewayFormField(BaseTemporalField):
def to_python(self, value):
if value in self.empty_values:
return None
if isinstance(value, PaymentGateway):
return value
if value and isinstance(value, basestring):
kls = import_by_path(value)
return kls()
return super(PaymentGatewayFormField, self).to_python(value)
And this is how it is used in a model:
class BillingToken(models.Model):
user = models.ForeignKey('User', related_name='billingtokens')
name = models.CharField(max_length=255)
card_number = models.CharField(max_length=255)
expire_on = models.DateField()
token = models.CharField(max_length=255)
payment_gateway = PaymentGatewayField(choices=[('project.contrib.paymentgateways.dps.DPS', 'DPS')])
I have added the model to admin:
class BillingTokenInline(admin.StackedInline):
model = BillingToken
extra = 0
class UserAdmin(admin.ModelAdmin):
inlines = [BillingTokenInline]
admin.site.register(User, UserAdmin)
So if I go to edit existing user record, which it's billingtoken record has 'DPS' already chosen, and hit save, I get a invalid choice error:
Select a valid choice. project.contrib.paymentgateways.dps.DPS is not one of the available choices.
I have tried to trace the django code and found the error message is defined in django.forms.fields.ChoiceField:
class ChoiceField(Field):
widget = Select
default_error_messages = {
'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
}
def __init__(self, choices=(), required=True, widget=None, label=None,
initial=None, help_text='', *args, **kwargs):
super(ChoiceField, self).__init__(required=required, widget=widget, label=label,
initial=initial, help_text=help_text, *args, **kwargs)
self.choices = choices
def __deepcopy__(self, memo):
result = super(ChoiceField, self).__deepcopy__(memo)
result._choices = copy.deepcopy(self._choices, memo)
return result
def _get_choices(self):
return self._choices
def _set_choices(self, value):
# Setting choices also sets the choices on the widget.
# choices can be any iterable, but we call list() on it because
# it will be consumed more than once.
self._choices = self.widget.choices = list(value)
choices = property(_get_choices, _set_choices)
def to_python(self, value):
"Returns a Unicode object."
if value in self.empty_values:
return ''
return smart_text(value)
def validate(self, value):
"""
Validates that the input is in self.choices.
"""
super(ChoiceField, self).validate(value)
if value and not self.valid_value(value):
raise ValidationError(
self.error_messages['invalid_choice'],
code='nvalid_choice',
params={'value': value},
)
def valid_value(self, value):
"Check to see if the provided value is a valid choice"
text_value = force_text(value)
for k, v in self.choices:
if isinstance(v, (list, tuple)):
# This is an optgroup, so look inside the group for options
for k2, v2 in v:
if value == k2 or text_value == force_text(k2):
return True
else:
if value == k or text_value == force_text(k):
return True
return False
But after putting some debug statements before the raise ValidationError line in this function, the exception is not raised here, but the error message is definitely referenced from here. Which hints me that somewhere else is extending ChoiceField might be raising this exception, and I have tried the obvious ones (ChoiceField, TypedChoiceField, MultipleChoiceField, TypedMultipleChoiceField) still no luck. This has already consumed a lot of my time and would like to seek some clever clues.
Finally, figured out where it is throwing the error:
It's in django/db/models/fields/__init__.py line 236
typically because of line 234:
elif value == option_key:
Where value is a PaymentGateway object and option_key is a string
To fix this problem, I had to override the clean method:
def clean(self, value, model_instance):
value = unicode(value)
self.validate(value, model_instance)
self.run_validators(value)
return self.to_python(value)
I've set up a MultiValueField that's used in an inline. If I leave the MultiValueField blank, it appears to think that it's stilled filled in. As a result, I keep getting form validation errors because as far as the form is concerned those inlines aren't empty (and so get validated).
So I guess I'm wondering: are there tricks to setting up the MultiValueField so that it can be explicitly blank, so that I can avoid raising a validation error?
Here's the code in question:
class TypedValueField(forms.MultiValueField):
def __init__(self, *args, **kwargs):
fields = (
forms.ChoiceField(required=False, choices=[(None, '(type)')] + [(c,c) for c in ['int','float','bool','string']]),
forms.CharField(required=False)
)
super(TypedValueField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if data_list:
# Raise a validation error if time or date is empty
# (possible if SplitDateTimeField has required=False).
if data_list[1]=="" or data_list[1]==None:
return data_list[1]
if data_list[0] == 'bool':
try:
if data_list[1].lower() == "true":
return True
except:
pass
try:
if int(data_list[1] == 1):
return True
except ValueError:
raise forms.ValidationError("You must enter True or False")
return False
if data_list[0] == 'int':
try:
return int(data_list[1])
except ValueError:
raise forms.ValidationError("You must enter a number")
if data_list[0] == 'float':
try:
return float(data_list[1])
except ValueError:
raise forms.ValidationError("You must enter a decimal number")
if data_list[0] == 'string':
return data_list[0]
else:
raise forms.ValidationError("Invalid data type")
return None
class TypedValueWidget(forms.MultiWidget):
def __init__(self, attrs=None):
widgets = (
forms.Select(choices=[(None, '(type)')] + [(c,c) for c in ['int','float','bool','string']]),
forms.TextInput()
)
super(TypedValueWidget, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
if isinstance(value, bool):
return ['bool', value]
if isinstance(value, float):
return ['float', value]
if isinstance(value, int):
return ['int', value]
if isinstance(value, basestring):
return ['string', value]
else:
raise Exception("Invalid type found: %s" % type(value))
return [None, None]
class ParamInlineForm(forms.ModelForm):
match = TypedValueField(required=False, widget=TypedValueWidget())
class Meta:
model = Param
override clean method of MutliValueField
(take a look into django source)
This was my problem, right here:
fields = (
forms.ChoiceField(required=False, choices=[(None, '(type)')] + [(c,c) for c in ['int','float','bool','string']]),
forms.CharField(required=False)
)
Needed to be changed to
fields = (
forms.ChoiceField(required=False, choices=[("", '(type)')] + [(c,c) for c in ['int','float','bool','string']]),
forms.CharField(required=False)
)
If you use None as the value for a choice in a choice field, it gets rendered as
<option value="None">(type)</option>
instead of
<option value="">(type)</option>
Here's the guilty source code in Django (In the Select widget class's render_option method):
return u'<option value="%s"%s>%s</option>' % (
escape(option_value), selected_html,
conditional_escape(force_unicode(option_label)))
I can't think of any situation where you'd specify None as a value and want to get "None" back. Although of course the empty value "" is also different than None.