Skip to content

Commit

Permalink
Addons: accept project-slug and version-slug on endpoint
Browse files Browse the repository at this point in the history
`/_/addons/` API endpoint now accepts `project-slug` and `version-slug`.
This is useful to be able to cache these requests in our CDN.

It still supports sending `url=` when necessary for those requests that relies
on the full URL to generate specific data in the response.

The NGINX config is updated to inject `meta` tags with project/version slugs,
emulating what our CF worker does on production.

Closes #10536
  • Loading branch information
humitos committed Oct 16, 2023
1 parent 59299d1 commit 11e3a76
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 31 deletions.
5 changes: 3 additions & 2 deletions dockerfiles/nginx/proxito.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ server {
set $rtd_force_addons $upstream_http_x_rtd_force_addons;
add_header X-RTD-Force-Addons $rtd_force_addons always;

# Inject our own script dynamically
# Inject our own script dynamically and project/version slugs into the HTML to emulate what CF worker does
# TODO: find a way to make this work _without_ running `npm run dev` from the `addons` repository
sub_filter '</head>' '<script language="javascript" src="http://localhost:8000/readthedocs-addons.js"></script>\n</head>';
sub_filter '</head>' '<script async language="javascript" src="http://localhost:8000/readthedocs-addons.js"></script>\n<meta name="readthedocs-project-slug" content="$rtd_project" />\n<meta name="readthedocs-version-slug" content="latest" />\n</head>';
sub_filter_types text/html;
sub_filter_last_modified on;
sub_filter_once on;
}
Expand Down
85 changes: 56 additions & 29 deletions readthedocs/proxito/views/hosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.http import Http404, JsonResponse
from django.shortcuts import get_object_or_404
from rest_framework.renderers import JSONRenderer
from rest_framework.views import APIView

Expand All @@ -21,7 +22,7 @@
from readthedocs.core.resolver import resolver
from readthedocs.core.unresolver import UnresolverError, unresolver
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.projects.models import Feature
from readthedocs.projects.models import Feature, Project

log = structlog.get_logger(__name__) # noqa

Expand All @@ -46,10 +47,19 @@ class BaseReadTheDocsConfigJson(CDNCacheTagsMixin, APIView):
Attributes:
url (required): absolute URL from where the request is performed
(e.g. ``window.location.href``)
api-version (required): API JSON structure version (e.g. ``0``, ``1``, ``2``).
project-slug (required): slug of the project.
Optional if "url" is sent.
version-slug (required): slug of the version.
Optional if "url" is sent.
url (optional): absolute URL from where the request is performed.
When sending "url" attribute, "project-slug" and "version-slug" are ignored.
(e.g. ``window.location.href``).
client-version (optional): JavaScript client version (e.g. ``0.6.0``).
"""

http_method_names = ["get"]
Expand All @@ -60,7 +70,10 @@ class BaseReadTheDocsConfigJson(CDNCacheTagsMixin, APIView):
@lru_cache(maxsize=1)
def _resolve_resources(self):
url = self.request.GET.get("url")
if not url:
project_slug = self.request.GET.get("project-slug")
version_slug = self.request.GET.get("version-slug")

if not project_slug or not version_slug:
# TODO: not sure what to return here when it fails on the `has_permission`
return None, None, None, None

Expand All @@ -69,28 +82,37 @@ def _resolve_resources(self):
# Main project from the domain.
project = unresolved_domain.project

try:
unresolved_url = unresolver.unresolve_url(url)
# Project from the URL: if it's a subproject it will differ from
# the main project got from the domain.
project = unresolved_url.project
version = unresolved_url.version
filename = unresolved_url.filename
if url:
try:
unresolved_url = unresolver.unresolve_url(url)
# Project from the URL: if it's a subproject it will differ from
# the main project got from the domain.
project = unresolved_url.project
version = unresolved_url.version
filename = unresolved_url.filename
build = version.builds.last()

except UnresolverError as exc:
# If an exception is raised and there is a ``project`` in the
# exception, it's a partial match. This could be because of an
# invalid URL path, but on a valid project domain. In this case, we
# continue with the ``project``, but without a ``version``.
# Otherwise, we return 404 NOT FOUND.
project = getattr(exc, "project", None)
if not project:
raise Http404() from exc

version = None
filename = None
build = None
else:
project = get_object_or_404(
Project.objects.for_user(user=self.request.user),
slug=project_slug,
)
version = get_object_or_404(Version, slug=version_slug, project=project)
build = version.builds.last()

except UnresolverError as exc:
# If an exception is raised and there is a ``project`` in the
# exception, it's a partial match. This could be because of an
# invalid URL path, but on a valid project domain. In this case, we
# continue with the ``project``, but without a ``version``.
# Otherwise, we return 404 NOT FOUND.
project = getattr(exc, "project", None)
if not project:
raise Http404() from exc

version = None
filename = None
build = None

return project, version, build, filename

Expand All @@ -104,11 +126,16 @@ def _get_version(self):

def get(self, request, format=None):
url = request.GET.get("url")
project_slug = request.GET.get("project_slug")
version_slug = request.GET.get("version_slug")
if not url:
return JsonResponse(
{"error": "'url' GET attribute is required"},
status=400,
)
if not project_slug or not version_slug:
return JsonResponse(
{
"error": "'project-slug' and 'version-slug' GET attributes are required when not sending 'url'"
},
status=400,
)

addons_version = request.GET.get("api-version")
if not addons_version:
Expand Down

0 comments on commit 11e3a76

Please sign in to comment.