Skip to content

Commit ad13571

Browse files
committed
Use the database for docs rendering
1 parent c7c0a47 commit ad13571

7 files changed

Lines changed: 205 additions & 23 deletions

File tree

docs/management/commands/update_docs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@ def zipfile_inclusion_filter(file_path):
278278

279279
json_built_dir = parent_build_dir / "_built" / "json"
280280
documents = gen_decoded_documents(json_built_dir)
281+
with open(json_built_dir / "globalcontext.json") as context:
282+
release.global_context = json.load(context)
283+
release.save(update_fields=["global_context"])
281284
release.sync_to_db(documents)
282285

283286
def update_git(self, url, destdir, changed_dir="."):
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Generated by Django 6.0.4 on 2026-05-06 09:20
2+
3+
import json
4+
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
8+
9+
def set_documentrelease_global_context(apps, schema_editor):
10+
DocumentRelease = apps.get_model("docs", "DocumentRelease")
11+
for doc_release in DocumentRelease.objects.filter(release__isnull=False):
12+
if not doc_release.global_context:
13+
try:
14+
with open(
15+
settings.DOCS_BUILD_ROOT
16+
/ doc_release.lang
17+
/ doc_release.release.version
18+
/ "_built"
19+
/ "json"
20+
/ "globalcontext.json"
21+
) as context:
22+
doc_release.global_context = json.load(context)
23+
except FileNotFoundError:
24+
pass
25+
else:
26+
doc_release.save(update_fields=["global_context"])
27+
28+
29+
class Migration(migrations.Migration):
30+
31+
dependencies = [
32+
("docs", "0007_add_docs_search_vector"),
33+
]
34+
35+
operations = [
36+
migrations.AddField(
37+
model_name="documentrelease",
38+
name="global_context",
39+
field=models.JSONField(default=dict),
40+
),
41+
migrations.RunPython(
42+
set_documentrelease_global_context, migrations.RunPython.noop, elidable=True
43+
),
44+
]

docs/models.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import datetime
22
import html
3-
import json
43
import logging
54
import operator
65
from functools import partial, reduce
@@ -111,6 +110,7 @@ class DocumentRelease(models.Model):
111110
limit_choices_to={"status": "f"},
112111
on_delete=models.CASCADE,
113112
)
113+
global_context = models.JSONField(default=dict)
114114
is_default = models.BooleanField(default=False)
115115

116116
objects = DocumentReleaseQuerySet.as_manager()
@@ -469,7 +469,4 @@ def full_path(self):
469469

470470
@cached_property
471471
def body(self):
472-
"""The document's body"""
473-
with self.full_path.open() as fp:
474-
doc = json.load(fp)
475-
return doc["body"]
472+
return self.metadata["body"]

docs/templates/docs/doc.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,11 @@ <h3 id="breadcrumbs-header">{% translate "You are here:" %}</h3>
166166
<ul>
167167
<li>
168168
<a href="{% url 'document-index' lang=lang version=version host 'docs' %}">{% blocktranslate %}Django {{ version }} documentation{% endblocktranslate %}</a>
169-
{% for p in doc.parents %}
170-
<ul><li><a href="{{ p.link }}">{{ p.title|safe }}</a>
169+
{% for p in doc.breadcrumbs %}
170+
<ul><li><a href="{% url 'document-detail' lang=lang version=version url=p.path host 'docs' %}">{{ p.title|safe }}</a>
171171
{% endfor %}
172172
<ul><li>{% block current-page-title %}{{ doc.title|safe }}{% endblock %}</li></ul>
173-
{% for p in doc.parents %}</li></ul>{% endfor %}
173+
{% for p in doc.breadcrumbs %}</li></ul>{% endfor %}
174174
</li>
175175
</ul>
176176
</nav>

docs/tests/test_templates.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,12 @@ def _assertOGTitleEqual(self, doc, expected):
214214

215215
def test_opengraph_title(self):
216216
doc = Document.objects.create(
217+
metadata={"body": "test body"},
217218
release=DocumentRelease.objects.create(
218219
lang=settings.DEFAULT_LANGUAGE_CODE,
219220
release=Release.objects.create(version="5.0"),
220221
),
221222
)
222-
doc.body = "test body" # avoids trying to load the underlying physical file
223223

224224
for title, expected in [
225225
("test title", "test title"),
@@ -231,6 +231,35 @@ def test_opengraph_title(self):
231231
with self.subTest(title=title):
232232
self._assertOGTitleEqual(doc, f"{expected} | Django documentation")
233233

234+
def test_breadcrumbs_render(self):
235+
doc = Document.objects.create(
236+
metadata={
237+
"breadcrumbs": [{"path": "releases", "title": "Release notes"}],
238+
"parents": "parent",
239+
},
240+
release=DocumentRelease.objects.create(
241+
lang=settings.DEFAULT_LANGUAGE_CODE,
242+
release=Release.objects.create(version="5.0"),
243+
),
244+
)
245+
output = render_to_string(
246+
"docs/doc.html",
247+
{
248+
"doc": doc.metadata,
249+
"lang": settings.DEFAULT_LANGUAGE_CODE,
250+
"version": "5.0",
251+
},
252+
request=RequestFactory().get("/"),
253+
)
254+
self.assertInHTML(
255+
"""
256+
<a href="http://docs.djangoproject.localhost:8000/en/5.0/releases/">
257+
Release notes
258+
</a>
259+
""",
260+
output,
261+
)
262+
234263

235264
class TemplateTagTestCase(TestCase):
236265
def test_search_form_renders_without_request_in_template(self):

docs/tests/test_views.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
import json
2+
import os
3+
import shutil
4+
import tempfile
15
from http import HTTPStatus
6+
from pathlib import Path
27

38
from django.conf import settings
49
from django.contrib.sites.models import Site
@@ -335,3 +340,102 @@ def test_security_txt(self):
335340
self.assertEqual(response.status_code, HTTPStatus.OK)
336341
self.assertEqual(response["Content-Type"], "text/plain")
337342
self.assertIn("Expires:", response.content.decode())
343+
344+
345+
class DocumentViewTestCase(TestCase):
346+
def test_document_renders_from_database(self):
347+
doc = Document.objects.create(
348+
metadata={
349+
"body": "document rendered from database",
350+
},
351+
path="document",
352+
release=DocumentRelease.objects.create(
353+
is_default=True,
354+
lang=settings.DEFAULT_LANGUAGE_CODE,
355+
release=Release.objects.create(version="5.0"),
356+
),
357+
)
358+
response = self.client.get(
359+
"/en/5.0/document/",
360+
headers={"host": "docs.djangoproject.localhost:8000"},
361+
)
362+
self.assertEqual(response.status_code, HTTPStatus.OK)
363+
self.assertContains(response, "document rendered from database")
364+
365+
def test_document_renders_from_filesystem(self):
366+
DocumentRelease.objects.create(
367+
is_default=True,
368+
lang=settings.DEFAULT_LANGUAGE_CODE,
369+
release=Release.objects.create(version="5.0"),
370+
)
371+
tmp_docs_build_root = Path(tempfile.mkdtemp())
372+
self.addCleanup(shutil.rmtree, tmp_docs_build_root)
373+
os.makedirs(
374+
tmp_docs_build_root
375+
/ settings.DEFAULT_LANGUAGE_CODE
376+
/ "5.0"
377+
/ "_built"
378+
/ "json"
379+
)
380+
with open(
381+
tmp_docs_build_root
382+
/ settings.DEFAULT_LANGUAGE_CODE
383+
/ "5.0"
384+
/ "_built"
385+
/ "json"
386+
/ "filesystem.fjson",
387+
"w",
388+
) as json_file:
389+
json_file.write(json.dumps({"body": "document rendered from filesystem"}))
390+
with self.settings(DOCS_BUILD_ROOT=tmp_docs_build_root):
391+
response = self.client.get(
392+
"/en/5.0/filesystem/",
393+
headers={"host": "docs.djangoproject.localhost:8000"},
394+
)
395+
self.assertEqual(response.status_code, HTTPStatus.OK)
396+
self.assertContains(response, "document rendered from filesystem")
397+
398+
def test_filesystem_rendering_template(self):
399+
DocumentRelease.objects.create(
400+
is_default=True,
401+
lang=settings.DEFAULT_LANGUAGE_CODE,
402+
release=Release.objects.create(version="5.0"),
403+
)
404+
tmp_docs_build_root = Path(tempfile.mkdtemp())
405+
self.addCleanup(shutil.rmtree, tmp_docs_build_root)
406+
os.makedirs(
407+
tmp_docs_build_root
408+
/ settings.DEFAULT_LANGUAGE_CODE
409+
/ "5.0"
410+
/ "_built"
411+
/ "json"
412+
)
413+
with open(
414+
tmp_docs_build_root
415+
/ settings.DEFAULT_LANGUAGE_CODE
416+
/ "5.0"
417+
/ "_built"
418+
/ "json"
419+
/ "genindex.fjson",
420+
"w",
421+
) as json_file:
422+
json_file.write("{}")
423+
with self.settings(DOCS_BUILD_ROOT=tmp_docs_build_root):
424+
response = self.client.get(
425+
"/en/5.0/genindex/",
426+
headers={"host": "docs.djangoproject.localhost:8000"},
427+
)
428+
self.assertEqual(response.status_code, HTTPStatus.OK)
429+
self.assertTemplateUsed(response, "docs/genindex.html")
430+
431+
def test_nonexistent_document(self):
432+
DocumentRelease.objects.create(
433+
is_default=True,
434+
lang=settings.DEFAULT_LANGUAGE_CODE,
435+
release=Release.objects.create(version="5.0"),
436+
)
437+
response = self.client.get(
438+
"/en/5.0/nonexistent/",
439+
headers={"host": "docs.djangoproject.localhost:8000"},
440+
)
441+
self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND)

docs/views.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import datetime
21
import json
32

43
from django.conf import settings
@@ -53,35 +52,44 @@ def document(request, lang, version, url):
5352
if version == "stable":
5453
version = canonical_version
5554

56-
docroot = get_doc_root_or_404(lang, version)
57-
doc_path = get_doc_path_or_404(docroot, url)
5855
try:
5956
release = DocumentRelease.objects.get_by_version_and_lang(version, lang)
6057
except DocumentRelease.DoesNotExist:
6158
raise Http404
6259

60+
try:
61+
document = Document.objects.get(release=release, path__in=[url, f"{url}index"])
62+
doc_name = document.path
63+
metadata = document.metadata
64+
except Document.DoesNotExist:
65+
# We won't find e.g. the genindex page nor partially
66+
# translated documents in the database.
67+
docroot = get_doc_root_or_404(lang, version)
68+
doc_path = get_doc_path_or_404(docroot, url)
69+
doc_name = str(doc_path.relative_to(docroot)).replace(str(doc_path.suffix), "")
70+
try:
71+
with doc_path.open("r") as f:
72+
metadata = json.load(f)
73+
except FileNotFoundError:
74+
raise Http404
75+
6376
if version == "dev":
6477
rtd_version = "latest"
6578
else:
6679
rtd_version = version + ".x"
6780

6881
template_names = [
69-
"docs/%s.html"
70-
% str(doc_path.relative_to(docroot)).replace(str(doc_path.suffix), ""),
82+
f"docs/{doc_name}.html",
7183
"docs/doc.html",
7284
]
7385

74-
def load_json_file(path):
75-
with path.open("r") as f:
76-
return json.load(f)
77-
7886
available_languages = DocumentRelease.objects.get_available_languages_by_version(
7987
version
8088
)
8189

8290
context = {
83-
"doc": load_json_file(doc_path),
84-
"env": load_json_file(docroot / "globalcontext.json"),
91+
"doc": metadata,
92+
"env": release.global_context,
8593
"lang": lang,
8694
"version": version,
8795
"canonical_version": canonical_version,
@@ -90,9 +98,6 @@ def load_json_file(path):
9098
"release": release,
9199
"rtd_version": rtd_version,
92100
"docurl": url,
93-
"update_date": datetime.datetime.fromtimestamp(
94-
(docroot / "last_build").stat().st_mtime
95-
),
96101
"redirect_from": request.GET.get("from", None),
97102
}
98103
response = render(request, template_names, context)

0 commit comments

Comments
 (0)