aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Joe Banks <[email protected]>2020-09-20 01:02:06 +0100
committerGravatar GitHub <[email protected]>2020-09-20 01:02:06 +0100
commit5e8b55146a13b99453595337f33c06d96767b0d5 (patch)
treec82797873c048b91b848e73f3d46a980e9a75372
parentMerge pull request #390 from python-discord/allow_blank_or_null_for_nominatio... (diff)
parentRemove delete permission for bot settings admin. (diff)
Merge pull request #289 from python-discord/admin-api-pages-improvements
Improvements to Admin pages for the API Section
-rwxr-xr-xmanage.py14
-rw-r--r--pydis_site/apps/api/admin.py431
-rw-r--r--pydis_site/apps/api/migrations/0064_auto_20200919_1900.py76
-rw-r--r--pydis_site/apps/api/migrations/0065_auto_20200919_2033.py17
-rw-r--r--pydis_site/apps/api/models/bot/deleted_message.py4
-rw-r--r--pydis_site/apps/api/models/bot/documentation_link.py5
-rw-r--r--pydis_site/apps/api/models/bot/message.py6
-rw-r--r--pydis_site/apps/api/models/bot/message_deletion_context.py11
-rw-r--r--pydis_site/apps/api/models/bot/nomination.py8
-rw-r--r--pydis_site/apps/api/models/bot/offensive_message.py9
-rw-r--r--pydis_site/apps/api/models/bot/role.py8
-rw-r--r--pydis_site/apps/api/models/bot/user.py17
-rw-r--r--pydis_site/apps/api/tests/test_deleted_messages.py21
-rw-r--r--pydis_site/apps/api/tests/test_users.py6
-rw-r--r--pydis_site/apps/staff/tests/test_logs_view.py2
15 files changed, 593 insertions, 42 deletions
diff --git a/manage.py b/manage.py
index ee071376..d4748a3a 100755
--- a/manage.py
+++ b/manage.py
@@ -112,6 +112,19 @@ class SiteManager:
print("Database could not be found, exiting.")
sys.exit(1)
+ @staticmethod
+ def set_dev_site_name() -> None:
+ """Set the development site domain in admin from default example."""
+ # import Site model now after django setup
+ from django.contrib.sites.models import Site
+ query = Site.objects.filter(id=1)
+ site = query.get()
+ if site.domain == "example.com":
+ query.update(
+ domain="pythondiscord.local:8000",
+ name="pythondiscord.local:8000"
+ )
+
def prepare_server(self) -> None:
"""Perform preparation tasks before running the server."""
django.setup()
@@ -125,6 +138,7 @@ class SiteManager:
call_command("collectstatic", interactive=False, clear=True, verbosity=self.verbosity)
if self.debug:
+ self.set_dev_site_name()
self.create_superuser()
def run_server(self) -> None:
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index dd1291b8..5093e605 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -1,7 +1,13 @@
-from typing import Optional
+from __future__ import annotations
+import json
+from typing import Iterable, Optional, Tuple
+
+from django import urls
from django.contrib import admin
+from django.db.models import QuerySet
from django.http import HttpRequest
+from django.utils.html import SafeString, format_html
from .models import (
BotSetting,
@@ -17,50 +23,419 @@ from .models import (
User
)
+admin.site.site_header = "Python Discord | Administration"
+admin.site.site_title = "Python Discord"
+
+
[email protected](BotSetting)
+class BotSettingAdmin(admin.ModelAdmin):
+ """Admin formatting for the BotSetting model."""
+
+ fields = ("name", "data")
+ list_display = ("name",)
+ readonly_fields = ("name",)
+
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
+
+ def has_delete_permission(self, *args) -> bool:
+ """Prevent deleting from django admin."""
+ return False
+
+
[email protected](DocumentationLink)
+class DocumentationLinkAdmin(admin.ModelAdmin):
+ """Admin formatting for the DocumentationLink model."""
+
+ fields = ("package", "base_url", "inventory_url")
+ list_display = ("package", "base_url", "inventory_url")
+ list_editable = ("base_url", "inventory_url")
+ search_fields = ("package",)
+
+
+class InfractionActorFilter(admin.SimpleListFilter):
+ """Actor Filter for Infraction Admin list page."""
+
+ title = "Actor"
+ parameter_name = "actor"
+
+ def lookups(self, request: HttpRequest, model: NominationAdmin) -> Iterable[Tuple[int, str]]:
+ """Selectable values for viewer to filter by."""
+ actor_ids = Infraction.objects.order_by().values_list("actor").distinct()
+ actors = User.objects.filter(id__in=actor_ids)
+ return ((a.id, a.username) for a in actors)
+
+ def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]:
+ """Query to filter the list of Users against."""
+ if not self.value():
+ return
+ return queryset.filter(actor__id=self.value())
+
+
[email protected](Infraction)
+class InfractionAdmin(admin.ModelAdmin):
+ """Admin formatting for the Infraction model."""
+
+ fieldsets = (
+ ("Members", {"fields": ("user", "actor")}),
+ ("Action", {"fields": ("type", "hidden", "active")}),
+ ("Dates", {"fields": ("inserted_at", "expires_at")}),
+ ("Reason", {"fields": ("reason",)}),
+ )
+ readonly_fields = (
+ "user",
+ "actor",
+ "type",
+ "inserted_at",
+ "expires_at",
+ "active",
+ "hidden"
+ )
+ list_display = (
+ "type",
+ "active",
+ "user",
+ "inserted_at",
+ "reason",
+ )
+ search_fields = (
+ "id",
+ "user__name",
+ "user__id",
+ "actor__name",
+ "actor__id",
+ "reason",
+ "type"
+ )
+ list_filter = (
+ "type",
+ "hidden",
+ "active",
+ InfractionActorFilter
+ )
+
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
+
class LogEntryAdmin(admin.ModelAdmin):
"""Allows viewing logs in the Django Admin without allowing edits."""
actions = None
- list_display = ('timestamp', 'application', 'level', 'message')
+ list_display = ('timestamp', 'level', 'message')
fieldsets = (
('Overview', {'fields': ('timestamp', 'application', 'logger_name')}),
('Metadata', {'fields': ('level', 'module', 'line')}),
('Contents', {'fields': ('message',)})
)
- list_filter = ('application', 'level', 'timestamp')
+ list_filter = ('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:
+ def has_change_permission(self, *args) -> bool:
+ """Prevent editing from django admin."""
+ return False
+
+ def has_delete_permission(self, request: HttpRequest, obj: Optional[LogEntry] = None) -> bool:
"""Deny LogEntry deletion."""
return False
-admin.site.register(BotSetting)
-admin.site.register(DeletedMessage)
-admin.site.register(DocumentationLink)
-admin.site.register(Infraction)
-admin.site.register(LogEntry, LogEntryAdmin)
-admin.site.register(MessageDeletionContext)
-admin.site.register(Nomination)
-admin.site.register(OffensiveMessage)
-admin.site.register(OffTopicChannelName)
-admin.site.register(Role)
-admin.site.register(User)
[email protected](DeletedMessage)
+class DeletedMessageAdmin(admin.ModelAdmin):
+ """Admin formatting for the DeletedMessage model."""
+
+ 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"
+ )
+
+ list_display = ("id", "author", "channel_id")
+
+ def embed_data(self, message: DeletedMessage) -> Optional[str]:
+ """Format embed data in a code block for better readability."""
+ if message.embeds:
+ return format_html(
+ "<pre style='word-wrap: break-word; white-space: pre-wrap; overflow-x: auto;'>"
+ "<code>{0}</code></pre>",
+ json.dumps(message.embeds, indent=4)
+ )
+
+ embed_data.short_description = "Embeds"
+
+ @staticmethod
+ def context(message: DeletedMessage) -> str:
+ """Provide full context info with a link through to context admin view."""
+ link = urls.reverse(
+ "admin:api_messagedeletioncontext_change",
+ args=[message.deletion_context.id]
+ )
+ details = (
+ f"Deleted by {message.deletion_context.actor} at "
+ f"{message.deletion_context.creation}"
+ )
+ return format_html("<a href='{0}'>{1}</a>", link, details)
+
+ @staticmethod
+ def view_full_log(message: 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>",
+ message.deletion_context.log_url
+ )
+
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
+
+ def has_change_permission(self, *args) -> bool:
+ """Prevent editing from django admin."""
+ return False
+
+
+class DeletedMessageInline(admin.TabularInline):
+ """Tabular Inline Admin model for Deleted Message to be viewed within Context."""
+
+ model = DeletedMessage
+
+
[email protected](MessageDeletionContext)
+class MessageDeletionContextAdmin(admin.ModelAdmin):
+ """Admin formatting for the MessageDeletionContext model."""
+
+ fields = ("actor", "creation")
+ list_display = ("id", "creation", "actor")
+ inlines = (DeletedMessageInline,)
+
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
+
+ def has_change_permission(self, *args) -> bool:
+ """Prevent editing from django admin."""
+ return False
+
+
+class NominationActorFilter(admin.SimpleListFilter):
+ """Actor Filter for Nomination Admin list page."""
+
+ title = "Actor"
+ parameter_name = "actor"
+
+ def lookups(self, request: HttpRequest, model: NominationAdmin) -> Iterable[Tuple[int, str]]:
+ """Selectable values for viewer to filter by."""
+ actor_ids = Nomination.objects.order_by().values_list("actor").distinct()
+ actors = User.objects.filter(id__in=actor_ids)
+ return ((a.id, a.username) for a in actors)
+
+ def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]:
+ """Query to filter the list of Users against."""
+ if not self.value():
+ return
+ return queryset.filter(actor__id=self.value())
+
+
[email protected](Nomination)
+class NominationAdmin(admin.ModelAdmin):
+ """Admin formatting for the Nomination model."""
+
+ search_fields = (
+ "user__name",
+ "user__id",
+ "actor__name",
+ "actor__id",
+ "reason",
+ "end_reason"
+ )
+
+ list_filter = ("active", NominationActorFilter)
+
+ list_display = (
+ "user",
+ "active",
+ "reason",
+ "actor",
+ )
+
+ fields = (
+ "user",
+ "active",
+ "actor",
+ "reason",
+ "inserted_at",
+ "ended_at",
+ "end_reason"
+ )
+
+ # only allow reason fields to be edited.
+ readonly_fields = (
+ "user",
+ "active",
+ "actor",
+ "inserted_at",
+ "ended_at"
+ )
+
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
+
+
[email protected](OffTopicChannelName)
+class OffTopicChannelNameAdmin(admin.ModelAdmin):
+ """Admin formatting for the OffTopicChannelName model."""
+
+ search_fields = ("name",)
+ list_filter = ("used",)
+
+
[email protected](OffensiveMessage)
+class OffensiveMessageAdmin(admin.ModelAdmin):
+ """Admin formatting for the OffensiveMessage model."""
+
+ def message_jumplink(self, message: OffensiveMessage) -> SafeString:
+ """Message ID hyperlinked to the direct discord jumplink."""
+ return format_html(
+ '<a href="https://canary.discordapp.com/channels/267624335836053506/{0}/{1}">{1}</a>',
+ message.channel_id,
+ message.id
+ )
+
+ message_jumplink.short_description = "Message ID"
+
+ search_fields = ("id", "channel_id")
+ list_display = ("id", "channel_id", "delete_date")
+ fields = ("message_jumplink", "channel_id", "delete_date")
+ readonly_fields = ("message_jumplink", "channel_id")
+
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
+
+
+class RoleAdmin(admin.ModelAdmin):
+ """Admin formatting for the Role model."""
+
+ def coloured_name(self, role: Role) -> SafeString:
+ """Role name with html style colouring."""
+ return format_html(
+ '<span style="color: {0}!important; font-weight: bold;">{1}</span>',
+ f"#{role.colour:06X}",
+ role.name
+ )
+
+ coloured_name.short_description = "Name"
+
+ def colour_with_preview(self, role: Role) -> SafeString:
+ """Show colour value in both int and hex, in bolded and coloured style."""
+ return format_html(
+ "<span style='color: {0}; font-weight: bold;'>{0} ({1})</span>",
+ f"#{role.colour:06x}",
+ role.colour
+ )
+
+ colour_with_preview.short_description = "Colour"
+
+ def permissions_with_calc_link(self, role: Role) -> SafeString:
+ """Show permissions with link to API permissions calculator page."""
+ return format_html(
+ "<a href='https://discordapi.com/permissions.html#{0}' target='_blank'>{0}</a>",
+ role.permissions
+ )
+
+ permissions_with_calc_link.short_description = "Permissions"
+
+ search_fields = ("name", "id")
+ list_display = ("coloured_name",)
+ fields = ("id", "name", "colour_with_preview", "permissions_with_calc_link", "position")
+
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
+
+ def has_change_permission(self, *args) -> bool:
+ """Prevent editing from django admin."""
+ return False
+
+
+class UserRoleFilter(admin.SimpleListFilter):
+ """List Filter for User list Admin page."""
+
+ title = "Role"
+ parameter_name = "role"
+
+ def lookups(self, request: HttpRequest, model: UserAdmin) -> Iterable[Tuple[str, str]]:
+ """Selectable values for viewer to filter by."""
+ roles = Role.objects.all()
+ return ((r.name, r.name) for r in roles)
+
+ def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]:
+ """Query to filter the list of Users against."""
+ if not self.value():
+ return
+ role = Role.objects.get(name=self.value())
+ return queryset.filter(roles__contains=[role.id])
+
+
+class UserAdmin(admin.ModelAdmin):
+ """Admin formatting for the User model."""
+
+ def top_role_coloured(self, user: User) -> SafeString:
+ """Returns the top role of the user with html style matching role colour."""
+ return format_html(
+ '<span style="color: {0}; font-weight: bold;">{1}</span>',
+ f"#{user.top_role.colour:06X}",
+ user.top_role.name
+ )
+
+ top_role_coloured.short_description = "Top Role"
+
+ def all_roles_coloured(self, user: User) -> SafeString:
+ """Returns all user roles with html style matching role colours."""
+ roles = Role.objects.filter(id__in=user.roles)
+ return format_html(
+ "</br>".join(
+ f'<span style="color: #{r.colour:06X}; font-weight: bold;">{r.name}</span>'
+ for r in roles
+ )
+ )
+
+ all_roles_coloured.short_description = "All Roles"
+
+ search_fields = ("name", "id", "roles")
+ list_filter = (UserRoleFilter, "in_guild")
+ list_display = ("username", "top_role_coloured", "in_guild")
+ fields = ("username", "id", "in_guild", "all_roles_coloured")
+ sortable_by = ("username",)
+
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
+
+ def has_change_permission(self, *args) -> bool:
+ """Prevent editing from django admin."""
+ return False
diff --git a/pydis_site/apps/api/migrations/0064_auto_20200919_1900.py b/pydis_site/apps/api/migrations/0064_auto_20200919_1900.py
new file mode 100644
index 00000000..0080eb42
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0064_auto_20200919_1900.py
@@ -0,0 +1,76 @@
+# Generated by Django 3.0.9 on 2020-09-19 19:00
+
+import django.core.validators
+from django.db import migrations, models
+import pydis_site.apps.api.models.bot.offensive_message
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0063_Allow_blank_or_null_for_nomination_reason'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='deletedmessage',
+ options={'ordering': ('-id',)},
+ ),
+ migrations.AlterModelOptions(
+ name='messagedeletioncontext',
+ options={'ordering': ('-creation',)},
+ ),
+ migrations.AlterModelOptions(
+ name='nomination',
+ options={'ordering': ('-inserted_at',)},
+ ),
+ migrations.AlterModelOptions(
+ name='role',
+ options={'ordering': ('-position',)},
+ ),
+ migrations.AlterField(
+ model_name='deletedmessage',
+ name='channel_id',
+ field=models.BigIntegerField(help_text='The channel ID that this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')], verbose_name='Channel ID'),
+ ),
+ migrations.AlterField(
+ model_name='deletedmessage',
+ name='id',
+ field=models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')], verbose_name='ID'),
+ ),
+ migrations.AlterField(
+ model_name='nomination',
+ name='end_reason',
+ field=models.TextField(blank=True, default='', help_text='Why the nomination was ended.'),
+ ),
+ migrations.AlterField(
+ model_name='offensivemessage',
+ name='channel_id',
+ field=models.BigIntegerField(help_text='The channel ID that the message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')], verbose_name='Channel ID'),
+ ),
+ migrations.AlterField(
+ model_name='offensivemessage',
+ name='delete_date',
+ field=models.DateTimeField(help_text='The date on which the message will be auto-deleted.', validators=[pydis_site.apps.api.models.bot.offensive_message.future_date_validator], verbose_name='To Be Deleted'),
+ ),
+ migrations.AlterField(
+ model_name='offensivemessage',
+ name='id',
+ field=models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')], verbose_name='Message ID'),
+ ),
+ migrations.AlterField(
+ model_name='role',
+ name='id',
+ field=models.BigIntegerField(help_text='The role ID, taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')], verbose_name='ID'),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='id',
+ field=models.BigIntegerField(help_text='The ID of this user, taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='User IDs cannot be negative.')], verbose_name='ID'),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='in_guild',
+ field=models.BooleanField(default=True, help_text='Whether this user is in our server.', verbose_name='In Guild'),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0065_auto_20200919_2033.py b/pydis_site/apps/api/migrations/0065_auto_20200919_2033.py
new file mode 100644
index 00000000..89bc4e02
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0065_auto_20200919_2033.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.0.9 on 2020-09-19 20:33
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0064_auto_20200919_1900'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='documentationlink',
+ options={'ordering': ['package']},
+ ),
+ ]
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/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py
index 5a46460b..2a0ce751 100644
--- a/pydis_site/apps/api/models/bot/documentation_link.py
+++ b/pydis_site/apps/api/models/bot/documentation_link.py
@@ -24,3 +24,8 @@ class DocumentationLink(ModelReprMixin, models.Model):
def __str__(self):
"""Returns the package and URL for the current documentation link, for display purposes."""
return f"{self.package} - {self.base_url}"
+
+ class Meta:
+ """Defines the meta options for the documentation link model."""
+
+ ordering = ['package']
diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py
index f6ae55a5..ff06de21 100644
--- a/pydis_site/apps/api/models/bot/message.py
+++ b/pydis_site/apps/api/models/bot/message.py
@@ -21,7 +21,8 @@ class Message(ModelReprMixin, models.Model):
limit_value=0,
message="Message IDs cannot be negative."
),
- )
+ ),
+ verbose_name="ID"
)
author = models.ForeignKey(
User,
@@ -38,7 +39,8 @@ class Message(ModelReprMixin, models.Model):
limit_value=0,
message="Channel IDs cannot be negative."
),
- )
+ ),
+ verbose_name="Channel ID"
)
content = models.CharField(
max_length=2_000,
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..11b9e36e 100644
--- a/pydis_site/apps/api/models/bot/nomination.py
+++ b/pydis_site/apps/api/models/bot/nomination.py
@@ -34,7 +34,8 @@ class Nomination(ModelReprMixin, models.Model):
)
end_reason = models.TextField(
help_text="Why the nomination was ended.",
- default=""
+ default="",
+ blank=True
)
ended_at = models.DateTimeField(
auto_now_add=False,
@@ -46,3 +47,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/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py
index 6c0e5ffb..74dab59b 100644
--- a/pydis_site/apps/api/models/bot/offensive_message.py
+++ b/pydis_site/apps/api/models/bot/offensive_message.py
@@ -24,7 +24,8 @@ class OffensiveMessage(ModelReprMixin, models.Model):
limit_value=0,
message="Message IDs cannot be negative."
),
- )
+ ),
+ verbose_name="Message ID"
)
channel_id = models.BigIntegerField(
help_text=(
@@ -36,11 +37,13 @@ class OffensiveMessage(ModelReprMixin, models.Model):
limit_value=0,
message="Channel IDs cannot be negative."
),
- )
+ ),
+ verbose_name="Channel ID"
)
delete_date = models.DateTimeField(
help_text="The date on which the message will be auto-deleted.",
- validators=(future_date_validator,)
+ validators=(future_date_validator,),
+ verbose_name="To Be Deleted"
)
def __str__(self):
diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py
index 721e4815..cfadfec4 100644
--- a/pydis_site/apps/api/models/bot/role.py
+++ b/pydis_site/apps/api/models/bot/role.py
@@ -22,7 +22,8 @@ class Role(ModelReprMixin, models.Model):
message="Role IDs cannot be negative."
),
),
- help_text="The role ID, taken from Discord."
+ help_text="The role ID, taken from Discord.",
+ verbose_name="ID"
)
name = models.CharField(
max_length=100,
@@ -65,3 +66,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/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py
index cd2d58b9..afc5ba1e 100644
--- a/pydis_site/apps/api/models/bot/user.py
+++ b/pydis_site/apps/api/models/bot/user.py
@@ -26,11 +26,12 @@ class User(ModelReprMixin, models.Model):
message="User IDs cannot be negative."
),
),
+ verbose_name="ID",
help_text="The ID of this user, taken from Discord."
)
name = models.CharField(
max_length=32,
- help_text="The username, taken from Discord."
+ help_text="The username, taken from Discord.",
)
discriminator = models.PositiveSmallIntegerField(
validators=(
@@ -57,12 +58,13 @@ class User(ModelReprMixin, models.Model):
)
in_guild = models.BooleanField(
default=True,
- help_text="Whether this user is in our server."
+ help_text="Whether this user is in our server.",
+ verbose_name="In Guild"
)
def __str__(self):
"""Returns the name and discriminator for the current user, for display purposes."""
- return f"{self.name}#{self.discriminator:0>4}"
+ return f"{self.name}#{self.discriminator:04d}"
@property
def top_role(self) -> Role:
@@ -75,3 +77,12 @@ class User(ModelReprMixin, models.Model):
if not roles:
return Role.objects.get(name="Developers")
return max(roles)
+
+ @property
+ def username(self) -> str:
+ """
+ Returns the display version with name and discriminator as a standard attribute.
+
+ For usability in read-only fields such as Django Admin.
+ """
+ return str(self)
diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py
index f079a8dd..40450844 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,23 @@ 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):
+ cls.author = cls.actor = User.objects.create(
+ id=324888,
+ name='Black Knight',
+ discriminator=1975,
+ )
+
+ 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/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 4c0f6e27..a02fce8a 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -143,7 +143,7 @@ class UserModelTests(APISubdomainTestCase):
cls.user_with_roles = User.objects.create(
id=1,
name="Test User with two roles",
- discriminator=1111,
+ discriminator=1,
in_guild=True,
)
cls.user_with_roles.roles.extend([cls.role_bottom.id, cls.role_top.id])
@@ -166,3 +166,7 @@ class UserModelTests(APISubdomainTestCase):
top_role = self.user_without_roles.top_role
self.assertIsInstance(top_role, Role)
self.assertEqual(top_role.id, self.developers_role.id)
+
+ def test_correct_username_formatting(self):
+ """Tests the username property with both name and discriminator formatted together."""
+ self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001")
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())
)