From 0cc5ceb876f1c8ec837401786e2763441eb57c52 Mon Sep 17 00:00:00 2001 From: saadmk11 Date: Sat, 26 Sep 2020 15:18:46 +0600 Subject: [PATCH 01/15] Add List API for Remote Repository --- docs/api/v3.rst | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/docs/api/v3.rst b/docs/api/v3.rst index 6ed788e947d..f8195e464e9 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1624,6 +1624,73 @@ Organization projects list } + +Remote Repositories +~~~~~~~~~~~~~~~~~~~ + +Remote Repositories are the importable repositories connected via +``GitHub``, ``GitLab`` and ``BitBucket``. + + +Remote Repositories listing ++++++++++++++++++++++++++++ + + +.. http:get:: /api/v3/remoterepositories/ + + Retrieve a list of all Remote Repositories for the authenticated user. + + **Example request**: + + .. tabs:: + + .. code-tab:: bash + + $ curl -H "Authorization: Token " https://readthedocs.org/api/v3/remoterepositories/ + + .. code-tab:: python + + import requests + URL = 'https://readthedocs.org/api/v3/remoterepositories/' + TOKEN = '' + HEADERS = {'Authorization': f'token {TOKEN}'} + response = requests.get(URL, headers=HEADERS) + print(response.json()) + + **Example response**: + + .. sourcecode:: json + + { + "count": 20, + "next": "api/v3/remoterepositories/?limit=10&offset=10", + "previous": null, + "results": [ + { + "avatar_url": "https://avatars3.githubusercontent.com/u/test-rtd?v=4", + "clone_url": "https://github.com/rtd/project.git", + "created": "2019-04-29T10:00:00Z", + "description": "This is a test project.", + "full_name": "rtd/project", + "html_url": "https://github.com/rtd/project", + "modified": "2019-04-29T12:00:00Z", + "name": "project", + "pk": 1, + "ssh_url": "git@github.com:rtd/project.git", + "vcs": "git" + } + ] + } + + + The ``results`` in response is an array of remote repositories data. + + :query string name: return remote repositories with matching name + :query string vcs: return remote repositories with specific vcs (``git``, ``svn``, ``hg`` or ``bzr``) + + :requestheader Authorization: token to authenticate. + + Additional APIs --------------- From 1c66784578d5cbd1c071151b8e215ba3da5f649f Mon Sep 17 00:00:00 2001 From: saadmk11 Date: Sat, 26 Sep 2020 15:19:54 +0600 Subject: [PATCH 02/15] Add List API for Remote Repository --- readthedocs/api/v3/filters.py | 12 ++++++ readthedocs/api/v3/serializers.py | 23 +++++++++++ .../responses/remoterepositories-list.json | 20 ++++++++++ .../api/v3/tests/test_remoterepositories.py | 39 +++++++++++++++++++ readthedocs/api/v3/urls.py | 8 ++++ readthedocs/api/v3/views.py | 21 +++++++++- 6 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 readthedocs/api/v3/tests/responses/remoterepositories-list.json create mode 100644 readthedocs/api/v3/tests/test_remoterepositories.py diff --git a/readthedocs/api/v3/filters.py b/readthedocs/api/v3/filters.py index 746cb802901..bbeec336f7f 100644 --- a/readthedocs/api/v3/filters.py +++ b/readthedocs/api/v3/filters.py @@ -2,6 +2,7 @@ from readthedocs.builds.constants import BUILD_STATE_FINISHED from readthedocs.builds.models import Build, Version +from readthedocs.oauth.models import RemoteRepository from readthedocs.projects.models import Project @@ -47,3 +48,14 @@ def get_running(self, queryset, name, value): return queryset.exclude(state=BUILD_STATE_FINISHED) return queryset.filter(state=BUILD_STATE_FINISHED) + + +class RemoteRepositoryFilter(filters.FilterSet): + name = filters.CharFilter(lookup_expr='icontains') + + class Meta: + model = RemoteRepository + fields = [ + 'name', + 'vcs', + ] diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index 8836067329f..3534422d811 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -13,6 +13,7 @@ from readthedocs.core.utils.extend import SettingsOverrideObject from readthedocs.builds.models import Build, Version from readthedocs.core.utils import slugify +from readthedocs.oauth.models import RemoteRepository from readthedocs.organizations.models import Organization, Team from readthedocs.projects.constants import ( LANGUAGES, @@ -891,3 +892,25 @@ class Meta: 'projects': (ProjectSerializer, {'many': True}), 'teams': (TeamSerializer, {'many': True}), } + + +class RemoteRepositorySerializer(serializers.ModelSerializer): + created = serializers.DateTimeField(source='pub_date', read_only=True) + modified = serializers.DateTimeField(source='modified_date', read_only=True) + + class Meta: + model = RemoteRepository + fields = [ + 'pk', + 'name', + 'full_name', + 'description', + 'avatar_url', + 'ssh_url', + 'clone_url', + 'html_url', + 'vcs', + 'created', + 'modified', + ] + read_only_fields = fields diff --git a/readthedocs/api/v3/tests/responses/remoterepositories-list.json b/readthedocs/api/v3/tests/responses/remoterepositories-list.json new file mode 100644 index 00000000000..28d32e2fa29 --- /dev/null +++ b/readthedocs/api/v3/tests/responses/remoterepositories-list.json @@ -0,0 +1,20 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "avatar_url": "https://avatars3.githubusercontent.com/u/test-rtd?v=4", + "clone_url": "https://github.com/rtd/project.git", + "created": "2019-04-29T10:00:00Z", + "description": "This is a test project.", + "full_name": "rtd/project", + "html_url": "https://github.com/rtd/project", + "modified": "2019-04-29T12:00:00Z", + "name": "project", + "pk": 1, + "ssh_url": "git@github.com:rtd/project.git", + "vcs": "git" + } + ] +} diff --git a/readthedocs/api/v3/tests/test_remoterepositories.py b/readthedocs/api/v3/tests/test_remoterepositories.py new file mode 100644 index 00000000000..66a13bb5bef --- /dev/null +++ b/readthedocs/api/v3/tests/test_remoterepositories.py @@ -0,0 +1,39 @@ +from django.urls import reverse + +import django_dynamic_fixture as fixture + +from .mixins import APIEndpointMixin +from readthedocs.oauth.models import RemoteRepository + + +class ProjectsEndpointTests(APIEndpointMixin): + + def setUp(self): + super().setUp() + + self.remote_repository = fixture.get( + RemoteRepository, + pub_date=self.created, + modified_date=self.modified, + avatar_url="https://avatars3.githubusercontent.com/u/test-rtd?v=4", + clone_url="https://github.com/rtd/project.git", + description="This is a test project.", + full_name="rtd/project", + html_url="https://github.com/rtd/project", + name="project", + ssh_url="git@github.com:rtd/project.git", + vcs="git", + users=[self.me] + ) + + def test_projects_list(self): + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get( + reverse('remoterepositories-list') + ) + self.assertEqual(response.status_code, 200) + + self.assertDictEqual( + response.json(), + self._get_response_dict('remoterepositories-list'), + ) diff --git a/readthedocs/api/v3/urls.py b/readthedocs/api/v3/urls.py index f1e5e09162a..630d0bee136 100644 --- a/readthedocs/api/v3/urls.py +++ b/readthedocs/api/v3/urls.py @@ -5,6 +5,7 @@ EnvironmentVariablesViewSet, ProjectsViewSet, RedirectsViewSet, + RemoteRepositoryViewSet, SubprojectRelationshipViewSet, TranslationRelationshipViewSet, VersionsViewSet, @@ -87,5 +88,12 @@ parents_query_lookups=['project__slug'], ) +# allows /api/v3/remoterepositories/ +remoterepositories = router.register( + r'remoterepositories', + RemoteRepositoryViewSet, + basename='remoterepositories', +) + urlpatterns = [] urlpatterns += router.urls diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 54d50b83930..f92e325489c 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -21,6 +21,7 @@ from readthedocs.builds.models import Build, Version from readthedocs.core.utils import trigger_build +from readthedocs.oauth.models import RemoteRepository from readthedocs.core.utils.extend import SettingsOverrideObject from readthedocs.organizations.models import Organization from readthedocs.projects.models import Project, EnvironmentVariable, ProjectRelationship @@ -28,7 +29,12 @@ from readthedocs.redirects.models import Redirect -from .filters import BuildFilter, ProjectFilter, VersionFilter +from .filters import ( + BuildFilter, + ProjectFilter, + RemoteRepositoryFilter, + VersionFilter, +) from .mixins import OrganizationQuerySetMixin, ProjectQuerySetMixin, UpdateMixin from .permissions import ( CommonPermissions, @@ -47,6 +53,7 @@ ProjectUpdateSerializer, RedirectCreateSerializer, RedirectDetailSerializer, + RemoteRepositorySerializer, SubprojectCreateSerializer, SubprojectSerializer, SubprojectDestroySerializer, @@ -414,3 +421,15 @@ def get_view_name(self): class OrganizationsProjectsViewSet(SettingsOverrideObject): _default_class = OrganizationsProjectsViewSetBase + + +class RemoteRepositoryViewSet(APIv3Settings, ListModelMixin, GenericViewSet): + model = RemoteRepository + serializer_class = RemoteRepositorySerializer + filterset_class = RemoteRepositoryFilter + queryset = RemoteRepository.objects.all() + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + queryset = super().get_queryset() + return queryset.filter(users=self.request.user) From 24224f405df941af47f624980389b4d3abb46cc5 Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Wed, 24 Mar 2021 16:00:13 +0600 Subject: [PATCH 03/15] Remove unused imports and fix lint --- docs/api/v3.rst | 1 - readthedocs/api/v3/views.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/docs/api/v3.rst b/docs/api/v3.rst index f8195e464e9..d6824fa1509 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1624,7 +1624,6 @@ Organization projects list } - Remote Repositories ~~~~~~~~~~~~~~~~~~~ diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index f92e325489c..e78857c597d 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -1,5 +1,4 @@ import django_filters.rest_framework as filters -from django.utils.safestring import mark_safe from rest_flex_fields.views import FlexFieldsMixin from rest_framework import status from rest_framework.authentication import SessionAuthentication, TokenAuthentication @@ -39,8 +38,6 @@ from .permissions import ( CommonPermissions, IsProjectAdmin, - IsOrganizationAdmin, - UserOrganizationsListing, ) from .renderers import AlphabeticalSortedJSONRenderer from .serializers import ( From d2c588c0e0f49cc55f2425590ed5bd1ce1b82a16 Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Wed, 24 Mar 2021 16:51:54 +0600 Subject: [PATCH 04/15] Added more filters and fixted test --- docs/api/v3.rst | 12 +++++---- readthedocs/api/v3/filters.py | 2 ++ readthedocs/api/v3/serializers.py | 16 ++++++++++-- .../responses/remoterepositories-list.json | 4 ++- .../api/v3/tests/test_remoterepositories.py | 26 ++++++++++++++----- readthedocs/api/v3/urls.py | 6 ++--- readthedocs/api/v3/views.py | 18 +++++++++++-- 7 files changed, 64 insertions(+), 20 deletions(-) diff --git a/docs/api/v3.rst b/docs/api/v3.rst index d6824fa1509..79c187274a0 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1635,7 +1635,7 @@ Remote Repositories listing +++++++++++++++++++++++++++ -.. http:get:: /api/v3/remoterepositories/ +.. http:get:: /api/v3/remote/repositories/ Retrieve a list of all Remote Repositories for the authenticated user. @@ -1645,12 +1645,12 @@ Remote Repositories listing .. code-tab:: bash - $ curl -H "Authorization: Token " https://readthedocs.org/api/v3/remoterepositories/ + $ curl -H "Authorization: Token " https://readthedocs.org/api/v3/remote/repositories/ .. code-tab:: python import requests - URL = 'https://readthedocs.org/api/v3/remoterepositories/' + URL = 'https://readthedocs.org/api/v3/remote/repositories/' TOKEN = '' HEADERS = {'Authorization': f'token {TOKEN}'} response = requests.get(URL, headers=HEADERS) @@ -1662,7 +1662,7 @@ Remote Repositories listing { "count": 20, - "next": "api/v3/remoterepositories/?limit=10&offset=10", + "next": "api/v3/remote/repositories/?limit=10&offset=10", "previous": null, "results": [ { @@ -1685,7 +1685,9 @@ Remote Repositories listing The ``results`` in response is an array of remote repositories data. :query string name: return remote repositories with matching name - :query string vcs: return remote repositories with specific vcs (``git``, ``svn``, ``hg`` or ``bzr``) + :query string vcs: return remote repositories for specific vcs (``git``, ``svn``, ``hg`` or ``bzr``) + :query string vcs_provider: return remote repositories for specific vcs provider (``github``, ``gitlab`` or ``bitbucket``) + :query string organization: return remote repositories for specific remote organization (Using remote organization ``pk``) :requestheader Authorization: token to authenticate. diff --git a/readthedocs/api/v3/filters.py b/readthedocs/api/v3/filters.py index bbeec336f7f..d8815fdabc8 100644 --- a/readthedocs/api/v3/filters.py +++ b/readthedocs/api/v3/filters.py @@ -58,4 +58,6 @@ class Meta: fields = [ 'name', 'vcs', + 'vcs_provider', + 'organization', ] diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index 3534422d811..465065a5130 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -895,8 +895,7 @@ class Meta: class RemoteRepositorySerializer(serializers.ModelSerializer): - created = serializers.DateTimeField(source='pub_date', read_only=True) - modified = serializers.DateTimeField(source='modified_date', read_only=True) + admin = serializers.SerializerMethodField('is_admin') class Meta: model = RemoteRepository @@ -905,12 +904,25 @@ class Meta: 'name', 'full_name', 'description', + 'admin', 'avatar_url', 'ssh_url', 'clone_url', 'html_url', 'vcs', + 'vcs_provider', 'created', 'modified', ] read_only_fields = fields + + def is_admin(self, obj): + request = self.context['request'] + + # Use annotated value from RemoteRepositoryViewSet queryset + if hasattr(obj, '_admin'): + return obj._admin + + return obj.remote_repository_relations.filter( + user=request.user, admin=True + ).exists() diff --git a/readthedocs/api/v3/tests/responses/remoterepositories-list.json b/readthedocs/api/v3/tests/responses/remoterepositories-list.json index 28d32e2fa29..9c0fd0a72f0 100644 --- a/readthedocs/api/v3/tests/responses/remoterepositories-list.json +++ b/readthedocs/api/v3/tests/responses/remoterepositories-list.json @@ -14,7 +14,9 @@ "name": "project", "pk": 1, "ssh_url": "git@github.com:rtd/project.git", - "vcs": "git" + "vcs": "git", + "vcs_provider": "github", + "admin": true } ] } diff --git a/readthedocs/api/v3/tests/test_remoterepositories.py b/readthedocs/api/v3/tests/test_remoterepositories.py index 66a13bb5bef..e9d85d643d8 100644 --- a/readthedocs/api/v3/tests/test_remoterepositories.py +++ b/readthedocs/api/v3/tests/test_remoterepositories.py @@ -1,20 +1,24 @@ +from allauth.socialaccount.models import SocialAccount from django.urls import reverse import django_dynamic_fixture as fixture +from readthedocs.oauth.constants import GITHUB +from readthedocs.oauth.models import RemoteRepository, RemoteRepositoryRelation +from readthedocs.projects.constants import REPO_TYPE_GIT from .mixins import APIEndpointMixin -from readthedocs.oauth.models import RemoteRepository -class ProjectsEndpointTests(APIEndpointMixin): + +class RemoteRepositoryEndpointTests(APIEndpointMixin): def setUp(self): super().setUp() self.remote_repository = fixture.get( RemoteRepository, - pub_date=self.created, - modified_date=self.modified, + created=self.created, + modified=self.modified, avatar_url="https://avatars3.githubusercontent.com/u/test-rtd?v=4", clone_url="https://github.com/rtd/project.git", description="This is a test project.", @@ -22,11 +26,19 @@ def setUp(self): html_url="https://github.com/rtd/project", name="project", ssh_url="git@github.com:rtd/project.git", - vcs="git", - users=[self.me] + vcs=REPO_TYPE_GIT, + vcs_provider=GITHUB, + ) + social_account = fixture.get(SocialAccount, user=self.me, provider=GITHUB) + fixture.get( + RemoteRepositoryRelation, + remote_repository=self.remote_repository, + user=self.me, + account=social_account, + admin=True ) - def test_projects_list(self): + def test_remote_repository_list(self): self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') response = self.client.get( reverse('remoterepositories-list') diff --git a/readthedocs/api/v3/urls.py b/readthedocs/api/v3/urls.py index 630d0bee136..475b2a6334b 100644 --- a/readthedocs/api/v3/urls.py +++ b/readthedocs/api/v3/urls.py @@ -88,9 +88,9 @@ parents_query_lookups=['project__slug'], ) -# allows /api/v3/remoterepositories/ -remoterepositories = router.register( - r'remoterepositories', +# allows /api/v3/remote/repositories/ +router.register( + r'remote/repositories', RemoteRepositoryViewSet, basename='remoterepositories', ) diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index e78857c597d..cddcbab3a0d 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -1,3 +1,5 @@ +from django.db.models import Case, When, Value, BooleanField + import django_filters.rest_framework as filters from rest_flex_fields.views import FlexFieldsMixin from rest_framework import status @@ -428,5 +430,17 @@ class RemoteRepositoryViewSet(APIv3Settings, ListModelMixin, GenericViewSet): permission_classes = (IsAuthenticated,) def get_queryset(self): - queryset = super().get_queryset() - return queryset.filter(users=self.request.user) + queryset = super().get_queryset().api(self.request.user).annotate( + _admin=Case( + When( + remote_repository_relations__user=self.request.user, + remote_repository_relations__admin=True, + then=Value(True) + ), + default=Value(False), + output_field=BooleanField() + ) + ) + return queryset.select_related('organization').order_by( + 'organization__name', 'full_name' + ).distinct() From 3b0f7730a1acee3fd70d5e00cb0f67d8cd582882 Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Wed, 24 Mar 2021 17:04:03 +0600 Subject: [PATCH 05/15] Use Subquery with Exists to get _admin attribute --- readthedocs/api/v3/views.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index cddcbab3a0d..438815dc0da 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -1,4 +1,4 @@ -from django.db.models import Case, When, Value, BooleanField +from django.db.models import Exists, OuterRef import django_filters.rest_framework as filters from rest_flex_fields.views import FlexFieldsMixin @@ -22,7 +22,7 @@ from readthedocs.builds.models import Build, Version from readthedocs.core.utils import trigger_build -from readthedocs.oauth.models import RemoteRepository +from readthedocs.oauth.models import RemoteRepository, RemoteRepositoryRelation from readthedocs.core.utils.extend import SettingsOverrideObject from readthedocs.organizations.models import Organization from readthedocs.projects.models import Project, EnvironmentVariable, ProjectRelationship @@ -431,14 +431,12 @@ class RemoteRepositoryViewSet(APIv3Settings, ListModelMixin, GenericViewSet): def get_queryset(self): queryset = super().get_queryset().api(self.request.user).annotate( - _admin=Case( - When( - remote_repository_relations__user=self.request.user, - remote_repository_relations__admin=True, - then=Value(True) - ), - default=Value(False), - output_field=BooleanField() + _admin=Exists( + RemoteRepositoryRelation.objects.filter( + remote_repository=OuterRef('pk'), + user=self.request.user, + admin=True + ) ) ) return queryset.select_related('organization').order_by( From ba529c133ee3cecbcf4f8a9995e5900f6edd58c6 Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Wed, 24 Mar 2021 22:41:36 +0600 Subject: [PATCH 06/15] Add RemoteOrganization API --- docs/api/v3.rst | 4 +- readthedocs/api/v3/filters.py | 13 ++++- readthedocs/api/v3/serializers.py | 21 +++++++- .../responses/remoteorganizations-list.json | 17 +++++++ .../api/v3/tests/test_remoteorganizations.py | 48 +++++++++++++++++++ .../api/v3/tests/test_remoterepositories.py | 2 +- readthedocs/api/v3/urls.py | 8 ++++ readthedocs/api/v3/views.py | 25 +++++++++- 8 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 readthedocs/api/v3/tests/responses/remoteorganizations-list.json create mode 100644 readthedocs/api/v3/tests/test_remoteorganizations.py diff --git a/docs/api/v3.rst b/docs/api/v3.rst index 79c187274a0..83fdfcd797d 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1676,7 +1676,9 @@ Remote Repositories listing "name": "project", "pk": 1, "ssh_url": "git@github.com:rtd/project.git", - "vcs": "git" + "vcs": "git", + "vcs_provider": "github", + "admin": true } ] } diff --git a/readthedocs/api/v3/filters.py b/readthedocs/api/v3/filters.py index d8815fdabc8..b133098ac96 100644 --- a/readthedocs/api/v3/filters.py +++ b/readthedocs/api/v3/filters.py @@ -2,7 +2,7 @@ from readthedocs.builds.constants import BUILD_STATE_FINISHED from readthedocs.builds.models import Build, Version -from readthedocs.oauth.models import RemoteRepository +from readthedocs.oauth.models import RemoteRepository, RemoteOrganization from readthedocs.projects.models import Project @@ -61,3 +61,14 @@ class Meta: 'vcs_provider', 'organization', ] + + +class RemoteOrganizationFilter(filters.FilterSet): + name = filters.CharFilter(lookup_expr='icontains') + + class Meta: + model = RemoteOrganization + fields = [ + 'name', + 'vcs_provider', + ] diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index 465065a5130..b3441716448 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -13,7 +13,7 @@ from readthedocs.core.utils.extend import SettingsOverrideObject from readthedocs.builds.models import Build, Version from readthedocs.core.utils import slugify -from readthedocs.oauth.models import RemoteRepository +from readthedocs.oauth.models import RemoteRepository, RemoteOrganization from readthedocs.organizations.models import Organization, Team from readthedocs.projects.constants import ( LANGUAGES, @@ -894,8 +894,26 @@ class Meta: } +class RemoteOrganizationSerializer(serializers.ModelSerializer): + + class Meta: + model = RemoteOrganization + fields = [ + 'pk', + 'slug', + 'name', + 'avatar_url', + 'url', + 'vcs_provider', + 'created', + 'modified', + ] + read_only_fields = fields + + class RemoteRepositorySerializer(serializers.ModelSerializer): admin = serializers.SerializerMethodField('is_admin') + organization = RemoteOrganizationSerializer() class Meta: model = RemoteRepository @@ -913,6 +931,7 @@ class Meta: 'vcs_provider', 'created', 'modified', + 'organization', ] read_only_fields = fields diff --git a/readthedocs/api/v3/tests/responses/remoteorganizations-list.json b/readthedocs/api/v3/tests/responses/remoteorganizations-list.json new file mode 100644 index 00000000000..1ca52010eff --- /dev/null +++ b/readthedocs/api/v3/tests/responses/remoteorganizations-list.json @@ -0,0 +1,17 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "avatar_url": "https://avatars.githubusercontent.com/u/366329?v=4", + "created": "2019-04-29T10:00:00Z", + "modified": "2019-04-29T12:00:00Z", + "name": "Read the Docs", + "pk": 1, + "slug": "readthedocs", + "url": "https://github.com/readthedocs", + "vcs_provider": "github" + } + ] +} diff --git a/readthedocs/api/v3/tests/test_remoteorganizations.py b/readthedocs/api/v3/tests/test_remoteorganizations.py new file mode 100644 index 00000000000..7deac239bf5 --- /dev/null +++ b/readthedocs/api/v3/tests/test_remoteorganizations.py @@ -0,0 +1,48 @@ +from django.urls import reverse + +from allauth.socialaccount.models import SocialAccount +import django_dynamic_fixture as fixture + +from readthedocs.oauth.constants import GITHUB +from readthedocs.oauth.models import ( + RemoteOrganization, + RemoteOrganizationRelation, +) +from .mixins import APIEndpointMixin + + + +class RemoteOrganizationEndpointTests(APIEndpointMixin): + + def setUp(self): + super().setUp() + + self.remote_organization = fixture.get( + RemoteOrganization, + created=self.created, + modified=self.modified, + avatar_url="https://avatars.githubusercontent.com/u/366329?v=4", + name="Read the Docs", + slug="readthedocs", + url="https://github.com/readthedocs", + vcs_provider=GITHUB, + ) + social_account = fixture.get(SocialAccount, user=self.me, provider=GITHUB) + fixture.get( + RemoteOrganizationRelation, + remote_organization=self.remote_organization, + user=self.me, + account=social_account + ) + + def test_remote_organization_list(self): + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get( + reverse('remoteorganizations-list') + ) + self.assertEqual(response.status_code, 200) + + self.assertDictEqual( + response.json(), + self._get_response_dict('remoteorganizations-list'), + ) diff --git a/readthedocs/api/v3/tests/test_remoterepositories.py b/readthedocs/api/v3/tests/test_remoterepositories.py index e9d85d643d8..6411dcf8bef 100644 --- a/readthedocs/api/v3/tests/test_remoterepositories.py +++ b/readthedocs/api/v3/tests/test_remoterepositories.py @@ -1,6 +1,6 @@ -from allauth.socialaccount.models import SocialAccount from django.urls import reverse +from allauth.socialaccount.models import SocialAccount import django_dynamic_fixture as fixture from readthedocs.oauth.constants import GITHUB diff --git a/readthedocs/api/v3/urls.py b/readthedocs/api/v3/urls.py index 475b2a6334b..c7abad3f459 100644 --- a/readthedocs/api/v3/urls.py +++ b/readthedocs/api/v3/urls.py @@ -5,6 +5,7 @@ EnvironmentVariablesViewSet, ProjectsViewSet, RedirectsViewSet, + RemoteOrganizationViewSet, RemoteRepositoryViewSet, SubprojectRelationshipViewSet, TranslationRelationshipViewSet, @@ -95,5 +96,12 @@ basename='remoterepositories', ) +# allows /api/v3/remote/organizations/ +router.register( + r'remote/organizations', + RemoteOrganizationViewSet, + basename='remoteorganizations', +) + urlpatterns = [] urlpatterns += router.urls diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 438815dc0da..8050b787199 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -22,10 +22,18 @@ from readthedocs.builds.models import Build, Version from readthedocs.core.utils import trigger_build -from readthedocs.oauth.models import RemoteRepository, RemoteRepositoryRelation +from readthedocs.oauth.models import ( + RemoteOrganization, + RemoteRepository, + RemoteRepositoryRelation, +) from readthedocs.core.utils.extend import SettingsOverrideObject from readthedocs.organizations.models import Organization -from readthedocs.projects.models import Project, EnvironmentVariable, ProjectRelationship +from readthedocs.projects.models import ( + EnvironmentVariable, + Project, + ProjectRelationship, +) from readthedocs.projects.views.mixins import ProjectImportMixin from readthedocs.redirects.models import Redirect @@ -33,6 +41,7 @@ from .filters import ( BuildFilter, ProjectFilter, + RemoteOrganizationFilter, RemoteRepositoryFilter, VersionFilter, ) @@ -52,6 +61,7 @@ ProjectUpdateSerializer, RedirectCreateSerializer, RedirectDetailSerializer, + RemoteOrganizationSerializer, RemoteRepositorySerializer, SubprojectCreateSerializer, SubprojectSerializer, @@ -442,3 +452,14 @@ def get_queryset(self): return queryset.select_related('organization').order_by( 'organization__name', 'full_name' ).distinct() + + +class RemoteOrganizationViewSet(APIv3Settings, ListModelMixin, GenericViewSet): + model = RemoteOrganization + serializer_class = RemoteOrganizationSerializer + filterset_class = RemoteOrganizationFilter + queryset = RemoteOrganization.objects.all() + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + return super().get_queryset().api(self.request.user) From 1f4f732c772b494cc7265a7962a693117e717b7f Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Wed, 24 Mar 2021 22:46:57 +0600 Subject: [PATCH 07/15] Add Documentation for RemoteOrganization API Endpoint --- docs/api/v3.rst | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/docs/api/v3.rst b/docs/api/v3.rst index 83fdfcd797d..a504b7c1837 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1624,6 +1624,69 @@ Organization projects list } +Remote Organizations +~~~~~~~~~~~~~~~~~~~ + +Remote Organizations are the importable organizations connected via +``GitHub``, ``GitLab`` and ``BitBucket``. + + +Remote Organization listing ++++++++++++++++++++++++++++ + + +.. http:get:: /api/v3/remote/organizations/ + + Retrieve a list of all Remote Organizations for the authenticated user. + + **Example request**: + + .. tabs:: + + .. code-tab:: bash + + $ curl -H "Authorization: Token " https://readthedocs.org/api/v3/remote/organizations/ + + .. code-tab:: python + + import requests + URL = 'https://readthedocs.org/api/v3/remote/organizations/' + TOKEN = '' + HEADERS = {'Authorization': f'token {TOKEN}'} + response = requests.get(URL, headers=HEADERS) + print(response.json()) + + **Example response**: + + .. sourcecode:: json + + { + "count": 20, + "next": "api/v3/remote/organizations/?limit=10&offset=10", + "previous": null, + "results": [ + { + "avatar_url": "https://avatars.githubusercontent.com/u/12345?v=4", + "created": "2019-04-29T10:00:00Z", + "modified": "2019-04-29T12:00:00Z", + "name": "Organization Name", + "pk": 1, + "slug": "organization", + "url": "https://github.com/organization", + "vcs_provider": "github" + } + ] + } + + + The ``results`` in response is an array of remote repositories data. + + :query string name: return remote organizations with matching name + :query string vcs_provider: return remote organizations for specific vcs provider (``github``, ``gitlab`` or ``bitbucket``) + + :requestheader Authorization: token to authenticate. + + Remote Repositories ~~~~~~~~~~~~~~~~~~~ From 30144bcbcded6c178480de2a05f300aba6aa2362 Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Wed, 24 Mar 2021 22:49:25 +0600 Subject: [PATCH 08/15] Fix documentation --- docs/api/v3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/v3.rst b/docs/api/v3.rst index a504b7c1837..e635e4f4f01 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1679,7 +1679,7 @@ Remote Organization listing } - The ``results`` in response is an array of remote repositories data. + The ``results`` in response is an array of remote organizations data. :query string name: return remote organizations with matching name :query string vcs_provider: return remote organizations for specific vcs provider (``github``, ``gitlab`` or ``bitbucket``) From 07c22efb0761c36d9a987600144774fce25a011b Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Wed, 24 Mar 2021 22:55:03 +0600 Subject: [PATCH 09/15] Add remote organization to remote_repository API response docs --- docs/api/v3.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/api/v3.rst b/docs/api/v3.rst index e635e4f4f01..6283db15112 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1694,7 +1694,7 @@ Remote Repositories are the importable repositories connected via ``GitHub``, ``GitLab`` and ``BitBucket``. -Remote Repositories listing +Remote Repository listing +++++++++++++++++++++++++++ @@ -1729,6 +1729,16 @@ Remote Repositories listing "previous": null, "results": [ { + "organization": { + "avatar_url": "https://avatars.githubusercontent.com/u/12345?v=4", + "created": "2019-04-29T10:00:00Z", + "modified": "2019-04-29T12:00:00Z", + "name": "Organization Name", + "pk": 1, + "slug": "organization", + "url": "https://github.com/organization", + "vcs_provider": "github" + }, "avatar_url": "https://avatars3.githubusercontent.com/u/test-rtd?v=4", "clone_url": "https://github.com/rtd/project.git", "created": "2019-04-29T10:00:00Z", From f55da196bfd1affe76866172e2f518245ffa233b Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Wed, 24 Mar 2021 22:56:41 +0600 Subject: [PATCH 10/15] Docs lint fix --- docs/api/v3.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/v3.rst b/docs/api/v3.rst index 6283db15112..da407bd90c5 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1625,7 +1625,7 @@ Organization projects list Remote Organizations -~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~ Remote Organizations are the importable organizations connected via ``GitHub``, ``GitLab`` and ``BitBucket``. @@ -1695,7 +1695,7 @@ Remote Repositories are the importable repositories connected via Remote Repository listing -+++++++++++++++++++++++++++ ++++++++++++++++++++++++++ .. http:get:: /api/v3/remote/repositories/ From b17d18f27f67a1535901bbc9b27e81f84c21d28c Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Wed, 24 Mar 2021 23:04:33 +0600 Subject: [PATCH 11/15] Fix RemoteRepository API test --- .../responses/remoterepositories-list.json | 10 +++++++ .../api/v3/tests/test_remoterepositories.py | 26 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/readthedocs/api/v3/tests/responses/remoterepositories-list.json b/readthedocs/api/v3/tests/responses/remoterepositories-list.json index 9c0fd0a72f0..8f9d56ffd30 100644 --- a/readthedocs/api/v3/tests/responses/remoterepositories-list.json +++ b/readthedocs/api/v3/tests/responses/remoterepositories-list.json @@ -4,6 +4,16 @@ "previous": null, "results": [ { + "organization": { + "avatar_url": "https://avatars.githubusercontent.com/u/366329?v=4", + "created": "2019-04-29T10:00:00Z", + "modified": "2019-04-29T12:00:00Z", + "name": "Read the Docs", + "pk": 1, + "slug": "readthedocs", + "url": "https://github.com/readthedocs", + "vcs_provider": "github" + }, "avatar_url": "https://avatars3.githubusercontent.com/u/test-rtd?v=4", "clone_url": "https://github.com/rtd/project.git", "created": "2019-04-29T10:00:00Z", diff --git a/readthedocs/api/v3/tests/test_remoterepositories.py b/readthedocs/api/v3/tests/test_remoterepositories.py index 6411dcf8bef..74c36cb2c86 100644 --- a/readthedocs/api/v3/tests/test_remoterepositories.py +++ b/readthedocs/api/v3/tests/test_remoterepositories.py @@ -4,7 +4,13 @@ import django_dynamic_fixture as fixture from readthedocs.oauth.constants import GITHUB -from readthedocs.oauth.models import RemoteRepository, RemoteRepositoryRelation +from readthedocs.oauth.models import ( + RemoteOrganization, + RemoteOrganizationRelation, + RemoteRepository, + RemoteRepositoryRelation, +) + from readthedocs.projects.constants import REPO_TYPE_GIT from .mixins import APIEndpointMixin @@ -15,8 +21,20 @@ class RemoteRepositoryEndpointTests(APIEndpointMixin): def setUp(self): super().setUp() + self.remote_organization = fixture.get( + RemoteOrganization, + created=self.created, + modified=self.modified, + avatar_url="https://avatars.githubusercontent.com/u/366329?v=4", + name="Read the Docs", + slug="readthedocs", + url="https://github.com/readthedocs", + vcs_provider=GITHUB, + ) + self.remote_repository = fixture.get( RemoteRepository, + organization=self.remote_organization, created=self.created, modified=self.modified, avatar_url="https://avatars3.githubusercontent.com/u/test-rtd?v=4", @@ -37,6 +55,12 @@ def setUp(self): account=social_account, admin=True ) + fixture.get( + RemoteOrganizationRelation, + remote_organization=self.remote_organization, + user=self.me, + account=social_account + ) def test_remote_repository_list(self): self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') From f186a4ad083793bcb8e77459601c51ef1a1f5b4f Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Wed, 24 Mar 2021 23:05:20 +0600 Subject: [PATCH 12/15] lint update --- readthedocs/api/v3/tests/test_remoterepositories.py | 1 - 1 file changed, 1 deletion(-) diff --git a/readthedocs/api/v3/tests/test_remoterepositories.py b/readthedocs/api/v3/tests/test_remoterepositories.py index 74c36cb2c86..6fd4e49d654 100644 --- a/readthedocs/api/v3/tests/test_remoterepositories.py +++ b/readthedocs/api/v3/tests/test_remoterepositories.py @@ -10,7 +10,6 @@ RemoteRepository, RemoteRepositoryRelation, ) - from readthedocs.projects.constants import REPO_TYPE_GIT from .mixins import APIEndpointMixin From d80d4087c1cdeb23bbe5c435ef4ca238bca014db Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Sat, 27 Mar 2021 22:25:10 +0600 Subject: [PATCH 13/15] Add Expandable Fields for RemoteRepository API and apply suggestions --- docs/api/v3.rst | 74 +++++++++++++++---- readthedocs/api/v3/filters.py | 10 +-- readthedocs/api/v3/mixins.py | 6 ++ readthedocs/api/v3/serializers.py | 14 +++- .../responses/remoterepositories-list.json | 55 +++++++++++--- .../api/v3/tests/test_remoteorganizations.py | 18 +++++ .../api/v3/tests/test_remoterepositories.py | 33 ++++++++- readthedocs/api/v3/views.py | 45 ++++++++--- 8 files changed, 213 insertions(+), 42 deletions(-) diff --git a/docs/api/v3.rst b/docs/api/v3.rst index da407bd90c5..a250c81a1a1 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1627,7 +1627,7 @@ Organization projects list Remote Organizations ~~~~~~~~~~~~~~~~~~~~ -Remote Organizations are the importable organizations connected via +Remote Organizations are the VCS organizations connected via ``GitHub``, ``GitLab`` and ``BitBucket``. @@ -1681,7 +1681,7 @@ Remote Organization listing The ``results`` in response is an array of remote organizations data. - :query string name: return remote organizations with matching name + :query string name__contains: return remote organizations with containing the name :query string vcs_provider: return remote organizations for specific vcs provider (``github``, ``gitlab`` or ``bitbucket``) :requestheader Authorization: token to authenticate. @@ -1708,12 +1708,12 @@ Remote Repository listing .. code-tab:: bash - $ curl -H "Authorization: Token " https://readthedocs.org/api/v3/remote/repositories/ + $ curl -H "Authorization: Token " https://readthedocs.org/api/v3/remote/repositories/?expand=project,organization .. code-tab:: python import requests - URL = 'https://readthedocs.org/api/v3/remote/repositories/' + URL = 'https://readthedocs.org/api/v3/remote/repositories/?expand=project,organization' TOKEN = '' HEADERS = {'Authorization': f'token {TOKEN}'} response = requests.get(URL, headers=HEADERS) @@ -1725,7 +1725,7 @@ Remote Repository listing { "count": 20, - "next": "api/v3/remote/repositories/?limit=10&offset=10", + "next": "api/v3/remote/repositories/?expand=project,organization&limit=10&offset=10", "previous": null, "results": [ { @@ -1739,18 +1739,64 @@ Remote Repository listing "url": "https://github.com/organization", "vcs_provider": "github" }, - "avatar_url": "https://avatars3.githubusercontent.com/u/test-rtd?v=4", - "clone_url": "https://github.com/rtd/project.git", + "project": { + "id": 12345, + "name": "project", + "slug": "project", + "created": "2010-10-23T18:12:31+00:00", + "modified": "2018-12-11T07:21:11+00:00", + "language": { + "code": "en", + "name": "English" + }, + "programming_language": { + "code": "py", + "name": "Python" + }, + "repository": { + "url": "https://github.com/organization/project", + "type": "git" + }, + "default_version": "stable", + "default_branch": "master", + "subproject_of": null, + "translation_of": null, + "urls": { + "documentation": "http://project.readthedocs.io/en/stable/", + "home": "https://readthedocs.org/projects/project/" + }, + "tags": [ + "test" + ], + "users": [ + { + "username": "dstufft" + } + ], + "_links": { + "_self": "/api/v3/projects/project/", + "versions": "/api/v3/projects/project/versions/", + "builds": "/api/v3/projects/project/builds/", + "subprojects": "/api/v3/projects/project/subprojects/", + "superproject": "/api/v3/projects/project/superproject/", + "redirects": "/api/v3/projects/project/redirects/", + "translations": "/api/v3/projects/project/translations/" + } + } + "avatar_url": "https://avatars3.githubusercontent.com/u/test-organization?v=4", + "clone_url": "https://github.com/organization/project.git", "created": "2019-04-29T10:00:00Z", "description": "This is a test project.", - "full_name": "rtd/project", - "html_url": "https://github.com/rtd/project", + "full_name": "organization/project", + "html_url": "https://github.com/organization/project", "modified": "2019-04-29T12:00:00Z", "name": "project", "pk": 1, - "ssh_url": "git@github.com:rtd/project.git", + "ssh_url": "git@github.com:organization/project.git", "vcs": "git", "vcs_provider": "github", + "default_branch": "master", + "private": false, "admin": true } ] @@ -1759,10 +1805,12 @@ Remote Repository listing The ``results`` in response is an array of remote repositories data. - :query string name: return remote repositories with matching name - :query string vcs: return remote repositories for specific vcs (``git``, ``svn``, ``hg`` or ``bzr``) + :query string name__contains: return remote repositories containing the name :query string vcs_provider: return remote repositories for specific vcs provider (``github``, ``gitlab`` or ``bitbucket``) - :query string organization: return remote repositories for specific remote organization (Using remote organization ``pk``) + :query string organization: return remote repositories for specific remote organization (Using remote organization ``slug``) + :query string expand: allows to add/expand some extra fields in the response. + Allowed values are ``project`` and ``organization``. + Multiple fields can be passed separated by commas. :requestheader Authorization: token to authenticate. diff --git a/readthedocs/api/v3/filters.py b/readthedocs/api/v3/filters.py index b133098ac96..73a62da3365 100644 --- a/readthedocs/api/v3/filters.py +++ b/readthedocs/api/v3/filters.py @@ -51,24 +51,24 @@ def get_running(self, queryset, name, value): class RemoteRepositoryFilter(filters.FilterSet): - name = filters.CharFilter(lookup_expr='icontains') + name__contains = filters.CharFilter(field_name='name', lookup_expr='icontains') + organization = filters.CharFilter(field_name='organization__slug') class Meta: model = RemoteRepository fields = [ - 'name', - 'vcs', + 'name__contains', 'vcs_provider', 'organization', ] class RemoteOrganizationFilter(filters.FilterSet): - name = filters.CharFilter(lookup_expr='icontains') + name__contains = filters.CharFilter(field_name='name', lookup_expr='icontains') class Meta: model = RemoteOrganization fields = [ - 'name', + 'name__contains', 'vcs_provider', ] diff --git a/readthedocs/api/v3/mixins.py b/readthedocs/api/v3/mixins.py index 1f7721e84c1..d58fdbf9d0a 100644 --- a/readthedocs/api/v3/mixins.py +++ b/readthedocs/api/v3/mixins.py @@ -184,3 +184,9 @@ def update(self, request, *args, **kwargs): # via Javascript super().update(request, *args, **kwargs) return Response(status=status.HTTP_204_NO_CONTENT) + + +class RemoteQuerySetMixin: + + def get_queryset(self): + return super().get_queryset().api(self.request.user) diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index b3441716448..ccd347ca78b 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -911,9 +911,8 @@ class Meta: read_only_fields = fields -class RemoteRepositorySerializer(serializers.ModelSerializer): +class RemoteRepositorySerializer(FlexFieldsModelSerializer): admin = serializers.SerializerMethodField('is_admin') - organization = RemoteOrganizationSerializer() class Meta: model = RemoteRepository @@ -929,11 +928,20 @@ class Meta: 'html_url', 'vcs', 'vcs_provider', + 'private', + 'default_branch', 'created', 'modified', - 'organization', ] read_only_fields = fields + expandable_fields = { + 'organization': ( + RemoteOrganizationSerializer, {'source': 'organization'} + ), + 'project': ( + ProjectSerializer, {'source': 'project'} + ) + } def is_admin(self, obj): request = self.context['request'] diff --git a/readthedocs/api/v3/tests/responses/remoterepositories-list.json b/readthedocs/api/v3/tests/responses/remoterepositories-list.json index 8f9d56ffd30..050f1238009 100644 --- a/readthedocs/api/v3/tests/responses/remoterepositories-list.json +++ b/readthedocs/api/v3/tests/responses/remoterepositories-list.json @@ -4,6 +4,16 @@ "previous": null, "results": [ { + "admin": true, + "avatar_url": "https://avatars3.githubusercontent.com/u/test-rtd?v=4", + "clone_url": "https://github.com/rtd/project.git", + "created": "2019-04-29T10:00:00Z", + "default_branch": "master", + "description": "This is a test project.", + "full_name": "rtd/project", + "html_url": "https://github.com/rtd/project", + "modified": "2019-04-29T12:00:00Z", + "name": "project", "organization": { "avatar_url": "https://avatars.githubusercontent.com/u/366329?v=4", "created": "2019-04-29T10:00:00Z", @@ -14,19 +24,44 @@ "url": "https://github.com/readthedocs", "vcs_provider": "github" }, - "avatar_url": "https://avatars3.githubusercontent.com/u/test-rtd?v=4", - "clone_url": "https://github.com/rtd/project.git", - "created": "2019-04-29T10:00:00Z", - "description": "This is a test project.", - "full_name": "rtd/project", - "html_url": "https://github.com/rtd/project", - "modified": "2019-04-29T12:00:00Z", - "name": "project", "pk": 1, + "private": false, + "project": { + "_links": { + "_self": "https://readthedocs.org/api/v3/projects/project/", + "builds": "https://readthedocs.org/api/v3/projects/project/builds/", + "environmentvariables": "https://readthedocs.org/api/v3/projects/project/environmentvariables/", + "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/", + "translations": "https://readthedocs.org/api/v3/projects/project/translations/", + "versions": "https://readthedocs.org/api/v3/projects/project/versions/" + }, + "created": "2019-04-29T10:00:00Z", + "default_branch": "master", + "default_version": "latest", + "homepage": "http://project.com", + "id": 1, + "language": { "code": "en", "name": "English" }, + "modified": "2019-04-29T12:00:00Z", + "name": "project", + "programming_language": { "code": "words", "name": "Only Words" }, + "repository": { "type": "git", "url": "https://github.com/rtfd/project" }, + "slug": "project", + "subproject_of": null, + "tags": ["tag", "project", "test"], + "translation_of": null, + "urls": { + "builds": "https://readthedocs.org/projects/project/builds/", + "documentation": "http://project.readthedocs.io/en/latest/", + "home": "https://readthedocs.org/projects/project/", + "versions": "https://readthedocs.org/projects/project/versions/" + }, + "users": [{ "username": "testuser" }] + }, "ssh_url": "git@github.com:rtd/project.git", "vcs": "git", - "vcs_provider": "github", - "admin": true + "vcs_provider": "github" } ] } diff --git a/readthedocs/api/v3/tests/test_remoteorganizations.py b/readthedocs/api/v3/tests/test_remoteorganizations.py index 7deac239bf5..8baeb3d698e 100644 --- a/readthedocs/api/v3/tests/test_remoteorganizations.py +++ b/readthedocs/api/v3/tests/test_remoteorganizations.py @@ -46,3 +46,21 @@ def test_remote_organization_list(self): response.json(), self._get_response_dict('remoteorganizations-list'), ) + + def test_remote_organization_list_name__contains_filter(self): + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get( + reverse('remoteorganizations-list'), + { + 'name__contains': 'Read' + } + ) + self.assertEqual(response.status_code, 200) + + response_data = response.json() + + self.assertEqual(len(response_data['results']), 1) + self.assertDictEqual( + response_data, + self._get_response_dict('remoteorganizations-list'), + ) diff --git a/readthedocs/api/v3/tests/test_remoterepositories.py b/readthedocs/api/v3/tests/test_remoterepositories.py index 6fd4e49d654..a31beb564ab 100644 --- a/readthedocs/api/v3/tests/test_remoterepositories.py +++ b/readthedocs/api/v3/tests/test_remoterepositories.py @@ -33,6 +33,7 @@ def setUp(self): self.remote_repository = fixture.get( RemoteRepository, + project=self.project, organization=self.remote_organization, created=self.created, modified=self.modified, @@ -45,6 +46,8 @@ def setUp(self): ssh_url="git@github.com:rtd/project.git", vcs=REPO_TYPE_GIT, vcs_provider=GITHUB, + default_branch="master", + private=False ) social_account = fixture.get(SocialAccount, user=self.me, provider=GITHUB) fixture.get( @@ -64,7 +67,13 @@ def setUp(self): def test_remote_repository_list(self): self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') response = self.client.get( - reverse('remoterepositories-list') + reverse('remoterepositories-list'), + { + 'expand': ( + 'project,' + 'organization' + ) + } ) self.assertEqual(response.status_code, 200) @@ -72,3 +81,25 @@ def test_remote_repository_list(self): response.json(), self._get_response_dict('remoterepositories-list'), ) + + def test_remote_repository_list_name__contains_filter(self): + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get( + reverse('remoterepositories-list'), + { + 'expand': ( + 'project,' + 'organization' + ), + 'name__contains': 'proj' + } + ) + self.assertEqual(response.status_code, 200) + + response_data = response.json() + self.assertEqual(len(response_data['results']), 1) + + self.assertDictEqual( + response_data, + self._get_response_dict('remoterepositories-list'), + ) diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 8050b787199..cb7cd5fd96d 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -1,6 +1,7 @@ from django.db.models import Exists, OuterRef import django_filters.rest_framework as filters +from rest_flex_fields import is_expanded from rest_flex_fields.views import FlexFieldsMixin from rest_framework import status from rest_framework.authentication import SessionAuthentication, TokenAuthentication @@ -45,7 +46,12 @@ RemoteRepositoryFilter, VersionFilter, ) -from .mixins import OrganizationQuerySetMixin, ProjectQuerySetMixin, UpdateMixin +from .mixins import ( + OrganizationQuerySetMixin, + ProjectQuerySetMixin, + RemoteQuerySetMixin, + UpdateMixin, +) from .permissions import ( CommonPermissions, IsProjectAdmin, @@ -432,15 +438,25 @@ class OrganizationsProjectsViewSet(SettingsOverrideObject): _default_class = OrganizationsProjectsViewSetBase -class RemoteRepositoryViewSet(APIv3Settings, ListModelMixin, GenericViewSet): +class RemoteRepositoryViewSet( + APIv3Settings, + RemoteQuerySetMixin, + FlexFieldsMixin, + ListModelMixin, + GenericViewSet +): model = RemoteRepository serializer_class = RemoteRepositorySerializer filterset_class = RemoteRepositoryFilter queryset = RemoteRepository.objects.all() permission_classes = (IsAuthenticated,) + permit_list_expands = [ + 'organization', + 'project' + ] def get_queryset(self): - queryset = super().get_queryset().api(self.request.user).annotate( + queryset = super().get_queryset().annotate( _admin=Exists( RemoteRepositoryRelation.objects.filter( remote_repository=OuterRef('pk'), @@ -449,17 +465,26 @@ def get_queryset(self): ) ) ) - return queryset.select_related('organization').order_by( - 'organization__name', 'full_name' - ).distinct() + + if is_expanded(self.request, 'organization'): + queryset = queryset.select_related('organization') + + if is_expanded(self.request, 'project'): + queryset = queryset.select_related('project').prefetch_related( + 'project__users', + ) + + return queryset.order_by('organization__name', 'full_name').distinct() -class RemoteOrganizationViewSet(APIv3Settings, ListModelMixin, GenericViewSet): +class RemoteOrganizationViewSet( + APIv3Settings, + RemoteQuerySetMixin, + ListModelMixin, + GenericViewSet +): model = RemoteOrganization serializer_class = RemoteOrganizationSerializer filterset_class = RemoteOrganizationFilter queryset = RemoteOrganization.objects.all() permission_classes = (IsAuthenticated,) - - def get_queryset(self): - return super().get_queryset().api(self.request.user) From 38b2591e7cc88a56210c5cdb3c8f7041422231de Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Mon, 29 Mar 2021 17:14:24 +0600 Subject: [PATCH 14/15] Update docs/api/v3.rst Co-authored-by: Manuel Kaufmann --- docs/api/v3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/v3.rst b/docs/api/v3.rst index a250c81a1a1..2c1e451b229 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1807,7 +1807,7 @@ Remote Repository listing :query string name__contains: return remote repositories containing the name :query string vcs_provider: return remote repositories for specific vcs provider (``github``, ``gitlab`` or ``bitbucket``) - :query string organization: return remote repositories for specific remote organization (Using remote organization ``slug``) + :query string organization: return remote repositories for specific remote organization (using remote organization ``slug``) :query string expand: allows to add/expand some extra fields in the response. Allowed values are ``project`` and ``organization``. Multiple fields can be passed separated by commas. From 540b10eb414300cc3abb13ebbe3545f19c9b1f28 Mon Sep 17 00:00:00 2001 From: Maksudul Haque Date: Mon, 29 Mar 2021 17:24:26 +0600 Subject: [PATCH 15/15] revert name__contains filter to name --- docs/api/v3.rst | 4 ++-- readthedocs/api/v3/filters.py | 8 ++++---- readthedocs/api/v3/tests/test_remoteorganizations.py | 4 ++-- readthedocs/api/v3/tests/test_remoterepositories.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/api/v3.rst b/docs/api/v3.rst index 2c1e451b229..ebea47c866f 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1681,7 +1681,7 @@ Remote Organization listing The ``results`` in response is an array of remote organizations data. - :query string name__contains: return remote organizations with containing the name + :query string name: return remote organizations with containing the name :query string vcs_provider: return remote organizations for specific vcs provider (``github``, ``gitlab`` or ``bitbucket``) :requestheader Authorization: token to authenticate. @@ -1805,7 +1805,7 @@ Remote Repository listing The ``results`` in response is an array of remote repositories data. - :query string name__contains: return remote repositories containing the name + :query string name: return remote repositories containing the name :query string vcs_provider: return remote repositories for specific vcs provider (``github``, ``gitlab`` or ``bitbucket``) :query string organization: return remote repositories for specific remote organization (using remote organization ``slug``) :query string expand: allows to add/expand some extra fields in the response. diff --git a/readthedocs/api/v3/filters.py b/readthedocs/api/v3/filters.py index 73a62da3365..cf3efc29573 100644 --- a/readthedocs/api/v3/filters.py +++ b/readthedocs/api/v3/filters.py @@ -51,24 +51,24 @@ def get_running(self, queryset, name, value): class RemoteRepositoryFilter(filters.FilterSet): - name__contains = filters.CharFilter(field_name='name', lookup_expr='icontains') + name = filters.CharFilter(field_name='name', lookup_expr='icontains') organization = filters.CharFilter(field_name='organization__slug') class Meta: model = RemoteRepository fields = [ - 'name__contains', + 'name', 'vcs_provider', 'organization', ] class RemoteOrganizationFilter(filters.FilterSet): - name__contains = filters.CharFilter(field_name='name', lookup_expr='icontains') + name = filters.CharFilter(field_name='name', lookup_expr='icontains') class Meta: model = RemoteOrganization fields = [ - 'name__contains', + 'name', 'vcs_provider', ] diff --git a/readthedocs/api/v3/tests/test_remoteorganizations.py b/readthedocs/api/v3/tests/test_remoteorganizations.py index 8baeb3d698e..8475e0e1a79 100644 --- a/readthedocs/api/v3/tests/test_remoteorganizations.py +++ b/readthedocs/api/v3/tests/test_remoteorganizations.py @@ -47,12 +47,12 @@ def test_remote_organization_list(self): self._get_response_dict('remoteorganizations-list'), ) - def test_remote_organization_list_name__contains_filter(self): + def test_remote_organization_list_name_filter(self): self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') response = self.client.get( reverse('remoteorganizations-list'), { - 'name__contains': 'Read' + 'name': 'Read' } ) self.assertEqual(response.status_code, 200) diff --git a/readthedocs/api/v3/tests/test_remoterepositories.py b/readthedocs/api/v3/tests/test_remoterepositories.py index a31beb564ab..58e2745c01f 100644 --- a/readthedocs/api/v3/tests/test_remoterepositories.py +++ b/readthedocs/api/v3/tests/test_remoterepositories.py @@ -82,7 +82,7 @@ def test_remote_repository_list(self): self._get_response_dict('remoterepositories-list'), ) - def test_remote_repository_list_name__contains_filter(self): + def test_remote_repository_list_name_filter(self): self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') response = self.client.get( reverse('remoterepositories-list'), @@ -91,7 +91,7 @@ def test_remote_repository_list_name__contains_filter(self): 'project,' 'organization' ), - 'name__contains': 'proj' + 'name': 'proj' } ) self.assertEqual(response.status_code, 200)