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

Version level review #1073

Open
wants to merge 63 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
9862677
Move permission logic to PackagePermissionsMixin
x753 Oct 21, 2024
6e8a90b
Update permissions.visibility
x753 Oct 22, 2024
d87b821
Add review_status to PackageVersion
x753 Oct 22, 2024
2231be9
Update Package version information
x753 Oct 22, 2024
2d1f673
Add visibility to PackageListing
x753 Oct 22, 2024
fd7dd2a
Add migrations to populate visibility
x753 Oct 22, 2024
eb0ed55
Add visibility filter to PackageListSearchView
x753 Oct 22, 2024
b8f56a3
Fix PackageListing/Version save()
x753 Oct 22, 2024
032a696
Update listing templates
x753 Oct 22, 2024
ef6ff6b
Fix visibility / recaching
x753 Oct 25, 2024
e809e8e
Add authentication to package version page
x753 Oct 25, 2024
2b14873
Add version review buttons to versions table
x753 Oct 25, 2024
a6d6ef6
Ensure APIs only return public listings
x753 Oct 25, 2024
3539db3
Make APIs return only public_list instance
x753 Oct 29, 2024
db2298b
Small fixes to satisfy tests
x753 Oct 29, 2024
f2e8a8f
Add tests for listing/version visibility
x753 Oct 29, 2024
304fafc
Update webhook audits
x753 Oct 29, 2024
7c5313e
Move permission logic to PackagePermissionsMixin
x753 Oct 21, 2024
793f0b2
Update permissions.visibility
x753 Oct 22, 2024
4f6054a
Add review_status to PackageVersion
x753 Oct 22, 2024
504256b
Update Package version information
x753 Oct 22, 2024
dde92a8
Add visibility to PackageListing
x753 Oct 22, 2024
191952d
Add migrations to populate visibility
x753 Oct 22, 2024
c48a020
Add visibility filter to PackageListSearchView
x753 Oct 22, 2024
82d1a4e
Fix PackageListing/Version save()
x753 Oct 22, 2024
bb4554d
Update listing templates
x753 Oct 22, 2024
2f4e6cb
Fix visibility / recaching
x753 Oct 25, 2024
272cbb3
Add authentication to package version page
x753 Oct 25, 2024
ad07c47
Add version review buttons to versions table
x753 Oct 25, 2024
600ee51
Ensure APIs only return public listings
x753 Oct 25, 2024
7eafb7b
Make APIs return only public_list instance
x753 Oct 29, 2024
9ca722a
Small fixes to satisfy tests
x753 Oct 29, 2024
beedbeb
Add tests for listing/version visibility
x753 Oct 29, 2024
f46bda1
Update webhook audits
x753 Oct 29, 2024
0c9f1e0
Merge branch 'version-level-review' of https://github.com/thunderstor…
x753 Nov 4, 2024
9d5d9e5
Update listing visibility
x753 Nov 4, 2024
fe9758c
Update version table caching
x753 Nov 4, 2024
c088a23
Add reverse to RunPython migrations
x753 Nov 6, 2024
27b8247
Refactor create_private -> create_unpublished
x753 Nov 6, 2024
991c9e1
Update package_version_list.py
x753 Nov 6, 2024
88f1ad0
Move update_visibility()
x753 Nov 6, 2024
448cca3
Add active() into public_list()
x753 Nov 6, 2024
8e1d9ea
Fix visibility tests
x753 Nov 7, 2024
e2ab39e
Update package.py
x753 Nov 7, 2024
14af6cc
Remove shorthands from visibility mixin
x753 Nov 7, 2024
eef28dc
Ensure inactive models are not visible
x753 Nov 7, 2024
5e2eeaf
Add package management / status to detail views
x753 Nov 7, 2024
1ad566b
Add unavailable warning to version detail page
x753 Nov 7, 2024
a94d464
Refactor can_be_viewed_by_user
x753 Nov 11, 2024
2cef9d4
Allow owners to see their rejected packages
x753 Nov 11, 2024
6503182
Turn review_status CharFields into TextFields
x753 Nov 12, 2024
8369d36
Remove redundancy in update_visibility
x753 Nov 12, 2024
b6567cd
Adjust views so latest can be None
x753 Nov 13, 2024
25e8e83
Move is_visible_to_user() to version/listing
x753 Nov 13, 2024
b6716db
Add a rate limit to celery_post
x753 Nov 27, 2024
b28c8a0
Update community aggregates
x753 Nov 27, 2024
10fb94f
Add system() to VisibilityMixin
x753 Dec 17, 2024
21de7da
Add visibility code convention test
x753 Dec 17, 2024
8b117bd
Merge remote-tracking branch 'origin/master' into version-level-review
x753 Dec 17, 2024
16256f0
Master into version-level-review merge migration
x753 Dec 17, 2024
b6227c9
Fix views 404ing shortly after approval
x753 Dec 17, 2024
a25b760
Fix changelog view breaking when latest is None
x753 Dec 20, 2024
39acac4
Add custom Flake8 plugin
x753 Jan 14, 2025
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
7 changes: 7 additions & 0 deletions builder/src/scss/package-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
transform-origin: center;
}

.unavailable {
position: absolute;
z-index: 1;
pointer-events: none;
text-align: right;
}

.search-options {
.checkbox {
display: flex;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
from rest_framework.test import APIClient

from thunderstore.permissions.factories import VisibilityFlagsFactory
from thunderstore.repository.consts import PackageVersionReviewStatus
from thunderstore.repository.factories import PackageVersionFactory
from thunderstore.repository.models import Package

Expand Down Expand Up @@ -48,3 +50,58 @@ def test_package_version_list_api_view__returns_versions(

assert len(actual) == 1
assert actual[0]["version_number"] == expected.version_number


@pytest.mark.django_db
def test_only_visible_versions_are_returned(
api_client: APIClient,
) -> None:
version1 = PackageVersionFactory(version_number="1.0.0")
version1.review_status = PackageVersionReviewStatus.approved
version1.save()

version2 = PackageVersionFactory(package=version1.package, version_number="2.0.0")
version2.review_status = PackageVersionReviewStatus.rejected
version2.save()

assert version1.visibility.public_list is True
assert version2.visibility.public_list is False

response = api_client.get(
f"/api/cyberstorm/package/{version1.package.namespace}/{version1.package.name}/versions/",
)
actual = response.json()

assert len(actual) == 1
assert actual[0]["version_number"] == version1.version_number


@pytest.mark.django_db
def test_latest_is_visible_if_possible(
api_client: APIClient,
) -> None:
version1 = PackageVersionFactory(version_number="1.0.0")
version1.visibility = VisibilityFlagsFactory(public_list=True)
version1.save()

version2 = PackageVersionFactory(package=version1.package, version_number="2.0.0")
version2.visibility = VisibilityFlagsFactory(public_list=True)
version2.save()

version1.package.recache_latest()

assert version1.package.latest == version2

version2.review_status = PackageVersionReviewStatus.rejected
version2.save()

version1.package.recache_latest()

assert version1.package.latest == version1

version1.review_status = PackageVersionReviewStatus.rejected
version1.save()

version1.package.recache_latest()

assert version1.package.latest == version1
3 changes: 1 addition & 2 deletions django/thunderstore/api/cyberstorm/views/package_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ def get_custom_package_listing(
listing_ref = PackageListing.objects.filter(pk=OuterRef("pk"))

qs = (
PackageListing.objects.active()
.filter_by_community_approval_rule()
PackageListing.objects.public_list()
.select_related(
"community",
"package__latest",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ def get_queryset(self):
name__iexact=self.kwargs["package_name"],
)

return package.versions.active()
return package.versions.public_list()
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 3.1.7 on 2024-10-03 19:34

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


class Migration(migrations.Migration):

dependencies = [
("permissions", "0001_initial"),
("community", "0029_packagelisting_is_auto_imported"),
]

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,80 @@
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):
listing.visibility.public_detail = True
listing.visibility.public_list = True
listing.visibility.owner_detail = True
listing.visibility.owner_list = True
listing.visibility.moderator_detail = True
listing.visibility.moderator_list = True

if not listing.package.is_active:
listing.visibility.public_detail = False
listing.visibility.public_list = False
listing.visibility.owner_detail = False
listing.visibility.owner_list = False
listing.visibility.moderator_detail = False
listing.visibility.moderator_list = False

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

versions = listing.package.versions.filter(is_active=True).all()
if versions.exclude(visibility__public_detail=False).count() == 0:
listing.visibility.public_detail = False
if versions.exclude(visibility__public_list=False).count() == 0:
listing.visibility.public_list = False
if versions.exclude(visibility__owner_detail=False).count() == 0:
listing.visibility.owner_detail = False
if versions.exclude(visibility__owner_list=False).count() == 0:
listing.visibility.owner_list = False
if versions.exclude(visibility__moderator_detail=False).count() == 0:
listing.visibility.moderator_detail = False
if versions.exclude(visibility__moderator_list=False).count() == 0:
listing.visibility.moderator_list = False

listing.visibility.save()


class Migration(migrations.Migration):

dependencies = [
("community", "0030_packagelisting_visibility"),
]

operations = [
migrations.RunPython(
create_default_visibility_for_existing_records, migrations.RunPython.noop
),
]
85 changes: 55 additions & 30 deletions django/thunderstore/community/models/package_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
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, VisibilityQuerySet
from thunderstore.permissions.models import VisibilityFlags
from thunderstore.permissions.utils import validate_user
from thunderstore.webhooks.audit import (
AuditAction,
Expand All @@ -27,12 +29,15 @@
from thunderstore.community.models import PackageCategory


class PackageListingQueryset(models.QuerySet):
class PackageListingQueryset(VisibilityQuerySet):
def active(self):
return self.exclude(package__is_active=False).exclude(
~Q(package__versions__is_active=True)
)

def public_list(self):
return super(PackageListingQueryset, self.active()).public_list()

def approved(self):
return self.exclude(~Q(review_status=PackageListingReviewStatus.approved))

Expand All @@ -46,7 +51,7 @@ def filter_by_community_approval_rule(self):
# 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, VisibilityMixin, AdminLinkMixin, models.Model):
"""
Represents a package's relation to how it's displayed on the site and APIs
"""
Expand Down Expand Up @@ -212,7 +217,7 @@ def reject(
message = "\n\n".join(filter(bool, (rejection_reason, internal_notes)))
fire_audit_event(
self.build_audit_event(
action=AuditAction.PACKAGE_REJECTED,
action=AuditAction.LISTING_REJECTED,
user_id=agent.pk if agent else None,
message=message,
)
Expand All @@ -238,7 +243,7 @@ def approve(
)
fire_audit_event(
self.build_audit_event(
action=AuditAction.PACKAGE_APPROVED,
action=AuditAction.LISTING_APPROVED,
user_id=agent.pk if agent else None,
message=internal_notes,
)
Expand Down Expand Up @@ -331,32 +336,52 @@ def check_update_categories_permission(self, user: Optional[UserType]) -> bool:
def can_user_manage_approval_status(self, user: Optional[UserType]) -> bool:
return self.can_be_moderated_by_user(user)

def ensure_can_be_viewed_by_user(self, user: Optional[UserType]) -> None:
def get_has_perms() -> bool:
return (
user is not None
and user.is_authenticated
and (
self.community.can_user_manage_packages(user)
or self.package.owner.can_user_access(user)
)
)

if self.community.require_package_listing_approval:
if (
self.review_status != PackageListingReviewStatus.approved
and not get_has_perms()
):
raise ValidationError("Insufficient permissions to view")
else:
if (
self.review_status == PackageListingReviewStatus.rejected
and not get_has_perms()
):
raise ValidationError("Insufficient permissions to view")

def can_be_viewed_by_user(self, user: Optional[UserType]) -> bool:
return check_validity(lambda: self.ensure_can_be_viewed_by_user(user))
@transaction.atomic
def update_visibility(self):
if not self.visibility:
self.visibility = VisibilityFlags.objects.create_unpublished()

self.visibility.public_detail = True
self.visibility.public_list = True
self.visibility.owner_detail = True
self.visibility.owner_list = True
self.visibility.moderator_detail = True
self.visibility.moderator_list = True

if not self.package.is_active:
self.visibility.public_detail = False
self.visibility.public_list = False
self.visibility.owner_detail = False
self.visibility.owner_list = False
self.visibility.moderator_detail = False
self.visibility.moderator_list = False

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

versions = self.package.versions.filter(is_active=True).all()
if versions.exclude(visibility__public_detail=False).count() == 0:
self.visibility.public_detail = False
if versions.exclude(visibility__public_list=False).count() == 0:
self.visibility.public_list = False
if versions.exclude(visibility__owner_detail=False).count() == 0:
self.visibility.owner_detail = False
if versions.exclude(visibility__owner_list=False).count() == 0:
self.visibility.owner_list = False
if versions.exclude(visibility__moderator_detail=False).count() == 0:
self.visibility.moderator_detail = False
if versions.exclude(visibility__moderator_list=False).count() == 0:
self.visibility.moderator_list = False

self.visibility.save()


signals.post_save.connect(PackageListing.post_save, sender=PackageListing)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% load encode_props %}

{% if show_management_panel %}
<div class="d-flex justify-content-end gap-1">
{% if review_panel_props %}
<div id="package-review-panel"></div>
<script type="text/javascript">
window.ts.PackageReviewPanel(
document.getElementById("package-review-panel"),
{{ review_panel_props|encode_props }}
);
</script>
{% endif %}
<div id="package-management-panel"></div>
{% if show_listing_admin_link %}
<a href="{% url "admin:community_packagelisting_change" object.pk %}" type="button" class="btn btn-primary"><span class="fas fa-external-link-alt"></span>&nbsp;&nbsp;Listing admin</a>
{% endif %}
{% if show_package_admin_link %}
<a href="{% url "admin:repository_package_change" object.package.pk %}" type="button" class="btn btn-primary"><span class="fas fa-external-link-alt"></span>&nbsp;&nbsp;Package admin</a>
{% endif %}
</div>
<script type="text/javascript">
window.ts.PackageManagementPanel(
document.getElementById("package-management-panel"),
{{ management_panel_props|encode_props }}
);
</script>
{% endif %}
Loading
Loading