Django formset - validate input based on user cookie? - django

I have a Django form (TestForm) that contains a single field, quantity. I also have a Django formset (TestFormset) that contains multiple instances of my TestForm.
I want to write a custom clean() method for my TestFormset that validates that the sum of the quantities specified within my multiple TestForms is equal to a number, max_quantity, stored in a session variable.
I know that I am able to perform this validation within views.py (for example, after my formset is validated and cleaned, I could manually sum up the 'quantity' variables in my TestForms and check to ensure that they are equal to request.session['max_quantity'], throwing an error if any problems are found).
But ideally I'd love to move all my form validation logic into the clean() method of forms.py. However, I can't figure out how to pass an external value into my Formset that is not linked to one of its individual forms.
Is this possible to do?
forms.py
from django.forms import BaseFormSet
class TestForm(forms.Form):
quantity = forms.IntegerField()
class BaseTestFormset(BaseFormset):
def clean(self):
if any(self.errors):
# Don't bother validating the formset unless each form is valid on its own
return
quantity = 0
for form in self.forms:
quantity += form.cleaned_data['quantity']
# IF QUANTITY IS NOT EQUAL TO MAX_QUANTITY, THROW AN ERROR...
# ...BUT HOW DO WE GET THE MAX_QUANTITY INTO THIS FUNCTION?
views.py
from .forms import TestForm, BaseTestFormset
def serve_form(request):
TestFormSet = formset_factory(TestForm, formset=BaseTestFormset)
if request.method == 'POST':
formset = TestFormSet(request.POST)
# This method should check to ensure that the sum of quantities within our formsets does not exceed max_quantity
if formset.is_valid():
# Proceed to take action
else:
# Sample initial data
formset = TestFormSet(initial=[{'quantity': 5}, {'quantity': 7}])
# I CAN PASS MAX_QUANTITY INTO THE TEMPLATE... BUT HOW DO I GET IT INTO THE FORMSET VALIDATION METHOD?
return render(request, 'template.html', {'formset': formset, 'max_quantity': request.session['max_quantity']}

As with forms, if you want access to something in a method you need to pass it in somewhere. You can do that in the initialiser if you like:
class BaseTestFormset(forms.BaseFormSet):
def __init__(self, *args, **kwargs):
self.max_quantity = kwargs.pop('max_quantity', None)
super(BaseTestFormset, self).__init__(*args, **kwargs)
def clean(self):
...
if quantity > self.max_quantity:
...
and in the view:
if request.method == 'POST':
formset = TestFormSet(request.POST, max_quantity=request.session['max_quantity'])

Related

How to add a Django form field dynamically depending on if the previous field was filled?

I have a Form (Formset) for users to update their profiles. This is a standard User model form, and custom Participants model form. Now, in cases when a participant provide his phone number, I need to refresh the whole Form with a new 'Code' filed dynamically. And the participant will type the code he received my SMS.
Here is how I am trying to do it:
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
form.save()
seller_form = SellerForm(self.request.POST, instance=self.object.seller)
if seller_form.is_valid():
seller = self.request.user.seller
seller.inn = seller_form.cleaned_data.get('inn')
if seller_form.cleaned_data.get('phone_number'):
seller_form.fields['code'] = models.CharField(max_length=4)
return render(request, self.template_name, {'form': form, 'seller_form': seller_form})
seller.save()
return HttpResponse('Seller updated')
return render(request, self.template_name, {'form': form, 'seller_form': seller_form})
Well I am not sure if this is the way I can add additional field. What would you suggest to handle this situation?
A technique I have used is to have an initially hidden field on the form. When the form otherwise becomes valid, I cause it to become visible, and then send the form around again. In class-based views and outline:
class SomeThingForm( forms.Modelform):
class Meta:
model=Thing
fields = [ ...
confirm = forms.BooleanField(
initial=False, required=False, widget=forms.widgets.HiddenInput,
label='Confirm your inputs?' )
class SomeView( CreateView): # or UpdateView
form_class = SomeThingForm
template_name = 'whatever'
def form_valid( self, form):
_warnings = []
# have a look at cleaned_data
# generate _warnings (a list of 2-tuples) about things which
# aren't automatically bad data but merit another look
if not form.cleaned_data['confirm'] and _warnings:
form.add_error('confirm', 'Correct possible typos and override if sure')
for field,error_text in _warnings:
form.add_error( field, error_text) # 'foo', 'Check me'
# make the confirm field visible
form.fields['confirm'].widget = forms.widgets.Select(
choices=((0, 'No'), (1, 'Yes')) )
# treat the form as invalid (which it now is!)
return self.form_invalid( form)
# OK it's come back with confirm=True
form.save() # save the model
return redirect( ...)
For this question, I think you would replace confirm with sms_challenge, a Charfield or IntegerField, initially hidden, with a default value that will never be a correct answer. When the rest of the form validates, form_valid() gets invoked, and then the same program flow, except you also emit the SMS to the phone number in cleaned_data.
_warnings = []
# retrieve sms_challenge that was sent
if form.cleaned_data['sms_challenge'] != sms_challenge:
_warnings.append( ['sms_challenge', 'Sorry, that's not right'] )
if _warnings:
...
form.fields['sms_challenge'].widget = forms.widgets.TextInput
return self.form_invalid( form)
I think that ought to work.

Django - Set initial selected value for formd.MultipleChoiceField

I want to display initial values as selected on a MultipleChoice form field in Django when the form loads. I populate a formset with different forms. Each form has only one field 'answer', which is initialized based on a custom parameter passed to the form's init() method.
class AnswerForm(forms.Form):
def __init__(self, *args, **kwargs):
"""
Initialize label & field
:returns None:
"""
question = kwargs.pop('question') # A Question object
super(AnswerForm, self).__init__(*args, **kwargs)
if question.type == Types.RADIO:
choices_ = [(op.id, op) for op in question.option_set.all()]
self.fields['answer'] = forms.ChoiceField(label=question.statement,
initial=1,
widget=forms.RadioSelect,
choices=choices_)
elif question.type == Types.CHECKBOX:
choices_ = [(op.id, op) for op in question.option_set.all()]
self.fields['answer'] = forms.MultipleChoiceField(label=question.statement,
initial=[1,3],
widget=forms.CheckboxSelectMultiple,
choices=choices_)
This renders the following HTML:
But it doesn't get into the form's cleaned_data. When I submit formset, the request.POST data goes into this view:
def post(self, request, form_id):
"""
Process & save the responses obtained from a form into DB
:param request: An HTTPRequest object
:param form_id: form id whose responses arrive
:returns HttpResponse object with a results template
"""
formset = FormHandler.AnswerFormSet(request.POST, request.FILES,
form_kwargs={'questions': FormHandler.qs})
if formset.is_valid():
for form in formset:
cd = form.cleaned_data
# Access cd['answer'] here but cd appears to be empty dict {}
# with no key named 'answer'
The cleaned_data does have the correct 'answer' value in the case of Radio, but in this case, it doesn't contain the list of selected IDs which it should. I've checked that request.POST.getlist('form_#_answer') does show the correct list of ['1', '3'] but it somehow doesn't get into the formset's cleaned_data. I've spent hours trying to find out why this happens. Can't find the answer anywhere in the Django docs either. Can anyone explain why this is happening?

Django dynamic forms - validating Select field

I am using Django 2.2
I am creating a form dynamically, by reading a JSON definition file; the configuration file specifies the types of widgets, permitted values etc.
I have come a bit unstuck with the Select widget however, because I prepend a '--' to the list of permitted values in the list (so that I will know when a user has not selected an item).
This is the code snippet where the Select widget is created:
class myForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# ...
elif widget_type_id == WIDGET_TYPE_DROPDOWN:
CHOICES.insert(0,('-', INVALID_SELECTION))
form_field = CharField(label=the_label, widget=Select(choices=CHOICES),required=is_required)
My problem is that when the is_valid() method is invoked on my form, any rendered Select widgets are are accepted as valid regardless of the selection.
I want to implement code that has this logic (pseudocode below):
def is_valid(self):
for field_name, field in self.fields.items():
if isinstance(field.type, Select):
if field.required and field.selected_value == INVALID_SELECTION:
return False
return super().is_valid()
What would be the correct way to implement this functionality? For instance, how would I even get the selected values for the field (in the form code)?
Validate a form data: docs
by creating a custom Field with validation
a specific field by using fieldname_clean()
by overriding clean()
by using validators
? What would be the correct way to implement this functionality?
I've deviated a bit from the pseudocode in what you've asked
Validators docs
There are already many buitin validators. For this purpose, create a custom validator which checks the value in the field and raise ValidationError.
Create validator.py in your app
# validator.py
from django.core.exceptions import ValidationError
def validate_select_option(value):
if value == '-':
raise ValidationError('Invalid selection')
In forms.py import validate_select_option and add validator to the field
# forms.py
from .validator import validate_select_option
# other parts of code
form_field = CharField(label=the_label, widget=Select(choices=CHOICES), required=is_required, validator=[validate_select_option])
This validator validate_select_option can now be used not only to the field which has choices but also to any other field to validate. Anyhow, that's not its intend. So can be used to any fields with choices :)
Since you said that you are creating the form dynamically and I'm assuming that you can add additional options to Field using JSON definition file. For fieldname_clean() and clean() you will need to add these methods in your Form class. Custom field can be created and imported into. But, I think simple validators can do this easily.
? how would I even get the selected values for the field (in the form code)
If Form class method clean(self, *args, **kwargs) and fieldname_clean(self, *args, **kwargs) are used : you can access the form data by self.cleaned_data dictionary. cleaned_data is created only after is_valid().
While overriding is_valid , in-order to access the form data, we need to call the parent validation first and then work on it.
def is_valid(self, *args, **kwargs):
# cannot access self.cleaned_data since it is not created yet
valid = super(myForm, self).is_valid()
# can access self.cleaned_data since it has been created when parent is_valid() has been called
for fieldname, field in self.fields.items():
if isinstance(field, forms.CharField): # the type of widget is not considered.
if fieldname in self.cleaned_data and field.required and self.cleaned_data[fieldname] == '-':
valid = False
return valid
It's kind of hidden because you're using a CharField with a Select widget but if you look at the ChoiceField documentation it says that the empty value should be an empty string.
Assuming the form field is required=True, you should just be able to change your empty value tuple to ('', INVALID_SELECTION).
CHOICES having already values. I insert ('INVALID_SELECTION', 'INVALID_SELECTION') this. And check if the value of form_field field is INVALID_SELECTION then add error in same field. Else form is submited.
views.py
class DynamicFormView(View):
template_name = 'test.html'
def get(self, request, *args, **kwargs):
form = myForm( request.POST or None)
context = {
'form' : form,
}
return render(request, self.template_name, context)
def post(self, request,id = None, *args, **kwargs):
context = {}
form = myForm(request.POST or None,)
if form.is_valid():
if request.POST['form_field'] == 'INVALID_SELECTION':
form.add_error("form_field",_("This field is required."))
else:
form.save()
context = {
'form' : form,
}
return render(request, self.template_name, context)
forms.py
class myForm(forms.Form):
CHOICES = [
('FR', 'Freshman'),
('SO', 'Sophomore'),
('JR', 'Junior'),
('SR', 'Senior'),
('GR', 'Graduate'),
]
CHOICES.insert(0,('INVALID_SELECTION', 'INVALID_SELECTION'))
form_field = forms.ChoiceField(label='the_label',choices=CHOICES,widget=forms.Select(attrs={'class':' form-control'}),required = False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def save(self):
# form is success
pass
class Meta:
model = Student
urls.py
path('dyanmic-form/create/', views.DynamicFormView.as_view(), name='dyanamic_form_create'),
Sorry my bad english language.

Saving inlineformset in Django class-based views (CBV)

So I'm in the process of working on a web application that has implemented security questions into it's registration process. Because of the way my models are setup and the fact that I am trying to use Django's Class based views (CBV), I've had a bit of problems getting this all to integrate cleanly. Here are what my models look like:
Model.py
class AcctSecurityQuestions(models.Model):
class Meta:
db_table = 'security_questions'
id = models.AutoField(primary_key=True)
question = models.CharField(max_length = 250, null=False)
def __unicode__(self):
return u'%s' % self.question
class AcctUser(AbstractBaseUser, PermissionsMixin):
...
user_questions = models.ManyToManyField(AcctSecurityQuestions, through='SecurityQuestionsInter')
...
class SecurityQuestionsInter(models.Model):
class Meta:
db_table = 'security_questions_inter'
acct_user = models.ForeignKey(AcctUser)
security_questions = models.ForeignKey(AcctSecurityQuestions, verbose_name="Security Question")
answer = models.CharField(max_length=128, null=False)
Here is what my current view looks like:
View.py
class AcctRegistration(CreateView):
template_name = 'registration/registration_form.html'
disallowed_url_name = 'registration_disallowed'
model = AcctUser
backend_path = 'registration.backends.default.DefaultBackend'
form_class = AcctRegistrationForm
success_url = 'registration_complete'
def form_valid(self, form):
context = self.get_context_data()
securityquestion_form = context['formset']
if securityquestion_form.is_valid():
self.object = form.save()
securityquestion_form.instance = self.object
securityquestion_form.save()
return HttpResponseRedirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(form=form))
def get_context_data(self, **kwargs):
ctx = super(AcctRegistration, self).get_context_data(**kwargs)
if self.request.POST:
ctx['formset'] = SecurityQuestionsInLineFormSet(self.request.POST, instance=self.object)
ctx['formset'].full_clean()
else:
ctx['formset'] = SecurityQuestionsInLineFormSet(instance=self.object)
return ctx
And for giggles and completeness here is what my form looks like:
Forms.py
class AcctRegistrationForm(ModelForm):
password1 = CharField(widget=PasswordInput(attrs=attrs_dict, render_value=False),
label="Password")
password2 = CharField(widget=PasswordInput(attrs=attrs_dict, render_value=False),
label="Password (again)")
class Meta:
model = AcctUser
...
def clean(self):
if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data:
if self.cleaned_data['password1'] != self.cleaned_data['password2']:
raise ValidationError(_("The two password fields didn't match."))
return self.cleaned_data
SecurityQuestionsInLineFormSet = inlineformset_factory(AcctUser,
SecurityQuestionsInter,
extra=2,
max_num=2,
can_delete=False
)
This post helped me a lot, however in the most recent comments of the chosen answer, its mentioned that formset data should be integrated into the form in the overidden get and post methods:
django class-based views with inline model-form or formset
If I am overiding the get and post how would I add in my data from my formset? And what would I call to loop over the formset data?
Inline formsets are handy when you already have the user object in the database. Then, when you initialize, it'll automatically preload the right security questions, etc. But for creation, a normal model formset is probably best, and one that doesn't include the field on the through table that ties back to the user. Then you can create the user and manually set the user field on the created through table.
Here's how I would do this using a just a model formset:
forms.py:
SecurityQuestionsFormSet = modelformset_factory(SecurityQuestionsInter,
fields=('security_questions', 'answer'),
extra=2,
max_num=2,
can_delete=False,
)
views.py:
class AcctRegistration(CreateView):
# class data like form name as usual
def form_valid(self):
# override the ModelFormMixin definition so you don't save twice
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form, formset):
return self.render_to_response(self.get_context_data(form=form, formset=formset))
def get(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = SecurityQuestionsFormSet(queryset=SecurityQuestionsInter.objects.none())
return self.render_to_response(self.get_context_data(form=form, formset=formset))
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = SecurityQuestionsFormSet(request.POST)
form_valid = form.is_valid()
formset_valid = formset.is_valid()
if form_valid and formset_valid:
self.object = form.save()
security_questions = formset.save(commit=False)
for security_question in security_questions:
security_question.acct_user = self.object
security_question.save()
formset.save_m2m()
return self.form_valid()
else:
return self.form_invalid(form, formset)
Regarding some questions in the comments about why this works the way it does:
I don't quite understand why we needed the queryset
The queryset defines the initial editable scope of objects for the formset. It's the set of instances to be bound to each form within the queryset, similar to the instance parameter of an individual form. Then, if the size of the queryset doesn't exceed the max_num parameter, it'll add extra unbound forms up to max_num or the specified number of extras. Specifying the empty queryset means we've said that we don't want to edit any of the model instances, we just want to create new data.
If you inspect the HTML of the unsubmitted form for the version that uses the default queryset, you'll see hidden inputs giving the IDs of the intermediary rows - plus you'll see the chosen question and answer displayed in the non-hidden inputs.
It's arguably confusing that forms default to being unbound (unless you specify an instance) while formsets default to being bound to the entire table (unless you specify otherwise). It certainly threw me off for a while, as the comments show. But formsets are inherently plural in ways that a single form aren't, so there's that.
Limiting the queryset is one of the things that inline formsets do.
or how the formset knew it was related until we set the acct_user for the formset. Why didn't we use the instance parameter
The formset actually never knows that it's related. Eventually the SecurityQuestionsInter objects do, once we set that model field.
Basically, the HTML form passes in the values of all its fields in the POST data - the two passwords, plus the IDs of two security question selections and the user's answers, plus maybe anything else that wasn't relevant to this question. Each of the Python objects we create (form and formset) can tell based on the field ids and the formset prefix (default values work fine here, with multiple formsets in one page it gets more complicated) which parts of the POST data are its responsibility. form handles the passwords but knows nothing about the security questions. formset handles the two security questions, but knows nothing about the passwords (or, by implication, the user). Internally, formset creates two forms, each of which handles one question/answer pair - again, they rely on numbering in the ids to tell what parts of the POST data they handle.
It's the view that ties the two together. None of the forms know about how they relate, but the view does.
Inline formsets have various special behavior for tracking such a relation, and after some more code review I think there is a way to use them here without needing to save the user before validating the security Q/A pairs - they do build an internal queryset that filters to the instance, but it doesn't look like they actually need to evaluate that queryset for validation. The main part that's throwing me off from just saying you can use them instead and just pass in an uncommitted user object (i.e. the return value of form.save(commit=False)) as the instance argument, or None if the user form is not valid is that I'm not 100% sure it would do the right thing in the second case. It might be worth testing if you find that approach clearer - set up your inline formset as you initially had it, initialize the formset in get with no arguments, then leave the final saving behavior in form_valid after all:
def form_valid(self, form, formset):
# commit the uncommitted version set in post
self.object.save()
form.save_m2m()
formset.save()
return HttpResponseRedirect(self.get_success_url())
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
if form.is_valid():
self.object = form.save(commit=False)
# passing in None as the instance if the user form is not valid
formset = SecurityQuestionsInLineFormSet(request.POST, instance=self.object)
if form.is_valid() and formset.is_valid():
return self.form_valid(form, formset)
else:
return self.form_invalid(form, formset)
If that works as desired when the form is not valid, I may have talked myself into that version being better. Behind the scenes it's just doing what the non-inline version does, but more of the processing is hidden. It also more closely parallels the implementation of the various generic mixins in the first place - although you could move the saving behavior into form_valid with the non-inline version too.

django admin - access request.user in BaseInlineFormSet

I've just created a forms.models.BaseInlineFormSet to override the default formset for a TabularInline model. I need to evaluate the user's group in formset validation (clean) because some groups must write a number inside a range (0,20).
I'm using django admin to autogenerate the interface.
I've tried getting the request and the user from the kwargs in the init method, but I couldn't get the reference.
This is what I have now:
class OrderInlineFormset(forms.models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(OrderInlineFormset, self).__init__(*args, **kwargs)
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data:
count += 1
if self.user.groups.filter(name='Seller').count() == 1:
if form.cleaned_data['discount'] > 20:
raise forms.ValidationError('Not authorized to specify a discount greater than 20%')
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You need to specify at least one item')
class OrderItemInline(admin.TabularInline):
model = OrderItem
formset = OrderInlineFormset
Then I use it as inlines = [OrderItemInline,] in my ModelAdmin.
Unfortunatly self.user is always None so I cannot compare the user group and the filter is not applied. I need to filter it because other groups should be able to specify any discount percent.
How can I do? If you also need the ModelAdmin code I'll publish it (I just avoided to copy the whole code to avoid confusions).
Well, I recognise my code there in your question, so I guess I'd better try and answer it. But I would say first of all that that snippet is really only for validating a minimum number of forms within the formset. Your use case is different - you want to check something within each form. That should be done with validation at the level of the form, not the formset.
That said, the trouble is not actually with the code you've posted, but with the fact that that's only part of it. Obviously, if you want to get the user from the kwargs when the form or formset is initialized, you need to ensure that the user is actually passed into that initialization - which it isn't, by default.
Unfortunately, Django's admin doesn't really give you a proper hook to intercept the initialization itself. But you can cheat by overriding the get_form function and using functools.partial to wrap the form class with the request argument (this code is reasonably untested, but should work):
from functools import partial
class OrderForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(OrderForm, self).__init__(*args, **kwargs)
def clean(self)
if self.user.groups.filter(name='Seller').count() == 1:
if self.cleaned_data['discount'] > 20:
raise forms.ValidationError('Not authorized to specify a discount greater than 20%')
return self.cleaned_data
class MyAdmin(admin.ModelAdmin):
form = OrderForm
def get_form(self, request, obj=None, **kwargs):
form_class = super(MyAdmin, self).get_form(request, obj, **kwargs)
return functools.partial(form_class, user=request.user)
Here's another option without using partials. First override the get_formset method in your TabularInline class.
Assign request.user or what ever extra varaibles you need to be available in the formset as in example below:
class OrderItemInline(admin.TabularInline):
model = OrderItem
formset = OrderInlineFormset
def get_formset(self, request, obj=None, **kwargs):
formset = super(OrderProductsInline, self).get_formset(request, obj, **kwargs)
formset.user = request.user
return formset
Now the user is available in the formset as self.user
class OrderInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
print(self.user) # is available here