Search code examples
djangobootstrap-modal

Django: How to use a modal to delete an object?


On the index page for a list of food objects I've included a button to delete an object that is not used. The delete button launches a Bootstrap 5 modal delete form. But when I click the Delete button, the error thrown includes the name of index page. In the code below, where & how can this be fixed?

Expected url on delete: .../food/1/delete

Page not found: Request URL: http://127.0.0.1:8000/food/foodIndex/1/delete

It may be worth noting that the 404 error considers the request to be GET rather than POST

[I'm attempting to learn Python & Django by recreating a working PHP project. All of the HTML & javascript comes from that project.]

food/urls.py:

urlpatterns = [
    path('foodIndex/', views.foodIndex, name="foodIndex"),
    path('<int:id>/edit/', views.foodEdit, name="foodEdit"),
    path('<int:id>/foodDelete/', views.foodDelete, name = "foodDelete")
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Note: a path('foodIndex/<int:id>/foodDelete/',... does not solve the problem

Template included in ".../food/foodIndex.html":

{% if allFoods.count > 0 %}
    {% for food in allFoods %}
        <tr>
            <td data-foodid="{{ food.id }}">{{ food }}</td>
            <td><a class="btn p-0" href='{% url "food:foodEdit" food.id %}'>Edit</a></td>
            {% if food.used == False %}
            <td><button id='btn_delete' type="button" class="btn p-0" data-bs-toggle="modal" data-bs-target="#modal-delete" data-bs-foodid='{{ food.id }}' data-bs-foodname='{{ food }}'>Delete</button></td>
            {% endif %}
        </tr>
    {% endfor %}
{% else %}
    <tr>
        <td colspan="3">no records found</td>
    </tr>
{% endif %}

Modal in ".../food/foodIndex.html":

<div id="modal-delete" class="modal fade" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Delete food item</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                <form >
                    <button id='food-delete' class="btn btn-warning" type="submit">Delete</button>
                </form>
            </div>
        </div>
    </div>
</div>

delete form:

{<form method="POST" action="{% url 'food:foodDelete' pk=food.id %}" onsubmit="return confirm('Are you sure you want to delete this item?');">
    <button class="btn">Delete</button>
</form>}

javascript for modal:

const deleteModal = document.getElementById('modal-delete');
deleteModal.addEventListener('show.bs.modal', event => {
    const button = event.relatedTarget
    const foodName = button.getAttribute('data-bs-foodname')
    const foodId = button.getAttribute('data-bs-foodid')
    const modalBodyInput = deleteModal.querySelector('.modal-body ')
    const modalFooterButtons= deleteModal.querySelector('form')

    modalBodyInput.textContent = `Confirm deletion of ${foodName}`
    modalFooterButtons.setAttribute('action',`${foodId}/delete`)
}
);

Solution

  • The problem is in the form on how you are building your URL.

    deleteModal.addEventListener('show.bs.modal', event => {
        ...
        modalFooterButtons.setAttribute('action',`${foodId}/delete`)
    }
    );
    

    That will always yield just some-id/delete as your action, you are not building the full path. One simple solution would be to use DTL to build your URL using reverse resolution, just like in your delete form code block.

    But, instead, use that as an attribute of the table delete button (you can just replace data-bs-foodid)

    <button 
        id='btn-delete' 
        class="btn-warning" 
        data-bs-toggle="modal" 
        data-bs-target="#modal-delete" 
        data-bs-foodname='{{ food }}' 
        data-bs-url="{% url 'food:foodDelete' food.id %}"
    >
        Delete
    </button>
    

    Then, we retrieve it and set those values on the form using the same method with JavaScript:

    deleteModal.addEventListener('show.bs.modal', event => {
        ...
        const deleteForm= deleteModal.querySelector('form');
        const url = button.getAttribute('data-bs-url');
        deleteForm.setAttribute('action', url);
        deleteForm.setAttribute('method', 'post');
    });
    

    Full example

    foodIndex.html (Table with Foods)

    <table class="table">
        <thead>
            <tr>
            <th scope="col">Name</th>
            <th scope="col" colspan="2">Actions</th>
            </tr>
        </thead>
        <tbody>
        {% if allFoods.count > 0 %}
            {% for food in allFoods %}
            <tr>
                <th scope="row">{{food.name}}</th>
                <td><button>Edit</button></td>
                <td>
                    <button 
                        id='btn-delete' 
                        class="btn-warning" 
                        data-bs-toggle="modal" 
                        data-bs-target="#modal-delete" 
                        data-bs-foodname='{{ food }}' 
                        data-bs-url="{% url 'food:foodDelete' food.id %}"
                    >
                        Delete
                    </button>
                </td>
            </tr>
            {% endfor %}
        {% else %}
            <tr>
                <td colspan="3">no records found</td>
            </tr>
        {% endif %}
        </tbody>
    </table>
    

    foodIndex.html (Modal - Note {% csrf_token %})

    <div id="modal-delete" class="modal fade" tabindex="-1">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Delete food item</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                    <form>
                        {% csrf_token %}
                        <button id='food-delete' class="btn btn-danger" type="submit">Delete</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
    

    foodIndex.html (script)

    <script>
        const deleteModal = document.getElementById('modal-delete');
    
        deleteModal.addEventListener('show.bs.modal', event => {
            const button = event.relatedTarget;
            const foodName = button.getAttribute('data-bs-foodname');
            const url = button.getAttribute('data-bs-url');
            const modalBodyInput = deleteModal.querySelector('.modal-body ');
    
            modalBodyInput.textContent = `Confirm deletion of ${foodName}`;
    
            const deleteForm= deleteModal.querySelector('form');
            deleteForm.setAttribute('action', url);
            deleteForm.setAttribute('method', 'post');
        });
    </script>