I need to populate a CharField with the result of a set of checkboxes plus an "other" [please specify] option (the value would be 'option-a,other' for the form state below). The form can basically look like:
[x] option a
[ ] option b
[x] other
If you selected "other", please specify. [__________]
I've managed to get most of the way there by Implementing a subclass of MultipleChoiceField:
class CommaStringMultipleChoiceField(MultipleChoiceField):
def to_python(self, value):
return [val.rstrip().lstrip() for val in value.split(',')]
def clean(self, value):
return ",".join([val.rstrip().lstrip() for val in value])
In my form class, I assign that field to my form field:
TYPE_CHOICES = [
(u'option-a',u"Option A"),
(u'option-b',u"Option B"),
(u'other', u"Other"),
]
type = CommaStringMultipleChoiceField(
choices=TYPE_CHOICES,
widget=CheckboxSelectMultiple
)
This renders the form properly, and my custom clean() method is called, but when I save the form I get a validation error:
Value u'education,other' is not a valid choice.
I've played with adding a custom validator, but that hasn't made any difference so far. What am missing?
As the error only occurs once you save a model instance out of the form, check if the “Value u'education,other' is not a valid choice” error is coming from model validation instead of form validation. That can happen if you also have a choices set on the model field when you really want to store a free-form string there.
You will probably need to override the Validate method on your class. In the code at https://code.djangoproject.com/browser/django/trunk/django/forms/fields.py#L682 you'll see where it is raising the error message.
Related
I have some form views similar to account.voucher.receipt.dialog.form which is in the file:
/addons_path/account_voucher/voucher_payment_receipt_view.xml.
Some field tags get their default values which were defined in the Model,
Some field tags get their default values from on change methods (defined by on_change attributes).
I want to bypass these form views and automate the process, so I need to know in advance these default field values.
In that way, I only need to add additional field values if needed, then call the create method on the model.
I'm using Odoo v8.
How can I achieve that?
If you want to print in the log all the default values of your model you can do this:
from inspect import isfunction
#api.multi
def get_default_fields(self):
for key, value in self._fields.iteritems():
if value.name not in models.MAGIC_COLUMNS:
if self._defaults.get(value.name, False):
if isfunction(self._defaults[value.name]):
_logger.debug(self._defaults[value.name](
self, self.env.cr, self.env.uid, None
))
else:
_logger.debug(self._defaults[value.name])
I think you can adapt this code to your needs.
And if you want get the value of one field assigned by an onchange method maybe you have to run the method manually
I have subclassed a text-field form field in Django to create my own custom widget for a field. I was wondering if it's possible to check if all other fields of the form are valid (I want its server side behavior to vary based on the validation of other fields)
See comment
Something like:
class CustomField(TextInput):
def __init__(self, *args, **kwargs):
...
super(CustomField, self).__init__(*args, **kwargs)
input_type = 'hidden'
def value_from_datadict(self, data, files, name):
aws_file_key = data.get(name, None)
_media_bucket = boto.connect_s3(settings.AWS_ACCESS_KEY_ID,
settings.AWS_SECRET_ACCESS_KEY)\
.lookup(settings.AWS_MEDIA_STORAGE_BUCKET_NAME)
try:
key = _media_bucket.get_key(aws_file_key)
except:
print 'Failed to get key.'
key = None
if key and aws_file_key:
fh = tempfile.TemporaryFile()
key.get_contents_to_file(fh)
fh.seek(0)
files = SimpleUploadedFile(key.name, fh.read())
### IF FORM IS VALID DELETE KEY, OTHERWISE, KEEP IT.
if code_to_check_if_valid:
_media_bucket.delete_key(key)
fh.close()
return files
...... etc.
If you want to validate a certain field depending on the values of other fields, you need to to it at the form level and overwrite the field's clean method. Here's the docs on the subject - they are very good.
class CustomForm(forms.Form):
custom_field = CustomField()
def clean(self):
cleaned_data = super(CustomForm, self).clean()
custom_field = cleaned_data.get("custom_field")
...
If you look at the flow of how forms are validated, you will see that the clean method is run if all the other fields validate independently, so at this stage, the form can be considered valid:
These methods are run in the order given above, one field at a time. That is, for each field in the form (in the order they are declared in the form definition), the Field.clean() method (or its override) is run, then clean_<fieldname>(). Finally, once those two methods are run for every field, the Form.clean() method, or its override, is executed.
The final clean method is actually run regardless of if there's an error so you have to iterate through the cleaned_data to make sure there are no errors
The clean() method for the Form class or subclass is always run. If that method raises a ValidationError, cleaned_data will be an empty dictionary.
The previous paragraph means that if you are overriding Form.clean(), you should iterate through self.cleaned_data.items(), possibly considering the _errors dictionary attribute on the form as well. In this way, you will already know which fields have passed their individual validation requirements.
The clean methods for individual fields are called in the same order as the form declaration order or explicitly specified order. [django source code]
Although I wouldn't recommend this approach over using the clean method for multi-field validation, if your custom field is the last field in the order, you can expect self._errors to indicate whether all other fields have passed validation or not. However at this stage, non-field errors won't be available.
In Django/Python, when you make a custom form, does it need to have a clean() method, or will calling .is_valid() perform a default validation?
if request.method == 'POST':
filter = FilterForm(request.POST)
if filter.is_valid():
print 'Month is ' + filter.cleaned_data['month']
print 'Type is ' + filter.cleaned_data['type']
print 'Number is ' +filter.cleaned_data['number']
else:
print 'Invalid form'
print filter.errors
"Invalid Form" gets printed but no errors get printed.
class FilterForm(forms.Form):
months = [('January','January'),
('February','February'),
('March','March'),
('April','April'),
('May','May'),
('June','June'),
('July','July'),
('August','August'),
('September','September'),
('October','October'),
('November','November'),
('December','December'),]
types = [('text','Text'),
('call','Call'),]
month = forms.ChoiceField(months)
type = forms.ChoiceField(choices=types,widget=forms.CheckboxSelectMultiple)
def __init__(self,numbers,*args, **kwargs):
super(FilterForm,self).__init__(*args, **kwargs)
self.fields['number'] = forms.ChoiceField(choices=numbers)
def clean(self):
return self.cleaned_data
I've tried it with and without the clean method.
does it need to have a clean() method
No. Completely optional.
There's a big list of things that Django does in a specific order when it validates forms. You can learn about the process here:
http://docs.djangoproject.com/en/dev/ref/forms/validation/
As for finding your problem, if you stick a {{form.errors}} on your template, you'll see which field is blowing up. I have a feeling it could be that your choices is defined in a place that something can't get a handle on when it needs to (Move them out of the class).
Edit: Almost missed this. Look at this line:
def __init__(self,numbers,*args, **kwargs)
And then look at this line:
filter = FilterForm(request.POST)
You need to pass the numbers argument in this time too. It's a completely new instance. it can't validate because it doesn't know what numbers is.
If you have no specific clean method, Django will still validate all the fields in your form to ensure that all required fields are present, that they have the correct type where necessary, and that fields with choices have a value corresponding to one of the choices.
There are a couple of issues with this form that could be causing your problem. Firstly, you have overridden __init__ so that the first parameter after self is numbers. However, when you instantiate the form you don't pass this parameter - you just pass request.POST.
Secondly, it's a bad idea to add fields dynamically to self.fields as you do in __init__. Instead, declare your number field with an empty choices list and just set that in __init__:
self.fields['number'].choices = numbers
Basically I have a model with a ManyToMany field, and then a modelform derived from that model where that field is rendered as a "multiple choice" selectbox. In my template I'm having that field omitted, electing instead to prepare the values for that field in the view, then pass those prepared values into request.POST (actually a copy of request.POST because request.POST is immutable), then feeding request.POST to the form and then carry on as normal. I can't figure out how to do this, because request.POST isn't just a simple python dictionary, but instead a QueryDict, which behaves a little differently.
The field I need to populate is called "not_bases". When I create the widget using the form, it works perfectly well internally, but just not to my liking UI-wise. When I inspect the django-form submitted POST value via django's handy debug error window, the working QueryDict looks like this:
<QueryDict: {u'not_bases': [u'005', u'00AR', u'00F', u'00FD'], [...] }>
It appears the value for "not_bases" is a list, but it's not simply a list. I can't just .append() to it because it won't work. I dug around the documentation and found .update(), which appears to work, but doesn't. Here is my code:
newPOST = request.POST.copy()
for base in bases:
newPOST.update({"not_bases": base.identifier})
and here is the output:
<QueryDict: {u'not_bases': [u'KMER', u'KYIP'], u'reference': [u''], [...] }>
But when I feed that QueryDict to the form, I get an form validation error that says "not_bases: Enter a list of values.". Its obvious that the list-looking things coming from the str() representation of the QueryDict are not the same in the two cases above, even though they look exactly the same
So how do I do this?
It's really not clear what you're trying to do here, but I doubt that hacking the QueryDict is the right way to achieve it.
If you are trying to customise the display of the not_bases field, you can simply override the definition in your modelform declaration:
class MyModelForm(forms.ModelForm):
not_bases = forms.ChoiceField(choices=[(base, base) for base in bases])
class Meta:
model = MyModel
Or, if you simply want to avoid showing it on the form, you can exclude it from the form and set the value after validation.
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
exclude = ['not_bases']
....
if request.POST:
if form.is_valid():
instance = form.save(commit=False)
instance.not_bases = bases
instance.save()
Does either of these do what you want?
Thanks to Insin for answering a previous question related to this one.
His answer worked and works well, however, I'm perplexed at the provision of 'cleaned_data', or more precisely, how to use it?
class RegistrationFormPreview(FormPreview):
preview_template = 'workshops/workshop_register_preview.html'
form_template = 'workshops/workshop_register_form.html'
def done(self, request, cleaned_data):
# Do something with the cleaned_data, then redirect
# to a "success" page.
registration = Registration(cleaned_data)
registration.user = request.user
registration.save()
# an attempt to work with cleaned_data throws the error: TypeError
# int() argument must be a string or a number, not 'dict'
# obviously the fk are python objects(?) and not fk_id
# but how to proceed here in an easy way?
# the following works fine, however, it seems to be double handling the POST data
# which had already been processed in the django.formtools.preview.post_post
# method, and passed through to this 'done' method, which is designed to
# be overidden.
'''
form = self.form(request.POST) # instansiate the form with POST data
registration = form.save(commit=False) # save before adding the user
registration.user = request.user # add the user
registration.save() # and save.
'''
return HttpResponseRedirect('/register/success')
For quick reference, here's the contents of the post_post method:
def post_post(self, request):
"Validates the POST data. If valid, calls done(). Else, redisplays form."
f = self.form(request.POST, auto_id=AUTO_ID)
if f.is_valid():
if self.security_hash(request, f) != request.POST.get(self.unused_name('hash')):
return self.failed_hash(request) # Security hash failed.
return self.done(request, f.cleaned_data)
else:
return render_to_response(self.form_template,
{'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state},
context_instance=RequestContext(request))
I've never tried what you're doing here with a ModelForm before, but you might be able to use the ** operator to expand your cleaned_data dictionary into the keyword arguments expected for your Registration constructor:
registration = Registration (**cleaned_data)
The constructor to your model classes take keyword arguments that Django's Model meta class converts to instance-level attributes on the resulting object. The ** operator is a calling convention that tells Python to expand your dictionary into those keyword arguments.
In other words...
What you're doing currently is tantamount to this:
registration = Registration ({'key':'value', ...})
Which is not what you want because the constructor expects keyword arguments as opposed to a dictionary that contains your keyword arguments.
What you want to be doing is this
registration = Registration (key='value', ...)
Which is analogous to this:
registration = Registration (**{'key':'value', ...})
Again, I've never tried it, but it seems like it would work as long as you aren't doing anything fancy with your form, such as adding new attributes to it that aren't expected by your Registration constructor. In that case you'd likely have to modify the items in the cleaned_data dictionary prior to doing this.
It does seem like you're losing out on some of the functionality inherent in ModelForms by going through the form preview utility, though. Perhaps you should take your use case to the Django mailing list and see if there's a potential enhancement to this API that could make it work better with ModelForms.
Edit
Short of what I've described above, you can always just extract the fields from your cleaned_data dictionary "by hand" and pass those into your Registration constructor too, but with the caveat that you have to remember to update this code as you add new fields to your model.
registration = Registration (
x=cleaned_data['x'],
y=cleaned_data['y'],
z=cleaned_data['z'],
...
)