Skip to content

Implement Visibility for Listings / Versions #1112

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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
36 changes: 26 additions & 10 deletions django/thunderstore/community/admin/package_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
@transaction.atomic
def reject_listing(modeladmin, request, queryset: QuerySet[PackageListing]):
for listing in queryset:
listing.review_status = PackageListingReviewStatus.rejected
listing.save(update_fields=("review_status",))
listing.reject(
agent=request.user, rejection_reason="Invalid submission", is_system=False
)


reject_listing.short_description = "Reject"
Expand All @@ -26,8 +27,7 @@ def reject_listing(modeladmin, request, queryset: QuerySet[PackageListing]):
@transaction.atomic
def approve_listing(modeladmin, request, queryset: QuerySet[PackageListing]):
for listing in queryset:
listing.review_status = PackageListingReviewStatus.approved
listing.save(update_fields=("review_status",))
listing.approve(agent=request.user, is_system=False)


approve_listing.short_description = "Approve"
Expand All @@ -52,6 +52,28 @@ class PackageListingAdmin(admin.ModelAdmin):
approve_listing,
reject_listing,
)

fields = (
"categories",
"is_review_requested",
"review_status",
"rejection_reason",
"notes",
"has_nsfw_content",
"is_auto_imported",
"package_link",
"community",
"datetime_created",
"datetime_updated",
"visibility",
)
readonly_fields = (
"package_link",
"community",
"datetime_created",
"datetime_updated",
"visibility",
)
filter_horizontal = ("categories",)
raw_id_fields = ("package", "community")
list_filter = (
Expand Down Expand Up @@ -85,12 +107,6 @@ class PackageListingAdmin(admin.ModelAdmin):
"package__namespace",
"community",
)
readonly_fields = (
"package_link",
"community",
"datetime_created",
"datetime_updated",
)
exclude = ("package",)

def package_link(self, obj):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 3.1.7 on 2025-03-06 21:33

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("permissions", "0001_initial"),
("community", "0035_add_janitor_role"),
]

operations = [
migrations.AddField(
model_name="packagelisting",
name="visibility",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="permissions.visibilityflags",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from django.db import migrations

from thunderstore.community.consts import PackageListingReviewStatus


def create_default_visibility_for_existing_records(apps, schema_editor):
PackageListing = apps.get_model("community", "PackageListing")
VisibilityFlags = apps.get_model("permissions", "VisibilityFlags")

for instance in PackageListing.objects.filter(visibility__isnull=True):
visibility_flags = VisibilityFlags.objects.create(
public_list=False,
public_detail=False,
owner_list=True,
owner_detail=True,
moderator_list=True,
moderator_detail=True,
admin_list=True,
admin_detail=True,
)
instance.visibility = visibility_flags
instance.save()
update_visibility(instance)


def update_visibility(listing):
package = listing.package
listing.visibility.public_detail = package.visibility.public_detail
listing.visibility.public_list = package.visibility.public_list
listing.visibility.owner_detail = package.visibility.owner_detail
listing.visibility.owner_list = package.visibility.owner_list
listing.visibility.moderator_detail = package.visibility.moderator_detail
listing.visibility.moderator_list = package.visibility.moderator_list
listing.visibility.admin_detail = package.visibility.admin_detail
listing.visibility.admin_list = package.visibility.admin_list

if listing.review_status == PackageListingReviewStatus.rejected:
listing.visibility.public_detail = False
listing.visibility.public_list = False

if (
listing.community.require_package_listing_approval
and listing.review_status == PackageListingReviewStatus.unreviewed
):
listing.visibility.public_detail = False
listing.visibility.public_list = False

listing.visibility.save()


class Migration(migrations.Migration):

dependencies = [
("community", "0036_packagelisting_visibility"),
("repository", "0058_package_visibility"),
("repository", "0059_create_default_visibility_for_existing_records"),
]

operations = [
migrations.RunPython(
create_default_visibility_for_existing_records, migrations.RunPython.noop
),
]
60 changes: 56 additions & 4 deletions django/thunderstore/community/models/package_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@
from thunderstore.core.types import UserType
from thunderstore.core.utils import check_validity
from thunderstore.frontend.url_reverse import get_community_url_reverse_args
from thunderstore.permissions.mixins import VisibilityMixin
from thunderstore.permissions.models.visibility import VisibilityFlagsQuerySet
from thunderstore.permissions.utils import validate_user
from thunderstore.webhooks.audit import (
AuditAction,
AuditEvent,
AuditEventField,
AuditTarget,
fire_audit_event,
)

if TYPE_CHECKING:
from thunderstore.community.models import PackageCategory


class PackageListingQueryset(models.QuerySet):
class PackageListingQueryset(VisibilityFlagsQuerySet):
def active(self):
return self.exclude(package__is_active=False).exclude(
~Q(package__versions__is_active=True)
Expand Down Expand Up @@ -55,7 +58,7 @@
# TODO: Add a db constraint that ensures a package listing and it's categories
# belong to the same community. This might require actually specifying
# the intermediate model in code rather than letting Django handle it
class PackageListing(TimestampMixin, AdminLinkMixin, models.Model):
class PackageListing(TimestampMixin, AdminLinkMixin, VisibilityMixin):
"""
Represents a package's relation to how it's displayed on the site and APIs
"""
Expand Down Expand Up @@ -166,6 +169,7 @@
def build_audit_event(
self,
*,
target: AuditTarget,
action: AuditAction,
user_id: Optional[int],
message: Optional[str] = None,
Expand All @@ -174,6 +178,7 @@
timestamp=timezone.now(),
user_id=user_id,
community_id=self.community.pk,
target=target,
action=action,
message=message,
related_url=self.get_full_url(),
Expand Down Expand Up @@ -221,7 +226,8 @@
message = "\n\n".join(filter(bool, (rejection_reason, internal_notes)))
fire_audit_event(
self.build_audit_event(
action=AuditAction.PACKAGE_REJECTED,
target=AuditTarget.LISTING,
action=AuditAction.REJECTED,
user_id=agent.pk if agent else None,
message=message,
)
Expand All @@ -247,7 +253,8 @@
)
fire_audit_event(
self.build_audit_event(
action=AuditAction.PACKAGE_APPROVED,
target=AuditTarget.LISTING,
action=AuditAction.APPROVED,
user_id=agent.pk if agent else None,
message=internal_notes,
)
Expand Down Expand Up @@ -378,6 +385,51 @@
def can_be_viewed_by_user(self, user: Optional[UserType]) -> bool:
return check_validity(lambda: self.ensure_can_be_viewed_by_user(user))

def is_visible_to_user(self, user: Optional[UserType]) -> bool:
if not self.visibility:
return False

Check warning on line 390 in django/thunderstore/community/models/package_listing.py

View check run for this annotation

Codecov / codecov/patch

django/thunderstore/community/models/package_listing.py#L390

Added line #L390 was not covered by tests

if self.visibility.public_detail:
return True

if user is None:
return False

if self.visibility.owner_detail:
if self.package.owner.can_user_access(user):
return True

if self.visibility.moderator_detail:
for listing in self.package.community_listings.all():
if listing.community.can_user_manage_packages(user):
return True

if self.visibility.admin_detail:
if user.is_superuser:
return True

return False

def set_visibility_from_review_status(self):
if self.review_status == PackageListingReviewStatus.rejected:
self.visibility.public_detail = False
self.visibility.public_list = False

if (
self.community.require_package_listing_approval
and self.review_status == PackageListingReviewStatus.unreviewed
):
self.visibility.public_detail = False
self.visibility.public_list = False

@transaction.atomic
def update_visibility(self):
self.visibility.copy_from(self.package.visibility)

self.set_visibility_from_review_status()

self.visibility.save()


signals.post_save.connect(PackageListing.post_save, sender=PackageListing)
signals.post_delete.connect(PackageListing.post_delete, sender=PackageListing)
14 changes: 12 additions & 2 deletions django/thunderstore/community/tests/test_admin_package_listing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from django.test import RequestFactory

from thunderstore.community.admin.package_listing import (
PackageListingAdmin,
Expand All @@ -7,6 +8,7 @@
)
from thunderstore.community.consts import PackageListingReviewStatus
from thunderstore.community.models import Community, PackageListing
from thunderstore.core.factories import UserFactory
from thunderstore.repository.factories import NamespaceFactory
from thunderstore.repository.models import Package, Team

Expand All @@ -28,8 +30,12 @@ def test_admin_package_listing_approve_listing(
for i in range(5)
]

request = RequestFactory().get("/")
request.user = UserFactory()
request.user.is_staff = True

modeladmin = PackageListingAdmin(PackageListing, None)
approve_listing(modeladmin, None, PackageListing.objects.all())
approve_listing(modeladmin, request, PackageListing.objects.all())

for entry in listings:
entry.refresh_from_db()
Expand All @@ -51,8 +57,12 @@ def test_admin_package_listing_reject_listing(team: Team, community: Community)
for i in range(5)
]

request = RequestFactory().get("/")
request.user = UserFactory()
request.user.is_staff = True

modeladmin = PackageListingAdmin(PackageListing, None)
reject_listing(modeladmin, None, PackageListing.objects.all())
reject_listing(modeladmin, request, PackageListing.objects.all())

for entry in listings:
entry.refresh_from_db()
Expand Down
Loading
Loading