Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CS PackageRating rate form and view #1023

Open
wants to merge 2 commits into
base: 06-24-Make_all_DRF_endpoints_require_Auth_by_default
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_rating.rate",
),
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
Loading