django alter form data in clean method - django

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

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

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

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:
You must call super(YourModelFormClass, self).clean() so that unique fields are checked.
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.

"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/

Related

How do you only run a validator on a form field at the end after no validation errors have been raised?

I would like to only run a specific checksum validation if things like required, min and max validations as well as a custom is_digit() validation is run.
The reason is I do not want to show the error message for the checksum validation if some other validation is failing.
I've tried:
id_number = ZaIdField(
required=False,
max_length=13,
min_length=13,
validators=[validate_numeric, ]
)
then I have the checksum validator after others run in super():
class ZaIdField(forms.CharField):
'''
Field for validating ZA Id Numbers
'''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def validate(self, value):
"""Check if id is valid"""
# Use the parent's handling of required fields, etc.
super().validate(value)
validate_sa_id(value)
Update:
In other words, my final validation is dependent on the length being correct and all digits.
So I just want to ensure that is correct before running it.
Check the django docs for more information. It's pretty simple actually.
def clean_id_number(self):
data = self.cleaned_data['id_number']
if checksum:
raise forms.ValidationError("Checksum error!")
return data
This has probably been answered somewhere before but it looks like the right palce to do this is in the form's clean():
def clean(self):
cleaned_data = super().clean()
id_num = cleaned_data.get('id_number')
if id_num:
validate_sa_id(id_num)
return cleaned_data
The key part of the docs is:
By the time the form’s clean() method is called, all the individual
field clean methods will have been run (the previous two sections), so
self.cleaned_data will be populated with any data that has survived so
far.
So you just check if the field has survived, if it has then it has passed prior validations
If you want to mess with the order of the validators, I would override the ZaIdFieldsrun_validators method.
Note that the fields validate method that you're overriding will always be called before.
Example (untested):
class ZaIdField(forms.CharField):
'''
Field for validating ZA Id Numbers
'''
def run_validators(self, value):
super().run_validators(value) # will potentially throw ValidationError, exiting
validate_sa_id(value) # your late validator, will throw its own ValidationError

Assigning a custom form error to a field in a modelform in django

I am trying to assign a custom form error to a field in a modelform in django so that it appears where a 'standard' error such as the field being left blank, with the same formatting (which is handled by crispy forms).
My model form clean method looks like this:
def clean(self):
cleaned_data = super(CreatorForm, self).clean()
try:
if cleaned_data['email'] != cleaned_data['re_email']:
raise forms.ValidationError({'email': "Your emails don't match!"})
except KeyError:
pass
return cleaned_data
And in my template I display the form/re-submitted form like this:
{{creator_form|crispy}}
I would like the error to appear below the re_email field if possible (though currently I thought I'd have better luck getting it below the email field. At the moment it appears at the top of the form, unformatted.
For the re_email field, despite not being part of the model, the error displayed for leaving it blank appears below the re_email field. How do I 'attach' errors to fields so they are displayed beneath/near them?
All help appreciated thanks
To get the error to display on a specific field you need to explicitly define what field the error goes on since you're overriding .clean(). Here is a sample taken from the Django docs:
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super(ContactForm, self).clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject and "help" not in subject:
# We know these are not in self._errors now (see discussion
# below).
msg = u"Must put 'help' in subject when cc'ing yourself."
self._errors["cc_myself"] = self.error_class([msg])
self._errors["subject"] = self.error_class([msg])
# These fields are no longer valid. Remove them from the
# cleaned data.
del cleaned_data["cc_myself"]
del cleaned_data["subject"]
# Always return the full collection of cleaned data.
return cleaned_data

Django form: ask for confirmation before committing to db

Update: The solution can be found as a separate answer
I am making a Django form to allow users to add tvshows to my db. To do this I have a Tvshow model, a TvshowModelForm and I use the generic class-based views CreateTvshowView/UpdateTvshowView to generate the form.
Now comes my problem: lets say a user wants to add a show to the db, e.g. Game of Thrones. If a show by this title already exists, I want to prompt the user for confirmation that this is indeed a different show than the one in the db, and if no similar show exists I want to commit it to the db. How do I best handle this confirmation?
Some of my experiments are shown in the code below, but maybe I am going about this the wrong way. The base of my solution is to include a hidden field force, which should be set to 1 if the user gets prompted if he is sure he wants to commit this data, so that I can read out whether this thing is 1 to decide whether the user clicked submit again, thereby telling me that he wants to store it.
I would love to hear what you guy's think on how to solve this.
views.py
class TvshowModelForm(forms.ModelForm):
force = forms.CharField(required=False, initial=0)
def __init__(self, *args, **kwargs):
super(TvshowModelForm, self).__init__(*args, **kwargs)
class Meta:
model = Tvshow
exclude = ('user')
class UpdateTvshowView(UpdateView):
form_class = TvshowModelForm
model = Tvshow
template_name = "tvshow_form.html"
#Only the user who added it should be allowed to edit
def form_valid(self, form):
self.object = form.save(commit=False)
#Check for duplicates and similar results, raise an error/warning if one is found
dup_list = get_object_duplicates(Tvshow, title = self.object.title)
if dup_list:
messages.add_message(self.request, messages.WARNING,
'A tv show with this name already exists. Are you sure this is not the same one? Click submit again once you\'re sure this is new content'
)
# Experiment 1, I don't know why this doesn't work
# form.fields['force'] = forms.CharField(required=False, initial=1)
# Experiment 2, does not work: cleaned_data is not used to generate the new form
# if form.is_valid():
# form.cleaned_data['force'] = 1
# Experiment 3, does not work: querydict is immutable
# form.data['force'] = u'1'
if self.object.user != self.request.user:
messages.add_message(self.request, messages.ERROR, 'Only the user who added this content is allowed to edit it.')
if not messages.get_messages(self.request):
return super(UpdateTvshowView, self).form_valid(form)
else:
return super(UpdateTvshowView, self).form_invalid(form)
Solution
Having solved this with the help of the ideas posted here as answers, in particular those by Alexander Larikov and Chris Lawlor, I would like to post my final solution so others might benefit from it.
It turns out that it is possible to do this with CBV, and I rather like it. (Because I am a fan of keeping everything OOP) I have also made the forms as generic as possible.
First, I have made the following forms:
class BaseConfirmModelForm(BaseModelForm):
force = forms.BooleanField(required=False, initial=0)
def clean_force(self):
data = self.cleaned_data['force']
if data:
return data
else:
raise forms.ValidationError('Please confirm that this {} is unique.'.format(ContentType.objects.get_for_model(self.Meta.model)))
class TvshowModelForm(BaseModelForm):
class Meta(BaseModelForm.Meta):
model = Tvshow
exclude = ('user')
"""
To ask for user confirmation in case of duplicate title
"""
class ConfirmTvshowModelForm(TvshowModelForm, BaseConfirmModelForm):
pass
And now making suitable views. The key here was the discovery of get_form_class as opposed to using the form_class variable.
class EditTvshowView(FormView):
def dispatch(self, request, *args, **kwargs):
try:
dup_list = get_object_duplicates(self.model, title = request.POST['title'])
if dup_list:
self.duplicate = True
messages.add_message(request, messages.ERROR, 'Please confirm that this show is unique.')
else:
self.duplicate = False
except KeyError:
self.duplicate = False
return super(EditTvshowView, self).dispatch(request, *args, **kwargs)
def get_form_class(self):
return ConfirmTvshowModelForm if self.duplicate else TvshowModelForm
"""
Classes to create and update tvshow objects.
"""
class CreateTvshowView(CreateView, EditTvshowView):
pass
class UpdateTvshowView(EditTvshowView, UpdateObjectView):
model = Tvshow
I hope this will benefit others with similar problems.
I will post it as an answer. In your form's clean method you can validate user's data in the way you want. It might look like that:
def clean(self):
# check if 'force' checkbox is not set on the form
if not self.cleaned_data.get('force'):
dup_list = get_object_duplicates(Tvshow, title = self.object.title)
if dup_list:
raise forms.ValidationError("A tv show with this name already exists. "
"Are you sure this is not the same one? "
"Click submit again once you're sure this "
"is new content")
You could stick the POST data in the user's session, redirect to a confirmation page which contains a simple Confirm / Deny form, which POSTs to another view which processes the confirmation. If the update is confirmed, pull the POST data out of the session and process as normal. If update is cancelled, remove the data from the session and move on.
I have to do something similar and i could do it using Jquery Dialog (to show if form data would "duplicate" things) and Ajax (to post to a view that make the required verification and return if there was a problem or not). If data was possibly duplicated, a dialog was shown where the duplicated entries appeared and it has 2 buttons: Confirm or Cancel. If someone hits in "confirm" you can continue with the original submit (for example, using jquery to submit the form). If not, you just close the dialog and let everything as it was.
I hope it helps and that you understand my description.... If you need help doing this, tell me so i can copy you an example.
An alternative, and cleaner than using a vaidationerror, is to use Django's built in form Wizard functionality: https://django-formtools.readthedocs.io/en/latest/wizard.html
This lets you link multiple forms together and act on them once they are all validated.

How to reset "DELETE" param to False in django's custom formset's clean method

I made custom formset with custom clean method in it. It's look like:
class MyFormsetBase(forms.models.BaseModelFormSet):
def __init__(self, *args, **kwargs):
....
super(AdvOrderBidFormsetBase, self).__init__(*args, **kwargs)
def clean(self):
....
if error:
raise forms.ValidationError('some validation error')
When I deleting some form(with wrong paramaeters) from formset and press submit button(form has DELETE field value == True) I get error = True in my clean method so I get ValidationError and return to form window. But the DELETE field value in form doesn't reset. And next time when I will submit my form I will get same ValidationError.
So, could I somehow change value of DELETE field in my custom clean method?
Even though this question is quite old, I recently ran into the same problem. I was able to reset the DELETE field by tweaking with the form.data dictionary. Not a pretty solution, but it works:
class MyFormset(BaseModelFormSet):
def clean(self):
for form in self.deleted_forms:
#iterating over only the forms marked for being delete
if <YOUR_CONDITION>:
form.data = form.data.copy() # form.data is immutable unless we copy it
form.data.pop('{prefix}-DELETE'.format(form.prefix)) # Delete the correct 'DELETE' field from the POST data
form.cleaned_data.pop('DELETE') # Also remove the field from cleaned data as the formset checks this field for deletion
What exactly do you return in your clean method?
If the "DELETE field value" attribute (or whatever you mean by this phrase) is still contained in your cleaned data, it will persist.
To remove it, delete it from your cleaned data.
Similar issues are reported here, maybe this helps Clearing Django form fields on form validation error?

Any danger in manually setting cleaned_data in Django?

This may be a dumb question, but I'm a bit unsure if it's safe to manually set the cleaned_data. The docs says:
Once is_valid() returns True, you can process the form submission
safe in the knowledge that it conforms to the validation rules defined
by your form. While you could access request.POST directly at this
point, it is better to access form.cleaned_data. This data has not
only been validated but will also be converted in to the relevant
Python types for you.
For more context, say we have a modelform which has several fields such as a book's title, book's author, and a field which asks for a url.
The form conditions are: if the url field is empty, the user must provide the title and author. If the url field is given and nothing else, I would parse the html from the given url and extract the title and author automatically for the user.
In the case where I automatically grab the title and author from the url, what would be the best way to handle saving this data to the model, since the form would return an empty cleaned_data for author and title? I made sure the data parsed will conform to the validate rules I have in the model, but setting cleaned_data like this seems suspicious.
In modelform class:
def save(self, commit = True, *args, **kwargs):
parsed_title = ... # String returned by my html parsing function
parsed_author = ... # String returned by my html parsing function
self.cleaned_data['title'] = parsed_title
self.cleaned_data['author'] = parsed_author
EDIT:
Thanks, I made it like so:
def save(self, commit=True, *args, **kwargs):
instance = super(BookInfoForm, self).save(commit=commit, *args, **kwargs)
....
instance.title = parsed_title
instance.author = parsed_author
return instance
This is a bit off topic since you've already answered the original question, but the above code breaks some other part. Instead of saving the compiled info to http://..../media/books/<id> where <id> is the book id, it saves it to http://..../media/books/None.
I have a add/edit function in my views.py that handles adding and editing:
def insert_or_modify(request, id=None):
if id is not None:
book = BookModel.objects.get(pk=id)
else:
book = BookModel()
if request.method == 'POST':
form = BookInfoForm(request.POST, instance=book)
if form.is_valid():
form.save()
....
return render_to_response(...)
Is there a way to make sure the id is present so that I won't get id=None? I guess more specifically, in the save() in the modelform, is there a way to create a new instance with an id if instance.id = None? Although I thought calling super(ModelForm, self).save(...) would do that for me?
Thanks again!
In the case you present, your intention isn't setting the cleaned_data, but the model data. Therefore, instead of setting cleaned_data in the save method, just set the attributes of self.instance and then save it.
About setting cleaned_data manually, I don't think it's necessarily wrong, it may make sense to do it in the form's clean method for some cross-field validation, although it's not a common case.