Skip to content

SiteCountdown

from django_countdown.models import SiteCountdown

One row per Site, holding the whole state of a maintenance window.

  • Tabledjango_countdown_sitecountdown
  • Verbose nameSite shutdown countdown / Site shutdown countdowns
  • Ordering["-countdown_time"], newest window first

Fields

Field Type Null Description
site OneToOneField(Site, on_delete=CASCADE) no The domain this countdown closes. One-to-one, so a site has at most one countdown; deleting the site deletes the countdown.
countdown_time DateTimeField yes When the site starts returning 503. NULL disables blocking entirely — the row exists but never matches.
maintenance_until DateTimeField yes When the site reopens. NULL means indefinite: it stays blocked until the row is deleted.
message CharField(max_length=200) no Headline shown in the banner and on the maintenance page.
long_description TextField(blank=True, default="") no Optional longer text, shown on the maintenance page only, rendered through linebreaks.
created_at DateTimeField(auto_now_add=True) no Set on creation.
updated_at DateTimeField(auto_now=True) no Refreshed on every save().

countdown_time = NULL never blocks, but does render a banner

is_expired() returns False for a row with no countdown time, so the middleware never blocks — but the context processor still publishes it as active_countdown, and the banner renders with an empty timer (there is no target time to count down to).

A row like this is reachable from the admin, where Countdown time is an optional field. Treat it as unfinished configuration, not as a way to park a countdown: delete the row instead.

Methods

is_expired() -> bool

True once now >= countdown_time, i.e. the window has opened. Always False when countdown_time is NULL.

Exposed in the admin as the boolean column Expired?.

is_indefinite() -> bool

True when a countdown is set but has no end — countdown_time is not None and maintenance_until is None. Admin column Indefinite?.

is_maintenance_finished() -> bool

True once now >= maintenance_until. Always False for indefinite maintenance, which is what keeps an open-ended window open.

This is the method that reopens the site: the middleware stops blocking as soon as it returns True.

time_remaining() -> str

Human-readable time until countdown_time, translated:

State Returns
countdown_time is None "Not set"
is_expired() "Expired"
otherwise e.g. "2 days 3 h 15 min"

Components that are zero are dropped, except that seconds are always shown when nothing else would be. This is a snapshot rendered server-side — it does not tick. For a live timer, use the banner's JavaScript.

maintenance_time_remaining() -> int | None

Whole seconds until maintenance_until. None when maintenance is indefinite or already finished — those two cases are not distinguishable from the return value alone; check is_indefinite() if you need to tell them apart.

maintenance_duration_minutes() -> int | None

Planned window length in minutes, maintenance_until - countdown_time. None if either timestamp is missing. The banner uses it for "Estimated maintenance duration: 30 minutes".

Note this is the planned duration, computed from the two timestamps — it does not shrink as the window elapses.

clean()

Model validation, run by full_clean() and therefore by the admin and by start_countdown, but not by plain save(), update(), update_or_create() or bulk_create().

Rule Field error raised on
countdown_time must be in the future countdown_time
maintenance_until must be later than countdown_time maintenance_until

The first rule applies to every save, forever

Once countdown_time is in the past, the row can no longer be saved through validated paths — the admin form rejects it even if you only touched maintenance_until.

This is why extend_countdown and shorten_countdown do not call full_clean(). They check both invariants above explicitly instead, against the specific boundary each mode moves, and then save with update_fields. That is what lets them adjust a window that is already running, which is the state you actually need them in. See Managing a running countdown and Django admin.

__str__()

"{site.domain} - {message} ({countdown_time})".

Working with it in code

Creating a window programmatically, with validation:

from datetime import timedelta

from django.contrib.sites.models import Site
from django.utils import timezone

from django_countdown.models import SiteCountdown

start = timezone.now() + timedelta(minutes=15)

countdown = SiteCountdown(
    site=Site.objects.get_current(),
    countdown_time=start,
    maintenance_until=start + timedelta(minutes=30),
    message="Database upgrade",
)
countdown.full_clean()   # enforce clean(); skip only if you know why
countdown.save()

Replacing whatever is there (the pattern start_countdown uses):

SiteCountdown.objects.update_or_create(
    site=site,
    defaults={
        "countdown_time": start,
        "maintenance_until": None,     # indefinite
        "message": "Unplanned maintenance",
    },
)

Reopening:

SiteCountdown.objects.filter(site=site).delete()

Checking state without a request:

countdown = SiteCountdown.objects.filter(site=site).first()
if countdown and countdown.is_expired() and not countdown.is_maintenance_finished():
    ...  # the site is currently closed to the public

That condition is exactly what the middleware evaluates, minus the superuser bypass — see How it works.

Migrations

Five migrations ship with the package, 0001_initial through 0005_alter_sitecountdown_options_and_more. They are ordinary Django migrations; ./manage.py migrate applies them, and the app declares a dependency on sites.