Metadata-Version: 2.4
Name: djxi
Version: 0.1.9
Summary: Django HTMX Integration
Author-email: Philipp Rollinger <philipp.rollinger@protonmail.com>
License: MIT
Project-URL: Homepage, https://github.com/rollinger/djxi
Project-URL: Repository, https://github.com/rollinger/djxi
Project-URL: Documentation, https://djxi.readthedocs.io/
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENCE.md
Requires-Dist: django>=4.2
Provides-Extra: dev
Requires-Dist: pytest>=7.4.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: ruff>=0.0.275; extra == "dev"
Requires-Dist: pre-commit>=3.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Dynamic: license-file

# <img src="https://raw.githubusercontent.com/rollinger/djxi/master/assets/Djxi_logo.png" alt="djxi logo" style="max-width: 111px; background: white;"> HTMX Integration for Django

[![PyPI](https://img.shields.io/pypi/v/Djxi)](https://pypi.org/project/djxi)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/Djxi)](https://pypi.org/project/djxi)
[![PyPI - Django Version](https://img.shields.io/pypi/djversions/Djxi)](https://pypi.org/project/djxi)
[![CI](https://github.com/rollinger/djxi/actions/workflows/main.yml/badge.svg)](https://github.com/rollinger/djxi/actions/workflows/main.yml)
[![codecov](https://codecov.io/gh/rollinger/djxi/branch/master/graph/badge.svg)](https://codecov.io/gh/rollinger/djxi)
![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)

**[Full documentation →](https://djxi.readthedocs.io/en/latest/)**

---

HTMX features tend to scatter across `urls.py`, `views.py`, and a handful of template
snippets. djxi solves this by bundling the URL patterns, view logic, and HTML sections
for a feature into a single class — the **`DXEndpointBattery`**.

```python
from djxi import DXEndpointBattery, dx_get, dx_post, dx_delete

class TodoBattery(DXEndpointBattery):
    inline_template = """
    <dx-section name="list">
        <ul>{% for item in items %}<li>{{ item }}</li>{% endfor %}</ul>
    </dx-section>
    <dx-section name="form">
        <form hx-post="{% url 'todo:create' %}">...</form>
    </dx-section>
    """

    @dx_get("list", name="list")
    def list(self, request):
        return self.render_section(request, "list", {"items": Item.objects.all()})

    @dx_post("create", name="create")
    def create(self, request):
        Item.objects.create(title=request.POST["title"])
        return self.render_section(request, "list", {"items": Item.objects.all()})

    @dx_delete("item/<int:pk>/delete", name="delete")
    def delete(self, request, pk):
        Item.objects.filter(pk=pk).delete()
        return self.render_empty(request)
```

```python
# urls.py
urlpatterns = [
    path("todo/", include((TodoBattery.url_patterns(), "todo"), namespace="todo")),
]
```

---

## Features

| Feature | Description |
|---|---|
| **DXEndpointBattery** | Bundles URLs, view logic, and HTML in one class |
| **`<dx-section>` / `<dx-include>`** | Split templates into named, reusable sections |
| **Routing decorators** | `@dx_get`, `@dx_post`, `@dx_put`, `@dx_patch`, `@dx_delete`, `@dx_action` |
| **`battery_prefix`** | Per-battery default URL prefix (overrides global `DX_ROUTER_PREFIX`) |
| **Async handlers** | `async def` methods get ASGI-compatible views; `arender_section()` for async rendering |
| **Permission hooks** | `requires_auth`, `login_url`, and `check_permissions()` override |
| **HTMX headers** | `request.htmx` / `response.htmx` with fluent chainable setters |
| **Method override** | `X-HTTP-Method-Override` header **and** `<input name="_method">` POST field |
| **Django messages** | Out-of-band message injection via `hx-swap-oob` |
| **`djxi_routes`** | Management command listing all registered battery routes |
| **`djxi.testing`** | `DXRequestFactory`, `DXBatteryTestCase`, assertion helpers |
| **Typed** | Full type annotations; ships `py.typed` for PEP 561 |
| **HTMX 2 & 4** | Both HTMX versions supported; switch with `DX_HTMX_VERSION` |

---

## Installation

```bash
pip install djxi
```

**`settings.py`:**

```python
INSTALLED_APPS = [
    # ...
    "djxi",
]

MIDDLEWARE = [
    # ... Django middleware ...
    "djxi.middleware.DjxiHeadersMiddleware",  # optional but recommended
]
```

**Base template:**

```html
{% load djxi %}
<!doctype html>
<html>
  <head>
    {% htmx_script_inclusion %}
  </head>
  <body {% htmx_headers %}>
    {% flash_messages_inclusion %}
    {% block content %}{% endblock %}
  </body>
</html>
```

---

## Core Concepts

### Sections and Includes

Templates are split using custom HTML tags (no Django template syntax
required — they sit alongside it):

```html
<dx-section name="item-form">
  <form hx-post="{% url 'todo:create' %}">
    <input name="title">
    <button>Add</button>
  </form>
</dx-section>

<dx-section name="item-row">
  <li id="item-{{ item.pk }}">
    {{ item.title }}
    <dx-include name="item-actions"/>   {# reuse another section inline #}
  </li>
</dx-section>

<dx-section name="item-actions">
  <button hx-delete="{% url 'todo:delete' item.pk %}">Delete</button>
</dx-section>
```

### Routing

Decorators mark methods as endpoints. `url_patterns()` converts them to Django paths:

```python
class MyBattery(DXEndpointBattery):
    battery_prefix = "htmx"  # overrides DX_ROUTER_PREFIX for this battery

    @dx_action("item/<int:pk>", methods=["GET", "POST"], name="item")
    def item(self, request, pk):
        ...

    @dx_delete("item/<int:pk>/delete", name="item-delete")
    def delete(self, request, pk):
        ...
```

```python
urlpatterns = [
    path("api/", include(MyBattery.url_patterns())),
    # → GET/POST  api/htmx/item/<pk>
    # → DELETE    api/htmx/item/<pk>/delete
]
```

### HTMX Headers (Middleware)

```python
@dx_put("item/<int:pk>/flag", name="flag")
def flag(self, request, pk):
    item = Item.objects.get(pk=pk)
    response = self.render_section(request, "item-row", {"item": item})
    # Fluent chaining — all setters return self
    response.htmx.set_trigger("itemFlagged").set_retarget(f"#item-{pk}")
    return response
```

### Async Handlers

```python
@dx_get("items/", name="items")
async def items(self, request):
    items = [item async for item in Item.objects.all()]
    return await self.arender_section(request, "item-list", {"items": items})
```

### Permission / Auth Hooks

```python
class ProtectedBattery(DXEndpointBattery):
    requires_auth = True      # 403 for anonymous users
    login_url = "/login/"     # redirect instead of 403

    # Or fine-grained:
    def check_permissions(self, request):
        if not request.user.has_perm("myapp.can_edit"):
            return HttpResponseForbidden()
        return None  # allow through
```

### Testing

```python
from djxi.testing import DXBatteryTestCase

class TodoBatteryTests(DXBatteryTestCase):
    def test_list_renders_items(self):
        request = self.dx.get("/")          # middleware already applied
        response = TodoBattery().render_section(request, "list", {"items": []})
        self.assert_section_rendered(response, "No todos found")
```

---

## Configuration

All settings are optional. Override in your `settings.py`:

| Setting | Default | Description |
|---|---|---|
| `DX_HTMX_VERSION` | `"4"` | HTMX version for CDN script tag (`"2"` or `"4"`) |
| `DX_HTMX_COMPRESSION` | `".js"` | Script variant (`".js"` or `".min.js"`) |
| `DX_ROUTER_PREFIX` | `"dx"` | Global URL prefix prepended to all battery routes |
| `DX_MESSAGE_CONTAINER_ID` | `"message-container"` | DOM ID for OOB message swap target |
| `DX_MESSAGE_SWAP_METHOD` | `"beforeend"` | `hx-swap-oob` insertion method |
| `DX_MESSAGE_TEMPLATE` | `"djxi/messages/message_list.html"` | Template for the message list |
| `DJXI_ROUTE_MODULES` | `["views","endpoints","batteries"]` | Modules scanned by `djxi_routes` |

Constants (cannot be overridden via `settings.py`):

| Constant | Value |
|---|---|
| `DX_SECTION_TAG` | `"dx-section"` |
| `DX_INCLUDE_TAG` | `"dx-include"` |

---

## Development Status

Pre-Alpha — experimental. API may change between minor versions.

- v0.2.0: Public Alpha — improve existing facilities / coverage
- v0.3.0: Public Beta
- v1.0.0: Stable release
