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.
Related
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"),
)
Consider this :
{% for user in users.query.all() %}
<tr>
<form method='POST' action="">
<td>{{form.username}}</td>
<td>{{form.description}}</td>
<td>{{form.submit(value="Update")}}</td>
</form>
</tr>
{% endfor %}
For each user this will create a small form that I can update, I want to populate these forms with current database data
What I tried to do in the routes file:
#app.route("/Users")
def listUsers():
users = Users
form = UserForm()
if request.method == 'GET':
for user in users.query.all():
form.username.data = user.username
form.description.data = user.description
return render_template('Users.html', users=users, form=form)
This results in having the data of the last user populating all of the forms, how can I go about fixing this ?
I was thinking of assigning an id to each form that matchs the user, but how would I be able to send a dynamic number of forms ?
It took me a while, but I got a work around, just gonna post it if anyone else has the same issue:
I used javascript ... created a function and called it within the for loop which populated the fields for me
function populateForm(username,description){
var form = document.getElementById('form id here');
form.nextElementSibling.value = username;
form.nextElementSibling.nextElementSibling.textContent = description;
}
note that I used value for input field and textContent for textfield, then inside the for loop i added a script tag
<script>
populateForm('{{user.username}}','{{user,description}}');
</script>
I'm trying to show a ChoiceField in a template on Django but I'm unable to make it work.
I have found some solutions here, but seems not work to me (Possible solution), but I get the error: too many values to unpack on line {{ form.as_p }}.
So searching on the web, I've found this Solution but I'm not able to addapt to my code and make it works. I'm getting an TextField instead a "Dropdown" (in Django Choicefield). And also, this solution list all items on a for loop and I get 4 textfields, instead 2 Choicefields with the elements.
My forms.py looks like:
class SimpleDeploy(forms.Form):
def __init__(self, networkList, policiesList, *args, **kwargs):
super(SimpleDeploy, self).__init__(*args, **kwargs)
if networkList and policiesList:
self.fields['networkPartitions'] = forms.ChoiceField(choices=networkList)
self.fields['applicationPolicies'] = forms.ChoiceField(choices=policiesList)
else:
self.fields['networkPartitions'] = forms.ChoiceField(choices='No network partitions found')
self.fields['applicationPolicies'] = forms.ChoiceField(choices='No application policies found')
And on my views.py:
def simpleDeploy(request):
netList = getDetailsNetworkPartitions(request)
polList = getDetailsApplicationPolicies(request)
if request.method == 'POST':
abs(5) #Nothing here by the moment
else:
simpleForm = SimpleDeploy(networkList=netList, policiesList=polList)
return render(request, 'apacheStratos/simpleDeploy.html', {'form': simpleForm})
Where netList and polList are list of tuples like:
[(u'application-policy-2', u'application-policy-2'), (u'application-policy-1', u'application-policy-1')]
And on my template, I'm trying to show the ChoiceField like:
<table class="table">
{% for item in form.networkPartitions.field.choices %}
<label for="">Network Partitions</label> <input type="choicefield" name="networkPartitions" value="{{item.1}}"/>
{% endfor %}
{% for item in form.applicationPolicies.field.choices %}
<label for="">Application Policies</label> <input type="choicefield" name="applicationPolicies" value="{{item.1}}"/>
{% endfor %}
</table>
How can I get the choicefield and access to the elements without using a for loop? What I'm doing wrong?
Thanks.
Thanks to #raphv, the solution was put {{form.networkPartitions}} and {{form.applicationPolicies}} on the template. So simple....
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
I use Django 1.8.4 with Python 3.4
I have a model for tournaments that defines a method which returns a string if a subscription is forbidden.
class Tournament(models.Model):
name = models.CharField(max_length=200, null=True, blank=True)
subscriptions = models.ManyToManyField('ap_users.Profile')
is_subscription_open = models.BooleanField(default=True)
# ...
def why_subscription_impossible(self, request):
if not request.user.profile.is_profile_complete():
return 'Your profile is not complete'
elif not self.is_subscription_open:
return 'Subscriptions are closed'
elif <another_condition>:
return 'Another error message'
return None
I want to display the list of tournaments, using a generic ListView, and I want to use the result of the method to modify the way it is displayed:
<table class="table">
<thead>
<td>Tournament</td>
<td>Subscription</td>
</thead>
{% for tournament in tournament_list %}
<tr>
<td>{{ tournament.name }}</td>
<td>
{% if tournament.why_subscription_impossible %}
{{ tournament.why_subscription_impossible }}
{% else %}
Subscribe
{% endif %}
</td>
</tr>
{% endfor %}
</table>
The view is a class based generic view inherited from generic.ListView.
class IndexView(generic.ListView):
template_name = 'ap_tournament/index.html'
def get_queryset(self):
return Tournament.objects.all()
The shown solution doesn't work, because I need to pass the current request, to get information about logged user. So I tried to add the result of the method to a context in the view
class IndexView(generic.ListView):
template_name = 'ap_tournament/index.html'
def get_queryset(self):
return Tournament.objects.all()
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
additional_ctx_info = []
for tournament in self.get_queryset():
additional_ctx_info.append({
'reason_to_not_subscribe': tournament.why_subscription_impossible(self.request)
})
context['subscr_info'] = additional_ctx_info
return context
Obviously, this doesn't work too. I don't know how to access to the subscr_info[n] with n the current index in the tournament_list. I know the forloop.counter0 to get the index, but I can't use it in the template (or I don't know how). I tried :
{{ subscr_info.forloop.counter0.reason_to_not_subscribe }}
{{ subscr_info.{{forloop.counter0}}.reason_to_not_subscribe }}
I also tried to annotate the QuerySet in get_queryset() view method and read about aggregate(), but I feel that works only with operations supported by the database (AVG, COUNT, MAX, etc.).
I also feels that using a filter or a template tag will not work in my case since I need to use the result of the method in a if tag.
Is there a better solution or a completely diffferent method to achieve what I want ?
In your view, you could also do:
tournaments = self.get_queryset()
for tournament in tournaments:
tournament.reason_to_not_subscribe = tournament.why_subscription_impossible(self.request)
Then add tournaments to the context.
You have to create tournament_list in your views.py file, and set it depending on whether the user is logged and has the corresponding permissions.
If you need to count something, you can create the following Counter class :
class Counter:
count = 0
def increment(self):
self.count += 1
return ''
def decrement(self):
self.count -= 1
return ''
You can then use it in your templates by calling {{ counter.increment }} and {{ counter.count }}. ({{ subscr_info.{{counter.count}}.reason_to_not_subscribe }} and don't forget to place {{ counter.increment }} in your loop.
However, a nicer workaround I used was to create a dictionnary containing both the main element and the additional information, i.e.
ctx_info = []
for tournament in self.get_queryset():
ctx_info.append({
'tournament': tournament
'reason_to_not_subscribe': tournament.why_subscription_impossible(self.request)
})
and then loop on ctx_info. It's cleaner, but I however do not know how this can be implemented within a ListView (which I never used)
By the way, your template contains {{ why_subscription_impossible }} instead of {{ tournament.why_subscription_impossible }}, I don't know if it was intended...