I can have custom validators for my django models and what I would like to do is perform validation at the form level where the form elements have dependencies with each other. To illustrate, say I have the following model:
class MyModel(models.Model):
num_average = models.IntegerField(verbose_name='Number of averages',
default=1)
num_values = models.IntegerField(verbose_name='Number of values',
default=3)
The dependency is that num_values = num_average * 3. I know I can set this automatically but for this purposes let us assume we want the user input. I have a form as:
class MyForm(ModelForm):
class Meta:
model = MyModel
fields = ['num_average', 'num_values']
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
Is there a way to validate the form as a whole before the submit gets triggered?
Yes, as the form docs point out, this kind of thing is done in a clean method.
class MyForm(ModelForm):
class Meta:
model = MyModel
fields = ['num_average', 'num_values']
def clean(self):
data = self.cleaned_data
if data['num_values'] != data['num_average'] *3:
raise forms.ValidationError('values must be three times average')
As an aside, you shouldn't define __init__ if you're not doing anything with it; overriding a method just to call the superclass method is pointless.
Related
Imagine having a simple model like the one bellow:
from utils.validators import name_validator
class Customer(models.Model):
name = models.CharField(verbose_name="Customer Name", validators=[name_validator])
email = models.EmailField(verbose_name="Customer Email")
def __str__(self):
return self.name
Now if I explicitly define a filed on my serializer, both validators and verbose_name are lost. I can use label= and validatos= when defining the field on my serializer but I don't want to repeat myself. What if I have multiple serializer pointing to the same Model?
class CustomerSerilizer(serializers.ModelSerializer):
custom_field_name = serializers.CharField(source="name")
class Meta:
model = Customer
fields = "__all__"
Is there anyway to prevent this from happening?
I'm not sure if it's the perfect way of doing this or not, but I managed to achieve my desired behavior by writing a custom ModelSerializer which sets label and validators if they are not being passed when explicitly defining a field on the serializer.
class CustomModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(CustomModelSerializer, self).__init__(*args, **kwargs)
model = self.Meta.model
model_fields = [f.name for f in model._meta.get_fields()]
for field_name, field_instance in self.fields.items():
source_field = field_instance.source
if source_field in model_fields:
model_field = model._meta.get_field(source_field)
if "label" not in field_instance._kwargs:
field_instance.label = model_field.verbose_name
if "validators" not in field_instance._kwargs:
field_instance.validators.extend(model_field.validators)
So, I have created a model which has some attributes, but I want to focus on these two.
class Profile(models.Model):
RANK_OPTIONS = (
('A', 'A'),
('B', 'B'),
)
related_office = models.ForeignKey('OtherModule.office',
related_name='office',
on_delete=models.CASCADE
)
rank = models.CharField(('rank'),max_length=1, choices=RANK_OPTIONS)
Basically, I need these two attributes to be optional, but the moment the attribute "Rank" is filled, then the attribute related_office must also be filled.
You can have a related_office and not a Rank, but the moment you have a Rank, you also need a related Office.
how can I do that?
You can just override the clean method in your model.py:
def clean(self):
if self.rank and not self.related_office:
raise forms.ValidationError({'related_office':["related_office is required!"]})
You can enforce such condition by overriding the save method:
def save(self, *args, **kwargs):
if self.rank and not self.related_office:
raise ValueError("related_office is required!")
super().save(*args, **kwargs)
I would not do this in admin save method. If its in your admin, write a ModelForm and pass it to your admin class, like this:
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = "__all__"
def clean(self):
cleaned_data = super().clean()
if cleaned_data.get("rank") and not cleaned_data.get("related_office"):
raise ValidationError('You have to select your related office for your rank')
return cleaned_data
and reference that form in your admin class:
class ProfileAdmin(admin.ModelAdmin):
form = ProfileForm
If you have your custom html template, either validate it in frontend, or in backend. Just like in the example above
I have a simple model form what I use through the admin interface. Some of my model fields store datas that require a bit more time to calculate (they come from other sites). So I decided to put an extra boolean field to the form to decide to crawl these datas again or not.
class MyModelForm(forms.ModelForm):
update_values = forms.BooleanField(required=False) #this field has no model field
class Meta:
model = MyModel
This extra field doesn't exist in the model because only the form needs it.
The problem is that I only want it to appear if it's an existing record in the database.
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
if self.instance.pk is None:
#remove that field somehow
I tried nearly everything. Exclude it, delete the variable but nothing wants to work. I also tried dynamically add the field if self.instance.pk is exists but that didn't work too.
Any idea how to do the trick?
Thanks for your answers.
You could subclass the form and add the extra field in the subclass:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
class MyUpdateModelForm(MyModelForm):
update_values = forms.BooleanField(required=False) #this field has no model field
class Meta:
model = MyModel
You can then override the get_form method of your admin, which is passed the current instance: get_form(self, request, obj=None, **kwargs)
Rather than removing the field in __init__ if instance.pk is not None, how about adding it if it is None? Remove the class-level declaration and just change the logic:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
if self.instance and self.instance.pk is not None:
self.fields['update_values'] = forms.BooleanField(required=False)
Is there any way to load different admin forms for editing an objects depending of what object is needed to be updated?
For example - we have an MPTTModelAdmin objects. And for root objects we don't want to see some fields:
class RootObjectForm(ModelForm):
class Meta:
model = Author
exclude = ('title',)
class ChildObjectForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'birth_date')
But I don't know how to get object fields in forms.py or admin.py.
You can always supply your own form class for a ModelAdmin class: https://docs.djangoproject.com/en/1.5/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form
From there you can access fields by key, just like any other Django form:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyModeForm, self).__init__(*args, **kwargs)
# access whatever field by key
# self.fields['field-name']
In forms.py file you can get object fields and their value at two stages.
1 : when form is submitted.
clean method does initial validations.
def clean(self):
""" validation of address form """
cleaned_data = super(WebsiteAddressForm, self).clean()
field1_value = self.cleaned_data.get("field1")
print field1_value
return cleaned_data
2 : when form is initialized. ____init____ method will call.
class MyForm(forms.ModelForm):
class Meta:
model = Model1
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
#self.fields['field1']
I'm using Django profiles and was inspired by James Bennett to create a dynamic form (http://www.b-list.org/weblog/2008/nov/09/dynamic-forms/ )
What I need is a company field that only shows up on my user profile form when the user_type is 'pro'.
Basically my model and form look like:
class UserProfile(models.Model):
user_type = models.CharField(...
company_name = models.CharField(...
class UserProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
exclude = ('company_name',)
And I add the company_name field in init like James Bennett showed:
def __init__(self, *args, **kwargs):
super(UserProfileForm, self).__init__(*args,**kwargs)
if (self.instance.pk is None) or (self.instance.user_type == 'pro'):
self.fields['company_name'] = forms.CharField(...
The problem is that, when I try to save() an instance of UserProfileForm, the field 'company_name' is not saved...
I have gone around this by calling the field explicitly in the save() method:
def save(self, commit=True):
upf = super(UserProfileForm, self).save(commit=False)
if 'company_name' in self.fields:
upf.company_name = self.cleaned_data['company_name']
if commit:
upf.save()
return upf
But I am not happy with this solution (what if there was more fields ? what with Django's beauty ? etc.). It kept me up at night trying to make the modelform aware of the new company_name field at init .
And that's the story of how I ended up on stackoverflow posting this...
I would remove this logic from form and move it to factory. If your logic is in factory, you can have two forms:
UserProfileForm
ProUserProfileForm
ProUserProfileForm inherits from UserProfileForm and changes only "exclude" constant.
You will have then following factory:
def user_profile_form_factory(*args, instance=None, **kwargs):
if (self.instance.pk is None) or (self.instance.user_type == 'pro'):
cls = ProUserProfileForm
else:
cls = UserProfileForm
return cls(*args, instance, **kwargs)
It seems I found a solution:
def AccountFormCreator(p_fields):
class AccountForm(forms.ModelForm):
class Meta:
model = User
fields = p_fields
widgets = {
'photo': ImageWidget()
}
return AccountForm
#...
AccountForm = AccountFormCreator( ('email', 'first_name', 'last_name', 'photo', 'region') )
if request.POST.get('acforms', False):
acform = AccountForm(request.POST, request.FILES, instance=request.u)
if acform.is_valid():
u = acform.save()
u.save()
ac_saved = True
else:
acform = AccountForm(instance = request.u)
When are you expecting the user_type property to be set? This seems like something that should be handled by javascript rather than trying to do funny things with the model form.
If you want the company_name field to appear on the client after they've designated themselves as a pro, then you can 'unhide' the field using javascript.
If instead, they've already been designated a pro user, then use another form that includes the company_name field. You can sub-class the original model form in the following manner.
class UserProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
exclude = ('company_name',)
class UserProfileProForm(UserProfileForm):
class Meta:
exclude = None # or maybe tuple() you should test it
Then in your view, you can decide which form to render:
def display_profile_view(request):
if user.get_profile().user_type == 'Pro':
display_form = UserProfileProForm()
else:
display_form = UserProfileForm()
return render_to_response('profile.html', {'form':display_form}, request_context=...)
This would be the preferred way to do it in my opinion. It doesn't rely on anything fancy. There is very little code duplication. It is clear, and expected.
Edit: (The below proposed solution does NOT work)
You could try changing the exclude of the meta class, and hope that it uses the instances version of exclude when trying to determine whether to include the field or not. Given an instance of a form:
def __init__(self, *args, **kwargs):
if self.instance.user_type == 'pro':
self._meta.exclude = None
Not sure if that will work or not. I believe that the _meta field is what is used after instantiation, but I haven't verified this. If it doesn't work, try reversing the situation.
def __init__(self, *args, **kwargs):
if self.instance.user_type != 'pro':
self._meta.exclude = ('company_name',)
And remove the exclude fields altogether in the model form declaration. The reason I mention this alternative, is because it looks like the meta class (python sense of Meta Class) will exclude the field even before the __init__ function is called. But if you declare the field to be excluded afterwards, it will exist but not be rendered.. maybe. I'm not 100% with my python Meta Class knowledge. Best of luck.
What about removing exclude = ('company_name',) from Meta class? I'd think that it is the reason why save() doesn't save company_name field