diff --git a/Makefile b/Makefile
index 910ce300e4..cec476d565 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,7 @@ SCSS = djangoproject/scss
STATIC = djangoproject/static
ci: compilemessages test
+ @python tools/generate_release_roadmap.py --data tools/releases.json --out djangoproject/static/img/release-roadmap.svg || true
@python -m coverage report
compilemessages:
diff --git a/djangoproject/static/img/release-roadmap.svg b/djangoproject/static/img/release-roadmap.svg
index 45f91e1ae3..616c4b8dee 100644
--- a/djangoproject/static/img/release-roadmap.svg
+++ b/djangoproject/static/img/release-roadmap.svg
@@ -1,740 +1,69 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- image/svg+xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 2023
- 2024
- 2025
- 2026
- 2027
- 2028
- 2029
-
-
- April
- August
- December
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- LTS
- LTS
- LTS
-
-
- 4.2
- 5.0
- 5.1
- 5.2
- 6.0
- 6.1
- 6.2
- 7.0
- Mainstream Support
-
- Extended Support
-
-
-
+
+Django release roadmap
+Shows bugfix and security-only support windows for Django releases
+
+
+
+
+
+
+2024
+
+2025
+
+2026
+
+2027
+
+2028
+
+2029
+
+2030
+
+
+
+
+Bugfix support
+
+Security-only
+
+7.0
+
+
+Apr 2029
+6.2 LTS
+
+
+Apr 2030
+6.1
+
+
+Dec 2027
+6.0
+
+
+Apr 2027
+5.2 LTS
+
+
+Apr 2028
+5.1
+
+
+Dec 2025
+4.2 LTS
+
+
+Apr 2026
+
\ No newline at end of file
diff --git a/djangoproject/templates/releases/_download_links.html b/djangoproject/templates/releases/_download_links.html
index d73ba79092..786202ba47 100644
--- a/djangoproject/templates/releases/_download_links.html
+++ b/djangoproject/templates/releases/_download_links.html
@@ -1,12 +1,16 @@
{% load release_notes %}
-
- {{ release.tarball.name }}
-
-{% if release.checksum %}
- Checksums:
-
- {{ release.checksum.name }}
+{% if release and release.version %}
+
+ {{ release.tarball.name }}
+ {% if release.checksum %}
+ Checksums:
+
+ {{ release.checksum.name }}
+
+ {% endif %}
+ Release notes: {% release_notes release.version %}
+{% else %}
+ No download links available for this release.
{% endif %}
-Release notes: {% release_notes release.version %}
diff --git a/djangoproject/templates/releases/download.html b/djangoproject/templates/releases/download.html
index 21d31c81a2..069837aed6 100644
--- a/djangoproject/templates/releases/download.html
+++ b/djangoproject/templates/releases/download.html
@@ -6,7 +6,7 @@
{% block layout_class %}sidebar-right{% endblock %}
{% block og_title %}Download Django{% endblock %}
-{% block og_image %}{% static "img/release-roadmap.png" %}{% endblock %}
+{% block og_image %}{% static "img/release-roadmap.svg" %}{% endblock %}
{% block og_image_alt %}Django's release roadmap{% endblock %}
{% block og_description %}The latest official version is {{ current.version }}{% if current.is_lts %} (LTS){% endif %}{% endblock %}
{% block og_image_width %}1030{% endblock %}
@@ -25,14 +25,19 @@
How to get Django
the FAQ for the Python versions supported by each version of Django.
Here’s how to get it:
+
Option {% cycle '1' '2' '3' as options %}: Get the latest official version
- The latest official version is {{ current.version }}{% if current.is_lts %} (LTS){% endif %}. Read the
- {% release_notes current.version show_version=True %}, then install it with
- pip :
- Linux / macOS:
- python -m pip install Django=={{ current.version }}
- Windows:
- py -m pip install Django=={{ current.version }}
+ {% if current and current.version %}
+ The latest official version is {{ current.version }}{% if current.is_lts %} (LTS){% endif %}. Read the
+ {% release_notes current.version show_version=True %}, then install it with
+ pip :
+ Linux / macOS:
+ python -m pip install Django=={{ current.version }}
+ Windows:
+ py -m pip install Django=={{ current.version }}
+ {% else %}
+ Note: No current release is set in the database. Please add a supported Release object to see the latest version here.
+ {% endif %}
{% if preview %}
{% with preview.version|slice:":3" as major_version %}
@@ -74,7 +79,7 @@ Supported Versions
See the
supported versions policy for detailed guidelines about what fixes will be backported.
-
+
diff --git a/tools/generate_release_roadmap.py b/tools/generate_release_roadmap.py
new file mode 100644
index 0000000000..ee33134cd1
--- /dev/null
+++ b/tools/generate_release_roadmap.py
@@ -0,0 +1,238 @@
+#!/usr/bin/env python3
+"""
+Generate an SVG release roadmap image from machine-readable data, similar to
+https://devguide.python.org/versions/.
+
+Usage:
+ python tools/generate_release_roadmap.py --data data/releases.json --out djangoproject/static/img/release-roadmap.svg
+
+The input is a JSON array of objects with keys:
+- version: string, e.g. "5.2"
+- release_date: YYYY-MM-DD
+- bugfix_end: YYYY-MM-DD
+- security_end: YYYY-MM-DD
+- is_lts: boolean
+"""
+from __future__ import annotations
+
+import argparse
+import datetime as dt
+import json
+from dataclasses import dataclass
+from typing import List
+
+DATE_FMT = "%Y-%m-%d"
+
+
+@dataclass
+class Cycle:
+ version: str
+ release_date: dt.date
+ bugfix_end: dt.date
+ security_end: dt.date
+ is_lts: bool
+
+ @classmethod
+ def from_dict(cls, d: dict) -> Cycle:
+ return cls(
+ version=str(d["version"]),
+ release_date=dt.date.fromisoformat(d["release_date"]),
+ bugfix_end=dt.date.fromisoformat(d["bugfix_end"]),
+ security_end=dt.date.fromisoformat(d["security_end"]),
+ is_lts=bool(d.get("is_lts", False)),
+ )
+
+
+def load_cycles(path: str) -> list[Cycle]:
+ with open(path, encoding="utf-8") as f:
+ data = json.load(f)
+ cycles = [Cycle.from_dict(x) for x in data]
+ # sort newest first for display
+ cycles.sort(key=lambda c: (c.release_date, c.version), reverse=True)
+ return cycles
+
+
+def month_floor(d: dt.date) -> dt.date:
+ return dt.date(d.year, d.month, 1)
+
+
+def month_add(d: dt.date, months: int) -> dt.date:
+ y = d.year + (d.month - 1 + months) // 12
+ m = (d.month - 1 + months) % 12 + 1
+ day = min(
+ d.day,
+ [
+ 31,
+ 29 if y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) else 28,
+ 31,
+ 30,
+ 31,
+ 30,
+ 31,
+ 31,
+ 30,
+ 31,
+ 30,
+ 31,
+ ][m - 1],
+ )
+ return dt.date(y, m, day)
+
+
+def generate_svg(
+ cycles: list[Cycle],
+ width=1100,
+ row_h=24,
+ row_gap=14,
+ left_margin=150,
+ right_margin=20,
+ top_margin=60,
+ bottom_margin=30,
+) -> str:
+ # Determine time bounds
+ min_start = min(c.release_date for c in cycles)
+ max_end = max(c.security_end for c in cycles)
+
+ # pad bounds by one month on each side
+ min_start = month_add(month_floor(min_start), -1)
+ max_end = month_add(month_floor(max_end), 1)
+
+ # build a list of month ticks from min_start to max_end
+ ticks = []
+ t = dt.date(min_start.year, min_start.month, 1)
+ while t <= max_end:
+ ticks.append(t)
+ t = month_add(t, 1)
+
+ inner_w = width - left_margin - right_margin
+
+ def x_for(date: dt.date) -> float:
+ # linear mapping by months
+ total_months = (max_end.year - min_start.year) * 12 + (
+ max_end.month - min_start.month
+ )
+ if total_months == 0:
+ return left_margin
+ months_from_start = (
+ (date.year - min_start.year) * 12
+ + (date.month - min_start.month)
+ + (date.day - 1) / 31.0
+ )
+ return left_margin + inner_w * (months_from_start / total_months)
+
+ height = top_margin + bottom_margin + len(cycles) * (row_h + row_gap) - row_gap
+
+ # SVG header
+ parts = [
+ f"",
+ "Django release roadmap ",
+ "Shows bugfix and security-only support windows for Django releases ",
+ (
+ ""
+ ),
+ ]
+
+ # axis and month grid
+ axis_y = top_margin - 25
+ parts.append(f"")
+ parts.append(
+ f" "
+ )
+ parts.append(" ")
+
+ # month tick labels for Jan of each year
+ parts.append("")
+ for tick in ticks:
+ x = x_for(tick)
+ if tick.month == 1:
+ parts.append(
+ f" "
+ )
+ parts.append(f"{tick.year} ")
+ parts.append(" ")
+
+ # legend
+ legend_x = left_margin
+ legend_y = 20
+ parts.append("")
+ parts.append(
+ f" "
+ )
+ parts.append(
+ f" "
+ )
+ parts.append(f"Bugfix support ")
+ parts.append(
+ f" "
+ )
+ parts.append(f"Security-only ")
+ parts.append(" ")
+
+ # rows for cycles
+ y = top_margin
+ for c in cycles:
+ # label
+ label = f"{c.version}{' LTS' if c.is_lts else ''}"
+ parts.append(
+ f"{label} "
+ )
+ parts.append(
+ f"{label} "
+ )
+ # bars
+ x1 = x_for(c.release_date)
+ x2 = x_for(c.bugfix_end)
+ x3 = x_for(c.security_end)
+ # bugfix bar
+ parts.append(
+ f" "
+ )
+ # security bar
+ parts.append(
+ f" "
+ )
+ # end markers
+ parts.append(
+ f"{c.security_end.strftime('%b %Y')} "
+ )
+ parts.append(
+ f"{c.security_end.strftime('%b %Y')} "
+ )
+ y += row_h + row_gap
+
+ parts.append(" ")
+ return "\n".join(parts)
+
+
+def main() -> None:
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--data", required=True)
+ ap.add_argument("--out", required=True)
+ args = ap.parse_args()
+
+ cycles = load_cycles(args.data)
+
+ # simple validation
+ for c in cycles:
+ if not (c.release_date <= c.bugfix_end <= c.security_end):
+ raise SystemExit(
+ f"Invalid dates for {c.version}: release <= bugfix_end <= security_end is required"
+ )
+
+ svg = generate_svg(cycles)
+
+ # ensure output directory exists
+ out_path = args.out
+ import os
+
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
+ with open(out_path, "w", encoding="utf-8") as f:
+ f.write(svg)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/releases.json b/tools/releases.json
new file mode 100644
index 0000000000..729af68af4
--- /dev/null
+++ b/tools/releases.json
@@ -0,0 +1,51 @@
+[
+ {
+ "version": "4.2",
+ "release_date": "2023-04-03",
+ "bugfix_end": "2023-12-04",
+ "security_end": "2026-04-01",
+ "is_lts": true
+ },
+ {
+ "version": "5.1",
+ "release_date": "2024-08-07",
+ "bugfix_end": "2025-04-02",
+ "security_end": "2025-12-01",
+ "is_lts": false
+ },
+ {
+ "version": "5.2",
+ "release_date": "2025-04-01",
+ "bugfix_end": "2025-12-01",
+ "security_end": "2028-04-01",
+ "is_lts": true
+ },
+ {
+ "version": "6.0",
+ "release_date": "2025-12-01",
+ "bugfix_end": "2026-08-01",
+ "security_end": "2027-04-01",
+ "is_lts": false
+ },
+ {
+ "version": "6.1",
+ "release_date": "2026-08-01",
+ "bugfix_end": "2027-04-01",
+ "security_end": "2027-12-01",
+ "is_lts": false
+ },
+ {
+ "version": "6.2",
+ "release_date": "2027-04-01",
+ "bugfix_end": "2027-12-01",
+ "security_end": "2030-04-01",
+ "is_lts": true
+ },
+ {
+ "version": "7.0",
+ "release_date": "2027-12-01",
+ "bugfix_end": "2028-08-01",
+ "security_end": "2029-04-01",
+ "is_lts": false
+ }
+]
\ No newline at end of file