diff --git a/docs/user/api/v3.rst b/docs/user/api/v3.rst index 4239c2681cb..3d2887102cf 100644 --- a/docs/user/api/v3.rst +++ b/docs/user/api/v3.rst @@ -468,6 +468,39 @@ Project update :statuscode 204: Updated successfully +Project versions sync ++++++++++++++++++++++ + +.. http:post:: /api/v3/projects/(string:project_slug)/sync-versions/ + + Trigger a background task to sync the versions of the project. + + **Example request**: + + .. tabs:: + + .. code-tab:: bash + + $ curl \ + -X POST \ + -H "Authorization: Token " \ + https://readthedocs.org/api/v3/projects/pip/sync-versions/ + + .. code-tab:: python + + import requests + URL = 'https://readthedocs.org/api/v3/projects/pip/sync-versions/' + TOKEN = '' + HEADERS = {'Authorization': f'token {TOKEN}'} + response = requests.post( + URL, + headers=HEADERS, + ) + print(response.json()) + + :statuscode 202: Task created successfully + :statuscode 400: Bad request, task not created + Versions ~~~~~~~~ diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index 6efd5c2290d..3dbcdfe6fcb 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -499,6 +499,7 @@ class ProjectLinksSerializer(BaseLinksSerializer): superproject = serializers.SerializerMethodField() translations = serializers.SerializerMethodField() notifications = serializers.SerializerMethodField() + sync_versions = serializers.SerializerMethodField() def get__self(self, obj): path = reverse("projects-detail", kwargs={"project_slug": obj.slug}) @@ -558,6 +559,15 @@ def get_superproject(self, obj): ) return self._absolute_url(path) + def get_sync_versions(self, obj): + path = reverse( + "projects-sync-versions", + kwargs={ + "project_slug": obj.slug, + }, + ) + return self._absolute_url(path) + def get_translations(self, obj): path = reverse( "projects-translations-list", diff --git a/readthedocs/api/v3/tests/responses/projects-detail.json b/readthedocs/api/v3/tests/responses/projects-detail.json index ad114a4b691..a27c32ac04a 100644 --- a/readthedocs/api/v3/tests/responses/projects-detail.json +++ b/readthedocs/api/v3/tests/responses/projects-detail.json @@ -76,6 +76,7 @@ "redirects": "https://readthedocs.org/api/v3/projects/project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/project/superproject/", + "sync_versions": "https://readthedocs.org/api/v3/projects/project/sync-versions/", "translations": "https://readthedocs.org/api/v3/projects/project/translations/", "versions": "https://readthedocs.org/api/v3/projects/project/versions/" }, diff --git a/readthedocs/api/v3/tests/responses/projects-list.json b/readthedocs/api/v3/tests/responses/projects-list.json index 14f5986f2cf..df7345d31ec 100644 --- a/readthedocs/api/v3/tests/responses/projects-list.json +++ b/readthedocs/api/v3/tests/responses/projects-list.json @@ -52,6 +52,7 @@ "redirects": "https://readthedocs.org/api/v3/projects/project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/project/superproject/", + "sync_versions": "https://readthedocs.org/api/v3/projects/project/sync-versions/", "translations": "https://readthedocs.org/api/v3/projects/project/translations/" }, "privacy_level": "public", diff --git a/readthedocs/api/v3/tests/responses/projects-list_POST.json b/readthedocs/api/v3/tests/responses/projects-list_POST.json index 168380f622b..a15f5edc60b 100644 --- a/readthedocs/api/v3/tests/responses/projects-list_POST.json +++ b/readthedocs/api/v3/tests/responses/projects-list_POST.json @@ -7,6 +7,7 @@ "redirects": "https://readthedocs.org/api/v3/projects/test-project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/test-project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/test-project/superproject/", + "sync_versions": "https://readthedocs.org/api/v3/projects/test-project/sync-versions/", "translations": "https://readthedocs.org/api/v3/projects/test-project/translations/", "versions": "https://readthedocs.org/api/v3/projects/test-project/versions/" }, diff --git a/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json b/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json index 8e2cb546dee..22fae242ad4 100644 --- a/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json +++ b/readthedocs/api/v3/tests/responses/projects-subprojects-detail.json @@ -13,6 +13,7 @@ "redirects": "https://readthedocs.org/api/v3/projects/subproject/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/subproject/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/subproject/superproject/", + "sync_versions": "https://readthedocs.org/api/v3/projects/subproject/sync-versions/", "translations": "https://readthedocs.org/api/v3/projects/subproject/translations/", "versions": "https://readthedocs.org/api/v3/projects/subproject/versions/" }, diff --git a/readthedocs/api/v3/tests/responses/projects-subprojects-list.json b/readthedocs/api/v3/tests/responses/projects-subprojects-list.json index d6ca5e3f47c..3b510e14755 100644 --- a/readthedocs/api/v3/tests/responses/projects-subprojects-list.json +++ b/readthedocs/api/v3/tests/responses/projects-subprojects-list.json @@ -18,6 +18,7 @@ "redirects": "https://readthedocs.org/api/v3/projects/subproject/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/subproject/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/subproject/superproject/", + "sync_versions": "https://readthedocs.org/api/v3/projects/subproject/sync-versions/", "translations": "https://readthedocs.org/api/v3/projects/subproject/translations/", "versions": "https://readthedocs.org/api/v3/projects/subproject/versions/" }, diff --git a/readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json b/readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json index 7579eeb04df..947e867a6a1 100644 --- a/readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json +++ b/readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json @@ -13,6 +13,7 @@ "redirects": "https://readthedocs.org/api/v3/projects/new-project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/new-project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/new-project/superproject/", + "sync_versions": "https://readthedocs.org/api/v3/projects/new-project/sync-versions/", "translations": "https://readthedocs.org/api/v3/projects/new-project/translations/", "versions": "https://readthedocs.org/api/v3/projects/new-project/versions/" }, diff --git a/readthedocs/api/v3/tests/responses/projects-superproject.json b/readthedocs/api/v3/tests/responses/projects-superproject.json index 0d6e20a1540..4bd0f798443 100644 --- a/readthedocs/api/v3/tests/responses/projects-superproject.json +++ b/readthedocs/api/v3/tests/responses/projects-superproject.json @@ -15,6 +15,7 @@ "redirects": "https://readthedocs.org/api/v3/projects/project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/project/superproject/", + "sync_versions": "https://readthedocs.org/api/v3/projects/project/sync-versions/", "translations": "https://readthedocs.org/api/v3/projects/project/translations/", "versions": "https://readthedocs.org/api/v3/projects/project/versions/" }, diff --git a/readthedocs/api/v3/tests/responses/projects-sync-versions.json b/readthedocs/api/v3/tests/responses/projects-sync-versions.json new file mode 100644 index 00000000000..e99aa88be6d --- /dev/null +++ b/readthedocs/api/v3/tests/responses/projects-sync-versions.json @@ -0,0 +1 @@ +{"triggered": true} diff --git a/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json b/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json index d434936ee6c..7876ab1e1ee 100644 --- a/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json +++ b/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json @@ -42,6 +42,7 @@ "notifications": "https://readthedocs.org/api/v3/projects/project/notifications/", "subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/project/superproject/", + "sync_versions": "https://readthedocs.org/api/v3/projects/project/sync-versions/", "translations": "https://readthedocs.org/api/v3/projects/project/translations/", "versions": "https://readthedocs.org/api/v3/projects/project/versions/" }, diff --git a/readthedocs/api/v3/tests/responses/remoterepositories-list.json b/readthedocs/api/v3/tests/responses/remoterepositories-list.json index 8ddfbab20e3..063f143a5b5 100644 --- a/readthedocs/api/v3/tests/responses/remoterepositories-list.json +++ b/readthedocs/api/v3/tests/responses/remoterepositories-list.json @@ -25,6 +25,7 @@ "redirects": "https://readthedocs.org/api/v3/projects/project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/project/superproject/", + "sync_versions": "https://readthedocs.org/api/v3/projects/project/sync-versions/", "translations": "https://readthedocs.org/api/v3/projects/project/translations/", "versions": "https://readthedocs.org/api/v3/projects/project/versions/" }, diff --git a/readthedocs/api/v3/tests/test_projects.py b/readthedocs/api/v3/tests/test_projects.py index b8cd5c04279..1ae012cfc92 100644 --- a/readthedocs/api/v3/tests/test_projects.py +++ b/readthedocs/api/v3/tests/test_projects.py @@ -203,6 +203,37 @@ def test_projects_superproject(self): self._get_response_dict("projects-superproject"), ) + def test_projects_sync_versions(self): + # Ensure a default version exists to sync + self.project.update_latest_version() + + url = reverse( + "projects-sync-versions", + kwargs={ + "project_slug": self.project.slug, + }, + ) + + self.client.logout() + response = self.client.get(url) + self.assertEqual(response.status_code, 401) + response = self.client.post(url) + self.assertEqual(response.status_code, 401) + + # Test with a user that is not the owner + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.others_token.key}") + response = self.client.post(url) + self.assertEqual(response.status_code, 403) + + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") + response = self.client.post(url) + self.assertEqual(response.status_code, 202) + + self.assertDictEqual( + response.json(), + self._get_response_dict("projects-sync-versions"), + ) + def test_others_projects_builds_list(self): self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") response = self.client.get( diff --git a/readthedocs/api/v3/urls.py b/readthedocs/api/v3/urls.py index c722eb7b725..5ff97f8742d 100644 --- a/readthedocs/api/v3/urls.py +++ b/readthedocs/api/v3/urls.py @@ -24,6 +24,7 @@ # allows /api/v3/projects/ # allows /api/v3/projects/pip/ # allows /api/v3/projects/pip/superproject/ +# allows /api/v3/projects/pip/sync-versions/ projects = router.register( r"projects", ProjectsViewSet, diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 3aa13905929..c0b88fdc861 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -28,6 +28,7 @@ from readthedocs.builds.models import Build, Version 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.notifications.models import Notification from readthedocs.oauth.models import ( RemoteOrganization, @@ -161,6 +162,9 @@ def get_serializer_class(self): if self.action in ("update", "partial_update"): return ProjectUpdateSerializer + # Default serializer so that sync_versions works with the BrowseableAPI + return ProjectSerializer + def get_queryset(self): # Allow hitting ``/api/v3/projects/`` to list their own projects if self.basename == "projects" and self.action == "list": @@ -218,6 +222,27 @@ def superproject(self, request, project_slug): except Exception: return Response(status=status.HTTP_404_NOT_FOUND) + @action(detail=True, methods=["post"], url_path="sync-versions") + def sync_versions(self, request, project_slug): + """ + Kick off a task to sync versions for a project. + + POST to this endpoint to trigger a task that syncs versions for the project. + + This will be used in a button in the frontend, + but also can be used to trigger a sync from the API. + """ + project = self.get_object() + triggered = trigger_sync_versions(project) + data = {} + if triggered: + data.update({"triggered": True}) + code = status.HTTP_202_ACCEPTED + else: + data.update({"triggered": False}) + code = status.HTTP_400_BAD_REQUEST + return Response(data=data, status=code) + class ProjectsViewSet(SettingsOverrideObject): _default_class = ProjectsViewSetBase