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.
Related
I currently have fully functional commenting form in my blog post view that I want to display in the ListView. Sort of like linkedin has under every list item, if you have noticed, i think facebook has the same thing.
Is there a shortcut to achieve this?
I supposed you can combine a ListView with a FormMixin (https://docs.djangoproject.com/fr/4.1/ref/class-based-views/mixins-editing/#django.views.generic.edit.ModelFormMixin)
In each item of list, you create your form html and checking if form exist and if form instance corresponds to current list view for displaying errors and data in case of invalid form sent.
class MyPostList(FormMixin, ListView);
model = Post
form = CommentAddForm
template...
class CommentAddForm(ModelForm):
class Meta:
model = Comment
fields = ('post_id', 'txt'...)
{% for post in post_list %}
{{post}}
<form>
{% if form.data.post_id == post.pk %}{{form.errors}}{% endif %}
<input type="hidden" name="post_id" value="{{post.pk}}" />
</form>
{% endfor %}
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"),
)
I have two models User and Group.
I'm implementing an action "Change Groups" in UsersAdmin that redirects to an intermediate page with 2 MultipleChoiceFields for Groups, that I want to be used to either remove users from certain groups, add users to other groups, or do both in one go (i.e. move them).
The docs are very short about this subject, so in order to do this, I'm following this article.
Here's my form:
class ChangeUsersGroupsForm(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
from_groups = forms.ModelMultipleChoiceField(Group.objects, required=False)
to_groups = forms.ModelMultipleChoiceField(Group.objects, required=False)
My admin action:
def change_groups_action(self, request, queryset):
if 'apply' in request.POST:
from_groups = request.POST["from_groups"]
to_groups = request.POST["to_groups"]
from_groups_qs = Group.objects.filter(pk__in=from_groups).all()
to_groups_qs = Group.objects.filter(pk__in=to_groups).all()
user_ids = [u.user_id for u in queryset]
# task that will do the job of actually moving the users
change_users_groups.delay(from_groups_qs, to_groups_qs)
self.message_user(request, "Changed groups of %s users" % len(user_ids))
return HttpResponseRedirect(request.get_full_path())
form = ChangeUsersGroupsForm(initial={'_selected_action': queryset.values_list('id', flat=True)})
return render(request, "admin/change_users_groups.html", {'queryset': queryset, 'form': form})
change_groups_action.short_description = "Change Groups"
Here's my template:
<!-- users/templates/admin/change_users_groups.html -->
{% extends "admin/base_site.html" %} {% block content %}
<form action="" method="post">
{% csrf_token %}
{{ form }}
<br />
<br />
<p>The Group changes will be applied to the following users:</p>
<ul>
{{ queryset|unordered_list }}
</ul>
<input type="hidden" name="action" value="change_groups_action" />
<input type="submit" name="apply" value="Confirm" />
</form>
{% endblock %}
This is how the intermediate page renders:
First (but minor) issue is that the form fields are displayed in a row, instead of each in one row. But let's skip that for now.
The big issue is that when I select a Group, nothing happens, the Group doesn't seem to be selected.
Instead I see the following error on the browser Console:
Uncaught TypeError: django.jQuery is not a function
This error is printed every time I click on an option.
Anyone knows what's going on?
Django 2.2
Python 3.8.10
I've had this problem before, but in different situation (unrelated to Django admin). I almost always turned out to be because JQuery was not loaded or it was loaded too late in the template.
According to Django documentation:
If you want to use jQuery in your own admin JavaScript without
including a second copy, you can use the django.jQuery object on
changelist and add/edit views. Also, your own admin forms or widgets
depending on django.jQuery must specify js=['admin/js/jquery.init.js',
…] when declaring form media assets.
So that would make your form class look like:
class ChangeUsersGroupsForm(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
from_groups = forms.ModelMultipleChoiceField(Group.objects, required=False)
to_groups = forms.ModelMultipleChoiceField(Group.objects, required=False)
class Media:
js = ['admin/js/jquery.init.js']
Regarding your form field form. I suggest rendering each field separately like so:
{{ form.from_groups }}
<br/>
{{ form.to_groups }}
This seems like the simplest solution
Let me know if that helps :)
I am working with django forms and am using django bootstrap form(https://django-bootstrap-form.readthedocs.org/en/latest/) for UI. I am able to create forms in html using the django bootstrap form. Now the problem is that i want to edit the forms and update the record in the database.
My question is how can i use the django bootstrap form to provide a form for editing
for eg:
i am using
<form role="form" action="/abc/" method="POST">{% csrf_token %}
<div class="form-group">
{{ form.name.errors }}
{{ form.name|bootstrap }}
</div>
</form>
this is the form when filling for the first time. When i click on the edit option i want the same UI but with value="the_value_saved_in_the_database" something like
{{ form.name|bootstrap value="_data_"}}
How can i achieve it?
Hope you understand the problem.
Thanks in advance
You need to load the form with data (called binding the form) before you render it. If the form represents some data that you have stored in the model, then create a ModelForm and pass in the model instance for which you want to edit the data.
Here is an example:
class AddressBook(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField()
class AddressForm(forms.ModelForm):
class Meta:
model = AddressBook
def edit_address(request, pk=None):
existing_entry = AddressBook.objects.get(pk=pk)
form = AddressForm(instance=existing_entry)
return render(request, 'edit.html', {'form': form})
In urls.py:
url('^address/edit/(?P<pk>\d+)$', 'edit_address', name="edit"),
url('^address/save/$', 'save_address', name="save"),
Now, when you call your view http://localhost:8000/address/edit/1, the form will be populated by the data for the entry whose primary key is 1 ready for editing. In your template, simply render the form, and point it to the save view:
<form method="post" action="{% url 'save' %}">
{% csrf_token %}
{{ form|bootstrap }}
</form>
If you are going to be doing this often, its easier to use the generic class based views (like CreateView, EditView) to simplify your code.
I'm familar with using templates to collect the data, but on displaying is there a smart way that Django will display the fields and populate them with the right values. I can do it manually of course, but the model knows the field type. I didn't see any documentation on this. For example I collect data from the template with:
<strong>Company Name</strong>
<font color="red">{{ form.companyname.errors }}</font>
{{ form.companyname }}
where form is my company model containing all the fields. How would I go about ensuring that I could use this type of methodology such that Django would render the text fields and populate with the current values. For example is there a way to send in values in the following way:
myid = int(self.request.get('id'))
myrecord = Company.get_by_id(myid)
category_list = CompanyCategory.all()
path = os.path.join(os.path.dirname(__file__), 'editcompany.html')
self.response.out.write(template.render(path, {'form': myrecord, 'category_list': category_list}))
Can I do the same this with records and will the template populate with values sent in? Thanks
It sounds like you may be confused about the difference and proper usage of Form vs ModelForm
Regardless of which type of form you use, the templating side of forms stays the same:
Note: all of the values in your form (as long as its bound to POST or has an instance) will be prepopulated at render.
<form class="well" action="{% url member-profile %}" method="POST" enctype="multipart/form-data">{% csrf_token %}
<fieldset>
{{ form.non_field_errors }}
{{ form.display_name.label_tag }}
<span class="help-block">{{ form.display_name.help_text }}</span>
{{ form.display_name }}
<span class="error">{{ form.display_name.errors }}</span>
{{ form.biography.label_tag }}
<span class="help-block">{{ form.biography.help_text }}</span>
{{ form.biography }}
<span class="error">{{ form.biography.errors }}</span>
<input type="submit" class="button primary" value="Save" />
</fieldset>
</form>
if you want to be populating a form from a record (or submit a form as a record) its probably best to use ModelForm
EX a profile form that doesn't display the User FK dropdown:
class ProfileForm(forms.ModelForm):
"""Profile form"""
class Meta:
model = Profile
exclude = ('user',)
The View:
def profile(request):
"""Manage Account"""
if request.user.is_anonymous() :
# user isn't logged in
messages.info(request, _(u'You are not logged in!'))
return redirect('member-login')
# get the currently logged in user's profile
profile = request.user.profile
# check to see if this request is a post
if request.method == "POST":
# Bind the post to the form w/ profile as initial
form = ProfileForm(request.POST, instance=profile)
if form.is_valid() :
# if the form is valid
form.save()
messages.success(request, _(u'Success! You have updated your profile.'))
else :
# if the form is invalid
messages.error(request, _(u'Error! Correct all errors in the form below and resubmit.'))
else:
# set the initial form values to the current user's profile's values
form = ProfileForm(instance=profile)
return render(
request,
'membership/manage/profile.html',
{
'form': form,
}
)
notice that the outer else initializes the form with an instance: form = ProfileForm(instance=profile) and that the form submit initializes the form with post, BUT still binds to instance form = ProfileForm(request.POST, instance=profile)
If you're looking at forms, it would seem like a good idea to start with Django's forms framework, specifically forms for models.