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
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"),
)
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.
I got crispy forms working with my model, though the form looks plain and bootstrap not showing up, also there seems to be no button. Even when adding button and clicking it(it refreshes) no data has been saved to the database. I have tried many ways. What seems to be wrong? Any help would be highly appreciated.
forms.py
class PlotForm(forms.ModelForm):
helper = FormHelper()
helper.form_tag = False
helper.form_method = 'POST'
class Meta:
model = Plot
fields = '__all__'
views:
def plot_form(request):
return render(request, 'plot_form.html', {'form': PlotForm()})
the html:
{% load crispy_forms_tags %}
<form action="" method="POST">
{% crispy form %}
<input type="submit" class="btn btn-default" value="save">
First the {% csrf_token %} is missing. Second If I remember it correct you need to use {{ form|crispy }} to load the form.
And third I would recommend to use Widget Tweaks
<form method='POST' action="/" enctype='multipart/form-data'>
{% load widget_tweaks %}
{% csrf_token %}
{{ form.first_name |add_class:"customCSS1 customCSS2" }}
{{ form.second_name |add_class:"customCSS3 customCSS4" }}
</form>
{{ form.media.js }}
with this plugin you can style the form as you wish. All Css classes work. Crispy is nice but you have get into the documentation and there are always some workarounds you need to do when you want to style the form. With Widget Tweaks you can simply apply any CSS class. When you really know your way around with crispy you can do a lot but to get to that point....
I switched at some Point and now everything works like a charm
Hope that helps if not leave a comment :)
Edit
I just saw something in your views.py. You are not referencing the form correct as far as I can tell.
from appName.forms import PlotForm
def plot_form(request):
form = PlotForm(request.POST or None, request.FILES or None) #request files is only required when you want to upload a file
if form.is_valid():
instance = form.save(commit = False)
...
instance.save()
#messages.success(request, 'form was saved') #optional
context = {
'form':form,
}
return render(request, 'AppName/plot_form.html', context)
Maybe that will do the trick. You did not have a form validation and Im not sure if the "()" at {'form': PlotForm()} would break the code.
I also faced the same issue. Solved it by testing it on firefox instead of google chrome. Apparently, Chrome does not load CSS style for a few local web apps. Got it to work after following this hack. https://css-tricks.com/new-in-chrome-css-overview/
So, a total Django Model Formset Newb question. I'm trying to save my form and keep getting this error:
['ManagementForm data is missing or has been tampered with']
Here is what I have for my TemplateView:
class AttendanceTemplate(TemplateView):
template_name = 'attendance/index.html'
def get_context_data(self, **kwargs):
context = super(AttendanceTemplate, self).get_context_data(**kwargs)
instruction = Instruction(self.request.user.username)
sections_list = self.request.GET.getlist('sections_list')
term = self.request.GET.get('term', instruction.term)
enrollments = Enrollment.objects.using('wisp').prefetch_related('profile').filter(section_id__in=['111111'], term=term)
attendanceQuery = Enrollment.objects.using('wisp').prefetch_related('student').filter(section_id__in=['111111'], term=term)
for enrollment in attendanceQuery:
attendance, created = Attendance.objects.update_or_create(
section_id=enrollment.section_id,
term=enrollment.term,
first_name=enrollment.student.first_name,
last_name=enrollment.student.last_name,
email_address=enrollment.student.email_address,
)
something = Attendance.objects.filter(section_id__in=['111111'], term=term)
formset = AttendanceFormSet(queryset=something)
combined = zip(enrollments, formset)
context['combined'] = combined
return context
Here is how I'm trying to save the form:
def post(self, request):
formset = AttendanceFormSet(request.POST)
if formset.is_valid():
for thing in formset:
formset = thing.save()
return render_to_response("template/index.html",{'formset': formset}, RequestContext(request))
else:
return HttpResponse(error.msg)
Here is what I have in my template:
<form method="POST" action="">
{% csrf_token %}
{% for enrollment, form in combined %}
<div class="wrapper-formset">
<div>
{{ form.first_name.value }}
{{ form.last_name.value }}
{{ form.email_address.value }}
</div>
<div class="clear-all"></div>
</div>
{% endfor %}
<button type="submit" class="save btn btn-default">Save</button>
</form>
Am I saving my form wrong? Maybe my loop is wrong? Also, I'd prefer to print each field out individually, so using the "myform.management_Form" may not work for me? (e.g., myform.management_form.field_name)
If you render the forms separately, then you must include the management form in your template. The fact that you are zipping your forms makes no difference.
Including the management form is easy, just add {% formset.management_form %} to your template.
<form method="POST" action="">
{% csrf_token %}
{{ formset.management_form }}
{% for enrollment, form in combined %}
...
For that to work, you'll need to make sure that formset is in the template context, for example:
context['formset'] = formset
You might find the docs on using model formsets in the template useful. It would be a good idea to start with the simplest option, {{ formset }}, test it, then gradually customize the template. That makes it easier to debug when stuff goes wrong. At the moment it looks like you have missed out {{ form.id }}.
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.