HTMX with Django Example 2 - Bulk Update

Introduction

Here is the Django approach to the Bulk Update example from the HTMX Website. You can get the full working example from here .

Create App

$ python manage.py startapp bulk_update

This creates a django app in the root but I then move it into the django_htmx_examples as that is my preference.

Model

We need to create a model and as part of our migration I want to add data to make it easy for people to see it working straight away when they pull the example down from github.

# django_htmx_examples/bulk_update/models.py

from django.db import models

# Create your models here.
class Customer(models.Model):
    name = models.CharField(null=False, blank=False, max_length=50)
    email = models.EmailField(null=False, blank=False, max_length=254)
    status = models.BooleanField(null=False, blank=False)

    def __str__(self):
        return f"{self.name}"

I've given it the name Customer. Now I want to makemigrations and then tweak the file to populate the table after creation with some data.

python manage.py makemigrations
# django_htmx_examples/bulk_update/migrations/0001_initial.py

# Generated by Django 3.2.6 on 2021-09-09 20:09

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = []

    def create_customer(apps, schema_editor):
        # We can't import the Person model directly as it may be a newer
        # version than this migration expects. We use the historical version.
        Customer = apps.get_model("bulk_update", "Customer")
        Customer.objects.update_or_create(
            email="dr@htmx.org", defaults={"name": "Django", "status": True}
        )
        Customer.objects.update_or_create(
            email="brian@htmx.org", defaults={"name": "Brian", "status": True}
        )
        Customer.objects.update_or_create(
            email="celia@htmx.org", defaults={"name": "Celia", "status": True}
        )
        Customer.objects.update_or_create(
            email="emma@htmx.org", defaults={"name": "Emma", "status": True}
        )

    operations = [
        migrations.CreateModel(
            name="Customer",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("name", models.CharField(max_length=50)),
                ("email", models.EmailField(max_length=254)),
                ("status", models.BooleanField()),
            ],
        ),
        migrations.RunPython(create_customer),
    ]

I use a migrations.RunPython call in my migrations file to populate the table using the create_customer function following guidance from the django docs

URLS

# django_htmx_examples/urls.py
# This is the relevant section  
path(
        "bulk-update/",
        bulk_update_views.customer_status,
        name="bulk-update-customer-status",
    ),
    path(
        "bulk-update/activate",
        bulk_update_views.customer_activate,
        name="bulk-update-customer-activate",
    ),
    path(
        "bulk-update/deactivate",
        bulk_update_views.customer_deactivate,
        name="bulk-update-customer-deactivate",
    ),

Views

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.urls import reverse
from django.views.decorators.http import require_http_methods
from rest_framework.decorators import api_view
from rest_framework.response import Response
import json
from rest_framework.parsers import JSONParser
from django.http import QueryDict

from .models import Customer
from .forms import Customer


def customer_status(request):
    context = {}
    customers = Customer.objects.all()
    context["customers"] = customers
    return render(request, "bulk_update/customer_status.html", context)


@require_http_methods(["PUT"])
def customer_activate(request):
    context = {}
    data = request.body.decode("utf-8")
    q = QueryDict(data, mutable=True)
    activated_ids = q.getlist("ids")
    Customer.objects.filter(pk__in=activated_ids).update(status=True)
    customers = Customer.objects.all()
    context["customers"] = customers
    return render(request, "bulk_update/customer_status_update.html", context)


@require_http_methods(["PUT"])
def customer_deactivate(request):
    context = {}
    data = request.body.decode("utf-8")
    q = QueryDict(data, mutable=True)
    activated_ids = q.getlist("ids")
    Customer.objects.filter(pk__in=activated_ids).update(status=False)
    customers = Customer.objects.all()
    context["customers"] = customers
    return render(request, "bulk_update/customer_status_update.html", context)

I have used the QueryDict here to pull out the duplicate ids in the PUT request. This seems to work so I am going with it! If there is a better way do let me know on twitter @chriswedgwood

Templates

# django_htmx_examples/templates/bulk_update/customer_status.html

{% extends "base.html" %}

{% block content %}

  
  <form id="checked-contacts">
      <table>
        <thead>
        <tr>
          <th></th>
          <th>Name</th>
          <th>Email</th>
          <th>Status</th>
        </tr>
        </thead>
        <tbody id="tbody">
            {% for customer in customers %}
          <tr class="">
            <td><input type='checkbox' name='ids' value='{{customer.id}}'></td>
            <td>{{customer.name}}</td>
            <td>{{customer.email}}</td>
            <td>{% if customer.status %} Active {% else %} Inactive {% endif %} </td>
          </tr>
          {% endfor %}
          
        </tbody>
      </table>
  </form>

  <div hx-include="#checked-contacts" hx-target="#tbody">
    <button hx-put="activate" type="button" class="ml-2 inline-flex mt-10 items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
        Activate
      </button>
      <button hx-put="deactivate" type="button" class="ml-2 inline-flex mt-10 items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
        Deactivate
      </button>
    
  </div>
  
{% endblock content %}
# django_htmx_examples/templates/bulk_update/customer_status_update.html 
{% for customer in customers %}
          <tr class="">
            <td><input type='checkbox' name='ids' value='{{customer.id}}'></td>
            <td>{{customer.name}}</td>
            <td>{{customer.email}}</td>
            <td>{% if customer.status %} Active {% else %} Inactive {% endif %} </td>
          </tr>
          {% endfor %}