Overriding render of RadioSelect in django forms gives unexpected results - django

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.

Related

Dynamic Multiwidget/MultivalueField from Model

The beginning is simple:
class Question(models.Model):
question_string = models.CharField(max_length=255)
answers = models.CharField(max_length=255)
answers are json of list of strings e.g ['Yes', 'No']. Number of answers is dynamic.
The challenge for me now is to write a form for this model.
Current state is:
class NewQuestionForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(NewQuestionForm, self).__init__(*args, **kwargs)
if self.instance:
self.fields['answers'] = AnswerField(num_widgets=len(json.loads(self.instance.answers)))
class Meta:
model = Question
fields = ['question']
widgets = {
'question': forms.TextInput(attrs={'class': "form-control"})
}
class AnswerField(forms.MultiValueField):
def __init__(self, num_widgets, *args, **kwargs):
list_fields = []
list_widgets = []
for garb in range(0, num_widgets):
field = forms.CharField()
list_fields.append(field)
list_widgets.append(field.widget)
self.widget = AnswerWidget(widgets=list_widgets)
super(AnswerField, self).__init__(fields=list_fields, *args, **kwargs)
def compress(self, data_list):
return json.dumps(data_list)
class AnswerWidget(forms.MultiWidget):
def decompress(self, value):
return json.loads(value)
The problem is: i get 'the JSON object must be str, not 'NoneType'' in template with '{{ field }}'
What is wrong?
I found the problem. I forgot to add 'answers' to class Meta 'fields'.
So my example of dynamic Multiwidget created from Model is:
class NewQuestionForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
# need this to create right number of fields from POST
edit_mode = False
if len(args) > 0:
edit_mode = True
answer_fields = 0
for counter in range(0, 20):
answer_key = "answers_" + str(counter)
if args[0].get(answer_key, None) is not None:
answer_fields = counter + 1
else:
break
super(NewQuestionForm, self).__init__(*args, **kwargs)
if edit_mode:
self.fields['answers'] = AnswerField(num_widgets=answer_fields, required=False)
# get number of fields from DB
elif 'instance' in kwargs:
self.fields['answers'] = AnswerField(num_widgets=len(json.loads(self.instance.answers)), required=False)
else:
self.fields['answers'] = AnswerField(num_widgets=1, required=False)
class Meta:
model = Question
fields = ['question', 'answers']
widgets = {
'question': forms.TextInput(attrs={'class': "form-control"})
}
def clean_answers(self):
temp_data = []
for tdata in json.loads(self.cleaned_data['answers']):
if tdata != '':
temp_data.append(tdata)
if not temp_data:
raise forms.ValidationError('Please provide at least 1 answer.')
return json.dumps(temp_data)
'clean_answers' has 2 porposes: 1. Remove empty answers. 2. I failed to set required attribute on first widget. So i check here at least 1 answer exists
class AnswerWidget(forms.MultiWidget):
def decompress(self, value):
if value:
return json.loads(value)
else:
return ['']
class AnswerField(forms.MultiValueField):
def __init__(self, num_widgets, *args, **kwargs):
list_fields = []
list_widgets = []
for loop_counter in range(0, num_widgets):
list_fields.append(forms.CharField())
list_widgets.append(forms.TextInput(attrs={'class': "form-control"}))
self.widget = AnswerWidget(widgets=list_widgets)
super(AnswerField, self).__init__(fields=list_fields, *args, **kwargs)
def compress(self, data_list):
return json.dumps(data_list)

Create + Update with custom widget

I have created a custom widget that holds "SelecTimeWidget" & "SelectDateWidget"
It works fine when i'm creating a new event but when i turn it into a (UpdateView)
I get an error
global name 'to_current_timezone' is not defined
I don't know how to go about this to allow the widget to be used in the creation and edit of an event.
class EventUpdateView(UpdateView):
form_class = CreateEvent
model = Event
class EventCreateView(CreateView):
form_class = CreateEvent
model = Event
def form_valid(self, form):
Event = form.save(commit=False)
Event.created_by = self.request.user
Event.save()
return HttpResponseRedirect('/calendar/')
class CreateEvent(forms.ModelForm):
class Meta:
model = Event
fields = ['title', 'start', 'end', 'description', 'category']
widgets = {
'start': SelectDateTimeWidget(date_format= '%m/%m/%Y'),
'end': SelectDateTimeWidget(date_format= '%d/%m/%Y')
}
The Widget, obviously SelectTimeWidget is also a custom one but the error is leading me to "value = to_current_timezone(value)" this line within the code below
class SelectDateTimeWidget(forms.MultiWidget):
supports_microseconds = False
def __init__(self, attrs=None, date_format=None, time_format=None):
widgets = (SelectDateWidget(empty_label=( "Year", "Month", "Day")),
SelectTimeWidget(use_seconds=False))
super(SelectDateTimeWidget, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
value = to_current_timezone(value)
return [value.date(), value.time().replace(microsecond=0)]
return [None, None]
def subwidgets(self, name, value, attrs=None):
if self.is_localized:
for widget in self.widgets:
widget.is_localized = self.is_localized
# value is a list of values, each corresponding to a widget
# in self.widgets.
if not isinstance(value, list):
value = self.decompress(value)
output = []
final_attrs = self.build_attrs(attrs)
id_ = final_attrs.get('id')
for i, widget in enumerate(self.widgets):
try:
widget_value = value[i]
except IndexError:
widget_value = None
if id_:
final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
return output
Does Anyone know a way around this?
"from django.forms.util import to_current_timezone"
I did not realise To_current_timezone was an import, once i found this it worked :)
Love answering my own question after some time worrying about it

Custom widget not being marked safe

I'm creating a custom widget to display a choice field as a row of buttons.
So far, I've copied the code from the Django source for rendering a radio-choice field as my starting point:
#html_safe
#python_2_unicode_compatible
class ButtonInput(SubWidget):
input_type = 'radio'
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
self.value = force_text(self.value)
def __str__(self):
return self.render()
def render(self, name=None, value=None, attrs=None):
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', '')
class ButtonFieldRenderer(ChoiceFieldRenderer):
choice_input_class = ButtonInput
class ButtonSelect(RendererMixin, Select):
renderer = ButtonFieldRenderer
_empty_value = ''
My issue is that this code renders the correct HTML, but it's not marked safe - the HTML code renders onto the page. Given that this code is essentially copied straight from the Django source code, this is very surprising.
What is missing? How do I make my widget class html-safe?
Changing the strings in the render method for u'' worked for me
def render(self, name=None, value=None, attrs=None):
if self.id_for_label:
label_for = format_html(u' for="{}"', self.id_for_label)
else:
label_for = ''
attrs = dict(self.attrs, **attrs) if attrs else self.attrs
return format_html(
u'<label{}>{} {}</label>', label_for, self.tag(attrs), self.choice_label
)

django custom model field in admin form gives invalid choice error

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)

how to work with modelform and multiwidget

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.