Skip to content

Commit 6528d5d

Browse files
ericholscherstsewd
andauthored
Add an initial resync_versions API to v3 (#11484)
* Add an initial resync_versions API to v3 This will be used in the frontend, but also available as an API. Mostly curious if this is a good approach, and I can get some tests together for it. Refs #6090 * Add test * Fix test * Add default serializer * Add to serializer * Update readthedocs/api/v3/views.py Co-authored-by: Santos Gallegos <[email protected]> * Update sync_versions endpoint * Fix tests * Add docs * Add missing test --------- Co-authored-by: Santos Gallegos <[email protected]>
1 parent d281701 commit 6528d5d

15 files changed

+110
-0
lines changed

docs/user/api/v3.rst

+33
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,39 @@ Project update
468468
:statuscode 204: Updated successfully
469469

470470

471+
Project versions sync
472+
+++++++++++++++++++++
473+
474+
.. http:post:: /api/v3/projects/(string:project_slug)/sync-versions/
475+
476+
Trigger a background task to sync the versions of the project.
477+
478+
**Example request**:
479+
480+
.. tabs::
481+
482+
.. code-tab:: bash
483+
484+
$ curl \
485+
-X POST \
486+
-H "Authorization: Token <token>" \
487+
https://readthedocs.org/api/v3/projects/pip/sync-versions/
488+
489+
.. code-tab:: python
490+
491+
import requests
492+
URL = 'https://readthedocs.org/api/v3/projects/pip/sync-versions/'
493+
TOKEN = '<token>'
494+
HEADERS = {'Authorization': f'token {TOKEN}'}
495+
response = requests.post(
496+
URL,
497+
headers=HEADERS,
498+
)
499+
print(response.json())
500+
501+
:statuscode 202: Task created successfully
502+
:statuscode 400: Bad request, task not created
503+
471504
Versions
472505
~~~~~~~~
473506

readthedocs/api/v3/serializers.py

+10
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ class ProjectLinksSerializer(BaseLinksSerializer):
499499
superproject = serializers.SerializerMethodField()
500500
translations = serializers.SerializerMethodField()
501501
notifications = serializers.SerializerMethodField()
502+
sync_versions = serializers.SerializerMethodField()
502503

503504
def get__self(self, obj):
504505
path = reverse("projects-detail", kwargs={"project_slug": obj.slug})
@@ -558,6 +559,15 @@ def get_superproject(self, obj):
558559
)
559560
return self._absolute_url(path)
560561

562+
def get_sync_versions(self, obj):
563+
path = reverse(
564+
"projects-sync-versions",
565+
kwargs={
566+
"project_slug": obj.slug,
567+
},
568+
)
569+
return self._absolute_url(path)
570+
561571
def get_translations(self, obj):
562572
path = reverse(
563573
"projects-translations-list",

readthedocs/api/v3/tests/responses/projects-detail.json

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"redirects": "https://readthedocs.org/api/v3/projects/project/redirects/",
7777
"subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/",
7878
"superproject": "https://readthedocs.org/api/v3/projects/project/superproject/",
79+
"sync_versions": "https://readthedocs.org/api/v3/projects/project/sync-versions/",
7980
"translations": "https://readthedocs.org/api/v3/projects/project/translations/",
8081
"versions": "https://readthedocs.org/api/v3/projects/project/versions/"
8182
},

readthedocs/api/v3/tests/responses/projects-list.json

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"redirects": "https://readthedocs.org/api/v3/projects/project/redirects/",
5353
"subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/",
5454
"superproject": "https://readthedocs.org/api/v3/projects/project/superproject/",
55+
"sync_versions": "https://readthedocs.org/api/v3/projects/project/sync-versions/",
5556
"translations": "https://readthedocs.org/api/v3/projects/project/translations/"
5657
},
5758
"privacy_level": "public",

readthedocs/api/v3/tests/responses/projects-list_POST.json

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"redirects": "https://readthedocs.org/api/v3/projects/test-project/redirects/",
88
"subprojects": "https://readthedocs.org/api/v3/projects/test-project/subprojects/",
99
"superproject": "https://readthedocs.org/api/v3/projects/test-project/superproject/",
10+
"sync_versions": "https://readthedocs.org/api/v3/projects/test-project/sync-versions/",
1011
"translations": "https://readthedocs.org/api/v3/projects/test-project/translations/",
1112
"versions": "https://readthedocs.org/api/v3/projects/test-project/versions/"
1213
},

readthedocs/api/v3/tests/responses/projects-subprojects-detail.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"redirects": "https://readthedocs.org/api/v3/projects/subproject/redirects/",
1414
"subprojects": "https://readthedocs.org/api/v3/projects/subproject/subprojects/",
1515
"superproject": "https://readthedocs.org/api/v3/projects/subproject/superproject/",
16+
"sync_versions": "https://readthedocs.org/api/v3/projects/subproject/sync-versions/",
1617
"translations": "https://readthedocs.org/api/v3/projects/subproject/translations/",
1718
"versions": "https://readthedocs.org/api/v3/projects/subproject/versions/"
1819
},

readthedocs/api/v3/tests/responses/projects-subprojects-list.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"redirects": "https://readthedocs.org/api/v3/projects/subproject/redirects/",
1919
"subprojects": "https://readthedocs.org/api/v3/projects/subproject/subprojects/",
2020
"superproject": "https://readthedocs.org/api/v3/projects/subproject/superproject/",
21+
"sync_versions": "https://readthedocs.org/api/v3/projects/subproject/sync-versions/",
2122
"translations": "https://readthedocs.org/api/v3/projects/subproject/translations/",
2223
"versions": "https://readthedocs.org/api/v3/projects/subproject/versions/"
2324
},

readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"redirects": "https://readthedocs.org/api/v3/projects/new-project/redirects/",
1414
"subprojects": "https://readthedocs.org/api/v3/projects/new-project/subprojects/",
1515
"superproject": "https://readthedocs.org/api/v3/projects/new-project/superproject/",
16+
"sync_versions": "https://readthedocs.org/api/v3/projects/new-project/sync-versions/",
1617
"translations": "https://readthedocs.org/api/v3/projects/new-project/translations/",
1718
"versions": "https://readthedocs.org/api/v3/projects/new-project/versions/"
1819
},

readthedocs/api/v3/tests/responses/projects-superproject.json

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"redirects": "https://readthedocs.org/api/v3/projects/project/redirects/",
1616
"subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/",
1717
"superproject": "https://readthedocs.org/api/v3/projects/project/superproject/",
18+
"sync_versions": "https://readthedocs.org/api/v3/projects/project/sync-versions/",
1819
"translations": "https://readthedocs.org/api/v3/projects/project/translations/",
1920
"versions": "https://readthedocs.org/api/v3/projects/project/versions/"
2021
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"triggered": true}

readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"notifications": "https://readthedocs.org/api/v3/projects/project/notifications/",
4343
"subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/",
4444
"superproject": "https://readthedocs.org/api/v3/projects/project/superproject/",
45+
"sync_versions": "https://readthedocs.org/api/v3/projects/project/sync-versions/",
4546
"translations": "https://readthedocs.org/api/v3/projects/project/translations/",
4647
"versions": "https://readthedocs.org/api/v3/projects/project/versions/"
4748
},

readthedocs/api/v3/tests/responses/remoterepositories-list.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"redirects": "https://readthedocs.org/api/v3/projects/project/redirects/",
2626
"subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/",
2727
"superproject": "https://readthedocs.org/api/v3/projects/project/superproject/",
28+
"sync_versions": "https://readthedocs.org/api/v3/projects/project/sync-versions/",
2829
"translations": "https://readthedocs.org/api/v3/projects/project/translations/",
2930
"versions": "https://readthedocs.org/api/v3/projects/project/versions/"
3031
},

readthedocs/api/v3/tests/test_projects.py

+31
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,37 @@ def test_projects_superproject(self):
203203
self._get_response_dict("projects-superproject"),
204204
)
205205

206+
def test_projects_sync_versions(self):
207+
# Ensure a default version exists to sync
208+
self.project.update_latest_version()
209+
210+
url = reverse(
211+
"projects-sync-versions",
212+
kwargs={
213+
"project_slug": self.project.slug,
214+
},
215+
)
216+
217+
self.client.logout()
218+
response = self.client.get(url)
219+
self.assertEqual(response.status_code, 401)
220+
response = self.client.post(url)
221+
self.assertEqual(response.status_code, 401)
222+
223+
# Test with a user that is not the owner
224+
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.others_token.key}")
225+
response = self.client.post(url)
226+
self.assertEqual(response.status_code, 403)
227+
228+
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")
229+
response = self.client.post(url)
230+
self.assertEqual(response.status_code, 202)
231+
232+
self.assertDictEqual(
233+
response.json(),
234+
self._get_response_dict("projects-sync-versions"),
235+
)
236+
206237
def test_others_projects_builds_list(self):
207238
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")
208239
response = self.client.get(

readthedocs/api/v3/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
# allows /api/v3/projects/
2525
# allows /api/v3/projects/pip/
2626
# allows /api/v3/projects/pip/superproject/
27+
# allows /api/v3/projects/pip/sync-versions/
2728
projects = router.register(
2829
r"projects",
2930
ProjectsViewSet,

readthedocs/api/v3/views.py

+25
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from readthedocs.builds.models import Build, Version
2929
from readthedocs.core.utils import trigger_build
3030
from readthedocs.core.utils.extend import SettingsOverrideObject
31+
from readthedocs.core.views.hooks import trigger_sync_versions
3132
from readthedocs.notifications.models import Notification
3233
from readthedocs.oauth.models import (
3334
RemoteOrganization,
@@ -161,6 +162,9 @@ def get_serializer_class(self):
161162
if self.action in ("update", "partial_update"):
162163
return ProjectUpdateSerializer
163164

165+
# Default serializer so that sync_versions works with the BrowseableAPI
166+
return ProjectSerializer
167+
164168
def get_queryset(self):
165169
# Allow hitting ``/api/v3/projects/`` to list their own projects
166170
if self.basename == "projects" and self.action == "list":
@@ -218,6 +222,27 @@ def superproject(self, request, project_slug):
218222
except Exception:
219223
return Response(status=status.HTTP_404_NOT_FOUND)
220224

225+
@action(detail=True, methods=["post"], url_path="sync-versions")
226+
def sync_versions(self, request, project_slug):
227+
"""
228+
Kick off a task to sync versions for a project.
229+
230+
POST to this endpoint to trigger a task that syncs versions for the project.
231+
232+
This will be used in a button in the frontend,
233+
but also can be used to trigger a sync from the API.
234+
"""
235+
project = self.get_object()
236+
triggered = trigger_sync_versions(project)
237+
data = {}
238+
if triggered:
239+
data.update({"triggered": True})
240+
code = status.HTTP_202_ACCEPTED
241+
else:
242+
data.update({"triggered": False})
243+
code = status.HTTP_400_BAD_REQUEST
244+
return Response(data=data, status=code)
245+
221246

222247
class ProjectsViewSet(SettingsOverrideObject):
223248
_default_class = ProjectsViewSetBase

0 commit comments

Comments
 (0)