diff --git a/djangoproject/scss/_style.scss b/djangoproject/scss/_style.scss
index 3a6eb69d0..2fe7c0cfd 100644
--- a/djangoproject/scss/_style.scss
+++ b/djangoproject/scss/_style.scss
@@ -2529,14 +2529,48 @@ dl.data {
white-space: normal;
}
+table {
+ th {
+ background: var(--sidebar-bg);
+ font-weight: bold;
+ text-align: left;
+ }
+
+ td {
+ border-bottom: 1px solid var(--hairline-color);
+ }
+
+ td,
+ th {
+ padding: 0.5em 1em;
+ }
+}
+
table.foundation td {
- border-bottom: 1px solid var(--hairline-color);
padding: 0 5px;
}
-table.docutils td,
-table.docutils th {
- border-bottom: 1px solid var(--hairline-color);
+table.django-supported-versions,
+table.django-unsupported-versions {
+ border: 1px solid var(--hairline-color);
+ color: var(--table-color);
+
+ th,
+ td {
+ border-bottom: none;
+ padding: 5px;
+ text-align: center;
+ }
+}
+
+table.django-supported-versions th,
+table.django-supported-versions tr {
+ background-color: var(--secondary-accent);
+}
+
+table.django-unsupported-versions th,
+table.django-unsupported-versions tr {
+ background-color: var(--error-light);
}
.list-links {
@@ -3420,26 +3454,6 @@ form .footnote {
}
}
-table.django-supported-versions,
-table.django-unsupported-versions {
- border: 1px solid black;
- text-align: center;
- color: var(--table-color);
-
- th,
- td {
- padding: 5px;
- }
-}
-
-table.django-supported-versions tr {
- background-color: var(--secondary-accent);
-}
-
-table.django-unsupported-versions tr {
- background-color: var(--error-light);
-}
-
/* Corporate membership list page */
ul.corporate-members li {
diff --git a/djangoproject/templates/releases/download.html b/djangoproject/templates/releases/download.html
index bb8b9e472..21d31c81a 100644
--- a/djangoproject/templates/releases/download.html
+++ b/djangoproject/templates/releases/download.html
@@ -85,10 +85,10 @@
Supported Versions
End of extended support2 |
- 5.2 LTS |
- {% get_latest_micro_release '5.2' %} |
- December 2025 |
- April 2028 |
+ 4.2 LTS |
+ {% get_latest_micro_release '4.2' %} |
+ December 4, 2023 |
+ April 2026 |
5.1 |
@@ -97,10 +97,10 @@ Supported Versions
December 2025 |
- 4.2 LTS |
- {% get_latest_micro_release '4.2' %} |
- December 4, 2023 |
- April 2026 |
+ 5.2 LTS |
+ {% get_latest_micro_release '5.2' %} |
+ December 2025 |
+ April 2028 |
@@ -114,28 +114,28 @@ Future Roadmap
End of extended support2 |
- 7.0 |
- December 2027 |
- August 2028 |
- April 2029 |
+ 6.0 |
+ December 2025 |
+ August 2026 |
+ April 2027 |
- 6.2 LTS |
+ 6.1 |
+ August 2026 |
April 2027 |
December 2027 |
- April 2030 |
- 6.1 |
- August 2026 |
+ 6.2 LTS |
April 2027 |
December 2027 |
+ April 2030 |
- 6.0 |
- December 2025 |
- August 2026 |
- April 2027 |
+ 7.0 |
+ December 2027 |
+ August 2028 |
+ April 2029 |
diff --git a/djangoproject/templates/releases/roadmap.html b/djangoproject/templates/releases/roadmap.html
new file mode 100644
index 000000000..7892c9ff8
--- /dev/null
+++ b/djangoproject/templates/releases/roadmap.html
@@ -0,0 +1,120 @@
+{% extends "base.html" %}
+{% load date_format %}
+
+{% block sectionid %}roadmap{% endblock %}
+{% block title %}Django {{ series }} Roadmap{% endblock %}
+{% block layout_class %}sidebar-right{% endblock %}
+
+{% block header %}
+ Download
+{% endblock %}
+
+{% block content %}
+ Django {{ series }} Roadmap
+ This document details the schedule and roadmap towards Django {{ series }}.
+
+
+ What features will be in Django {{ series }}?
+ Whatever gets committed by the alpha feature freeze!
+ Django {{ series }} will be a time-based release. Any features completed and committed
+ to main by the alpha feature freeze deadline noted below will be included. Any
+ that miss the deadline won't.
+ If you have a major feature you'd like to contribute, please introduce yourself
+ on the django-internals forum
+ so you can find a shepherd for your feature.
+ Minor features and bug fixes will be merged as they are completed. If you
+ have submitted a patch, ensure the flags on the Trac ticket are correct so it
+ appears in the "Patches needing review" filter of the
+ Django Development Dashboard.
+ Better yet, find someone to review your patch and mark the ticket as
+ "Ready for checkin". Tickets marked "Ready for checkin" are regularly reviewed
+ by mergers.
+
+
+
+ Schedule
+
+ Major milestones along the way to {{ series }} are scheduled below.
+ See Process for more details. Dates are subject to change.
+
+
+
+
+
+ Date |
+ Milestone |
+
+
+
+
+ {{ releases.a.date|default:"TBD" }} |
+ Django {{ series }} alpha; feature freeze. |
+
+
+ {{ releases.b.date|default:"TBD" }} |
+ Django {{ series }} beta; non-release blocking bug fix freeze. |
+
+
+ {{ releases.c.date|default:"TBD" }} |
+ Django {{ series }} RC 1; translation string freeze. |
+
+
+ {{ releases.f.date|default:"TBD" }} |
+ Django {{ series }} final. |
+
+
+
+
+
+
+
+ Process
+ Any features not completed by the feature freeze date won't make it into {{ series }}.
+ The release manager will keep the schedule updated and ensure efficient
+ routing of issues and reminders for deadlines.
+
+
+ Feature freeze / Alpha 1
+ All major and minor features must be merged by the Alpha 1 deadline. Any
+ features not done by this point will be deferred or dropped. At this time, we
+ will fork stable/{{ series }}.x
from main
.
+ After the alpha, non-release blocking bug fixes may be backported at the
+ mergers' discretion.
+
+
+
+ Beta 1
+ Beta 1 marks the end of changes that aren't release blocking bugs. Only release
+ blocking bug fixes will be allowed to be backported after the beta.
+
+
+
+ RC 1
+ If release blockers are still coming in at the planned release candidate date,
+ we'll release beta 2 to encourage further testing. RC 1 marks the freeze for
+ translation strings; translators will have two weeks to submit updates. Release
+ blocking bug fixes may continue to be backported.
+
+
+
+ Final
+ Django {{ series }} final will ideally ship two weeks after the last RC. If no major bugs
+ are found by then, {{ series }} final will be issued; otherwise, the timeline will be
+ adjusted as needed.
+
+
+
+ How you can help
+ Community effort is key. You can help by:
+
+
+
+
+{% endblock %}
diff --git a/releases/tests.py b/releases/tests.py
index f838eb435..c48116350 100644
--- a/releases/tests.py
+++ b/releases/tests.py
@@ -4,6 +4,7 @@
from django.contrib import admin
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
+from django.template.defaultfilters import date as datefilter
from django.test import SimpleTestCase, TestCase, override_settings
from django.urls import reverse
from django.utils.safestring import SafeString
@@ -567,3 +568,89 @@ def test_no_diamond_and_platinum_members(self):
self.assertNotContains(response, member.display_name)
self.assertNotContains(response, member.url)
self.assertNotContains(response, member.description)
+
+
+class RoadmapViewTestCase(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ # Define release schedule for 5.2, 6.0, and 6.1 series.
+ cls.release_schedule = {
+ "5.2": [
+ ("a1", datetime.date(2025, 1, 15)),
+ ("b1", datetime.date(2025, 2, 19)),
+ ("rc1", datetime.date(2025, 3, 19)),
+ ("", datetime.date(2025, 4, 2)), # final
+ ],
+ "6.0": [
+ ("a1", datetime.date(2025, 9, 17)),
+ ("b1", datetime.date(2025, 10, 22)),
+ ("rc1", datetime.date(2025, 11, 19)),
+ ("", datetime.date(2025, 12, 3)), # final
+ ],
+ "6.1": [
+ ("a1", datetime.date(2026, 5, 20)),
+ ("b1", datetime.date(2026, 6, 24)),
+ ("rc1", datetime.date(2026, 7, 22)),
+ ("", datetime.date(2026, 8, 5)), # final
+ ],
+ }
+ for series, milestones in cls.release_schedule.items():
+ for milestone, date in milestones:
+ version = f"{series}{milestone}" if milestone else series
+ Release.objects.create(
+ version=version,
+ is_active=True,
+ date=date,
+ is_lts=series.endswith(".2"),
+ )
+
+ def test_roadmap_page_renders_series_title(self):
+ for series in self.release_schedule.keys():
+ url = reverse("roadmap", kwargs={"series": series})
+ response = self.client.get(url)
+ self.assertContains(response, f"Django {series} Roadmap", html=True)
+
+ def test_roadmap_page_contains_milestones(self):
+ for series, releases in self.release_schedule.items():
+ with self.subTest(series=series):
+ url = reverse("roadmap", kwargs={"series": series})
+ response = self.client.get(url)
+ for detail, date in [
+ (f"Django {series} alpha; feature freeze.", releases[0][1]),
+ (
+ f"Django {series} beta; non-release blocking bug fix freeze.",
+ releases[1][1],
+ ),
+ (
+ f"Django {series} RC 1; translation string freeze.",
+ releases[2][1],
+ ),
+ (f"Django {series} final.", releases[3][1]),
+ ]:
+ expected = f"{datefilter(date)} | {detail} |
"
+ self.assertContains(response, expected, html=True)
+
+ def test_series_non_digits(self):
+ for series in (0, "", "a.b", "2.2.0"):
+ with self.subTest(series=series):
+ response = self.client.get(f"/download/{series}/roadmap/")
+ self.assertEqual(response.status_code, 404)
+
+ def test_major_lower_bound(self):
+ for minor in (0, 1, 2, 3, 11):
+ with self.subTest(minor=minor):
+ response = self.client.get(f"/download/1.{minor}/roadmap/")
+ self.assertEqual(response.status_code, 404)
+
+ def test_links_to_contributing_and_release_process_present(self):
+ url = reverse("roadmap", kwargs={"series": "20.0"})
+ response = self.client.get(url)
+ self.assertContains(
+ response,
+ 'href="http://docs.djangoproject.com/en/dev/internals/contributing/"',
+ )
+ self.assertContains(
+ response,
+ 'href="http://docs.djangoproject.com/en/dev/internals/release-process/"',
+ )
diff --git a/releases/urls.py b/releases/urls.py
index 3e1feb598..96f26b088 100644
--- a/releases/urls.py
+++ b/releases/urls.py
@@ -1,10 +1,11 @@
from django.urls import path, re_path
-from .views import index, redirect
+from .views import index, redirect, roadmap
urlpatterns = [
path("", index, name="download"),
re_path(
"^([0-9a-z_.-]+)/(tarball|wheel|checksum)/$", redirect, name="download-redirect"
),
+ re_path(r"^(?P\d{1,2}\.[0-2])/roadmap/$", roadmap, name="roadmap"),
]
diff --git a/releases/views.py b/releases/views.py
index 291e49e6c..e894ea3f7 100644
--- a/releases/views.py
+++ b/releases/views.py
@@ -31,6 +31,20 @@ def index(request):
return render(request, "releases/download.html", context)
+def roadmap(request, series):
+ major, minor = series.split(".")
+ major = int(major)
+ if major < 2:
+ raise Http404
+
+ releases = Release.objects.filter(major=major, minor=minor, micro=0)
+ context = {
+ "series": series,
+ "releases": {r.status: r for r in releases},
+ }
+ return render(request, "releases/roadmap.html", context)
+
+
def redirect(request, version, kind):
release = get_object_or_404(Release, version=version)