Skip to content

Commit 2530f73

Browse files
authored
Merge pull request #1116 from thunderstore-io/cyberstorm-api-update-team
Implement update team endpoint (TS-2314)
2 parents 762f236 + e40a166 commit 2530f73

File tree

13 files changed

+318
-11
lines changed

13 files changed

+318
-11
lines changed

django/thunderstore/api/cyberstorm/serializers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
CyberstormTeamAddMemberResponseSerializer,
1212
CyberstormTeamMemberSerializer,
1313
CyberstormTeamSerializer,
14+
CyberstormTeamUpdateSerializer,
1415
)
1516

1617
__all__ = [
@@ -25,4 +26,5 @@
2526
"CyberstormTeamMemberSerializer",
2627
"CyberstormTeamSerializer",
2728
"PackagePermissionsSerializer",
29+
"CyberstormTeamUpdateSerializer",
2830
]

django/thunderstore/api/cyberstorm/serializers/team.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Optional
22

3+
from django.core.validators import URLValidator
34
from rest_framework import serializers
45

56
from thunderstore.repository.forms import AddTeamMemberForm
@@ -53,3 +54,9 @@ class CyberstormCreateTeamSerializer(serializers.Serializer):
5354
name = serializers.CharField(
5455
max_length=64, validators=[PackageReferenceComponentValidator("Author name")]
5556
)
57+
58+
59+
class CyberstormTeamUpdateSerializer(serializers.Serializer):
60+
donation_link = serializers.CharField(
61+
max_length=1024, validators=[URLValidator(["https"])]
62+
)

django/thunderstore/api/cyberstorm/services/team.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,14 @@ def create_team(user: UserType, team_name: str) -> Team:
3131
team = Team.objects.create(name=team_name)
3232
team.add_member(user=user, role=TeamMemberRole.owner)
3333
return team
34+
35+
36+
@transaction.atomic
37+
def update_team(agent: UserType, team: Team, donation_link: str) -> Team:
38+
team.ensure_user_can_access(agent)
39+
team.ensure_user_can_edit_info(agent)
40+
41+
team.donation_link = donation_link
42+
team.save()
43+
44+
return team

django/thunderstore/api/cyberstorm/tests/services/test_team_services.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.http import Http404
44

55
from thunderstore.api.cyberstorm.services import team as team_services
6+
from thunderstore.core.exceptions import PermissionValidationError
67
from thunderstore.repository.models import Namespace, Team, TeamMemberRole
78

89

@@ -67,3 +68,38 @@ def test_create_team_success(user):
6768
assert Team.objects.filter(name=team_name).exists()
6869
assert team.name == team_name
6970
assert team.members.filter(user=user, role=TeamMemberRole.owner).exists()
71+
72+
73+
@pytest.mark.django_db
74+
def test_update_team_success(team_owner):
75+
team = team_owner.team
76+
new_donation_link = "https://example.com/donate"
77+
updated_team = team_services.update_team(
78+
agent=team_owner.user, team=team, donation_link=new_donation_link
79+
)
80+
81+
assert updated_team.donation_link == new_donation_link
82+
83+
84+
@pytest.mark.django_db
85+
def test_update_team_user_cannot_access(user, team):
86+
new_donation_link = "https://example.com/donate"
87+
88+
error_msg = "Must be a member to access team"
89+
with pytest.raises(PermissionValidationError, match=error_msg):
90+
team_services.update_team(
91+
agent=user, team=team, donation_link=new_donation_link
92+
)
93+
94+
95+
@pytest.mark.django_db
96+
def test_update_team_user_cannot_edit_info(team_member):
97+
new_donation_link = "https://example.com/donate"
98+
99+
error_msg = "Must be an owner to edit team info"
100+
with pytest.raises(PermissionValidationError, match=error_msg):
101+
team_services.update_team(
102+
agent=team_member.user,
103+
team=team_member.team,
104+
donation_link=new_donation_link,
105+
)

django/thunderstore/api/cyberstorm/tests/test_endpoints.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@
4242
put_payload_map = {}
4343

4444

45-
patch_payload_map = {}
45+
patch_payload_map = {
46+
"/api/cyberstorm/team/{team_name}/update/": {
47+
"donation_link": "https://example.com/donate",
48+
}
49+
}
4650

4751

4852
payload_mapping = {
@@ -162,7 +166,7 @@ def test_validate_extracted_paths_with_urlpatterns(api_client):
162166

163167
@pytest.mark.django_db
164168
@pytest.mark.parametrize("http_verb", ["put", "patch", "post"])
165-
def test_find_missing_post_endpoints(api_client, http_verb):
169+
def test_find_missing_endpoints(api_client, http_verb):
166170
schema = get_schema(api_client)
167171
api_paths = extract_paths(schema, "cyberstorm", http_verb)
168172
failures = []

django/thunderstore/api/cyberstorm/tests/test_team.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,137 @@ def test_team_member_add_api_view__when_adding_a_member__fails_because_user_is_n
322322
.count()
323323
== 0
324324
)
325+
326+
327+
@pytest.mark.django_db
328+
def test_team_update_succeeds(
329+
api_client: APIClient,
330+
user: UserType,
331+
team: Team,
332+
):
333+
TeamMemberFactory(team=team, user=user, role="owner")
334+
api_client.force_authenticate(user)
335+
336+
new_donation_link = "https://example.com"
337+
338+
response = api_client.patch(
339+
f"/api/cyberstorm/team/{team.name}/update/",
340+
json.dumps({"donation_link": new_donation_link}),
341+
content_type="application/json",
342+
)
343+
344+
expected_response = {"donation_link": new_donation_link}
345+
assert response.status_code == 200
346+
347+
assert response.json() == expected_response
348+
assert Team.objects.get(pk=team.pk).donation_link == new_donation_link
349+
350+
351+
@pytest.mark.django_db
352+
def test_team_update_fails_user_not_authenticated(
353+
api_client: APIClient,
354+
team: Team,
355+
):
356+
new_donation_link = "https://example.com"
357+
358+
response = api_client.patch(
359+
f"/api/cyberstorm/team/{team.name}/update/",
360+
json.dumps({"donation_link": new_donation_link}),
361+
content_type="application/json",
362+
)
363+
364+
expected_response = {"detail": "Authentication credentials were not provided."}
365+
366+
assert response.status_code == 401
367+
assert response.json() == expected_response
368+
assert Team.objects.get(pk=team.pk).donation_link is None
369+
370+
371+
@pytest.mark.django_db
372+
def test_team_update_fails_validation(
373+
api_client: APIClient,
374+
user: UserType,
375+
team: Team,
376+
):
377+
TeamMemberFactory(team=team, user=user, role="owner")
378+
api_client.force_authenticate(user)
379+
380+
new_bad_donation_link = "example.com"
381+
382+
response = api_client.patch(
383+
f"/api/cyberstorm/team/{team.name}/update/",
384+
json.dumps({"donation_link": new_bad_donation_link}),
385+
content_type="application/json",
386+
)
387+
388+
expected_response = {"donation_link": ["Enter a valid URL."]}
389+
390+
assert response.status_code == 400
391+
assert response.json() == expected_response
392+
393+
394+
@pytest.mark.django_db
395+
def test_team_update_fail_user_not_owner(
396+
api_client: APIClient,
397+
user: UserType,
398+
team: Team,
399+
):
400+
TeamMemberFactory(team=team, user=user, role="member")
401+
api_client.force_authenticate(user)
402+
403+
new_donation_link = "https://example.com"
404+
405+
response = api_client.patch(
406+
f"/api/cyberstorm/team/{team.name}/update/",
407+
json.dumps({"donation_link": new_donation_link}),
408+
content_type="application/json",
409+
)
410+
411+
expected_response = {"non_field_errors": ["Must be an owner to edit team info"]}
412+
413+
assert response.status_code == 403
414+
assert response.json() == expected_response
415+
assert Team.objects.get(pk=team.pk).donation_link is None
416+
417+
418+
@pytest.mark.django_db
419+
def test_team_update_fail_team_does_not_exist(
420+
api_client: APIClient,
421+
user: UserType,
422+
):
423+
api_client.force_authenticate(user)
424+
425+
new_donation_link = "https://example.com"
426+
427+
response = api_client.patch(
428+
"/api/cyberstorm/team/FakeTeam/update/",
429+
json.dumps({"donation_link": new_donation_link}),
430+
content_type="application/json",
431+
)
432+
433+
expected_response = {"detail": "Not found."}
434+
435+
assert response.status_code == 404
436+
assert response.json() == expected_response
437+
438+
439+
@pytest.mark.django_db
440+
def test_team_update_fail_user_not_team_member(
441+
api_client: APIClient,
442+
user: UserType,
443+
team: Team,
444+
):
445+
api_client.force_authenticate(user)
446+
447+
new_donation_link = "https://example.com"
448+
449+
response = api_client.patch(
450+
f"/api/cyberstorm/team/{team.name}/update/",
451+
json.dumps({"donation_link": new_donation_link}),
452+
content_type="application/json",
453+
)
454+
455+
expected_response = {"non_field_errors": ["Must be a member to access team"]}
456+
457+
assert response.status_code == 403
458+
assert response.json() == expected_response

django/thunderstore/api/cyberstorm/views/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
TeamMemberAddAPIView,
2525
TeamMemberListAPIView,
2626
TeamServiceAccountListAPIView,
27+
UpdateTeamAPIView,
2728
)
2829

2930
__all__ = [
@@ -49,4 +50,5 @@
4950
"UpdatePackageListingCategoriesAPIView",
5051
"RejectPackageListingAPIView",
5152
"ApprovePackageListingAPIView",
53+
"UpdateTeamAPIView",
5254
]

django/thunderstore/api/cyberstorm/views/team.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from django.core.exceptions import ValidationError as DjangoValidationError
21
from django.db.models import Q, QuerySet
32
from rest_framework import status
43
from rest_framework.exceptions import PermissionDenied, ValidationError
@@ -16,8 +15,13 @@
1615
CyberstormTeamAddMemberResponseSerializer,
1716
CyberstormTeamMemberSerializer,
1817
CyberstormTeamSerializer,
18+
CyberstormTeamUpdateSerializer,
19+
)
20+
from thunderstore.api.cyberstorm.services.team import (
21+
create_team,
22+
disband_team,
23+
update_team,
1924
)
20-
from thunderstore.api.cyberstorm.services import team as team_services
2125
from thunderstore.api.ordering import StrictOrderingFilter
2226
from thunderstore.api.utils import (
2327
CyberstormAutoSchemaMixin,
@@ -66,7 +70,7 @@ def post(self, request, *args, **kwargs):
6670
serializer = CyberstormCreateTeamSerializer(data=request.data)
6771
serializer.is_valid(raise_exception=True)
6872
team_name = serializer.validated_data["name"]
69-
team = team_services.create_team(user=request.user, team_name=team_name)
73+
team = create_team(user=request.user, team_name=team_name)
7074
return_data = CyberstormTeamSerializer(team).data
7175
return Response(return_data, status=status.HTTP_201_CREATED)
7276

@@ -135,5 +139,32 @@ class DisbandTeamAPIView(APIView):
135139
)
136140
def delete(self, request, *args, **kwargs):
137141
team_name = kwargs["team_name"]
138-
team_services.disband_team(user=request.user, team_name=team_name)
142+
disband_team(user=request.user, team_name=team_name)
139143
return Response(status=status.HTTP_204_NO_CONTENT)
144+
145+
146+
class UpdateTeamAPIView(APIView):
147+
permission_classes = [IsAuthenticated]
148+
serializer_class = CyberstormTeamUpdateSerializer
149+
http_method_names = ["patch"]
150+
151+
@conditional_swagger_auto_schema(
152+
operation_id="cyberstorm.team.update",
153+
tags=["cyberstorm"],
154+
request_body=CyberstormTeamUpdateSerializer,
155+
responses={status.HTTP_200_OK: serializer_class},
156+
)
157+
def patch(self, request, team_name, *args, **kwargs):
158+
team = get_object_or_404(Team.objects.exclude(is_active=False), name=team_name)
159+
160+
serializer = self.serializer_class(data=request.data)
161+
serializer.is_valid(raise_exception=True)
162+
163+
updated_team = update_team(
164+
agent=request.user,
165+
team=team,
166+
donation_link=serializer.validated_data["donation_link"],
167+
)
168+
169+
return_data = self.serializer_class(instance=updated_team).data
170+
return Response(return_data, status=status.HTTP_200_OK)

django/thunderstore/api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
TeamMemberListAPIView,
2424
TeamServiceAccountListAPIView,
2525
UpdatePackageListingCategoriesAPIView,
26+
UpdateTeamAPIView,
2627
)
2728

2829
cyberstorm_urls = [
@@ -126,6 +127,11 @@
126127
TeamAPIView.as_view(),
127128
name="cyberstorm.team",
128129
),
130+
path(
131+
"team/<str:team_name>/update/",
132+
UpdateTeamAPIView.as_view(),
133+
name="cyberstorm.team.update",
134+
),
129135
path(
130136
"team/<str:team_name>/disband/",
131137
DisbandTeamAPIView.as_view(),

django/thunderstore/repository/forms/team.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.contrib.auth import get_user_model
55
from django.core.exceptions import ObjectDoesNotExist, ValidationError
66

7+
from thunderstore.api.cyberstorm.services.team import update_team
78
from thunderstore.core.exceptions import PermissionValidationError
89
from thunderstore.core.types import UserType
910
from thunderstore.repository.models import (
@@ -183,10 +184,20 @@ def __init__(self, user: UserType, *args, **kwargs):
183184
def clean(self):
184185
if not self.instance.pk:
185186
raise ValidationError("Missing team instance")
186-
self.instance.ensure_user_can_edit_info(self.user)
187187
return super().clean()
188188

189189
@transaction.atomic
190190
def save(self, **kwargs):
191-
self.instance.ensure_user_can_edit_info(self.user)
192-
return super().save(**kwargs)
191+
if self.errors:
192+
raise ValidationError(self.errors)
193+
194+
try:
195+
update_team(
196+
agent=self.user,
197+
team=self.instance,
198+
donation_link=self.cleaned_data["donation_link"],
199+
)
200+
except ValidationError as e:
201+
self.add_error(None, e)
202+
203+
return self.instance

0 commit comments

Comments
 (0)