KeyError when setting field specific error on def clean() - django

I've got a multiple choice field in my form.
paymentoption = forms.MultipleChoiceField(required=False, widget=forms.CheckboxSelectMultiple)
def __init__(self, *args, **kwargs):
super(BasisOfPricingForm, self).__init__(*args, **kwargs)
self.fields['paymentoption'].choices = [(t.id, t.title) for t in PaymentOption.objects.all()]
def clean(self):
cleaned_data = super(BasisOfPricingForm, self).clean()
paymentoption = cleaned_data.get('paymentoption')
if paymethod1 == 3 and len(paymentoption) == 0:
self.add_error('paymentoption', 'You must select at lease one Offline Payment Option if users must pay in person or over the phone.')
When the conditions of the error are met, I get a "KeyError" on field ''paymentoption'.

I will give you a couple of hints:
First, most of the time you don't have (and don't want to mess with the form's initializer)
Instead of initializing the values of your paymentoption choices in the form's __init__ is better to create a CustomMultipleChoiceFiled for that matter.
For example:
class PaymentOptions(MultipleChoiceField):
def __init__(self, choices=None, **kwargs):
super(PaymentOptions, self).__init__(choices=choices, **kwargs)
if choices is None:
self.choices = # ...
then you can use it without messing around with the Form's __init__.
Second, do not reimplement clean if not absolutely necessary you can add a method called clean_paymentoption and perform there all the necessary validation.
You can read about this in the documentation:
The clean_<fieldname>() method is called on a form subclass – where <fieldname> is replaced with the name of the form field attribute. This method does any cleaning that is specific to that particular attribute, unrelated to the type of field that it is. This method is not passed any parameters. You will need to look up the value of the field in self.cleaned_data and remember that it will be a Python object at this point, not the original string submitted in the form (it will be in cleaned_data because the general field clean() method, above, has already cleaned the data once).
For example, if you wanted to validate that the contents of a CharField called serialnumber was unique, clean_serialnumber() would be the right place to do this. You don’t need a specific field (it’s just a CharField), but you want a formfield-specific piece of validation and, possibly, cleaning/normalizing the data.
The return value of this method replaces the existing value in cleaned_data, so it must be the field’s value from cleaned_data (even if this method didn’t change it) or a new cleaned value.
In your case it could be:
def clean_paymentoption(self):
paymentoption = self.cleaned_data.get('paymentoption', None)
if paymethod1 == 3 and len(paymentoption) == 0:
self.add_error('paymentoption', 'You must select at lease one Offline Payment Option if users must pay in person or over the phone.')
And again you avoid another super call, and super calls are a good thing to avoid when you start to get errors hard to track like this you just posted here.

Related

Django formset save() writes new object on database?

So I have MyModel:
class MyModel(models.Model):
name = models.CharField(_('Name'), max_length=80, unique=True)
...
def save(self, *args, **kwargs):
if MyModel.objects.count() >= 5:
raise ValidationError("Can not have more than 5 MyModels!")
super().save(*args, **kwargs) # Call the "real" save() method.
There are already 5 objects from MyModel on the database.
I have a page where I can edit them all at the same time with a formset.
When I change one or more of them, I will get the Validation Error "Can not have more than 5 MyModels!".
Why is this happenning? I tought the formset was supposed to edit the existing objects, but it appears to be writing a new one and deleting the old one.
What is really happening when I do a formset.save() on the database?
Do I have to remove the save() method?
The save method inside the Model is called regardless you are creating or editing. Although you can distinguish between them by checking if the object has a primary key, like this:
if not self.pk and MyModel.objects.count() >= 5:
If you want more sophisticated control over validation, I suggest putting them in the forms. Specially if you want to limit the number of formset, you can check this documentation.

Access Django ModelChoiceField form field value inside __init__

I would like to make the certain field in my formset read-only once the value has been chosen. Kind of like this:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.fields["my_field"].value is not None:
self.fields["my_field"].disabled == True
Except ModelChoiceField object doesn't have an argument value and I am not even sure __init__ it is the right place to try access value of a formset form's field.
UPDATE
if self["my_field"].value() is not None:
self.fields["my_field"].disabled = True
does the trick. But reveals another issue. When I change the other field's value in this form and try and save it, I get This field is required. error on the disabled field. Does disabled argument have any effect on the fields value in form.is_valid? Because according to man Even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data. And initial data is actually None. I guess I need to find another way to make the field read-only (like simply ignore the change in the post).

Don't require field based on another field's value

I have a Django ModelForm where I want one field to be treated as conditionally required. I want it to be required by default, but not required depending on the value of another field. My model looks something along the lines of:
class MyModel(models.Model):
foo = models.BooleanField()
bar = models.CharField(choices=(('A', 'A'),('B', 'B'),('C', 'C')),)
Neither have null=True or blank=True so both appear as "required" when I render my form, which is what I want.
However, I have some javascript on my form template that hides the bar field input depending on the value of the input for foo. In this case, when the form is submitted, I don't want to have any validation errors about bar being required.
I am trying to implement this logic in my ModelForm's clean method, like so:
def clean(self):
data = super(MyModelForm, self).clean()
if data.get('foo') == True and 'bar' in self.errors:
del self.errors['bar']
return data
However, this still seems to give me a "This field cannot be blank." error. I've noticed that my ModelForm also has an _errors dict, but if I try adding del self._errors['bar'] I end up with an exception.
I have found some ways to do the opposite of what I want(1, 2), which is have the field not be required by default and then check it should be required in some cases in clean, but this is not what I want.
I've also tried adding data['bar'] = " " as a workaround but then I get an error about not choosing one of the choices. Ideally, I'd rather just be able to save the empty string.
The issue here is that in addition to the form's clean method being called, the form also calls post_clean on the form's instance. Because the field on the model does not have blank=True, when the model is cleaned another error is raised and it gets passed back to the form.
To resolve, set blank=True on the model. This has the unwanted side effect of making the field appear as not required on the form. To make the field required on the form, simply set the required flag in init:
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__( *args, **kwargs)
self.fields['bar'].required = True
This, in addition to the clean method in the original question, is enough to create the conditionally required field that is required by default and not-required based on the value of another field.
You can override the _clean_fields method in order to provide your conditional changes:
def _clean_fields(self):
if self.data.get('foo', None) == True:
self.fields['bar'].required = False
super(MyForm, self)._clean_fields()

defining the value of a field in the view before validating a form

I have a form with a field "status" that will be set by the view depending on the value of some fields of my form
is it possible to do that BEFORE form.is_valid() (otherwise the form won't be valid)?
the only way I can think of doing that is allowing the "status" field to be blank, but that doesn't seem good design
defining a clean() method that set those fields is a better solution?
If the status field isn't intended be set by the user filling out the form, it probably shouldn't be in the form at all.
If that value is later used in creating/updating a model instance or something, then it would be better to just include the value determined by your view in the form's save method, like form.save(status=my_status) (if it's a ModelForm) or when you're instantiating your model, otherwise.
If the user does enter the status, then you can just have a custom clean() method that ensures it is an acceptable value.
Then you should init form with this status value and then hide field.
class StatusForm(forms.ModelForm):
def __init__(self, *arg, **kwargs):
super(StatusForm, self).__init__(*arg, **kwargs)
# set needed value here and hide field

fields and base_fields - Django

When creating a flatpage, I want the user to select a template from a predefined list. In order to keep the Flatpage model untouched, I prefer ChoiceField over ModelChoiceField (the latter provides the PK of the template, but I need the name for the template_name field):
class NewFlatpageForm(FlatpageForm):
template_name = forms.ChoiceField(choices = [])
def __init__(self, *args, **kwargs):
self.base_fields['template_name'].choices = ProjectTemplate.objects.values_list('path', 'name')
super(NewFlatpageForm, self).__init__(*args, **kwargs)
I override __init__ or Django populates choices at server start and does not update the list then.
I don't have any admin experience, but I did similar things using the fields attribute when not using admin. However in this case, I got an exception telling fields is not an attribute of the form. __dict__ showed me there's a base_fields attribute and using it works. So, why use base_fields here, and why is fields not present and finally am I doing something hacky?
fields doesn't exist until after you've called super. So just swap the order of the lines, so that super comes first.
A lesson from my own experience: modifying base_fields means that your modifications stick around "forever" (until python exits). In your case, that's probably not a problem, as you are always using the same field name, and you are replacing its values with the assignment from ProjectTemplate...
In my case, I wanted completely different fields based on parameters in the constructor. Since my field names were usually different, each time I instantiated a form, I added new fields but didn't eliminate the ones from the last time.
By calling super early (as indicated here) and then making my dynamic changes to self.fields instead of self.base_fields, I was able to eliminate the problem of an ever growing list of fields. It makes perfect sense now, but I wasn't familiar with all of the syntax details and was hacking through instead of trying to understand it first.
In addition to Joe Germuska. If you truly need to change the form based on the request, you can use a deepcopy to make sure nothing is changed by reference:
def get_form(self, request, obj=None, **kwargs):
form = super(ResourceAdmin, self).get_form(request, obj, **kwargs)
form = copy.deepcopy(form)
if obj:
form.base_fields['email'] = EmailField(initial=obj.user.email)
if not request.user.is_superuser:
form.base_fields['user'].widget = HiddenInput(attrs={'class': 'hide_form_row'})
return form