In part 4 of this series, we got forms in place to add and edit books in the library, then made them look better using django-crispy-forms. The many-to-many relationship between books and authors brought some weaknesses of the Crispy Tailwind theme to light, and it took a bit of effort to address that. Now it’s time to get deletion working before we make everything work a little better using HTMX.

Deleting Books

In my opinion, the cleanest way to make deletion work is to use the same pattern as for editing: a GET request on the delete endpoint will render a confirmation form, and a POST will carry out the deletion. So in urls.py, add the book deletion endpoint:

urlpatterns = [
    path("", views.index, name="index"),
    path("books", views.library, name="library"),
    path("book/<int:book_id>", views.book_details, name="book"),
    path("book/<int:book_id>/edit", views.edit_book, name="edit_book"),
    path("book/<int:book_id>/delete", views.delete_book, name="delete_book"),
    path("book/add", views.add_book, name="add_book"),
]

Create a template for it:

{% extends "reading_journal/base.html" %}
{% block content %}
    <div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
        <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
        <div class="fixed z-10 inset-0 overflow-y-auto">
            <div class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0">
                <div class="relative bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full sm:p-6">
                    <div class="sm:flex sm:items-start">
                        <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
                            <!-- Heroicon name: outline/exclamation -->
                            <svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
                            </svg>
                        </div>
                        <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
                            <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">Delete {{ book }}</h3>
                            <div class="mt-2">
                                <p class="text-sm text-gray-500">This will permanently delete <span class="text-underline">{{ book }}</span> from your library. This action cannot be undone.</p>
                            </div>
                        </div>
                    </div>
                    <div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
                        <form action="{% url 'delete_book' book.pk %}" method="post">
                            {% csrf_token %}
                            <button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">Delete</button>
                        </form>
                        <a href="{% url 'book' book.pk %}" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm">Cancel</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endblock content %}

And a view:

@login_required
def delete_book(request: HttpRequest, book_id: int):
    book = get_object_or_404(Book, pk=book_id)
    nav_links = get_nav_links("")
    ctx = {
        "book": book,
        "nav_links": nav_links,
        "action_url": reverse("delete_book", kwargs={"book_id": book.pk}),
    }
    if request.method == "POST":
        # Nothing to validate; the middleware will have required a valid CSRF token, and there's no
        # other data in the form
        book.delete()
        return redirect(reverse("library"))

    return render(request, "reading_journal/delete_book.html", ctx)

Once that’s confirmed working, I connect the delete buttons on the library list to the view, and add delete buttons to the detail view and edit form. As always, the full project is available here.

Since this is a nice stopping point, I will leave the introduction of HTMX for part 6. That will make everything flow more smoothly.


I’m trying on Kev Quirk’s “100 Days To Offload” idea. You can see details and join yourself by visiting 100daystooffload.com.

This is day 7.