3

I am following the instructions from django-bootstrap-modal-forms and what I am finding is that my form is posting or submitting twice when I submit the form. First, the object was simply being created twice, I could see that from the admin. Now it seems the form is creating the object, but also entering its validation state, which I obviously don't want if the form was successful, which it is.

Has anyone experienced this? I've done nothing more than what was outlined in the docs that I linked to and I cannot identify any reason why this should be submitting twice.

Here is my code:

home.html

<a href="#" class="create-shipment dropdown-itemcreatenew" onclick="closeNav()">Shipment</a>

<div class="modal fade" tabindex="-1" role="dialog" id="modal">
  <div class="modal-dialog" role="document">
    <div class="modal-content">

    </div>
  </div>
</div>

<script>
$(document).ready(function() {

    $(".create-shipment").modalForm({
        formURL: "{% url 'CreateShipmentView' %}"
    });

});
</script>

views.py

class CreateShipmentView(BSModalCreateView):
    template_name = 'create_shipment.html'
    form_class = CreateShipmentForm
    success_message = 'Success: Shipment Has Been Created!'
    success_url = reverse_lazy('HomeView')

create_shipment.html (template for modal form)

{% load crispy_forms_tags %}
<form method="post" action="">
  {% csrf_token %}

 <div class="modal-header">
    <h5 class="modal-title">Create New Shipment</h5>
    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
      <span aria-hidden="true">&times;</span>
    </button>
  </div>

  <div class="modal-body">
    {{form|crispy}}
  </div>

  <div class="modal-footer">
    <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
    <button type="button" class="submit-btn btn btn-primary">Create</button>
  </div>

</form>

forms.py

class CreateShipmentForm(BSModalForm):
    class Meta:
        model = Shipment
        fields = ['Reference_Number', 'Ultimate_Consignee']

urls.py

url(r'^create_shipment/', views.CreateShipmentView.as_view(), name='CreateShipmentView'),

Event Listeners on Submit Button

 // Add click listener to the submitBtn
    var ajaxSubmit = function (modalID, modalContent, modalForm, formURL, errorClass, submitBtn) {
        $(submitBtn).on("click", function () {
            // Check if form.is_valid() via ajax request when submitBtn is clicked
            isFormValid(modalID, modalContent, modalForm, formURL, errorClass, submitBtn, submitForm);
        });
    };

    // Check if form.is_valid() & either show errors or submit it
    var isFormValid = function (modalID, modalContent, modalForm, formURL, errorClass, submitBtn, callback) {
        $.ajax({
            type: $(modalForm).attr("method"),
            url: $(modalForm).attr("action"),
            // Serialize form data
            data: $(modalForm).serialize(),
            success: function (response) {
                if ($(response).find(errorClass).length > 0) {
                    // Form is not valid, update it with errors
                    $(modalID).find(modalContent).html(response);
                    $(modalForm).attr("action", formURL);
                    // Reinstantiate click listener on submitBtn
                    ajaxSubmit(modalID, modalContent, modalForm, formURL, errorClass, submitBtn);
                } else {
                    // Form is valid, submit it
                    callback(modalForm);
                }
            }
        });
    };
GXM100
  • 417
  • 6
  • 20

2 Answers2

7

Update to this post: It seems like that the package was updated and the original issue was solved, thus links in this answer won't point you to the correct codes/comments anymore. If you are still experiencing the same problem, try upgrading the package first.


After reading the source code for the package, I believe that receiving two requests on the backend is normal.

Your code

$(document).ready(function() {

    $(".create-shipment").modalForm({
        formURL: "{% url 'CreateShipmentView' %}"
    });

});

triggers the function modalForm where it takes your options (formURL) and assign the function newForm to a click event.

Then in the newForm function it calls the function addListeners to bind a click event to the submit button in the modal, and the event is called as following:

isFormValid(modalID, modalContent, modalForm, formURL, errorClass, submitBtn, submitForm);

Note that the last parameter submitForm points to the following function

var submitForm = function(modalForm) {
      $(modalForm).submit();
    };

Finally in the function isFormValid, all the data you entered in the form will be posted to the url you defined in the action attribute for validation, and if there is no error, the form will be submitted to the very same url.

If you dig into the python code in this package, things become interesting. The BSModalForm is based on two classes in mixins.py and in here it says that when the request was made by anything other than ajax, save the instance created using the form data, or otherwise (if requested by ajax call) don't save the instance and return it. That's why it validated the form at first but should never save it (remember the first call was indeed originated by using the ajax call in jQuery).

You mentioned that the form was being saved twice - try to add a line at the beginning of the save function as

print(request.is_ajax())

and then check the output. it could be that the call failed to be sent as an AJAX call. (if that's the case, update your jquery version or use something else to make the call like axios)

If you don't like how things are happening, few options (package under MIT license):

  1. Change the save function to validate the instance then save it as a normal django save function, but that involves changing some of the JS code.

  2. Make an API endpoint to receive the data and communicate using json instead of returning html code every time (I guess this is also the reason why the author wrote the JS in the current way because you then face the rendering issue). Since currently you don't need to perform any other action after submitting the form, it doesn't make sense to return the instance anymore. (no need for DRF as there is a JsonResponse class built-in in django and if you only need this one endpoint)

  3. Use BootStrap directly since the story here is rather simple: a modal on the page, a button to trigger the modal, a form in the modal and you can submit it. You might need to write some of your own JS for error displaying but it should still be easier than changing the existing package.


An example

# forms.py
from django import forms


# use django ModelForm for creating the form based on model
class CreateShipmentForm(forms.ModelForm):
    class Meta:
        model = Shipment
        fields = ['Reference_Number', 'Ultimate_Consignee']

view for rendering the form (GET) and receiving form submissions (POST)

# views.py
from yourapp.forms import CreateShipmentForm
from django.shortcuts import render


def create_shipment_view(request):
    # http POST request means that the form was submitted
    if request.method == 'POST':
        # create the form and map the data to CreateShipmentForm
        shipment_form = CreateShipmentForm(request.POST)
        # based on your design of the form/modal, validate the data passed into the form
        if shipment_form.is_valid():
            # create a new shipment object
            new_shipment = shipment_form.save()
            # form processing is finished now do something to alert user, for example redirect the user to the success page
            return redirect('success')
        # if the form is not valid, then shipment_form.errors contain all the errors
    else:
        # for all other accessing methods including GET
        # create an empty form
        shipment_form = CreateShipmentForm()
    
    # if the request was POST, the form contains errors, if request was GET, it's an empty form
    return render(request, 'path/to/template.html', {
        'form': shipment_form
    })

Finally in your template, you can just display the form as you would normally. If you don't know or need some other features for your form, keep using crispy forms.

check if shipment_form.errors in your template and display it in your template if you want to show errors to the users.

Foocli
  • 157
  • 1
  • 11
  • Hi @Foocli thank you for the detailed explanation! I think, based on what you've detailed, that I'd like to go with option 3 - but am curious what you mean by 'use Bootstrap directly'. Are you meaning to leave JS out of this entirely? – GXM100 Feb 17 '20 at 22:05
  • Sorry if that wasn't clear, I meant instead of using django-bootstrap-modal-forms, just import bootstrap into your base template (which you probably have already done) and just write the modal in your template. You should be able to just copy paste most of the code from BS documentation, but you will need to implement a small js function to display the error if validation fails, and since you are using crispy forms, you might also be able to find some existing code for such function – Foocli Feb 17 '20 at 22:32
  • If you don't really want to write any JS code, you can also consider changing the place to display the errors. For example, do you really have to display the error within the modal? can it be a toast message? or can it be an alert
    on the top of your page to remind the users to try again? Note that these workarounds might also cause the user input to be lost (so if there is any error, the user might have to fill in the form all over again)
    – Foocli Feb 17 '20 at 22:36
  • Hi @Foocli I apologize this might sound dumb but I thought that I had to use that package for modals in Django. How could I append the Django model form to the modal? I can include the html for the modal in my template but if I want to do form.UltimateConsignee than I need some reference to the form – GXM100 Feb 18 '20 at 22:44
  • @GXM100 can you give me an example of your use case? There is no need to append the form to the modal. Validation, field mapping comes standard in django forms. – Foocli Feb 19 '20 at 20:23
  • my use case is simply to have a modal which contains a form that will allow a user to create a Customer. My confusion is if I simply write the modal in html and use bootstrap classes, how do I get the CreateCustomer form from forms.py to display in that modal? This is where I struggle and why I thought I needed to use a package because I couldn’t find a way. – GXM100 Feb 19 '20 at 23:10
  • This was actually discussed in [here](https://docs.djangoproject.com/en/3.0/topics/forms/#the-view). You are already using crispy forms, how did you render the forms before... I updated the answer yesterday with an example of how you can pass the form object to the template, then in your template, something like `{% crispy ship_form %}` (assume you passed the form to the template and called it ship_form) will render the whole form, or if you don't know about crispy form, `{{ form }}` will be fine as described [here](https://django-crispy-forms.readthedocs.io/en/latest/crispy_tag_forms.html) – Foocli Feb 20 '20 at 16:10
  • Hi @Foocli sorry but I still think I'm miscommunicating here. I understand the edit that you've made. But I think my primary question is what is the ``` – GXM100 Feb 25 '20 at 02:22
  • If you only need this one modal, you don't need to open a new view. Try copy paste the [example code](https://getbootstrap.com/docs/4.4/components/modal/#live-demo) and you should be able to see how a modal works. If you do need a new view for something else, open the view and in your template, show the modal **when the document is ready** by using [the show function](https://getbootstrap.com/docs/4.4/components/modal/#modalshow) – Foocli Feb 25 '20 at 17:10
  • this was now many months ago but I have finally come back to this. I have the modal working, but I do have many modals which should show from the screen so I am trying to understand how to use the show function which you mentioned. Does this get attached to a click event? and how do I incorporate the URL? – GXM100 Apr 29 '20 at 21:36
  • Depending on your use case, a click event surely should work. Try look into the bootstrap example, specifically, this binds your button with the model `data-toggle="modal" data-target="#exampleModal"` (these are called [datasets](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes)). Put a form inside of the modal just like you normal would. It does not require any advanced implementations. – Foocli May 08 '20 at 19:26
3

I had similar problem. My solution was:

class AppCreateView(BSModalCreateView):
    template_name = 'apps/app_create.html'
    form_class = UserAppForm
    success_message = 'Success: App was created.'
    success_url = reverse_lazy('dashboard')

    def form_valid(self, form):
        if not self.request.is_ajax():
            app = form.save(commit=False)
            profile = Profile.objects.get(user=self.request.user)
            app.profile = profile
            app.save()
        return HttpResponseRedirect(self.success_url)
alex
  • 101
  • 6