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

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
DateMilestone
{{ 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)