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, then share buttons
  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/forecast/ the forecast maps drawn from ?date= (today by default), and the periods they cover
POST api/forecast/ promote one ({date, period}) into the image library, frozen
GET api/forecast/cities/ the published forecast of ?date=&period= city by city, and every city
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>/issues/ a new issue of this template, the one of the period containing {date} (today by default), made on the server
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. A template sets the period each of its issues covers — a day, a dekad (1–10, 11–20, 21 to the end of the month) or a month, the one that begins. "New bulletin" asks the server for the issue of the current period: a draft whose "Effective from" and "Effective until" are that period's, titled "{template} — {period}" in the admin's language, its forecast maps frozen. Its html is rendered by the studio when it is published — until then, neither the studio nor Wagtail's own publish actions will put it online.

The banner of a template can hold dates ("Insert a date": period start, period end, period, previous period, publication date), which each issue fills with its own when it is displayed and rendered — "Valid from {start} to {end}" in the document, "Valid from 11 October 2026 to 20 October 2026" on the page.

A template can also prepare its issues ("Draft at", in the template's header, site time by the quarter hour): on the first day of each period, a Celery beat task makes the draft once that time has passed, and a forecaster reviews and publishes it. Nothing is published automatically. 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.

Forecast maps (meteorological sites only, with forecastmanager) are redrawn after each forecast pull, so a bulletin never points at them: POST api/forecast/ copies the current map into the library, in a "Forecast maps" collection, and hands back its rendition like any image. Promoting the same map twice returns the same image; the library picker leaves that collection to searches.

The same forecast also comes as a grid of cities (the "Cities" block): each city's weather icon, max/min temperatures and wind (an arrow, the compass point, the speed), in one to three columns, for all cities or a chosen list in a chosen order. It is html, not an image: it reflows on a phone, follows the editor's language and prints. An issue copies the forecast's values into its document, so a published bulletin keeps showing the forecast of its own date; "Update" appears when the office's forecast has changed since.

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.0a2

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.0a2
File Size Uploaded Provenance Actions
climweb_bulletin_studio-0.1.0a2.tar.gz 304.0 kB With provenance Details for climweb_bulletin_studio-0.1.0a2.tar.gz

Built distribution (wheel)

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

Total release size: 614.0 kB

Release files / climweb_bulletin_studio-0.1.0a2.tar.gz

Download URL climweb_bulletin_studio-0.1.0a2.tar.gz
Size 304.0 kB
Tags Source
SHA-256 checksum
How to use checksums
924b99c6d60a5c5a5fd2deb528b465fbbb04a364a001083e46e893190125b4a4
BLAKE2b-256 checksum
How to use checksums
58675b1475f5b16b04355c891a08618852d204d91f266be04dca2af30ef0f143
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 12, 2026.

Transparency log

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

Download URL climweb_bulletin_studio-0.1.0a2-py3-none-any.whl
Size 310.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9229bae2f32915e3865e5c26ccc0531b85f34ba06feec5f32f9b29ae8de274aa
BLAKE2b-256 checksum
How to use checksums
caf974881ee67a2189a54b820572114ca866ac076a7ccdd5ad8886f4361ab75f
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 12, 2026.

Transparency log

Security for climweb-bulletin-studio 0.1.0a2

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.0a2.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 12, 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