import datetime 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 ) context = { "doc": load_json_file(doc_path), "env": load_json_file(docroot / "globalcontext.json"), "lang": lang, "version": version, "canonical_version": canonical_version, "canonical": canonical, "available_languages": available_languages, "release": release, "rtd_version": rtd_version, "docurl": url, "update_date": datetime.datetime.fromtimestamp( (docroot / "last_build").stat().st_mtime ), "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", )