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 - MainstreamSupport - - ExtendedSupport - - - + +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.

- Django release roadmap + Django release roadmap
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