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 unavailable status (TS-2276) #1085

Open
wants to merge 6 commits into
base: master
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
79 changes: 74 additions & 5 deletions django/thunderstore/api/cyberstorm/tests/test_package_listing.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from datetime import datetime
from typing import Optional
from unittest.mock import PropertyMock, patch
from unittest.mock import Mock, PropertyMock, patch

import pytest
from django.db import connection
from django.test.utils import CaptureQueriesContext
from rest_framework.test import APIClient

from thunderstore.api.cyberstorm.views.package_listing import (
Expand Down Expand Up @@ -302,9 +304,8 @@ def test_dependency_serializer__reads_is_active_from_correct_field(
dependency.package.save()
dependant.dependencies.set([dependency])

# community_identifier is normally added using annotations, but
# it's irrelavant for this test case.
dependency.community_identifier = "greendale"
dependency.version_is_unavailable = False

actual = DependencySerializer(dependency).data

Expand All @@ -313,10 +314,9 @@ def test_dependency_serializer__reads_is_active_from_correct_field(

@pytest.mark.django_db
def test_dependency_serializer__when_dependency_is_not_active__censors_icon_and_description() -> None:
# community_identifier is normally added using annotations, but
# it's irrelavant for this test case.
dependency = PackageVersionFactory()
dependency.community_identifier = "greendale"
dependency.version_is_unavailable = False

actual = DependencySerializer(dependency).data

Expand Down Expand Up @@ -369,3 +369,72 @@ def test_package_listing_is_removed(

assert "is_removed" in response_dependencies
assert response_dependencies["is_removed"] == return_val


@pytest.mark.django_db
@pytest.mark.parametrize("return_val", [True, False])
@patch("thunderstore.repository.models.package_version.PackageVersion.is_unavailable")
def test_package_listing_is_unavailable(
is_unavailable_func: Mock,
return_val: bool,
api_client: APIClient,
community: Community,
) -> None:
is_unavailable_func.return_value = return_val

package = "Mod"
target_ns = NamespaceFactory()

target_dependency = PackageListingFactory(
community_=community,
package_kwargs={"name": package, "namespace": target_ns},
)

target_package = PackageListingFactory(community_=community)
target_package.package.latest.dependencies.set(
[target_dependency.package.latest.id],
)

community_id = target_package.community.identifier
namespace = target_package.package.namespace.name
package_name = target_package.package.name

url = f"/api/cyberstorm/listing/{community_id}/{namespace}/{package_name}/"
response = api_client.get(url)
response_dependencies = response.json()["dependencies"][0]

assert "is_unavailable" in response_dependencies
assert response_dependencies["is_unavailable"] == return_val


@pytest.mark.django_db
def test_package_listing_query_count(
api_client: APIClient, community: Community
) -> None:
package = "Mod"
target_ns = NamespaceFactory()

target_dependencies = [
PackageListingFactory(
community_=community,
package_kwargs={"name": f"{package}_{i}", "namespace": target_ns},
)
for i in range(10)
]

target_package = PackageListingFactory(community_=community)
target_package.package.latest.dependencies.set(
[dep.package.latest.id for dep in target_dependencies],
)

community_id = target_package.community.identifier
namespace = target_package.package.namespace.name
package_name = target_package.package.name

url = f"/api/cyberstorm/listing/{community_id}/{namespace}/{package_name}/"

with CaptureQueriesContext(connection) as ctx:
response = api_client.get(url)

assert response.status_code == 200
assert len(ctx.captured_queries) < 20
11 changes: 11 additions & 0 deletions django/thunderstore/api/cyberstorm/views/package_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
CyberstormTeamMemberSerializer,
)
from thunderstore.api.utils import CyberstormAutoSchemaMixin
from thunderstore.community.models.community import Community
from thunderstore.community.models.package_listing import PackageListing
from thunderstore.repository.models.package import get_package_dependants
from thunderstore.repository.models.package_version import PackageVersion
Expand All @@ -45,6 +46,7 @@ class DependencySerializer(serializers.Serializer):
namespace = serializers.CharField(source="package.namespace.name")
version_number = serializers.CharField()
is_removed = serializers.BooleanField()
is_unavailable = serializers.SerializerMethodField()

def get_description(self, obj: PackageVersion) -> str:
return (
Expand All @@ -56,6 +58,11 @@ def get_description(self, obj: PackageVersion) -> str:
def get_icon_url(self, obj: PackageVersion) -> Optional[str]:
return obj.icon.url if obj.is_effectively_active else None

def get_is_unavailable(self, obj: PackageVersion) -> bool:
# Annotated result of PackageVersion.is_unavailable
# See get_custom_package_listing()
return obj.version_is_unavailable


class TeamSerializer(serializers.Serializer):
"""
Expand Down Expand Up @@ -182,10 +189,14 @@ def get_custom_package_listing(
package__name__iexact=package_name,
)

community = listing.community
version_is_unavailable = listing.package.latest.is_unavailable(community)

dependencies = (
listing.package.latest.dependencies.listed_in(community_id)
.annotate(
community_identifier=Value(community_id, CharField()),
version_is_unavailable=Value(version_is_unavailable, BooleanField()),
)
.select_related("package", "package__namespace")
.order_by("package__namespace__name", "package__name")
Expand Down
4 changes: 4 additions & 0 deletions django/thunderstore/community/models/package_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,10 @@ def update_categories(self, agent: UserType, categories: List["PackageCategory"]
)
self.categories.set(categories)

@property
def is_unavailable(self):
return self.is_rejected or self.is_waiting_for_approval

def can_be_moderated_by_user(self, user: Optional[UserType]) -> bool:
return self.community.can_user_manage_packages(user)

Expand Down
26 changes: 26 additions & 0 deletions django/thunderstore/community/tests/test_package_listing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import PropertyMock, patch

import pytest
from django.core.exceptions import ValidationError
from django.db import IntegrityError
Expand Down Expand Up @@ -416,3 +418,27 @@ def test_package_listing_filter_with_multiple_community_packages() -> None:
count = community.package_listings.filter_with_single_community().count()
assert count == 1
assert community.aggregated.package_count == count


@pytest.mark.django_db
@pytest.mark.parametrize(
("is_rejected", "is_waiting_for_approval", "expected"),
[(True, False, True), (False, True, True), (False, False, False)],
)
def test_package_listing_is_unavailable(
is_rejected: bool,
is_waiting_for_approval: bool,
expected: bool,
) -> None:
with patch(
"thunderstore.community.models.PackageListing.is_rejected",
new_callable=PropertyMock,
return_value=is_rejected,
), patch(
"thunderstore.community.models.PackageListing.is_waiting_for_approval",
new_callable=PropertyMock,
return_value=is_waiting_for_approval,
):

listing = PackageListingFactory()
assert listing.is_unavailable == expected
7 changes: 7 additions & 0 deletions django/thunderstore/repository/models/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ def update_listing(self, has_nsfw_content, categories, community):
listing.categories.add(*categories)
listing.save(update_fields=("has_nsfw_content",))

def is_unavailable(self, community) -> bool:
if not self.is_effectively_active:
return True

listing = self.get_package_listing(community)
return listing is None or listing.is_unavailable

@cached_property
def has_wiki(self) -> bool:
try:
Expand Down
3 changes: 3 additions & 0 deletions django/thunderstore/repository/models/package_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ def get_page_url(self, community_identifier: str) -> str:
},
)

def is_unavailable(self, community) -> bool:
return self.package.is_unavailable(community) or not self.is_active

@cached_property
def is_removed(self):
if self.package.is_removed:
Expand Down
72 changes: 71 additions & 1 deletion django/thunderstore/repository/tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
from django.core.exceptions import ValidationError

from conftest import TestUserTypes
from thunderstore.community.factories import PackageCategoryFactory, SiteFactory
from thunderstore.community.consts import PackageListingReviewStatus
from thunderstore.community.factories import (
CommunityFactory,
PackageCategoryFactory,
PackageListingFactory,
SiteFactory,
)
from thunderstore.community.models.package_listing import PackageListing
from thunderstore.core.types import UserType
from thunderstore.repository.factories import PackageFactory, PackageVersionFactory
Expand Down Expand Up @@ -245,3 +251,67 @@ def test_package_is_removed(
PackageVersionFactory(package=package, is_active=version_is_active)

assert package.is_removed == expected_is_removed


@pytest.mark.django_db
@pytest.mark.parametrize(
(
"review_status",
"package_is_active",
"require_package_listing_approval",
"expected_is_unavailable_result",
),
[
(PackageListingReviewStatus.approved, True, False, False),
(PackageListingReviewStatus.approved, False, False, True),
(PackageListingReviewStatus.rejected, True, False, True),
(PackageListingReviewStatus.rejected, False, False, True),
(PackageListingReviewStatus.unreviewed, True, True, True),
(PackageListingReviewStatus.unreviewed, True, False, False),
(PackageListingReviewStatus.unreviewed, False, True, True),
(PackageListingReviewStatus.unreviewed, False, False, True),
],
)
def test_package_is_unavailable(
review_status: PackageListingReviewStatus,
package_is_active: bool,
require_package_listing_approval: bool,
expected_is_unavailable_result: bool,
) -> None:
community = CommunityFactory(
require_package_listing_approval=require_package_listing_approval
)

package = PackageFactory(is_active=package_is_active)
if package_is_active:
PackageVersionFactory(package=package, version_number="1.0.0")

PackageListingFactory(
package_=package,
community_=community,
review_status=review_status,
)

assert package.is_unavailable(community) == expected_is_unavailable_result


@pytest.mark.django_db
def test_package_is_unavailable_no_listing() -> None:
community = CommunityFactory()
package = PackageFactory(is_active=True)
PackageVersionFactory(package=package, version_number="1.0.0")

assert package.is_unavailable(community) is True


@pytest.mark.django_db
def test_package_is_unavailable_no_version() -> None:
community = CommunityFactory()
package = PackageFactory(is_active=True)
PackageListingFactory(
package_=package,
community_=community,
review_status=PackageListingReviewStatus.approved,
)

assert package.is_unavailable(community) is True
25 changes: 24 additions & 1 deletion django/thunderstore/repository/tests/test_package_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
from django.db import IntegrityError

from thunderstore.community.factories import PackageListingFactory
from thunderstore.community.factories import CommunityFactory, PackageListingFactory
from thunderstore.community.models.package_listing import PackageListing
from thunderstore.repository.factories import PackageFactory, PackageVersionFactory
from thunderstore.repository.models import PackageVersion
Expand Down Expand Up @@ -150,3 +150,26 @@ def test_package_version_is_removed(
version = PackageVersionFactory(package=package, is_active=version_is_active)

assert version.is_removed == expected_is_removed


@pytest.mark.django_db
@pytest.mark.parametrize(
("package_is_unavailable", "version_is_active", "expected_is_unavailable"),
[
(True, True, True),
(True, False, True),
(False, True, False),
(False, False, True),
],
)
def test_package_version_is_unavailable(
package_is_unavailable: bool,
version_is_active: bool,
expected_is_unavailable: bool,
) -> None:
community = CommunityFactory()
package = PackageFactory()
package.is_unavailable = lambda _: package_is_unavailable
version = PackageVersionFactory(package=package, is_active=version_is_active)

assert version.is_unavailable(community) == expected_is_unavailable
Loading