Metadata-Version: 2.4
Name: wagtail-seeder
Version: 1.1.2
Summary: Safe development-data seeding for Wagtail projects.
Author: Four Digits
License-Expression: BSD-3-Clause
Classifier: Framework :: Django
Classifier: Framework :: Wagtail
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: wagtail<8,>=7
Requires-Dist: Faker>=24
Requires-Dist: Pillow>=10
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-django>=4.8; extra == "dev"
Requires-Dist: psycopg[binary]>=3.1; extra == "dev"
Requires-Dist: ruff>=0.8; extra == "dev"
Requires-Dist: build>=1; extra == "dev"
Requires-Dist: twine>=6.1; extra == "dev"
Dynamic: license-file

# Wagtail Seeder

Development-data seeder for Wagtail 7.x (Python 3.10 and later). It creates representative
pages, media, snippets and settings by inspecting installed Wagtail models.

## Installation

Install from PyPI:

```bash
python -m pip install wagtail-seeder
```

Add `"wagtail_seeder"` to `INSTALLED_APPS`. Configure
an explicit environment; the package fails closed when it is omitted or unsupported:

```python
WAGTAIL_SEEDER = {
    "ENVIRONMENT": "dev",
    "HOMEPAGE_MODEL": "pages.HomePage",
    "DEFAULT_SITE_PORT": 80,  # use 8000 for Django's runserver
}
```

Apply the project's migrations, then inspect and seed:

```bash
python manage.py migrate
python manage.py inspect_wagtail_seed --format=json --require-complete
python manage.py seed_wagtail --confirm --require-complete
python manage.py seed_wagtail_pages --page-type pages.ArticlePage
```

`seed_wagtail` is destructive: it removes Wagtail `Site` objects and pages below
its technical root. Use an isolated development database. `seed_wagtail_pages`
preserves existing pages and reuses representatives when the configured count is met.
On an existing site, selective seeding uses its current sites and hostnames and
leaves unrelated snippets, forms and site settings untouched. Missing related
objects needed by the selected pages are still generated.
During a full reset, nullable protected links into the old page tree are detached
and populated again. Required protected links block deletion; the original sites,
pages and detached links are restored when deletion fails.
Selected page types can also be seeded through **Seeder** in the Wagtail admin
without resetting the site. Choose an existing subsite first; with a single site,
it is selected automatically. Each dashboard run creates one new page per selected
type with a distinct generated title and slug. Existing pages keep their content
and revisions. Required new ancestors are populated and published too.
Placement and generated page links stay within the chosen site's root locale,
excluding nested subsites. Availability is checked for that site, subject to
Wagtail's placement and count limits. Create a Site first if none exists.
The destructive reset is only available through the CLI.
The dashboard is available to active users with the explicitly assigned `wagtail_seeder.run_seeder` permission.
Superusers also need a direct or group grant; their automatic Django permission
bypass does not apply. Existing users are not granted access on upgrade.
CLI commands require the explicit allowed environment and do not impersonate a user.

After `migrate`, assign **Can run Wagtail Seeder** through Django's user/group
administration or Wagtail's group permissions. Users also need normal Wagtail
admin access. Removing the grant immediately hides the menu and blocks seed requests.

The dashboard includes a moving loading bar and elapsed time during a seed,
searchable page selection, and a searchable, sortable result table with status
filters. The loading bar indicates activity rather than a completion percentage.
The report starts collapsed at the top of the dashboard and can be removed with
**Clear report**. It is shown once after a seed and disappears when the page is
refreshed or left. Each concrete page has one result row with expandable details;
other results are grouped separately. Report items include optional `page_id`,
`page_title` and `page_model` metadata, an optional translated `user_message`, plus
`is_summary` (default `False`). The original `message` remains available for CLI
output and technical details. Summary rows are excluded from outcome counts so a
failure is counted once.
Report rows offer **Open page** for live, routable pages and **Edit** when
the current user can edit the page. Frontend links use the page's own site URL.
Skipped block variants include a readable location and explanation of the limit;
raw block paths and validation errors are available under **Technical details**.
All interface text and user messages use English source strings with Dutch
translations. They follow the interface language selected in the Wagtail user
profile, falling back to Wagtail's active/default admin language. The dashboard's
JavaScript catalog uses the same language for counters, elapsed time and request
errors. Translation source and compiled catalogs are included in both packages.

Existing selective CLI calls still reuse representative pages. Opt into the
dashboard's new-page behavior and restrict generation to an existing Wagtail Site:

```bash
python manage.py seed_wagtail_pages --page-type pages.ArticlePage --site-id 2 --create-new
```

The Python equivalents are
`Seeder().seed_page_types(["pages.ArticlePage"], site_id=2, create_new=True)` and
`Seeder().page_seed_plans(site_id=2)`. Both keyword arguments are optional;
without them, selective generation retains its previous behavior.

## Sites and languages

A single-site project needs only the configuration above. Use `SITE_ROOTS` for
multiple public sites or languages. Every root creates its own `Site`; descendants,
page links, settings and translatable snippets stay within their applicable scope.

```python
WAGTAIL_SEEDER = {
    "ENVIRONMENT": "dev",
    "DEFAULT_SITE_PORT": 80,
    "SITE_ROOTS": [
        {
            "page_model": "pages.HomePage",
            "hostname": "localhost",
            "language_code": "nl",
            "is_default_site": True,
        },
        {
            "page_model": "pages.HomePage",
            "hostname": "en.localhost",
            "language_code": "en",
        },
    ],
}
```

`language_code` must be in Wagtail's content languages. It resolves or creates a
`Locale`; it does not enable translation settings or translate text. Omit it to use
the project's default content language. Do not put a language string in
`field_values["locale"]`: that field expects a `Locale` instance.

Each root requires `page_model`; optional values include `hostname`, `port`,
`site_name`, `is_default_site`, `slug` and protected `field_values`. Give it a unique
`key` when another configured root uses `parent_page_key`.

`SEED_ROOT_PAGES` uses the same page configuration without creating another Site.
Use it only for technical branches that otherwise cannot be reached.
`parent_page_model` or `parent_page_key` deliberately selects an explicit parent;
`force_root: True` deliberately overrides creation restrictions at the technical
root. An explicit parent and `force_root` cannot be combined.

## Generation and validation

The seeder creates draft page structures before filling dependencies and publishing.
Editor presets keep their existing text and structure while empty required fields
and nested blocks are filled. Existing valid relationships are preserved, including
relationships in explicitly declared many-to-many intermediate models.
Scalar fields required by the page editor are filled even when the model permits
blank values, without changing the model's field definitions.
It uses Wagtail's allowed parent/child models, `can_create_at()`, global `max_count`
and per-parent limits. Titles come from model names: `NewsOverviewPage` becomes
**seed-News Overview**, followed by **seed-News Overview 2** when that title already
exists. Explicitly configured titles are retained. Generated text uses readable
page/block and field names, for example `seed-Hero-Title`, with sample paragraphs
for longer text. Unique values
only receive an extra suffix when needed, including uniqueness within a locale.
Valid defaults and populated content are retained, including
visibility flags, locales, translation keys and configured values.

`PAGE_COUNTS` maps model labels to positive counts **per public tree and locale**;
the default is one of each applicable concrete type. A subclass does not count as
its parent type. Wagtail's global singleton limits still take precedence.
The dashboard and `--create-new` add one page per selected type per run instead
of filling this representative count.
`UNIQUE_PAGE_SCOPES` adds post-seed checks for combinations of `site`, `region`
and/or `language`.

Stream/List blocks and InlinePanels follow their minimum/maximum counts, including
nested `block_counts`. By default, streams contain a compact representative sample.
Enable **Populate all block types** in the dashboard, pass `--all-block-types` to
`seed_wagtail` or `seed_wagtail_pages`, or use `Seeder(all_block_types=True)` to try
every available block type, including nested streams and streams inside lists.
Existing blocks are retained and missing types are added when space permits.
Configured providers and protected field values remain authoritative. Excluded,
unsupported or capacity-limited types are reported; mandatory content must still
validate. This does not generate a separate block for each dropdown choice.

`REQUIRED_STREAMFIELD_ITEM_COUNTS` supplies additional
business constraints not declared on the block:

```python
"REQUIRED_STREAMFIELD_ITEM_COUNTS": {"pages.ArticlePage.highlights": 2}
```

`SNIPPET_COUNTS` accepts model labels or legacy class names (labels take precedence).
The default is four; use zero to request no new instances. Translatable snippets
are generated for every configured root locale. Existing instances are reused and
validated, and stale chooser links are repaired without replacing their text.
Site settings are populated before saving, then revisited after their hooks have
created any additional pages. Page-owned inlines keep their original parent.

When `wagtailstreamforms` is installed, the seeder builds local contact forms from
registered single-line, email and multiline field blocks and configured templates.
It also supports older installations with separate FormField models. Submission
hooks stay empty; the seeder does not submit forms or send mail.

The inspector only reads configuration and model structure; it does not execute
model validation or prove rendering works. `seed_wagtail --require-complete` checks
both that graph and the results of actual model/block validation. Missing mandatory
content fails the command. Optional unsupported blocks/inlines are reported as
skipped; declared exclusions are intentional. Failed objects are rolled back,
while successful objects remain available for diagnosis.

## Small project providers

Use providers for business rules that cannot be inferred from field declarations.
Keep them in the consuming project and configure dotted paths:

```python
"MODEL_VALUE_PROVIDERS": ["myproject.seeding.model_values"],
"FIELD_VALUE_PROVIDERS": ["myproject.seeding.field_values"],
"BLOCK_VALUE_PROVIDERS": ["myproject.seeding.block_values"],
```

Model providers receive `(instance, context)` during `prepare` (before generic
population) and `validate` (before validation). Make them idempotent. Field providers
receive `(instance, field, context)` during `pre`/`post`, including relation and
StreamField fields. Block providers receive `(block, context)`. Field and block
providers return `wagtail_seeder.providers.UNHANDLED` to delegate generation;
returning `None` intentionally supplies an empty value.

Context includes `seeder`, `config`, scoped `pages`, `phase`, `instance`/`owner`,
`site`, `locale`, a zero-based `index`, `field_name` and a tuple `block_path`.
For model providers, `index` is stable within the model's ordered instances and
locale scope. For nested block providers it identifies the current stream/list item.
Protected `field_values` bypass generic field generation and link repair; model
providers may deliberately change them.

For example, a project that retrieves exactly one navigation object per language
can assign the configured languages using the model provider's index. Match
`SNIPPET_COUNTS` to the number of languages. See the
[configuration guide](docs/compatibility.md) for generic examples covering multiple
sites, custom validation, technical page types and external dependencies.

## External integrations

Exclude models and blocks whose validation or rendering needs an external service:

```python
"EXCLUDED_SNIPPET_MODELS": ["newsletter.MailingList"],
"SKIP_BLOCK_CLASS_NAMES": {"NewsletterBlock": "requires an external service"},
```

Exclusions also apply to indirect chooser/required-relation creation. Block exclusions
match class names, qualified names and subclasses. A required excluded dependency
is an error; a provider or a different valid local block is needed. External optional
video URL fields with empty valid defaults stay empty. Seeder settings cannot turn
off network calls hardcoded in a project's page rendering or save hooks.

Other optional keys are `ALLOWED_ENVIRONMENTS` (default `{"dev"}`),
`ALLOWED_HOSTNAMES` (dashboard request guard), `FAKER_LOCALE` (default `"nl_NL"`),
`DEFAULT_SITE_HOSTNAME` (default `"localhost"`) and `DEFAULT_SITE_PORT`
(default `8000`). Complete local compatibility requires testing the generated
frontend and editor in the consuming project; static inspection cannot guarantee
arbitrary custom validation or JavaScript behavior.

## Development and testing

The repository separates the distributable package from its local test project:

```text
src/wagtail_seeder/    Python package, management commands and admin templates
tests/test_project/   Runnable Django/Wagtail project, local database and media
tests/models.py       Example page and relation models
tests/migrations/     One initial migration for the example models
tests/test_*.py       Automated tests
```

From the repository root, create an environment and install the package in editable
mode. Changes in `src` then take effect immediately:

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
pytest
```

The default installation uses the latest available Wagtail 7.x. To test the lowest
supported release series, use a separate environment and install with
`python -m pip install -e ".[dev]" "wagtail>=7,<7.1"`.

Run the same checks as CI:

```bash
python tests/test_project/manage.py check
python tests/test_project/manage.py makemigrations --check --dry-run
ruff check .
ruff format --check .
pytest
```

Automated tests use an isolated database and temporary media directories. They do
not change the local demo database or leave generated media in the repository root.

## Local Wagtail project

No external project or Bakery Demo checkout is required. From the repository root:

```bash
python tests/test_project/manage.py migrate
python tests/test_project/manage.py createsuperuser
python tests/test_project/manage.py inspect_wagtail_seed --require-complete
python tests/test_project/manage.py seed_wagtail --confirm --require-complete
python tests/test_project/manage.py runserver
```

Open `http://127.0.0.1:8000/admin/` and log in. Explicitly assign **Can run Wagtail
Seeder** to the account or its group before opening **Seeder** in the menu.
Visit `http://127.0.0.1:8000/admin/seeder/`, choose a subsite and select page types
to create new pages. The destructive full seed requires `seed_wagtail --confirm`
through the CLI. Open `http://127.0.0.1:8000/` to browse the generated pages.

For example, test the selective command with:

```bash
python tests/test_project/manage.py seed_wagtail_pages --page-type tests.ArticlePage
```

The local database, uploaded/generated media and collected static files live under
`tests/test_project/` and are ignored by Git. The project is configured for local
development only.
The example models use one initial migration. If a local demo database was created
with the previous migration layout, recreate that demo database before migrating.
Installed packages continue to use the separate `wagtail_seeder` permission migration.

## Building and releasing

Build the wheel and source distribution and check their metadata:

```bash
python -m build
python -m twine check dist/*
python scripts/check_distribution.py
tar -tf dist/wagtail_seeder-*.tar.gz
unzip -l dist/wagtail_seeder-*.whl
```

Before publishing, install the built wheel in the target project's environment
with `python -m pip install /path/to/wagtail_seeder-1.0.0-py3-none-any.whl`, then
apply migrations and the local Seeder configuration. Check a full seed, selective
seeding, frontend rendering and saving generated pages in the editor. This tests
the packaged assets and any custom validation in the application.

Runtime code and templates come from `src/wagtail_seeder/`. Tests, demo databases
and media are excluded from both distributions. The source archive also contains
the README, license and build metadata.

Public releases are published from GitLab CI to PyPI using
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/using-a-publisher/).
Before the first release, configure a pending publisher on PyPI (or a publisher
on the existing project) with:

- PyPI project: `wagtail-seeder`
- GitLab namespace: `fourdigits/private-packages`
- GitLab project: `wagtail-seeder`
- Pipeline file: `.gitlab-ci.yml`
- Environment: `pypi`

Protect the `v*` tag pattern in GitLab. No stored PyPI API token is needed.
Update the version in the project metadata and
`src/wagtail_seeder/__init__.py`, then push a protected tag matching that version,
for example `v1.0.0`. CI requires the tag and both version declarations to agree,
and publishes only after tests and package checks pass. Published versions cannot
be overwritten; bump the version for each new release.

## License

BSD-3-Clause. See [LICENSE](LICENSE).
