How to make a selection button inside a multistep form wizard in Django that renders an output without proceeding to the next step? - django

I am new to Django and I am making a project with a multistep form using django-formtools. The problem is, in my step 2 form, I have selection fields that I need to pass in the backend to perform some calculations and then render the output. The user can make changes anytime based on the output. I made an apply changes button which should trigger the backend process and a proceed to next step button if the user decides to finalize the selected changes. However, when I click the apply changes button, it leads me to the next step instead.
Here's my HTML code:
<form action="" method="POST">
{% csrf_token %}
{{ wizard.management_form }}
{% if wizard.form.forms %}
{{ wizard.form.management_form }}
{% for form in wizard.form.forms %}
{{ form }}
{% endfor %}
{% else %}
{{ form }} # three selection fields
<button name="apply_changes">Apply Changes</button>
{% endif %}
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}">{% trans '‹ Previous Step' %}</button>
{% endif %}
<input type="submit" value="{% trans 'Finish' %}">
</form>
Here's my SessionWizardView method code snippet:
def get_context_data(self, form, **kwargs):
context = super(StepWizard, self).get_context_data(form=form, **kwargs)
if self.steps.current == 'step_1':
# save step 1 data to sessions
if self.steps.current == 'step_2':
step1_data = self.get_all_cleaned_data()
# if apply changes button is clicked
data = self.request.POST.get('apply_changes')
# process data
# add output to context
return context
I need help on how can it rightly be done. Thanks in advance!

So for future django developers who encountered the same problem as me, here's the answer to my question:
1) validate the data in step 2 which is temporarily the default values of my selection fields; and
2) override the post method to load the current page using the goto_step wizard function and embed it in the apply changes button
You can find the guide here :)
And then there 'ya go! Once the user clicks the apply changes button, the page reloads and the output is rendered in the form.
Still needs to optimize it though :D

Related

trouble showing desired checkbox validation state w/ bootstrap5 for django model form w/ m2m field and checkboxselectmultiple widget

I have a checkboxselectmultiple on an m2m model field in an ModelForm that is required - meaning at least one of the choices must be selected. I am using the boostrap5 was-validated class on my form:
<form method="POST" action="{{ request.path }}" {% if attempt_submit %}class="was-validated"{% endif %}>
This question is about how the validation shows up on my form with bootstrap5. Should be red border and red ! if not validated, green border and checkmark if so. However, for my checkboxes, if I don't have any selected (and everything else on the form validates), the form will show each checkbox option as green instead of red. Yet, it does know that it's invalid because the page focus will come back up the checkbox area to show the user what to correct (and it doesn't pass form.is_valid() in views.py.
Why are these labels and boxes still showing green and how can I show them as red until I select one and it's now valid?
Along the lines of this post, I have tried adding
{% if form.sales_location.field.required %}required{% else %}form.sales_location.field.required=""{% endif %}
to the checkbox <input>, but then each field is required and if I select one, the other remaining options still remain red - as if every option would have to be selected for the form to validate. Am I supposed to do this anyway and then add something else (JS?) to disable that?
Not sure exactly what code would be helpful to see...
in models.py, this is the field:
sales_location = models.ManyToManyField(SalesLocation, verbose_name="Where do you sell your products? (select all that apply)" )
in forms.py
model = AssessmentProfile
fields = [
'sales_location',
...
]
widgets = {
'sales_location': forms.CheckboxSelectMultiple(attrs={
'class': 'form-check',}),
}
I add this because I read this post about making sure that I use a `ModelMultipleChoiceField' - but I assume that is already happening because it's a model form.(?)
Probably most important, in the template thisform.html, here's how I'm manually adding this form element:
<div class="field-wrapper">
{{ form.sales_location.label_tag }}
<ul id="id_sales_location" class="form-check">
{% for pk, choice in form.sales_location.field.widget.choices %}
<li>
<input {% for location in location_qs %}{% if location == pk %}checked='checked'{% endif %}{% endfor %}
name="sales_location" class="form-check-input" type="checkbox" value="{{ pk }}" id="id_sales_location_{{forloop.counter0}}"
{% if already_submitted %}disabled="disabled"{% endif %}>
<label class="form-check-label" for="id_sales_location_{{forloop.counter0}}">
{{ choice }}
</label>
</li>
{% endfor %}
</ul>
</div>
Also, I tried updating css to manually format red, but think that doesn't address the root of the problem, plus, I wasn't able to do it successfully anyway.
Thanks for taking a look and for any suggestions.
In the end, I used javascript to solve this problem.
I updated the form template
<div class="field-wrapper">
{{ form.sales_location.label_tag }}
<ul id="id_sales_location" class="form-check">
{% for pk, choice in form.sales_location.field.widget.choices %}
<li>
<input {% for location in location_qs %}{% if location == pk %}checked='checked'{% endif %}{% endfor %}
name="sales_location" class="form-check-input" type="checkbox" value="{{ pk }}" id="id_sales_location_{{forloop.counter0}}"
{% if not form.sales_location.field.required %} {% else %} required {% endif %}
{% if already_submitted %}disabled="disabled"{% endif %}>
<label class="form-check-label" for="id_sales_location_{{forloop.counter0}}">
{{ choice }}
</label>
</li>
{% endfor %}
</ul>
</div>
to add required to the input if the checkbox is required. This allows all the checkboxes to come up red when validating, if the field is empty.
Then, I added this javascript to remove 'required' if it's checked.
<script>
// Select all checkboxes using querySelectorAll.
var checkboxes = document.querySelectorAll("input[type=checkbox][name=sales_location]");
checkboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
for (var cb of checkboxes) {
cb.removeAttribute('required');
}
})
});
</script>
If the field is not required, nothing changes. But if it is, then the required attribute on the <input>is gone and all the checkboxes show up green, which is what I wanted.
It's not perfect because if the checkboxes become unchecked, they don't change back to red. So I am making a dirty assumption that if someone checked a box, they wouldn't go back and uncheck it and try to submit. In which case, the validation would show green (and unchecked) until Submit was pressed again, but then it would take them back to this field which would be red again. If you know how to improve my code by adding the different case for the change function (only if the field is required), please feel free to add that. Cheers.

SessionWizardView state only saved on final form, done() not executed

I have several forms I have added to a wizard, but form state is only maintained for the final step, and done() is not executed.
I have created the following, heavily based off the examples on django's documentation, to try get to the bottom of this. It seems the final step is the only one that saves the state when moving amongst the steps.
class OneForm( Form ):
field_one = forms.CharField(label='1', max_length=100)
field_two = forms.CharField(label='2', max_length=100)
field_three = forms.CharField(label='3', max_length=100)
class TwoForm( Form ):
field_one = forms.CharField(label='4', max_length=100)
field_two = forms.CharField(label='5', max_length=100)
field_three = forms.CharField(label='6', max_length=100)
TEST_WIZARD_FORMS = [
("one", OneForm),
("two", TwoForm),
]
TEST_TEMPLATES = {
'one': 'tour/one.html',
'two': 'tour/two.html',
}
class TestWizardView( SessionWizardView ):
form_list = TEST_WIZARD_FORMS
def done(self, form_list, **kwargs):
print('done executed')
return reverse('home')
def get_template_names(self):
return [TEST_TEMPLATES[self.steps.current]]
and this for templates (both one.html and two.html are identical)
<html>
<body>
<p>Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}</p>
<form action="" method="post">{% csrf_token %}
<table>
{{ wizard.management_form }}
{% if wizard.form.forms %}
{{ wizard.form.management_form }}
{{ wizard.form.non_field_errors }}
{{ wizard.form.errors }}
{% for form in wizard.form.forms %}
{{ form }}
{% endfor %}
{% else %}
{{ wizard.form }}
{% endif %}
</table>
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.first }}">"first step"</button>
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}">"prev step"</button>
{% endif %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.next }}">"next step"</button>
<input type="submit" value="submit"/>
</form>
</body>
</html>
If I enter data on step 1, proceed to step 2 and enter data, then return to step 1, the first step has no data saved and no form errors displayed. When I hit next to return to step 2, step 2's data is still present. Intentionally putting invalid data on step 1 has shown me that it does not validate the form either, as the wizard continues to step 2 without displaying errors.
When I submit the form, done() does not execute. This makes sense if only the last step is actually successful, but seeing no errors on step 1 has me baffled.
Why is form data not maintained except on the final form? Why is the final step the only one that actually validates form data? Why is done not executed?
Update: It appears the form validation is happening afterall, and I do see it succeeding via printing relevant information in the post function, but done() still does not seem to get executed.
Thank you.
Step 1 in the documentation found here is the answer. It states the following.
The user visits the first page of the wizard, fills in the form and submits it.
The key here is the "submit." No form validation or state is saved unless the form is submitted. Using the wizard_goto_step for next/previous/jump does not submit the form, does not validate the form, and does not save the form in the session/cookie (depending which you choose).
It is obvious now, but I still think this is misleading to potential end users of the form wizard. It is easy enough for me to replace the wizard_goto_step with an actual submit when going to the next step, but when users enter some data in a form, and subsequently choose to revisit another step, all of their data on that step is lost.
It feels like form data should be saved even when incomplete. My intention is to save this data manually using the storage.set_step_data() function, as all form steps are re-validated at the final processing anyway. Even if a user fills out incorrect data on a step they will still be redirected to the step with missing data at the end. This feels like better behavior than blindly wiping the user's data on a step when they visit a previous step.
It is certainly a bug. You have to remove the following lines of code from
\formtools\wizard\views.py
# walk through the form list and try to validate the data again.
# for form_key in self.get_form_list():
# form_obj = self.get_form(step=form_key,
# data=self.storage.get_step_data(form_key),
# files=self.storage.get_step_files(form_key))
# if not form_obj.is_valid():
# return self.render_revalidation_failure(form_key,
# form_obj,
# **kwargs)
# final_forms[form_key] = form_obj
Actually I don't understand the reason of this code. Every form was validated after it was submitted. I see no reason to validate them again?
This validation fails for some unknown reason, and that is why the done routine later on is never called.

Django endless pagination url issue on page reload

I am using endless pagination for my django project. On a page where I display records as a report, things work well.
However, when I include some "operations" with my records, I have issues.
e.g. my table row displays information and has additional column which takes the user to edit form.
{% extends "base/home.html" %}
{% load endless %}
{% block maincontent %}
{% paginate 5 atlist %}
<table> class="table">
{% for rec in atlist %}
<tr>
<!-- ... Headers ... and other columns code taken out .... -->
<td>
<button class="btn btn-primary btn-sm" onclick="location.href='/secure/editmytypes?ID={{rec.uuid}}'">Edit</button>
<button class="btn btn-primary btn-sm" onclick="location.href='/secure/deletemytypes?ID={{rec.uuid}}'">Delete</button>
</td>
</tr>
{% endfor %}
</table>
{{ pages.previous }} {{ pages.next }}
{% endblock %}
When the above template is loaded the first time, {{ pages.previous }} {{ pages.next }} display proper links like
"/list?page=2"
and behaves properly if I only do next / previous page navigation.
But, when the user clicks on Edit link (Edit Button) in a row to goto the edit form - do the operation and come back to this list (both the forms save data and transfers control back to this list) the {{ pages.previous }} {{ pages.next }} links become
"/secure/editmytypes?ID='..uuid...'&pages=2"
or
"/secure/deletemytypes?ID='..uuid...'&pages=2"
Does anyone have any pointers I could use ?
Thanks in advance.
Changing the way my edit for returns control back to this list form did the trick for me.
I was doing
# return render(request,'base/list_mytypes.html',{'atlist':mytypeslist,},)
which was the problem .. I changed it to
return redirect('/secure/listmytypes',{'atlist':mytypeslist,},)
and now suddenly the page links are proper !!
Doing some R&D in this since I think both the shortcuts essentially achieve the same effect (I know they return different HttpResponse objects but that should not have any effect on a url in some other page - unless I am missing something here).

Django Forms to values of html <input> field

I am trying to access the values of a Bootstrap btn-group from Django and from the documentation I have found, it seems that you should use Forms in Django for such tasks.
This is what the html looks like, right now:
<div class="col-md-6">
{% for metric in metrics %}
<input name="{{ metric.name }}" type="hidden" value="0"/>
{% endfor %}
<div class="btn-group" data-toggle="buttons">
{% for metric in metrics %}
<button type="button" class="btn btn-default" data-checkbox-name="{{ metric.name }}">{{ metric.name }}</button>
{% endfor %}
</div>
</div>
How can I use forms to get the values of the input fields?
Here it is a basic example about using a form in django
views.py:
#login_required
def your_view(request): # Add this code into your view
if request.method == 'POST':
# So here you can do a loop over POST fields like this
data_list = [] # We will insert all the inputs in this array
for key in request.POST:
data_list.append(request.POST[key])
# Here you can manage the the data_list and do whatever you need
# The content of the data_list depend on your inputs
# It could be string, integer....
# YOUR VIEW CODE
template (form example):
<form action="." method="post" id="add_user_form">
{% csrf_token %}
{% for metric in metrics %}
<input type="text" name="{{ metric.name }}" placeholder="whatever you want">
{% endfor %}
<input type="submit" value="submit" class="default"/> # Submit button
</form>
{% csrf_token %} : You need to put this in every form you use
action="." : This make the post to the actual page
But anyway I strongly recommend you to check this Django Forms Documentation to unterstand better the logic, and also check the ModelForms because can save you a lot of time when you need to make a form for a model that exists in your Django Models
You are'n forced to use django forms, this is just a way to get a sort of organization.
in you views toy can get the values sent to the server by using request.GET or request.POST, depending of the method of the form.
to get a list of values you have received just do a
print request.POST
request.POST is a dictionary, so you can get any value fron them by its key:
print request.POST['<key>']

Django URL is being changed by a submit button

I'm very new to Django and not super familiar with web programming in general, so it's very likely that there is an easy fix to my problem that I'm just unaware of.
My web app is a photo gallery. People can click on a photo to see an enlarged version with buttons on either side for older or newer pictures. In addition, the photos in the gallery can be sorted by tags, which are passed along as URL parameters.
My problem is that when I click on one of the submit buttons, Django replaces the parameters in my URL with the name of the button, thus destroying my reference to what tag I was using. For example, "127.0.0.1:8000/gallery/view/6/?tag=people" upon clicking next, gets converted to "127.0.0.1:8000/gallery/view/6/?older=Older" when it's trying to process the URL.
Code from my HTML:
<form action="/gallery/view/{{ photo.id }}/?tag={{ tag }}" method="get">
{% if has_newer %}
<input type="submit" name="newer" value="Newer">
{% endif %}
<img src="{{ photo.photofile.url }}">
{% if has_older %}
<input type="submit" name="older" value="Older">
{% endif %}
</form>
In my view.py I pass in the tag plus other information in a render_to_response, but I'm not sure how to/if I can reclaim it while handling the buttons.
render_to_response('item/view.html', {'photo':photo, 'tag':tag, 'related_tags': related_tags, 'related_photos': related_photos, 'has_newer': has_newer, 'has_older': has_older}, context_instance=RequestContext(request))
Here's the view.py code for processing the buttons:
if 'newer' in request.GET:
if has_newer:
return HttpResponseRedirect('/gallery/view/%s/?tag=%s'%(newer[1].id, tag))
else:
return HttpResponseRedirect('/gallery/')
if 'older' in request.GET:
if has_older:
return HttpResponseRedirect('/gallery/view/%s/?tag=%s'%(older[1].id, tag))
else:
return HttpResponseRedirect('/gallery/')
<form action="/gallery/view/{{ photo.id }}/" method="get">
{% if has_newer %}
<input type="submit" name="newer" value="Newer">
{% endif %}
<!--This will append a tag parameter with given value to the querystring -->
<input type="hidden" name="tag" value="{{ tag }}">
<img src="{{ photo.photofile.url }}">
{% if has_older %}
<input type="submit" name="older" value="Older">
{% endif %}
</form>
Note that the query string is removed from action (as it won't be used) and the older and newer parameters will still be sent along.