SelectField WTForm rendering on Template - python

I've been struggling for nearly a day on a simple rendering of a form field. That would be great if you could help me on this one.
I'm using Flask-WTF, python 2.7.
I'm trying to render a SelectField using a custom ListWidget. The field basically must be rendered within a UL HTML tag, rather than a SELECT html tag, and this seems to be what I'm struggling with.
This is how my custom widget class is:
widget.py
class CustomListWidget(ListWidget):
def __init__(self, *args, **kwargs):
super(CustomListWidget, self).__init__(*args, **kwargs)
def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)
html = ['<{} {}>'.format(self.html_tag, html_params(**kwargs))]
for subfield in field:
html.append('<li><span>{}</span></li>'.format(subfield()))
html.append('</{}>'.format(self.html_tag))
return HTMLString(''.join(html))
This is how my form looks, and the category field is the one I'm struggling with.
form.py
from widget import CustomListWidget
from wtforms import SelectField, SubmitField
widget = CustomListWidget(html_tag='ul')
class MyForm(Form):
category = SelectField('category', [DataRequired()], widget=widget, default='1', choices=[
('1', 'whatever'),
('2', 'whatever2')
])
submit = SubmitField('Search')
view.py
from form import MyForm
from flask import render_template
#app.route('/formtest', methods=['GET', 'POST'])
def formtest():
form = MyForm()
if request.method == 'GET':
render_template('form.html', form=form)
if form.validate_on_submit():
return redirect(url_for('whatever', data=-form.data))
return 'form not validated'
form.html
<div class="search-input with-dropdown">
<div class="dropdown">
{{ form.category(class='dropdown-content hide') }} }}
</div>
</div>
With this code I'm able to get the display expected, but no value are being passed from the field. Whichever value I select, after I submit, only the default value is there.

I've done something similar recently with a loop to iterate over all the choices.
Basically, I've created a SelectMultipleField copy to have my own html for it as we needed specific stuff.
As you can see in the snipped in bellow, I iterate over the field.iter_choices().
{%- if field.type in ['SelectMultipleField'] %}
<ul>
{% for key, value, checked in field.iter_choices() %}
<li>{{ value }}</li>
{% endfor %}
</ul>
{%- endif %}
You can read more about custom widgets in http://wtforms.simplecodes.com/docs/0.6/widgets.html

Related

Is it possible to add an input field to Wagtails custom bulk actions?

Is it possible to add an input field to Wagtails custom bulk actions?
In the template from the documentation example there is a block called form_section. Here I want to add a separate form to add another input field. Another position would be possible as well, of course.
<!-- /path/to/confirm_bulk_import.html -->
# ...
{% block form_section %}
{% if images %}
{% trans 'Yes, import' as action_button_text %}
{% trans "No, don't import" as no_action_button_text %}
# Can I use my own confirmation form here? How about its view?:
{% include 'wagtailadmin/bulk_actions/confirmation/form.html' with action_button_class="serious" %}
{% else %}
{% include 'wagtailadmin/bulk_actions/confirmation/go_back.html' %}
{% endif %}
{% endblock form_section %}
I would love to bulk select Image instances to add them to a Page. So I need to have a ChoiceField to select the Page. This would also require a customized View for the logic behind this "import". The latter is not the question. I am just wondering how I can add this input field and alter the view of a these marvelous bulk actions.
Standard bulk actions for images in Wagtail also include "Add images to collection":
The following is how the second step of this action looks like. I would love to add a custom bulk action in this sense to add images to a page (via a ImagePageRelation / InlinePanel)
Wagtail admin portal is using pure HTML and CSS. So everything coming to the python side is received via a HTML form. That means every button click in UI should associate with a HTML form and from wagtail side you can find it in the request.
Execute Action Method
If you went through the bulk action documentation, you will find that after the form is submitted, execute_action class method will be executed. Now you need to understand the parameters of this method.
#classmethod
def execute_action(cls, objects, **kwargs):
raise NotImplementedError("execute_action needs to be implemented")
As this is a class method, the first parameter is the class type which this method is on. You can learn more about class methods in the python documentation.
The 2nd parameter objects is the list of objects that you have selected for this bulk operation. To be precise, this is the list of objects that you have selected with the correct permission level. In the default implementation, permission is given for all the objects. But you can override this behavior.
def check_perm(self, obj):
return True
You can override this method in your custom bulk action class and check permission for each object. As the objects parameter, you will receive the only objects which have check_perm(obj)==True, from the list of objects you selected.
The 3rd parameter of execute_action class method is a keyworded argument list (a dictionary to be precise). This dictionary is obtained by calling the following method.
def get_execution_context(self):
return {}
Default behavior of this method is to return empty dictionary. But you can override this to send anything. Because execute_action is a class method, it can't access the instant variables. So this method is very helpful to pass instance variables to execute_action class method.
Lets look at an example.
#hooks.register('register_bulk_action')
class CustomBulkAction(ImageBulkAction):
display_name = _("A Thing")
aria_label = _("A thing to do")
action_type = "thing"
template_name = "appname/bulk/something.html"
def get_execution_context(self):
print(self.request)
return super().get_execution_context()
If you run this example, you can see the data submitted from the HTML form.
<WSGIRequest: POST '/admin/bulk/image/customimage/thing/?next=%2Fadmin%2Fimages%2F&id=1'>
Override the HTML Form
In the bulk action template, you can't find any HTML <form></form> tag. It is because the form with action buttons are in wagtailadmin/bulk_actions/confirmation/form.html file that you have import in the template. You can create the copy of that file and change it's behavior.
<form action="{{ submit_url }}" method="POST">
{% include 'wagtailadmin/shared/non_field_errors.html' %}
{% csrf_token %}
{% block form_fields %}
<!-- Custom Fields goes here -->
{% endblock form_fields %}
<input type="submit" value="{{ action_button_text }}" class="button {{ action_button_class }}" />
{{ no_action_button_text }}
</form>
You can add custom fields you need in the area that I mentioned above sample code and values of those additional fields will be there in self.request.POST parameter. This is the easiest way to get something from the template to python side.
Django Forms
But that is not the best way. Django recommends using forms for these purposes. You can find more about Django forms in the documentation.
Almost every place that there is a form in a wagtail template, there is a associated Django form. In this case, the instance variable form_class is used to associate a bulk action template with a Django form.
class MyForm(forms.Form):
extra_field = forms.CharField(
max_length=100,
required=True,
)
#hooks.register('register_bulk_action')
class CustomBulkAction(ImageBulkAction):
display_name = _("A Thing")
aria_label = _("A thing to do")
action_type = "thing"
template_name = "appname/bulk/something.html"
form_class = MyForm
def get_execution_context(self):
print(self.cleaned_form.data)
return super().get_execution_context()
And very simply, I will add all the form fields to the template as in the below sample code.
<form action="{{ submit_url }}" method="POST">
{% include 'wagtailadmin/shared/non_field_errors.html' %}
{% csrf_token %}
{% block form_fields %}
{% for field in form %}
<div class="fieldWrapper">
{{ field.label_tag }} {{ field }}
{{ field.errors }}
</div>
{% endfor %}
{% endblock form_fields %}
<input type="submit" value="{{ action_button_text }}" class="button {{ action_button_class }}" />
{{ no_action_button_text }}
</form>
Now this will print the data received from the HTML form. What we need to do is to pass the form data as kwargs to the execute_action class method.
Final Example
#hooks.register('register_bulk_action')
class CustomBulkAction(ImageBulkAction):
display_name = _("A Thing")
aria_label = _("A thing to do")
action_type = "thing"
template_name = "appname/bulk/something.html"
form_class = MyForm
def get_execution_context(self):
data = super().get_execution_context()
data['form'] = self.cleaned_form
return data
#classmethod
def execute_action(cls, objects, **kwargs):
print("KWARGS:", kwargs)
print(kwargs['form'].cleaned_data['extra_field'])
# Do what you want
return 0, 0
I believe this was helpful and answered all the questions related to bulk action submission.
With forms.ModelChoiceField in your form, you can get values from Django Models and pass them to the HTML field. You have to pass a queryset in the constructor.
extra_field = forms.ModelChoiceField(
required=True,
queryset=Collection.objects.order_by("name"),
)

Stuck with django form validation

I'm trying to get validation running on a django form used to retrieve a list of objects in a ListView View. Despite having read django docs and many other questions here, I can't find out what's wrong in this simple test code:
form.html
<form action="list.html" method="get">
{{ form }}
<input type="submit" value="Submit">
</form>
list.html
<ul>
{% for area in object_list %}
<li>{{ area.name }}</li>
{% endfor %}
</ul>
forms.py
from django import forms
class SearchArea(forms.Form):
area = forms.CharField(label='Area code', max_length=6)
def clean_area(self):
area = self.cleaned_data['area'].upper()
if '2' in area:
raise forms.ValidationError("Error!")
return area
views.py
class HomePageView(FormView):
template_name = 'form.html'
form_class = SearchArea
class AreaListView(ListView):
template_name = 'list.html'
model = AreaCentral
def get_queryset(self):
q = self.request.GET.get('area')
return AreaCentral.objects.filter(area__istartswith=q)
When I try to submit something like "2e" I would expect a validation error, instead the form is submitted. Moreover I can see in the GET parameters that 'area' is not even converted to uppercase ('2E' instead of '2e').
The default a FormView will only process the form on POST; the GET is for initially displaying the empty form. So you need to use method="post" in your template form element.
Your action attribute is also suspect; it needs to point to the URL of the form view. If that actually is the URL, note it's not usual to use extensions like ".html" in Django URLs, and I would recommend not doing so.

FormSet rendering always two forms as default Django

I have the following code for my FormSet:
Forms.py
class RequiredFormSet(BaseFormSet):
def __init__(self, *args, **kwargs):
super(RequiredFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.empty_permitted = False
CityFormSet = formset_factory(CityNameForm, min_num=1, max_num=5, validate_min=True, validate_max=True, formset=RequiredFormSet)
Template.html
{{ city_formset.management_form }}
{% for city_form in city_formset %}
{{ city_form.id }}
<div>
{{ city_form.city_name }}
</div>
<div>
{{ city_form.region }}
</div>
{% endfor %}
Creating and deleting forms in the formset is handled by JavaScript, but it is only activated when a button is clicked. If the user edits the JavaScript and removes the condition where it verifies how many forms the page must have, he can submit the page without any form in the formset.
If I set min_num=0 in the constructor, the user is able to use the scheme above and go over the validation. If I set it to min_num=1, he can't but two forms are generated within the formset.
Sure, just change this line:
CityFormSet = formset_factory(CityNameForm, min_num=1, max_num=5, validate_min=True, validate_max=True, formset=RequiredFormSet)
to this:
CityFormSet = formset_factory(CityNameForm, min_num=1, max_num=5,
validate_min=True, validate_max=True,
formset=RequiredFormSet, extra=0)
Also made your line a bit shorter so you don't have to scroll so much.
in formset_factory has option with extra, you can utilize as extra=2

Flask WTF (Python) Size-varying list of lists of fields not working (unbound error)

I'm trying to make a uncertainty calculator, where I need a variable number of Fields (this is the final idea). However, while testing for number of 1 field of fields, I came across a issue. Instead of the fields being rendered in the page, there is only some random code in its place:
I tried checking if the issue was related to them having the same name "values" or something, but it seems that is not the issue. I don't know what to try anymore.
forms.py
from flask.ext.wtf import Form
from wtforms import StringField, BooleanField, DecimalField
from wtforms.validators import DataRequired
class Receiver(Form):
expression = StringField('expression', validators=[DataRequired()])
# ve_list = [[StringField('expreson'), DecimalField('expression', places=10)], [StringField('expreson'), DecimalField('expression', places=10)]]
# remember_me = BooleanField('remember_me', default=False)
ve_list = [[DecimalField('value', validators=[DataRequired()]), DecimalField('value', validators=[DataRequired()])]]
The views.py:
from flask import render_template, flash, redirect
from app import app
from .forms import Receiver
#app.route('/login', methods=['GET', 'POST'])
def login():
form = Receiver()
return render_template('request.html',
title='Calculate',
form=form)
request.html:
{% block content %}
<h1>Error propagation calculator</h1>
<form action="" method="post" name="login">
{{ form.hidden_tag() }}
<p>
Please enter the expression:<br>
{{ form.expression }}<br>
</p>
Enter the value and respective error:<br>
{% for ve in form.ve_list %}
{{ ve[0] }} +/- {{ ve[1] }}<br>
{% endfor %}
<p><input type="submit" value="Calculate"></p>
</form>
{% endblock %}
Fields are classes and need to be called in order to run their call method which will render the html to your page.
Example 1:
{{ form.expression() }}
Your field is rendering but it is best to call the field properly.
Edited:
Your list of fields is not going to work because you need to have an instantiated class attached to the form class attribute. When you load up the field like this it is an UnboundField.
I recommend adding fields dynamically in your view. You can see an answer to that problem here.
The fields that are part of the form need to be class variables of the form class. If they are not, then WTForms is not going to find them, so they are never bound to the form.
If you want to add a list of fields, you can do so by setting the attributes on the Form class. Something like this:
class Receiver(Form):
expression = StringField('expression', validators=[DataRequired()])
setattr(Receiver, 've_list0', DecimalField('value', validators=[DataRequired()]))
setattr(Receiver, 've_list1', DecimalField('value', validators=[DataRequired()]))
Then in the template, you can iterate on the form fields to render them, instead of rendering them one by one.

How to render django form field in template

I want to make a page with a list of users and checkboxes that signal if a user is selected, which will apply some action to selected users.
I created a form class which looks like this:
#in forms.py
class UserSelectionForm(forms.Form):
"""form for selecting users"""
def __init__(self, userlist, *args, **kwargs):
self.custom_fields = userlist
super(forms.Form, self).__init__(*args, **kwargs)
for f in userlist:
self.fields[str(f.id)] = forms.BooleanField(initial=False)
def get_selected(self):
"""returns selected users"""
return filter(lambda u: self.fields[str(u.id)], self.custom_fields)
In my template I have users listed in a table and I want the last column of this table to be those checkboxes. I need to render fields one by one depending on their name.
I tried creating a template tag that would return the html code of the needed form element:
#in templatetags/user_list_tags.py
from django import template
register = template.Library()
#this is django template tag for user selection form
#register.filter
def user_select_field(form, userid):
"""
returns UserSelectionForm field for a user with userid
"""
key = std(userid)
if key not in form.fields.keys():
print 'Key %s not found in dict' % key
return None
return form.fields[key].widget.render(form, key)
Finally, here's the template code:
<form action="" method="post">
{% csrf_token %}
<table class="listtable">
<tr>
<th>Username</th>
<th>Select</th>
</tr>
{% for u in userlist %}
<tr>
<td>{{u.username}}</td>
<td>{{select_form|user_select_field:u.id}}</td>
</tr>
{% endfor %}
</table>
<p><input type="submit" value="make actions" /></p>
However, this does not bind those widgets to the form and thus, after submitting the form, validation fails. The error message says that all the custom fields are required.
So here are my questions:
What is the right way to render separate form fields?
What is the right way of creating such a form with checkboxes? (I mean maybe my method is stupid and there is a much easier way of achieving what I want.
You're making the template far too complicated. Add a label to each field when you create it in the form's __init__ method.
for f in userlist:
self.fields[str(f.id)] = forms.BooleanField(label=f.username, initial=False)
Then just loop over the fields in the form and don't worry about the userlist anymore.
{% for field in form %}
<tr>
<td>{{ field.label_tag }}</td>
<td>{{ field }}</td>
</tr>
{% endfor %}
Ok So I think I have found a way to correctly render separate form fields. I found it watching django sources. Django.forms.forms.BaseForm class has _html_output method which creates an instance of Django.forms.forms.BoundField and then adds unicode(boundField) to the html output. I did the exact same thing and it worked perfectly:
#in templatetags/user_list_tags.py
from django import template
from django import forms
register = template.Library()
#this is djangp template tag for user selection form
#register.filter
def user_select_field(form, userid):
"""
returns UserSelectionForm field for a user with userid
"""
key = str(userid)
if key not in form.fields.keys():
print 'Key %s not found in dict' % key
return None
#here i use BoundField:
boundField = forms.forms.BoundField(form, form.fields[key], key)
return unicode(boundField)
That generated the same html as {{form.as_p}}, so the POST request will look exactly the same and form will be processed correctly.
I also fixed some mistakes in my form class:
#in UserSelectionForm definition:
...
#__init__
for f in userlist:
self.fields[str(f.id)] = forms.BooleanField(initial=False, required=False)
#get_selected
return filter(lambda u: self.cleaned_data[str(u.id)],
self.custom_fields)
That now seems to work as I planned, without any javascript.

Categories

Resources