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 ''
Related
This is the problem: I have a serializer field pointing to another serializer. I sending an allright request to server and I get this response:
{
"purchase_header": [
"This field is required."
]
}
But the field was sended by POST request (in debugger I can see it).
I finded this code in html.py (from DRF):
def parse_html_dict(dictionary, prefix=''):
"""
Used to support dictionary values in HTML forms.
{
'profile.username': 'example',
'profile.email': 'example#example.com',
}
-->
{
'profile': {
'username': 'example',
'email': 'example#example.com'
}
}
"""
ret = MultiValueDict()
regex = re.compile(r'^%s\.(.+)$' % re.escape(prefix))
for field in dictionary:
match = regex.match(field)
if not match:
continue
key = match.groups()[0]
value = dictionary.getlist(field)
ret.setlist(key, value)
return ret
Well, when debuging I can see that prefix variable contain "purchase_header" and field variable contain the same "purchase_header" string but de match fail returning None. Very rare... dictionary parameter contain all keys and values.
But If you can helpme I appreciate much.
This is the rest of significant code:
urls.py
router = DefaultRouter()
router.register(r'cancel-requests', PurchaseCancellationsRequestViewSet,
basename='purchase-cancel-requests')
router.register(r'invoices', PurchaseViewSet, basename='purchase')
urlpatterns = router.urls
urlpatterns += [path('payment-headers/', PaymentHeadersListAPIView.as_view(
), name='PaymentHeadersListAPIView')]
api.py
class PurchaseViewSet(viewsets.ModelViewSet):
"""
A viewset for viewing and editing purchases instances.
"""
serializer_class = PurchaseSerializer
queryset = Purchase.objects.all().order_by('-created_at')
permission_classes = (IsAuthenticated,)
serializer_action_classes = {
'list': PurchaseSerializer,
'create': PurchaseSerializer,
'retrieve': PurchaseSerializer,
'update': PurchaseSerializer,
'partial_update': PurchaseSerializer,
'destroy': PurchaseSerializer
}
def get_serializer_class(self):
try:
return self.serializer_action_classes[self.action]
except (KeyError, AttributeError):
return super().get_serializer_class()
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs["context"] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
def get_list_queryset(self):
try:
store = Store.objects.get(my_owner=self.request.user)
except EmptyResultSet as e:
print("Data Error: {0}".format(e))
urlquery = self.request.GET.get('cancelled', '')
cancelled = False
if urlquery == 'true':
cancelled = True
elif urlquery == '':
# return all
return Purchase.objects.filter(created_for=store)
return Purchase.objects.filter(cancelled=cancelled, created_for=store)
def put(self, request, *args, **kwargs):
if request.user.is_seller:
raise PermissionDenied(
detail='Only owners can cancellate Purchases')
# seller create invoice cancellation request
return self.update(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
request.data['created_by'] = request.user.pk
print(request.data)
if request.user.is_owner is True:
try:
request.data['created_for'] = Store.objects.get(
my_owner=request.user).pk
except Exception as e:
print(e)
return Response(data='User has no Store associated',
status=status.HTTP_400_BAD_REQUEST)
else:
try:
request.data['created_for'] = request.user.seller_profile \
.my_store.pk
except Exception as e:
print(e)
return Response(data='User has no Store associated',
status=status.HTTP_400_BAD_REQUEST)
return self.create(request, *args, **kwargs)
def list(self, request, *args, **kwargs):
# TODO: date ranges
if self.request.user.is_seller:
# TODO: Change -> Indeed the seller can get his own created
# purchases and refactor this to method
raise PermissionDenied(
detail='Only owners can get purchase list.')
queryset = self.get_list_queryset()
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
serializers.py
from rest_framework import serializers
from .models import Purchase, PurchaseCancellationRequest, PurchaseHeader
class PurchaseHeaderSerializer(serializers.ModelSerializer):
class Meta:
model = PurchaseHeader
fields = ['id', 'name']
extra = {
'id': {'read_only': True},
'name': {'read_only': True}
}
class PurchaseSerializer(serializers.ModelSerializer):
purchase_header = PurchaseHeaderSerializer()
class Meta:
model = Purchase
fields = ['id', 'cancelled', 'created_at', 'created_by',
'created_for', 'pic', 'total_amount', 'purchase_header']
extra = {
'cancelled': {'read_only': True},
'created_at': {'read_only': True},
'id': {'read_only': True}
}
def validate_total_amount(self, value):
"""
Sanitize the amount value.
:return: total_amount as float
"""
total_amount = None
try:
total_amount = float(value)
except Exception as e:
print(e)
if total_amount:
return value
else:
raise serializers.ValidationError('Exists an error with '
'total_amount value, probably '
'an empty param or bad float '
'format')
def validate_cancelled(self, value):
"""
Sanitize the cancelled value.
:return: cancelled as python boolean
"""
cancelled = None
try:
cancelled = False if value == 'false' else True
except Exception as e:
print(e)
if cancelled:
return value
else:
raise serializers.ValidationError('Exists an error with '
'cancelled value, probably an '
'empty param or bad boolean '
'value')
def create(self, validated_data):
payment_header_pk = validated_data.pop('purchase_header')
payment_header_obj = PaymentHeader.objects.get(pk=payment_header_pk)
purchase_obj = Purchase.objects.create(
payment_header=payment_header_obj, **validated_data)
return purchase_obj
class PurchaseCancellationRequestSerializer(serializers.ModelSerializer):
class Meta:
model = PurchaseCancellationRequest
fields = ['purchase']
models.py
class PurchaseHeader(SoftDeleteModel):
name = models.CharField(max_length=50)
city = models.ManyToManyField(City, through='purchases.ProviderCity')
created_at = models.DateTimeField(default=timezone.now(), editable=False)
modified_at = models.DateTimeField(default=timezone.now())
class ProviderCity(SoftDeleteModel):
city = models.ForeignKey(City, on_delete=models.PROTECT)
purchase_header = models.ForeignKey(PurchaseHeader,
on_delete=models.PROTECT)
class Meta:
unique_together = ('city', 'purchase_header')
Thank for all friends!!
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)
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 was trying to dynamically generate fields as shown in http://jacobian.org/writing/dynamic-form-generation/. My case slightly differs in that I am looking to use multiplechoicefield that is dynamically created. This is what I came up with...
views.py
def browseget(request):
success = False
if request.method == 'POST':
list_form = ListForm(request.POST)
if list_form.is_valid():
success = True
path = list_form.cleaned_data['path']
minimum_size = list_form.cleaned_data['minimum_size']
follow_link = list_form.cleaned_data['follow_link']
checkboxes = list_form.cleaned_data['checkboxes']
....do something
else:
list_form = ListForm(name_list)
ctx = {'success': success, 'list_form': list_form, 'path': path, 'minimum_size': minimum_size}
return render_to_response('photoget/browseget.html', ctx, context_instance=RequestContext(request))
forms.py
class ListForm(forms.Form):
path = forms.CharField(required=False)
minimum_size = forms.ChoiceField(choices=size_choices)
follow_link = forms.BooleanField(required=False, initial=True)
def __init__(self, *args, **kwargs):
name_list = kwargs.pop('name_list', None)
super(ListForm, self).__init__(*args, **kwargs)
print 'Received data:', self.data
if name_list:
name_choices = [(u, u) for u in name_list]
self.fields['checkboxes'] = forms.MultipleChoiceField(required=False, label='Select Name(s):', widget=forms.CheckboxSelectMultiple(), choices=name_choices)
def clean_path(self):
cd = self.cleaned_data
path = cd.get('path')
if path == '': path = None
return path
def clean_minimum_size(self):
cd = self.cleaned_data
minimum_size = cd.get('minimum_size')
if minimum_size is None: minimum_size = 0
return int(minimum_size)
The form generates and displays perfectly... until I post some data. The 'checkboxes' field doesn't show up in list_form.cleaned_data.items() while it shows in self.data. As it is the form breaks with a KeyError exception. So Im asking, how do i access the checkboxes data?
You're not passing in the name_list parameter when you re-instantiate the form on POST, so the field is not created because if name_list is False.
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.