I'm currently work on some project in pyramid and have problem with wtforms SelectField.
I have a 3 SelectField fields:
car_make (e.g., "audi")
car_model (e.g., "audi 80")
car_version (e.g., "AUDI 80 B4").
The car_make choices I can load in the view. The choices for rest of SelectFields (car_model, car_version) I will load on the client side via AJAX/javascript (I can choose car_model when car_make is selected and so on).
The problem is that when I submit the form, car_model and car_version raise 'Not valid choice' because (in SelectField.pre_validation line 431) self.choices is empty.
How can I get around this problem?
What you are asking to do is have WTForms handle "cascading selects" (having the valid fields of one choice be determined by the value of another field). There really isn't a good way using the built in fields.
The SelectField in WTForms does NOT provide you with an option to say "Don't validate that the choice supplied is valid". You MUST provide choices in order for the field to validate the choice.
As shown in the docs, while you typically could fill the choices field with a static list of choices...
class PastebinEntry(Form):
language = SelectField(u'Programming Language', choices=[('cpp', 'C++'), ('py', 'Python'), ('text', 'Plain Text')])
...however, since you are dynamically coming up with the options, you need to set the choices attribute after instantiating the form.
def edit_user(request, id):
user = User.query.get(id)
form = UserDetails(request.POST, obj=user)
form.group_id.choices = [(g.id, g.name) for g in Group.query.order_by('name')]
In the above sample, the choices for "group_id" is filled dynamically in what would be your Pyramid view. So, that's what you would need to do: you need to fill the choices in your view. This is how you can fix your issue with car_make (although I think in your question you said that car_make was okay).
The problem that you have, however, is that the valid choices for car_model cannot be determined, since they depend on car_make having already been parsed and validated. WTForms doesn't really handle this well (at least with SelectFields) because it assumes that all of the fields should be validated at once. In other words, in order to generate the list of valid choices for car_model, you first need to validate the value for car_make, which is not possible to easily do given how the SelectField works.
The best way I see doing this is to create a new field type that extends the SelectField type, but removes the validation:
class NonValidatingSelectField(SelectField):
def pre_validate(self, form):
pass
This new type overrides pre_validate, which typically does the check to determine if a choice is valid.
If you use this for car_model, you won't have the error anymore. However, this now means that your field isn't actually being validated! To fix this, you can add an in-line validator on your form...
class MyForm(Form):
car_make = SelectField(u'Make', choices=[...])
car_model = NonValidatingSelectField(u'Model', choices=[])
def validate_car_model(self, field):
choices = query_for_valid_models(self.car_make.data)
# check that field.data is in choices...
You might need to tweak this a bit to get it to work exactly how you want, I haven't actually tested that this works.
Related
I'm writing a function that updates one of three possible fields in Django--either a boolean, a string, or an integer.
I attempted to to it this way:
class AccountsForm(forms.Form):
update_key = forms.CharField(required=True)
update_value = forms.ComboField(fields=[forms.CharField(),
forms.IntegerField(), forms.BooleanField()],
required=True)
But this is not the functionality I'm looking for, as it expects a combination of a CharField+IntegerField+BooleanField. I need at exactly one of the three fields to be present in the form. Is this possible with django forms?
Yes there is. The Django docs actually show you how to do that:
Use three separate fields
Make all of them not required (required=False)
Add a clean() method to your form where you validate the three fields together. If you first call super().clean(), you can be sure that the fields have been validated separately first, e.g. that the input to the IntegerField is actually an integer, so you don't need to do that anymore.
Use the cleaned_data dictionary to look at the values for each field. You could also check self.errors[<field>] to see if any of the fields was filled in incorrectly.
Raise a ValidationError is you're not happy with the input.
In your template, don't forget to display the {{ form.non_field_errors }} at the top of your form so the user knows the error you're raising. Or just add the error on specifically one of the fields to display it close to the field.
I have a form with a foreign key field. The field is disabled and I initialized the value with initial. The value shows up but when I submit it, an error shows up: 'This field is required'
views.py:
def updateTicket(request, ticket_id):
ticketDetails = editTicket1.objects.filter(ticketID=ticket_id).last()
updateTrouble = editTrouble(request.POST or None, instance=ticketDetails, initial={'ticketID': ticket_id})
if updateTrouble.is_valid():
updateTrouble.save()
forms.py:
ticketID = forms.ModelChoiceField(queryset=tblTicket.objects.all(),widget=forms.Select(attrs={'disabled':'disabled'}))
How come when I disable the field, a foreign key field, the server does not accept the value. Even with initialization, when posted, it submits a blank value.
from thee django docs, it states this for the form field: "By default, each Field class assumes the value is required, so if you pass an empty value – either None or the empty string ("") – then clean() will raise a ValidationError exception:"
So, instead you should have done this; ticketID = forms.ModelChoiceField(required:False, queryset=tblTicket.objects.all(),widget=forms.Select(attrs={'disabled':'disabled'}))
or in your Models you'd have set a blank argument to the field to False, but I'll suggest to do it in the Form.
No sure if this is the issue but I think your form will always by showing the ticket_id since this is given in the initial form option which overrides the instance.
It's described here in the docs: https://docs.djangoproject.com/en/1.8/topics/forms/modelforms/#providing-initial-values
Also - if you want to correctly process the form I think you should expand the update ticket function like it is described in the docs:
https://docs.djangoproject.com/en/1.8/topics/forms/#the-view
The difference between the above example and your code will be in the else statement where instead of a blank form you initiate it using initial data like you provided in your code.
Hope that helps!
I think I have a pretty common use case and am surprised at how much trouble it's giving me.
I want to use a key-value pair for a ReferenceField in the Flask-Admin edit form generated by the following two classes:
class Communique(db.Document):
users = db.ListField(db.ReferenceField(User), default=[])
class User(db.Document):
email = db.StringField(max_length=255, required=True)
def __unicode__(self):
return '%s' % self.id
I want the select to be constructed out of the ObjectId and the an email field in my model.
By mapping the __unicode__
attribute to the id field I get nice things on the mongoengine side like using the entire object in queries:
UserInformation.objects(user=current_user)
This has the unfortunate effect of causing the Flask-Admin form to display the mongo ObjectId in the edit form like so:
The docs say I have to provide the label_attr to the ModelSelectMultipleField created by Flask-Admin. I've done so by overriding the get_form method on my ModelView:
def get_form(self):
form = super(ModelView, self).get_form()
form.users = ModelSelectMultipleField(model=User,
label_attr='email',
widget=form.users.__dict__['kwargs']['widget'])
return form
I'm reusing the the widget used by the original form.users (which may be wrong). It works fine when editing an existing item, BUT throws an exception when creating a new one (perhaps because I'm reusing the widget).
All of this seems like way more work than should be needed to simply provide a label_attr to my SelectField. Fixing up the listing view was a simple matter of adding an entry to the column_formatters dictionary. Is there no simple way to specify the label_attr when creating my ModelView class?
I know I could make this problem go away by returning the email property in the __unicode__ attribute, but I feel like I shouldn't have to do that! Am I missing something?
Oy, now I see how to do it, though it's not that obvious from the docs. form_args is a dictionary with items keyed to the form models. All I needed to do was...
form_args = dict(users=dict(label_attr='email'))
Which does seem about the right amount of effort (considering Flask-Admin isn't some sort of java framework).
I have a form, which i am rendering. It also allow user to add a new instance if it does not exist in the list.
lets say the field is name from category modal.
name is an drop down list, and if user does not find his value in the list, he writes the name in the next input box.
When i validate it, it fails. because its not an instance of category.
Then I got it by using self.data and i compare if its an Integer, or not?
If its an integer, than its simply an existing element, if not, the user might have chosen a new User, and thus by getting it from the self.data, i can create the
category
object and save it and replace the original value with the ID so that validation does not fail.
Problem.
I know using self.data and using it for a db query can be dangerous. As user is allowed to input anything in it, and it might empty my database with that query, (i have heard it like that). Is this really possible? if yes, how to avoid that.
Hope someone can give me an idea.
Suppose your form has a name field for existing category names and a new_name field for the input box. In your form's clean() method, you'll want to check to see if self.cleaned_data has a value for new_name and act accordingly. You might do:
self.cleaned_data['name'] = self.cleaned_data['new_name']
for example.
Unless you override other field or form methods, the data in these fields are 'safe' as far as malicious input is concerned. If you raise a ValidationError in the form's clean() method, is_valid() will return false. So, as long as you:
form = YourForm(...)
if form.is_valid():
form.save()
The save() will not be called with invalid data.
This should be pretty straightforward but I'm not figuring it out from the Django documentation. I have an IntegerField Model Field from which I've created a ModelForm Field. The field in question contains $ values (price) and it would be preferable from a UX standpoint if the user did not receive a error message ('enter whole number') when they input $10 instead of 10.
I've tried manual form cleaning but it seems that the clean_field method is run after other validation methods. My reading so far seems to confirm that as well.
def clean_bill(self):
bill = self.cleaned_data["bill"]
if '$' in bill:
bill=bill.replace('$','',1)
return bill
Is there a way around this while maintaining the IntegerField in the modelform? Should I just make it a RegexField?
EDIT: I ended up going with a combination of the above and a RegexField. Still curious if there is another/better way.
bill= forms.RegexField(label=_("Bill"), max_length=10,required=True, regex=r'^\$?[\d]+$',
error_messages = {'invalid': _("Please enter a whole number.")})
Create an IntegerField in the model, and a CharField in the form.
You can still use modelform to do this but you'll have to:
'exclude' the model field from the form,
write a clean_bill method in the form
set the value of the model's field to the parsed integer value
try this and/or this
Yes, you are right. According to the Django docs of form and field validation it will not even go to your clean method and already raise ValidationError in the first step (to_python method).
There is nothing much you can do on the form level I think. What you could do though is processing the POST data in the view before passing it to the form. Some simple example:
post_dict = request.POST.copy() #makes a copy of request.POST so you can modify it
bill = post_dict['bill']
if '$' in bill:
bill = bill.replace('$','',1)
post_dict['bill'] = bill
# pass post_dict to your form instead of request.POST
...