Date: Fri, 19 Sep 2025 18:21:10 -0500
Subject: [PATCH 14/17] Use sans-serif font everywhere
This change was requested for simplicity and accessibility.
---
djangoproject/scss/_style.scss | 3 +--
djangoproject/scss/_utils.scss | 4 ----
djangoproject/templates/styleguide.html | 2 +-
3 files changed, 2 insertions(+), 7 deletions(-)
diff --git a/djangoproject/scss/_style.scss b/djangoproject/scss/_style.scss
index b504c850b..3a6eb69d0 100644
--- a/djangoproject/scss/_style.scss
+++ b/djangoproject/scss/_style.scss
@@ -13,7 +13,7 @@ CSS rendered with Libsass 0.7.0
//------------------- Globals
body {
- @include serif;
+ @include sans-serif;
@include font-size(18);
background: var(--sidebar-bg);
color: var(--body-fg);
@@ -636,7 +636,6 @@ header {
}
p {
- @include serif;
@include font-size(18);
color: var(--white-color);
left: -9999px;
diff --git a/djangoproject/scss/_utils.scss b/djangoproject/scss/_utils.scss
index 2896cbf24..8277cdfa9 100644
--- a/djangoproject/scss/_utils.scss
+++ b/djangoproject/scss/_utils.scss
@@ -114,10 +114,6 @@ $logo-bg-dark: #272c27;
}
// Font Family Mixins
-@mixin serif {
- font-family: Palatino, "Palatino Linotype", "Book Antiqua", "Hoefler Text", Georgia, "Lucida Bright", Cambria, Times, "Times New Roman", serif;
-}
-
@mixin sans-serif {
font-family: "Roboto", Corbel, Avenir, "Lucida Grande", "Lucida Sans", sans-serif;
}
diff --git a/djangoproject/templates/styleguide.html b/djangoproject/templates/styleguide.html
index eac7540bd..7c7eb018b 100644
--- a/djangoproject/templates/styleguide.html
+++ b/djangoproject/templates/styleguide.html
@@ -104,7 +104,7 @@ Secondary colors
Typography
- The djangoproject.com website is set in both serif and sans-serif fonts. The serif font is Palatino, which is widely available as a system font. The sans-serif font is Roboto, a web font that is loaded via Google Fonts. 5 styles of this typeface are loaded, Light (300), Normal (400), Normal Italic, Bold (700) and Bold Italic. Refrain from using weights and italics that are not loaded.
+ The djangoproject.com website is set in Roboto, a sans-serif font. 5 styles of this typeface are provided, Light (300), Normal (400), Normal Italic, Bold (700) and Bold Italic. Refrain from using weights and italics that are not loaded.
From 103c2635d8794eb6a0b8e5cab3a1fd42b41d03f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=9Clgen=20Sar=C4=B1kavak?=
Date: Thu, 2 Oct 2025 16:11:59 +0300
Subject: [PATCH 15/17] Update Django version to 5.2.7
https://docs.djangoproject.com/en/5.2/releases/5.2.7/
---
requirements/common.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/common.txt b/requirements/common.txt
index 1904cfa7c..7488cd775 100644
--- a/requirements/common.txt
+++ b/requirements/common.txt
@@ -7,7 +7,7 @@ django-push @ git+https://github.com/brutasse/django-push.git@22fda99641cfbd2f30
django-read-only==1.21.0
django-recaptcha==4.1.0
django-registration-redux==2.13
-Django==5.2.6
+Django==5.2.7
docutils==0.21.2
feedparser==6.0.12
Jinja2==3.1.6
From 366a7ebeaa09216a211418b4884f2cc4fc738d98 Mon Sep 17 00:00:00 2001
From: Alice Wong <146962159+alarthast@users.noreply.github.com>
Date: Thu, 2 Oct 2025 16:01:29 +0100
Subject: [PATCH 16/17] Added `Cache-Control` header to never cache draft blog
posts (#2223)
Fixes #2158.
Added `Cache-Control` header to never cache draft blog posts (since we
want changes to be reflected immediately).
Django's cache middleware will not cache responses when the
Cache-Control header is set to no-cache, no-store, or private, which is
our case here for draft blog posts.
Co-authored-by: Tobias McNulty
---
blog/tests.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++
blog/views.py | 7 +++++
2 files changed, 80 insertions(+)
diff --git a/blog/tests.py b/blog/tests.py
index cb484c6ee..b5c1b1802 100644
--- a/blog/tests.py
+++ b/blog/tests.py
@@ -3,10 +3,12 @@
from io import StringIO
import time_machine
+from django.conf import settings
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import ContentFile
from django.test import TestCase
+from django.test.utils import override_settings
from django.urls import reverse
from django.utils import timezone, translation
@@ -407,6 +409,77 @@ def test_user_cannot_see_unpublished_entries(self):
self.assertEqual(response.status_code, 200)
+@override_settings(
+ # Caching middleware is added in the production settings file;
+ # simulate that here for the tests.
+ MIDDLEWARE=(
+ ["django.middleware.cache.UpdateCacheMiddleware"]
+ + settings.MIDDLEWARE
+ + ["django.middleware.cache.FetchFromCacheMiddleware"]
+ ),
+)
+class ViewsCachingTestCase(DateTimeMixin, TestCase):
+ def test_drafts_have_no_cache_headers(self):
+ """
+ Draft (unpublished) entries have no-cache headers.
+ """
+ user = User.objects.create(username="staff", is_staff=True)
+ content_type = ContentType.objects.get_for_model(Entry)
+ change_permission = Permission.objects.get(
+ content_type=content_type, codename="change_entry"
+ )
+ user.user_permissions.add(change_permission)
+ self.client.force_login(user)
+
+ unpublished_entry = Entry.objects.create(
+ pub_date=self.tomorrow,
+ is_active=True,
+ headline="unpublished",
+ slug="unpublished",
+ )
+ unpublished_url = reverse(
+ "weblog:entry",
+ kwargs={
+ "year": unpublished_entry.pub_date.year,
+ "month": unpublished_entry.pub_date.strftime("%b").lower(),
+ "day": unpublished_entry.pub_date.day,
+ "slug": unpublished_entry.slug,
+ },
+ )
+
+ response = self.client.get(unpublished_url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Cache-Control", response.headers)
+ self.assertEqual(
+ response.headers["Cache-Control"],
+ "max-age=0, no-cache, no-store, must-revalidate, private",
+ )
+
+ def test_published_blogs_have_cache_control_headers(self):
+ """
+ Published blog posts has Cache-Control header.
+ """
+ entry = Entry.objects.create(
+ pub_date=self.yesterday,
+ is_active=True,
+ headline="published",
+ slug="published",
+ )
+ url = reverse(
+ "weblog:entry",
+ kwargs={
+ "year": entry.pub_date.year,
+ "month": entry.pub_date.strftime("%b").lower(),
+ "day": entry.pub_date.day,
+ "slug": entry.slug,
+ },
+ )
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.headers["Cache-Control"], "max-age=300")
+
+
class SitemapTests(DateTimeMixin, TestCase):
def test_sitemap(self):
entry = Entry.objects.create(
diff --git a/blog/views.py b/blog/views.py
index dc1012ac7..ac51f7c1f 100644
--- a/blog/views.py
+++ b/blog/views.py
@@ -1,3 +1,4 @@
+from django.utils.cache import add_never_cache_headers
from django.views.generic.dates import (
ArchiveIndexView,
DateDetailView,
@@ -58,3 +59,9 @@ def get_queryset(self):
return Entry.objects.all()
else:
return super().get_queryset()
+
+ def get(self, request, *args, **kwargs):
+ response = super().get(request, *args, **kwargs)
+ if not self.object.is_published():
+ add_never_cache_headers(response)
+ return response
From 7a888e92f5428d5c5c01251727d3bff08d416998 Mon Sep 17 00:00:00 2001
From: Natalia <124304+nessita@users.noreply.github.com>
Date: Thu, 18 Sep 2025 22:07:37 -0300
Subject: [PATCH 17/17] Automatically generate Django series roadmaps from
Release data
This branch adds a new view that replaces the Roadmap wiki pages, e.g.:
* https://code.djangoproject.com/wiki/Version5.2Roadmap
* https://code.djangoproject.com/wiki/Version6.0Roadmap
* https://code.djangoproject.com/wiki/Version6.1Roadmap
The wiki pages were almost identical except for version-specific details
and target dates. This information is now generated dynamically from the
Release model in the database.
The supported and future Roadmap tables were reorganized so they follow
a more intuitive timeline (from older to newer versions).
---
djangoproject/scss/_style.scss | 62 +++++----
.../templates/releases/download.html | 40 +++---
djangoproject/templates/releases/roadmap.html | 120 ++++++++++++++++++
releases/tests.py | 87 +++++++++++++
releases/urls.py | 3 +-
releases/views.py | 14 ++
6 files changed, 281 insertions(+), 45 deletions(-)
create mode 100644 djangoproject/templates/releases/roadmap.html
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)