aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site
diff options
context:
space:
mode:
Diffstat (limited to 'pydis_site')
-rw-r--r--pydis_site/apps/api/admin.py278
-rw-r--r--pydis_site/apps/api/models/bot/deleted_message.py4
-rw-r--r--pydis_site/apps/api/models/bot/message_deletion_context.py11
-rw-r--r--pydis_site/apps/api/models/bot/nomination.py5
-rw-r--r--pydis_site/apps/api/models/bot/role.py5
-rw-r--r--pydis_site/apps/api/tests/test_deleted_messages.py22
-rw-r--r--pydis_site/apps/staff/tests/test_logs_view.py2
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())
)