Django wizard: Form instantiated and clean called multiple times - python

I have a form wizard with 3 forms. The first form has only one field and uses that field to do a lookup in an external API to validate the data.
I noticed that the requests were taking unusually long, so I just added a print statement to
the form's init method and to the external API client call.
It seems that my first form is being initialized and cleaned exactly 28 times every time I execute that step in the form wizard.
My first form looks like this:
forms.py
class MyForm1(forms.Form):
issue_id = forms.CharField()
def __init__(self, *args, **kwargs):
print "init form 1"
super(MyForm1, self).__init__(*args, **kwargs)
def clean(self):
cleaned_data = super(MyForm1, self).clean()
print "clean issue id"
issue_id = cleaned_data.get("issue_id")
if issue_id is not None:
try:
issue = ApiCLient.get_issue(issue_id)
except APIError:
raise forms.ValidationError("Issue not found. Try again!")
else:
return cleaned_data
raise forms.ValidationError("Issue not found. Try again!")
The wizard stuff is pretty standard from the Django Docs, but here it goes:
views.py
class MyWizard(SessionWizardView):
def done(self, form_list, **kwargs):
# some logic at the end of the wizard
urls.py
wizard_forms = [MyForm1, MyForm2, MyForm3]
...
url(r'wizard/$', login_required(views.MyWizard.as_view(wizard_forms))),
As I said before, executing the first step results in
init form 1
clean issue id
Being printed to console exactly 28 times.
Any ideas? Is this a feature or a bug in the Django Form Wizard?

Related

Where is this self.add_error adds the error to?

I'm having a hard time trying to figure out the following code:
from django import forms
from django.contrib.auth.hashers import check_password
class CheckPasswordForm(forms.Form):
password = forms.CharField(label='password_check', widget=forms.PasswordInput(
attrs={'class': 'form-control',}),
)
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get('password')
confirm_password = self.user.password
if password:
if not check_password(password, confirm_password):
self.add_error('password', 'password is wrong')
here, I don't get the self.add_error('password', 'password is wrong') part. In the documentation, it says that the password here is the field in add_error('field', 'error').
So, is the error being added to the password field? or is it being added to the following part?
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
because if I have to access this error in the html template, I would have to do something like this,
{% if password_form.password.errors %}
and if it means accessing errors from password field, it should mean that the error is added to the password field... but the self.add_error part confuses me
Form clean method is responsible for validation of your form when the data is sent to it
normally it will run validation on all field using clean_<fieldname>() method and if there are errors store them in fieldname.errors
As documented
Since the field validation methods have been run by the time clean()
is called, you also have access to the form’s errors attribute which
contains all the errors raised by cleaning of individual fields.
Note that any errors raised by your Form.clean() override will not be
associated with any field in particular. They go into a special
“field” (called __all__)
if you don't want error to end into __all__ you can attach it to particular field in your case you added it to password
If you want to attach errors to a specific field in the form, you
need to call add_error().

Django - Handle update form submition trigerred from admin section

I have articles that I manage via the admin section of Django.
My problem here is that I need to modify a field right after the administrator submited the form before it goes into database.
My specific need is to replace a part of a string with something else, but I don't know how to handle admin form submittions.
def save(self, *args, **kwargs):
self.title = 'someStuff' #Example
super().save(*args, **kwargs)
Here is the function to place under the model class. As simple as that

Sending different errors depending on url Django

I am working in Django and I have a situation where I have written a custom validator that lives in the model.py
This validator should return a validationError when the input is bad.
In the project I am working on, we are using Django Rest Framework for our API and the Django admin panel for our admin panel. They connect to the same DB
My problem is that when the request comes from the API I need to return a 'serializers.ValidationError' (which contains a status code of 400), but when the request comes from the admin panel I want to return a 'django.core.exceptions.ValidationError' which works on the admin panel. The exceptions.ValidationError does not display correctly in the API and the serializers.ValidationError causes the admin panel to break. Is there some way I can send the appropriate ValidationError to the appropriate place?
here is my validation function (it lives in the model)
def validate_unique(self, *args, **kwargs):
super(OrganizationBase, self).validate_unique(*args, **kwargs)
qs = self.__class__._default_manager.filter(organization_type="MEMBER")
if not self._state.adding and self.pk is not None:
qs = qs.exclude(pk=self.pk)
if qs.exists():
raise serializers.ValidationError("Only one organization with \'Organization Type\' of \'Member\' is allowed.") #api
raise exceptions.ValidationError("Only one organization with \'Organization Type\' of \'Member\' is allowed.") #admin
Those two lines at the end are the two errors written together for illustration's sake, in this case only the #api one would run
Basically I want to send errorA when the request is coming from the admin panel and errorB when the request is coming from the API
Thank you
For raising different error classes write different validators.
rest framework api:
You can use the UniqueValidator or a custom validation function. check link [1]
eg:
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = (....)
def validate(self, data):
# my validation code
raise serializers.ValidationError(....)
return data
admin panel:
for the admin panel you can use a custom form [2].
eg:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def clean(self):
cleaned_data = super(MyForm, self).clean()
# my validation code
raise exceptions.ValidationError(....)
return cleaned_data
class MyAdmin(admin.ModelAdmin):
form = MyForm
In both the serializer and form you can access the instance object if not none.
[1] http://www.django-rest-framework.org/api-guide/validators/#uniquevalidator
[2] https://docs.djangoproject.com/en/1.11/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form

Setting the form value when the form is initiated

I am trying to set a field value when a form is initiated.
The value of this field is retrieved when we enter the view - the view being the timesheet. Then for each Time set in the view, I want to relate it back to the timesheet.
#login_required
#requires_csrf_token
def timesheet(request, timesheet_id):
timesheet = TimeSheet.objects.get(pk=timesheet_id)
NewTimeFormSet = modelformset_factory(Time, form=TimeForm, formset=RequiredFormSet)
if request.method == 'POST':
newtime_formset = NewTimeFormSet(request.POST, request.FILES)
for form in newtime_formset:
if form.is_valid():
form.save()
#then render template etc
So, to make sure the form validates I want to set this field when the form is initiated. When I try to set this field after POST in the view, I haven't been able to get the field to set or form to validate.
My code gets the timesheet_id when the model instance is initiated on entering the view
def __init__(self, *args, **kwargs):
# this allows it to get the timesheet_id
print "initiating a timesheet"
super(TimeSheet, self).__init__(*args, **kwargs)
And then the form is generated and I run the form init. So this is what I've tried
class TimeForm(forms.ModelForm):
class Meta:
model = Time
fields = ['project_id', 'date_worked', 'hours', 'description', 'timesheet_id',]
# some labels and widgets, the timesheet_id has a hidden input
def __init__(self, *args, **kwargs):
print "initiating form"
super(TimeForm, self).__init__(*args, **kwargs)
timesheet = TimeSheet.objects.get(id=timesheet_id)
self.fields['timesheet_id'] = timesheet
This raises the error
NameError: global name 'timesheet_id' is not defined
I don't know how to do this...
I've also attempted setting the field in the form clean() method, but it populates (shown by a print) and then still doesn't validate and I raise a formset error 'This field is required'.
Help!
You don't actually accept a timesheet_id parameter in the form init method, so that value is not defined hence the error.
However, this is the wrong approach. There is no point passing a value to a form, outputting it as a hidden field, then getting it back, when you had it all along. The way to do this is to exclude the value from the form's fields, and set it on save.
class TimeForm(forms.ModelForm):
class Meta:
model = Time
fields = ['project_id', 'date_worked', 'hours', 'description',]
...
if request.method == 'POST':
newtime_formset = NewTimeFormSet(request.POST, request.FILES)
if newtime_formset.is_valid():
for form in newtime_formset:
new_time = form.save(commit=False)
new_time.timesheet_id = 1 # or whatever
new_time.save()
Note, again, you should check the validity of the whole formset before iterating through to save; otherwise you might end up saving some of them before encountering an invalid form.

Django admin site modify save

In the admin site, I have a custom form. However, I want to override the save method so that if a certain keyword is entered, I do not save it into the database. Is this possible?
class MyCustomForm (forms.ModelForm):
def save(self, commit=True):
input_name = self.cleaned_data.get('input_name', None)
if input_name == "MyKeyword":
//Do not save
else:
return super(MyCustomForm, self).save(commit=commit)
but this returns the error:
AttributeError 'NoneType' object has no attribute 'save'
Edit
Following this: Overriding the save method in Django ModelForm
I tried:
def save(self, force_insert=False, force_update=False, commit=True):
m = super(MyCustomCategoryForm, self).save(commit=False)
input_name = self.cleaned_data.get('input_name', None)
if input_name != "MyKeyword":
m.save()
return m
But the admin site still creates a new entry if the input_name is "MyKeyword"
The save() method is supposed to return the object, whether saved or not. It should rather look like:
def save(self, commit=True):
input_name = self.cleaned_data.get('input_name')
if input_name == "MyKeyword":
commit = False
return super().save(commit=commit)
However, as you can see here and here, the form is already called with commit=False and the object is saved later by the save_model() method of your ModelAdmin. This is the reason why you got AttributeError 'NoneType' object has no attribute 'save'. If you check the traceback of the exception, I'm pretty sure the error comes from this line.
Thus you should, in addition, override your ModelAdmin's save_model() method:
def save_model(self, request, obj, form, change):
if form.cleaned_data.get('input_name') == 'MyKeyword':
return # Don't save in this case
super().save_model(request, obj, form, change)
# And you'll also have to override the `ModelAdmin`'s `save_related()` method
# in the same flavor:
def save_related(self, request, form, formsets, change):
if form.cleaned_data.get('input_name') == 'MyKeyword':
return # Don't save in this case
super().save_related(request, form, formsets, change)
Given your comment in this answer
It is working now but I just have one related question. The admin site will still display a green banner at the top saying "The model was added successfully". Can I override some method so that it is red and says "The model already exists"?
It looks that what you want to do is not overriding the save method but controlling form validation. Which is completely different. You just have to override your form's clean method.
from django import forms
def clean(self):
"""
super().clean()
if self.cleaned_data.get('input_name') == 'MyKeyword':
raise forms.ValidationError("The model already exists")
Or, even better since it looks like you want to clean a single field:
from django import form
def clean_input_name(self):
data = self.cleaned_data['input_name']
if data == 'MyKeyWord':
raise forms.ValidationError("The model already exists")
However, I guess input_name is also a field of your model and you don't want to raise this error only on one form but across all your project. In which case, what you are looking for are Model validators:
from django.core.exceptions import ValidationError
def validate_input_name(value):
if value == 'MyKeyword':
raise ValidationError("The model already exists")

Categories

Resources