diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index b9f02b6c2d8..8b7343b4a6b 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -1103,6 +1103,7 @@ class Meta: class OrganizationLinksSerializer(BaseLinksSerializer): _self = serializers.SerializerMethodField() projects = serializers.SerializerMethodField() + notifications = serializers.SerializerMethodField() def get__self(self, obj): path = reverse( @@ -1122,6 +1123,15 @@ def get_projects(self, obj): ) return self._absolute_url(path) + def get_notifications(self, obj): + path = reverse( + "organizations-notifications-list", + kwargs={ + "parent_lookup_organization__slug": obj.slug, + }, + ) + return self._absolute_url(path) + class TeamSerializer(FlexFieldsModelSerializer): # TODO: add ``projects`` as flex field when we have a diff --git a/readthedocs/api/v3/tests/test_organizations.py b/readthedocs/api/v3/tests/test_organizations.py index b1b97167048..075d191aa0f 100644 --- a/readthedocs/api/v3/tests/test_organizations.py +++ b/readthedocs/api/v3/tests/test_organizations.py @@ -114,7 +114,7 @@ def test_organizations_notifications_detail_other(self): self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.others_token.key}") response = self.client.get(url) - self.assertEqual(response.status_code, 404) + self.assertEqual(response.status_code, 403) def test_organizations_notifications_detail_patch(self): url = reverse( diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 3087c9516a1..f3934ad0502 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -58,7 +58,12 @@ UpdateMixin, UserQuerySetMixin, ) -from .permissions import CommonPermissions, IsProjectAdmin +from .permissions import ( + CommonPermissions, + IsOrganizationAdmin, + IsOrganizationAdminMember, + IsProjectAdmin, +) from .renderers import AlphabeticalSortedJSONRenderer from .serializers import ( BuildCreateSerializer, @@ -614,7 +619,7 @@ def get_queryset(self): ) -class OrganizationsViewSet( +class OrganizationsViewSetBase( APIv3Settings, GenericViewSet, ): @@ -622,6 +627,7 @@ class OrganizationsViewSet( # /api/v3/organizations//notifications/ # However, accessing to /api/v3/organizations/ or /api/v3/organizations// will return 404. # We can implement these endpoints when we need them, tho. + # Also note that Read the Docs for Business expose this endpoint already. model = Organization serializer_class = OrganizationSerializer @@ -629,6 +635,31 @@ class OrganizationsViewSet( permission_classes = (IsAuthenticated,) +class OrganizationsViewSet(SettingsOverrideObject): + _default_class = OrganizationsViewSetBase + + +class OrganizationsProjectsViewSet( + APIv3Settings, + NestedViewSetMixin, + OrganizationQuerySetMixin, + ReadOnlyModelViewSet, +): + model = Project + lookup_field = "slug" + lookup_url_kwarg = "project_slug" + queryset = Project.objects.all() + serializer_class = ProjectSerializer + permission_classes = [IsAuthenticated & IsOrganizationAdminMember] + permit_list_expands = [ + "organization", + "organization.teams", + ] + + def get_view_name(self): + return f"Organizations Projects {self.suffix}" + + class NotificationsOrganizationViewSet( APIv3Settings, NestedViewSetMixin, @@ -645,6 +676,7 @@ class NotificationsOrganizationViewSet( serializer_class = NotificationSerializer queryset = Notification.objects.all() filterset_class = NotificationFilter + permission_classes = [IsAuthenticated & IsOrganizationAdmin] def get_queryset(self): content_type = ContentType.objects.get_for_model(Organization)