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()
Related
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).
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.
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.
I have a fairly simple Django application (v1.3 on Red Hat) for which I'm using the admin application to create and modify database records. One of the fields in my underlying model is a date field. Each time the corresponding field is displayed in the admin's new or edit form I'd like the initial value of this field to be today's date (and time). The user may choose to modify it thereafter, if she desires.
I know that I can set the default field value within my model definition (i.e. in models.py). Which works fine when a database record is first created. But for subsequent invocations of the change form the callable that I've assigned to the default parameter (datetime.datetime.now) obviously doesn't get invoked.
I've looked at - and tried - pretty well all of the many proposed solutions described elsewhere in stackoverflow, without success. Most of these appear to revolve around inserting initialisation code into the ModelForm subclass, e.g. either something like this...
class ConstantDefAdminForm(ModelForm) :
a_date_field = DateField(initial="datetime.datetime.now") # or now()
class Meta :
model = ConstantDef
widgets = {
...
}
or something like this...
class ConstantDefAdminForm(ModelForm) :
class Meta :
model = ConstantDef
widgets = {
...
}
def __init__(self, ...) :
# some initialisation of a_date_field
super(ConstantDefAdminForm, self).__init__(...)
But neither of these approaches work. The initial field value is always set to the value that is stored in the database. My reading of the Django documentation is that the various ways of imposing initial field values in forms only work for unbound forms, not bound forms. Right?
But this capability (to selectively override currently stored values) would seem to be such a popular requirement that I'm convinced that there must be a way to do it.
Has anyone out there succeeded in doing this?
Thanks in advance,
Phil
In Django 1.4 the default=<callable> in model's declaration works well:
class MyModel(models.Model):
dt = models.TimeField(null=True, blank=True, default=datetime.datetime.now)
every time you add a record the default value of the field is updated.
But the use the field's default parameter cause me some problem with the Admin log history of DateField objects, that are every time recorded as changed also when they are not modified. So I've adopted a solution based on https://stackoverflow.com/a/11145346/1838607:
import datetime
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyModelAdminForm, self).__init__(*args, **kwargs)
self.fields['dt'].initial = datetime.datetime.now
class MyModelAdmin(admin.ModelAdmin):
form = MyModelAdminForm
fields = ('dt',)
Here's an approach that might work. In your model admin class, change the value of obj.a_date_field before the form is bound. The 'default' value for the date field should be the new value.
class MyModelAdmin(ModelAdmin):
...
def get_object(self, request, object_id):
obj = super(MyModelAdmin, self).get_object(request, object_id)
if obj is not None:
obj.a_date_field = datetime.now()
return obj
Note that get_object is not documented, so this is a bit hacky.
I had a similar problem, and I found the solution from here
I think what you will want to do is this:
class yourAdminModel(admin.ModelAdmin):
fields = ['your_date_field']
def add_view(self, request, form_url="", extra_context=None):
data = request.GET.copy()
data['your_date_field'] = datetime.date.today() # or whatever u need
request.GET = data
return super(yourAdminModel, self).add_view(request, form_url="", extra_context=extra_context)
You should be able to use auto_now with your DateTime Field which according to the docs will automatically set the value to now() each time the form is saved
Since Django 1.7 there is a function get_changeform_initial_data in ModelAdmin that sets initial form values:
def get_changeform_initial_data(self, request):
return {'dt': datetime.now()}
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