unit testing django ModelFormset clean methods - python

What is the best way to unit test the validation/clean part of a Django ModelFormset? My formset has a clean method that does some validation based on the values of its forms and I want to have a unit test for it. Generified code look like this:
class AForm(ModelForm):
a = ChoiceField(choices=CHOICES)
b = FloatField()
def __init__(self, *args, **kwargs):
super(AForm, self).__init__(*args, **kwargs)
class Meta:
model = AModel
fields =['a', 'b']
class AFormset(BaseInlineFormSet):
def clean(self):
# Some logic to validate relationships between the forms
But while testing the form is trivial: form = AForm(formdata) and then verifying its validity or errors based on the data. I'm having trouble writing tests for the formset.
I've tried:
formset = AFormset()
And using modelformset_factory
Formset = modelformset_factory(AModel, AForm, formset=AFormset, fields=('a', 'b'))
formset = Formset(formdata)
with various combinations of arguments and mocks (instance, queryset). But I always get errors related to model foreign or primary keys. One example:
Error Traceback (most recent call last):
File "/tests/test_forms.py", line 62, in test_validation formset = Formset(formdata)
File "lib/python2.7/site-packages/django/forms/models.py", line 853, in __init__
self.instance = self.fk.rel.to()
AttributeError: 'AFormset' object has no attribute 'fk'
What am I missing? Is there an easier way to instantiate a formset with a dictionary of data and have it run its clean method? Should I just test the view that the form is used in? (In the views I'm using the form and formset with the Django Extra Views package)

The problem is not with your test, but has to do with an incompatibility between your base formset class and the formset factory.
Your AFormset class inherits from BaseInlineFormSet. That class expects an fk property to exist, which should determine the foreign key of the form model to the object to which it is "inline". That property is created by the inlineformset_factory function. However, you are using modelformset_factory to construct your concrete formset class; this does not set that fk property.
You should either use inlineformset_factory (and pass in the parent model), or change your formset class to inherit from BaseModelFormSet if it is not actually inline.

Related

What is the main difference between clean and full_clean function in Django?

What is the main difference between clean and full_clean function in Django model ?
From the documentation:
Model.full_clean(exclude=None, validate_unique=True):
This method calls Model.clean_fields(), Model.clean(), and
Model.validate_unique() (if validate_unique is True), in that order
and raises a ValidationError that has a message_dict attribute
containing errors from all three stages.
Model.clean():
This method should be used to provide custom model validation, and to
modify attributes on your model if desired.
For more detailed explanation, have a look at the Validating objects section of the documentation.
they are not against each other usually you call Model.full_clean()
to be able to trigger Model.clean() for costume validation
for example:
from django.core.exceptions import ValidationError
from django.db import models
class Brand(models.Model):
title = models.CharField(max_length=512)
def clean(self):
if self.title.isdigit():
raise ValidationError("title must be meaningful not only digits")
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)
here are three steps involved in validating a model:
Validate the model fields - Model.clean_fields()
Validate the model as a whole - Model.clean()
Validate the field uniqueness - Model.validate_unique()
All three steps are performed when you call a model’s full_clean() method. for more information click here

simplest way to override Django admin inline to request formfield_for_dbfield for each instance

I would like to provide different widgets to input form fields for the same type of model field in a Django admin inline.
I have implemented a version of the Entity-Attribute-Value paradigm in my shop application (I tried eav-django and it wasn't flexible enough). In my model it is Product-Parameter-Value (see Edit below).
Everything works as I want except that when including an admin inline for the Parameter-Value pair, the same input formfield is used for every value. I understand that this is the default Django admin behaviour because it uses the same formset for each Inline row.
I have a callback on my Parameter that I would like to use (get_value_formfield). I currently have:
class SpecificationValueAdminInline(admin.TabularInline):
model = SpecificationValue
fields = ('parameter', 'value')
readonly_fields = ('parameter',)
max_num = 0
def get_formset(self, request, instance, **kwargs):
"""Take a copy of the instance"""
self.parent_instance = instance
return super().get_formset(request, instance, **kwargs)
def formfield_for_dbfield(self, db_field, **kwargs):
"""Override admin function for requesting the formfield"""
if self.parent_instance and db_field.name == 'value':
# Notice first() on the end -->
sv_instance = SpecificationValue.objects.filter(
product=self.parent_instance).first()
formfield = sv_instance.parameter.get_value_formfield()
else:
formfield = super().formfield_for_dbfield(db_field, **kwargs)
return formfield
formfield_for_dbfield is only called once for each admin page.
How would I override the default behaviour so that formfield_for_dbfield is called once for each SpecificationValue instance, preferably passing the instance in each time?
Edit:
Here is the model layout:
class Product(Model):
specification = ManyToManyField('SpecificationParameter',
through='SpecificationValue')
class SpecificationParameter(Model):
"""Other normal model fields here"""
type = models.PositiveSmallIntegerField(choices=TUPLE)
def get_value_formfield(self):
"""
Return the type of form field for parameter instance
with the correct widget for the value
"""
class SpecificationValue(Model):
product = ForeignKey(Product)
parameter = ForeignKey(SpecificationParameter)
# To store and retrieve all types of value, overrides CharField
value = CustomValueField()
The way I eventually solved this is using the form = attribute of the Admin Inline. This skips the form generation code of the ModelAdmin:
class SpecificationValueForm(ModelForm):
class Meta:
model = SpecificationValue
def __init__(self, instance=None, **kwargs):
super().__init__(instance=instance, **kwargs)
if instance:
self.fields['value'] = instance.parameter.get_value_formfield()
else:
self.fields['value'].disabled = True
class SpecificationValueAdminInline(admin.TabularInline):
form = SpecificationValueForm
Using standard forms like this, widgets with choices (e.g. RadioSelect and CheckboxSelectMultiple) have list bullets next to them in the admin interface because the <ul> doesn't have the radiolist class. You can almost fix the RadioSelect by using AdminRadioSelect(attrs={'class': 'radiolist'}) but there isn't an admin version of the CheckboxSelectMultiple so I preferred consistency. Also there is an aligned class missing from the <fieldset> wrapper element.
Looks like I'll have to live with that!

Django: ValueError: cannot assign none on ForeignField when trying to create in clean fails

I'm having a lot of trouble figuring out how to automatically create an instance of a model for a ForeignKey field when a form is submitted. Here's a simple toy website that illustrates the problem:
I have two models, Model1 and Model2. Model2 contains a ForeignKey to Model1. I want the user to be able to create an instance of Model2 by either specifically selecting an instance of Model1 to store in the ForeignKey, or by leaving that value blank and letting an instance of Model1 be automatically generated.
Here's what I feel like that code should look like. My models.py code is very straightforward:
# models.py
from django.db import models
from django.core.validators import MinValueValidator
class Model1(models.Model):
# Note this field cannot be negative
my_field1 = models.IntegerField(validators=[MinValueValidator(0)])
class Model2(models.Model):
# blank = True will make key_to_model1 not required on the form,
# but since null = False, I will still require the ForeignKey
# to be set in the database.
related_model1 = models.ForeignKey(Model1, blank=True)
# Note this field cannot be negative
my_field2 = models.IntegerField(validators=[MinValueValidator(0)])
forms.py is a bit involved, but what's going on is quite straightforward. If Model2Form does not receive an instance of Model1, it tries to automatically create one in the clean method, validates it, and if it's valid, it saves it. If it's not valid, it raises an exception.
#forms.py
from django import forms
from django.forms.models import model_to_dict
from .models import Model1, Model2
# A ModelForm used for validation purposes only.
class Model1Form(forms.ModelForm):
class Meta:
model = Model1
class Model2Form(forms.ModelForm):
class Meta:
model = Model2
def clean(self):
cleaned_data = super(Model2Form, self).clean()
if not cleaned_data.get('related_model1', None):
# Don't instantiate field2 if it doesn't exist.
val = cleaned_data.get('my_field2', None)
if not val:
raise forms.ValidationError("My field must exist")
# Generate a new instance of Model1 based on Model2's data
new_model1 = Model1(my_field1=val)
# validate the Model1 instance with a form form
validation_form_data = model_to_dict(new_model1)
validation_form = Model1Form(validation_form_data)
if not validation_form.is_valid():
raise forms.ValidationError("Could not create a proper instance of Model1.")
# set the model1 instance to the related model and save it to the database.
new_model1.save()
cleaned_data['related_model1'] = new_model1
return cleaned_data
However, this approach does not work. If I enter valid data into my form, it works fine. But, if I don't enter anything for the ForeignKey and put a negative value for the integer, I get a ValueError.
Traceback: File
"/Library/Python/2.7/site-packages/django/core/handlers/base.py" in
get_response
111. response = callback(request, *callback_args, **callback_kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/base.py" in
view
48. return self.dispatch(request, *args, **kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/base.py" in
dispatch
69. return handler(request, *args, **kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/edit.py" in
post
172. return super(BaseCreateView, self).post(request, *args, **kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/edit.py" in
post
137. if form.is_valid(): File "/Library/Python/2.7/site-packages/django/forms/forms.py" in is_valid
124. return self.is_bound and not bool(self.errors) File "/Library/Python/2.7/site-packages/django/forms/forms.py" in
_get_errors
115. self.full_clean() File "/Library/Python/2.7/site-packages/django/forms/forms.py" in
full_clean
272. self._post_clean() File "/Library/Python/2.7/site-packages/django/forms/models.py" in
_post_clean
309. self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) File
"/Library/Python/2.7/site-packages/django/forms/models.py" in
construct_instance
51. f.save_form_data(instance, cleaned_data[f.name]) File
"/Library/Python/2.7/site-packages/django/db/models/fields/init.py"
in save_form_data
454. setattr(instance, self.name, data) File "/Library/Python/2.7/site-packages/django/db/models/fields/related.py"
in set
362. (instance._meta.object_name, self.field.name))
Exception Type: ValueError at /add/ Exception Value: Cannot assign
None: "Model2.related_model1" does not allow null values.
So, what's happening is that Django is catching my ValidationError and still creating an instance of Model2 even though validation fails.
I could fix this by overriding the _post_clean method to not create the instance of Model2 if there are errors. But, that solution is ugly. In particular, _post_clean's behavior is very helpful in general--In more complicated projects I need _post_clean to run for other reasons.
I could also allow the ForeignKey to be null but never set it to null in practice. But, again, that seems like a bad idea.
I could even set up a dummy Model1 that I use whenever validation on the attempted new Model1 fails, but that also seems hackish.
In general, I can think of lots of hacks to fix this, but I have no idea how to fix this in a reasonably clean, pythonic way.
I've found a solution that I think might be acceptable, based somewhat on karthikr's discussion in the comments. I'm definitely still open to alternatives.
The idea is to use logic in the view to choose between two forms to do validation: One form is the standard model form and one is the model form without the ForeignKey field.
So, my models.py is identical.
My forms.py has two Model2 forms... one extremely simple one and one without the ForeignKey field and with new logic to dynamically generate a new instance of Model1 for the ForeignKey. The new form's clean logic is just the clean logic that I used to put in my Model2Form:
#forms.py
from django import forms
from django.forms.models import model_to_dict
from .models import Model1, Model2
# A ModelForm used for validation purposes only.
class Model1Form(forms.ModelForm):
class Meta:
model = Model1
class Model2Form(forms.ModelForm):
class Meta:
model = Model2
# This inherits from Model2Form so that any additional logic that I put in Model2Form
# will apply to it.
class Model2FormPrime(Model2Form):
class Meta:
model = Model2
exclude = ('related_model1',)
def clean(self):
cleaned_data = super(Model2Form, self).clean()
if cleaned_data.get('related_model1', None):
raise Exception('Huh? This should not happen...')
# Don't instantiate field2 if it doesn't exist.
val = cleaned_data.get('my_field2', None)
if not val:
raise forms.ValidationError("My field must exist")
# Generate a new instance of Model1 based on Model2's data
new_model1 = Model1(my_field1=val)
# validate the Model1 instance with a form form
validation_form_data = model_to_dict(new_model1)
validation_form = Model1Form(validation_form_data)
if not validation_form.is_valid():
raise forms.ValidationError("Could not create a proper instance of Model1.")
# set the Model1 instance to the related model and save it to the database.
cleaned_data['related_model1'] = new_model1
return cleaned_data
def save(self, commit=True):
# Best to wait til save is called to save the instance of Model1
# so that instances aren't created when the Model2Form is invalid
self.cleaned_data['related_model1'].save()
# Need to handle saving this way because otherwise related_model1 is excluded
# from the save due to Meta.excludes
instance = super(Model2FormPrime, self).save(False)
instance.related_model1 = self.cleaned_data['related_model1']
instance.save()
return instance
And then my view logic uses one of the two forms to validate, depending on the post data. If it uses Model2FormPrime and validation fails, it will move the data and errors to a regular Model2Form to show the user:
# Create your views here.
from django.views.generic.edit import CreateView
from django.http import HttpResponseRedirect
from .forms import Model2Form, Model2FormPrime
class Model2CreateView(CreateView):
form_class = Model2Form
template_name = 'form_template.html'
success_url = '/add/'
def post(self, request, *args, **kwargs):
if request.POST.get('related_model', None):
# Complete data can just be sent to the standard CreateView form
return super(Model2CreateView, self).post(request, *args, **kwargs)
else:
# super does this, and I won't be calling super.
self.object = None
# use Model2FormPrime to validate the post data without the related model.
validation_form = Model2FormPrime(request.POST)
if validation_form.is_valid():
return self.form_valid(validation_form)
else:
# Create a normal instance of Model2Form to be displayed to the user
# Insantiate it with post data and validation_form's errors
form = Model2Form(request.POST)
form._errors = validation_form._errors
return self.form_invalid(form)
This solution works, and it's quite flexible. I can add logic to my models and to the base Model2Form without worrying too much about breaking it or violating DRY.
It's slightly ugly, though, since it requires me to use two forms to essentially do the job of one, pass errors between forms. So, I'm definitely open to alternative solutions if anyone can suggest anything.

Making disabled field in ModelForm subclass

I got a model Layout in my Django app with the following fields:
meta_layout - ForeignKey on model MetaLayout
name - CharField
edited - DateTimeField
is_active - BooleanField
And I have two views using this model - one called NewLayout and other EditLayout each subclassing standard CreateView and UpdateView accordingly. In EditLayout view I want to use some special form that looks the same as form used in NewLayout (which is simply plain ModelForm for this model) but has meta_layout select field displayed with attribute disabled="disabled" (e.d. user can choose meta_layout for each Layout only once - while creating it). Ok, I can create custom ModelForm where widget for meta_layout field has the desired attribute, but the problem is actually that when such attribute set on form field it will not send any values with request - so my validation fails trying to check value for this field and select element does not support "readonly" attribute which will would be just fine here.
I found some really ugly hack to workaround this:
#Here is my Form:
class LayoutEditForm(forms.ModelForm):
meta_layout = forms.ModelChoiceField(
queryset=MetaLayout.objects.all(),
widget=forms.Select(attrs=dict(disabled='disabled')),
empty_label=None,
required=False) # if required=True validation will fail
# because value is not supplied in POST
class Meta:
fields = ('meta_layout', 'name', 'is_active')
model = Layout
class EditLayout(UpdateView):
...
# And one modified method from my View-class
def get_form_kwargs(self):
kwargs = super(EditLayout, self).get_form_kwargs()
# actually POST parameters
if kwargs.has_key('data'):
# can't change QueryDict itself - it's immutable
data = dict(self.request.POST.items())
# emulate POST params from ModelChoiceField
data['meta_layout'] = u'%d' % self.object.meta_layout.id
kwargs['data'] = data
return kwargs
But I believe that it's non-Django, non-Pythonic and not a good-programming-style-at-all of doing such simple thing. Can you suggest any better solution?
Edit:
Oh, I found much less ugly solution: added this in my form class:
def clean_meta_layout(self):
return self.instance.meta_layout
But I still open for suggestions) - may I missed something?

Django ModelForm Meta

I want create a ModelForm class where model is a parameter passed from the view.(i want a dynamic form, so i can create all forms using the same class ObjectForm by just changing model value in Meta) :
class ObjectForm(ModelForm):
model_name = None
def __init__(self, *args, **kwargs):
model_name = kwargs.pop('model_name ')
super(ModelForm, self).__init__(*args, **kwargs)
class Meta:
model = models.get_model('core', model_name )
exclude = ("societe")
An error is occured and say that model_name is not a global field.
Please help me on this problem.
your problem is that the class (and the Meta class) are processed at compile time, not when you instantiate your ObjectForm. at compile time, the model name is unknown. creating classes dynamically is possible, but a bit more complicated. as luck has it, the django devs have done the hard work for you:
>>> from django.forms.models import modelform_factory
>>> modelform_factory(MyModel)
<class 'django.forms.models.MyModelForm'>
update
So you want something like
def my_view(request):
# ...
MyForm = modelform_factory(MyModel)
form = MyForm(request.POST) # or however you would use a 'regular' form
Well, your basic error is that you are accessing model_name as a local variable, rather than as a model instance. That's fairly basic Python.
But even once you've fixed this, it still wouldn't work. The Meta class is evaluated at define time, by the form metaclass, rather than at runtime. You need to call forms.models.modelform_factory - you can pass in your modelform subclass to the factory, if you want to define some standard validation and/or fields.
form_class = modelform_factory(MyModel, form=MyModelForm)

Categories