Skip to content

Commit

Permalink
Add CS PackageRating rate form and view
Browse files Browse the repository at this point in the history
  • Loading branch information
Oksamies committed Sep 10, 2024
1 parent 5680776 commit c0f231d
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 1 deletion.
96 changes: 96 additions & 0 deletions django/thunderstore/api/cyberstorm/tests/test_package_rating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import json

import pytest
from rest_framework.test import APIClient

from thunderstore.core.types import UserType
from thunderstore.repository.models import Package


@pytest.mark.django_db
def test_package_rating_api_view__succeeds(
api_client: APIClient,
package: Package,
user: UserType,
) -> None:
api_client.force_authenticate(user)

response = api_client.post(
f"/api/cyberstorm/package/{package.namespace}/{package.name}/rate/",
json.dumps({"target_state": "rated"}),
content_type="application/json",
)
actual = response.json()

assert actual["state"] == "rated"
assert actual["score"] == 1

response = api_client.post(
f"/api/cyberstorm/package/{package.namespace}/{package.name}/rate/",
json.dumps({"target_state": "unrated"}),
content_type="application/json",
)
actual = response.json()

assert actual["state"] == "unrated"
assert actual["score"] == 0


@pytest.mark.django_db
def test_package_rating_api_view__returns_error_for_non_existent_package(
api_client: APIClient,
user: UserType,
) -> None:
api_client.force_authenticate(user)
response = api_client.post(
f"/api/cyberstorm/package/BAD/BAD/rate/",
json.dumps({"target_state": "rated"}),
content_type="application/json",
)
actual = response.json()

assert actual["detail"] == "Not found."


@pytest.mark.django_db
def test_package_rating_api_view__returns_error_for_no_user(
api_client: APIClient,
package: Package,
) -> None:
response = api_client.post(
f"/api/cyberstorm/package/{package.namespace}/{package.name}/rate/",
json.dumps({"target_state": "rated"}),
content_type="application/json",
)
actual = response.json()

assert actual["detail"] == "Authentication credentials were not provided."


@pytest.mark.django_db
def test_package_rating_api_view__returns_error_for_bad_data(
api_client: APIClient,
package: Package,
user: UserType,
) -> None:
api_client.force_authenticate(user)
package.is_active = False
package.save()

response = api_client.post(
f"/api/cyberstorm/package/{package.namespace}/{package.name}/rate/",
json.dumps({"bad_data": "rated"}),
content_type="application/json",
)
actual = response.json()

assert actual["target_state"] == ["This field is required."]

response = api_client.post(
f"/api/cyberstorm/package/{package.namespace}/{package.name}/rate/",
json.dumps({"target_state": "bad"}),
content_type="application/json",
)
actual = response.json()

assert actual["__all__"] == ["Given target_state is invalid"]
2 changes: 2 additions & 0 deletions django/thunderstore/api/cyberstorm/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
PackageListingByDependencyListAPIView,
PackageListingByNamespaceListAPIView,
)
from .package_rating import PackageRatingRateAPIView
from .package_version_list import PackageVersionListAPIView
from .team import (
TeamAPIView,
Expand All @@ -31,4 +32,5 @@
"TeamMemberAddAPIView",
"TeamMemberListAPIView",
"TeamServiceAccountListAPIView",
"PackageRatingRateAPIView",
]
53 changes: 53 additions & 0 deletions django/thunderstore/api/cyberstorm/views/package_rating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from django.http import HttpRequest
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from thunderstore.api.utils import conditional_swagger_auto_schema
from thunderstore.repository.forms import RateForm
from thunderstore.repository.models import Package


class CyberstormPackageRatingRateRequestSerialiazer(serializers.Serializer):
target_state = serializers.CharField()


class CyberstormPackageRatingRateResponseSerialiazer(serializers.Serializer):
state = serializers.CharField()
score = serializers.IntegerField()


class PackageRatingRateAPIView(APIView):
permission_classes = [IsAuthenticated]

@conditional_swagger_auto_schema(
request_body=CyberstormPackageRatingRateRequestSerialiazer,
responses={200: CyberstormPackageRatingRateResponseSerialiazer},
operation_id="cyberstorm.package_rating.rate",
tags=["cyberstorm"],
)
def post(self, request: HttpRequest, namespace_id: str, package_name: str):
serializer = CyberstormPackageRatingRateRequestSerialiazer(data=request.data)
serializer.is_valid(raise_exception=True)
package = get_object_or_404(
Package,
namespace__name=namespace_id,
name__iexact=package_name,
)
form = RateForm(
user=request.user,
package=package,
data=serializer.validated_data,
)
if form.is_valid():
(result_state, score) = form.execute()
return Response(
CyberstormPackageRatingRateResponseSerialiazer(
{"state": result_state, "score": score}
).data
)
else:
raise ValidationError(form.errors)
6 changes: 6 additions & 0 deletions django/thunderstore/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
PackageListingByCommunityListAPIView,
PackageListingByDependencyListAPIView,
PackageListingByNamespaceListAPIView,
PackageRatingRateAPIView,
PackageVersionChangelogAPIView,
PackageVersionListAPIView,
PackageVersionReadmeAPIView,
Expand Down Expand Up @@ -78,6 +79,11 @@
PackageVersionListAPIView.as_view(),
name="cyberstorm.package.versions",
),
path(
"package/<str:namespace_id>/<str:package_name>/rate/",
PackageRatingRateAPIView.as_view(),
name="cyberstorm.package.deprecate",
),
path(
"team/<str:team_id>/",
TeamAPIView.as_view(),
Expand Down
1 change: 1 addition & 0 deletions django/thunderstore/repository/forms/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .package_rating import *
from .team import *
38 changes: 38 additions & 0 deletions django/thunderstore/repository/forms/package_rating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Optional

from django import forms
from django.contrib.auth import get_user_model

from thunderstore.core.types import UserType
from thunderstore.repository.models import Package, PackageRating
from thunderstore.repository.permissions import ensure_can_rate_package

User = get_user_model()


class RateForm(forms.Form):
def __init__(self, user: Optional[UserType], package: Package, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
self.package = package

def clean(self):
ensure_can_rate_package(self.user, self.package)
target_state = self.data.get("target_state", None)
if target_state == "rated":
self.cleaned_data["target_state"] = "rated"
elif target_state == "unrated":
self.cleaned_data["target_state"] = "unrated"
else:
raise forms.ValidationError("Given target_state is invalid")

return super().clean()

def execute(self):
if self.cleaned_data["target_state"] == "rated":
PackageRating.objects.get_or_create(rater=self.user, package=self.package)
result_state = "rated"
else:
PackageRating.objects.filter(rater=self.user, package=self.package).delete()
result_state = "unrated"
return (result_state, self.package.rating_score)
149 changes: 149 additions & 0 deletions django/thunderstore/repository/tests/test_package_rating_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import pytest
from rest_framework.exceptions import PermissionDenied

from thunderstore.core.types import UserType
from thunderstore.repository.forms.package_rating import RateForm
from thunderstore.repository.models import Package
from thunderstore.repository.models.package_rating import PackageRating


@pytest.mark.django_db
def test_package_rating_form__correct_values__succeeds(
user: UserType, package: Package
) -> None:
# Rated
p = Package.objects.get(pk=package.pk)
form = RateForm(
user=user,
package=p,
data={"target_state": "rated"},
)
assert form.is_valid() is True
(result_state, score) = form.execute()
assert len(PackageRating.objects.filter(rater=user, package=package)) == 1
assert result_state == "rated"
assert score == 1
# Unrated
p = Package.objects.get(pk=package.pk)
form = RateForm(
user=user,
package=p,
data={"target_state": "unrated"},
)
assert form.is_valid() is True
(result_state, score) = form.execute()
assert len(PackageRating.objects.filter(rater=user, package=package)) == 0
assert result_state == "unrated"
assert score == 0


@pytest.mark.django_db
def test_package_rating_form__already_on_state__succeeds(
user: UserType, package: Package
) -> None:
# Rated
p = Package.objects.get(pk=package.pk)
form = RateForm(
user=user,
package=p,
data={"target_state": "rated"},
)
assert form.is_valid() is True
(result_state, score) = form.execute()
assert len(PackageRating.objects.filter(rater=user, package=package)) == 1
assert result_state == "rated"
assert score == 1
# Second time
p = Package.objects.get(pk=package.pk)
form = RateForm(
user=user,
package=p,
data={"target_state": "rated"},
)
assert form.is_valid() is True
(result_state, score) = form.execute()
assert len(PackageRating.objects.filter(rater=user, package=package)) == 1
assert result_state == "rated"
assert score == 1
# Unrated
p = Package.objects.get(pk=package.pk)
form = RateForm(
user=user,
package=p,
data={"target_state": "unrated"},
)
assert form.is_valid() is True
(result_state, score) = form.execute()
assert len(PackageRating.objects.filter(rater=user, package=package)) == 0
assert result_state == "unrated"
assert score == 0
# Second time
p = Package.objects.get(pk=package.pk)
form = RateForm(
user=user,
package=p,
data={"target_state": "unrated"},
)
assert form.is_valid() is True
(result_state, score) = form.execute()
assert len(PackageRating.objects.filter(rater=user, package=package)) == 0
assert result_state == "unrated"
assert score == 0


@pytest.mark.django_db
def test_package_rating_form__bad_target_state__fails(
user: UserType, package: Package
) -> None:
error = "Given target_state is invalid"
form = RateForm(
user=user,
package=package,
data={"target_state": "bad"},
)
assert form.is_valid() is False
assert error in str(repr(form.errors))


@pytest.mark.django_db
def test_package_rating_form__user_none__fails(
package: Package,
) -> None:
form = RateForm(
user=None,
package=package,
data={"target_state": "rated"},
)
with pytest.raises(PermissionDenied) as e:
form.is_valid()
assert "Must be authenticated" in str(e.value)


@pytest.mark.django_db
def test_package_rating_form__user_deactivated__fails(
user: UserType, package: Package
) -> None:
user.is_active = False
user.save()
form = RateForm(
user=user,
package=package,
data={"target_state": "rated"},
)
with pytest.raises(PermissionDenied) as e:
form.is_valid()
assert "User has been deactivated" in str(e.value)


@pytest.mark.django_db
def test_package_rating_form__user_is_service_account__fails(
service_account: UserType, package: Package
) -> None:
form = RateForm(
user=service_account.user,
package=package,
data={"target_state": "rated"},
)
with pytest.raises(PermissionDenied) as e:
form.is_valid()
assert "Service accounts are unable to perform this action" in str(e.value)
Loading

0 comments on commit c0f231d

Please sign in to comment.