diff options
Diffstat (limited to 'pydis_site')
-rw-r--r-- | pydis_site/apps/api/admin.py | 278 | ||||
-rw-r--r-- | pydis_site/apps/api/models/bot/deleted_message.py | 4 | ||||
-rw-r--r-- | pydis_site/apps/api/models/bot/message_deletion_context.py | 11 | ||||
-rw-r--r-- | pydis_site/apps/api/models/bot/nomination.py | 5 | ||||
-rw-r--r-- | pydis_site/apps/api/models/bot/role.py | 5 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_deleted_messages.py | 22 | ||||
-rw-r--r-- | pydis_site/apps/staff/tests/test_logs_view.py | 2 |
7 files changed, 311 insertions, 16 deletions
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index dd1291b8..271ff119 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -1,7 +1,11 @@ -from typing import Optional +import json +from typing import Optional, Tuple +from django import urls from django.contrib import admin +from django.db.models.query import QuerySet from django.http import HttpRequest +from django.utils.html import format_html from .models import ( BotSetting, @@ -44,23 +48,271 @@ class LogEntryAdmin(admin.ModelAdmin): """Deny manual LogEntry creation.""" return False - def has_delete_permission( - self, - request: HttpRequest, - obj: Optional[LogEntry] = None - ) -> bool: + def has_delete_permission(self, request: HttpRequest, obj: Optional[LogEntry] = None) -> bool: """Deny LogEntry deletion.""" return False +class DeletedMessageAdmin(admin.ModelAdmin): + """Admin formatting for the DeletedMessage model.""" + + readonly_fields = ( + "id", + "author", + "channel_id", + "content", + "embed_data", + "context", + "view_full_log" + ) + + exclude = ("embeds", "deletion_context") + + search_fields = ( + "id", + "content", + "author__name", + "author__id", + "deletion_context__actor__name", + "deletion_context__actor__id" + ) + + @staticmethod + def embed_data(instance: DeletedMessage) -> Optional[str]: + """Format embed data in a code block for better readability.""" + if instance.embeds: + return format_html( + "<pre style='word-wrap: break-word; white-space: pre-wrap; overflow-x: auto;'>" + "<code>{0}</code></pre>", + json.dumps(instance.embeds, indent=4) + ) + + @staticmethod + def context(instance: DeletedMessage) -> str: + """Provide full context info with a link through to context admin view.""" + link = urls.reverse( + "admin:api_messagedeletioncontext_change", + args=[instance.deletion_context.id] + ) + details = ( + f"Deleted by {instance.deletion_context.actor} at " + f"{instance.deletion_context.creation}" + ) + return format_html("<a href='{0}'>{1}</a>", link, details) + + @staticmethod + def view_full_log(instance: DeletedMessage) -> str: + """Provide a link to the message logs for the relevant context.""" + return format_html( + "<a href='{0}'>Click to view full context log</a>", + instance.deletion_context.log_url + ) + + +class MessageDeletionContextAdmin(admin.ModelAdmin): + """Admin formatting for the MessageDeletionContext model.""" + + readonly_fields = ("actor", "creation", "message_log") + + @staticmethod + def message_log(instance: MessageDeletionContext) -> str: + """Provide a formatted link to the message logs for the context.""" + return format_html( + "<a href='{0}'>Click to see deleted message log</a>", + instance.log_url + ) + + +class InfractionAdmin(admin.ModelAdmin): + """Admin formatting for the Infraction model.""" + + fields = ( + "user", + "actor", + "type", + "reason", + "inserted_at", + "expires_at", + "active", + "hidden" + ) + readonly_fields = ( + "user", + "actor", + "type", + "inserted_at" + ) + list_display = ( + "type", + "user", + "actor", + "inserted_at", + "expires_at", + "reason", + "active", + ) + search_fields = ( + "id", + "user__name", + "user__id", + "actor__name", + "actor__id", + "reason", + "type" + ) + list_filter = ( + "type", + "hidden", + "active" + ) + + +class NominationAdmin(admin.ModelAdmin): + """Admin formatting for the Nomination model.""" + + list_display = ( + "user", + "active", + "reason", + "actor", + "inserted_at", + "ended_at" + ) + fields = ( + "user", + "active", + "actor", + "reason", + "inserted_at", + "ended_at", + "end_reason" + ) + readonly_fields = ( + "user", + "active", + "actor", + "inserted_at", + "ended_at" + ) + search_fields = ( + "actor__name", + "actor__id", + "user__name", + "user__id", + "reason" + ) + list_filter = ("active",) + + +class OffTopicChannelNameAdmin(admin.ModelAdmin): + """Admin formatting for the OffTopicChannelName model.""" + + search_fields = ("name",) + + +class RoleAdmin(admin.ModelAdmin): + """Admin formatting for the Role model.""" + + exclude = ("permissions", "colour") + readonly_fields = ( + "name", + "id", + "colour_with_preview", + "permissions_with_calc_link", + "position" + ) + search_fields = ("name", "id") + + def colour_with_preview(self, instance: Role) -> str: + """Show colour value in both int and hex, in bolded and coloured style.""" + return format_html( + "<span style='color: #{0}!important; font-weight: bold;'>{1} / #{0}</span>", + f"{instance.colour:06x}", + instance.colour + ) + + def permissions_with_calc_link(self, instance: Role) -> str: + """Show permissions with link to API permissions calculator page.""" + return format_html( + "<a href='https://discordapi.com/permissions.html#{0}' target='_blank'>{0}</a>", + instance.permissions + ) + + colour_with_preview.short_description = "Colour" + permissions_with_calc_link.short_description = "Permissions" + + +class TagAdmin(admin.ModelAdmin): + """Admin formatting for the Tag model.""" + + fields = ("title", "embed", "preview") + readonly_fields = ("preview",) + search_fields = ("title", "embed") + + @staticmethod + def preview(instance: Tag) -> Optional[str]: + """Render tag markdown contents to preview actual appearance.""" + if instance.embed: + import markdown + return format_html( + markdown.markdown( + instance.embed["description"], + extensions=[ + "markdown.extensions.nl2br", + "markdown.extensions.extra" + ] + ) + ) + + +class StaffRolesFilter(admin.SimpleListFilter): + """Filter options for Staff Roles.""" + + title = "Staff Role" + parameter_name = "staff_role" + + @staticmethod + def lookups(*_) -> Tuple[Tuple[str, str], ...]: + """Available filter options.""" + return ( + ("Owners", "Owners"), + ("Admins", "Admins"), + ("Moderators", "Moderators"), + ("Core Developers", "Core Developers"), + ("Helpers", "Helpers"), + ) + + def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: + """Returned data filter based on selected option.""" + value = self.value() + if value: + return queryset.filter(roles__name=value) + + +class UserAdmin(admin.ModelAdmin): + """Admin formatting for the User model.""" + + search_fields = ("name", "id", "roles__name", "roles__id") + list_filter = ("in_guild", StaffRolesFilter) + exclude = ("name", "discriminator") + readonly_fields = ( + "__str__", + "id", + "avatar_hash", + "top_role", + "roles", + "in_guild", + ) + + admin.site.register(BotSetting) -admin.site.register(DeletedMessage) +admin.site.register(DeletedMessage, DeletedMessageAdmin) admin.site.register(DocumentationLink) -admin.site.register(Infraction) +admin.site.register(Infraction, InfractionAdmin) admin.site.register(LogEntry, LogEntryAdmin) -admin.site.register(MessageDeletionContext) -admin.site.register(Nomination) +admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin) +admin.site.register(Nomination, NominationAdmin) admin.site.register(OffensiveMessage) -admin.site.register(OffTopicChannelName) -admin.site.register(Role) -admin.site.register(User) +admin.site.register(OffTopicChannelName, OffTopicChannelNameAdmin) +admin.site.register(Role, RoleAdmin) +admin.site.register(User, UserAdmin) diff --git a/pydis_site/apps/api/models/bot/deleted_message.py b/pydis_site/apps/api/models/bot/deleted_message.py index 1eb4516e..50b70d8c 100644 --- a/pydis_site/apps/api/models/bot/deleted_message.py +++ b/pydis_site/apps/api/models/bot/deleted_message.py @@ -14,6 +14,6 @@ class DeletedMessage(Message): ) class Meta: - """Sets the default ordering for list views to oldest first.""" + """Sets the default ordering for list views to newest first.""" - ordering = ["id"] + ordering = ("-id",) diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 04ae8d34..1410250a 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,4 +1,5 @@ from django.db import models +from django_hosts.resolvers import reverse from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.models.mixins import ModelReprMixin @@ -28,3 +29,13 @@ class MessageDeletionContext(ModelReprMixin, models.Model): # the deletion context does not take place in the future. help_text="When this deletion took place." ) + + @property + def log_url(self) -> str: + """Create the url for the deleted message logs.""" + return reverse('logs', host="staff", args=(self.id,)) + + class Meta: + """Set the ordering for list views to newest first.""" + + ordering = ("-creation",) diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index 183b22d5..54f56c98 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -46,3 +46,8 @@ class Nomination(ModelReprMixin, models.Model): """Representation that makes the target and state of the nomination immediately evident.""" status = "active" if self.active else "ended" return f"Nomination of {self.user} ({status})" + + class Meta: + """Set the ordering of nominations to most recent first.""" + + ordering = ("-inserted_at",) diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py index 721e4815..b23fc5f4 100644 --- a/pydis_site/apps/api/models/bot/role.py +++ b/pydis_site/apps/api/models/bot/role.py @@ -65,3 +65,8 @@ class Role(ModelReprMixin, models.Model): def __le__(self, other: Role) -> bool: """Compares the roles based on their position in the role hierarchy of the guild.""" return self.position <= other.position + + class Meta: + """Set role ordering from highest to lowest position.""" + + ordering = ("-position",) diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index f079a8dd..287c1737 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -1,5 +1,6 @@ from datetime import datetime +from django.utils import timezone from django_hosts.resolvers import reverse from .base import APISubdomainTestCase @@ -76,3 +77,24 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) [context] = MessageDeletionContext.objects.all() self.assertEqual(context.actor.id, self.actor.id) + + +class DeletedMessagesLogURLTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): # noqa + cls.author = cls.actor = User.objects.create( + id=324888, + name='Black Knight', + discriminator=1975, + avatar_hash=None + ) + + cls.deletion_context = MessageDeletionContext.objects.create( + actor=cls.actor, + creation=timezone.now() + ) + + def test_valid_log_url(self): + expected_url = reverse('logs', host="staff", args=(1,)) + [context] = MessageDeletionContext.objects.all() + self.assertEqual(context.log_url, expected_url) diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py index 17910bb6..00e0ab2f 100644 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ b/pydis_site/apps/staff/tests/test_logs_view.py @@ -133,7 +133,7 @@ class TestLogsView(TestCase): response = self.client.get(url) self.assertIn("messages", response.context) self.assertListEqual( - [self.deleted_message_one, self.deleted_message_two], + [self.deleted_message_two, self.deleted_message_one], list(response.context["deletion_context"].deletedmessage_set.all()) ) |