i'm tring to make a custom MultipleChoiceField but i have 2 problems
1st seems i can't overwrite validate function... when i try it print("i'm here") never come in my console even when that field triggers invalid_choice error
class OdsTaggerlMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return obj.tag
def __init__(self, *args, **kwargs):
super(OdsTaggerlMultipleChoiceField, self).__init__(*args, **kwargs)
self.to_field_name="tag"
self.widget.attrs['class'] = 'odstagger'
def validate(self, value):
#azione cattiva valida la qualunque
if self.required and len(value) < 1:
raise forms.ValidationError(self.error_messages['required'], code='required')
print("i'm here")
2nd problem is about queryset it's required to use that field, but i think resolving 1ts problem i can just use a empty choices list
tags = OdsTaggerlMultipleChoiceField(queryset=Tag.objects.all())
do you know how resolve that?
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 have following simple form field
class PhoneField(CharField):
widget = PhoneWidget
def clean(self, value):
value = re.sub('\D', '', super(PhoneField, self).clean(value))
if len(value) < 7:
raise ValidationError(_("Phone number is too short"), code='too_short')
return value
However, widget is still text input.
However, if I write
def __init__(self, *args, **kwargs):
kwargs['widget'] = PhoneWidget
super(PhoneField, self).__init__(*args, **kwargs)
Then it works perfectly. Digging in the code I noticed, that if widget is not specified in kwargs, then self.widget is used, but it is not a case. Why is that?
Note, that I don't pass widget in code.
field = PhoneField(label='Phone')
Found the problem.
The problem was in admin. Admin passed in kwargs default widget for CharField - widgets.AdminTextInputWidget.
I got form:
class SearchForm(Form):
owner = ModelMultipleChoiceField(queryset=User.objects.all(), required=False)
and after customizing get_queryset() of related view it works as expected but I got objects without owner. I want to add additional new choice on top of the list (0,'Without owner') so I could then filter only objects without owner.
How to add this option?
UPDATE:
I add the choice in form.__init__ and wrote custom clean method for it but if I choose added option something raises ValidationError before getting to my clean method.I'm guessing I have to override form.is_valid but I'm not sure how to do it so I can still use default is_valid method.
My code
def __init__(self, *args, **kwargs):
super(ClientListSearchForm, self).__init__(*args, **kwargs)
self.fields['owner'].choices = \
list(self.fields['owner'].choices)+[('0', 'n/a')]
def clean_owner(self):
logger.debug('CLEAN_OWNER:')
data = self.cleaned_data.get('owner')
logger.debug('data: %s' % data)
if data == 0:
logger.debug('Data zero - not assigned')
return data
users = User.objects.all()
if all(e in users for e in data):
logger.debug('Data in users - validating ok')
return data
else:
raise ValidationError('Incorrect owner')
I tried:
def is_valid(self):
try:
super(ClientListSearchForm, self).is_valid()
except ValidationError as e:
logger.debug('val error: %s' % e.args)
but it's nor validating nor caching exception
UPDATE2 Added custom validator
def userWithEmpty(value):
users = User.objects.values_list('pk').all()
v =list()
for va in value:
v.append(int(va))
u = list()
for us in users:
u.append(int(us[0]))
if not (all(e in u for e in v)or v ==0):
raise ValidationError('Invalid Value: %s' % value)
Is there a better way to convert every value in iterable than my for loops?
Didn’t post it as answer because there is a lot of place for improvement. Waiting for rants about what I'm doing wrong- and I will appreciate it all...
It STOPPED WORKING eee - there is something before validator from validators=[]
I ended up with other approach
class ModelMultipleChoiceWithEmptyField(ModelMultipleChoiceField):
def __init__(self, *args, **kwargs):
super(ModelMultipleChoiceWithEmptyField, self).__init__(*args, **kwargs)
self.choices = list(self.choices) + [('0', 'Brak')]
def clean(self, value):
if self.required and not value:
raise ValidationError(self.error_messages['required'], code='required')
if value == [u'0']:
return value
return super(ModelMultipleChoiceWithEmptyField,self).clean(value)
It's much cleaner and it works. Fell free to reuse and improve
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
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 :)