Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

ClimWeb Bulletin Studio

Block-based bulletin editor for ClimWeb: a Wagtail admin entry that hosts the Bulletin Studio JS app (Vue 3 + Lexical) and publishes what it composes into ClimWeb's Products section.

A ClimWeb app, shipped like climweb-dataset-helper: an ordinary pip-installable Django app that ClimWeb pins in its requirements and lists in INSTALLED_APPS. It depends on ClimWeb (it subclasses products.ProductItemPage) and will not boot in a bare Wagtail project.

How it fits into ClimWeb

HomePage → ProductIndexPage → ProductPage ("Agromet bulletin")
                                   ├── template  (draft, is_template=True, never public)
                                   ├── issue n°12  (live ProductItemPage)
                                   └── issue n°13
  • A template is just a bulletin flagged as one, living as a draft under the ProductPage an editor picks when creating it. It is a starting point, nothing stays linked afterwards — an issue may freely diverge from it.
  • An issue is a copy of the template's document, published as a real ProductItemPage: it shows up in the native product listing with its year/month filters, at a stable URL.
  • The page serves the HTML the JS app rendered. There is no Python renderer of the block document, so the editor stays the single source of layout truth; the document itself (doc) is stored opaquely next to it.

Status

Alpha. Usable end to end (templates, issues, publication, images, maps), but not yet run on a production site. See Known limitations before putting it in front of editors.

Requirements

  • ClimWeb 1.1.7 or later. The migration builds on products/0033, so an older ClimWeb refuses to migrate. Developed and tested against ClimWeb 1.2.2 (Python 3.10, Django 5.2, Wagtail 7.3).
  • Nothing else to install: geomanager and adminboundarymanager, which the map block relies on, ship with every ClimWeb. The only new dependency is nh3 (html sanitizer).

The package bundles the built frontend: there is no Node step on the ClimWeb side.

Installation

On ClimWeb

Bulletin Studio ships with ClimWeb the way the dataset helper does: ClimWeb pins climweb-bulletin-studio in climweb/requirements/base.in and lists bulletin_studio in INSTALLED_APPS. On a ClimWeb image that includes it there is nothing to install: its migrations and static files run with ClimWeb's own at startup.

Do not also set CLIMWEB_ADDITIONAL_APPS=bulletin_studio there. The app would be registered twice and Django refuses to start:

ImproperlyConfigured: Application labels aren't unique, duplicates: bulletin_studio

No ClimWeb release ships it yet (1.2.2 is the latest). Until one does, install it as an additional app, as below.

Before ClimWeb ships it

ClimWeb runs from Docker images (ghcr.io/wmo-raf/climweb, deployed with climweb-docker). A pip install run inside a running container is lost the next time the container is recreated, so build the package into an image derived from the one you run:

# Dockerfile
FROM ghcr.io/wmo-raf/climweb:v1.2.2
RUN /climweb/venv/bin/pip install --no-cache-dir climweb-bulletin-studio==0.1.0a1

Pre-releases need that exact pin (or pip install --pre): pip skips them otherwise. To try a wheel that is not on PyPI yet, COPY it into the image and pip install the file instead.

Then, in climweb-docker:

  1. Build the image (docker build -t climweb-with-studio .) and point all three ClimWeb services at it in docker-compose.yml: climweb, climweb_celery_worker and climweb_celery_beat. They share the same environment, so a service still on the stock image fails at startup with No module named 'bulletin_studio'.

  2. Enable the app in .env, next to any app already listed there (comma-separated):

    CLIMWEB_ADDITIONAL_APPS=bulletin_studio
    
  3. docker compose up -d. The ClimWeb entrypoint migrates and collects static files on startup (MIGRATE_ON_STARTUP and COLLECT_STATICFILES_ON_STARTUP, both on by default). If your deployment disables them, run them by hand:

    docker compose exec climweb climweb migrate
    docker compose exec climweb climweb collectstatic --noinput
    

To upgrade, bump the pin and rebuild the image. When you move to a ClimWeb release that ships the studio, go back to the stock image and remove bulletin_studio from CLIMWEB_ADDITIONAL_APPS. The bulletins carry over: same app label, same tables.

First use

The admin menu shows a Bulletin Studio entry (/<admin path>/bulletin-studio/, /cms-admin/ by default). The studio stores bulletins under ClimWeb Product pages, so at least one must exist before the first template can be created. The studio never creates product pages itself.

Known limitations

  • Permissions. Any user with access to the Wagtail admin can create, publish and delete bulletins, whatever their page permissions on the Products section. For now, keep the studio to trusted editors.
  • A published bulletin is a snapshot. The page serves the html the editor rendered. A ClimWeb theme change does not restyle past issues, and a newer frontend only renders a bulletin again once someone opens and saves it.
  • Images are not reference-tracked. Bulletins point at renditions of library images. Deleting an image from the library breaks every bulletin using it, without any warning.
  • Languages. The studio UI exists in English, French, Spanish, Portuguese and Arabic. Other ClimWeb languages (Amharic, Swahili) fall back to English.
  • Uninstalling. Delete the bulletin pages first. Wagtail cannot display pages whose model is no longer installed.

Layout

bulletin_studio/          the Django app
  wagtail_hooks.py        admin menu entry + admin urls
  apps.py                 ready(): lets BulletinPage live under a ProductPage
  models.py               BulletinPage(ProductItemPage): doc, html, is_template
  views.py                the admin page + the JSON store the JS app talks to
  templates/…/app.html    Wagtail admin shell + mount point for the JS app
  templates/…/bulletin_page.html   the public page: the rendered html, nothing else
  static/…/app/           built JS bundle (see sync-frontend.sh)

Frontend

The UI lives in its own repo (bulletin-studio-js) and the admin page can load it two ways.

Dev — Vite dev server, HMR, no build step. Point the app at a running dev server:

cd ../bulletin-studio-js && npm run dev          # fixed port 5180 (strictPort)
# then BULLETIN_STUDIO_DEV_SERVER=http://localhost:5180 in .env

The template then loads /@vite/client and /src/main.ts from that origin instead of the bundle, and styles arrive through Vite's module graph.

Built bundle. ./sync-frontend.sh ../bulletin-studio-js builds and copies dist/ into bulletin_studio/static/bulletin_studio/app/. In this checkout that path is a symlink to ../bulletin-studio-js/dist, so a plain npm run build in the JS repo is enough (the script detects the symlink and skips the copy).

The build emits two stylesheets: bulletin-studio.css for the editor, and bulletin-studio-content.css — the bulletin's own content styles, which the public page loads. Both come from the same source, so what the author composes is what the site shows.

Either way the page hands the app its API urls, CSRF token and the admin user's active language through data attributes on #bulletin-studio-app — the studio translates itself into whatever language the Wagtail admin is set to (English, French, Spanish, Portuguese, Arabic; other ClimWeb languages fall back to English).

Store API

All under /<admin path>/bulletin-studio/, staff-only, CSRF-protected (send X-CSRFToken):

Method Path Purpose
GET api/product-pages/ the products a bulletin can live under
GET api/images/ browse the ClimWeb image library (?q= filters on title)
GET api/map-config/ where geomanager serves its layers, admin boundary tiles, country bounds
POST api/images/ upload an image into that library
GET api/bulletins/ dashboard listing (templates and issues, metadata only)
POST api/bulletins/ create under a product page
GET api/bulletins/<id>/ read one, with its document (latest draft)
PUT api/bulletins/<id>/ save — a Wagtail draft revision
POST api/bulletins/<id>/publish/ publish the draft
DELETE api/bulletins/<id>/ delete

Saving never publishes: a live issue keeps serving its published html until someone hits publish. The html payload is sanitized on write (nh3) — it comes from a browser. Publishing and deleting clear ClimWeb's page cache, as Wagtail's own editor does: in production, ClimWeb caches public pages for hours.

Images are ClimWeb images: the studio uploads through Wagtail's own image form (which validates the real file format, not the extension) and hands back a rendition url, so a 4000px photo never reaches the page. The document keeps that url plus the image id; the library endpoint lets an editor reuse an image that is already on the site — the agency logo, typically.

Maps come from geomanager's own layer catalogue (api/datasets/), no DashboardMap snippet to configure first. Picking a layer freezes its tile path, its date and its legend into the document — a published issue keeps showing the data of its own decade, and geomanager's raster tiles require a time parameter anyway. On the public page each map is a declarative <div class="bs-map" data-tiles data-time data-bounds> that bulletin-studio-embed.js hydrates with ClimWeb's own maplibre; the legend is plain html, so it prints.

Development

The dev stack is the package installed (editable) into the ClimWeb dev image, next to its own PostGIS and Redis. Expected layout — three sibling checkouts:

wmo/
  climweb/                    ClimWeb, builds climweb_dev:latest
  bulletin-studio-js/         the frontend
  climweb-bulletin-studio/    this repo

1. The ClimWeb image. Build climweb_dev:latest from ../climweb (see its docs/_docs/technical/development/running-dev-environment.md). It only has to exist; this stack does not need ClimWeb's own compose to be running.

2. The frontend bundle. bulletin_studio/static/bulletin_studio/app is a symlink to ../bulletin-studio-js/dist. It is gitignored, so create it once after cloning, then build the JS app:

mkdir -p bulletin_studio/static/bulletin_studio
ln -s ../../../../bulletin-studio-js/dist bulletin_studio/static/bulletin_studio/app

cd ../bulletin-studio-js && npm ci && npm run build
# no Node on the host:
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$PWD":/src -w /src \
  node:22-alpine sh -c 'npm ci && npm run build'

The compose file mounts ../bulletin-studio-js/dist at /bulletin-studio-js/dist, which is where the symlink resolves inside the container.

3. The stack.

cp .env.sample .env          # set DB_PASSWORD (anything, it is a local database)
docker compose -f docker-compose.dev.yml up -d --build   # http://localhost:8010/admin

The first start runs every ClimWeb migration (MIGRATE_ON_STARTUP), which takes a minute; docker compose -f docker-compose.dev.yml logs -f climweb until daphne listens. 2FA, which ClimWeb forces on the admin, is switched off in the compose file.

4. Seed the database. It starts empty; dev-bootstrap.py creates an admin/admin superuser, a HomePage as the site root, a ProductIndexPage and an "Agromet bulletin" ProductPage for templates to live under. It is idempotent, and dev-only: it is not part of the package, and a real ClimWeb already has its page tree.

docker compose -f docker-compose.dev.yml exec -T climweb \
  /climweb/web/src/climweb/manage.py shell < dev-bootstrap.py

The studio is then at http://localhost:8010/admin/bulletin-studio/.

Day to day. bulletin_studio/ is bind-mounted, so Python changes hot-reload; packaging changes (setup.cfg, new dependencies) need --build. For the frontend, either rebuild (npm run build, then manage.py collectstatic --noinput in the container — the entrypoint only collects at startup) or run npm run dev in ../bulletin-studio-js and set BULLETIN_STUDIO_DEV_SERVER=http://localhost:5180 in .env (then up -d again). docker compose -f docker-compose.dev.yml down -v wipes the database.

docker compose -f docker-compose.dev.yml exec climweb /climweb/web/src/climweb/manage.py test bulletin_studio
docker compose -f docker-compose.dev.yml exec climweb /climweb/web/src/climweb/manage.py makemigrations bulletin_studio

Check the dependencies of a new migration before committing it. When a change references a ClimWeb model, Django pins the latest migration of that app in the ClimWeb it runs against. climweb_dev is ClimWeb's main, ahead of every release: a dependency on an unreleased migration (products/0034 and later, today) makes migrate fail on released ClimWeb with NodeNotFoundError. Point it back at a released migration (0001_initial depends on products/0033).

Known ClimWeb issue: ClimWeb's main branch (after 1.2.2) removed four HomePage fields (hero_type, show_banner_video, …) without a migration, so their NOT NULL columns survive and creating a HomePage fails with an IntegrityError. dev-bootstrap.py works around it with column defaults. BulletinStoreTests, whose test database comes straight from the migrations, fails on climweb_dev until ClimWeb ships home/0040. It passes on the released image, the one to check a release against: install the wheel into ghcr.io/wmo-raf/climweb:v1.2.2 and run manage.py test bulletin_studio there with DJANGO_SETTINGS_MODULE=climweb.config.settings.test.

Releasing

The frontend bundle is not in git. frontend.ref pins the bulletin-studio-js commit a release ships, and the publish workflow builds that commit into the package.

  1. Bump version in setup.cfg (PEP 440: 0.1.0a2, 0.1.0b1, 0.1.0), point frontend.ref at the frontend commit to ship, and move the CHANGELOG.md entry out of "unreleased".

  2. Check the package locally. In a dev checkout the bundle symlink is followed, so build the frontend at that commit first:

    (cd ../bulletin-studio-js && git checkout "$(cat ../climweb-bulletin-studio/frontend.ref)" && npm ci && npm run build)
    uv build            # or: python -m build
    unzip -l dist/*.whl # bulletin_studio/static/bulletin_studio/app/bulletin-studio*.{js,css}
    
  3. Tag <version> (0.1.0a1, no v, as climweb-dataset-helper does) and publish a GitHub release from it, ticking "pre-release" for an alpha or beta. .github/workflows/publish.yml builds the frontend, checks that the bundle is in the wheel and uploads to PyPI through trusted publishing. On PyPI, the climweb-bulletin-studio project's trusted publisher is repository wmo-raf/climweb-bulletin-studio, workflow publish.yml, environment pypi. Before the very first upload, declare it as a "pending publisher".

  4. Bump the pin in ClimWeb (climweb/requirements/base.in, then regenerate base.txt). Sites get the new version with the next ClimWeb release.

License

MIT

Metadata

Release files for climweb-bulletin-studio 0.1.0a1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for climweb-bulletin-studio 0.1.0a1
File Size Uploaded Provenance Actions
climweb_bulletin_studio-0.1.0a1.tar.gz 243.3 kB With provenance Details for climweb_bulletin_studio-0.1.0a1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for climweb-bulletin-studio 0.1.0a1
File Interpreter ABI Platform Actions
climweb_bulletin_studio-0.1.0a1-py3-none-any.whl Python 3 none any Details for climweb_bulletin_studio-0.1.0a1-py3-none-any.whl

Total release size: 484.0 kB

Release files / climweb_bulletin_studio-0.1.0a1.tar.gz

Download URL climweb_bulletin_studio-0.1.0a1.tar.gz
Size 243.3 kB
Tags Source
SHA-256 checksum
How to use checksums
5861bf0455e81edb6d201c86e6677d8ef180cb39a242f9aa6404fcbfd4485771
BLAKE2b-256 checksum
How to use checksums
0e93d2b041d9a3d5d35162e4620fd01f31a3b070ab33be566867215028cf94b9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / climweb_bulletin_studio-0.1.0a1-py3-none-any.whl

Download URL climweb_bulletin_studio-0.1.0a1-py3-none-any.whl
Size 240.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d1eca88158a8dd306237b30f3c57d8a413c30095919d09a5e6d5e452f9942cbd
BLAKE2b-256 checksum
How to use checksums
4e379ba801cd6ca3c4302613a8869a02804c0f136a6118a94fa33ffe625688e5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Security for climweb-bulletin-studio 0.1.0a1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for climweb_bulletin_studio-0.1.0a1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page