From 98626774849395bb88655794bd8ace6275f363e3 Mon Sep 17 00:00:00 2001 From: "753.network" Date: Mon, 21 Oct 2024 16:57:21 -0400 Subject: [PATCH 01/61] Move permission logic to PackagePermissionsMixin Move permission logic to PackagePermissionsMixin because PackageDetailView is a bad place for it --- .../thunderstore/repository/views/mixins.py | 141 +++++++++++++++++- .../repository/views/package/detail.py | 120 --------------- 2 files changed, 138 insertions(+), 123 deletions(-) diff --git a/django/thunderstore/repository/views/mixins.py b/django/thunderstore/repository/views/mixins.py index 96741ac51..d2c90e3ce 100644 --- a/django/thunderstore/repository/views/mixins.py +++ b/django/thunderstore/repository/views/mixins.py @@ -1,14 +1,23 @@ import dataclasses from typing import Dict, List, Optional, TypedDict +from django.core.exceptions import PermissionDenied from django.http import Http404 +from django.middleware import csrf +from django.shortcuts import redirect +from django.utils.functional import cached_property from django.views.generic import DetailView -from thunderstore.community.models import PackageListing +from thunderstore.community.models import PackageCategory, PackageListing from thunderstore.core.types import UserType +from thunderstore.core.utils import check_validity from thunderstore.plugins.registry import plugin_registry from thunderstore.repository.mixins import CommunityMixin -from thunderstore.repository.views.package._utils import get_package_listing_or_404 +from thunderstore.repository.views.package._utils import ( + can_view_listing_admin, + can_view_package_admin, + get_package_listing_or_404, +) @dataclasses.dataclass @@ -78,7 +87,133 @@ def get_tab_context( } -class PackageListingDetailView(CommunityMixin, PackageTabsMixin, DetailView): +class PackagePermissionsMixin: + @cached_property + def can_manage(self): + return any( + ( + self.can_manage_deprecation, + self.can_manage_categories, + self.can_unlist, + ) + ) + + @cached_property + def can_manage_deprecation(self): + return self.object.package.can_user_manage_deprecation(self.request.user) + + @cached_property + def can_manage_categories(self) -> bool: + return check_validity( + lambda: self.object.ensure_update_categories_permission(self.request.user) + ) + + @cached_property + def can_deprecate(self): + return ( + self.can_manage_deprecation and self.object.package.is_deprecated is False + ) + + @cached_property + def can_undeprecate(self): + return self.can_manage_deprecation and self.object.package.is_deprecated is True + + @cached_property + def can_unlist(self): + return self.request.user.is_superuser + + @cached_property + def can_moderate(self) -> bool: + return self.object.community.can_user_manage_packages(self.request.user) + + def get_review_panel(self): + if not self.can_moderate: + return None + return { + "reviewStatus": self.object.review_status, + "rejectionReason": self.object.rejection_reason, + "internalNotes": self.object.notes, + "packageListingId": self.object.pk, + } + + def format_category(cat: PackageCategory): + return {"name": cat.name, "slug": cat.slug} + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + package_listing = context["object"] + context.update( + **self.get_tab_context( + self.request.user, package_listing, self.get_tab_name() + ) + ) + + context["show_management_panel"] = self.can_manage + context["show_listing_admin_link"] = can_view_listing_admin( + self.request.user, package_listing + ) + context["show_package_admin_link"] = can_view_package_admin( + self.request.user, package_listing.package + ) + context["show_review_status"] = self.can_manage + context["show_internal_notes"] = self.can_moderate + + context["management_panel_props"] = { + "isDeprecated": package_listing.package.is_deprecated, + "canDeprecate": self.can_deprecate, + "canUndeprecate": self.can_undeprecate, + "canUnlist": self.can_unlist, + "canUpdateCategories": self.can_manage_categories, + "csrfToken": csrf.get_token(self.request), + "currentCategories": [ + self.format_category(x) for x in package_listing.categories.all() + ], + "availableCategories": [ + self.format_category(x) + for x in package_listing.community.package_categories.all() + ], + "packageListingId": package_listing.pk, + } + context["review_panel_props"] = self.get_review_panel() + + return context + + def post_deprecate(self): + if not self.can_deprecate: + raise PermissionDenied() + self.object.package.deprecate() + + def post_undeprecate(self): + if not self.can_undeprecate: + raise PermissionDenied() + self.object.package.undeprecate() + + def post_unlist(self): + if not self.can_unlist: + raise PermissionDenied() + self.object.package.deactivate() + + def post(self, request, **kwargs): + self.object = self.get_object() + if not self.can_manage: + raise PermissionDenied() + if "deprecate" in request.POST: + self.post_deprecate() + elif "undeprecate" in request.POST: + self.post_undeprecate() + elif "unlist" in request.POST: + self.post_unlist() + get_package_listing_or_404.clear_cache_with_args( + namespace=self.kwargs["owner"], + name=self.kwargs["name"], + community=self.community, + ) + return redirect(self.object) + + +class PackageListingDetailView( + PackagePermissionsMixin, CommunityMixin, PackageTabsMixin, DetailView +): model = PackageListing object: Optional[PackageListing] = None tab_name: Optional[str] = None diff --git a/django/thunderstore/repository/views/package/detail.py b/django/thunderstore/repository/views/package/detail.py index aaa4f85fa..97080798b 100644 --- a/django/thunderstore/repository/views/package/detail.py +++ b/django/thunderstore/repository/views/package/detail.py @@ -1,72 +1,13 @@ -from django.core.exceptions import PermissionDenied -from django.middleware import csrf -from django.shortcuts import redirect from django.utils.decorators import method_decorator -from django.utils.functional import cached_property from django.views.decorators.csrf import ensure_csrf_cookie -from thunderstore.community.models import PackageCategory -from thunderstore.core.utils import check_validity from thunderstore.repository.views.mixins import PackageListingDetailView -from thunderstore.repository.views.package._utils import ( - can_view_listing_admin, - can_view_package_admin, - get_package_listing_or_404, -) @method_decorator(ensure_csrf_cookie, name="dispatch") class PackageDetailView(PackageListingDetailView): tab_name = "details" - @cached_property - def can_manage(self): - return any( - ( - self.can_manage_deprecation, - self.can_manage_categories, - self.can_unlist, - ) - ) - - @cached_property - def can_manage_deprecation(self): - return self.object.package.can_user_manage_deprecation(self.request.user) - - @cached_property - def can_manage_categories(self) -> bool: - return check_validity( - lambda: self.object.ensure_update_categories_permission(self.request.user) - ) - - @cached_property - def can_deprecate(self): - return ( - self.can_manage_deprecation and self.object.package.is_deprecated is False - ) - - @cached_property - def can_undeprecate(self): - return self.can_manage_deprecation and self.object.package.is_deprecated is True - - @cached_property - def can_unlist(self): - return self.request.user.is_superuser - - @cached_property - def can_moderate(self) -> bool: - return self.object.community.can_user_manage_packages(self.request.user) - - def get_review_panel(self): - if not self.can_moderate: - return None - return { - "reviewStatus": self.object.review_status, - "rejectionReason": self.object.rejection_reason, - "internalNotes": self.object.notes, - "packageListingId": self.object.pk, - } - def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) @@ -83,66 +24,5 @@ def get_context_data(self, *args, **kwargs): ) context["dependants_string"] = dependants_string - context["show_management_panel"] = self.can_manage - context["show_listing_admin_link"] = can_view_listing_admin( - self.request.user, package_listing - ) - context["show_package_admin_link"] = can_view_package_admin( - self.request.user, package_listing.package - ) - context["show_review_status"] = self.can_manage - context["show_internal_notes"] = self.can_moderate - def format_category(cat: PackageCategory): - return {"name": cat.name, "slug": cat.slug} - - context["management_panel_props"] = { - "isDeprecated": package_listing.package.is_deprecated, - "canDeprecate": self.can_deprecate, - "canUndeprecate": self.can_undeprecate, - "canUnlist": self.can_unlist, - "canUpdateCategories": self.can_manage_categories, - "csrfToken": csrf.get_token(self.request), - "currentCategories": [ - format_category(x) for x in package_listing.categories.all() - ], - "availableCategories": [ - format_category(x) - for x in package_listing.community.package_categories.all() - ], - "packageListingId": package_listing.pk, - } - context["review_panel_props"] = self.get_review_panel() return context - - def post_deprecate(self): - if not self.can_deprecate: - raise PermissionDenied() - self.object.package.deprecate() - - def post_undeprecate(self): - if not self.can_undeprecate: - raise PermissionDenied() - self.object.package.undeprecate() - - def post_unlist(self): - if not self.can_unlist: - raise PermissionDenied() - self.object.package.deactivate() - - def post(self, request, **kwargs): - self.object = self.get_object() - if not self.can_manage: - raise PermissionDenied() - if "deprecate" in request.POST: - self.post_deprecate() - elif "undeprecate" in request.POST: - self.post_undeprecate() - elif "unlist" in request.POST: - self.post_unlist() - get_package_listing_or_404.clear_cache_with_args( - namespace=self.kwargs["owner"], - name=self.kwargs["name"], - community=self.community, - ) - return redirect(self.object) From 6e8a90b4ab2fb4a325e13cb85e4adbb0a71bdf04 Mon Sep 17 00:00:00 2001 From: "753.network" Date: Tue, 22 Oct 2024 16:01:24 -0400 Subject: [PATCH 02/61] Update permissions.visibility Add more filtering functions, change default visibility to private --- django/thunderstore/permissions/mixins.py | 14 +++++++++++++- .../thunderstore/permissions/models/visibility.py | 12 ++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/django/thunderstore/permissions/mixins.py b/django/thunderstore/permissions/mixins.py index 477f3bc58..c2e5b12c6 100644 --- a/django/thunderstore/permissions/mixins.py +++ b/django/thunderstore/permissions/mixins.py @@ -11,6 +11,18 @@ def public_list(self): def public_detail(self): return self.exclude(visibility__public_detail=False) + def owner_list(self): + return self.exclude(visibility__owner_list=False) + + def owner_detail(self): + return self.exclude(visibility__owner_detail=False) + + def moderator_list(self): + return self.exclude(visibility__moderator_list=False) + + def moderator_detail(self): + return self.exclude(visibility__moderator_detail=False) + def visible_list(self, is_owner: bool, is_moderator: bool, is_admin: bool): filter = Q(visibility__public_list=True) if is_owner: @@ -44,7 +56,7 @@ class VisibilityMixin(models.Model): @transaction.atomic def save(self, *args, **kwargs): if not self.pk and not self.visibility: - self.visibility = VisibilityFlags.objects.create_public() + self.visibility = VisibilityFlags.objects.create_private() super().save() class Meta: diff --git a/django/thunderstore/permissions/models/visibility.py b/django/thunderstore/permissions/models/visibility.py index 83116055d..b01d19329 100644 --- a/django/thunderstore/permissions/models/visibility.py +++ b/django/thunderstore/permissions/models/visibility.py @@ -14,6 +14,18 @@ def create_public(self): admin_detail=True, ) + def create_private(self): + return self.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, + ) + class VisibilityFlags(models.Model): objects = VisibilityFlagsQuerySet.as_manager() From d87b821bb8d873894ac7ca0c60a98b391217dfa8 Mon Sep 17 00:00:00 2001 From: "753.network" Date: Tue, 22 Oct 2024 16:11:35 -0400 Subject: [PATCH 03/61] Add review_status to PackageVersion Add review_status to PackageVersion Add reject and approve version actions to Django admin Add update_visibility method to PackageVersion --- .../repository/admin/package_version.py | 30 +++++++++++++++ django/thunderstore/repository/consts.py | 11 +++++- .../0055_packageversion_review_status.py | 28 ++++++++++++++ .../repository/models/package_version.py | 37 ++++++++++++++++++- 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 django/thunderstore/repository/migrations/0055_packageversion_review_status.py diff --git a/django/thunderstore/repository/admin/package_version.py b/django/thunderstore/repository/admin/package_version.py index 603fcbea1..12b679687 100644 --- a/django/thunderstore/repository/admin/package_version.py +++ b/django/thunderstore/repository/admin/package_version.py @@ -1,10 +1,15 @@ from django.contrib import admin +from django.core.cache import cache +from django.db import transaction from django.db.models import BooleanField, ExpressionWrapper, Q, QuerySet from django.http import HttpRequest from django.urls import reverse from django.utils.safestring import mark_safe +from thunderstore.cache.enums import CacheBustCondition +from thunderstore.cache.tasks import invalidate_cache from thunderstore.community.models import PackageListing +from thunderstore.repository.consts import PackageVersionReviewStatus from thunderstore.repository.models import PackageVersion from thunderstore.repository.tasks.files import extract_package_version_file_tree @@ -17,11 +22,35 @@ def extract_file_list(modeladmin, request, queryset: QuerySet): extract_file_list.short_description = "Queue file list extraction" +@transaction.atomic +def reject_version(modeladmin, request, queryset: QuerySet[PackageVersion]): + for version in queryset: + version.review_status = PackageVersionReviewStatus.rejected + version.save(update_fields=("review_status",)) + version.package.recache_latest() + + +reject_version.short_description = "Reject" + + +@transaction.atomic +def approve_version(modeladmin, request, queryset: QuerySet[PackageVersion]): + for version in queryset: + version.review_status = PackageVersionReviewStatus.approved + version.save(update_fields=("review_status",)) + version.package.recache_latest() + + +approve_version.short_description = "Approve" + + @admin.register(PackageVersion) class PackageVersionAdmin(admin.ModelAdmin): model = PackageVersion actions = [ extract_file_list, + reject_version, + approve_version, ] list_select_related = ( "package", @@ -33,6 +62,7 @@ class PackageVersionAdmin(admin.ModelAdmin): "package", "version_number", "is_active", + "review_status", "file_size", "downloads", "date_created", diff --git a/django/thunderstore/repository/consts.py b/django/thunderstore/repository/consts.py index b0ae54ee4..32dc76a3b 100644 --- a/django/thunderstore/repository/consts.py +++ b/django/thunderstore/repository/consts.py @@ -1,9 +1,18 @@ import re +from thunderstore.core.utils import ChoiceEnum + PACKAGE_NAME_REGEX = re.compile(r"^[a-zA-Z0-9\_]+$") PACKAGE_VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$") - PACKAGE_REFERENCE_COMPONENT_REGEX = re.compile( r"^[a-zA-Z0-9]+([a-zA-Z0-9\_]+[a-zA-Z0-9])?$" ) + + +class PackageVersionReviewStatus(ChoiceEnum): + pending = "pending" + approved = "approved" + rejected = "rejected" + skipped = "skipped" + immune = "immune" diff --git a/django/thunderstore/repository/migrations/0055_packageversion_review_status.py b/django/thunderstore/repository/migrations/0055_packageversion_review_status.py new file mode 100644 index 000000000..413aab4a8 --- /dev/null +++ b/django/thunderstore/repository/migrations/0055_packageversion_review_status.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.7 on 2024-09-30 15:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("repository", "0054_alter_chunked_package_cache_index"), + ] + + operations = [ + migrations.AddField( + model_name="packageversion", + name="review_status", + field=models.CharField( + choices=[ + ("pending", "pending"), + ("approved", "approved"), + ("rejected", "rejected"), + ("skipped", "skipped"), + ("immune", "immune"), + ], + default="skipped", + max_length=512, + ), + ), + ] diff --git a/django/thunderstore/repository/models/package_version.py b/django/thunderstore/repository/models/package_version.py index 23a434172..2f1d84874 100644 --- a/django/thunderstore/repository/models/package_version.py +++ b/django/thunderstore/repository/models/package_version.py @@ -14,7 +14,10 @@ from thunderstore.core.mixins import AdminLinkMixin from thunderstore.permissions.mixins import VisibilityMixin, VisibilityQuerySet -from thunderstore.repository.consts import PACKAGE_NAME_REGEX +from thunderstore.repository.consts import ( + PACKAGE_NAME_REGEX, + PackageVersionReviewStatus, +) from thunderstore.repository.models import Package from thunderstore.repository.package_formats import PackageFormats from thunderstore.utils.decorators import run_after_commit @@ -39,6 +42,9 @@ class PackageVersionQuerySet(VisibilityQuerySet): def active(self) -> "QuerySet[PackageVersion]": # TODO: Generic type return self.exclude(is_active=False) + def filter_by_review_status(self): + return self.exclude(review_status__in=["pending", "rejected"]) + def chunked_enumerate(self, chunk_size=1000) -> Iterator["PackageVersion"]: """ Enumerate over all the results without fetching everything at once. @@ -120,6 +126,13 @@ class PackageVersion(VisibilityMixin, AdminLinkMixin): readme = models.TextField() changelog = models.TextField(blank=True, null=True) + # TODO: Default should be pending once all versions require automated scanning before appearing to users + review_status = models.CharField( + default=PackageVersionReviewStatus.skipped, + choices=PackageVersionReviewStatus.as_choices(), + max_length=512, + ) + # .zip file = models.FileField( upload_to=get_version_zip_filepath, @@ -148,6 +161,11 @@ def validate(self): def save(self, *args, **kwargs): self.validate() + + old_review_status = PackageVersion.objects.get(pk=self.pk).review_status + if old_review_status != self.review_status: + self.update_visibility() + return super().save(*args, **kwargs) class Meta: @@ -297,6 +315,23 @@ def log_download_event(version_id: int, client_ip: Optional[str]): log_version_download.delay(version_id, timezone.now().isoformat()) + def update_visibility(self): + 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 ( + self.review_status == PackageVersionReviewStatus.rejected + or self.review_status == PackageVersionReviewStatus.pending + ): + self.visibility.public_detail = False + self.visibility.public_list = False + + self.visibility.save() + signals.post_save.connect(PackageVersion.post_save, sender=PackageVersion) signals.post_delete.connect(PackageVersion.post_delete, sender=PackageVersion) From 2231be9ad40fe92e5b25b87d5814d0121df45a74 Mon Sep 17 00:00:00 2001 From: "753.network" Date: Tue, 22 Oct 2024 16:27:07 -0400 Subject: [PATCH 04/61] Update Package version information Update available_versions to exclude private versions Update recache latest to only save if latest isn't None --- django/thunderstore/repository/models/package.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/django/thunderstore/repository/models/package.py b/django/thunderstore/repository/models/package.py index 5abe5bf58..393b4eea8 100644 --- a/django/thunderstore/repository/models/package.py +++ b/django/thunderstore/repository/models/package.py @@ -162,8 +162,10 @@ def display_name(self): @cached_property def available_versions(self): # TODO: Caching - versions = self.versions.filter(is_active=True).values_list( - "pk", "version_number" + versions = ( + self.versions.filter(is_active=True) + .public_list() + .values_list("pk", "version_number") ) ordered = sorted(versions, key=lambda version: StrictVersion(version[1])) pk_list = [version[0] for version in reversed(ordered)] @@ -272,7 +274,7 @@ def recache_latest(self): if hasattr(self, "available_versions"): del self.available_versions # Bust the version cache self.latest = self.available_versions.first() - if old_latest != self.latest: + if old_latest != self.latest and self.latest is not None: self.save() def handle_created_version(self, version): From 2d1f67307d2b91709d336a6d3e5985a17460de93 Mon Sep 17 00:00:00 2001 From: "753.network" Date: Tue, 22 Oct 2024 16:42:47 -0400 Subject: [PATCH 05/61] Add visibility to PackageListing Add VisibilityMixin to PackageListing Add VisibilityQuerySet to PackageListingQueryset Add visibility logic to ensure_can_be_viewed_by_user Add update_visibility method to PackageListing Use listing.update_visibility in recache_latest --- .../0030_packagelisting_visibility.py | 25 +++++++++ .../community/models/package_listing.py | 53 ++++++++++++++++++- .../thunderstore/repository/models/package.py | 2 + 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 django/thunderstore/community/migrations/0030_packagelisting_visibility.py diff --git a/django/thunderstore/community/migrations/0030_packagelisting_visibility.py b/django/thunderstore/community/migrations/0030_packagelisting_visibility.py new file mode 100644 index 000000000..d1ed2e079 --- /dev/null +++ b/django/thunderstore/community/migrations/0030_packagelisting_visibility.py @@ -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", + ), + ), + ] diff --git a/django/thunderstore/community/models/package_listing.py b/django/thunderstore/community/models/package_listing.py index e0b7a3263..9988b90cb 100644 --- a/django/thunderstore/community/models/package_listing.py +++ b/django/thunderstore/community/models/package_listing.py @@ -15,6 +15,7 @@ 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.utils import validate_user from thunderstore.webhooks.audit import ( AuditAction, @@ -27,7 +28,7 @@ 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) @@ -46,7 +47,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 """ @@ -101,6 +102,11 @@ def validate(self): def save(self, *args, **kwargs): self.validate() + + old_review_status = PackageListing.objects.get(pk=self.pk).review_status + if old_review_status != self.review_status: + self.update_visibility() + return super().save(*args, **kwargs) def __str__(self): @@ -342,6 +348,21 @@ def get_has_perms() -> bool: ) ) + if not self.visibility: + raise ValidationError("Insufficient permissions to view") + + if not self.visibility.public_detail: + if not ( + self.visibility.owner_detail + and self.package.owner.can_user_access(user) + ): + if not ( + self.visibility.moderator_detail + and self.community.can_user_manage_packages(user) + ): + if not (self.visibility.admin_detail and user.is_superuser): + raise ValidationError("Insufficient permissions to view") + if self.community.require_package_listing_approval: if ( self.review_status != PackageListingReviewStatus.approved @@ -358,6 +379,34 @@ def get_has_perms() -> bool: def can_be_viewed_by_user(self, user: Optional[UserType]) -> bool: return check_validity(lambda: self.ensure_can_be_viewed_by_user(user)) + def update_visibility(self): + 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 self.review_status == PackageListingReviewStatus.rejected: + 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) signals.post_delete.connect(PackageListing.post_delete, sender=PackageListing) diff --git a/django/thunderstore/repository/models/package.py b/django/thunderstore/repository/models/package.py index 393b4eea8..d753747c5 100644 --- a/django/thunderstore/repository/models/package.py +++ b/django/thunderstore/repository/models/package.py @@ -276,6 +276,8 @@ def recache_latest(self): self.latest = self.available_versions.first() if old_latest != self.latest and self.latest is not None: self.save() + for listing in self.community_listings.all(): + listing.update_visibility() def handle_created_version(self, version): self.date_updated = timezone.now() From fd7dd2a511927814d2147e8e41bbe9c554ae7c63 Mon Sep 17 00:00:00 2001 From: "753.network" Date: Tue, 22 Oct 2024 16:50:55 -0400 Subject: [PATCH 06/61] Add migrations to populate visibility Add migrations to populate visibility field for PackageVersion and PackageListing --- ...efault_visibility_for_existing_listings.py | 63 +++++++++++++++++++ ...efault_visibility_for_existing_versions.py | 52 +++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 django/thunderstore/community/migrations/0031_create_default_visibility_for_existing_listings.py create mode 100644 django/thunderstore/repository/migrations/0056_create_default_visibility_for_existing_versions.py diff --git a/django/thunderstore/community/migrations/0031_create_default_visibility_for_existing_listings.py b/django/thunderstore/community/migrations/0031_create_default_visibility_for_existing_listings.py new file mode 100644 index 000000000..5e57bd652 --- /dev/null +++ b/django/thunderstore/community/migrations/0031_create_default_visibility_for_existing_listings.py @@ -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): + 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 listing.review_status == PackageListingReviewStatus.rejected: + 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), + ] diff --git a/django/thunderstore/repository/migrations/0056_create_default_visibility_for_existing_versions.py b/django/thunderstore/repository/migrations/0056_create_default_visibility_for_existing_versions.py new file mode 100644 index 000000000..5dee13dfe --- /dev/null +++ b/django/thunderstore/repository/migrations/0056_create_default_visibility_for_existing_versions.py @@ -0,0 +1,52 @@ +from django.db import migrations + +from thunderstore.repository.consts import PackageVersionReviewStatus + + +def create_default_visibility_for_existing_records(apps, schema_editor): + PackageVersion = apps.get_model("repository", "PackageVersion") + VisibilityFlags = apps.get_model("permissions", "VisibilityFlags") + + for instance in PackageVersion.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(version): + version.visibility.public_detail = True + version.visibility.public_list = True + version.visibility.owner_detail = True + version.visibility.owner_list = True + version.visibility.moderator_detail = True + version.visibility.moderator_list = True + + if ( + version.review_status == PackageVersionReviewStatus.rejected + or version.review_status == PackageVersionReviewStatus.pending + ): + version.visibility.public_detail = False + version.visibility.public_list = False + + version.visibility.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("repository", "0055_packageversion_review_status"), + ] + + operations = [ + migrations.RunPython(create_default_visibility_for_existing_records), + ] From eb0ed55d4eae2f6f335289561999820f175f9b6a Mon Sep 17 00:00:00 2001 From: "753.network" Date: Tue, 22 Oct 2024 16:53:05 -0400 Subject: [PATCH 07/61] Add visibility filter to PackageListSearchView Add visibility filter to PackageListSearchView so searches do not return packages that are not publicly listed --- django/thunderstore/repository/views/package/list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django/thunderstore/repository/views/package/list.py b/django/thunderstore/repository/views/package/list.py index 46e5c75eb..f12adafb3 100644 --- a/django/thunderstore/repository/views/package/list.py +++ b/django/thunderstore/repository/views/package/list.py @@ -271,6 +271,7 @@ def get_queryset(self): queryset = queryset.exclude(package__is_deprecated=True) queryset = self.filter_approval_status(queryset) + queryset = queryset.public_list() search_query = self.get_search_query() if search_query: From b8f56a39a97d88cc9714e21f9e4d48b20c2a6bc5 Mon Sep 17 00:00:00 2001 From: "753.network" Date: Tue, 22 Oct 2024 18:45:54 -0400 Subject: [PATCH 08/61] Fix PackageListing/Version save() Fix PackageListing/Version save() which would fail for new instances --- django/thunderstore/community/models/package_listing.py | 7 ++++--- django/thunderstore/repository/models/package_version.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/django/thunderstore/community/models/package_listing.py b/django/thunderstore/community/models/package_listing.py index 9988b90cb..0da9da831 100644 --- a/django/thunderstore/community/models/package_listing.py +++ b/django/thunderstore/community/models/package_listing.py @@ -103,9 +103,10 @@ def validate(self): def save(self, *args, **kwargs): self.validate() - old_review_status = PackageListing.objects.get(pk=self.pk).review_status - if old_review_status != self.review_status: - self.update_visibility() + old_self = PackageListing.objects.filter(pk=self.pk).first() + if old_self is not None: + if old_self.review_status != self.review_status: + self.update_visibility() return super().save(*args, **kwargs) diff --git a/django/thunderstore/repository/models/package_version.py b/django/thunderstore/repository/models/package_version.py index 2f1d84874..3490eb497 100644 --- a/django/thunderstore/repository/models/package_version.py +++ b/django/thunderstore/repository/models/package_version.py @@ -162,9 +162,10 @@ def validate(self): def save(self, *args, **kwargs): self.validate() - old_review_status = PackageVersion.objects.get(pk=self.pk).review_status - if old_review_status != self.review_status: - self.update_visibility() + old_self = PackageVersion.objects.filter(pk=self.pk).first() + if old_self is not None: + if old_self.review_status != self.review_status: + self.update_visibility() return super().save(*args, **kwargs) From 032a69642bb6c9a3f36063d00b68ea9a60da60a4 Mon Sep 17 00:00:00 2001 From: "753.network" Date: Tue, 22 Oct 2024 18:47:55 -0400 Subject: [PATCH 09/61] Update listing templates Add unavailable_versions property to Package Update PackageListing Detail and Versions pages to show unavailability information --- .../community/packagelisting_detail.html | 10 ++++++-- .../community/packagelisting_versions.html | 18 ++++++++++++- .../thunderstore/repository/models/package.py | 25 +++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/django/thunderstore/community/templates/community/packagelisting_detail.html b/django/thunderstore/community/templates/community/packagelisting_detail.html index ef854a324..0072a31c4 100644 --- a/django/thunderstore/community/templates/community/packagelisting_detail.html +++ b/django/thunderstore/community/templates/community/packagelisting_detail.html @@ -49,7 +49,7 @@ {% endif %} -{% cache_until "any_package_updated" "mod-detail-header" 300 object.package.pk community_identifier %} +{% cache_until "any_package_updated" "mod-detail-header" 300 object.package.pk community_identifier object.package.latest.pk %}