Django ModelForm individual fields not validated when overriding clean() on Model - django

I am using standard Django Models and ModelForms.
On the Model, I am overriding the clean() method to check that 2 fields in combination are valid. I am having validators[], not nulls etc defined on other fields.
In the ModelForm, I am not modifying anything. I only set the corresponding Model in the ModelForm's Meta class.
When I try to create a new object through the form, the individual fields in the ModelForm/Model are not validated when I call form.is_valid() in the View. According to the docs (https://docs.djangoproject.com/en/1.6/ref/models/instances/#validating-objects) the is_valid() method on the Form should call the Model's clean_fields() method (first).
This doesn't seem to work when you submit a form without a Model instance (or a new instance not in the db). When I'm editing an existing object, all is well. It nicely triggers invalid values in individual fields before calling the Model's clean() method.
When I remove the overridden clean() method from my Model, all is well. Individual fields are validated both when creating new objects and editing existing ones.
I have also tested this with the admin module's forms. It has exactly the same behaviour.
So the question is, why does overriding the clean() method on my Model prevent the ModelForm validating the individual fields before calling the clean() method to test additional cross-field stuff???
Note that I am not validating the ModelForm. All validation is on the Model itself.
Model:
class Survey(models.Model):
from_date = models.DateField(null=False, blank=False, validators=[...])
to_date = models.DateField(null=False, blank=False, validators=[...])
(...)
def clean(self):
errors = []
# At this point I expect self.to_date already to be validated for not null etc.
# It isn't for new Model instances, only when editing an existing one
if self.to_date < self.from_date:
errors.append(ValidationError("..."))
ModelForm:
class TestForm(ModelForm):
class Meta:
model = Survey
View (to render blank form for new Model data entry):
(...)
if request.method == "POST":
survey_form = TestForm(request.POST)
if '_save' in request.POST:
if survey_form.is_valid():
survey_form.save()
return HttpResponseRedirect(next_url)
else:
return HttpResponseRedirect(next_url)
else:
survey_form = TestForm()
context = {'form': survey_form}
(...)

Related

Django Iterating over Many to Many Objects

My main problem is that my code never goes into the for loop though within the debugger I can see that hardware exists. The for loop gets just skipped and I can´t figure out why this is the case.
Models:
class Hardware(models.Model):
name = models.CharField(max_length=30)
description = models.TextField()
class Bundle(models.Model):
name = models.CharField(max_length=30)
description = models.TextField()
devices = models.ManyToManyField(Hardware)
class BundleForm(ModelForm):
class Meta:
model = Bundle
fields = ('name', 'description', 'devices')
labels = {
'name': _('Bundlename'),
'description': _('Beschreibung des Bundle'),
'devices': _('Hardware im Bundle')
}
Views:
elif request.method == 'POST' and 'newbundle' in request.POST:
form = BundleForm(request.POST)
if form.is_valid():
bundle = form.save(commit=False)
bundle.save()
for hardware in bundle.devices.all():
print(hardware)
messages.success(request, 'Erfolg! Die Daten wurden erfolgreich gespeichert.')
return redirect('/knowledgeeditor/bundle/', {'messages': messages})
Your question is not about iterating many-to-many fields, but saving them.
The modelforms documentation has this to say about using commit=False with a many-to-many field:
Another side effect of using commit=False is seen when your model has a many-to-many relation with another model. If your model has a many-to-many relation and you specify commit=False when you save a form, Django cannot immediately save the form data for the many-to-many relation. This is because it isn’t possible to save many-to-many data for an instance until the instance exists in the database.
To work around this problem, every time you save a form using commit=False, Django adds a save_m2m() method to your ModelForm subclass. After you’ve manually saved the instance produced by the form, you can invoke save_m2m() to save the many-to-many form data.
As noted there, you could use save_m2m() to save the field, but instead of doing that you should ask yourself why you are using commit=False here at all. There is no reason to do so, so you should omit that parameter and the subsequent separate save, and just let the form save itself in one go.

Django model validation

I'm facing a validation problem.
I need to use form validation and model validation together, but django (1.10) doesn't seem to like this.
Here is a short version of my setup:
class MyModel(models.Model):
fk = models.ForeignKey('ap.Model')
foo = models.CharField(max_length=12)
def clean(self):
if self.fk.som_field != self.foo:
raise ValidationError("This did not validate")
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('fk',)
def view(request):
instance = MyModel(foo='bar')
form = MyModelForm(data=request.POST, instance=instance)
if form.is_valid():
# process
# redirect
# display template
So I have some model field validation in the model itself.
I need this here because it is re-used in other non-form related parts of my application.
And I have some user input validation in the form.
In this case, checking that the provided fk is valid and exists.
But when the form is validated and the user provided 'fk' is not valid, the form is rejected.
But the form also calls MyModel.full_clean add more model validation.
The problem is that calling MyModel.clean() without having any data in the field fk will raise a RelatedObjectDoesNotExist exception.
How can I do this the proper way ?
I feel that MyModel.full_clean() should not be called by the form until the form itself is valid to ensure that the model is validated with at least correct field types in it.
I could embed my MyModel.clean operations in a try/except that would catch a RealtedObjectDoesNotExist, but it has a bad code smell for me.
The fact that MyModel.fk exists should be ensured by the first form layer validation.
As you have found, the model form performs the instance validation (by calling self.instance.full_clean()`) whether or not the form is valid.
You could prevent this behaviour (I might try to override the _post_clean, but I think it's simpler to take account of the behaviour in the clean method.
Rather than catching the exception, it would be simpler to check that self.fk_id is not None before accessing self.fk.
class MyModel(models.Model):
fk = models.ForeignKey('ap.Model')
foo = models.CharField(max_length=12)
def clean(self):
if self.fk_id is not None and self.fk.som_field != self.foo:
raise ValidationError("This did not validate")

Edit related object using ModelFormSet

I was using a model formset to generate a table of forms for a list of objects.
Forms:
class UserTypeModelForm(ModelForm):
account_type = ChoiceField(label='User type',
choices=ACCOUNT_OPTIONS, required=False)
class Meta:
model = get_user_model()
fields = ('account_type',)
UserTypeModelFormSet = modelformset_factory(get_user_model(),
form=UserTypeModelForm,
extra=0)
View:
formset = UserTypeModelFormSet(queryset=users, prefix='formset')
Now my client wants to be able to modify a related field: user.employee_profile.visible.
I tryed to add a field to the form, and then passing "initial" and "queryset" to the formset, but It looks like it just takes one.
How would you guys do this?
Thanks
with model formsets, the initial values only apply to extra forms, those that aren’t bound to an existing object instance.
Django docs
The queryset provides the selected/entered values for the bound fields, the initial for the extra fields (in your case 0).
But you can override the initial value in e.g. your views when you created a field called employee in this case:
for form in forms:
# Don't override a selected value.
if not form.fields['employee'].initial:
form.fields['employee'].initial = my_init

Django: Can I restrict which fields are saved back to the database using forms?

I have a form that I use to display several fields from a record to the user. However, the user should not be able to update all the fields that are displayed. How do I enforce this? It would nice if I could specify which fields to save when calling form.save, but I couldn't get this to work. Here's some of the code:
obj = get_object_or_404(Record, pk=record_id)
if request.method == 'POST':
form = forms.RecordForm(request.POST, instance=obj)
if form.is_valid():
form.save()
I don't think using exclude or fields in the form's Meta definition will work as this will only display the fields the user is allowed to update.
You can override the form's save() method:
class MyModelForm(forms.ModelForm):
def save(self, commit=True):
if self.instance.pk is None:
fail_message = 'created'
else:
fail_message = 'changed'
exclude = ['field_a', 'field_b'] #fields to exclude from saving
return save_instance(self, self.instance, self._meta.fields,
fail_message, commit, construct=False,
exclude=exclude)
Option 1: exclude those fields, and use your template to display the data that should not be changed completely outside of the form itself. It sounds to me like they're not really part of the form, if the user can't change them.
Option 2: In a Django form, how do I make a field readonly (or disabled) so that it cannot be edited?
take this answer to mark your fields as read only... but understand there's no server side security here, so you would want to do something like getting the target model before you update it, and update those offending form fields to the existing data, before you save the form.

Inline formset in Django - removing certain fields

I need to create an inline formset which
a) excludes some fields from MyModel being displayed altogether
b) displays some some fields MyModel but prevents them from being editable.
I tried using the code below, using values() in order to filter the query set to just those values I wanted returned. However, this failed.
Anybody with any idea?
class PointTransactionFormset(BaseInlineFormSet):
def get_queryset(self):
qs = super(PointTransactionFormset, self).get_queryset()
qs = qs.filter(description="promotion feedback")
qs = qs.values('description','points_type') # this does not work
return qs
class PointTransactionInline(admin.TabularInline):
model = PointTransaction
#formset = points_formset()
#formset = inlineformset_factory(UserProfile,PointTransaction)
formset = PointTransactionFormset
One thing that doesn't seem to be said in the documentation is that you can include a form inside your parameters for model formsets. So, for instance, let's say you have a person modelform, you can use it in a model formset by doing this
PersonFormSet = inlineformset_factory(User, Person, form=PersonForm, extra=6)
This allows you to do all the form validation, excludes, etc on a modelform level and have the factory replicate it.
Is this a formset for use in the admin? If so, just set "exclude = ['field1', 'field2']" on your InlineModelAdmin to exclude fields. To show some fields values uneditable, you'll have to create a simple custom widget whose render() method just returns the value, and then override the formfield_for_dbfield() method to assign your widget to the proper fields.
If this is not for the admin, but a formset for use elsewhere, then you should make the above customizations (exclude attribute in the Meta inner class, widget override in __init__ method) in a ModelForm subclass which you pass to the formset constructor. (If you're using Django 1.2 or later, you can just use readonly_fields instead).
I can update with code examples if you clarify which situation you're in (admin or not).
I just had a similar issue (not for admin - for the user-facing site) and discovered you can pass the formset and fields you want displayed into inlineformset_factory like this:
factory = inlineformset_factory(UserProfile, PointTransaction,
formset=PointTransactionFormset,
fields=('description','points_type'))
formset = factory(instance=user_profile, data=request.POST)
where user_profile is a UserProfile.
Be warned that this can cause validation problems if the underlying model has required fields that aren't included in the field list passed into inlineformset_factory, but that's the case for any kind of form.