Metadata-Version: 2.4
Name: wagtail-in-a-tree
Version: 0.1.0
Summary: Manage any django-treebeard model (AL, MP or NS) in the Wagtail admin: tree listings with drag and drop, alphabetical sorting, and a tree checkbox widget.
Keywords: wagtail,django,treebeard,tree,admin,snippets
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Django :: 6.1
Classifier: Framework :: Wagtail
Classifier: Framework :: Wagtail :: 7
Classifier: Framework :: Wagtail :: 8
Requires-Dist: django>=5.2
Requires-Dist: wagtail>=7.4
Requires-Dist: django-treebeard>=5.0
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/incubateur-ademe/wagtail-in-a-tree
Project-URL: Repository, https://github.com/incubateur-ademe/wagtail-in-a-tree
Project-URL: Changelog, https://github.com/incubateur-ademe/wagtail-in-a-tree/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/incubateur-ademe/wagtail-in-a-tree/blob/main/README.md
Project-URL: Issues, https://github.com/incubateur-ademe/wagtail-in-a-tree/issues
Description-Content-Type: text/markdown

# wagtail-in-a-tree

Manage any [django-treebeard](https://django-treebeard.readthedocs.io/) model in
the Wagtail admin, as a snippet. Wagtail already depends on treebeard; this
package gives your own tree models (categories, themes, regions…) an admin made
for trees. Adjacency list (`AL_Node`), materialized path (`MP_Node`) and nested
sets (`NS_Node`) models all work, sorted by treebeard (`node_order_by`) or
ordered by hand.

- **Listing:** Wagtail's standard snippet listing, with a layout switch next to
  the search box (like the grid / list switch of the image listing): the
  top-level nodes only, each linking to its own listing, or the whole tree.
  Rows are indented by depth, with a chevron to hide or show a node's
  descendants. Searching, filtering or sorting a column gives the usual flat,
  paginated listing.
- **A node's own listing** (`children/<pk>/`) shows its whole subtree, with
  breadcrumbs back up the tree. "Add" from there starts the new node under it.
- **Moving:** drag a row onto another one to nest it under it, or onto its top
  or bottom edge to place it before or after it. Without a mouse, each row's "…"
  menu has "Move up" and "Move down", and the edit form has treebeard's
  "Relative to" / "Position" fields.
- **Sorting:** "Sort alphabetically" in the header "…" menu (the whole tree, or
  the subtree of the node being listed) and "Sort sub-items alphabetically" in
  a row's "…" menu. A confirmation page shows the tree as it will be. Accents
  and case are ignored.
- **Choosing nodes:** many-to-many fields to the model are edited with
  `TreeCheckboxSelectMultiple`, checkboxes in a collapsible tree.

## Links

- [Documentation](https://github.com/incubateur-ademe/wagtail-in-a-tree/blob/main/README.md)
- [Changelog](https://github.com/incubateur-ademe/wagtail-in-a-tree/blob/main/CHANGELOG.md)
- [Contributing](https://github.com/incubateur-ademe/wagtail-in-a-tree/blob/main/CONTRIBUTING.md)
- [Issues](https://github.com/incubateur-ademe/wagtail-in-a-tree/issues)

## Supported versions

This package supports Wagtail 7.4 and up, and all [compatible versions of Python
and Django](https://docs.wagtail.org/en/stable/releases/upgrading.html#compatible-django-python-versions),
with django-treebeard 5. It is tested from the lowest supported versions
(Python 3.11, Django 5.2, Wagtail 7.4, django-treebeard 5.0) to the latest ones.

## Installation

The package is not on PyPI yet: install it from the repository. Pick the command
for your preferred package installer:

```bash
uv add "wagtail-in-a-tree @ git+https://github.com/incubateur-ademe/wagtail-in-a-tree.git"
pip install "wagtail-in-a-tree @ git+https://github.com/incubateur-ademe/wagtail-in-a-tree.git"
```

```python
INSTALLED_APPS = [
    ...,
    "wagtail_in_a_tree",
    "treebeard",  # for treebeard's own translations ("Relative to", "Child of"…)
]
```

## Usage

### The model

```python
from django.db import models
from treebeard.mp_tree import MP_Node
from wagtail.admin.panels import FieldPanel
from wagtail_in_a_tree.forms import TreeNodeForm, tree_panel


class Category(MP_Node):
    name = models.CharField(max_length=80)

    base_form_class = TreeNodeForm  # adds the "Relative to" / "Position" fields
    panels = [FieldPanel("name"), tree_panel()]  # tree_panel(heading="…") to rename it

    def __str__(self):
        return self.name  # labels, breadcrumbs, and the key of "Sort alphabetically"
```

An adjacency list model needs treebeard's two columns:

```python
from treebeard.al_tree import AL_Node


class Category(AL_Node):
    name = models.CharField(max_length=80)
    parent = models.ForeignKey(
        "self",
        null=True,
        blank=True,
        related_name="children_set",
        on_delete=models.CASCADE,
    )
    sib_order = models.PositiveIntegerField()  # not needed with node_order_by
    ...
```

`sib_order` counts from 1 within each group of siblings: treebeard does not make
room for a move before a node whose `sib_order` is 0. When adding the column to
a populated table, number the existing rows from 1 in a data migration.

Set `node_order_by = ["name"]` to let treebeard keep siblings sorted: rows can
then only be nested, and the reordering actions (edges of a row, Move up / down,
Sort alphabetically) are not offered.

### The admin

In `wagtail_hooks.py`:

```python
from wagtail.snippets.models import register_snippet
from wagtail_in_a_tree.views import TreeSnippetViewSet

from .models import Category


class CategoryViewSet(TreeSnippetViewSet):
    model = Category
    icon = "tag"
    list_display = ["name"]
    search_fields = ["name"]  # the layout switch sits in the header's search form
    add_to_admin_menu = True  # its own sidebar item rather than under Snippets


register_snippet(CategoryViewSet)
```

`TreeSnippetViewSet` is a `SnippetViewSet`: everything it supports still works.
Only snippets are supported, not a plain `ModelViewSet`.

### Many-to-many fields to the model

Once the model is registered with `TreeSnippetViewSet`, every `ManyToManyField`
or `ParentalManyToManyField` pointing to it gets `TreeCheckboxSelectMultiple` in
Wagtail's admin forms, with no panel change. Choices left out by
`limit_choices_to` are hidden, their ancestors staying as plain labels. Other
many-to-many fields (to pages, collections or any other model) are not affected.

To use another widget on one field: `FieldPanel("categories", widget=...)`. To
use the tree widget on a model registered otherwise:

```python
from wagtail_in_a_tree.widgets import TreeCheckboxSelectMultiple

FieldPanel("categories", widget=TreeCheckboxSelectMultiple)
```

## Reference

**Public API**

| Import | What for |
|---|---|
| `wagtail_in_a_tree.views.TreeSnippetViewSet` | the snippet viewset to register |
| `wagtail_in_a_tree.forms.TreeNodeForm` | `base_form_class` of the model |
| `wagtail_in_a_tree.forms.tree_panel(heading=None)` | the panel of the move fields |
| `wagtail_in_a_tree.widgets.TreeCheckboxSelectMultiple` | the tree checkbox widget |
| `wagtail_in_a_tree.tree.sort_alphabetically(model, parent=None)` | the sort, for scripts and migrations |

**URLs** added to the viewset, next to Wagtail's own:

| URL | View |
|---|---|
| `children/<pk>/` | the subtree of a node |
| `move/` | POST `node`, `target`, `position` (`child`, `before` or `after`) |
| `sort/?parent=<pk>` | confirmation, then POST to sort |

**Permissions:** moving and sorting need the model's "change" permission. Users
without it see the listings without drag and drop or move actions.

**Customizing:** `TreeSnippetViewSet` takes Wagtail's usual view class
attributes (`index_view_class`, `add_view_class`…) plus `move_view_class` and
`sort_view_class`. The templates live under `wagtail_in_a_tree/`.

**Translations:** French is included.

### Limits

- **Tree listings are not paginated:** every node of the tree (or subtree) is on
  one page, so that any row can be a drop target.
- **Large trees:** the "Children" column counts each top-level row with one query,
  and sorting moves one node at a time. This suits trees of a few hundred nodes,
  not page-tree sizes.
- **Translatable models (`TranslatableMixin`):** listings follow the locale
  selected in the listing, and "Relative to" only offers nodes of the node's
  locale. Sorting orders every group of siblings, whatever its locale.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md): `just demo` runs a test project with an
AL, an MP and an NS tree, `just test` the test suite.

## License

[MIT](LICENSE).
