Skip to content

Commit 72742f3

Browse files
committed
Add CS PackageRating rate form and view
1 parent f01c509 commit 72742f3

File tree

8 files changed

+359
-1
lines changed

8 files changed

+359
-1
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import json
2+
3+
import pytest
4+
from rest_framework.test import APIClient
5+
6+
from thunderstore.core.types import UserType
7+
from thunderstore.repository.models import Package
8+
9+
10+
@pytest.mark.django_db
11+
def test_package_rating_api_view__succeeds(
12+
api_client: APIClient,
13+
package: Package,
14+
user: UserType,
15+
) -> None:
16+
api_client.force_authenticate(user)
17+
18+
response = api_client.post(
19+
f"/api/cyberstorm/package/{package.namespace}/{package.name}/rate/",
20+
json.dumps({"target_state": "rated"}),
21+
content_type="application/json",
22+
)
23+
actual = response.json()
24+
25+
assert actual["state"] == "rated"
26+
assert actual["score"] == 1
27+
28+
response = api_client.post(
29+
f"/api/cyberstorm/package/{package.namespace}/{package.name}/rate/",
30+
json.dumps({"target_state": "unrated"}),
31+
content_type="application/json",
32+
)
33+
actual = response.json()
34+
35+
assert actual["state"] == "unrated"
36+
assert actual["score"] == 0
37+
38+
39+
@pytest.mark.django_db
40+
def test_package_rating_api_view__returns_error_for_non_existent_package(
41+
api_client: APIClient,
42+
user: UserType,
43+
) -> None:
44+
api_client.force_authenticate(user)
45+
response = api_client.post(
46+
f"/api/cyberstorm/package/BAD/BAD/rate/",
47+
json.dumps({"target_state": "rated"}),
48+
content_type="application/json",
49+
)
50+
actual = response.json()
51+
52+
assert actual["detail"] == "Not found."
53+
54+
55+
@pytest.mark.django_db
56+
def test_package_rating_api_view__returns_error_for_no_user(
57+
api_client: APIClient,
58+
package: Package,
59+
) -> None:
60+
response = api_client.post(
61+
f"/api/cyberstorm/package/{package.namespace}/{package.name}/rate/",
62+
json.dumps({"target_state": "rated"}),
63+
content_type="application/json",
64+
)
65+
actual = response.json()
66+
67+
assert actual["detail"] == "Authentication credentials were not provided."
68+
69+
70+
@pytest.mark.django_db
71+
def test_package_rating_api_view__returns_error_for_bad_data(
72+
api_client: APIClient,
73+
package: Package,
74+
user: UserType,
75+
) -> None:
76+
api_client.force_authenticate(user)
77+
package.is_active = False
78+
package.save()
79+
80+
response = api_client.post(
81+
f"/api/cyberstorm/package/{package.namespace}/{package.name}/rate/",
82+
json.dumps({"bad_data": "rated"}),
83+
content_type="application/json",
84+
)
85+
actual = response.json()
86+
87+
assert actual["target_state"] == ["This field is required."]
88+
89+
response = api_client.post(
90+
f"/api/cyberstorm/package/{package.namespace}/{package.name}/rate/",
91+
json.dumps({"target_state": "bad"}),
92+
content_type="application/json",
93+
)
94+
actual = response.json()
95+
96+
assert actual["__all__"] == ["Given target_state is invalid"]

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
PackageListingByDependencyListAPIView,
99
PackageListingByNamespaceListAPIView,
1010
)
11+
from .package_rating import PackageRatingRateAPIView
1112
from .package_version_list import PackageVersionListAPIView
1213
from .team import (
1314
TeamAPIView,
@@ -31,4 +32,5 @@
3132
"TeamMemberAddAPIView",
3233
"TeamMemberListAPIView",
3334
"TeamServiceAccountListAPIView",
35+
"PackageRatingRateAPIView",
3436
]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from django.http import HttpRequest
2+
from rest_framework import serializers
3+
from rest_framework.exceptions import ValidationError
4+
from rest_framework.generics import get_object_or_404
5+
from rest_framework.permissions import IsAuthenticated
6+
from rest_framework.response import Response
7+
from rest_framework.views import APIView
8+
9+
from thunderstore.api.utils import conditional_swagger_auto_schema
10+
from thunderstore.repository.forms import RateForm
11+
from thunderstore.repository.models import Package
12+
13+
14+
class CyberstormPackageRatingRateRequestSerialiazer(serializers.Serializer):
15+
target_state = serializers.CharField()
16+
17+
18+
class CyberstormPackageRatingRateResponseSerialiazer(serializers.Serializer):
19+
state = serializers.CharField()
20+
score = serializers.IntegerField()
21+
22+
23+
class PackageRatingRateAPIView(APIView):
24+
permission_classes = [IsAuthenticated]
25+
26+
@conditional_swagger_auto_schema(
27+
request_body=CyberstormPackageRatingRateRequestSerialiazer,
28+
responses={200: CyberstormPackageRatingRateResponseSerialiazer},
29+
operation_id="cyberstorm.package_rating.rate",
30+
tags=["cyberstorm"],
31+
)
32+
def post(self, request: HttpRequest, namespace_id: str, package_name: str):
33+
serializer = CyberstormPackageRatingRateRequestSerialiazer(data=request.data)
34+
serializer.is_valid(raise_exception=True)
35+
package = get_object_or_404(
36+
Package,
37+
namespace__name=namespace_id,
38+
name__iexact=package_name,
39+
)
40+
form = RateForm(
41+
user=request.user,
42+
package=package,
43+
data=serializer.validated_data,
44+
)
45+
if form.is_valid():
46+
(result_state, score) = form.execute()
47+
return Response(
48+
CyberstormPackageRatingRateResponseSerialiazer(
49+
{"state": result_state, "score": score}
50+
).data
51+
)
52+
else:
53+
raise ValidationError(form.errors)

django/thunderstore/api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
PackageListingByCommunityListAPIView,
99
PackageListingByDependencyListAPIView,
1010
PackageListingByNamespaceListAPIView,
11+
PackageRatingRateAPIView,
1112
PackageVersionChangelogAPIView,
1213
PackageVersionListAPIView,
1314
PackageVersionReadmeAPIView,
@@ -78,6 +79,11 @@
7879
PackageVersionListAPIView.as_view(),
7980
name="cyberstorm.package.versions",
8081
),
82+
path(
83+
"package/<str:namespace_id>/<str:package_name>/rate/",
84+
PackageRatingRateAPIView.as_view(),
85+
name="cyberstorm.package.deprecate",
86+
),
8187
path(
8288
"team/<str:team_id>/",
8389
TeamAPIView.as_view(),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from .package_rating import *
12
from .team import *
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from typing import Optional
2+
3+
from django import forms
4+
from django.contrib.auth import get_user_model
5+
6+
from thunderstore.core.types import UserType
7+
from thunderstore.repository.models import Package, PackageRating
8+
from thunderstore.repository.permissions import ensure_can_rate_package
9+
10+
User = get_user_model()
11+
12+
13+
class RateForm(forms.Form):
14+
def __init__(self, user: Optional[UserType], package: Package, *args, **kwargs):
15+
super().__init__(*args, **kwargs)
16+
self.user = user
17+
self.package = package
18+
19+
def clean(self):
20+
ensure_can_rate_package(self.user, self.package)
21+
target_state = self.data.get("target_state", None)
22+
if target_state == "rated":
23+
self.cleaned_data["target_state"] = "rated"
24+
elif target_state == "unrated":
25+
self.cleaned_data["target_state"] = "unrated"
26+
else:
27+
raise forms.ValidationError("Given target_state is invalid")
28+
29+
return super().clean()
30+
31+
def execute(self):
32+
if self.cleaned_data["target_state"] == "rated":
33+
PackageRating.objects.get_or_create(rater=self.user, package=self.package)
34+
result_state = "rated"
35+
else:
36+
PackageRating.objects.filter(rater=self.user, package=self.package).delete()
37+
result_state = "unrated"
38+
return (result_state, self.package.rating_score)
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import pytest
2+
from rest_framework.exceptions import PermissionDenied
3+
4+
from thunderstore.core.types import UserType
5+
from thunderstore.repository.forms.package_rating import RateForm
6+
from thunderstore.repository.models import Package
7+
from thunderstore.repository.models.package_rating import PackageRating
8+
9+
10+
@pytest.mark.django_db
11+
def test_package_rating_form__correct_values__succeeds(
12+
user: UserType, package: Package
13+
) -> None:
14+
# Rated
15+
p = Package.objects.get(pk=package.pk)
16+
form = RateForm(
17+
user=user,
18+
package=p,
19+
data={"target_state": "rated"},
20+
)
21+
assert form.is_valid() is True
22+
(result_state, score) = form.execute()
23+
assert len(PackageRating.objects.filter(rater=user, package=package)) == 1
24+
assert result_state == "rated"
25+
assert score == 1
26+
# Unrated
27+
p = Package.objects.get(pk=package.pk)
28+
form = RateForm(
29+
user=user,
30+
package=p,
31+
data={"target_state": "unrated"},
32+
)
33+
assert form.is_valid() is True
34+
(result_state, score) = form.execute()
35+
assert len(PackageRating.objects.filter(rater=user, package=package)) == 0
36+
assert result_state == "unrated"
37+
assert score == 0
38+
39+
40+
@pytest.mark.django_db
41+
def test_package_rating_form__already_on_state__succeeds(
42+
user: UserType, package: Package
43+
) -> None:
44+
# Rated
45+
p = Package.objects.get(pk=package.pk)
46+
form = RateForm(
47+
user=user,
48+
package=p,
49+
data={"target_state": "rated"},
50+
)
51+
assert form.is_valid() is True
52+
(result_state, score) = form.execute()
53+
assert len(PackageRating.objects.filter(rater=user, package=package)) == 1
54+
assert result_state == "rated"
55+
assert score == 1
56+
# Second time
57+
p = Package.objects.get(pk=package.pk)
58+
form = RateForm(
59+
user=user,
60+
package=p,
61+
data={"target_state": "rated"},
62+
)
63+
assert form.is_valid() is True
64+
(result_state, score) = form.execute()
65+
assert len(PackageRating.objects.filter(rater=user, package=package)) == 1
66+
assert result_state == "rated"
67+
assert score == 1
68+
# Unrated
69+
p = Package.objects.get(pk=package.pk)
70+
form = RateForm(
71+
user=user,
72+
package=p,
73+
data={"target_state": "unrated"},
74+
)
75+
assert form.is_valid() is True
76+
(result_state, score) = form.execute()
77+
assert len(PackageRating.objects.filter(rater=user, package=package)) == 0
78+
assert result_state == "unrated"
79+
assert score == 0
80+
# Second time
81+
p = Package.objects.get(pk=package.pk)
82+
form = RateForm(
83+
user=user,
84+
package=p,
85+
data={"target_state": "unrated"},
86+
)
87+
assert form.is_valid() is True
88+
(result_state, score) = form.execute()
89+
assert len(PackageRating.objects.filter(rater=user, package=package)) == 0
90+
assert result_state == "unrated"
91+
assert score == 0
92+
93+
94+
@pytest.mark.django_db
95+
def test_package_rating_form__bad_target_state__fails(
96+
user: UserType, package: Package
97+
) -> None:
98+
error = "Given target_state is invalid"
99+
form = RateForm(
100+
user=user,
101+
package=package,
102+
data={"target_state": "bad"},
103+
)
104+
assert form.is_valid() is False
105+
assert error in str(repr(form.errors))
106+
107+
108+
@pytest.mark.django_db
109+
def test_package_rating_form__user_none__fails(
110+
package: Package,
111+
) -> None:
112+
form = RateForm(
113+
user=None,
114+
package=package,
115+
data={"target_state": "rated"},
116+
)
117+
with pytest.raises(PermissionDenied) as e:
118+
form.is_valid()
119+
assert "Must be authenticated" in str(e.value)
120+
121+
122+
@pytest.mark.django_db
123+
def test_package_rating_form__user_deactivated__fails(
124+
user: UserType, package: Package
125+
) -> None:
126+
user.is_active = False
127+
user.save()
128+
form = RateForm(
129+
user=user,
130+
package=package,
131+
data={"target_state": "rated"},
132+
)
133+
with pytest.raises(PermissionDenied) as e:
134+
form.is_valid()
135+
assert "User has been deactivated" in str(e.value)
136+
137+
138+
@pytest.mark.django_db
139+
def test_package_rating_form__user_is_service_account__fails(
140+
service_account: UserType, package: Package
141+
) -> None:
142+
form = RateForm(
143+
user=service_account.user,
144+
package=package,
145+
data={"target_state": "rated"},
146+
)
147+
with pytest.raises(PermissionDenied) as e:
148+
form.is_valid()
149+
assert "Service accounts are unable to perform this action" in str(e.value)

0 commit comments

Comments
 (0)