diff --git a/django/thunderstore/community/admin/package_listing.py b/django/thunderstore/community/admin/package_listing.py
index bc49bb6d3..07bdf3651 100644
--- a/django/thunderstore/community/admin/package_listing.py
+++ b/django/thunderstore/community/admin/package_listing.py
@@ -1,8 +1,10 @@
+from html import escape
from typing import Optional
from django.contrib import admin
from django.db import transaction
from django.db.models import QuerySet
+from django.utils.safestring import mark_safe
from ..consts import PackageListingReviewStatus
from ..forms import PackageListingAdminForm
@@ -62,6 +64,7 @@ class PackageListingAdmin(admin.ModelAdmin):
"package__namespace__name",
"package__owner__name",
"package__name",
+ "package__versions__file_tree__entries__blob__checksum_sha256",
)
list_select_related = (
"package",
@@ -70,11 +73,19 @@ class PackageListingAdmin(admin.ModelAdmin):
"community",
)
readonly_fields = (
- "package",
+ "package_link",
"community",
"datetime_created",
"datetime_updated",
)
+ exclude = ("package",)
+
+ def package_link(self, obj):
+ return mark_safe(
+ f'{escape(str(obj.package))}'
+ )
+
+ package_link.short_description = "Package"
def get_readonly_fields(self, request, obj=None):
if obj:
@@ -82,6 +93,12 @@ def get_readonly_fields(self, request, obj=None):
else:
return []
+ def get_exclude(self, request, obj=None):
+ if obj:
+ return self.exclude
+ else:
+ return []
+
def get_view_on_site_url(
self, obj: Optional[PackageListing] = None
) -> Optional[str]:
diff --git a/django/thunderstore/community/models/package_listing.py b/django/thunderstore/community/models/package_listing.py
index 86ee191e4..03c97f29b 100644
--- a/django/thunderstore/community/models/package_listing.py
+++ b/django/thunderstore/community/models/package_listing.py
@@ -11,7 +11,7 @@
from thunderstore.cache.enums import CacheBustCondition
from thunderstore.cache.tasks import invalidate_cache_on_commit_async
from thunderstore.community.consts import PackageListingReviewStatus
-from thunderstore.core.mixins import TimestampMixin
+from thunderstore.core.mixins import AdminLinkMixin, TimestampMixin
from thunderstore.core.types import UserType
from thunderstore.core.utils import check_validity
from thunderstore.frontend.url_reverse import get_community_url_reverse_args
@@ -46,7 +46,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, models.Model):
+class PackageListing(TimestampMixin, AdminLinkMixin, models.Model):
"""
Represents a package's relation to how it's displayed on the site and APIs
"""
diff --git a/django/thunderstore/core/mixins.py b/django/thunderstore/core/mixins.py
index 127b4b1e6..846581331 100644
--- a/django/thunderstore/core/mixins.py
+++ b/django/thunderstore/core/mixins.py
@@ -4,6 +4,7 @@
from django.db import DEFAULT_DB_ALIAS, connections, models
from django.db.models import Q
from django.shortcuts import redirect
+from django.urls import reverse
from thunderstore.cache.storage import CACHE_STORAGE
@@ -106,3 +107,14 @@ class Meta:
indexes = [
models.Index(fields=["last_modified"]),
]
+
+
+class AdminLinkMixin(models.Model):
+ def get_admin_url(self):
+ return reverse(
+ f"admin:{self._meta.app_label}_{self._meta.model_name}_change",
+ args=[self.pk],
+ )
+
+ class Meta:
+ abstract = True
diff --git a/django/thunderstore/repository/admin/package.py b/django/thunderstore/repository/admin/package.py
index 06c2eabc0..81e3a9523 100644
--- a/django/thunderstore/repository/admin/package.py
+++ b/django/thunderstore/repository/admin/package.py
@@ -1,9 +1,11 @@
+from html import escape
from typing import Optional
from django.contrib import admin
from django.db import transaction
from django.db.models import QuerySet
from django.http import HttpRequest
+from django.utils.safestring import mark_safe
from thunderstore.repository.admin.actions import activate, deactivate
from thunderstore.repository.models import Package, PackageVersion
@@ -12,6 +14,7 @@
class PackageVersionInline(admin.StackedInline):
model = PackageVersion
readonly_fields = (
+ "version_link",
"date_created",
"description",
"downloads",
@@ -19,12 +22,18 @@ class PackageVersionInline(admin.StackedInline):
"file_size",
"format_spec",
"icon",
- "version_number",
+ "file_tree_link",
+ "visibility",
"website_url",
+ )
+ exclude = (
+ "version_number",
"file_tree",
- "visibility",
+ "dependencies",
+ "name",
+ "readme",
+ "changelog",
)
- exclude = ("readme", "changelog", "dependencies", "name")
extra = 0
def has_add_permission(self, request: HttpRequest, obj) -> bool:
@@ -33,6 +42,20 @@ def has_add_permission(self, request: HttpRequest, obj) -> bool:
def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
return False
+ def version_link(self, obj):
+ return mark_safe(f'{escape(str(obj))}')
+
+ version_link.short_description = "Version"
+
+ def file_tree_link(self, obj):
+ if not obj.file_tree:
+ return None
+ return mark_safe(
+ f'{escape(str(obj.file_tree))}'
+ )
+
+ file_tree_link.short_description = "File Tree"
+
@transaction.atomic
def deprecate_package(modeladmin, request, queryset: QuerySet[Package]):
@@ -93,6 +116,7 @@ class PackageAdmin(admin.ModelAdmin):
"name",
"namespace__name",
"owner__name",
+ "versions__file_tree__entries__blob__checksum_sha256",
)
list_select_related = (
"latest",
diff --git a/django/thunderstore/repository/admin/package_version.py b/django/thunderstore/repository/admin/package_version.py
index 73e274cd1..54a29c09a 100644
--- a/django/thunderstore/repository/admin/package_version.py
+++ b/django/thunderstore/repository/admin/package_version.py
@@ -4,6 +4,7 @@
from django.urls import reverse
from django.utils.safestring import mark_safe
+from thunderstore.community.models import PackageListing
from thunderstore.repository.models import PackageVersion
from thunderstore.repository.tasks.files import extract_package_version_file_tree
@@ -41,10 +42,15 @@ class PackageVersionAdmin(admin.ModelAdmin):
"package__owner__name",
"package__namespace__name",
"version_number",
+ "file_tree__entries__blob__checksum_sha256",
)
date_hierarchy = "date_created"
readonly_fields = [x.name for x in PackageVersion._meta.fields] + [
"file_tree_link",
+ "listings",
+ ]
+ exclude = [
+ "file_tree",
]
def get_queryset(self, request: HttpRequest) -> QuerySet:
@@ -58,6 +64,9 @@ def get_queryset(self, request: HttpRequest) -> QuerySet:
)
)
+ def get_readonly_fields(self, request, obj=None):
+ return [x for x in self.readonly_fields if x not in self.exclude]
+
def has_file_tree(self, obj):
return obj.has_file_tree
@@ -73,6 +82,16 @@ def file_tree_link(self, obj):
)
return mark_safe(f'{obj.file_tree}')
+ file_tree_link.short_description = "File tree"
+
+ def listings(self, obj):
+ url = reverse(
+ f"admin:{PackageListing._meta.app_label}_{PackageListing._meta.model_name}_changelist",
+ )
+ return mark_safe(
+ f'View package listings'
+ )
+
def has_add_permission(self, request: HttpRequest) -> bool:
return False
diff --git a/django/thunderstore/repository/models/package.py b/django/thunderstore/repository/models/package.py
index 29eb74d3c..5abe5bf58 100644
--- a/django/thunderstore/repository/models/package.py
+++ b/django/thunderstore/repository/models/package.py
@@ -16,6 +16,7 @@
from thunderstore.cache.enums import CacheBustCondition
from thunderstore.cache.tasks import invalidate_cache_on_commit_async
from thunderstore.core.enums import OptionalBoolChoice
+from thunderstore.core.mixins import AdminLinkMixin
from thunderstore.core.types import UserType
from thunderstore.core.utils import check_validity
from thunderstore.permissions.utils import validate_user
@@ -43,7 +44,7 @@ def get_package_dependants_list(package_pk: int):
return list(get_package_dependants(package_pk))
-class Package(models.Model):
+class Package(AdminLinkMixin, models.Model):
objects = PackageQueryset.as_manager()
wiki: Optional["PackageWiki"]
diff --git a/django/thunderstore/repository/models/package_version.py b/django/thunderstore/repository/models/package_version.py
index fa0931593..23a434172 100644
--- a/django/thunderstore/repository/models/package_version.py
+++ b/django/thunderstore/repository/models/package_version.py
@@ -12,6 +12,7 @@
from django.utils import timezone
from django.utils.functional import cached_property
+from thunderstore.core.mixins import AdminLinkMixin
from thunderstore.permissions.mixins import VisibilityMixin, VisibilityQuerySet
from thunderstore.repository.consts import PACKAGE_NAME_REGEX
from thunderstore.repository.models import Package
@@ -66,7 +67,7 @@ def listed_in(self, community_identifier: str):
)
-class PackageVersion(VisibilityMixin):
+class PackageVersion(VisibilityMixin, AdminLinkMixin):
installers: "Manager[PackageInstaller]"
installer_declarations: "Manager[PackageInstallerDeclaration]"
objects: "Manager[PackageVersion]" = PackageVersionQuerySet.as_manager()
diff --git a/django/thunderstore/repository/tests/test_tabs_mixin.py b/django/thunderstore/repository/tests/test_tabs_mixin.py
index 37b060ed8..6dbe7d836 100644
--- a/django/thunderstore/repository/tests/test_tabs_mixin.py
+++ b/django/thunderstore/repository/tests/test_tabs_mixin.py
@@ -98,3 +98,28 @@ def assert_disabled(user: UserType, expected: bool) -> None:
active_package_listing.package.latest.changelog = "# Foo bar"
active_package_listing.package.latest.save()
assert_disabled(user, False)
+
+
+@pytest.mark.django_db
+def test_active_tabs_are_visible(
+ user: UserType,
+ active_package_listing: PackageListing,
+) -> None:
+ tabs_mixin = PackageTabsMixin()
+
+ tabs1 = tabs_mixin.get_tab_context(
+ user,
+ active_package_listing,
+ "details",
+ )["tabs"]
+
+ for tab1 in tabs1:
+ tabs2 = tabs_mixin.get_tab_context(
+ user,
+ active_package_listing,
+ tab1.name,
+ )["tabs"]
+
+ for tab2 in tabs2:
+ if tab2.is_active:
+ assert tab2.is_visible
diff --git a/django/thunderstore/repository/views/mixins.py b/django/thunderstore/repository/views/mixins.py
index baa2aa0c9..96741ac51 100644
--- a/django/thunderstore/repository/views/mixins.py
+++ b/django/thunderstore/repository/views/mixins.py
@@ -16,6 +16,7 @@ class PartialTab:
url: str
title: str
is_disabled: bool = False
+ is_visible: bool = True
@dataclasses.dataclass
@@ -24,6 +25,7 @@ class Tab:
name: str
url: str
is_disabled: bool
+ is_visible: bool
is_active: bool
@@ -67,9 +69,11 @@ def get_tab_context(
name=k,
url=v.url,
is_disabled=v.is_disabled and k != active_tab,
+ is_visible=v.is_visible or k == active_tab,
is_active=k == active_tab,
)
for k, v in tabs.items()
+ if (v.is_visible or k == active_tab)
],
}
diff --git a/django/thunderstore/storage/admin/group.py b/django/thunderstore/storage/admin/group.py
index 9450b6254..75388ec16 100644
--- a/django/thunderstore/storage/admin/group.py
+++ b/django/thunderstore/storage/admin/group.py
@@ -1,3 +1,5 @@
+from html import escape
+
from django.contrib import admin
from django.http import HttpRequest
from django.utils.safestring import mark_safe
@@ -9,17 +11,23 @@
class DataBlobReferenceInline(ReadOnlyInline, admin.TabularInline):
model = DataBlobReference
fields = (
- "name",
+ "link",
"data_size",
"file",
)
readonly_fields = (
+ "link",
"data_size",
"file",
)
def file(self, obj: DataBlobReference):
- return mark_safe(f'{obj.blob.data}')
+ return mark_safe(f'{escape(str(obj.blob.data))}')
+
+ def link(self, obj: DataBlobReference):
+ return mark_safe(f'{escape(str(obj))}')
+
+ link.short_description = "Name"
@admin.register(DataBlobGroup)
diff --git a/django/thunderstore/storage/admin/reference.py b/django/thunderstore/storage/admin/reference.py
index 914cde174..88eb73fad 100644
--- a/django/thunderstore/storage/admin/reference.py
+++ b/django/thunderstore/storage/admin/reference.py
@@ -1,3 +1,5 @@
+from html import escape
+
from django.contrib import admin
from django.http import HttpRequest
from django.utils.safestring import mark_safe
@@ -9,7 +11,7 @@
class DataBlobReferenceAdmin(admin.ModelAdmin):
list_display = (
"name",
- "group",
+ "linked_group",
"data_size",
"content_type",
"content_encoding",
@@ -32,7 +34,14 @@ class DataBlobReferenceAdmin(admin.ModelAdmin):
date_hierarchy = "datetime_created"
def file(self, obj: DataBlobReference):
- return mark_safe(f'{obj.blob.data}')
+ return mark_safe(f'{escape(str(obj.blob.data))}')
+
+ def linked_group(self, obj: DataBlobReference):
+ return mark_safe(
+ f'{escape(str(obj.group))}'
+ )
+
+ linked_group.short_description = "group"
def has_add_permission(self, request: HttpRequest) -> bool:
return False
diff --git a/django/thunderstore/storage/models/blob.py b/django/thunderstore/storage/models/blob.py
index 4c4720469..77a9c4740 100644
--- a/django/thunderstore/storage/models/blob.py
+++ b/django/thunderstore/storage/models/blob.py
@@ -5,14 +5,14 @@
from django.core.files.storage import get_storage_class
from django.db import models
-from thunderstore.core.mixins import SafeDeleteMixin
+from thunderstore.core.mixins import AdminLinkMixin, SafeDeleteMixin
def get_object_file_path(_, filename: str) -> str:
return f"blob-storage/sha256/{filename}.sha256.blob"
-class DataBlob(SafeDeleteMixin):
+class DataBlob(SafeDeleteMixin, AdminLinkMixin):
"""
The DataBlob class is responsible for storing arbitrary blobs of data with
automatic deduplication by blob sha256 checksum. It is not interested in
diff --git a/django/thunderstore/storage/models/group.py b/django/thunderstore/storage/models/group.py
index fc0600715..9b3e44d65 100644
--- a/django/thunderstore/storage/models/group.py
+++ b/django/thunderstore/storage/models/group.py
@@ -2,11 +2,11 @@
from django.db import models
-from thunderstore.core.mixins import TimestampMixin
+from thunderstore.core.mixins import AdminLinkMixin, TimestampMixin
from thunderstore.storage.models.reference import DataBlobReference
-class DataBlobGroup(TimestampMixin):
+class DataBlobGroup(TimestampMixin, AdminLinkMixin):
"""
The DataBlobGroup class is intended to support grouping of multiple
data blobs into logical groups, e.g. file trees. It does not hold much
diff --git a/django/thunderstore/storage/models/reference.py b/django/thunderstore/storage/models/reference.py
index 78b766a74..2e8d05061 100644
--- a/django/thunderstore/storage/models/reference.py
+++ b/django/thunderstore/storage/models/reference.py
@@ -2,7 +2,7 @@
from django.db import models
-from thunderstore.core.mixins import TimestampMixin
+from thunderstore.core.mixins import AdminLinkMixin, TimestampMixin
from thunderstore.storage.models.blob import DataBlob
if TYPE_CHECKING:
@@ -19,7 +19,7 @@ def get_queryset(self):
return super().get_queryset().select_related("blob")
-class DataBlobReference(TimestampMixin):
+class DataBlobReference(TimestampMixin, AdminLinkMixin):
"""
Acts as a middle man between DataBlob and the consumers of DataBlob, and
is responsible for storing contextual use-case dependant information (such
diff --git a/django/thunderstore/webhooks/audit.py b/django/thunderstore/webhooks/audit.py
index 0fe8b6d2f..c77667339 100644
--- a/django/thunderstore/webhooks/audit.py
+++ b/django/thunderstore/webhooks/audit.py
@@ -9,6 +9,7 @@
class AuditAction(str, Enum):
PACKAGE_REJECTED = "PACKAGE_REJECTED"
PACKAGE_APPROVED = "PACKAGE_APPROVED"
+ PACKAGE_WARNING = "PACKAGE_WARNING"
class AuditEventField(BaseModel):
diff --git a/django/thunderstore/webhooks/models/audit.py b/django/thunderstore/webhooks/models/audit.py
index bfb2a1b82..569b85d14 100644
--- a/django/thunderstore/webhooks/models/audit.py
+++ b/django/thunderstore/webhooks/models/audit.py
@@ -44,6 +44,8 @@ def get_event_color(action: AuditAction) -> int:
return 5763719
if action == AuditAction.PACKAGE_REJECTED:
return 15548997
+ if action == AuditAction.PACKAGE_WARNING:
+ return 16705372
return 9807270
@staticmethod
diff --git a/django/thunderstore/webhooks/tasks/tests/test_audit.py b/django/thunderstore/webhooks/tasks/tests/test_audit.py
index c383af9a5..8b4eac65e 100644
--- a/django/thunderstore/webhooks/tasks/tests/test_audit.py
+++ b/django/thunderstore/webhooks/tasks/tests/test_audit.py
@@ -15,6 +15,7 @@
(
AuditAction.PACKAGE_APPROVED,
AuditAction.PACKAGE_REJECTED,
+ AuditAction.PACKAGE_WARNING,
),
)
@pytest.mark.parametrize("message", (None, "Test message"))