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
Related
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.
I created the FormView below that will dynamically return a form class based on what step in the process that the user is in. I'm having trouble with the get_form method. It returns the correct form class in a get request, but the post request isn't working.
tournament_form_dict = {
'1':TournamentCreationForm,
'2':TournamentDateForm,
'3':TournamentTimeForm,
'4':TournamentLocationForm,
'5':TournamentRestrictionForm,
'6':TournamentSectionForm,
'7':TournamentSectionRestrictionForm,
'8':TournamentSectionRoundForm,}
class CreateTournament(FormView):
template_name = 'events/create_tournament_step.html'
def __init__(self, *args, **kwargs):
form_class = self.get_form()
success_url = self.get_success_url()
super(CreateTournament, self).__init__(*args, **kwargs)
def get_form(self, **kwargs):
if 'step' not in kwargs:
step = '1'
else:
step = kwargs['step']
return tournament_form_dict[step]
def get_success_url(self, **kwargs):
if 'step' not in kwargs:
step = 1
else:
step = int(kwargs['step'])
step += 1
if 'record_id' not in kwargs:
record_id = 0
else:
record_id = int(kwargs['record_id'])
return 'events/tournaments/create/%d/%d/' % (record_id, step)
The post request fails at the django\views\generic\edit.py at the get_form line, which I realize is because I've overwritten it in my FormView:
def post(self, request, *args, **kwargs):
"""
Handle POST requests: instantiate a form instance with the passed
POST variables and then check if it's valid.
"""
form = self.get_form()
if form.is_valid(): …
return self.form_valid(form)
else:
return self.form_invalid(form)
However, when I change the name of my custom get_form method to say gen_form, like so:
def __init__(self, *args, **kwargs):
form_class = self.gen_form()
success_url = self.get_success_url()
super(CreateTournament, self).__init__(*args, **kwargs)
def gen_form(self, **kwargs):
if 'step' not in kwargs:
step = '1'
else:
step = kwargs['step']
return tournament_form_dict[step]
my form class doesn't get processed in the get request and evaluates to None. I'm scratching my head as to why when I override the get_form method, it works, but my own named method doesn't? Does anyone know what the flaw might be?
Django's FormMixin [Django-doc] defines a get_form function [Django-doc]. You here thus basically subclassed the FormView and "patched" the get_form method.
Your attempt with the gen_form does not work, since you only defined local variables, and thus do not make much difference anyway, only the super(..) call will have some side effects. The other commands will keep the CPU busy for some time, but at the end, will only assign a reference to a Form calls to the form_class variable, but since it is local, you will throw it away.
That being said, your function contains some errors. For example the **kwargs will usually contain at most one parameter: form_class. So the steps will not do much. You can access the URL parameters through self.args and self.kwargs, and the querystring parameters through self.request.GET. Furthermore you probably want to patch the get_form_class function anyway, since you return a reference to a class, not, as far as I understand it, a reference to an initilized form.
Constructing URLs through string processing is probably not a good idea either, since if you would (slightly) change the URL pattern, then it is likely you will forget to replace the success_url, and hence you will refer to a path that no longer exists. Using the reverse function is a safer way, since you pass the name of the view, and parameters, and then this function will "calculate" the correct URL. This is basically the mechanism behind the {% url ... %} template tag in Django templates.
A better approach is thus:
from django.urls import reverse
class CreateTournament(FormView):
template_name = 'events/create_tournament_step.html'
def get_form_class(self):
return tournament_form_dict[self.kwargs.get('step', '1')]
def get_success_url(self):
new_step = int(self.kwargs.get('step', 1)) + 1
# use a reverse
return reverse('name_of_view', kwargs={'step': new_step})
I want to override the bahaviour of saveas button - i need after pushing it to redirecrt me not in list of objects, but in a new object directly.
So i need to override the standart save method of ModelForm and get in there the request object - to check if saveas button was pressed:
*admin.py
class AirplanesAdmin(admin.ModelAdmin):
form = AirplaneEditForm
forms.py
class AirplaneEditForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(AirplaneEditForm, self).__init__(*args, **kwargs)
def clean(self):
print self.request
return self.cleaned_data
def save(self, force_insert=False, force_update=False, commit=True,):
plane = super(AirplaneEditForm, self).save(commit=False)
print self.request
if commit:
plane.save()
return plane
class Meta:
model = Airplanes
But in both prints request is None... Did I do something wrong ?
Django forms are something between models and views, which means they are context-agnostic. You generally should not do things that depend on request objects inside your form.
What #Rohan means is that AirplanesAdmin does not pass in request objects when your form is initialized, so when you kwargs.pop('request', None) the is actually an internal KeyError and the default value (the second argument, None) is returned. Nothing is really popped from kwargs. To override this behavior, you will need to override rendering methods of ModelAdmin.
Read the doc for methods you can use.
Request object is not being passed to forms.py from admin.py
So, in admin.py:
class AirplanesAdmin(admin.ModelAdmin):
form = AirplaneEditForm(request=request)
See another example here
I am building a TemplateView with 2 forms, one to allow user to select the customer (CustomerForm) and another to add the order (OrderForm) for the customer.
Code:
class DisplayOrdersView(TemplateView):
template_name = 'orders/orders_details_form.html'
def get_context_data(self, **kwargs):
context = kwargs
context['shippingdetailsform'] = ShippingDetailsForm(prefix='shippingdetailsform')
context['ordersform'] = OrdersForm(prefix='ordersform')
return context
def dispatch(self, request, *args, **kwargs):
return super(DisplayOrdersView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
profile=request.user.get_profile()
if context['shippingdetailsform'].is_valid():
instance = context['shippingdetailsform'].save(commit=False)
instance.profile = profile
instance.save()
messages.success(request, 'orders for {0} saved'.format(profile))
elif context['ordersform'].is_valid():
instance = ordersform.save(commit=False)
shippingdetails, created = shippingdetails.objects.get_or_create(profile=profile)
shippingdetails.save()
instance.user = customer
instance.save()
messages.success(request, 'orders details for {0} saved.'.format(profile))
else:
messages.error(request, 'Error(s) saving form')
return self.render_to_response(context)
Firstly, I can't seem to load any existing data into the forms. Assuming a onetoone relationship between UserProfile->ShippingDetails (fk: UserProfile)->Orders (fk:ShippingDetails), how can I query the appropriate variables into the form on load?
Also, how can I save the data? It throws an error when saving and I have been unable to retrieve useful debug information.
Is my approach correct for having multiple forms in a templateview?
You're not passing the POST data into the forms at any point. You need to do this when you instantiate them. I would move the instantiation out of get_context_data and do it in get and post: the first as you have it now, and the second passing request.POST.
Also note that you probably want to check both forms are valid before saving either of them, rather than checking and saving each in turn. The way you have it now, if the first one is valid it won't even check the second, let alone save it, so you won't get any errors on the template if the first is valid but the second is invalid.
I am writing an Edit form, where some fields already contain data. Example:
class EditForm(forms.Form):
name = forms.CharField(label='Name',
widget=forms.TextInput(),
initial=Client.objects.get(pk=??????)) #how to get the id?
What I did for another form was the following (which does not work for the case of the previous EditForm):
class AddressForm(forms.Form):
address = forms.CharField(...)
def set_id(self, c_id):
self.c_id = c_id
def clean_address(self):
# i am able to use self.c_id here
views.py
form = AddressForm()
form.set_id(request.user.get_profile().id) # which works in the case of AddressForm
So what is the best way to pass an id or a value to the form, and that could be used in all forms for that session/user?
Second: is it right to use initial to fill in the form field the way I am trying to do it?
You need to override the __init__ method for your form, like so:
def __init__(self, *args, **kwargs):
try:
profile = kwargs.pop('profile')
except KeyError:
super(SelectForm, self).__init__(*args, **kwargs)
return
super(SelectForm, self).__init__(*args, **kwargs)
self.fields['people'].queryset = profile.people().order_by('name')
and, obviously, build your form passing the right parameter when needed :)