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, DeletedMessage, DocumentationLink, Infraction, LogEntry, MessageDeletionContext, Nomination, OffTopicChannelName, Role, Tag, User ) class LogEntryAdmin(admin.ModelAdmin): """Allows viewing logs in the Django Admin without allowing edits.""" actions = None list_display = ('timestamp', 'application', 'level', 'message') fieldsets = ( ('Overview', {'fields': ('timestamp', 'application', 'logger_name')}), ('Metadata', {'fields': ('level', 'module', 'line')}), ('Contents', {'fields': ('message',)}) ) list_filter = ('application', 'level', 'timestamp') search_fields = ('message',) readonly_fields = ( 'application', 'logger_name', 'timestamp', 'level', 'module', 'line', 'message' ) def has_add_permission(self, request: HttpRequest) -> bool: """Deny manual LogEntry creation.""" return False 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( "
"
                "{0}
", 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("{1}", link, details) @staticmethod def view_full_log(instance: DeletedMessage) -> str: """Provide a link to the message logs for the relevant context.""" return format_html( "Click to view full context log", 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( "Click to see deleted message log", 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( "{1} / #{0}", 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( "{0}", 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, DeletedMessageAdmin) admin.site.register(DocumentationLink) admin.site.register(Infraction, InfractionAdmin) admin.site.register(LogEntry, LogEntryAdmin) admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin) admin.site.register(Nomination, NominationAdmin) admin.site.register(OffTopicChannelName, OffTopicChannelNameAdmin) admin.site.register(Role, RoleAdmin) admin.site.register(Tag, TagAdmin) admin.site.register(User, UserAdmin)