17

I have a django form that I want to custom clean. Instead of just specifying an error message like here (Django form and field validation), I'd like to just alter the field myself. I tried severl ways, but keep running into error like cleaned_data is immutable.

So to solve this I made a copy, changed it and reassigned it to self. Is this the best way to do this? Could/should I have handled this in the view? Making a copy seems poor form but I keep running into 'immutable' road blocks. Sample code below where I simply check if the subject has '--help' at the end, and if not add it. Thanks

def clean(self):
        cleaned_data=self.cleaned_data.copy()
        subject=cleaned_data.get['subject']
        if not subject.endswith('--help'):
            cleaned_data['subject']=subject+='--help'
        self.cleaned_data=cleaned_data
        return self.cleaned_data
rich
  • 595
  • 1
  • 7
  • 15
  • All the answers below are no good at all. [Here's an answer](http://stackoverflow.com/a/33903087/1925257) that actually works. – xyres Mar 21 '16 at 15:06

4 Answers4

17

The correct way to deal with this is by using the field specific clean methods.

Whatever you return from the clean_FOO method is what the cleaned_data will be populated with by the time it gets to the clean function.

Do the following instead:

def clean_subject(self):
        data = self.cleaned_data.get('subject', '')
        if not data:
             raise forms.ValidationError("You must enter a subject")
             # if you don't want this functionality, just remove it.

        if not data.endswith('--help'):
             return data += '--help'
        return data
Yuji 'Tomita' Tomita
  • 115,817
  • 29
  • 282
  • 245
  • This was really helpful because the documentation was unclear on this point. – Bobort Mar 04 '16 at 19:12
  • 2
    This is correct, but it can only be done if there is just one field involved. If another value is needed then `clean` must be used. – gdvalderrama Apr 29 '19 at 14:26
6

So, I found this recently having googled about possibly the same problem, whereby in a ModelForm instance of a form, I was trying to edit the data post-validation to provide a suggestion for the end user as to something that would be a valid response (computed from another value they enter into the form).

TL;DR

If you are dealing with a ModelForm descendent specifically, there are two things that are important:

  1. You must call super(YourModelFormClass, self).clean() so that unique fields are checked.
  2. If you are editing cleaned_data, you must also edit the same field on the instance of your model which is attached to your ModelForm:

    def clean(self)
      self.cleaned_data = super(MyModelFormClass, self).clean()
      self.cleaned_data['name']='My suggested value'
      self.instance.name = 'My suggested value'
      return self.cleaned_data
    

Documentation source for this behaviour

EDIT:

Contrary to the documentation, I have just found that this does not work. you have to edit the form's self.data in order to get the changes to show up when the form displays.

John R Perry
  • 3,916
  • 2
  • 38
  • 62
Philip Adler
  • 2,111
  • 17
  • 25
  • 1
    Modifying the instance in `clean()` is a bad idea. It means that doing `if form.is_valid():` will modify the instance, which is completely unexpected behavior. You should modify the instance in the `save()` method of the form, that is its purpose. I'll post an edit suggestion. – spectras Aug 07 '15 at 02:04
  • Unless you want something to appear in a form that you *know* is going to fail or has failed the validation, in which case, I wanted the suggest value to appear in the field, so the above becomes necessary, as there is no other way to accomplish the aim, because the save method does not get called. Further, in my particular instance, I did actually need to have access to two fields. Obviously if only on operating on one field, then one should use the `clean_` methods. – Philip Adler Aug 07 '15 at 12:28
  • 1
    The documentation you linked is that for `ModelFormSet`, which works mostly like `ModelForm`, but has specific quirks, like that one. It is alright though because the instance that is modified in the `ModelFormSet` example is *not* the one that is seen by calling code. That one still remains untouched, not breaking the expectation that `is_valid()` has no side effects other than compiling the list of errors. – spectras Aug 07 '15 at 13:29
  • 2
    By the way, you should be careful if you modify `self.data` as it may be a read-only object (such as a `django.http.QueryDict`). If you want to modify it, you should make a copy in your form's constructor. – spectras Aug 07 '15 at 13:34
  • @spectras First, why does `cleaned_data` exist in the first place? Because `is_valid()` has side effects. If a form hasn't been validated, it calls `full_clean()`, `clean()`, and all that (again, pay attention to the names). At least because, one might need to [convert](https://django.readthedocs.io/en/latest/howto/custom-model-fields.html#converting-values-to-python-objects) a value before storing it to the database, and back... – x-yuri Oct 13 '19 at 21:06
  • ...Second, [the documentations](https://django.readthedocs.io/en/latest/topics/forms/modelforms.html#overriding-clean-on-a-modelformset) says: "If you wish to modify a value in ModelFormSet.clean() you must modify form.instance..." So, they suggest that `clean()` method can be used for that. In my case, I want to autofill `order` fields of an inline formset when user clicks Save on a Django admin page. I'm still considering options, but should I try to use `save()` for that, it will stop on the validation phase. – x-yuri Oct 13 '19 at 21:06
  • ...On second thought, in my case I can override the save methods, since I'm allowing `order` field to be blank. Otherwise, by the time formset's `clean()` method got called, the forms would be already invalid. You can compare [the solutions](https://gist.github.com/x-yuri/38061cf0c0eec0c909f5c1b94976ce66). The "save" solution is arguable better. First, it indeed makes more sense to change data in the save method. Second, the implementation has fewer things involved (simpler). But `save_new()`, `save_existing()` don't seem to be documented. – x-yuri Oct 13 '19 at 23:25
5

I think your problem is that you have called self.cleaned_data.get['subject'], and then used it as an array later on.

I have this code for a messaging app that replaces an empty subject with 'No Subject'

def clean(self):
    super(forms.ModelForm, self).clean()
    subject = self.cleaned_data['subject']
    if subject.isspace():
        self.cleaned_data['subject'] = 'No Subject'
    return self.cleaned_data

For your code, this should work.

def clean(self):
    super(forms.Form, self).clean() #I would always do this for forms.
    subject = self.cleaned_data['subject']
    if not subject.endswith('--help'):
        subject += '--help'
        self.cleaned_data['subject'] = subject
    return self.cleaned_data
jonescb
  • 22,013
  • 7
  • 46
  • 42
  • 2
    Ok, great! I am altering it in the clean method, but when it returns to the view. The field is not changed. Maybe this was the problem all along? Can you take a look at how I set up the view? – rich Mar 11 '11 at 17:11
  • view is:def createNote(request): if ( request.method == 'POST' ): e = noteForm(request.POST) try: e.is_valid() newE=e.save(commit=False) newE.save() redirect='/notes/' return HttpResponseRedirect(redirect) except Exception, error: print "Failed in register", error else: e = noteForm() return render_to_response('event_form.html', {'e': e,}) – rich Mar 11 '11 at 17:14
0

"This method should return the cleaned value obtained from cleaned_data, regardless of whether it changed anything or not." from https://docs.djangoproject.com/en/dev/ref/forms/validation/

Pier1 Sys
  • 1,250
  • 2
  • 12
  • 21