Access Django ModelChoiceField form field value inside __init__ - django

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).

Related

KeyError when setting field specific error on def clean()

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.

Django model forms - disabled fields in changed_data

I have a form base class which checks if the instance the form is updating has changed, and does not save if it has not changed.
this is in my custom model form, I override save:
class MyModelForm(models.ModelForm):
# .. more code here..
def save(self, commit=True):
if self.has_changed():
# Won't do anything if the instance did not changed
return self.instance
return super(MyModelForm, self).save(commit)
A LOT of my forms use this base class.
Now, one of my forms have a few fields which I set to disabled=True (django 1.9 +). So in one of my forms:
def __init__(self, *args, **kwargs):
## ..code
self.fields['address'].disabled = True
After a lot of debugging why the form.has_changed() is True (hence the instance is saved for no reason), even when I save the form without changing the instance. I've found out that django includes disabled fields in changed_data - which makes no sense, as disabled fields should not be altered by the user anyway.
Am I missing something or it is a bug, or maybe that how it should work?
How can I resolve this without too much changes, as the form base class is used a lot in my code.
This is a known issue with DjangoProject with the ticket at https://code.djangoproject.com/ticket/27431 and the corresponding PR at https://github.com/django/django/pull/7502. As this answer is being written the PR is merged with master so the latest version should have this fixed.
A workaround this is as follows
for form in formset:
if form.has_changed() and form not in formset.deleted_forms:
fields = form.changed_data
up_f = [field for field in fields if not form.fields[field].disabled]
if len(up_f) > 0:
updated_data.append(form.cleaned_data)
This results in updated_data having the only forms that are updated and not deleted.

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()

When subclassing a model field, __init__'s arguments are not processed

I have a subclass of models.ForeignKey, the only purpose of which is to use a custom widget:
from django.db import models
from .models import Images
class GAEImageField(models.ForeignKey):
def __init__(self, **kwargs):
super(GAEImageField, self).__init__(Images, **kwargs)
def formfield(self, *args, **kwargs):
field = forms.ModelChoiceField(self, widget=ImageUploadWidget, *args, **kwargs)
field.queryset = Images.objects.all()
return field
The problem is, when I try to use this field, any parameters to __init__ are ignored. For instance, if I try this model:
class SomethingWithImage(models.Model):
name = models.CharField('Awesome name', max_length=64)
image = GAEImageField(verbose_name='Awesome image', blank=True)
...despite the fact I specified verbose_name, the label on a generated form will be "Image" and trying to specify empty value will raise an error even though I use blank=True
Well, the problem is with your formfield method. If you look at the implementation used in the default ForeignKey for example, you'll see it calls super. I'd recommend something like this:
def formfield(self, **kwargs):
defaults = {
'widget': ImageUploadWidget,
}
defaults.update(kwargs)
return super(GAEImageField, self).formfield(**defaults)
The problem is, form fields don't extract any information from model fields. Form fields can be used completely independently from models, they don't have to be necessarily backed by a model field. That means all settings, like whether the field is required or not, its label etc. have to be passed as parameters to their constructors. It is the responsibility of the model field to create an appropriate instance of a form field, all settings included. The default implementation of django.db.models.fields.Field.formfield takes care of that which is why you usually want to call the parent's method.
As for the blank issue, try also setting null=True, otherwise even though the form will accept a blank value, the database will reject it. Also, note that modifying the value of null after syncdb has been run requires a database migration.

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