Changelog¶
All notable changes to this project will be documented in this file.
The format follows Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
[0.4.0] — 2026-08-25¶
The maintenance page stops guessing. It used to reload itself on a timer and hope the site was back; now it asks, and moves only on an answer.
Added¶
- The maintenance page now waits for the site to come back on its own. It polls a small status endpoint in the background and, the moment the site is unblocked, sends the visitor to the page they originally asked for. A deployment window is three states, not one — the old server still serving, nothing serving while the container is replaced, the new server up — and the page now tells them apart and says which one it is looking at.
DJANGO_COUNTDOWN_STATUS_PATH(default/__countdown_status__/) — the path whereCountdownBlockingMiddlewareanswers that poll. It is matched before anything else inprocess_request, so it needs noURLconfentry and answers even while the rest of the site is blocked. The response is always HTTP 200, with the state in the body behind a"service": "django-countdown"marker: a reverse proxy with no upstream answers 5xx on its own, so a status endpoint using those codes could not be told apart from the proxy speaking for it.DJANGO_COUNTDOWN_POLL_INTERVAL(default10seconds) — how often the page checks, scheduled with ±20 % of jitter so the end of a window does not wake every waiting tab into a stampede against a server that has just started. Only one check is ever outstanding, so switching away from the tab and back while a check is in flight cannot leave two polling loops running in parallel.0turns the polling off.- The destination of that return trip is validated against the site's own
origin, on the server and again in the browser before navigating. A path
beginning with
//— or with a backslash, which browsers read as a slash — resolves as an address of its own; unchecked, the maintenance page would be an unusually effective open redirect, since it is a page visitors are told to trust and wait on. Django's development server and gunicorn normalise such paths away before the request arrives, uWSGI does not, and a library cannot know which one it runs under. blockedin that response has three values, not two.nullmeans the state could not be read — a worker that has started but cannot reach the database yet does not know whether the site is blocked. Only an explicitfalsesends the visitor back in; reportingfalsefor an unreadable state would put them on exactly the error page this feature exists to avoid.blocked_status_lineandblocked_status_classtemplate blocks for the new status line, andcountdown_status_path/countdown_poll_interval/countdown_return_urlin the blocked-page context.
Changed¶
- The maintenance page no longer reloads itself blindly — neither three seconds after the timer expires nor every 30 seconds during an indefinite window. Both reloads assumed the site would be back by the time they fired; when it was not, they dropped the visitor on the proxy's error page, where no script was left to try again and the only way back was a manual refresh. The timer now only reports, and navigation happens solely on a confirmed answer from the server.
- When the planned end passes and the site is still down, the timer's label switches from "Estimated end of maintenance in:" to "Planned end exceeded by:" and counts the overrun, instead of announcing "Maintenance finished!" for something that plainly has not finished.
- Each poll feeds the server's current
maintenance_untilback into the timer, so extending a running window withextend_countdowncorrects pages that are already open, without a reload. - The always-open prefixes (
/admin/,/static/,/media/) are now matched against the path without the mount prefix. An application served under/tenantused to lose all three during a window — including the admin login page, the one door left for lifting the block. DJANGO_COUNTDOWN_POLL_INTERVALaccepts a string, since settings are often read from the environment, and falls back to the default with a warning when the value is not a number at all. It is read while rendering the blocked page, outside the middleware's fail-open guard, so raising there would answer every visitor with a 500 for the length of the window.- The blocking decision moved into
get_blocking_countdown(), shared by the middleware and the status endpoint. The page a browser is shown and the answer it polls for now come from one function and cannot disagree.
[0.3.1] — 2026-08-07¶
A metadata-only release: no source changes, just the declared support for Django 6.1 plus the CI and packaging fixes listed below.
Added¶
- Support for Django 6.1, released 2026-08-05. The CI matrix gains
Django 6.1 on Python 3.12, 3.13 and 3.14 — 6.1 requires Python 3.12 or
newer, so the 3.10/3.11 cells stay 5.2-only. No source changes were
needed; the
Framework :: Django :: 6.1trove classifier is declared.
Changed¶
- Trove classifier moved from
Development Status :: 3 - Alphato4 - Beta. The command set now covers the whole countdown lifecycle, tests run across Python 3.10–3.14 × Django 5.2/6.0/6.1, and the documented behaviour is under test — the API is stable enough to depend on. Takes effect on PyPI with the next release; 0.3.0 shipped as Alpha.
Fixed¶
- CI actions bumped off Node 20, which GitHub has deprecated:
actions/checkout@v5,upload-artifact@v7,download-artifact@v8,deploy-pages@v5,setup-uvv9. Note thatupload-artifact@v5anddownload-artifact@v5are still Node 20 — the jump to v7/v8 is what actually moves them. setup-uv's cache key was never invalidated: it globs foruv.lock, which this repository gitignores (a library pins nothing), so the file never reached the runner and nothing ever matched. Keyed onpyproject.tomlinstead, with a per-jobcache-suffix. The suffix is not cosmetic:setup-uvruns beforeuv python install, so its key carries the runner's Python rather than the matrix one, and all eight matrix jobs would otherwise compute an identical key and race to reserve it.
[0.3.0] — 2026-08-05¶
Until now the package could only ever create a countdown. There was no
scriptable way to remove one, adjust one that was already running, or ask what
the current state was — the documentation's own answer was a manage.py shell
one-liner, composed while the site was down.
Added¶
stop_countdown— deletes countdowns and reopens the site. In indefinite mode this is the only way back. Sweeps every site by default, because it runs when something is blocked and you should not have to work out whichSITE_IDis to blame first;--site-idnarrows it. Nothing to delete is a success, not an error.show_countdown— reports which phase each countdown is in and how long until the next transition. Always exits0, including when the site is blocked, because that is a normal state for this package rather than a failure of the command.--jsonemits a machine-readable array for monitoring and deploy gates.extend_countdownandshorten_countdown— move a boundary of a window already in flight.--bannermoves when the site closes and slides the whole schedule, preserving the window's length;--servicemoves when it reopens. Both default to the current site, with--allto widen: they edit a schedule rather than ending one, so a wrong target would move another tenant's window.extend_countdown --at-least 5m— raises a floor rather than adding time, so repeated runs absorb instead of accumulating and a retry can never overshoot. This makes a dead man's switch possible: a deploy loop holds the window open while it works, and if the deploy dies the site reopens by itself.
Changed¶
start_countdown's indefinite-mode warning now namesstop_countdowninstead of pointing at the admin andmanage.py shell.
Fixed¶
- Documentation stated "There is no
stop_countdown" and built a deploy recipe on shell one-liners. Both are replaced, and the guide now lays the heartbeat and indefinite-window patterns side by side rather than declaring a winner — they fail in opposite directions, and the heartbeat's own host is often the machine being deployed.
[0.2.1] — 2026-05-13¶
Example-project polish only — the published django_countdown wheel is
unchanged from 0.2.0. No migration or code change is required for users
of the package.
Fixed¶
- Example app's discovery page (
example/) referenced the management command asstart-countdown(hyphen) in three places. Django commands use the underscored filename (start_countdown), so copy-pasting the snippets failed. All three snippets corrected.
Added¶
- Example app is now fully translatable end-to-end. Every user-visible
string on the discovery page goes through
{% trans %}/{% blocktrans %}, the preview-variant labels inviews.pyusegettext_lazy, and a complete Polish catalog ships atexample/example_project/locale/pl/LC_MESSAGES/django.{po,mo}. SetAccept-Language: pl(or run withLANGUAGE_CODE = "pl") to see it. - "Refresh this page after running the command" hints next to both CLI-invocation blocks on the discovery page, so demo users notice the banner appears on the next request.
[0.2.0] — 2026-05-12¶
Added¶
- i18n: every user-visible string now flows through
gettext_lazy/{% trans %}, with full Polish translations shipped indjango_countdown/locale/pl/LC_MESSAGES/django.{po,mo}. manage.py start_countdownmanagement command — interactive by default (asks how long the banner shows, then how long service mode lasts), or non-interactive via--banner +5m --service +30m --message … --noinput. Service duration acceptsindefinite/forever/infto keep the site blocked until an admin removes the countdown.- Indefinite maintenance mode: when
maintenance_untilisNonethe site stays blocked indefinitely. NewSiteCountdown.is_indefinite()helper + admin column; banner and blocked templates render an "indefinite — remove the countdown to unblock" affordance instead of a broken counter. - CSS-framework-agnostic blocked-page themes: three variants —
blocked.html(plain, default — ships its own stylesheet, no Bootstrap / Foundation / Tailwind required),blocked_foundation.html,blocked_bootstrap.html— all extending a sharedblocked_base.htmlso themes inherit translations automatically. Pick one via the newDJANGO_COUNTDOWN_BLOCKED_TEMPLATEsetting. - Example app discovery page: the example project now has a real
/page listing all template variants with one-click previews (/preview/<plain|foundation|bootstrap>/[indefinite/]) and a/healthz/endpoint for confirming the middleware blocks correctly. SiteCountdownmodel + admin (extracted from iplweb/bpp @ 75f3c70f7) withcountdown_timeand optionalmaintenance_until.CountdownBlockingMiddlewarereturning HTTP 503 for non-superuser requests once the countdown has expired and maintenance has not yet ended.countdown_contextcontext processor exposingactive_countdown/maintenance_countdownto templates.- Template partials:
countdown_banner.htmlandblocked.html.
Changed¶
countdown_banner.htmlno longer depends ondjango-compressor; it just{% static %}-loads the package stylesheet.blocked.htmlno longer pulls Foundation Sites or foundation-icons by default. Useblocked_foundation.htmlif you want that look.- All hard-coded Polish field labels, help-texts, and admin headings in the model + admin migrated to English as the source language, with Polish kept as a first-class translation.
Fixed¶
admin.time_remaining_displaytwoformat_html()calls with no substitutions raisedTypeErroron Django 6.0 (strict signature). Replaced withmark_safe()for the literal-HTML branches.
Migrations¶
0005_alter_sitecountdown_options_and_more.pyrecords the English field metadata. Runningmigrateis required after upgrading.
[0.1.0]¶
Never released to PyPI. Skipped in favour of 0.2.0, which is the first public release.