Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

APIv3: proxy these URLs to be served from El Proxito /_/api/v3/ #11831

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions readthedocs/api/v3/proxied_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
from readthedocs.api.v3.proxied_views import ProxiedEmbedAPI
from readthedocs.search.api.v3.views import ProxiedSearchAPI

from .urls import router

api_proxied_urls = [
path("embed/", ProxiedEmbedAPI.as_view(), name="embed_api_v3"),
path("search/", ProxiedSearchAPI.as_view(), name="search_api_v3"),
]

urlpatterns = api_proxied_urls
urlpatterns += router.urls
12 changes: 12 additions & 0 deletions readthedocs/api/v3/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
BuildsCreateViewSet,
BuildsViewSet,
EnvironmentVariablesViewSet,
FileTreeDiffViewSet,
NotificationsBuildViewSet,
NotificationsForUserViewSet,
NotificationsOrganizationViewSet,
Expand Down Expand Up @@ -77,6 +78,17 @@
],
)

# allows /api/v3/projects/pip/versions/v3.6.2/filetreediff/
versions.register(
r"filetreediff",
FileTreeDiffViewSet,
basename="projects-versions-filetreediff",
parents_query_lookups=[
"project__slug",
"version__slug",
],
)

# allows /api/v3/projects/pip/builds/
# allows /api/v3/projects/pip/builds/1053/
builds = projects.register(
Expand Down
125 changes: 123 additions & 2 deletions readthedocs/api/v3/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import BrowsableAPIRenderer
from rest_framework.response import Response
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from rest_framework_extensions.mixins import NestedViewSetMixin

from readthedocs.api.v2.permissions import ReadOnlyPermission
from readthedocs.builds.constants import EXTERNAL
from readthedocs.builds.models import Build, Version
from readthedocs.core.resolver import Resolver
from readthedocs.core.utils import trigger_build
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.core.views.hooks import trigger_sync_versions
from readthedocs.filetreediff import get_diff
from readthedocs.notifications.models import Notification
from readthedocs.oauth.models import (
RemoteOrganization,
Expand Down Expand Up @@ -110,7 +111,7 @@ class APIv3Settings:
LimitOffsetPagination.default_limit = 10

renderer_classes = (AlphabeticalSortedJSONRenderer, BrowsableAPIRenderer)
throttle_classes = (UserRateThrottle, AnonRateThrottle)
# throttle_classes = (UserRateThrottle, AnonRateThrottle)
filter_backends = (filters.DjangoFilterBackend,)
metadata_class = SimpleMetadata

Expand Down Expand Up @@ -382,6 +383,126 @@ def get_queryset(self):
return super().get_queryset().exclude(type=EXTERNAL)


class FileTreeDiffViewSet(
APIv3Settings,
NestedViewSetMixin,
ProjectQuerySetMixin,
FlexFieldsMixin,
ReadOnlyModelViewSet,
):
model = Version
lookup_field = "slug"
lookup_url_kwarg = "version_slug"

# Allow ``.`` (dots) on version slug
lookup_value_regex = r"[^/]+"

permission_classes = [ReadOnlyPermission | (IsAuthenticated & IsProjectAdmin)]

def _get_filetreediff_response(self, *, request, project, version, resolver):
"""
Get the file tree diff response for the given version.

This response is only enabled for external versions,
we do the comparison between the current version and the latest version.
"""
if not version.is_external:
return None

if not project.addons.filetreediff_enabled:
return None

base_version = (
project.addons.options_base_version or project.get_latest_version()
)
# TODO: check if `self._has_permission` is important after the migration here.
# if not base_version or not self._has_permission(
# request=request, version=base_version
# ):
if not base_version:
return None

diff = get_diff(version_a=version, version_b=base_version)
if not diff:
return None

return {
"outdated": diff.outdated,
"diff": {
"added": [
{
"filename": filename,
"urls": {
"current": resolver.resolve_version(
project=project,
filename=filename,
version=version,
),
"base": resolver.resolve_version(
project=project,
filename=filename,
version=base_version,
),
},
}
for filename in diff.added
],
"deleted": [
{
"filename": filename,
"urls": {
"current": resolver.resolve_version(
project=project,
filename=filename,
version=version,
),
"base": resolver.resolve_version(
project=project,
filename=filename,
version=base_version,
),
},
}
for filename in diff.deleted
],
"modified": [
{
"filename": filename,
"urls": {
"current": resolver.resolve_version(
project=project,
filename=filename,
version=version,
),
"base": resolver.resolve_version(
project=project,
filename=filename,
version=base_version,
),
},
}
for filename in diff.modified
],
},
}

def list(self, request, **kwargs):
project = self._get_parent_project()
version = self._get_parent_version()
resolver = Resolver()

data = (
self._get_filetreediff_response(
request=request,
project=project,
version=version,
resolver=resolver,
)
or {}
)
return Response(data=data)


class BuildsViewSet(
APIv3Settings,
NestedViewSetMixin,
Expand Down
128 changes: 1 addition & 127 deletions readthedocs/proxito/views/hosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from readthedocs.core.resolver import Resolver
from readthedocs.core.unresolver import UnresolverError, unresolver
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.filetreediff import get_diff
from readthedocs.projects.constants import (
ADDONS_FLYOUT_SORTING_CALVER,
ADDONS_FLYOUT_SORTING_CUSTOM_PATTERN,
Expand Down Expand Up @@ -406,36 +405,6 @@ def _v1(self, project, version, build, filename, url, request):

data = {
"api_version": "1",
"projects": {
"current": ProjectSerializerNoLinks(
project,
resolver=resolver,
version_slug=version.slug if version else None,
).data,
"translations": ProjectSerializerNoLinks(
project_translations,
resolver=resolver,
version_slug=version.slug if version else None,
many=True,
).data,
},
"versions": {
"current": VersionSerializerNoLinks(
version,
resolver=resolver,
).data
if version
else None,
# These are "sorted active, built, not hidden versions"
"active": VersionSerializerNoLinks(
sorted_versions_active_built_not_hidden,
resolver=resolver,
many=True,
).data,
},
"builds": {
"current": BuildSerializerNoLinks(build).data if build else None,
},
# TODO: consider creating one serializer per field here.
# The resulting JSON will be the same, but maybe it's easier/cleaner?
"domains": {
Expand Down Expand Up @@ -529,21 +498,12 @@ def _v1(self, project, version, build, filename, url, request):
},
},
"filetreediff": {
"enabled": False,
"enabled": project.addons.filetreediff_enabled,
},
},
}

if version:
response = self._get_filetreediff_response(
request=request,
project=project,
version=version,
resolver=resolver,
)
if response:
data["addons"]["filetreediff"].update(response)

# Show the subprojects filter on the parent project and subproject
# TODO: Remove these queries and try to find a way to get this data
# from the resolver, which has already done these queries.
Expand Down Expand Up @@ -621,92 +581,6 @@ def _v1(self, project, version, build, filename, url, request):

return data

def _get_filetreediff_response(self, *, request, project, version, resolver):
"""
Get the file tree diff response for the given version.

This response is only enabled for external versions,
we do the comparison between the current version and the latest version.
"""
if not version.is_external:
return None

if not project.addons.filetreediff_enabled:
return None

base_version = (
project.addons.options_base_version or project.get_latest_version()
)
if not base_version or not self._has_permission(
request=request, version=base_version
):
return None

diff = get_diff(version_a=version, version_b=base_version)
if not diff:
return None

return {
"enabled": True,
"outdated": diff.outdated,
"diff": {
"added": [
{
"filename": filename,
"urls": {
"current": resolver.resolve_version(
project=project,
filename=filename,
version=version,
),
"base": resolver.resolve_version(
project=project,
filename=filename,
version=base_version,
),
},
}
for filename in diff.added
],
"deleted": [
{
"filename": filename,
"urls": {
"current": resolver.resolve_version(
project=project,
filename=filename,
version=version,
),
"base": resolver.resolve_version(
project=project,
filename=filename,
version=base_version,
),
},
}
for filename in diff.deleted
],
"modified": [
{
"filename": filename,
"urls": {
"current": resolver.resolve_version(
project=project,
filename=filename,
version=version,
),
"base": resolver.resolve_version(
project=project,
filename=filename,
version=base_version,
),
},
}
for filename in diff.modified
],
},
}

def _v2(self, project, version, build, filename, url, user):
return {
"api_version": "2",
Expand Down