-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make team-related Cyberstorm endpoints support required functionality
Instead of one endpoint for all team information, use separate endpoints for team's basic information, members, and service accounts. On the client side these are used in separate tabs, so this works better with Suspenses, reduces passing props around and avoids overfetching. The basic information ("details") is public information, while the members and service accounts are intended only for team members. The actual information returned was changed to reflect what's actually needed on the client. When checking if the requested team exists, the name is compared case insensitively. I assume this is the correct approach since creation of teams with names that differ only in case is prohibited. Refs TS-1856
- Loading branch information
Showing
7 changed files
with
268 additions
and
50 deletions.
There are no files selected for viewing
14 changes: 12 additions & 2 deletions
14
django/thunderstore/api/cyberstorm/serializers/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,13 @@ | ||
from .community import CyberstormCommunitySerializer | ||
from .team import CyberstormTeamMemberSerializer, CyberstormTeamSerializer | ||
from .user import CyberstormUserSerializer | ||
from .team import ( | ||
CyberstormServiceAccountSerializer, | ||
CyberstormTeamMemberSerializer, | ||
CyberstormTeamSerializer, | ||
) | ||
|
||
__all__ = [ | ||
"CyberstormCommunitySerializer", | ||
"CyberstormServiceAccountSerializer", | ||
"CyberstormTeamMemberSerializer", | ||
"CyberstormTeamSerializer", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,32 @@ | ||
from typing import Optional | ||
|
||
from rest_framework import serializers | ||
|
||
from .user import CyberstormUserSerializer | ||
from thunderstore.social.utils import get_avatar_url | ||
|
||
|
||
class CyberstormTeamSerializer(serializers.Serializer): | ||
""" | ||
This is for team's public profile and readably by anyone. Don't add | ||
any sensitive information here. | ||
""" | ||
|
||
identifier = serializers.IntegerField(source="id") | ||
name = serializers.CharField() | ||
donation_link = serializers.CharField(required=False) | ||
|
||
|
||
class CyberstormTeamMemberSerializer(serializers.Serializer): | ||
user = CyberstormUserSerializer() | ||
identifier = serializers.IntegerField(source="user.id") | ||
username = serializers.CharField(source="user.username") | ||
avatar = serializers.SerializerMethodField() | ||
role = serializers.CharField() | ||
|
||
def get_avatar(self, obj) -> Optional[str]: | ||
return get_avatar_url(obj.user) | ||
|
||
class CyberstormTeamSerializer(serializers.Serializer): | ||
identifier = serializers.CharField(source="id") | ||
name = serializers.CharField() | ||
members = CyberstormTeamMemberSerializer(many=True) | ||
donation_link = serializers.CharField(required=False) | ||
|
||
class CyberstormServiceAccountSerializer(serializers.Serializer): | ||
identifier = serializers.CharField(source="uuid") | ||
name = serializers.CharField(source="user.first_name") | ||
last_used = serializers.DateTimeField() |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,169 @@ | ||
import pytest | ||
from rest_framework.test import APIClient | ||
|
||
from thunderstore.community.models import CommunitySite | ||
from thunderstore.repository.models.team import Team, TeamMember | ||
from thunderstore.account.factories import ServiceAccountFactory | ||
from thunderstore.core.types import UserType | ||
from thunderstore.repository.factories import TeamFactory, TeamMemberFactory | ||
from thunderstore.repository.models.team import Team | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_api_cyberstorm_team_detail_success( | ||
client: APIClient, | ||
community_site: CommunitySite, | ||
def test_team_detail_api_view__for_active_team__returns_data( | ||
api_client: APIClient, | ||
team: Team, | ||
team_member: TeamMember, | ||
): | ||
response = client.get( | ||
f"/api/cyberstorm/team/{team.name}/", | ||
HTTP_HOST=community_site.site.domain, | ||
) | ||
response = api_client.get(f"/api/cyberstorm/team/{team.name}/") | ||
result = response.json() | ||
|
||
assert response.status_code == 200 | ||
assert team.name == result["name"] | ||
assert team.donation_link == result["donation_link"] | ||
|
||
members = team.members.all() | ||
assert len(members) == 1 | ||
assert len(members) == len(result["members"]) | ||
|
||
member = members.first() | ||
assert result["members"][0]["role"] == member.role | ||
assert result["members"][0]["user"]["identifier"] == str(member.user.pk) | ||
assert result["members"][0]["user"]["username"] == member.user.username | ||
@pytest.mark.django_db | ||
def test_team_detail_api_view__for_nonexisting_team__returns_404(api_client: APIClient): | ||
response = api_client.get("/api/cyberstorm/team/bad/") | ||
|
||
assert response.status_code == 404 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_api_cyberstorm_team_detail_failure( | ||
client: APIClient, community_site: CommunitySite | ||
def test_team_detail_api_view__when_fetching_team__is_case_insensitive( | ||
api_client: APIClient, | ||
): | ||
response = client.get( | ||
f"/api/cyberstorm/team/bad/", | ||
HTTP_HOST=community_site.site.domain, | ||
) | ||
TeamFactory(name="RaDTeAm") | ||
|
||
response = api_client.get("/api/cyberstorm/team/radteam/") | ||
|
||
assert response.status_code == 200 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_team_detail_api_view__for_inactive_team__returns_404( | ||
api_client: APIClient, | ||
team: Team, | ||
): | ||
team.is_active = False | ||
team.save() | ||
|
||
response = api_client.get(f"/api/cyberstorm/team/{team.name}/") | ||
|
||
assert response.status_code == 404 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_team_membership_permission__for_unauthenticated_user__returns_401( | ||
api_client: APIClient, | ||
team: Team, | ||
): | ||
response = api_client.get(f"/api/cyberstorm/team/{team.name}/members/") | ||
|
||
assert response.status_code == 401 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_team_membership_permission__for_nonexisting_team__returns_404( | ||
api_client: APIClient, | ||
user: UserType, | ||
): | ||
api_client.force_authenticate(user) | ||
|
||
response = api_client.get("/api/cyberstorm/team/bad/members/") | ||
|
||
assert response.status_code == 404 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_team_membership_permission__for_inactive_team__returns_404( | ||
api_client: APIClient, | ||
team: Team, | ||
user: UserType, | ||
): | ||
team.is_active = False | ||
team.save() | ||
api_client.force_authenticate(user) | ||
|
||
response = api_client.get(f"/api/cyberstorm/team/{team.name}/members/") | ||
|
||
assert response.status_code == 404 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_team_membership_permission__for_nonmember__returns_403( | ||
api_client: APIClient, | ||
team: Team, | ||
user: UserType, | ||
): | ||
api_client.force_authenticate(user) | ||
|
||
response = api_client.get(f"/api/cyberstorm/team/{team.name}/members/") | ||
|
||
assert response.status_code == 403 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_team_membership_permission__for_member__returns_200( | ||
api_client: APIClient, | ||
team: Team, | ||
user: UserType, | ||
): | ||
TeamMemberFactory(team=team, user=user) | ||
api_client.force_authenticate(user) | ||
|
||
response = api_client.get(f"/api/cyberstorm/team/{team.name}/members/") | ||
|
||
assert response.status_code == 200 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_team_membership_permission__when_fetching_team__is_case_insensitive( | ||
api_client: APIClient, | ||
team: Team, | ||
user: UserType, | ||
): | ||
team = TeamFactory(name="ThunderGods") | ||
TeamMemberFactory(team=team, user=user) | ||
api_client.force_authenticate(user) | ||
|
||
response = api_client.get("/api/cyberstorm/team/thundergods/members/") | ||
|
||
assert response.status_code == 200 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_team_members_api_view__for_member__returns_only_real_users( | ||
api_client: APIClient, | ||
team: Team, | ||
user: UserType, | ||
): | ||
TeamMemberFactory(team=team, user=user, role="member") | ||
ServiceAccountFactory(owner=team) | ||
api_client.force_authenticate(user) | ||
|
||
response = api_client.get(f"/api/cyberstorm/team/{team.name}/members/") | ||
result = response.json() | ||
|
||
assert len(result) == 1 | ||
assert result[0]["identifier"] == user.id | ||
assert result[0]["username"] == user.username | ||
assert result[0]["avatar"] is None | ||
assert result[0]["role"] == "member" | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_team_service_accounts_api_view__for_member__returns_only_service_accounts( | ||
api_client: APIClient, | ||
team: Team, | ||
user: UserType, | ||
): | ||
TeamMemberFactory(team=team, user=user, role="member") | ||
sa = ServiceAccountFactory(owner=team) | ||
api_client.force_authenticate(user) | ||
|
||
response = api_client.get(f"/api/cyberstorm/team/{team.name}/service-accounts/") | ||
result = response.json() | ||
|
||
assert len(result) == 1 | ||
assert result[0]["identifier"] == str(sa.uuid) | ||
assert result[0]["name"] == sa.user.first_name | ||
assert result[0]["last_used"] is None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,11 @@ | ||
from .community_detail import CommunityDetailAPIView | ||
from .community_list import CommunityListAPIView | ||
from .team import TeamDetailAPIView | ||
from .team import TeamDetailAPIView, TeamMembersAPIView, TeamServiceAccountsAPIView | ||
|
||
__all__ = [ | ||
"CommunityDetailAPIView", | ||
"CommunityListAPIView", | ||
"TeamDetailAPIView", | ||
"TeamMembersAPIView", | ||
"TeamServiceAccountsAPIView", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,67 @@ | ||
from rest_framework.generics import RetrieveAPIView | ||
from django.db.models import Q, QuerySet | ||
from rest_framework.exceptions import NotFound, PermissionDenied | ||
from rest_framework.filters import OrderingFilter | ||
from rest_framework.generics import ListAPIView, RetrieveAPIView | ||
from rest_framework.permissions import IsAuthenticated | ||
from rest_framework.request import Request | ||
|
||
from thunderstore.api.cyberstorm.serializers import CyberstormTeamSerializer | ||
from thunderstore.api.utils import conditional_swagger_auto_schema | ||
from thunderstore.repository.models.team import Team | ||
from thunderstore.account.models.service_account import ServiceAccount | ||
from thunderstore.api.cyberstorm.serializers import ( | ||
CyberstormServiceAccountSerializer, | ||
CyberstormTeamMemberSerializer, | ||
CyberstormTeamSerializer, | ||
) | ||
from thunderstore.api.utils import CyberstormAutoSchemaMixin | ||
from thunderstore.repository.models.team import Team, TeamMember | ||
|
||
|
||
class TeamDetailAPIView(RetrieveAPIView): | ||
permission_classes = [] | ||
class TeamDetailAPIView(CyberstormAutoSchemaMixin, RetrieveAPIView): | ||
serializer_class = CyberstormTeamSerializer | ||
queryset = Team.objects.exclude(is_active=False).prefetch_related("members") | ||
lookup_field = "name" | ||
queryset = Team.objects.exclude(is_active=False) | ||
lookup_field = "name__iexact" | ||
lookup_url_kwarg = "team_id" | ||
|
||
@conditional_swagger_auto_schema(tags=["cyberstorm"]) | ||
def get(self, *args, **kwargs): | ||
return super().get(*args, **kwargs) | ||
|
||
class TeamRestrictedAPIView(ListAPIView): | ||
""" | ||
Ensure the user is a member of the Team. | ||
""" | ||
|
||
permission_classes = [IsAuthenticated] | ||
|
||
def check_permissions(self, request: Request) -> None: | ||
super().check_permissions(request) | ||
|
||
try: | ||
team = Team.objects.exclude(is_active=False).get( | ||
name__iexact=self.kwargs["team_id"], | ||
) | ||
except Team.DoesNotExist: | ||
raise NotFound() | ||
|
||
if not team.can_user_access(request.user): | ||
raise PermissionDenied() | ||
|
||
|
||
class TeamMembersAPIView(CyberstormAutoSchemaMixin, TeamRestrictedAPIView): | ||
serializer_class = CyberstormTeamMemberSerializer | ||
filter_backends = [OrderingFilter] | ||
ordering = ["-role", "user__username"] | ||
|
||
def get_queryset(self) -> QuerySet[TeamMember]: | ||
return ( | ||
TeamMember.objects.real_users() | ||
.exclude(~Q(team__name__iexact=self.kwargs["team_id"])) | ||
.prefetch_related("user__social_auth") | ||
) | ||
|
||
|
||
class TeamServiceAccountsAPIView(CyberstormAutoSchemaMixin, TeamRestrictedAPIView): | ||
serializer_class = CyberstormServiceAccountSerializer | ||
filter_backends = [OrderingFilter] | ||
ordering = ["user__first_name"] | ||
|
||
def get_queryset(self) -> QuerySet[ServiceAccount]: | ||
return ServiceAccount.objects.exclude( | ||
~Q(owner__name__iexact=self.kwargs["team_id"]), | ||
).select_related("user") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters