Skip to content

Commit

Permalink
Added code references to search results.
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahboyce committed Feb 24, 2025
1 parent 0070473 commit 2257959
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 36 deletions.
32 changes: 32 additions & 0 deletions djangoproject/scss/_style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2635,6 +2635,38 @@ table.docutils th {
color: var(--search-mark-text);
}
}

.code-links {
margin-top: 15px;
margin-left: 10px;

a {
&:active,
&:focus,
&:hover {
code {
color: var(--primary);
}
.meta {
color: var(--text-light);
}
}
}

code {
color: var(--primary-accent);
font-weight: 700;
}

div {
margin: 10px 0;

.meta {
margin: 5px 0 0;
color: var(--body-fg);
}
}
}
}

.list-links-small {
Expand Down
39 changes: 39 additions & 0 deletions docs/builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from functools import cached_property

from sphinxcontrib.serializinghtml import JSONHTMLBuilder


class JSONTOCHTMLBuilder(JSONHTMLBuilder):
name = "jsontoc"

@cached_property
def domain_objects(self):
domain = self.env.get_domain("py")
return [item for item in domain.get_objects() if item[2] != "module"]

def get_doc_context(self, docname, body, metatags):
out_dict = super().get_doc_context(docname, body, metatags)
python_objects = self.get_python_objects(docname)
out_dict["python_objects"] = python_objects
out_dict["python_objects_search"] = " ".join(
# Keeps the code suffix to improve the search results for terms such as
# "select" for QuerySet.select_related.
[key.split(".")[-1] for key in python_objects.keys()]
)
return out_dict

def get_python_objects(self, docname):
entries = {}
for name, _, _, obj_docname, _, _ in self.domain_objects:
if obj_docname == docname:
code_path = name.split(".")[-2:]
if code_path[0][0].isupper():
short_name = ".".join(code_path)
else:
short_name = code_path[-1]
entries[short_name] = name
return entries


def setup(app):
app.add_builder(JSONTOCHTMLBuilder)
43 changes: 24 additions & 19 deletions docs/management/commands/update_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from django.conf import settings
from django.core.management import BaseCommand, call_command
from django.utils.translation import to_locale
from sphinx.application import Sphinx

from ...models import DocumentRelease

Expand Down Expand Up @@ -55,7 +55,7 @@ def handle(self, **kwargs):
self.update_index = kwargs["update_index"]
self.purge_cache = kwargs["purge_cache"]

self.default_builders = ["json", "djangohtml"]
self.default_builders = ["jsontoc", "djangohtml"]
default_docs_version = DocumentRelease.objects.get(
is_default=True
).release.version
Expand Down Expand Up @@ -172,28 +172,33 @@ def build_doc_release(self, release, force=False):
if build_dir.exists():
shutil.rmtree(str(build_dir))
build_dir.mkdir(parents=True)
app = Sphinx(
srcdir=str(source_dir),
confdir=str(source_dir),
outdir=str(build_dir),
doctreedir=str(build_dir / ".doctrees"),
buildername=builder,
status=sys.stdout,
verbosity=1,
confoverrides={
"extensions": [
"djangodocs",
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
"sphinx.ext.autosectionlabel",
"sphinx.ext.linkcode",
"docs.builder",
]
},
)

if self.verbosity >= 2:
self.stdout.write(f" building {builder} ({source_dir} -> {build_dir})")
try:
# Translated docs builds generate a lot of warnings, so send
# stderr to stdout to be logged (rather than generating an
# email)
subprocess.check_call(
[
"sphinx-build",
"-b",
builder,
"-D",
"language=%s" % to_locale(release.lang),
"-j",
"auto",
"-Q" if self.verbosity == 0 else "-q",
str(source_dir), # Source file directory
str(build_dir), # Destination directory
],
stderr=sys.stdout,
)
app.build()
except subprocess.CalledProcessError:
self.stderr.write(
"sphinx-build returned an error (release %s, builder %s)"
Expand Down Expand Up @@ -252,8 +257,8 @@ def zipfile_inclusion_filter(file_path):
if self.verbosity >= 2:
self.stdout.write(" reindexing...")

json_built_dir = parent_build_dir.joinpath("_built", "json")
documents = gen_decoded_documents(json_built_dir)
jsontoc_built_dir = parent_build_dir.joinpath("_built", "jsontoc")
documents = gen_decoded_documents(jsontoc_built_dir)
release.sync_to_db(documents)

def update_git(self, url, destdir, changed_dir="."):
Expand Down
25 changes: 14 additions & 11 deletions docs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import html
import json
import operator
from functools import reduce
from functools import partial, reduce
from pathlib import Path

from django.conf import settings
Expand Down Expand Up @@ -252,6 +252,12 @@ def search(self, query_text, release):
query_text, config=models.F("config"), search_type="websearch"
)
search_rank = SearchRank(models.F("search"), search_query)
search = partial(
SearchHeadline,
start_sel=START_SEL,
stop_sel=STOP_SEL,
config=models.F("config"),
)
base_qs = (
self.prefetch_related(
Prefetch(
Expand All @@ -264,21 +270,18 @@ def search(self, query_text, release):
)
.filter(release_id=release.id)
.annotate(
headline=SearchHeadline(
"title",
headline=search("title", search_query),
highlight=search(
KeyTextTransform("body", "metadata"),
search_query,
start_sel=START_SEL,
stop_sel=STOP_SEL,
config=models.F("config"),
),
highlight=SearchHeadline(
KeyTextTransform("body", "metadata"),
searched_python_objects=search(
KeyTextTransform("python_objects_search", "metadata"),
search_query,
start_sel=START_SEL,
stop_sel=STOP_SEL,
config=models.F("config"),
highlight_all=True,
),
breadcrumbs=models.F("metadata__breadcrumbs"),
python_objects=models.F("metadata__python_objects"),
)
.only(
"path",
Expand Down
13 changes: 13 additions & 0 deletions docs/templates/docs/search_results.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ <h2 class="result-title">
{% if result.highlight %}
…&nbsp;{{ result.highlight|cut:"¶"|safe }}&nbsp;…
{% endif %}
{% code_links result.searched_python_objects result.python_objects as result_code_links %}
{% if result_code_links %}
<div class="code-links">
{% for name, value in result_code_links.items %}
<a href="{% url 'document-detail' lang=result.release.lang version=result.release.version url=result.path host 'docs' %}#{{ value.full_path }}">
<div>
<code>{{ name }}</code>
{% if value.module_path %}<div class="meta">{{ value.module_path }}</div>{% endif %}
</div>
</a>
{% endfor %}
</div>
{% endif %}
</dd>
{% endfor %}
</dl>
Expand Down
28 changes: 27 additions & 1 deletion docs/templatetags/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from ..forms import DocSearchForm
from ..models import DocumentRelease
from ..search import START_SEL, STOP_SEL
from ..utils import get_doc_path, get_doc_root
from ..utils import get_doc_path, get_doc_root, get_module_path

register = template.Library()

Expand Down Expand Up @@ -121,3 +121,29 @@ def generate_scroll_to_text_fragment(highlighted_text):
# Due to Python code such as timezone.now(), remove the space after a bracket.
single_spaced = re.sub(r"([(\[])\s", r"\1", single_spaced)
return f"#:~:text={quote(single_spaced)}"


@register.simple_tag(name="code_links")
def code_links(searched_python_objects, python_objects):
if not searched_python_objects or START_SEL not in searched_python_objects:
return {}
python_objects_matched_short_names = [
word.replace(START_SEL, "").replace(STOP_SEL, "")
for word in searched_python_objects.split(" ")
if START_SEL in word
]
matched_reference = {}
# Map "select_related" to "QuerySet.select_related" in code_references.
reference_map = {key.split(".")[-1]: key for key in python_objects.keys()}
for short_name in python_objects_matched_short_names:
if full_path := python_objects.get(short_name):
matched_reference[short_name] = {
"full_path": full_path,
"module_path": get_module_path(short_name, full_path),
}
elif name := reference_map.get(short_name):
matched_reference[name] = {
"full_path": python_objects[name],
"module_path": get_module_path(name, python_objects[name]),
}
return dict(sorted(matched_reference.items()))
Loading

0 comments on commit 2257959

Please sign in to comment.