summaryrefslogtreecommitdiff
path: root/docs/views.py
blob: b5a7a39ee7a28d26ecea776b6f7dbaec35a4dca9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
import json

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.sitemaps.views import x_robots_tag
from django.contrib.sites.models import Site
from django.core.paginator import InvalidPage, Paginator
from django.http import Http404, JsonResponse
from django.shortcuts import redirect, render
from django.template.response import TemplateResponse
from django.utils.translation import activate, gettext_lazy as _
from django.views.decorators.cache import cache_page
from django_hosts.resolvers import reverse

from .forms import DocSearchForm
from .models import Document, DocumentRelease
from .search import START_SEL, DocumentationCategory
from .utils import get_doc_path_or_404, get_doc_root_or_404

SIMPLE_SEARCH_OPERATORS = ["+", "|", "-", '"', "*", "(", ")", "~"]


def index(request):
    return redirect(DocumentRelease.objects.current())


def language(request, lang):
    return redirect(DocumentRelease.objects.current(lang))


def stable(request, lang, version, url):
    path = request.get_full_path()
    current_version = DocumentRelease.objects.current_version()
    return redirect(path.replace(version, current_version, 1))


def document(request, lang, version, url):
    # If either of these can't be encoded as ascii then later on down the line an
    # exception will be emitted by unipath, proactively check for bad data (mostly
    # from the Googlebot) so we can give a nice 404 error.
    try:
        lang.encode("ascii")
        version.encode("ascii")
        url.encode("ascii")
    except UnicodeEncodeError:
        raise Http404

    activate(lang)

    canonical_version = DocumentRelease.objects.current_version()
    canonical = version == canonical_version
    if version == "stable":
        version = canonical_version

    docroot = get_doc_root_or_404(lang, version)
    doc_path = get_doc_path_or_404(docroot, url)
    try:
        release = DocumentRelease.objects.get_by_version_and_lang(version, lang)
    except DocumentRelease.DoesNotExist:
        raise Http404

    if version == "dev":
        rtd_version = "latest"
    else:
        rtd_version = version + ".x"

    template_names = [
        "docs/%s.html"
        % str(doc_path.relative_to(docroot)).replace(str(doc_path.suffix), ""),
        "docs/doc.html",
    ]

    def load_json_file(path):
        with path.open("r") as f:
            return json.load(f)

    available_languages = DocumentRelease.objects.get_available_languages_by_version(
        version
    )

    try:
        doc = Document.objects.get(
            release=release, path__in=[url, f"{url}index"]
        ).metadata
    except Document.DoesNotExist:
        # We won't find e.g. the genindex page nor partially
        # translated documents in the database.
        doc = load_json_file(doc_path)

    context = {
        "doc": doc,
        "env": release.global_context,
        "lang": lang,
        "version": version,
        "canonical_version": canonical_version,
        "canonical": canonical,
        "available_languages": available_languages,
        "release": release,
        "rtd_version": rtd_version,
        "docurl": url,
        "redirect_from": request.GET.get("from", None),
    }
    response = render(request, template_names, context)
    # Tell Fastly to re-fetch from the origin once a week
    # (we'll invalidate the cache sooner if needed)
    response["Surrogate-Control"] = "max-age=%d" % (7 * 24 * 60 * 60)
    return response


if not settings.DEBUG:
    # Specify a dedicated cache for docs pages that need to be purged after
    # docs rebuilds (see docs/management/commands/update_docs.py):
    document = cache_page(settings.CACHE_MIDDLEWARE_SECONDS, cache="docs-pages")(
        document
    )


def redirect_index(request, *args, **kwargs):
    assert request.path.endswith("index/")
    return redirect(request.path[:-6])


def redirect_search(request):
    """
    Legacy search view to handle old queries correctly, e.g. in scraping
    sites, command line interface etc.
    """
    release = DocumentRelease.objects.current()
    kwargs = {
        "lang": release.lang,
        "version": release.version,
    }
    search_url = reverse("document-search", host="docs", kwargs=kwargs)
    q = request.GET.get("q") or None
    if q:
        search_url += "?q=%s" % q
    return redirect(search_url)


def search_results(request, lang, version, per_page=10, orphans=3):
    """
    Search view to handle language and version specific queries.
    The old search view is being redirected here.
    """
    try:
        release = DocumentRelease.objects.get_by_version_and_lang(version, lang)
    except DocumentRelease.DoesNotExist:
        raise Http404

    activate(lang)

    doc_category = DocumentationCategory.parse(request.GET.get("category"))
    form = DocSearchForm(request.GET or None, release=release)

    # Get available languages for the language switcher
    available_languages = DocumentRelease.objects.get_available_languages_by_version(
        version
    )

    context = {
        "form": form,
        "lang": release.lang,
        "version": release.version,
        "release": release,
        "available_languages": available_languages,
        "searchparams": request.GET.urlencode(),
        "active_category": doc_category or "",
    }

    if form.is_valid():
        q = form.cleaned_data.get("q")

        if q:
            # catch queries that are coming from browser search bars
            exact = Document.objects.filter(release=release, title=q).first()
            if exact is not None:
                return redirect(exact)

            results = Document.objects.search(
                q, release, document_category=doc_category
            )

            page_number = request.GET.get("page") or 1
            paginator = Paginator(results, per_page=per_page, orphans=orphans)

            try:
                page_number = int(page_number)
            except ValueError:
                if page_number == "last":
                    page_number = paginator.num_pages
                else:
                    raise Http404(
                        _("Page is not 'last', " "nor can it be converted to an int.")
                    )

            try:
                page = paginator.page(page_number)
            except InvalidPage as e:
                raise Http404(
                    _("Invalid page (%(page_number)s): %(message)s")
                    % {"page_number": page_number, "message": str(e)}
                )

            context.update(
                {
                    "query": q,
                    "page": page,
                    "paginator": paginator,
                    "start_sel": START_SEL,
                    "DocumentationCategory": DocumentationCategory,
                }
            )

    return render(request, "docs/search_results.html", context)


def search_suggestions(request, lang, version, per_page=20):
    """
    The endpoint for the OpenSearch browser integration.

    This will do a simple prefix match against the title to catch
    documents with a meaningful title.

    The link list contains redirect URLs so that IE will correctly
    redirect to those documents.
    """
    try:
        release = DocumentRelease.objects.get_by_version_and_lang(version, lang)
    except DocumentRelease.DoesNotExist:
        raise Http404

    activate(lang)

    form = DocSearchForm(request.GET or None, release=release)
    suggestions = []

    if form.is_valid():
        q = form.cleaned_data.get("q")
        if q:
            results = (
                Document.objects.filter(
                    release__lang=release.lang,
                )
                .filter(
                    release__release__version=release.version,
                )
                .filter(
                    title__contains=q,
                )
            )
            suggestions.append(q)
            titles = []
            links = []
            content_type = ContentType.objects.get_for_model(Document)
            for result in results:
                titles.append(result.title)
                kwargs = {
                    "content_type_id": content_type.pk,
                    "object_id": result.id,
                }
                links.append(reverse("contenttypes-shortcut", kwargs=kwargs))
            suggestions.append(titles)
            suggestions.append([])
            suggestions.append(links)

    return JsonResponse(suggestions, safe=False)


if not settings.DEBUG:
    # 1 hour to handle the many requests
    search_suggestions = cache_page(60 * 60)(search_suggestions)


def search_description(request, lang, version):
    """
    Render an OpenSearch description.
    """
    try:
        release = DocumentRelease.objects.get_by_version_and_lang(version, lang)
    except DocumentRelease.DoesNotExist:
        raise Http404

    activate(lang)

    context = {
        "site": Site.objects.get_current(),
        "release": release,
    }
    return render(
        request,
        "docs/search_description.xml",
        context,
        content_type="application/opensearchdescription+xml",
    )


if not settings.DEBUG:
    # 1 week because there is no need to render it more often
    search_description = cache_page(60 * 60 * 24 * 7)(search_description)


@x_robots_tag
def sitemap_index(request, sitemaps):
    """
    Simplified version of django.contrib.sitemaps.views.index that uses
    django_hosts for URL reversing.
    """
    sites = []
    for section in sitemaps.keys():
        sitemap_url = reverse(
            "document-sitemap", host="docs", kwargs={"section": section}
        )
        sites.append({"location": sitemap_url})
    return TemplateResponse(
        request,
        "sitemap_index.xml",
        {"sitemaps": sites},
        content_type="application/xml",
    )